Skip to content

Commit 5392362

Browse files
committed
wasm: Fix and re-enable implicit space skipping
1 parent 711acca commit 5392362

7 files changed

Lines changed: 122 additions & 50 deletions

File tree

packages/wasm/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ build/es5.wasm: dist/index.js
4141
$(NODE) scripts/es5ToWasm.js build/es5.wasm
4242

4343
build/liquid-html.wasm: dist/index.js
44-
$(NODE) src/cli.js -g LiquidHTML -o build/liquid-html.wasm test/data/liquid-html-mod.ohm
44+
$(NODE) src/cli.js -g LiquidHTML -o build/liquid-html.wasm test/data/liquid-html.ohm
4545

4646
.PHONY: go-test-es5
4747
go-test-es5: test/go/testmain build/es5.wasm

packages/wasm/runtime/ohmRuntime.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ declare function fillInputBuffer(offset: i32, maxLen: i32): i32;
44
declare function printI32(val: i32): void;
55
declare function isRuleSyntactic(ruleId: i32): bool;
66

7-
@inline const IMPLICIT_SPACE_SKIPPING = false;
7+
@inline const IMPLICIT_SPACE_SKIPPING = true;
88

99
// TODO: Find a way to share these.
1010
@inline const WASM_PAGE_SIZE: usize = 64 * 1024;
@@ -84,6 +84,15 @@ function hasMemoizedResult(ruleId: i32): boolean {
8484
return memoTableGet(pos, ruleId) !== 0;
8585
}
8686

87+
@inline function maybeSkipSpaces(ruleId: i32): void {
88+
// TODO: Find a better way to deal with the bindings here.
89+
if (IMPLICIT_SPACE_SKIPPING && isRuleSyntactic(ruleId)) {
90+
const origNumBindings = bindings.length;
91+
evalApply0(2);
92+
bindings.length = origNumBindings;
93+
}
94+
}
95+
8796
export function match(startRuleId: i32): Result {
8897
// (Re-)initialize globals, clear memo table.
8998
pos = 0;
@@ -93,15 +102,12 @@ export function match(startRuleId: i32): Result {
93102

94103
// Get the input and do the match.
95104
let inputLen = fillInputBuffer(0, i32(WASM_PAGE_SIZE));
96-
const succeeded = evalApply0(startRuleId) !== 0;
97105

98-
// Potentially skip trailing spaces before checking for the end.
99-
if (IMPLICIT_SPACE_SKIPPING && succeeded && isRuleSyntactic(startRuleId)) {
100-
evalApply0(2);
101-
}
102-
103-
if (inputLen === pos) {
104-
return succeeded;
106+
maybeSkipSpaces(startRuleId);
107+
const succeeded = evalApply0(startRuleId) !== 0;
108+
if (succeeded) {
109+
maybeSkipSpaces(startRuleId);
110+
return inputLen === pos;
105111
}
106112
return 0;
107113
}
@@ -218,5 +224,6 @@ export function setBindingsLength(len: i32): void {
218224
}
219225

220226
export function getCstRoot(): usize {
227+
// TODO: Figure out how to handle this w.r.t. leading and trailing space.
221228
return bindings[0];
222229
}

packages/wasm/scripts/bench.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const inputs = {
1919
underscore: readFileSync(join(datadir, '_underscore-1.8.3.js'), 'utf-8'),
2020
};
2121

22-
const liquid = ohm.grammars(readFileSync(join(datadir, 'liquid-html-mod.ohm'), 'utf8'));
22+
const liquid = ohm.grammars(readFileSync(join(datadir, 'liquid-html.ohm'), 'utf8'));
2323

2424
let liquidHtmlMatcher;
2525
let es5Matcher;

packages/wasm/scripts/parseLiquid.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {wasmMatcherForGrammar} from '../test/_helpers.js';
2222
const __dirname = dirname(fileURLToPath(import.meta.url));
2323
const datadir = join(__dirname, '../test/data');
2424

25-
const liquid = ohm.grammars(readFileSync(join(datadir, 'liquid-html-mod.ohm'), 'utf8'));
25+
const liquid = ohm.grammars(readFileSync(join(datadir, 'liquid-html.ohm'), 'utf8'));
2626

2727
// Get pattern from command line arguments
2828
const pattern = process.argv[2];
@@ -76,7 +76,10 @@ const matchWithInput = (m, str) => (m.setInput(str), m.match());
7676
const start = performance.now();
7777
assert.equal(matchWithInput(m, input), 1, `failed: ${path}`);
7878
wasmTimes.push(performance.now() - start);
79-
assert.equal(input.length, unparseW(input, m.getCstRoot()).length);
79+
80+
// Trailing/leading spaces are currently dropped, so trim both
81+
// to after unparsing.
82+
assert.equal(input.trim().length, unparseW(input, m.getCstRoot()).trim().length);
8083
}
8184

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

packages/wasm/src/index.js

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const WASM_PAGE_SIZE = 64 * 1024;
1212
const DEBUG = process.env.OHM_DEBUG === '1';
1313
const FAST_SAVE_BINDINGS = true;
1414
const FAST_RESTORE_BINDINGS = true;
15-
const IMPLICIT_SPACE_SKIPPING = false;
15+
16+
const IMPLICIT_SPACE_SKIPPING = true;
1617

1718
// When specializing rules, should we emit a generalized version that
1819
// handles the specific cases? If false, code size will be larger.
@@ -63,7 +64,10 @@ function uniqueName(names, str) {
6364
return name;
6465
}
6566

66-
const isSyntactic = ruleName => ruleName[0] === ruleName[0].toUpperCase();
67+
function isSyntacticRule(ruleName) {
68+
assert(ruleName[0] !== '$', ruleName);
69+
return ruleName[0] === ruleName[0].toUpperCase();
70+
}
6771

6872
class IndexedSet {
6973
constructor() {
@@ -617,11 +621,11 @@ export class Compiler {
617621
return idx;
618622
}
619623

620-
inLexifiedContext() {
621-
return this._lexContextStack.at(-1);
624+
inLexicalContext() {
625+
return checkNotNull(this._lexContextStack.at(-1));
622626
}
623627

624-
liftPExpr(exp) {
628+
liftPExpr(exp, isSyntactic) {
625629
assert(!(exp instanceof pexprs.Terminal));
626630

627631
// Note: the same expression might appear in more than one place, and
@@ -656,7 +660,7 @@ export class Compiler {
656660
formals = newParams.filter(isNonNull).map(p => `__${p.index}`);
657661
}
658662
const actuals = freeVars.filter(isNonNull);
659-
const ruleInfo = {body, formals, source: exp.source};
663+
const ruleInfo = {body, formals, isSyntactic, source: exp.source};
660664
return [name, ruleInfo, actuals];
661665
}
662666

@@ -733,13 +737,26 @@ export class Compiler {
733737
const {grammar} = this;
734738

735739
const lookUpRule = name => {
736-
if (name in grammar.rules) return grammar.rules[name];
737-
if (grammar.superGrammar) return lookUpRule(name, grammar.superGrammar);
740+
const isSyntactic = isSyntacticRule(name);
741+
if (name in grammar.rules) {
742+
return {...grammar.rules[name], isSyntactic};
743+
}
744+
if (grammar.superGrammar) {
745+
return lookUpRule(name, grammar.superGrammar);
746+
}
738747
};
739748

740749
// Begin with all the rules in the grammar + spaces.
741-
const rules = Object.entries(this.grammar.rules);
742-
rules.push(['spaces', lookUpRule('spaces')]);
750+
const rules = Object.entries(this.grammar.rules).map(([name, info]) => {
751+
const isSyntactic = isSyntacticRule(name);
752+
return [name, {...info, isSyntactic}];
753+
});
754+
rules.push([
755+
'spaces',
756+
{
757+
...lookUpRule('spaces'),
758+
},
759+
]);
743760

744761
const liftedTerminals = new IndexedSet();
745762

@@ -751,25 +768,25 @@ export class Compiler {
751768

752769
// If `exp` is not an Apply or Param, lift it into its own rule and return
753770
// a new application of that rule.
754-
const simplifyArg = exp => {
771+
const simplifyArg = (exp, isSyntactic) => {
755772
if (isApplyLike(exp)) {
756-
return simplify(exp);
773+
return simplify(exp, isSyntactic);
757774
}
758775
if (exp instanceof pexprs.Terminal) {
759776
return liftTerminal(exp);
760777
}
761778

762-
const [name, info, env] = this.liftPExpr(exp);
779+
const [name, info, env] = this.liftPExpr(exp, isSyntactic);
763780
const args = env.map(p => {
764781
assert(p instanceof pexprs.Param, 'Expected Param');
765782
return ir.param(p.index);
766783
});
767784
rules.push([name, info]);
768785
return ir.apply(name, args);
769786
};
770-
const simplify = exp => {
787+
const simplify = (exp, isSyntactic) => {
771788
if (exp instanceof pexprs.Alt) {
772-
return ir.alt(exp.terms.map(e => simplify(e)));
789+
return ir.alt(exp.terms.map(e => simplify(e, isSyntactic)));
773790
}
774791
if (exp === pexprs.any) return ir.any();
775792
if (exp === pexprs.end) return ir.end();
@@ -778,24 +795,24 @@ export class Compiler {
778795
rules.push([exp.ruleName, checkNotNull(lookUpRule(exp.ruleName))]);
779796
return ir.apply(
780797
exp.ruleName,
781-
exp.args.map(arg => simplifyArg(arg)),
798+
exp.args.map(arg => simplifyArg(arg, isSyntactic)),
782799
);
783800
case pexprs.CaseInsensitive:
784801
return ir.caseInsensitive(exp.obj);
785802
case pexprs.Lex:
786-
return ir.lex(simplify(exp.expr));
803+
return ir.lex(simplify(exp.expr, true));
787804
case pexprs.Lookahead:
788-
return ir.lookahead(simplify(exp.expr));
805+
return ir.lookahead(simplify(exp.expr, isSyntactic));
789806
case pexprs.Not:
790-
return ir.not(simplify(exp.expr));
807+
return ir.not(simplify(exp.expr, isSyntactic));
791808
case pexprs.Opt:
792-
return ir.opt(simplify(exp.expr));
809+
return ir.opt(simplify(exp.expr, isSyntactic));
793810
case pexprs.Plus:
794-
return ir.plus(simplify(exp.expr));
811+
return ir.plus(simplify(exp.expr, isSyntactic));
795812
case pexprs.Seq:
796-
return ir.seq(exp.factors.map(e => simplify(e)));
813+
return ir.seq(exp.factors.map(e => simplify(e, isSyntactic)));
797814
case pexprs.Star:
798-
return ir.star(simplify(exp.expr));
815+
return ir.star(simplify(exp.expr, isSyntactic));
799816
case pexprs.Param:
800817
return ir.param(exp.index);
801818
case pexprs.Range:
@@ -818,7 +835,7 @@ export class Compiler {
818835
if (!newRules.has(name)) {
819836
newRules.set(name, {
820837
...info,
821-
body: simplify(info.body),
838+
body: simplify(info.body, info.isSyntactic),
822839
});
823840
}
824841
}
@@ -828,10 +845,10 @@ export class Compiler {
828845

829846
compileTerminalRule(name) {
830847
const {asm} = this;
848+
this.beginLexContext(true);
831849
asm.addFunction(`$${name}`, [w.valtype.i32], [w.valtype.i32], () => {
832850
asm.addLocal('ret', w.valtype.i32);
833851
asm.addLocal('tmp', w.valtype.i32);
834-
835852
asm.switch(
836853
w.blocktype.empty,
837854
() => asm.localGet('__arg0'),
@@ -842,18 +859,28 @@ export class Compiler {
842859
);
843860
asm.localGet('ret');
844861
});
862+
this.endLexContext();
845863
return this.asm._functionDecls.at(-1);
846864
}
847865

866+
beginLexContext(initialVal) {
867+
assert(this._lexContextStack.length === 0);
868+
this._lexContextStack.push(initialVal);
869+
}
870+
871+
endLexContext() {
872+
this._lexContextStack.pop();
873+
assert(this._lexContextStack.length === 0);
874+
}
875+
848876
compileRule(name) {
849877
const {asm} = this;
850878
const ruleInfo = getNotNull(this.rules, name);
851879
let paramTypes = [];
852880
if (ruleInfo.patterns) {
853881
paramTypes = [w.valtype.i32];
854882
}
855-
assert(this._lexContextStack.length === 0);
856-
this._lexContextStack.push(!isSyntactic(name));
883+
this.beginLexContext(!ruleInfo.isSyntactic);
857884
asm.addFunction(`$${name}`, paramTypes, [w.valtype.i32], () => {
858885
asm.addLocal('ret', w.valtype.i32);
859886
asm.addLocal('tmp', w.valtype.i32);
@@ -862,8 +889,7 @@ export class Compiler {
862889
asm.emit(`END eval:${name}`);
863890
asm.localGet('ret');
864891
});
865-
this._lexContextStack.pop();
866-
assert(this._lexContextStack.length === 0);
892+
this.endLexContext();
867893
return this.asm._functionDecls.at(-1);
868894
}
869895

@@ -1204,6 +1230,7 @@ export class Compiler {
12041230

12051231
emitApplyTerm({terminalId}) {
12061232
const {asm} = this;
1233+
this.maybeEmitSpaceSkipping();
12071234
asm.i32Const(terminalId);
12081235
asm.emit(w.instr.call, this.ruleEvalFuncIdx('$term'));
12091236
asm.localSet('ret');
@@ -1223,7 +1250,7 @@ export class Compiler {
12231250
assert(exp.children.length === 0);
12241251

12251252
if (exp !== this._applySpaces) {
1226-
this.maybeEmitSpaceSkipping();
1253+
this.maybeEmitSpaceSkipping(); // Avoid infinite recursion.
12271254
}
12281255

12291256
const {asm} = this;
@@ -1340,7 +1367,7 @@ export class Compiler {
13401367
}
13411368

13421369
maybeEmitSpaceSkipping() {
1343-
if (IMPLICIT_SPACE_SKIPPING && !this.inLexifiedContext()) {
1370+
if (IMPLICIT_SPACE_SKIPPING && !this.inLexicalContext()) {
13441371
this.asm.emit('BEGIN space skipping');
13451372
this.emitApply(this._applySpaces);
13461373
this.asm.emit('END space skipping');

packages/wasm/test/test-liquid-html.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {wasmMatcherForGrammar} from './_helpers.js';
1010
const matchWithInput = (m, str) => (m.setInput(str), m.match());
1111

1212
const scriptRel = relPath => new URL(relPath, import.meta.url);
13-
const grammarSource = fs.readFileSync(scriptRel('data/liquid-html-mod.ohm'), 'utf8');
13+
const grammarSource = fs.readFileSync(scriptRel('data/liquid-html.ohm'), 'utf8');
1414

1515
const liquid = ohm.grammars(grammarSource);
1616

@@ -36,6 +36,14 @@ test('swatch.liquid', async t => {
3636
t.is(matchWithInput(m, input), 1);
3737
});
3838

39+
test('html comment', async t => {
40+
const input = `{% if x %}
41+
<!-- x -->
42+
{% endif %}`;
43+
const m = await wasmMatcherForGrammar(liquid.LiquidHTML);
44+
t.is(matchWithInput(m, input), 1);
45+
});
46+
3947
test('book-review.liquid', async t => {
4048
const input = fs.readFileSync(scriptRel('data/book-review.liquid'), 'utf8');
4149
let start = performance.now();

packages/wasm/test/test-wasm.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -891,18 +891,45 @@ test('lifted terminals', async t => {
891891
t.is(matchWithInput(m, 'yy'), 0);
892892
});
893893

894-
// eslint-disable-next-line ava/no-skip-test
895-
test.skip('basic space skipping', async t => {
894+
test('basic space skipping', async t => {
896895
const g = ohm.grammar(`
897896
G {
898897
Start = ">" (digit "a".."z")*
899898
}`);
900899
const m = await wasmMatcherForGrammar(g);
901-
t.is(matchWithInput(m, '> 0 a 1 b '), 1);
900+
t.is(matchWithInput(m, '> 0 a 1 b'), 1);
901+
t.is(matchWithInput(m, ' > 0 a 1 b '), 1);
902902
});
903903

904-
// eslint-disable-next-line ava/no-skip-test
905-
test.skip('space skipping & lex', async t => {
904+
test('space skipping w/ lifted terminals', async t => {
905+
// It shouldn't matter that the terminal (as arg) appears in a syntactic
906+
// context; only the point of use.
907+
const g = ohm.grammar(`
908+
G {
909+
Start = two<"x">
910+
two<t> = t t
911+
}`);
912+
const m = await wasmMatcherForGrammar(g);
913+
t.is(matchWithInput(m, 'xx'), 1);
914+
t.is(matchWithInput(m, ' xx'), 1);
915+
t.is(matchWithInput(m, 'x x'), 0);
916+
});
917+
918+
test('space skipping w/ params', async t => {
919+
// Make sure space is skipped before params in the body of syntactic rule.
920+
const g = ohm.grammar(`
921+
G {
922+
Start = Reversed<(x | "x"), y, "z".."z"> Reversed<z, y, x>
923+
Reversed<a, b, c> = c b a
924+
x = "x"
925+
y = "y"
926+
z = "z"
927+
}`);
928+
const m = await wasmMatcherForGrammar(g);
929+
t.is(matchWithInput(m, ' z y x xyz'), 1);
930+
});
931+
932+
test('space skipping & lex', async t => {
906933
{
907934
const g = ohm.grammar('G { start = ">" digit+ #(space) }');
908935
const m = await wasmMatcherForGrammar(g);

0 commit comments

Comments
 (0)