Skip to content

Commit 1050ad0

Browse files
committed
feat(extras): Add stored attributes
1 parent 1a88964 commit 1050ad0

File tree

4 files changed

+88
-1
lines changed

4 files changed

+88
-1
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {MatchResult, Grammar, Semantics} from 'ohm-js';
1+
import {BaseActionDict, Grammar, MatchResult, Node, Semantics} from 'ohm-js';
22

33
interface LineAndColumnInfo {
44
offset: number;
@@ -44,3 +44,17 @@ interface Example {
4444
* `//- "shouldn't match"`. The examples text is a JSON string.
4545
*/
4646
export function extractExamples(grammarsDef: string): [Example];
47+
48+
export type StoredAttributeSetter<T> = (node: Node, val: T) => T;
49+
50+
/**
51+
* Add a stored attribute named `attrName` to `semantics`. A stored attribute
52+
* is similar to a normal attribute, but instead of being lazily computed, the
53+
* value for each node is initialized by an initialization operation.
54+
*/
55+
export function addStoredAttribute<T>(
56+
semantics: Semantics,
57+
attrName: string,
58+
initSignature: string,
59+
actionProducer: (setter: StoredAttributeSetter<T>) => BaseActionDict<void>
60+
): Semantics;

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 {addStoredAttribute} from './storedAttributes.js';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Normal attributes are lazily computed. A stored attribute is initialized
3+
by a separate initialization operation. After initialization, you can read
4+
a stored attribute's value just like a normal (computed) attribute.
5+
*/
6+
export function addStoredAttribute(semantics, attrName, initSignature, fn) {
7+
semantics.addAttribute(attrName, {
8+
_default() {
9+
throw new Error(`Attribute '${attrName}' not initialized`);
10+
}
11+
});
12+
13+
// Create the attribute setter, which the semantic actions of the init
14+
// operation will close over.
15+
const key = semantics._getSemantics().attributeKeys[attrName];
16+
const setAttr = (wrapper, val) => {
17+
wrapper._node[key] = val;
18+
return val;
19+
};
20+
21+
// Create the init operation. It's the user's responsibility to call it.
22+
semantics.addOperation(initSignature, fn(setAttr));
23+
return semantics;
24+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import test from 'ava';
2+
import fs from 'node:fs';
3+
import {URL} from 'node:url';
4+
5+
import {addStoredAttribute} from '../../extras/storedAttributes.js';
6+
import * as ohm from '../../index.mjs';
7+
8+
const grammarPath = new URL('../data/arithmetic.ohm', import.meta.url);
9+
const g = ohm.grammar(fs.readFileSync(grammarPath));
10+
11+
test('stored attributes', t => {
12+
const semantics = g.createSemantics();
13+
const exp = semantics(g.match('3 + 4 - 1'));
14+
15+
// A meaningless stored attribute, where nodes have a "polarity" depending
16+
// on the type of operation they are involved in. All nodes pass their
17+
// polarity downwards, and the default action gives the child the polarity
18+
// of its parent.
19+
addStoredAttribute(semantics, 'polarity', 'initPolarity(pol)', setPolarity => ({
20+
AddExp_plus(expA, _, expB) {
21+
setPolarity(this, '+');
22+
// Note that we explicitly skip initializing the operator.
23+
expA.initPolarity(this.polarity);
24+
expB.initPolarity(this.polarity);
25+
},
26+
AddExp_minus(expA, _, expB) {
27+
setPolarity(this, '-');
28+
// Note that we explicitly skip initializing the operator.
29+
expA.initPolarity(this.polarity);
30+
expB.initPolarity(this.polarity);
31+
},
32+
_default(...children) {
33+
// By default, inherit polarity from the parent node.
34+
setPolarity(this, this.args.pol);
35+
children.forEach(c => c.initPolarity(this.args.pol));
36+
}
37+
}));
38+
exp.initPolarity('=');
39+
t.is(exp.polarity, '='); // Exp
40+
t.is(exp.child(0).polarity, '='); // AddExp
41+
t.is(exp.child(0).child(0).polarity, '-'); // AddExp_minus
42+
t.is(exp.child(0).child(0).child(0).polarity, '-'); // AddExp
43+
t.is(exp.child(0).child(0).child(0).child(0).polarity, '+'); // AddExp_plus
44+
45+
t.throws(() => exp.child(0).child(0).child(1).polarity, {
46+
message: 'Attribute \'polarity\' not initialized'
47+
});
48+
});

0 commit comments

Comments
 (0)