Skip to content

Commit b65fa52

Browse files
committed
wasm: Implement arity for iter nodes, + map()
1 parent d1fd74e commit b65fa52

File tree

4 files changed

+133
-21
lines changed

4 files changed

+133
-21
lines changed

packages/miniohm-js/index.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,15 @@ export class CstNode {
213213
}
214214

215215
isNonterminal() {
216-
return (this._type & CST_NODE_TYPE_MASK) === CstNodeType.NONTERMINAL;
216+
return (this._typeAndDetails & CST_NODE_TYPE_MASK) === CstNodeType.NONTERMINAL;
217217
}
218218

219219
isTerminal() {
220-
return (this._type & CST_NODE_TYPE_MASK) === CstNodeType.TERMINAL;
220+
return (this._typeAndDetails & CST_NODE_TYPE_MASK) === CstNodeType.TERMINAL;
221221
}
222222

223223
isIter() {
224-
return (this._type & CST_NODE_TYPE_MASK) === CstNodeType.ITER;
224+
return (this._typeAndDetails & CST_NODE_TYPE_MASK) === CstNodeType.ITER;
225225
}
226226

227227
isOptional() {
@@ -245,10 +245,14 @@ export class CstNode {
245245
return this._view.getUint32(this._base + 4, true);
246246
}
247247

248-
get _type() {
248+
get _typeAndDetails() {
249249
return this._view.getInt32(this._base + 8, true);
250250
}
251251

252+
get arity() {
253+
return this._typeAndDetails >>> 2;
254+
}
255+
252256
get children() {
253257
if (!this._children) {
254258
this._children = this._computeChildren();
@@ -304,6 +308,16 @@ export class CstNode {
304308
const {sourceString, startIdx} = this;
305309
return `CstNode {ctorName: ${ctorName}, sourceString: ${sourceString}, startIdx: ${startIdx} }`;
306310
}
311+
312+
map(callbackFn) {
313+
const {arity, children} = this;
314+
assert(callbackFn.length === arity, 'bad arity');
315+
const ans = [];
316+
for (let i = 0; i < children.length; i += arity) {
317+
ans.push(callbackFn(...children.slice(i, i + arity)));
318+
}
319+
return ans;
320+
}
307321
}
308322

309323
export class MatchResult {

packages/wasm/src/index.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -599,11 +599,11 @@ class Assembler {
599599
this.emit(instr.call, w.funcidx(prebuiltFuncidx(name)));
600600
}
601601

602-
newIterNodeWithSavedPosAndBindings() {
602+
newIterNodeWithSavedPosAndBindings(arity) {
603603
this.getSavedPos();
604604
this.globalGet('pos');
605605
this.getSavedNumBindings();
606-
this.i32Const(0); // TODO: arity
606+
this.i32Const(arity);
607607
this.callPrebuiltFunc('newIterationNode');
608608
}
609609

@@ -1524,7 +1524,7 @@ export class Compiler {
15241524
asm.restorePos();
15251525
asm.restoreBindingsLength();
15261526
});
1527-
asm.newIterNodeWithSavedPosAndBindings();
1527+
asm.newIterNodeWithSavedPosAndBindings(ir.outArity(child));
15281528
asm.localSet('ret');
15291529
}
15301530

@@ -1609,8 +1609,7 @@ export class Compiler {
16091609
asm.restorePos();
16101610
asm.restoreBindingsLength();
16111611
asm.popStackFrame();
1612-
1613-
asm.newIterNodeWithSavedPosAndBindings();
1612+
asm.newIterNodeWithSavedPosAndBindings(ir.outArity(child));
16141613
asm.localSet('ret');
16151614
}
16161615

packages/wasm/src/ir.ts

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@ export type Expr =
2525
export interface Alt {
2626
type: 'Alt';
2727
children: Expr[];
28+
outArity: number;
2829
}
2930

30-
export const alt = (children: Expr[]): Alt => ({type: 'Alt', children});
31+
export const alt = (children: Expr[]): Alt => ({
32+
type: 'Alt',
33+
children,
34+
outArity: outArity(children[0])
35+
});
3136

3237
export interface Any {
3338
type: 'Any';
@@ -91,17 +96,27 @@ export const end = (): End => ({type: 'End'});
9196
export interface Lex {
9297
type: 'Lex';
9398
child: Expr;
99+
outArity: number;
94100
}
95101

96-
export const lex = (child: Expr): Lex => ({type: 'Lex', child});
102+
export const lex = (child: Expr): Lex => ({
103+
type: 'Lex',
104+
child,
105+
outArity: outArity(child)
106+
});
97107

98108
// TODO: Eliminate this, and replace with Not(Not(...))?
99109
export interface Lookahead {
100110
type: 'Lookahead';
101111
child: Expr;
112+
outArity: number;
102113
}
103114

104-
export const lookahead = (child: Expr): Lookahead => ({type: 'Lookahead', child});
115+
export const lookahead = (child: Expr): Lookahead => ({
116+
type: 'Lookahead',
117+
child,
118+
outArity: outArity(child)
119+
});
105120

106121
export interface Opt {
107122
type: 'Opt';
@@ -142,9 +157,14 @@ export const range = (lo: number, hi: number): Range => ({type: 'Range', lo, hi}
142157
export interface Seq {
143158
type: 'Seq';
144159
children: Expr[];
160+
outArity: number;
145161
}
146162

147-
export const seq = (children: Expr[]): Seq => ({type: 'Seq', children});
163+
export const seq = (children: Expr[]): Seq => ({
164+
type: 'Seq',
165+
children,
166+
outArity: children.reduce((acc, child) => acc + outArity(child), 0)
167+
});
148168

149169
export interface Star {
150170
type: 'Star';
@@ -247,15 +267,24 @@ export function substituteParams(exp: Expr, actuals: Expr[]) {
247267
case 'Seq':
248268
return {
249269
type: exp.type,
250-
children: exp.children.map(c => substituteParams(c, actuals))
270+
children: exp.children.map(c => substituteParams(c, actuals)),
271+
outArity: exp.outArity
251272
};
252273
case 'Lex':
253274
case 'Lookahead':
275+
return {
276+
type: exp.type,
277+
child: substituteParams(exp.child, actuals),
278+
outArity: exp.outArity
279+
};
254280
case 'Not':
255281
case 'Opt':
256282
case 'Plus':
257283
case 'Star':
258-
return {type: exp.type, child: substituteParams(exp.child, actuals)};
284+
return {
285+
type: exp.type,
286+
child: substituteParams(exp.child, actuals)
287+
};
259288
case 'Any':
260289
case 'ApplyGeneralized':
261290
case 'CaseInsensitive':
@@ -297,8 +326,9 @@ export function rewrite(exp: Expr, actions: RewriteActions): Expr {
297326

298327
switch (exp.type) {
299328
case 'Alt':
329+
return alt(exp.children.map((e: Expr) => rewrite(e, actions)));
300330
case 'Seq':
301-
return {type: exp.type, children: exp.children.map((e: Expr) => rewrite(e, actions))};
331+
return seq(exp.children.map((e: Expr) => rewrite(e, actions)));
302332
case 'Any':
303333
case 'Apply':
304334
case 'ApplyGeneralized':
@@ -311,14 +341,20 @@ export function rewrite(exp: Expr, actions: RewriteActions): Expr {
311341
case 'UnicodeChar':
312342
return exp;
313343
case 'Dispatch':
344+
// We don't use the constructor here, to avoid type checking issues.
314345
return {type: exp.type, child: rewrite(exp.child, actions), patterns: exp.patterns};
315346
case 'Lex':
347+
return lex(rewrite(exp.child, actions));
316348
case 'Lookahead':
349+
return lookahead(rewrite(exp.child, actions));
317350
case 'Not':
351+
return not(rewrite(exp.child, actions));
318352
case 'Opt':
353+
return opt(rewrite(exp.child, actions));
319354
case 'Plus':
355+
return plus(rewrite(exp.child, actions));
320356
case 'Star':
321-
return {type: exp.type, child: rewrite(exp.child, actions)};
357+
return star(rewrite(exp.child, actions));
322358
default:
323359
unreachable(exp, `not handled: ${exp}`);
324360
}
@@ -370,3 +406,38 @@ export function toString(exp: Expr): string {
370406
unreachable(exp, `not handled: ${exp}`);
371407
}
372408
}
409+
410+
// Returns the number of bindings that `expr` produces in its parent — the
411+
// "out arity" or "upwards arity". Note that there is potential confusion
412+
// with iter nodes: they produce a single binding, but an expression like
413+
// `(letter digit)*` can be said to have "arity 2".
414+
export function outArity(exp: Expr): number {
415+
console.log(JSON.stringify(exp));
416+
switch (exp.type) {
417+
case 'Alt':
418+
case 'Seq':
419+
case 'Lex':
420+
case 'Lookahead':
421+
return exp.outArity;
422+
case 'Any':
423+
case 'Apply':
424+
case 'ApplyGeneralized':
425+
case 'CaseInsensitive':
426+
case 'End':
427+
case 'LiftedTerminal':
428+
case 'Param':
429+
case 'Range':
430+
case 'Terminal':
431+
case 'UnicodeChar':
432+
case 'Dispatch':
433+
return 1;
434+
case 'Not':
435+
return 0;
436+
case 'Opt':
437+
case 'Plus':
438+
case 'Star':
439+
return 1;
440+
default:
441+
unreachable(exp, `not handled: ${exp}`);
442+
}
443+
}

packages/wasm/test/test-wasm.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ test('cst returns', async t => {
4646
let term = root.children[0];
4747
t.is(term.children.length, 0);
4848
t.is(term.matchLength, 1);
49-
t.is(term._type, 1);
49+
t.is(term._typeAndDetails, 1);
5050
t.true(term.isTerminal());
5151

5252
matcher = await wasmMatcherForGrammar(ohm.grammar('G { start = "a" b\nb = "b" }'));
@@ -974,13 +974,41 @@ test.failing('unicode', async t => {
974974
t.is(unparse(m), source);
975975
});
976976

977-
// eslint-disable-next-line ava/no-skip-test
978-
test.skip('iter node map', async t => {
977+
test('iter nodes: basic map (star)', async t => {
979978
const m = await wasmMatcherForGrammar(ohm.grammar('G { Start = (letter digit)* }'));
979+
t.is(matchWithInput(m, ''), 1, 'empty input matches');
980980
t.is(matchWithInput(m, 'a1 b2 c 3'), 1);
981+
t.is(m.getCstRoot().children.length, 1);
981982
const iter = m.getCstRoot().children[0];
982983
t.deepEqual(
983-
iter.map((letter, digit) => `${digit}${letter}`),
984+
iter.map((letter, digit) => `${digit.sourceString}${letter.sourceString}`),
984985
['1a', '2b', '3c'],
985986
);
987+
t.throws(() => iter.map(() => {}), {message: /bad arity/});
988+
});
989+
990+
test('iter nodes: basic map (plus)', async t => {
991+
const m = await wasmMatcherForGrammar(ohm.grammar('G { Start = (letter digit)+ }'));
992+
t.is(matchWithInput(m, ''), 0, 'empty input FAILS');
993+
t.is(matchWithInput(m, 'a1 b2 c 3'), 1);
994+
t.is(m.getCstRoot().children.length, 1);
995+
const iter = m.getCstRoot().children[0];
996+
t.deepEqual(
997+
iter.map((letter, digit) => `${digit.sourceString}${letter.sourceString}`),
998+
['1a', '2b', '3c'],
999+
);
1000+
t.throws(() => iter.map(() => {}), {message: /bad arity/});
1001+
});
1002+
1003+
test('iter nodes: basic map (opt)', async t => {
1004+
const m = await wasmMatcherForGrammar(ohm.grammar('G { Start = (letter digit)? }'));
1005+
t.is(matchWithInput(m, ''), 1, 'empty input matches');
1006+
t.is(matchWithInput(m, 'a1'), 1);
1007+
t.is(m.getCstRoot().children.length, 1);
1008+
const iter = m.getCstRoot().children[0];
1009+
t.deepEqual(
1010+
iter.map((letter, digit) => `${digit.sourceString}${letter.sourceString}`),
1011+
['1a'],
1012+
);
1013+
t.throws(() => iter.map(() => {}), {message: /bad arity/});
9861014
});

0 commit comments

Comments
 (0)