Skip to main content
Module

x/ohm_js/test/test-ohm-syntax.js

A library and language for building parsers, interpreters, compilers, etc.
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446
'use strict';
const test = require('ava-spec');
const fs = require('fs');const ohm = require('..');
const arithmeticGrammarSource = fs.readFileSync('test/arithmetic.ohm').toString();const ohmGrammarSource = fs.readFileSync('src/ohm-grammar.ohm').toString();
const {describe} = test;
// --------------------------------------------------------------------// Helpers// --------------------------------------------------------------------
function compareGrammars(t, expected, actual) { // The other property on grammars is "constructors", which contains // closures which cause spurious test failures if we compare // them. So we ignore that property here, concentrating on `rules` // and other "real" properties of each grammar.
t.is(typeof actual, typeof expected); // ^ e.g. when one is undefined and the other isn't
if (expected && actual) { compareGrammars(t, expected.superGrammar, actual.superGrammar); // In the list below, we exclude superGrammar (just tested above) // and constructors (for reasons given above). ['namespaceName', 'name', 'ruleDecls', 'rules'].forEach(prop => { t.deepEqual(actual[prop], expected[prop]); }); }}
function buildTreeNodeWithUniqueId(g) { let nextId = 0; const s = g.createSemantics().addAttribute('tree', { _iter(...children) { return children.map(c => c.tree); }, _nonterminal(...children) { return ['id', nextId++, this.ctorName].concat(children.map(child => child.tree)); }, _terminal() { return this.sourceString; }, });
function makeTree(node) { return s(node).tree; } makeTree._getNextId = function() { return nextId; }; return makeTree;}
function assertSucceeds(t, matchResult, optMessage) { t.is(matchResult.succeeded(), true, optMessage); t.is(matchResult.failed(), false, optMessage);}
function assertFails(t, matchResult, optMessage) { t.is(matchResult.succeeded(), false, optMessage); t.is(matchResult.failed(), true, optMessage);}
// --------------------------------------------------------------------// Tests// --------------------------------------------------------------------
test('char', t => { const m = ohm.grammar('M { bang = "!" }'); const s = m.createSemantics().addAttribute('v', { _terminal() { return this.sourceString; }, });
assertSucceeds(t, m.match('!')); assertFails(t, m.match('!a')); assertFails(t, m.match('')); const cst = m.match('!'); t.is(s(cst).v, '!');});
test('string', t => { const m = ohm.grammar('M { foo = "foo\\b\\n\\r\\t\\\\\\"\\u01bcff\\x8f" }'); const s = m.createSemantics().addAttribute('v', { _terminal() { return this.sourceString; }, });
assertSucceeds(t, m.match('foo\b\n\r\t\\"\u01bcff\x8f')); assertFails(t, m.match('foo1')); assertFails(t, m.match('bar'));
const cst = m.match('foo\b\n\r\t\\"\u01bcff\x8f'); t.is(s(cst).v, 'foo\b\n\r\t\\"\u01bcff\x8f');
t.throws( () => { ohm.grammar('G { r = "\\w" }'); }, {message: /Expected "\\""/}, 'unrecognized escape characters are parse errors' );});
test('unicode code point escapes', t => { assertSucceeds( t, ohm.grammar(String.raw`G { start = "\u{78}\u{78}" }`).match('\u{78}\u{78}') ); assertSucceeds(t, ohm.grammar(String.raw`G { start = "\u{1F920}" }`).match('🤠')); assertSucceeds(t, ohm.grammar(String.raw`G { start = "🤠" }`).match('🤠')); assertSucceeds(t, ohm.grammar(String.raw`G { a = "😬" b="🤠" }`).match('🤠', 'b'));
// More than 6 hex digits is just a parse error. (We'd like to make this nicer.) t.throws(() => ohm.grammar(String.raw`G { start = "\u{0000000} }`), { message: /Expected "\\"" or not "\\\\"/, });
t.throws(() => ohm.grammar('G { start = "\\u{FFFFFF}" }'), { message: /U\+FFFFFF is not a valid Unicode code point/, });});
describe('unicode', test => { const m = ohm.grammar('M {}');
test('recognition', t => { assertSucceeds(t, m.match('a', 'lower')); assertSucceeds(t, m.match('\u00E9', 'lower'), 'small letter e with acute'); assertSucceeds(t, m.match('\u03C9', 'lower'), 'Greek small letter Omega'); assertFails(t, m.match('`', 'lower')); assertFails(t, m.match('\u20AC', 'lower'), 'Euro sign'); assertFails(t, m.match('\u01C0', 'lower'), 'Latin letter dental click');
assertSucceeds(t, m.match('Z', 'upper')); assertSucceeds(t, m.match('\u03A9', 'upper'), 'Greek capital letter Omega'); assertFails(t, m.match('[', 'upper')); assertFails(t, m.match('\u20AC', 'upper'), 'Euro sign'); assertFails(t, m.match('\u01C0', 'upper'), 'Latin letter dental click');
assertSucceeds(t, m.match('\u01C0', 'letter'), 'dental click is a letter'); assertSucceeds(t, m.match(['\u01C0'], 'letter'), 'dental click in a list'); });
test('semantic actions', t => { const s = m.createSemantics().addAttribute('v', { _terminal() { return this.sourceString + this.sourceString; }, }); const r = m.match('\u01C0', 'letter'); t.is(s(r).v, '\u01C0\u01C0'); });});
test('ranges', t => { const m = ohm.grammar('M { charRange = "0".."9" }'); const s = m.createSemantics().addAttribute('v', { _terminal() { return this.sourceString; }, });
assertSucceeds(t, m.match('6', 'charRange')); assertFails(t, m.match('x', 'charRange')); t.is(s(m.match('4', 'charRange')).v, '4');
t.throws( () => { ohm.grammar('M { charRange = "ab".."c" }'); }, {message: /Expected "}"/}, 'from-terminal must have length 1' ); t.throws( () => { ohm.grammar('M { charRange = "ab".."cd" }'); }, {message: /Expected "}"/}, 'from-terminal must have length 1' ); t.throws( () => { ohm.grammar('M { charRange = "a".."bc" }'); }, {message: /Expected "\\""/}, 'to-terminal must have length 1' );});
test('ranges w/ code points > 0xFFFF', t => { const g = ohm.grammar(` G { face = "😇".."😈" notFace = ~face any } `);
// Every emoji by code point: https://emojipedia.org/emoji/ assertFails(t, g.match('😆')); // just below assertSucceeds(t, g.match('😇')); assertSucceeds(t, g.match('😈')); assertFails(t, g.match('😉')); // just above
assertSucceeds(t, g.match('x', 'notFace'));
const valActions = { _terminal() { return this.sourceString; }, };
const s = g.createSemantics().addAttribute('val', valActions); t.is(s(g.match('😈')).val, '😈');
// Test the same thing, but using Unicode code point escapes. const g2 = ohm.grammar(String.raw`G { face = "\u{1F607}".."\u{1F608}" }`); assertFails(t, g2.match('😆')); // just below assertSucceeds(t, g2.match('😇')); assertSucceeds(t, g2.match('😈')); assertFails(t, g2.match('😉')); // just above
const s2 = g2.createSemantics().addAttribute('val', valActions); t.is(s2(g2.match('😈')).val, '😈');});
test('ranges w/ code points > 0xFFFF, special cases', t => { // "Peace hand sign" is two code points, so this should fail. t.throws(() => ohm.grammar('G { start = "✌️".."✌️" }'));
const valActions = { _terminal() { return this.sourceString; }, };
const g = ohm.grammar('G { face = "\u{0}".."\u{1F608}" }'); assertSucceeds(t, g.match('😇')); const s = g.createSemantics().addAttribute('val', valActions); t.is(s(g.match('😈')).val, '😈');
const g2 = ohm.grammar(String.raw` G { start = "\u{1F603}".."\u{1F603}" | "\uD800".."\uFFFF" "x" -- fallback } `); // Try matching against a string where the first unit is a high surrogate, // but the second unit is *not* a low surrogate. assertSucceeds(t, g2.match('\u{D83D}x'));});
describe('alt', test => { const m = ohm.grammar('M { altTest = "a" | "b" }'); const s = m.createSemantics().addAttribute('v', { _terminal() { return this.sourceString; }, });
test('recognition', t => { assertFails(t, m.match('')); assertSucceeds(t, m.match('a')); assertSucceeds(t, m.match('b')); assertFails(t, m.match('ab')); });
test('semantic actions', t => { t.is(s(m.match('a')).v, 'a'); t.is(s(m.match('b')).v, 'b'); });});
describe("rule bodies in defs can start with a |, and it's a no-op", test => { const m = ohm.grammar('M { altTest = | "a" | "b" }'); const s = m.createSemantics().addAttribute('v', { _terminal() { return this.sourceString; }, });
test('recognition', t => { assertFails(t, m.match('')); assertSucceeds(t, m.match('a')); assertSucceeds(t, m.match('b')); assertFails(t, m.match('ab')); });
test('semantic actions', t => { t.is(s(m.match('a')).v, 'a'); t.is(s(m.match('b')).v, 'b'); });});
describe("rule bodies in overrides can start with a |, and it's a no-op", test => { const m = ohm.grammar('M { space := | "a" | "b" }'); const s = m.createSemantics().addAttribute('v', { _terminal() { return this.sourceString; }, });
test('recognition', t => { assertFails(t, m.match('', 'space')); assertSucceeds(t, m.match('a', 'space')); assertSucceeds(t, m.match('b', 'space')); assertFails(t, m.match(' ', 'space')); assertFails(t, m.match('\t', 'space')); });
test('semantic actions', t => { t.is(s(m.match('a', 'space')).v, 'a'); t.is(s(m.match('b', 'space')).v, 'b'); });});
describe("rule bodies in extends can start with a |, and it's a no-op", test => { const m = ohm.grammar('M { space += | "a" | "b" }'); const s = m.createSemantics().addAttribute('v', { _terminal() { return this.sourceString; }, });
test('recognition', t => { assertFails(t, m.match('', 'space')); assertSucceeds(t, m.match('a', 'space')); assertSucceeds(t, m.match('b', 'space')); assertSucceeds(t, m.match(' ', 'space')); assertSucceeds(t, m.match('\t', 'space')); });
test('semantic actions', t => { t.is(s(m.match('a', 'space')).v, 'a'); t.is(s(m.match('b', 'space')).v, 'b'); });});
describe('seq', test => { const m = ohm.grammar('M { start = "a" "bc" "z" }'); test('recognition', t => { assertFails(t, m.match('a')); assertFails(t, m.match('bc')); assertSucceeds(t, m.match('abcz')); assertFails(t, m.match('abbz')); });
test('semantic actions', t => { const f = m.match('abcz'); const s = m.createSemantics().addAttribute('v', { start(x, y, z) { return [x.sourceString, y.sourceString, z.sourceString]; }, }); t.deepEqual(s(f).v, ['a', 'bc', 'z']); });});
describe('alts and seqs together', test => { const m = ohm.grammar('M { start = "a" "b" "c" | "1" "2" "3" }');
test('recognition', t => { assertFails(t, m.match('ab')); assertFails(t, m.match('12')); assertSucceeds(t, m.match('abc')); assertSucceeds(t, m.match('123')); });
test('semantic actions', t => { const s = m.createSemantics().addAttribute('v', { start(x, _, y) { return [x.sourceString, y.sourceString]; }, }); t.deepEqual(s(m.match('abc')).v, ['a', 'c']); t.deepEqual(s(m.match('123')).v, ['1', '3']); });});
describe('kleene-* and kleene-+', test => { const m = ohm.grammar(` M { number = digit+ digits = digit* sss = &number number } `);
test('recognition', t => { assertFails(t, m.match('1234a', 'number')); assertSucceeds(t, m.match('1234', 'number')); assertSucceeds(t, m.match('5', 'number')); assertFails(t, m.match('', 'number'));
assertFails(t, m.match('1234a', 'digits')); assertSucceeds(t, m.match('1234', 'digits')); assertSucceeds(t, m.match('5', 'digits')); assertSucceeds(t, m.match('', 'digits')); });
test('semantic actions', t => { const s = m.createSemantics().addAttribute('v', { number(digits) { return ['digits', digits.children.map(c => c.v)]; }, digit(expr) { return ['digit', expr.v]; }, _terminal() { return this.sourceString; }, }); t.deepEqual(s(m.match('1234', 'number')).v, [ 'digits', [ ['digit', '1'], ['digit', '2'], ['digit', '3'], ['digit', '4'], ], ]); });
test('semantic actions are evaluated lazily', t => { const a = buildTreeNodeWithUniqueId(m); const tree = [ 'id', 1, 'number', [ ['id', 2, 'digit', '1'], ['id', 3, 'digit', '2'], ['id', 4, 'digit', '3'], ], ]; t.deepEqual(a(m.match('123', 'sss')), ['id', 0, 'sss', tree, tree]); t.is(a._getNextId(), 5); });});
describe('opt', test => { const m = ohm.grammar('M { name = "dr"? "warth" }');
test('recognition', t => { assertSucceeds(t, m.match('drwarth')); assertSucceeds(t, m.match('warth')); assertFails(t, m.match('mrwarth')); });
test('semantic actions', t => { const s = m.createSemantics().addAttribute('v', { name(title, last) { return [title.children.map(c => c.v)[0], last.sourceString]; }, _terminal() { return this.sourceString; }, }); t.deepEqual(s(m.match('drwarth')).v, ['dr', 'warth']); t.deepEqual(s(m.match('warth')).v, [undefined, 'warth']); });});
describe('not', test => { const m = ohm.grammar('M { start = ~"hello" any* }');
test('recognition', t => { assertSucceeds(t, m.match('yello world')); assertFails(t, m.match('hello world')); });
test('semantic actions', t => { const s = m.createSemantics().addAttribute('v', { start(x) { return x.sourceString; }, }); t.is(s(m.match('yello world')).v, 'yello world'); });});
describe('lookahead', test => { const m = ohm.grammar('M { start = &"hello" any* }');
test('recognition', t => { assertSucceeds(t, m.match('hello world')); assertFails(t, m.match('hell! world')); });
test('semantic actions', t => { const s = m.createSemantics().addAttribute('v', { start(x, _) { return x.sourceString; }, }); t.is(s(m.match('hello world')).v, 'hello'); });});
describe('simple left recursion', test => { const m = ohm.grammar(` M { number = numberRec | digit numberRec = number digit } `);
test('recognition', t => { assertFails(t, m.match('', 'number')); assertFails(t, m.match('a', 'number')); assertSucceeds(t, m.match('1', 'number')); assertSucceeds(t, m.match('12', 'number')); assertSucceeds(t, m.match('123', 'number')); assertSucceeds(t, m.match('7276218173', 'number')); });
test('semantic actions', t => { const f = m.match('1234', 'number'); const s = m .createSemantics() .addAttribute('v', { numberRec(n, d) { return n.v * 10 + d.v; }, digit(expr) { return expr.v.charCodeAt(0) - '0'.charCodeAt(0); }, _terminal() { return this.sourceString; }, }) .addAttribute('t', { number(expr) { return ['number', expr.t]; }, numberRec(n, d) { return ['numberRec', n.t, d.t]; }, _terminal() { return this.sourceString; }, }); t.is(s(f).v, 1234); t.deepEqual(s(f).t, [ 'number', [ 'numberRec', ['number', ['numberRec', ['number', ['numberRec', ['number', '1'], '2']], '3']], '4', ], ]); });
describe('simple left recursion, with non-involved rules', test => { const m = ohm.grammar(` M { add = addRec | pri addRec = add "+" pri pri = priX | priY priX = "x" priY = "y" } `);
test('recognition', t => { assertSucceeds(t, m.match('x+y+x', 'add')); });
test('semantic actions', t => { const s = m.createSemantics().addAttribute('v', { addRec(x, _, y) { return [x.v, '+', y.v]; }, _terminal() { return this.sourceString; }, }); t.deepEqual(s(m.match('x+y+x', 'add')).v, [['x', '+', 'y'], '+', 'x']); }); });
describe('indirect left recursion', test => { const m = ohm.grammar(` M { number = foo | digit foo = bar bar = baz baz = qux qux = quux quux = numberRec numberRec = number digit } `);
test('recognition', t => { assertFails(t, m.match('', 'number')); assertFails(t, m.match('a', 'number')); assertSucceeds(t, m.match('1', 'number')); assertSucceeds(t, m.match('123', 'number')); assertSucceeds(t, m.match('7276218173', 'number')); });
test('semantic actions', t => { const s = m.createSemantics().addAttribute('v', { numberRec(n, d) { return [n.v, d.v]; }, _terminal() { return this.sourceString; }, }); t.deepEqual(s(m.match('1234', 'number')).v, [[['1', '2'], '3'], '4']); }); });
describe('nested left recursion', test => { const m = ohm.grammar(` M { addExp = addExpRec | mulExp addExpRec = addExp "+" mulExp mulExp = mulExpRec | priExp mulExpRec = mulExp "*" priExp priExp = "0".."9" sss = &addExp addExp } `);
test('recognition', t => { assertSucceeds(t, m.match('1')); assertSucceeds(t, m.match('2+3')); assertFails(t, m.match('4+')); assertSucceeds(t, m.match('5*6')); assertSucceeds(t, m.match('7*8+9+0')); });
test('semantic actions', t => { const f = m.match('1*2+3+4*5'); const s = m .createSemantics() .addAttribute('t', { addExp(expr) { return ['addExp', expr.t]; }, addExpRec(x, _, y) { return ['addExpRec', x.t, y.t]; }, mulExp(expr) { return ['mulExp', expr.t]; }, mulExpRec(x, _, y) { return ['mulExpRec', x.t, y.t]; }, _terminal() { return this.sourceString; }, }) .addAttribute('v', { addExp(expr) { return expr.v; }, addExpRec(x, _, y) { return x.v + y.v; }, mulExp(expr) { return expr.v; }, mulExpRec(x, _, y) { return x.v * y.v; }, priExp(expr) { return parseInt(expr.v); }, _terminal() { return this.sourceString; }, }) .addAttribute('p', { addExpRec(x, _, y) { return '(' + x.p + '+' + y.p + ')'; }, mulExpRec(x, _, y) { return '(' + x.p + '*' + y.p + ')'; }, _terminal() { return this.sourceString; }, }); t.deepEqual(s(f).t, [ 'addExp', [ 'addExpRec', [ 'addExp', [ 'addExpRec', ['addExp', ['mulExp', ['mulExpRec', ['mulExp', '1'], '2']]], ['mulExp', '3'], ], ], ['mulExp', ['mulExpRec', ['mulExp', '4'], '5']], ], ]); t.is(s(f).v, 25); t.is(s(f).p, '(((1*2)+3)+(4*5))'); });
test('semantic actions are evaluated lazily', t => { const f = m.match('1*2+3+4*5', 'sss'); const a = buildTreeNodeWithUniqueId(m); const tree = [ 'id', 1, 'addExp', [ 'id', 2, 'addExpRec', [ 'id', 3, 'addExp', [ 'id', 4, 'addExpRec', [ 'id', 5, 'addExp', [ 'id', 6, 'mulExp', [ 'id', 7, 'mulExpRec', ['id', 8, 'mulExp', ['id', 9, 'priExp', '1']], '*', ['id', 10, 'priExp', '2'], ], ], ], '+', ['id', 11, 'mulExp', ['id', 12, 'priExp', '3']], ], ], '+', [ 'id', 13, 'mulExp', [ 'id', 14, 'mulExpRec', ['id', 15, 'mulExp', ['id', 16, 'priExp', '4']], '*', ['id', 17, 'priExp', '5'], ], ], ], ]; t.deepEqual(a(f), ['id', 0, 'sss', tree, tree]); t.is(a._getNextId(), 18); }); });
describe('nested and indirect left recursion', test => { const m = ohm.grammar(` G { addExp = a | c a = b b = addExpRec addExpRec = addExp "+" mulExp c = d d = mulExp mulExp = e | g e = f f = mulExpRec g = h h = priExp mulExpRec = mulExp "*" priExp priExp = "0".."9" } `);
test('recognition', t => { assertSucceeds(t, m.match('1')); assertSucceeds(t, m.match('2+3')); assertFails(t, m.match('4+')); assertSucceeds(t, m.match('5*6')); assertSucceeds(t, m.match('7+8*9+0')); });
test('semantic actions', t => { const s = m.createSemantics().addAttribute('t', { addExpRec(x, _, y) { return [x.t, '+', y.t]; }, mulExpRec(x, _, y) { return [x.t, '*', y.t]; }, _terminal() { return this.sourceString; }, }); t.deepEqual(s(m.match('7+8*9+0')).t, [['7', '+', ['8', '*', '9']], '+', '0']); }); });
describe('tricky left recursion (different heads at same position)', test => { const m = ohm.grammar(` G { tricky = &foo bar foo = fooRec | digit fooRec = bar digit bar = barRec | digit barRec = foo digit } `);
test('recognition', t => { assertSucceeds(t, m.match('1234', 'tricky')); });
test('semantic actions', t => { const f = m.match('1234', 'tricky'); // TODO: perhaps just use JSON.stringify(f) here, and compare the result? const s = m.createSemantics().addAttribute('t', { tricky(_, x) { return ['tricky', x.t]; }, foo(expr) { return ['foo', expr.t]; }, fooRec(x, y) { return ['fooRec', x.t, y.t]; }, bar(expr) { return ['bar', expr.t]; }, barRec(x, y) { return ['barRec', x.t, y.t]; }, _terminal() { return this.sourceString; }, }); t.deepEqual(s(f).t, [ 'tricky', [ 'bar', ['barRec', ['foo', ['fooRec', ['bar', ['barRec', ['foo', '1'], '2']], '3']], '4'], ], ]); }); });});
describe('inheritance', t => { test('no namespace', t => { t.throws( () => { ohm.grammar('G2 <: G1 {}'); }, {message: /Grammar G1 is not declared/} ); });
test('empty namespace', t => { t.throws( () => { ohm.grammar('G2 <: G1 {}', {}); }, {message: /Grammar G1 is not declared in namespace/} ); });
test('duplicate definition', t => { t.throws( () => { ohm.grammars('G1 { foo = "foo" } G2 <: G1 { foo = "bar" }'); }, { // eslint-disable-next-line max-len message: /Duplicate declaration for rule 'foo' in grammar 'G2' \(originally declared in 'G1'\)/, }, 'throws if rule is already declared in super-grammar' ); });
describe('override', test => { const ns = ohm.grammars('G1 { number = digit+ } G2 <: G1 { digit := "a".."z" }');
test('it checks that rule exists in super-grammar', t => { t.throws( () => { ohm.grammar('G3 <: G1 { foo := "foo" }', ns); }, {message: /Cannot override rule foo because it is not declared in G1/} ); });
test("shouldn't matter if arities aren't the same", t => { // It's OK for the semantic action "API" of a grammar to be different // from that of its super-grammar.
// arity(overriding rule) > arity(overridden rule) ns.M1 = ohm.grammar('M1 { foo = "foo" }'); ohm.grammar('M2 <: M1 { foo := "foo" "bar" }', ns);
// arity(overriding rule) < arity(overridden rule) ns.M3 = ohm.grammar('M3 { foo = digit digit }', ns); ns.M4 = ohm.grammar('M4 <: M3 { foo := digit }', ns); t.pass(); });
test('should be ok to add new cases', t => { t.truthy(ohm.grammar('G { space := "foo" -- newCaseLabel }')); });
test('recognition', t => { assertSucceeds(t, ns.G1.match('1234', 'number')); assertFails(t, ns.G1.match('hello', 'number')); assertFails(t, ns.G1.match('h3llo', 'number'));
assertFails(t, ns.G2.match('1234', 'number')); assertSucceeds(t, ns.G2.match('hello', 'number')); assertFails(t, ns.G2.match('h3llo', 'number')); });
test('semantic actions', t => { const s = ns.G2.createSemantics().addAttribute('v', { number(digits) { return ['number', digits.children.map(c => c.v)]; }, digit(d) { return ['digit', d.v]; }, _terminal() { return this.sourceString; }, }); const expected = [ 'number', [ ['digit', 'a'], ['digit', 'b'], ['digit', 'c'], ['digit', 'd'], ], ]; t.deepEqual(s(ns.G2.match('abcd', 'number')).v, expected); }); });
describe('extend', test => { const ns = ohm.grammars('G1 { foo = "aaa" "bbb" } G2 <: G1 { foo += "111" "222" }');
test('recognition', t => { assertSucceeds(t, ns.G1.match('aaabbb')); assertFails(t, ns.G1.match('111222'));
assertSucceeds(t, ns.G2.match('aaabbb')); assertSucceeds(t, ns.G2.match('111222')); });
test('semantic actions', t => { const s = ns.G2.createSemantics().addAttribute('v', { foo(x, y) { return [x.sourceString, y.sourceString]; }, }); t.deepEqual(s(ns.G2.match('aaabbb')).v, ['aaa', 'bbb']); t.deepEqual(s(ns.G2.match('111222')).v, ['111', '222']); });
test('should check that rule exists in super-grammar', t => { t.throws( () => { ohm.grammar('G3 <: G1 { bar += "bar" }', ns); }, {message: /Cannot extend rule bar because it is not declared in G1/} ); });
test('should make sure rule arities are compatible', t => { // An extending rule must produce the same number of values // as the underlying rule. This is to ensure the semantic // action "API" doesn't change.
// Too many: ns.M1 = ohm.grammar('M1 { foo = "foo" bar = "bar" baz = "baz" }'); try { ohm.grammar('M2 <: M1 { foo += bar baz }', ns); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, [ 'Line 1, col 19:', '> 1 | M2 <: M1 { foo += bar baz }', ' ^~~~~~~', 'Rule foo involves an alternation which has inconsistent arity (expected 1, got 2)', ].join('\n') ); }
// Too few: ns.M3 = ohm.grammar('M3 { foo = digit digit }'); try { ohm.grammar('M4 <: M3 { foo += digit }', ns); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, [ 'Line 1, col 19:', '> 1 | M4 <: M3 { foo += digit }', ' ^~~~~', 'Rule foo involves an alternation which has inconsistent arity (expected 2, got 1)', ].join('\n') ); } });
test('should be ok to add new cases', t => { t.truthy(ohm.grammar('G { space += "foo" -- newCaseLabel }')); }); });});
test('override with "..."', t => { let g = ohm.grammar('G { letter := "@" | ... }'); t.is(g.match('@', 'letter').succeeded(), true); t.is(g.match('a', 'letter').succeeded(), true);
g = ohm.grammar('G { letter := ... | "@" }'); t.is(g.match('@', 'letter').succeeded(), true); t.is(g.match('a', 'letter').succeeded(), true);
g = ohm.grammar('G { letter := "3" | ... | "@" }'); t.is(g.match('@', 'letter').succeeded(), true); t.is(g.match('a', 'letter').succeeded(), true); t.is(g.match('3', 'letter').succeeded(), true);
t.truthy(ohm.grammar('G { letter := ... }'), 'it allows `...` as the whole body');
// Check that the branches are evaluated in the correct order. g = ohm.grammar('G { letter := "" | ... }'); t.is(g.match('', 'letter').succeeded(), true); t.is(g.match('a', 'letter').succeeded(), false); g = ohm.grammar('G { letter := ... | "ab" }'); t.is(g.match('a', 'letter').succeeded(), true); t.is(g.match('ab', 'letter').succeeded(), false);
g = ohm.grammar(` G { Start = ListOf<letter, ","> ListOf<elem, sep> := "✌️" | ... }`); t.is(g.match('✌️').succeeded(), true, 'it works on parameterized rules');
t.throws( () => ohm.grammar('G { doesNotExist := ... }'), {message: /Cannot override rule doesNotExist/}, 'it gives the correct error message when overriding non-existent rule' );
t.throws( () => ohm.grammar('G { foo = ... }'), {message: /Expected "}"/}, "it's not allowed in a rule definition" );
t.throws( () => ohm.grammar('G { letter += ... }'), {message: /Expected "}"/}, "it's not allowed when extending" );
t.throws(() => ohm.grammar('G { letter := "@" "#" | ... }'), { message: /inconsistent arity/, });
t.throws( () => ohm.grammar('G { letter := ... | "@" | ... }'), {message: /at most once/}, "'...' can appear at most once in a rule body" );
/* TODO: - [ ] improve error message (inconsistent arity seems backwards) - [ ] improve error message when using `...` in a rule defintion/extension - [ ] unify Extend and Combine? - [ ] using '...' when overriding a non-existent rule */});
describe('bindings', test => { test('inconsistent arity in alts is an error', t => { try { ohm.grammar('G { foo = "a" "c" | "b" }'); } catch (e) { t.is( e.message, [ 'Line 1, col 21:', '> 1 | G { foo = "a" "c" | "b" }', ' ^~~', 'Rule foo involves an alternation which has inconsistent arity (expected 2, got 1)', ].join('\n') ); } });
test('by default, bindings are evaluated lazily', t => { const g = ohm.grammar(` G { foo = bar baz bar = "a" baz = "b" } `);
let id = 0; let s = g.createSemantics().addAttribute('v', { foo(x, y) { const xv = x.v; const yv = y.v; return { x: xv, y: yv, }; }, bar(expr) { return ['bar', expr.v, id++]; }, baz(expr) { return ['baz', expr.v, id++]; }, _terminal() { return this.sourceString; }, }); t.deepEqual(s(g.match('ab')).v, { x: ['bar', 'a', 0], y: ['baz', 'b', 1], });
id = 0; s = g.createSemantics().addAttribute('v', { foo(x, y) { const yv = y.v; const xv = x.v; return { x: xv, y: yv, }; }, bar(expr) { return ['bar', expr.v, id++]; }, baz(expr) { return ['baz', expr.v, id++]; }, _terminal() { return this.sourceString; }, }); t.deepEqual(s(g.match('ab')).v, { x: ['bar', 'a', 1], y: ['baz', 'b', 0], }); });});
test('inline rule declarations', t => { function makeEval(g) { const s = g.createSemantics().addAttribute('v', { addExp_plus(x, op, y) { return x.v + y.v; }, addExp_minus(x, op, y) { return x.v - y.v; }, mulExp_times(x, op, y) { return x.v * y.v; }, mulExp_divide(x, op, y) { return x.v / y.v; }, priExp_paren(oparen, e, cparen) { return e.v; }, number_rec(n, d) { return n.v * 10 + d.v; }, digit(expr) { return expr.v.charCodeAt(0) - '0'.charCodeAt(0); }, _terminal() { return this.sourceString; }, }); return function(node) { return s(node).v; }; }
const ns = {}; const Arithmetic = (ns.Arithmetic = ohm.grammar(arithmeticGrammarSource));
assertSucceeds(t, Arithmetic.match('1*(2+3)-4/5'), 'expr is recognized'); t.is( makeEval(Arithmetic)(Arithmetic.match('10*(2+123)-4/5')), 1249.2, 'semantic action works' );
const m2 = ohm.grammar( ` Good <: Arithmetic { addExp := addExp "~" mulExp -- minus | mulExp } `, ns ); t.is(makeEval(m2)(m2.match('2*3~4')), 2);
t.throws( () => { ohm.grammar('Bad <: Arithmetic { addExp += addExp "~" mulExp -- minus }', ns); }, { message: /rule 'addExp_minus' in grammar 'Bad' \(originally declared in 'Arithmetic'\)/, } );
t.throws( () => { ohm.grammar('Bad { start = "a" ("b" -- bad\n) }'); }, null, 'inline rules must be at the top level' );});
describe('lexical vs. syntactic rules', test => { test("can't call syntactic rule from lexical rule, not not the other way around", t => { t.truthy(ohm.grammar('G { foo = bar bar = "bar" }'), 'lexical calling lexical'); t.throws( () => { ohm.grammar('G { foo = Bar Bar = "bar" }'); }, { message: /Cannot apply syntactic rule Bar from here \(inside a lexical context\)/, }, 'lexical calling syntactic' ); t.truthy(ohm.grammar('G { Foo = bar bar = "bar" }'), 'syntactic calling lexical'); t.truthy(ohm.grammar('G { Foo = Bar Bar = "bar" }'), 'syntactic calling syntactic'); });
test("lexical rules don't skip spaces implicitly", t => { const g = ohm.grammar('G { start = "foo" "bar" }'); assertSucceeds(t, g.match('foobar', 'start')); assertFails(t, g.match('foo bar')); assertFails(t, g.match(' foo bar ')); });
test('syntactic rules skip spaces implicitly', t => { const g = ohm.grammar('G { Start = "foo" "bar" }'); assertSucceeds(t, g.match('foobar')); assertSucceeds(t, g.match('foo bar')); assertSucceeds(t, g.match(' foo bar ')); });
test('mixing lexical and syntactic rules works as expected', t => { const g = ohm.grammar(` G { Start = foo bar foo = "foo" bar = "bar" } `); assertSucceeds(t, g.match('foobar')); assertSucceeds(t, g.match('foo bar')); assertSucceeds(t, g.match(' foo bar ')); });
// TODO: write more tests for this operator (e.g., to ensure that it's "transparent", arity-wise) // and maybe move it somewhere else. test('lexification operator works as expected', t => { const g = ohm.grammar(` G { ArrowFun = name #(spacesNoNl "=>") "{}" name = "x" | "y" spacesNoNl = " "* } `); assertSucceeds(t, g.match('x => {}')); assertSucceeds(t, g.match(' y => \n\n \n{}')); assertFails(t, g.match('x \n => {}'));
t.throws( () => { ohm.grammar('G { R = #("a" R) | "b" "c" }'); }, { message: /Cannot apply syntactic rule R from here \(inside a lexical context\)/, } ); });});
test('space skipping semantics', t => { const g = ohm.grammar(` G { Iter = ">" letter+ #(space) Lookahead = ">" &letter #(space letter) NegLookahead = ">" ~digit #(space letter) } `); assertSucceeds(t, g.match('> a b ', 'Iter'), "iter doesn't consume trailing space"); assertSucceeds(t, g.match('> a', 'Lookahead'), "lookahead doesn't consume anything"); assertSucceeds( t, g.match('> a', 'NegLookahead'), "negative lookahead doesn't consume anything" );});
// https://github.com/harc/ohm/issues/282test('single-line comment after case name (#282)', t => { const {ohmGrammar} = ohm; assertSucceeds( t, ohmGrammar.match(`G { Start = -- foo // ok | "x" }`) ); assertSucceeds(t, ohmGrammar.match('G {Start = -- foo // A comment\n}')); assertSucceeds(t, ohmGrammar.match('G {} // This works too')); assertSucceeds(t, ohmGrammar.match('// And this'));});
describe('bootstrap', test => { const ns = ohm.grammars(ohmGrammarSource);
test('it can recognize arithmetic grammar', t => { assertSucceeds(t, ns.Ohm.match(arithmeticGrammarSource, 'Grammar')); });
test('it can recognize itself', t => { assertSucceeds(t, ns.Ohm.match(ohmGrammarSource, 'Grammar')); });
test('it can produce a grammar that works', t => { const g = ohm._buildGrammar( ns.Ohm.match(ohmGrammarSource, 'Grammar'), ohm.createNamespace(), ns.Ohm ); assertSucceeds( t, g.match(ohmGrammarSource, 'Grammar'), 'Ohm grammar can recognize itself' ); const Arithmetic = ohm._buildGrammar( g.match(arithmeticGrammarSource, 'Grammar'), ohm.createNamespace(), g ); const s = Arithmetic.createSemantics().addAttribute('v', { exp(expr) { return expr.v; }, addExp(expr) { return expr.v; }, addExp_plus(x, op, y) { return x.v + y.v; }, addExp_minus(x, op, y) { return x.v - y.v; }, mulExp(expr) { return expr.v; }, mulExp_times(x, op, y) { return x.v * y.v; }, mulExp_divide(x, op, y) { return x.v / y.v; }, priExp(expr) { return expr.v; }, priExp_paren(oparen, e, cparen) { return e.v; }, number(expr) { return expr.v; }, number_rec(n, d) { return n.v * 10 + d.v; }, digit(expr) { return expr.v.charCodeAt(0) - '0'.charCodeAt(0); }, _terminal() { return this.sourceString; }, }); t.is(s(Arithmetic.match('10*(2+123)-4/5')).v, 1249.2); });
test('full bootstrap!', t => { const g = ohm._buildGrammar( ns.Ohm.match(ohmGrammarSource, 'Grammar'), ohm.createNamespace(), ns.Ohm ); const gPrime = ohm._buildGrammar( g.match(ohmGrammarSource, 'Grammar'), ohm.createNamespace(), g ); gPrime.namespaceName = g.namespaceName; // make their namespaceName properties the same compareGrammars(t, g, gPrime); });});