Skip to content

Commit 9f43e2d

Browse files
committed
feat: Add recoverIterOrder to extras.
1 parent f550253 commit 9f43e2d

File tree

4 files changed

+98
-1
lines changed

4 files changed

+98
-1
lines changed

packages/ohm-js/extras/index.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {MatchResult, Grammar, Semantics} from '../index.d.ts';
1+
import type {Grammar, MatchResult, Node, Semantics} from '../index.d.ts';
22

33
interface LineAndColumnInfo {
44
offset: number;
@@ -44,3 +44,9 @@ interface Example {
4444
* `//- "shouldn't match"`. The examples text is a JSON string.
4545
*/
4646
export function extractExamples(grammarsDef: string): [Example];
47+
48+
/*
49+
Given an array of nodes, return a new array where (a) all iter nodes are replaced by
50+
their children, and (b) nodes appear in the correct source order.
51+
*/
52+
export function recoverIterOrder(nodes: Node[]): Node[];

packages/ohm-js/extras/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export {getLineAndColumnMessage, getLineAndColumn} from '../src/util.js';
22
export {VisitorFamily} from './VisitorFamily.js';
33
export {semanticsForToAST, toAST} from './semantics-toAST.js';
44
export {extractExamples} from './extractExamples.js';
5+
export {recoverIterOrder} from './recoverIterOrder.js';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
To find iter nodes that are derived from the same repetition expression, we
3+
look for adjacent iter nodes that have the same source interval and the same
4+
number of children.
5+
6+
A few things to note:
7+
- The children of `*` and `+` nodes can't be nullable, so the associated iter
8+
nodes always consume some input, and therefore consecutive nodes that have
9+
the same interval must come from the same repetition expression.
10+
- We *could* mistake `a? b?` for (a b)?`, if neither of them comsume any input.
11+
However, for the purposes of this module, those two cases are equivalent
12+
anyways, since we only care about finding the correct order of the non-iter
13+
nodes.
14+
*/
15+
const isIterSibling = (refNode, n) => {
16+
return (
17+
n.isIteration() &&
18+
n.source.startIdx === refNode.source.startIdx &&
19+
n.source.endIdx === refNode.source.endIdx &&
20+
n.children.length === refNode.children.length
21+
);
22+
};
23+
24+
export function recoverIterOrder(nodes, depth = 0) {
25+
const ans = [];
26+
for (let i = 0; i < nodes.length; i++) {
27+
const n = nodes[i];
28+
if (!n.isIteration()) {
29+
ans.push(n);
30+
continue;
31+
}
32+
33+
// We found an iter node, now find its siblings.
34+
const siblings = [n];
35+
// Find the first node that's *not* part of the current list.
36+
for (let j = i + 1; j < nodes.length && isIterSibling(n, nodes[j]); j++) {
37+
siblings.push(nodes[j]);
38+
i = j;
39+
}
40+
const cousins = [];
41+
const numRows = siblings[0].children.length;
42+
for (let row = 0; row < numRows; row++) {
43+
cousins.push(...siblings.map(sib => sib.children[row]));
44+
}
45+
ans.push(...recoverIterOrder(cousins, depth + 1));
46+
}
47+
return ans;
48+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import test from 'ava';
2+
import {recoverIterOrder} from '../../extras/recoverIterOrder.js';
3+
import * as ohm from '../../index.mjs';
4+
5+
const g = ohm.grammar(String.raw`
6+
G {
7+
start = test1 | test2 | test3 | test4
8+
test1 = (a b)+ c*
9+
test2 = (a b)? c? "."
10+
test3 = ((a b)* c)+
11+
test4 = (c? (a b*)+)*
12+
a = "a"
13+
b = "b"
14+
c = "c"
15+
}
16+
`);
17+
18+
test('visitInOrder', t => {
19+
const semantics = g.createSemantics().addOperation('fix()', {
20+
start: child => child.fix(),
21+
_default(...children) {
22+
return recoverIterOrder(children)
23+
.map(c => `${c.ctorName}[${c.source.startIdx}..${c.source.endIdx}]`)
24+
.join(' ');
25+
},
26+
});
27+
28+
const sig1 = semantics(g.match('abc', 'test1')).fix();
29+
t.is(sig1, 'a[0..1] b[1..2] c[2..3]');
30+
31+
let sig2 = semantics(g.match('.', 'test2')).fix();
32+
t.is(sig2, '_terminal[0..1]');
33+
34+
sig2 = semantics(g.match('ab.', 'test2')).fix();
35+
t.is(sig2, 'a[0..1] b[1..2] _terminal[2..3]');
36+
37+
const sig3 = semantics(g.match('ababcabc', 'test3')).fix();
38+
t.is(sig3, 'a[0..1] b[1..2] a[2..3] b[3..4] c[4..5] a[5..6] b[6..7] c[7..8]');
39+
40+
const sig4 = semantics(g.match('abbabab', 'test4')).fix();
41+
t.is(sig4, 'a[0..1] b[1..2] b[2..3] a[3..4] b[4..5] a[5..6] b[6..7]');
42+
});

0 commit comments

Comments
 (0)