Advanced SISR Results
This article assumes you are comfortable with the fundamentals covered in Intro to Semantic Interpretation — what SISR is, and how tags embed ECMAScript in a grammar. Here we go further, using moderately advanced ECMAScript to shape the results a grammar returns.
Using ECMAScript Within Tags
Tags are how semantic interpretation is embedded in a grammar, and a tag almost always contains ECMAScript. The recognizer executes the script in a tag as soon as the portion of the rule to its left is matched.
Because ECMAScript (sometimes called JavaScript) is a full scripting language, it can do far more than assign a literal value: it can build structured results, declare and reuse variables, and define functions. You will not need that power in every grammar, but when result handling gets complex, it collapses a great deal of branching into a few lines. The rest of this article works through that kind of case.
Important Reminders
Like any language, ECMAScript inside a grammar has syntax rules that are easy to get wrong — often in ways the recognizer will not catch until run time. A few are worth calling out before the example.
Whitespace
Whitespace — carriage returns, line feeds, spaces, tabs — is ignored when the recognizer processes ECMAScript. That leaves you free to format tags across multiple lines, with indentation that makes them readable and maintainable. For instance, this:
| $my_rule=test{out="some string";my_counter++;out.counter=my_counter}; |
is processed identically to this:
| $my_rule = test { out = "some string"; my_counter++; out.counter = my_counter }; |
The second form is far easier to read and maintain, but to the ASR the two are exactly the same.
Curly Brackets
ECMAScript also reserves the curly brace for its own blocks — conditionals, function bodies, and so on — which collides with the braces that delimit an SISR tag. When a tag’s code needs braces of its own, delimit the tag with the three-character sequences {!{ to open and }!} to close.
A simple tag with no internal braces can use a single pair:
| $my_rule = test { out = "some string"; }; |
But if your code uses a brace of its own — here, an if/else — wrap the tag in the three-character sequences:
| $my_rule = test {!{ if(out == "") { out = "some string"; } else { out = "other string"; } }!}; |
Using a single brace in that second case produces an SISR parsing error. These errors are easy to miss: they surface only after a decode, so nothing flags them while the grammar is loaded or compiled.
Variable Declaration
A common misconception concerns variables. The out variable is predefined, so a grammar never declares it — which leads many authors to assume the same of every other variable. It is not true, and the result is undefined-variable errors that are, again, hard to detect.
ECMAScript is loosely typed: a single variable can hold a string, an integer, a floating-point value, an array, an object, or other types:
| $my_rule = test {!{ out = "string"; // out is assigned a 'string' type out = 123; // out is assigned an 'integer' type out = 123.45; // out is assigned a 'floating point' type out = new Array('one', 'two', 'three'); // an 'array' type }!}; |
Any variable other than out must be declared before use, and given a type where it matters:
| $my_rule = test {!{ var res = "string"; // a new variable named res is declared // and assigned a 'string' type. out = res; // assigned the value of the res variable }!}; |
Note the var keyword declaring res; without it, the reference is a processing error. Assigning "string" does two things — it gives res a value and, more importantly, initializes it as a string, so the ECMAScript processor treats it as a string from then on. A variable with no clear type produces undefined behavior: had res never been assigned, copying it into out would return an undefined SISR result.
Functions
An often-overlooked capability is the ability to define and call functions. A function can process variables, perform on-the-fly checks, or simply factor out logic that would otherwise be repeated across several tags.
The following grammar defines a function and calls it each time the $option rule matches:
ABNF format:
| #ABNF 1.0; language en-US; mode voice; tag-format <semantics/1.0.2006>; root $rootrule; {!{ // This function appends items to the specified // array object, then returns the array object function add_to_array(array_to_fill, what_to_add) { array_to_fill.push(what_to_add); return array_to_fill; }; }!}; $option = cat | mouse | dog; $rootrule = {out = new Array();} ($option { out = add_to_array(out, rules.latest()); } )<1-3>; |
The function keyword declares add_to_array, which pushes what_to_add onto array_to_fill and returns the array. Note the tag at the start of $rootrule: it initializes out as an Array. That step is essential — the tag after $option treats out as an array, and without the initialization it would error.
The equivalent grammar in GrXML defines the same tags:
GrXML format:
| <?xml version="1.0" encoding="UTF-8" ?> <grammar xmlns="http://www.w3.org/2001/06/grammar" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.w3.org/2001/06/grammar http://www.w3.org/TR/speech-grammar/grammar.xsd" xml:lang="en-US" version="1.0" root="rootrule" mode="voice" tag-format="semantics/1.0.2006"> <tag> // This function appends items to the specified // array object, then returns the array object function add_to_array(array_to_fill, what_to_add) { array_to_fill.push(what_to_add); return array_to_fill; }; </tag> <rule id="option"> <one-of> <item>cat</item> <item>mouse</item> <item>dog</item> </one-of> </rule> <rule id="rootrule"> <tag>out = new Array();</tag> <item repeat="1-3"> <ruleref uri="#option" /> <tag> out = add_to_array(out, rules.latest()); </tag> </item> </rule> </grammar> |
Saying "cat dog" against either grammar returns an array of the animals spoken:
| <item index="0">cat</item><item index="1">dog</item> |
An Example Grammar
With those techniques in hand, consider a concrete requirement that needs real logic. You want the caller to be able to say any combination of up to three of "cat", "dog", or "mouse", in any order. With optional words and a repeat operator, that much is straightforward.
Now add a constraint: no word may be counted twice. You could enumerate every allowed combination — there would be many — or you could let ECMAScript do the work and keep the grammar compact:
ABNF:
| #ABNF 1.0 UTF-8; language en-US; mode voice; tag-format <semantics/1.0.2006>; root $rootrule; $animal = cat | mouse | dog; $animals = { // This is processed before the animals rule and is used // to declare and initializes the variables out.selections = ''; out.num_animals = 0; out.res_array = new Array(); out.skipped = 0; } ($animal // This next section is processed once per result from $animal {!{ if(out.selections.indexOf(rules.latest()) == -1) { if(out.selections != '') out.selections += ' '; // add spacing between results out.selections+=rules.latest(); out.res_array.push(rules.latest()); out.num_animals++; } else { out.skipped++; } }!} )<1-3>; $rootrule = $animals; |
GrXML:
| <?xml version="1.0" encoding="UTF-8" ?> <grammar xmlns="http://www.w3.org/2001/06/grammar" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.w3.org/2001/06/grammar http://www.w3.org/TR/speech-grammar/grammar.xsd" xml:lang="en-US" version="1.0" root="rootrule" mode="voice" tag-format="semantics/1.0.2006"> <rule id="animal"> <one-of> <item>cat</item> <item>mouse</item> <item>dog</item> </one-of> </rule> <rule id="rootrule"> <tag> // This is processed before the rootrule is used // to declare and initialize the variables out.selections = ''; out.num_animals = 0; out.res_array = new Array(); out.skipped = 0; </tag> <item repeat="1-3"> <ruleref uri="#animal" /> <tag> // This next section is processed once per result from $animal if(out.selections.indexOf(rules.latest()) == -1) { if(out.selections != '') out.selections += ' '; // add spacing between results out.selections+=rules.latest(); out.res_array.push(rules.latest()); out.num_animals++; } else { out.skipped++; } </tag> </item> </rule> </grammar> |
The built-in indexOf() method checks whether an animal has already been seen, preventing duplicates; the if statement then branches on the result.
For a new (non-duplicate) animal, the code appends a separating space to out.selections when it is non-empty, adds the animal, pushes it onto out.res_array, and increments out.num_animals. For a duplicate, it simply increments out.skipped.
All of these out sub-variables appear in the interpretation result. Note again the tag that runs before the repeated rule, which declares and initializes the variables — the step it is easiest to forget.
For both grammars, saying "cat dog" returns:
| <num_animals>2</num_animals><res_array length="2"><item index="0">cat</item><item index="1">dog</item></res_array><selections>cat dog</selections><skipped>0</skipped> |
which carries several parameters:
- num_animals — the number of unique animals spoken
- res_array — an array with one item per unique animal
- selections — a string of the unique animal names, space-separated
- skipped — the number of skipped (duplicate) animals
Saying a repeated animal, such as "cat dog cat", gives a similar result, except skipped is 1 rather than 0 — flagging the second "cat" as a duplicate:
| <num_animals>2</num_animals><res_array length="2"><item index="0">cat</item><item index="1">dog</item></res_array><selections>cat dog</selections><skipped>1</skipped> |
Conclusion
Combined, these techniques let you compute structured, deduplicated, application-ready results inside the grammar itself — turning what would otherwise be substantial post-processing code into a handful of tags.
