Skip to content

Commit 678d76a

Browse files
committed
wasm: toAST cleanups
1 parent 1763ba0 commit 678d76a

File tree

3 files changed

+84
-86
lines changed

3 files changed

+84
-86
lines changed

packages/miniohm-js/index.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,13 @@ export class WasmMatcher {
154154
throw new Error('Not implemented');
155155
}
156156

157-
match() {
157+
match(ruleName = this._ruleNames[0]) {
158158
if (process.env.OHM_DEBUG === '1') debugger; // eslint-disable-line no-debugger
159159
const succeeded = this._instance.exports.match(0);
160160
return new MatchResult(
161161
this,
162162
this._input,
163-
this._ruleNames[0],
163+
ruleName,
164164
succeeded ? this.getCstRoot() : null,
165165
this.getRightmostFailurePosition(),
166166
);
@@ -192,7 +192,7 @@ export class WasmMatcher {
192192
}
193193
}
194194

195-
class CstNode {
195+
export class CstNode {
196196
constructor(ruleNames, dataView, ptr, startIdx) {
197197
// Non-enumerable properties
198198
Object.defineProperties(this, {
@@ -221,6 +221,10 @@ class CstNode {
221221
return false; // TODO
222222
}
223223

224+
get ctorName() {
225+
return this.isTerminal() ? '_terminal' : this.isIter() ? '_iter' : this.ruleName;
226+
}
227+
224228
get ruleName() {
225229
const id = this._view.getInt32(this._base + 8, true);
226230
return this._ruleNames[id];
@@ -255,7 +259,7 @@ class CstNode {
255259
const ptr = this._view.getUint32(slotOffset, true);
256260
// TODO: Avoid allocating $spaces nodes altogether?
257261
const node = new CstNode(this._ruleNames, this._view, ptr, startIdx);
258-
if (node.ruleName === '$spaces') {
262+
if (node.ctorName === '$spaces') {
259263
assert(!spaces, 'Multiple $spaces nodes found');
260264
spaces = node;
261265
} else {

packages/miniohm-js/toAST.js

Lines changed: 51 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,29 @@ function assert(cond, message = 'Assertion failed') {
22
if (!cond) throw new Error(message);
33
}
44

5-
function handleListOf(child) {
6-
return child.toAST(this.args.mapping);
7-
}
8-
9-
function handleEmptyListOf() {
10-
return [];
11-
}
12-
13-
function handleNonemptyListOf(first, sep, rest) {
14-
return [first.toAST(this.args.mapping)].concat(rest.toAST(this.args.mapping));
15-
}
16-
17-
const defaultMapping = {
18-
listOf: handleListOf,
19-
ListOf: handleListOf,
20-
21-
emptyListOf: handleEmptyListOf,
22-
EmptyListOf: handleEmptyListOf,
23-
24-
nonemptyListOf: handleNonemptyListOf,
25-
NonemptyListOf: handleNonemptyListOf,
26-
};
27-
28-
class Visitor {
29-
constructor(mapping) {
30-
this.mapping = mapping;
31-
}
32-
33-
visit(node) {
34-
const ctorName = node.isTerminal() ? '_terminal' : node.isIter() ? '_iter' : node.ruleName;
35-
if (ctorName in this.mapping && typeof this.mapping[ctorName] === 'function') {
36-
return this.mapping[ctorName].apply(this, node.children);
37-
}
38-
39-
if (node.isTerminal()) {
40-
return this.visitTerminal(node);
41-
} else if (node.isNonterminal()) {
42-
return this.visitNonterminal(node);
43-
} else if (node.isIter()) {
44-
return this.visitIter(node);
45-
} else {
46-
throw new Error(`Unknown node type: ${node._type}`);
47-
}
48-
}
49-
50-
visitTerminal(node, offset) {
5+
export function toAstWithMapping(mapping) {
6+
const handleListOf = child => visit(child);
7+
const handleEmptyListOf = () => [];
8+
const handleNonemptyListOf = (first, sep, rest) => {
9+
return [visit(first), ...visit(rest)];
10+
};
11+
12+
mapping = {
13+
listOf: handleListOf,
14+
ListOf: handleListOf,
15+
emptyListOf: handleEmptyListOf,
16+
EmptyListOf: handleEmptyListOf,
17+
nonemptyListOf: handleNonemptyListOf,
18+
NonemptyListOf: handleNonemptyListOf,
19+
...mapping,
20+
};
21+
22+
function visitTerminal(node, offset) {
5123
return node.sourceString;
5224
}
5325

54-
visitNonterminal(node) {
26+
function visitNonterminal(node) {
5527
const {children, ruleName} = node;
56-
const {mapping} = this;
5728

5829
// without customization
5930
if (!Object.hasOwn(mapping, ruleName)) {
@@ -65,14 +36,14 @@ class Visitor {
6536
// singular node (e.g. only surrounded by literals or lookaheads)
6637
const realChildren = children.filter(c => !c.isTerminal());
6738
if (realChildren.length === 1) {
68-
return this.visit(realChildren[0]);
39+
return visit(realChildren[0]);
6940
}
7041

7142
// rest: terms with multiple children
7243
}
7344
// direct forward
7445
if (typeof mapping[ruleName] === 'number') {
75-
return this.visit(children[mapping[ruleName]]);
46+
return visit(children[mapping[ruleName]]);
7647
}
7748
assert(typeof mapping[ruleName] !== 'function', "shouldn't be possible");
7849

@@ -86,7 +57,7 @@ class Visitor {
8657
const mappedProp = mapping[ruleName] && mapping[ruleName][prop];
8758
if (typeof mappedProp === 'number') {
8859
// direct forward
89-
ans[prop] = this.visit(children[mappedProp]);
60+
ans[prop] = visit(children[mappedProp]);
9061
} else if (
9162
typeof mappedProp === 'string' ||
9263
typeof mappedProp === 'boolean' ||
@@ -102,7 +73,7 @@ class Visitor {
10273
ans[prop] = mappedProp.call(this, children);
10374
} else if (mappedProp === undefined) {
10475
if (children[prop] && !children[prop].isTerminal()) {
105-
ans[prop] = this.visit(children[prop]);
76+
ans[prop] = visit(children[prop]);
10677
} else {
10778
// delete predefined 'type' properties, like 'type', if explicitely removed
10879
delete ans[prop];
@@ -112,28 +83,42 @@ class Visitor {
11283
return ans;
11384
}
11485

115-
visitIter(node) {
86+
function visitIter(node) {
11687
const {children} = node;
11788
if (node.isOptional()) {
11889
if (children.length === 0) {
11990
return null;
12091
} else {
121-
return this.visit(children[0]);
92+
return visit(children[0]);
12293
}
12394
}
12495

125-
return children.map(c => this.visit(c));
96+
return children.map(c => visit(c));
97+
}
98+
99+
function visit(nodeOrResult) {
100+
let node = nodeOrResult;
101+
if (typeof nodeOrResult.succeeded === 'function') {
102+
assert(nodeOrResult.succeeded(), 'Cannot convert failed match result to AST');
103+
node = nodeOrResult._cst;
104+
}
105+
const {ctorName} = node;
106+
if (ctorName in mapping && typeof mapping[ctorName] === 'function') {
107+
return mapping[ctorName].apply(this, node.children);
108+
}
109+
if (node.isTerminal()) {
110+
return visitTerminal(node);
111+
} else if (node.isIter()) {
112+
return visitIter(node);
113+
} else {
114+
assert(node.isNonterminal(), `Unknown node type: ${node._type}`);
115+
return node.ctorName in mapping && typeof mapping[node.ctorName] === 'function' ?
116+
mapping[node.ctorName].apply(this, node.children) :
117+
visitNonterminal(node);
118+
}
126119
}
127-
}
128120

129-
// Returns a plain JavaScript object that includes an abstract syntax tree (AST)
130-
// for the given match result `res` containg a concrete syntax tree (CST) and grammar.
131-
// The optional `mapping` parameter can be used to customize how the nodes of the CST
132-
// are mapped to the AST (see /doc/extras.md#toastmatchresult-mapping).
133-
export function toAST(result, mapping) {
134-
const visitor = new Visitor({...defaultMapping, ...mapping});
135-
// Note: in the original implementation of toAST, any functions in `mapping`
136-
// are removed after copying over to the final mapping. Looking at the code,
137-
// it doesn't seem strictly necessary, but it's not 100% clear.
138-
return visitor.visit(result._cst, 0);
121+
return visit;
139122
}
123+
124+
export const toAst = toAstWithMapping({});

packages/wasm/test/test-toAST.js

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {toAST} from '@ohm-js/miniohm-js/toAST.js';
1+
import {toAstWithMapping} from '@ohm-js/miniohm-js/toAST.js';
22
import test from 'ava';
33
import * as ohm from 'ohm-js';
44

@@ -23,12 +23,12 @@ const arithmetic = ohm.grammar(`
2323
}
2424
`);
2525

26-
// Copied from test/extras/test-toAST.js and modified for the new toAST API.
26+
// Copied from test/extras/test-toAst.js and modified for the new toAST API.
2727
test('toAST basic', async t => {
2828
const m = await wasmMatcherForGrammar(arithmetic);
2929
m.setInput('10 + 20');
3030
let matchResult = m.match();
31-
let ast = toAST(matchResult, {
31+
let toAST = toAstWithMapping({
3232
AddExp_plus: {
3333
expr1: 0,
3434
expr2: 2,
@@ -39,15 +39,16 @@ test('toAST basic', async t => {
3939
expr2: '20',
4040
type: 'AddExp_plus',
4141
};
42-
t.deepEqual(ast, expected, 'proper AST with mapped properties');
42+
t.deepEqual(toAST(matchResult), expected, 'proper AST with mapped properties');
4343

44-
ast = toAST(matchResult, {
44+
toAST = toAstWithMapping({
4545
AddExp_plus: {
4646
expr1: 0,
4747
op: 1,
4848
expr2: 2,
4949
},
5050
});
51+
let ast = toAST(matchResult);
5152
expected = {
5253
expr1: '10',
5354
op: '+',
@@ -56,35 +57,38 @@ test('toAST basic', async t => {
5657
};
5758
t.deepEqual(ast, expected, 'proper AST with explicitly mapped property');
5859

59-
ast = toAST(matchResult, {
60+
toAST = toAstWithMapping({
6061
AddExp_plus: {
6162
0: 0,
6263
},
6364
});
65+
ast = toAST(matchResult);
6466
expected = {
6567
0: '10',
6668
type: 'AddExp_plus',
6769
};
6870
t.deepEqual(ast, expected, 'proper AST with explicitly removed property');
6971

70-
ast = toAST(matchResult, {
72+
toAST = toAstWithMapping({
7173
AddExp_plus: {
7274
0: 0,
7375
type: undefined,
7476
},
7577
});
78+
ast = toAST(matchResult);
7679
expected = {
7780
0: '10',
7881
};
7982
t.deepEqual(ast, expected, 'proper AST with explicitly removed type');
8083

81-
ast = toAST(matchResult, {
84+
toAST = toAstWithMapping({
8285
AddExp_plus: {
8386
expr1: 0,
8487
op: 'plus',
8588
expr2: 2,
8689
},
8790
});
91+
ast = toAST(matchResult);
8892
expected = {
8993
expr1: '10',
9094
op: 'plus',
@@ -93,13 +97,14 @@ test('toAST basic', async t => {
9397
};
9498
t.deepEqual(ast, expected, 'proper AST with static property');
9599

96-
ast = toAST(matchResult, {
100+
toAST = toAstWithMapping({
97101
AddExp_plus: {
98102
expr1: Object(0),
99103
op: 'plus',
100104
expr2: Object(2),
101105
},
102106
});
107+
ast = toAST(matchResult);
103108
expected = {
104109
expr1: 0,
105110
op: 'plus',
@@ -108,15 +113,16 @@ test('toAST basic', async t => {
108113
};
109114
t.deepEqual(ast, expected, 'proper AST with boxed number property');
110115

111-
ast = toAST(matchResult, {
116+
toAST = toAstWithMapping({
112117
AddExp_plus: {
113118
expr1: 0,
114119
expr2: 2,
115120
str(children) {
116-
return children.map(c => this.visit(c)).join('');
121+
return children.map(c => toAST(c)).join('');
117122
},
118123
},
119124
});
125+
ast = toAST(matchResult);
120126
expected = {
121127
expr1: '10',
122128
expr2: '20',
@@ -127,36 +133,39 @@ test('toAST basic', async t => {
127133

128134
m.setInput('10 + 20 - 30');
129135
matchResult = m.match();
130-
ast = toAST(matchResult, {
136+
toAST = toAstWithMapping({
131137
AddExp_plus: 2,
132138
});
139+
ast = toAST(matchResult);
133140
expected = {
134141
0: '20', // child 2 of AddExp_plus
135142
2: '30',
136143
type: 'AddExp_minus',
137144
};
138145
t.deepEqual(ast, expected, 'proper AST with forwarded child node');
139146

140-
ast = toAST(matchResult, {
147+
toAST = toAstWithMapping({
141148
AddExp_plus(expr1, _, expr2) {
142-
expr1 = this.visit(expr1);
143-
expr2 = this.visit(expr2);
149+
expr1 = toAST(expr1);
150+
expr2 = toAST(expr2);
144151
return 'plus(' + expr1 + ', ' + expr2 + ')';
145152
},
146153
});
154+
ast = toAST(matchResult);
147155
expected = {
148156
0: 'plus(10, 20)', // child 2 of AddExp_plus
149157
2: '30',
150158
type: 'AddExp_minus',
151159
};
152160
t.deepEqual(ast, expected, 'proper AST with computed node/operation extension');
153161

154-
ast = toAST(matchResult, {
162+
toAST = toAstWithMapping({
155163
Exp: {
156164
type: 'Exp',
157165
0: 0,
158166
},
159167
});
168+
ast = toAST(matchResult);
160169
expected = {
161170
0: {
162171
0: {

0 commit comments

Comments
 (0)