Skip to content

Commit 711acca

Browse files
committed
wasm: Fix bug with fallthrough in switch for lifted terminals
1 parent ad3c3cf commit 711acca

4 files changed

Lines changed: 127 additions & 52 deletions

File tree

packages/wasm/scripts/parseLiquid.js

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,35 +50,33 @@ const matchWithInput = (m, str) => (m.setInput(str), m.match());
5050
const jsTimes = [];
5151
const wasmTimes = [];
5252

53-
const m = await wasmMatcherForGrammar(liquid.LiquidHTML);
5453
for (const path of fg.sync(pattern)) {
5554
const input = readFileSync(path, 'utf8');
56-
if (
57-
input.length > 64 * 1024 ||
58-
path.includes('swatch') ||
59-
path.includes('password') ||
60-
path.includes('theme.liquid') ||
61-
path.includes('gift_card.liquid')
62-
) {
63-
console.log(`skipping ${path}`);
55+
// Wasm matcher currently has a limit of 64kB input size.
56+
if (input.length > 64 * 1024) {
57+
console.log(`skipping ${path} (too big)`);
6458
continue;
6559
}
66-
parsedPaths.add(path);
6760
const start = performance.now();
68-
matchWithInput(m, input);
69-
wasmTimes.push(performance.now() - start);
70-
assert.equal(input.length, unparseW(input, m.getCstRoot()).length);
61+
const r = liquid.LiquidHTML.match(input);
62+
const elapsed = performance.now() - start;
63+
if (!r.succeeded()) {
64+
console.error(`Failed to parse ${path}: ${r.message}`);
65+
continue;
66+
}
67+
parsedPaths.add(path);
68+
jsTimes.push(elapsed);
69+
assert.equal(r.succeeded(), true, `failed: ${path}`);
7170
}
7271

72+
const m = await wasmMatcherForGrammar(liquid.LiquidHTML);
7373
for (const path of fg.sync(pattern)) {
74+
if (!parsedPaths.has(path)) continue;
7475
const input = readFileSync(path, 'utf8');
75-
if (!parsedPaths.has(path)) {
76-
continue;
77-
}
7876
const start = performance.now();
79-
const r = liquid.LiquidHTML.match(input);
80-
jsTimes.push(performance.now() - start);
81-
assert.equal(r.succeeded(), true);
77+
assert.equal(matchWithInput(m, input), 1, `failed: ${path}`);
78+
wasmTimes.push(performance.now() - start);
79+
assert.equal(input.length, unparseW(input, m.getCstRoot()).length);
8280
}
8381

8482
const sum = arr => arr.reduce((a, b) => a + b, 0);

packages/wasm/src/index.js

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ const FAST_SAVE_BINDINGS = true;
1414
const FAST_RESTORE_BINDINGS = true;
1515
const IMPLICIT_SPACE_SKIPPING = false;
1616

17+
// When specializing rules, should we emit a generalized version that
18+
// handles the specific cases? If false, code size will be larger.
19+
// This doesn't seem to make a big performance difference either way.
20+
const EMIT_GENERALIZED_RULES = true;
21+
1722
const {instr} = w;
1823

1924
const isNonNull = x => x != null;
@@ -99,11 +104,6 @@ class IndexedSet {
99104
}
100105
}
101106

102-
function getDebugLabel(exp) {
103-
const loc = exp.source ? exp.source.startIdx : -1;
104-
return `${JSON.stringify(exp)}@${loc}`;
105-
}
106-
107107
function collectParams(exp, seen = new Set()) {
108108
switch (exp.constructor) {
109109
case pexprs.Param:
@@ -406,8 +406,16 @@ class Assembler {
406406
this.emit(w.instr.br_table, w.vec(labels.map(i => w.labelidx(i))), w.labelidx(defaultIdx));
407407
}
408408

409+
return() {
410+
const what = this._blockStack[0];
411+
assert(what === 'block', 'Invalid return');
412+
this.emit(w.instr.return);
413+
}
414+
409415
// Emit a dense jump table (switch-like) using br_table.
410416
switch(bt, condThunk, caseThunks, defaultThunk) {
417+
const startStackHeight = this._blockStack.length;
418+
411419
// Emit one block per case…
412420
caseThunks.forEach(_ => this._blockOnly(bt));
413421

@@ -419,10 +427,12 @@ class Assembler {
419427
this.brTable(labels, w.labelidx(labels.length));
420428
});
421429
caseThunks.forEach((fn, i) => {
422-
fn();
423-
this.break(labels.length - (i + 1)); // Jump to end.
430+
const depth = labels.length - (i + 1);
431+
fn(depth);
432+
this.break(depth); // Jump to end.
424433
this._endBlock();
425434
});
435+
assert(this._blockStack.length === startStackHeight);
426436
}
427437

428438
// "Macros" -- codegen helpers specific to Ohm.
@@ -825,7 +835,9 @@ export class Compiler {
825835
asm.switch(
826836
w.blocktype.empty,
827837
() => asm.localGet('__arg0'),
828-
this.liftedTerminals.values().map(str => () => this.emitTerminal(ir.terminal(str))),
838+
this.liftedTerminals
839+
.values()
840+
.map(str => depth => this.emitTerminal(ir.terminal(str), depth)),
829841
() => asm.emit(w.instr.unreachable),
830842
);
831843
asm.localGet('ret');
@@ -894,10 +906,12 @@ export class Compiler {
894906
const rulePatterns = setdefault(patternsByRule, ruleName, () => new Map());
895907
rulePatterns.set(specializedName, children);
896908

897-
// Note that we deliberately *don't* visit this application yet,
898-
// so it won't be assigned a rule ID.
899-
const caseIdx = rulePatterns.size - 1;
900-
body = ir.applyGeneralized(ruleName, caseIdx);
909+
if (EMIT_GENERALIZED_RULES) {
910+
// Note that we deliberately *don't* visit this application yet,
911+
// so it won't be assigned a rule ID.
912+
const caseIdx = rulePatterns.size - 1;
913+
body = ir.applyGeneralized(ruleName, caseIdx);
914+
}
901915
}
902916
newRules.set(specializedName, {...ruleInfo, body, formals: []});
903917
}
@@ -909,24 +923,26 @@ export class Compiler {
909923
specialize(ir.apply('spaces'));
910924
this.rules = newRules;
911925

912-
const insertDispatches = (exp, patterns) =>
913-
ir.rewrite(exp, {
914-
Apply: app => (app.children.length === 0 ? app : ir.dispatch(app, patterns)),
915-
Param: p => ir.dispatch(p, patterns),
916-
});
926+
if (EMIT_GENERALIZED_RULES) {
927+
const insertDispatches = (exp, patterns) =>
928+
ir.rewrite(exp, {
929+
Apply: app => (app.children.length === 0 ? app : ir.dispatch(app, patterns)),
930+
Param: p => ir.dispatch(p, patterns),
931+
});
917932

918-
// Save the observed patterns of the parameterized rules.
919-
// All non-parameterized & specialized rules have been discovered and
920-
// assigned IDs; any rule IDs assigned here won't be memoized.
921-
for (const [name, patterns] of patternsByRule.entries()) {
922-
this._ensureRuleId(name, {notMemoized: true});
923-
const ruleInfo = getNotNull(rules, name);
924-
const patternsArr = [...patterns.values()];
925-
newRules.set(name, {
926-
...ruleInfo,
927-
body: insertDispatches(ruleInfo.body, patternsArr),
928-
patterns: patternsArr,
929-
});
933+
// Save the observed patterns of the parameterized rules.
934+
// All non-parameterized & specialized rules have been discovered and
935+
// assigned IDs; any rule IDs assigned here won't be memoized.
936+
for (const [name, patterns] of patternsByRule.entries()) {
937+
this._ensureRuleId(name, {notMemoized: true});
938+
const ruleInfo = getNotNull(rules, name);
939+
const patternsArr = [...patterns.values()];
940+
newRules.set(name, {
941+
...ruleInfo,
942+
body: insertDispatches(ruleInfo.body, patternsArr),
943+
patterns: patternsArr,
944+
});
945+
}
930946
}
931947
}
932948

@@ -1124,11 +1140,12 @@ export class Compiler {
11241140
return;
11251141
}
11261142
if (exp.type === 'ApplyGeneralized') {
1143+
assert(EMIT_GENERALIZED_RULES);
11271144
this.emitApplyGeneralized(exp);
11281145
return;
11291146
}
11301147

1131-
const debugLabel = getDebugLabel(exp);
1148+
const debugLabel = ir.toString(exp);
11321149
asm.emit(`BEGIN ${debugLabel}`);
11331150
asm.pushStackFrame();
11341151

@@ -1355,14 +1372,14 @@ export class Compiler {
13551372
asm.localSet('ret');
13561373
}
13571374

1358-
emitTerminal({value}) {
1375+
emitTerminal({value}, depth = 0) {
13591376
// TODO:
13601377
// - proper UTF-8!
13611378
// - handle longer terminals with a loop
13621379
// - SIMD
13631380

13641381
const {asm} = this;
1365-
asm.emit('Terminal');
1382+
asm.emit(JSON.stringify(value));
13661383
this.maybeEmitSpaceSkipping();
13671384
for (const c of [...value]) {
13681385
// Compare next char
@@ -1371,7 +1388,7 @@ export class Compiler {
13711388
asm.emit(instr.i32.ne);
13721389
asm.if(w.blocktype.empty, () => {
13731390
asm.setRet(0);
1374-
asm.break(1);
1391+
asm.break(depth + 1);
13751392
});
13761393
asm.incPos();
13771394
}

packages/wasm/src/ir.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,50 @@ export function rewrite(exp: Expr, actions: RewriteActions): Expr {
317317
unreachable(exp, `not handled: ${exp}`);
318318
}
319319
}
320+
321+
export function toString(exp: Expr): string {
322+
switch (exp.type) {
323+
case 'Alt':
324+
return `(${exp.children.map(toString).join(' | ')})`;
325+
case 'Seq':
326+
return `(${exp.children.map(toString).join(' ')})`;
327+
case 'Any':
328+
return '$any';
329+
case 'Apply':
330+
return exp.children.length > 0
331+
? `${exp.ruleName}<${exp.children.map(toString).join(',')}>`
332+
: exp.ruleName;
333+
case 'ApplyGeneralized':
334+
return `${exp.ruleName}<#${exp.caseIdx}>`;
335+
case 'CaseInsensitive':
336+
return `$caseInsensitive<${JSON.stringify(exp.value)}>`;
337+
case 'End':
338+
return '$end';
339+
case 'LiftedTerminal':
340+
return `$term$${exp.terminalId}`;
341+
case 'Param':
342+
return `$${exp.index}`;
343+
case 'Range':
344+
return `${JSON.stringify(exp.lo)}..${JSON.stringify(exp.hi)}`;
345+
case 'Terminal':
346+
return JSON.stringify(exp.value);
347+
case 'UnicodeChar':
348+
return `$unicodeChar<${JSON.stringify(exp.value)}>`;
349+
case 'Dispatch':
350+
return `$dispatch`; // TODO: Improve this.
351+
case 'Lex':
352+
return `#${toString(exp.child)}`;
353+
case 'Lookahead':
354+
return `&${toString(exp.child)}`;
355+
case 'Not':
356+
return `~${toString(exp.child)}`;
357+
case 'Opt':
358+
return `${toString(exp.child)}?`;
359+
case 'Plus':
360+
return `${toString(exp.child)}+`;
361+
case 'Star':
362+
return `${toString(exp.child)}*`;
363+
default:
364+
unreachable(exp, `not handled: ${exp}`);
365+
}
366+
}

packages/wasm/test/test-wasm.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,19 @@ test('determinism', t => {
878878
t.deepEqual(new Compiler(g).compile(), new Compiler(g).compile());
879879
});
880880

881+
test('lifted terminals', async t => {
882+
const g = ohm.grammar(`
883+
G {
884+
start = two<"x"> | one<"y">
885+
one<t> = t
886+
two<t> = t t
887+
}`);
888+
const m = await wasmMatcherForGrammar(g);
889+
t.is(matchWithInput(m, 'xx'), 1);
890+
t.is(matchWithInput(m, 'y'), 1);
891+
t.is(matchWithInput(m, 'yy'), 0);
892+
});
893+
881894
// eslint-disable-next-line ava/no-skip-test
882895
test.skip('basic space skipping', async t => {
883896
const g = ohm.grammar(`

0 commit comments

Comments
 (0)