Skip to content

Commit

Permalink
(1) brought back the ability to write semantic actions for _iter node…
Browse files Browse the repository at this point in the history
…s. (2) added default behaviors for _iter and _terminal nodes, that way most people will never have to worry about them. (3) added default implementation of _default action that does what ohm.actions.passThrough used to do. (4) got rid of the default semantic actions (ohm.actions) because now that's what you get automatically.
  • Loading branch information
alexwarth committed Jun 3, 2015
1 parent d86d838 commit 45972f9
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 265 deletions.
149 changes: 91 additions & 58 deletions dist/ohm.js

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions dist/ohm.min.js

Large diffs are not rendered by default.

31 changes: 25 additions & 6 deletions examples/math/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,24 @@
PriExp_neg: function(_, e) { return ['neg', e.asLisp]; },
ident: function(_, _) { return this.interval.contents; },
number: function(_) { return this.interval.contents; },
_default: ohm.actions.passThrough // TODO: Explain _default.

/*
You can optionally provide a _default semantic action that will be invoked when the action
dictionary does not have a method that corresponds to the type of a CST node. The receiver
(`this`) of the _default method is that CST node, _default's only argument is an array that
contains the children of that node.
*/
_default: function(children) {
if (children.length === 1) {
// If this node only has one child, just return the Lisp tree of its child. This lets us avoid
// writing semantic actions for the Exp, AddExp, MulExp, ExpExp, and PriExp rules.
return children[0].asLisp;
} else {
// Hmm, this probably means that we forgot to write a semantic action for one of the rules.
// We'll throw an exception so we'll know that it's missing.
throw new Error("Uh-oh, missing semantic action for " + this.constructor);
}
}
});

/*
Expand Down Expand Up @@ -363,8 +380,7 @@
PriExp_pos: function(sign, e) { return elt('pos', sign.toTree(), e.toTree()); },
PriExp_neg: function(sign, e) { return elt('neg', sign.toTree(), e.toTree()); },
ident: function(_, _) { return elt('ident', this.interval.contents); },
number: function(_) { return elt('number', this.interval.contents); },
_terminal: ohm.actions.getPrimitiveValue // TODO: explain _terminal, ohm...getPrimitiveValue
number: function(_) { return elt('number', this.interval.contents); }
});

// -------------------------------------------------------------------------------------------------
Expand All @@ -387,9 +403,12 @@
PriExp_neg: function(sign, e) { return elt('neg', sign.toTwoD(), e.toTwoD()); },
ident: function(_, _) { var text = this.interval.contents;
return elt('ident', text === 'pi' ? '\u03C0' : text); },
number: function(_) { return elt('number', this.interval.contents); },
_terminal: ohm.actions.getPrimitiveValue,
_default: ohm.actions.passThrough
number: function(_) { return elt('number', this.interval.contents); }

// Remember the _default semantic action that we wrote for the `interpret` operation above? Turns
// out that's so convenient that the Ohm people decided to give it to you "for free", i.e., if you
// don't write a _default semantic action, you get that one automatically. Of course you're free
// to override it if that's not what you want, but in this case, it's exactly what we want.
});
</script>
</head>
Expand Down
3 changes: 1 addition & 2 deletions examples/viz/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,7 @@
keyword_false: function(_) {
add('prim', 'false');
},
Prop: function(n, _, p) {},
_terminal: ohm.actions.getPrimitiveValue
Prop: function(n, _, p) {}
});

s.addOperation('vizChoice', {
Expand Down
10 changes: 8 additions & 2 deletions src/Grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ Grammar.prototype = {
_terminal: function() {
this._node.parent = stack[stack.length - 1];
},
_iter: function(children) {
stack.push(this._node);
children.forEach(function(child) { child.setParents(); });
stack.pop();
this._node.parent = stack[stack.length - 1];
},
_default: function(children) {
stack.push(this._node);
children.forEach(function(child) { child.setParents(); });
Expand Down Expand Up @@ -123,7 +129,7 @@ Grammar.prototype = {
// a function of the correct arity. If not, throw an exception.
_checkTopDownActionDict: function(what, name, actionDict) {
function isSpecialAction(name) {
return name === '_terminal' || name === '_default';
return name === '_terminal' || name === '_iter' || name === '_default';
}

var problems = [];
Expand Down Expand Up @@ -157,7 +163,7 @@ Grammar.prototype = {
// Return the expected arity for a semantic action named `actionName`, which
// is either a rule name or a special action name like '_default'.
_topDownActionArity: function(actionName) {
if (actionName === '_default') {
if (actionName === '_default' || actionName === '_iter') {
return 1;
} else if (actionName === '_terminal') {
return 0;
Expand Down
77 changes: 38 additions & 39 deletions src/Semantics.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,37 @@ Semantics.prototype.addOperationOrAttribute = function(type, name, actionDict) {
var Ctor = type === 'operation' ? Operation : Attribute;

this.assertNewName(name, type);
this[typePlural][name] = new Ctor(name, actionDict);

// Create the action dictionary for this operation / attribute. We begin by defining the default
// behavior of terminal and iteration nodes, and add a '_default' action that throws an error if
// a rule in the grammar doesn't have a corresponding action in this operation / attribute...
var realActionDict = {
_terminal: function() {
return this.primitiveValue;
},
_iter: function(children) {
// This CST node corresponds to an iteration expression in the grammar (*, +, or ?). The
// default behavior is to map this operation or attribute over all of its child nodes.
var thisSemantics = this._semantics;
var thisThing = thisSemantics[typePlural][name];
return children.map(function(child) { return thisThing.execute(thisSemantics, child); });
},
_default: function(children) {
if (children.length === 1) {
var thisSemantics = this._semantics;
var thisThing = thisSemantics[typePlural][name];
return thisThing.execute(thisSemantics, children[0]);
}
throw new Error('missing semantic action for ' + this.ctorName + ' in ' + name + ' ' + type);
}
};
// ... and add in the actions supplied by the programmer, which may override some or all of the
// default ones.
Object.keys(actionDict).forEach(function(name) {
realActionDict[name] = actionDict[name];
});

this[typePlural][name] = new Ctor(name, realActionDict);

// The following check is not strictly necessary (it will happen later anyway) but it's better to
// catch errors early.
Expand Down Expand Up @@ -310,19 +340,6 @@ Semantics.createSemantics = function(grammar, optSuperSemantics) {
return proxy;
};

// ----------------- Default semantic actions -----------------

var actions = {
getPrimitiveValue: function() {
return this.primitiveValue;
},
passThrough: function(childNode) {
throw new Error('BUG: ohm.actions.passThrough should never be called');
}
};

Semantics.actions = actions;

// ----------------- Operation -----------------

// An Operation represents a function to be applied to a concrete syntax tree (CST) -- it's very
Expand All @@ -344,44 +361,26 @@ Operation.prototype.checkActionDict = function(grammar) {
// Execute this operation on the CST node associated with `nodeWrapper` in the context of the given
// Semantics instance.
Operation.prototype.execute = function(semantics, nodeWrapper) {
if (nodeWrapper.isIteration()) {
// This CST node corresponds to an iteration expression in the grammar (*, +, or ?), so we map
// this operation over all of its child nodes.
var results = [];
for (var idx = 0; idx < nodeWrapper._node.numChildren(); idx++) {
results.push(this.execute(semantics, nodeWrapper.child(idx)));
}
return results;
}

// Look for a semantic action whose name matches the node's constructor name, which is either the
// name of a rule in the grammar, or the special value '_terminal'.
// name of a rule in the grammar, or '_terminal' (for a terminal node), or '_iter' (for an
// iteration node). In the latter case, the action function receives a single argument, which is
// an array containing all of the children of the CST node.
var actionFn = this.actionDict[nodeWrapper._node.ctorName];
if (actionFn) {
return this.doAction(semantics, nodeWrapper, actionFn);
return this.doAction(semantics, nodeWrapper, actionFn, nodeWrapper.isIteration());
}

// The action dictionary does not contain a semantic action for this specific type of node, so
// invoke the '_default' semantic action if it exists (but only if this node is not a terminal).
var defaultActionFn = this.actionDict._default;
if (defaultActionFn && !nodeWrapper.isTerminal()) {
return this.doAction(semantics, nodeWrapper, defaultActionFn, true);
}

// The programmer hasn't written a semantic action for this type of node yet.
throw new Error(
'missing semantic action for ' + nodeWrapper._node.ctorName + ' in ' + this.name + ' ' +
this.typeName);
// invoke the '_default' semantic action. The built-in implementation of the `_default` semantic
// action just throws an error, but the programmer may have overridden it.
return this.doAction(semantics, nodeWrapper, this.actionDict._default, true);
};

// Invoke `actionFn` on the CST node that corresponds to `nodeWrapper`, in the context of
// `semantics`. If `optPassChildrenAsArray` is true, `actionFn` will be called with a single
// argument, which is an array of wrappers. Otherwise, the number of arguments to `actionFn` will
// be equal to the number of children in the CST node.
Operation.prototype.doAction = function(semantics, nodeWrapper, actionFn, optPassChildrenAsArray) {
if (actionFn === actions.passThrough) {
return this.execute(semantics, nodeWrapper._onlyChild());
}
return optPassChildrenAsArray ?
actionFn.call(nodeWrapper, nodeWrapper._children()) :
actionFn.apply(nodeWrapper, nodeWrapper._children());
Expand Down
7 changes: 1 addition & 6 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
var Builder = require('./Builder');
var Grammar = require('./Grammar');
var Namespace = require('./Namespace');
var Semantics = require('./Semantics');
var UnicodeCategories = require('../third_party/unicode').UnicodeCategories;
var common = require('./common');
var errors = require('./errors');
Expand Down Expand Up @@ -275,10 +274,7 @@ function buildGrammar(match, namespace, optOhmGrammarForTesting) {
},
ListOf_none: function() {
return [];
},

_terminal: Semantics.actions.getPrimitiveValue,
_default: Semantics.actions.passThrough
}
});
return helpers(match).visit();
}
Expand Down Expand Up @@ -379,7 +375,6 @@ function makeRecipe(recipeFn) {
// Stuff that users should know about

module.exports = {
actions: Semantics.actions,
createNamespace: Namespace.createNamespace,
error: errors,
grammar: grammar,
Expand Down
Loading

0 comments on commit 45972f9

Please sign in to comment.