@wgslx/wgslx
Version:
Extended WebGPU shading language tools
299 lines (248 loc) • 7.58 kB
text/typescript
// Template discovery
// https://www.w3.org/TR/WGSL/#template-lists-sec
//
// In our implementation of the parser, we preprocess code to deliminate template brackets from
// greater than and less than characters. We replace them with double angle quotation marks
// U+00AB «, and U+00BB ». As this is done per file, templates opening and closing can not cross
// file boundaries.
import {
LINE_BREAK_REGEX,
matchBlankspace,
matchBlockComment,
matchIdentPatternToken,
matchLineEndingComment,
matchLiteral,
} from './patterns';
import {TEMPLATE_END, TEMPLATE_START} from './util';
interface UnclosedCandidate {
position: number;
depth: number;
}
interface TemplateList {
startPosition: number;
endPosition: number;
}
export function preprocess(text: string) {
text = stripComments(text);
const templateLists = discoverTemplates(text);
text = text.replaceAll(/[<>]/g, (character, offset) => {
switch (character) {
case '<':
return templateLists.some((t) => t.startPosition === offset)
? TEMPLATE_START
: '<';
case '>':
return templateLists.some((t) => t.endPosition === offset)
? TEMPLATE_END
: '>';
}
throw new Error('Unidentified character.');
});
return text;
}
function firstIndexOf(
text: string,
position: number,
...values: string[]
): [number, string] {
let minIndex = Number.MAX_SAFE_INTEGER;
let minChar = '';
for (let value of values) {
const index = text.indexOf(value, position);
if (index !== -1 && index < minIndex) {
minIndex = index;
minChar = value;
}
}
if (minIndex === Number.MAX_SAFE_INTEGER) {
return [-1, ''];
}
return [minIndex, minChar];
}
export function stripComments(text: string) {
const lines = text.split(LINE_BREAK_REGEX);
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
const [firstCommentIndex, firstCommentType] = firstIndexOf(
lines[lineIndex],
0,
'//',
'/*',
'*/'
);
if (firstCommentIndex === -1) {
lines[lineIndex] = lines[lineIndex].trimEnd();
continue;
}
if (firstCommentType === '*/') {
throw new Error(`Line ${lineIndex + 1}: Unexpected block comment close.`);
}
if (firstCommentType === '//') {
// Trim the end as no tokens will reference it anymore.
lines[lineIndex] = lines[lineIndex].substring(0, firstCommentIndex);
continue;
}
let depth = 1;
let startLine = lineIndex;
let column = firstCommentIndex + 2;
while (depth !== 0) {
const [nextBlockIndex, nextBlockType] = firstIndexOf(
lines[lineIndex],
column,
'/*',
'*/'
);
if (nextBlockIndex === -1) {
if (lineIndex === lines.length - 1) {
throw new Error(`Line ${startLine}: Unclosed block comment.`);
}
// Try the next line.
lineIndex += 1;
column = 0;
continue;
}
if (nextBlockType === '/*') {
depth += 1;
column = nextBlockIndex + 2;
continue;
}
if (nextBlockType === '*/') {
depth -= 1;
column = nextBlockIndex + 2;
continue;
}
throw new Error('Unreachable code.');
}
if (startLine == lineIndex) {
// Same line block comment, replace with whitespace since it can affect token positioning.
lines[lineIndex] =
lines[lineIndex].substring(0, firstCommentIndex) +
' '.repeat(column - firstCommentIndex) +
lines[lineIndex].substring(column);
lineIndex -= 1; // Revisit the line for any other comments.
continue;
}
lines[startLine] = lines[startLine].substring(0, firstCommentIndex);
for (let j = startLine + 1; j < lineIndex; j++) {
lines[j] = '';
}
lines[lineIndex] = ' '.repeat(column) + lines[lineIndex].substring(column);
lineIndex -= 1; // Revisit the line for any other comments.
}
return lines.join('\n');
}
export function discoverTemplates(text: string) {
const discoveredTemplateLists: TemplateList[] = [];
const pendingCandidatesStack: UnclosedCandidate[] = [];
let currentPosition = 0;
let nestingDepth = 0;
function matchAdvance(
...matchers: Array<(text: string, position: number) => string | undefined>
) {
const startPosition = currentPosition;
let matched = false;
rematch: do {
for (let matcher of matchers) {
const match = matcher(text, currentPosition);
if (match) {
currentPosition += match.length;
matched = true;
continue rematch;
}
}
matched = false;
} while (matched === true && matchers.length > 1);
return startPosition === currentPosition
? undefined
: text.substring(startPosition, currentPosition);
}
function startsWithAdvance(...consts: string[]) {
const rest = text.substring(currentPosition);
for (const str of consts) {
if (rest.startsWith(str)) {
currentPosition += str.length;
return true;
}
}
return false;
}
// Advance CurrentPosition past blankspace, comments, and literals.
while (currentPosition < text.length) {
// Advance currentPosition past blankspace.
matchAdvance(
matchBlankspace,
matchLiteral,
matchLineEndingComment,
matchBlockComment
);
if (matchAdvance(matchIdentPatternToken)) {
// Advance CurrentPosition past blankspace and comments, if present.
matchAdvance(matchBlankspace, matchLineEndingComment, matchBlockComment);
if (startsWithAdvance('<')) {
pendingCandidatesStack.push({
position: currentPosition - 1,
depth: nestingDepth,
});
if (startsWithAdvance('<') || startsWithAdvance('=')) {
// From assumption 1, no template parameter starts with '<' (U+003C), so the
// previous code point cannot be the start of a template list. Therefore the
// current and previous code point must be '<<' operator.
pendingCandidatesStack.pop();
}
}
continue;
}
if (startsWithAdvance('>')) {
if (
pendingCandidatesStack.length > 0 &&
pendingCandidatesStack[pendingCandidatesStack.length - 1].depth ===
nestingDepth
) {
const pending = pendingCandidatesStack.pop()!; // Asserted due to array length check.
discoveredTemplateLists.push({
startPosition: pending.position,
endPosition: currentPosition - 1,
});
} else {
startsWithAdvance('=');
}
continue;
}
if (startsWithAdvance('(', '[')) {
nestingDepth++;
continue;
}
if (startsWithAdvance(')', ']')) {
nestingDepth--;
continue;
}
if (startsWithAdvance('!')) {
startsWithAdvance('=');
continue;
}
if (startsWithAdvance('=')) {
if (startsWithAdvance('=')) {
continue;
}
nestingDepth = 0;
pendingCandidatesStack.length = 0;
continue;
}
if (startsWithAdvance(';', '{', ':')) {
nestingDepth = 0;
pendingCandidatesStack.length = 0;
continue;
}
if (startsWithAdvance('&&', '||')) {
while (
pendingCandidatesStack.length &&
pendingCandidatesStack[pendingCandidatesStack.length - 1].depth >=
nestingDepth
) {
pendingCandidatesStack.pop();
}
continue;
}
currentPosition += 1;
}
return discoveredTemplateLists;
}