Add line numbers to errors [#15]

This commit is contained in:
David Evans 2017-11-08 19:57:41 +00:00
parent 2cb34c273c
commit e6064b72de
7 changed files with 342 additions and 109 deletions

View File

@ -153,12 +153,15 @@ define(['core/ArrayUtilities'], (array) => {
}}, }},
'define': {type: 'keyword', suggest: true, then: { 'define': {type: 'keyword', suggest: true, then: {
'': aliasListToEnd, '': aliasListToEnd,
'as': CM_ERROR,
}}, }},
'begin': {type: 'keyword', suggest: true, then: { 'begin': {type: 'keyword', suggest: true, then: {
'': aliasListToEnd, '': aliasListToEnd,
'as': CM_ERROR,
}}, }},
'end': {type: 'keyword', suggest: true, then: { 'end': {type: 'keyword', suggest: true, then: {
'': aliasListToEnd, '': aliasListToEnd,
'as': CM_ERROR,
'\n': end, '\n': end,
}}, }},
'if': {type: 'keyword', suggest: true, then: { 'if': {type: 'keyword', suggest: true, then: {

View File

@ -323,7 +323,7 @@ define(['core/ArrayUtilities'], (array) => {
}; };
} }
beginNested(mode, label, name) { beginNested(mode, label, name, ln) {
const leftAgent = makeAgent(name + '[', {anchorRight: true}); const leftAgent = makeAgent(name + '[', {anchorRight: true});
const rightAgent = makeAgent(name + ']'); const rightAgent = makeAgent(name + ']');
const agents = [leftAgent, rightAgent]; const agents = [leftAgent, rightAgent];
@ -332,6 +332,7 @@ define(['core/ArrayUtilities'], (array) => {
mode, mode,
label, label,
stages, stages,
ln,
}; };
this.currentNest = { this.currentNest = {
agents, agents,
@ -457,13 +458,13 @@ define(['core/ArrayUtilities'], (array) => {
]); ]);
} }
handleBlockBegin({mode, label}) { handleBlockBegin({ln, mode, label}) {
const name = '__BLOCK' + this.blockCount; const name = '__BLOCK' + this.blockCount;
this.beginNested(mode, label, name); this.beginNested(mode, label, name, ln);
++ this.blockCount; ++ this.blockCount;
} }
handleBlockSplit({mode, label}) { handleBlockSplit({ln, mode, label}) {
const containerMode = this.currentNest.stage.sections[0].mode; const containerMode = this.currentNest.stage.sections[0].mode;
if(containerMode !== 'if') { if(containerMode !== 'if') {
throw new Error( throw new Error(
@ -476,6 +477,7 @@ define(['core/ArrayUtilities'], (array) => {
mode, mode,
label, label,
stages: [], stages: [],
ln,
}; };
this.currentNest.stage.sections.push(this.currentSection); this.currentNest.stage.sections.push(this.currentSection);
} }
@ -501,7 +503,13 @@ define(['core/ArrayUtilities'], (array) => {
} }
handleStage(stage) { handleStage(stage) {
this.stageHandlers[stage.type](stage); try {
this.stageHandlers[stage.type](stage);
} catch(e) {
if(typeof e === 'object' && e.message) {
throw new Error(e.message + ' at line ' + (stage.ln + 1));
}
}
} }
generate({stages, meta = {}}) { generate({stages, meta = {}}) {
@ -511,14 +519,14 @@ define(['core/ArrayUtilities'], (array) => {
this.agents.length = 0; this.agents.length = 0;
this.blockCount = 0; this.blockCount = 0;
this.nesting.length = 0; this.nesting.length = 0;
const globals = this.beginNested('global', '', ''); const globals = this.beginNested('global', '', '', 0);
stages.forEach(this.handleStage); stages.forEach(this.handleStage);
if(this.nesting.length !== 1) { if(this.nesting.length !== 1) {
throw new Error( throw new Error(
'Invalid block nesting (' + 'Unterminated section at line ' +
(this.nesting.length - 1) + ' unclosed)' (this.currentSection.ln + 1)
); );
} }

View File

@ -14,12 +14,12 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
} }
const PARSED = { const PARSED = {
blockBegin: (mode, label) => { blockBegin: (mode, label, {ln = 0} = {}) => {
return {type: 'block begin', mode, label}; return {type: 'block begin', mode, label, ln};
}, },
blockSplit: (mode, label) => { blockSplit: (mode, label, {ln = 0} = {}) => {
return {type: 'block split', mode, label}; return {type: 'block split', mode, label, ln};
}, },
blockEnd: () => { blockEnd: () => {
@ -662,20 +662,20 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('records all sections within blocks', () => { it('records all sections within blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.blockBegin('if', 'abc'), PARSED.blockBegin('if', 'abc', {ln: 10}),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
PARSED.blockSplit('else', 'xyz'), PARSED.blockSplit('else', 'xyz', {ln: 20}),
PARSED.connect(['A', 'C']), PARSED.connect(['A', 'C']),
PARSED.blockEnd(), PARSED.blockEnd(),
]}); ]});
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
expect(block0.sections).toEqual([ expect(block0.sections).toEqual([
{mode: 'if', label: 'abc', stages: [ {mode: 'if', label: 'abc', ln: 10, stages: [
GENERATED.beginAgents(['A', 'B']), GENERATED.beginAgents(['A', 'B']),
GENERATED.connect(['A', 'B']), GENERATED.connect(['A', 'B']),
]}, ]},
{mode: 'else', label: 'xyz', stages: [ {mode: 'else', label: 'xyz', ln: 20, stages: [
GENERATED.beginAgents(['C']), GENERATED.beginAgents(['C']),
GENERATED.connect(['A', 'C']), GENERATED.connect(['A', 'C']),
]}, ]},
@ -756,11 +756,11 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
expect(block0.sections).toEqual([ expect(block0.sections).toEqual([
{mode: 'if', label: 'abc', stages: [ {mode: 'if', label: 'abc', ln: 0, stages: [
jasmine.anything(), jasmine.anything(),
jasmine.anything(), jasmine.anything(),
]}, ]},
{mode: 'else', label: 'xyz', stages: []}, {mode: 'else', label: 'xyz', ln: 0, stages: []},
]); ]);
}); });
@ -774,8 +774,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
expect(block0.sections).toEqual([ expect(block0.sections).toEqual([
{mode: 'if', label: 'abc', stages: []}, {mode: 'if', label: 'abc', ln: 0, stages: []},
{mode: 'else', label: 'xyz', stages: [ {mode: 'else', label: 'xyz', ln: 0, stages: [
jasmine.anything(), jasmine.anything(),
jasmine.anything(), jasmine.anything(),
]}, ]},
@ -832,11 +832,11 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
expect(block0.sections).toEqual([ expect(block0.sections).toEqual([
{mode: 'if', label: 'abc', stages: [ {mode: 'if', label: 'abc', ln: 0, stages: [
jasmine.anything(), jasmine.anything(),
jasmine.anything(), jasmine.anything(),
]}, ]},
{mode: 'else', label: 'xyz', stages: []}, {mode: 'else', label: 'xyz', ln: 0, stages: []},
]); ]);
}); });

View File

@ -71,6 +71,28 @@ define([
'end': {type: 'agent end', mode: 'cross'}, 'end': {type: 'agent end', mode: 'cross'},
}; };
function makeError(message, token = null) {
let suffix = '';
if(token) {
suffix = (
' at line ' + (token.b.ln + 1) +
', character ' + token.b.ch
);
}
return new Error(message + suffix);
}
function errToken(line, pos) {
if(pos < line.length) {
return line[pos];
}
const last = array.last(line);
if(!last) {
return null;
}
return {b: last.e};
}
function joinLabel(line, begin = 0, end = null) { function joinLabel(line, begin = 0, end = null) {
if(end === null) { if(end === null) {
end = line.length; end = line.length;
@ -93,14 +115,18 @@ define([
} }
function skipOver(line, start, skip, error = null) { function skipOver(line, start, skip, error = null) {
const pass = skip.every((expected, i) => ( for(let i = 0; i < skip.length; ++ i) {
tokenKeyword(line[start + i]) === expected const expected = skip[i];
)); const token = line[start + i];
if(!pass) { if(tokenKeyword(token) !== expected) {
if(error) { if(error) {
throw new Error(error + ': ' + joinLabel(line)); throw makeError(
} else { error + '; expected "' + expected + '"',
return start; token
);
} else {
return start;
}
} }
} }
return start + skip.length; return start + skip.length;
@ -124,7 +150,7 @@ define([
aliasSep = end; aliasSep = end;
} }
if(start >= aliasSep) { if(start >= aliasSep) {
throw new Error('Missing agent name'); throw makeError('Missing agent name', errToken(line, start));
} }
return { return {
name: joinLabel(line, start, aliasSep), name: joinLabel(line, start, aliasSep),
@ -139,11 +165,12 @@ define([
const flags = []; const flags = [];
let p = start; let p = start;
for(; p < end; ++ p) { for(; p < end; ++ p) {
const rawFlag = tokenKeyword(line[p]); const token = line[p];
const rawFlag = tokenKeyword(token);
const flag = flagTypes[rawFlag]; const flag = flagTypes[rawFlag];
if(flag) { if(flag) {
if(flags.includes(flag)) { if(flags.includes(flag)) {
throw new Error('Duplicate agent flag: ' + rawFlag); throw makeError('Duplicate agent flag: ' + rawFlag, token);
} }
flags.push(flag); flags.push(flag);
} else { } else {
@ -204,7 +231,7 @@ define([
const type = tokenKeyword(line[1]); const type = tokenKeyword(line[1]);
if(TERMINATOR_TYPES.indexOf(type) === -1) { if(TERMINATOR_TYPES.indexOf(type) === -1) {
throw new Error('Unknown termination: ' + joinLabel(line)); throw makeError('Unknown termination "' + type + '"', line[1]);
} }
meta.terminators = type; meta.terminators = type;
return true; return true;
@ -274,14 +301,11 @@ define([
let skip = 2; let skip = 2;
skip = skipOver(line, skip, type.skip); skip = skipOver(line, skip, type.skip);
const agents = readAgentList(line, skip, labelSep); const agents = readAgentList(line, skip, labelSep);
if( if(agents.length < type.min) {
agents.length < type.min || throw makeError('Too few agents for ' + mode.mode, line[0]);
(type.max !== null && agents.length > type.max) }
) { if(type.max !== null && agents.length > type.max) {
throw new Error( throw makeError('Too many agents for ' + mode.mode, line[0]);
'Invalid ' + mode.mode +
': ' + joinLabel(line)
);
} }
return { return {
type: type.type, type: type.type,
@ -343,9 +367,13 @@ define([
} }
} }
if(!stage) { if(!stage) {
throw new Error('Unrecognised command: ' + joinLabel(line)); throw makeError(
'Unrecognised command: ' + joinLabel(line),
line[0]
);
} }
if(typeof stage === 'object') { if(typeof stage === 'object') {
stage.ln = line[0].b.ln;
stages.push(stage); stages.push(stage);
} }
} }

View File

@ -4,7 +4,43 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parser = new Parser(); const parser = new Parser();
const PARSED = { const PARSED = {
blockBegin: ({
ln = jasmine.anything(),
mode = jasmine.anything(),
label = jasmine.anything(),
} = {}) => {
return {
type: 'block begin',
ln,
mode,
label,
};
},
blockSplit: ({
ln = jasmine.anything(),
mode = jasmine.anything(),
label = jasmine.anything(),
} = {}) => {
return {
type: 'block split',
ln,
mode,
label,
};
},
blockEnd: ({
ln = jasmine.anything(),
} = {}) => {
return {
type: 'block end',
ln,
};
},
connect: (agentNames, { connect: (agentNames, {
ln = jasmine.anything(),
line = jasmine.anything(), line = jasmine.anything(),
left = jasmine.anything(), left = jasmine.anything(),
right = jasmine.anything(), right = jasmine.anything(),
@ -12,6 +48,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
} = {}) => { } = {}) => {
return { return {
type: 'connect', type: 'connect',
ln,
agents: agentNames.map((name) => ({ agents: agentNames.map((name) => ({
name, name,
alias: '', alias: '',
@ -77,7 +114,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
it('propagates aliases', () => { it('propagates aliases', () => {
const parsed = parser.parse('define Foo Bar as A B'); const parsed = parser.parse('define Foo Bar as A B');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: 'agent define', agents: [ {type: 'agent define', ln: jasmine.anything(), agents: [
{name: 'Foo Bar', alias: 'A B', flags: []}, {name: 'Foo Bar', alias: 'A B', flags: []},
]}, ]},
]); ]);
@ -102,6 +139,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ {
type: 'connect', type: 'connect',
ln: jasmine.anything(),
agents: [ agents: [
{name: 'A', alias: '', flags: ['start']}, {name: 'A', alias: '', flags: ['start']},
{name: 'B', alias: '', flags: [ {name: 'B', alias: '', flags: [
@ -117,12 +155,18 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}); });
it('rejects duplicate flags', () => { it('rejects duplicate flags', () => {
expect(() => parser.parse('A -> +*+B')).toThrow(); expect(() => parser.parse('A -> +*+B')).toThrow(new Error(
expect(() => parser.parse('A -> **B')).toThrow(); 'Duplicate agent flag: + at line 1, character 7'
));
expect(() => parser.parse('A -> **B')).toThrow(new Error(
'Duplicate agent flag: * at line 1, character 6'
));
}); });
it('rejects missing agent names', () => { it('rejects missing agent names', () => {
expect(() => parser.parse('A -> +')).toThrow(); expect(() => parser.parse('A -> +')).toThrow(new Error(
'Missing agent name at line 1, character 6'
));
}); });
it('converts multiple entries', () => { it('converts multiple entries', () => {
@ -141,6 +185,14 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
]); ]);
}); });
it('stores line numbers', () => {
const parsed = parser.parse('A -> B\nB -> A');
expect(parsed.stages).toEqual([
PARSED.connect(['A', 'B'], {ln: 0}),
PARSED.connect(['B', 'A'], {ln: 1}),
]);
});
it('recognises all types of connection', () => { it('recognises all types of connection', () => {
const parsed = parser.parse( const parsed = parser.parse(
'A -> B\n' + 'A -> B\n' +
@ -215,6 +267,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('note over A: hello there'); const parsed = parser.parse('note over A: hello there');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'note over', type: 'note over',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
@ -232,30 +285,35 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ {
type: 'note left', type: 'note left',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
}, },
{ {
type: 'note left', type: 'note left',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
}, },
{ {
type: 'note right', type: 'note right',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
}, },
{ {
type: 'note right', type: 'note right',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
}, },
{ {
type: 'note between', type: 'note between',
ln: jasmine.anything(),
agents: [ agents: [
{name: 'A', alias: '', flags: []}, {name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []}, {name: 'B', alias: '', flags: []},
@ -270,6 +328,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('note over A B, C D: hi'); const parsed = parser.parse('note over A B, C D: hi');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'note over', type: 'note over',
ln: jasmine.anything(),
agents: [ agents: [
{name: 'A B', alias: '', flags: []}, {name: 'A B', alias: '', flags: []},
{name: 'C D', alias: '', flags: []}, {name: 'C D', alias: '', flags: []},
@ -280,13 +339,16 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}); });
it('rejects note between for a single agent', () => { it('rejects note between for a single agent', () => {
expect(() => parser.parse('note between A: hi')).toThrow(); expect(() => parser.parse('note between A: hi')).toThrow(new Error(
'Too few agents for note at line 1, character 0'
));
}); });
it('converts state', () => { it('converts state', () => {
const parsed = parser.parse('state over A: doing stuff'); const parsed = parser.parse('state over A: doing stuff');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'note over', type: 'note over',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'state', mode: 'state',
label: 'doing stuff', label: 'doing stuff',
@ -294,13 +356,16 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}); });
it('rejects multiple agents for state', () => { it('rejects multiple agents for state', () => {
expect(() => parser.parse('state over A, B: hi')).toThrow(); expect(() => parser.parse('state over A, B: hi')).toThrow(new Error(
'Too many agents for state at line 1, character 0'
));
}); });
it('converts text blocks', () => { it('converts text blocks', () => {
const parsed = parser.parse('text right of A: doing stuff'); const parsed = parser.parse('text right of A: doing stuff');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'note right', type: 'note right',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'text', mode: 'text',
label: 'doing stuff', label: 'doing stuff',
@ -316,6 +381,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ {
type: 'agent define', type: 'agent define',
ln: jasmine.anything(),
agents: [ agents: [
{name: 'A', alias: '', flags: []}, {name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []}, {name: 'B', alias: '', flags: []},
@ -323,6 +389,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}, },
{ {
type: 'agent begin', type: 'agent begin',
ln: jasmine.anything(),
agents: [ agents: [
{name: 'A', alias: '', flags: []}, {name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []}, {name: 'B', alias: '', flags: []},
@ -331,6 +398,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}, },
{ {
type: 'agent end', type: 'agent end',
ln: jasmine.anything(),
agents: [ agents: [
{name: 'A', alias: '', flags: []}, {name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []}, {name: 'B', alias: '', flags: []},
@ -344,6 +412,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('abc:'); const parsed = parser.parse('abc:');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'mark', type: 'mark',
ln: jasmine.anything(),
name: 'abc', name: 'abc',
}]); }]);
}); });
@ -352,6 +421,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('simultaneously:'); const parsed = parser.parse('simultaneously:');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'async', type: 'async',
ln: jasmine.anything(),
target: '', target: '',
}]); }]);
}); });
@ -360,6 +430,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('simultaneously with abc:'); const parsed = parser.parse('simultaneously with abc:');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'async', type: 'async',
ln: jasmine.anything(),
target: 'abc', target: 'abc',
}]); }]);
}); });
@ -376,21 +447,21 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
'end\n' 'end\n'
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: 'block begin', mode: 'if', label: 'something happens'}, PARSED.blockBegin({mode: 'if', label: 'something happens'}),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
{type: 'block split', mode: 'else', label: 'something else'}, PARSED.blockSplit({mode: 'else', label: 'something else'}),
PARSED.connect(['A', 'C']), PARSED.connect(['A', 'C']),
PARSED.connect(['C', 'B']), PARSED.connect(['C', 'B']),
{type: 'block split', mode: 'else', label: ''}, PARSED.blockSplit({mode: 'else', label: ''}),
PARSED.connect(['A', 'D']), PARSED.connect(['A', 'D']),
{type: 'block end'}, PARSED.blockEnd(),
]); ]);
}); });
it('converts loop blocks', () => { it('converts loop blocks', () => {
const parsed = parser.parse('repeat until something'); const parsed = parser.parse('repeat until something');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: 'block begin', mode: 'repeat', label: 'until something'}, PARSED.blockBegin({mode: 'repeat', label: 'until something'}),
]); ]);
}); });
@ -399,7 +470,9 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}); });
it('rejects invalid inputs', () => { it('rejects invalid inputs', () => {
expect(() => parser.parse('huh')).toThrow(); expect(() => parser.parse('huh')).toThrow(new Error(
'Unrecognised command: huh at line 1, character 0'
));
}); });
it('rejects partial links', () => { it('rejects partial links', () => {
@ -409,7 +482,9 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}); });
it('rejects invalid terminators', () => { it('rejects invalid terminators', () => {
expect(() => parser.parse('terminators foo')).toThrow(); expect(() => parser.parse('terminators foo')).toThrow(new Error(
'Unknown termination "foo" at line 1, character 12'
));
}); });
it('rejects malformed notes', () => { it('rejects malformed notes', () => {
@ -417,7 +492,9 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}); });
it('rejects malformed block commands', () => { it('rejects malformed block commands', () => {
expect(() => parser.parse('else nope foo')).toThrow(); expect(() => parser.parse('else nope foo')).toThrow(new Error(
'Invalid block command; expected "if" at line 1, character 5'
));
}); });
it('rejects invalid notes', () => { it('rejects invalid notes', () => {

View File

@ -104,30 +104,93 @@ define(['./CodeMirrorMode'], (CMMode) => {
} }
} }
function copyPos(pos) {
return {i: pos.i, ln: pos.ln, ch: pos.ch};
}
function advancePos(pos, src, steps) {
for(let i = 0; i < steps; ++ i) {
++ pos.ch;
if(src[pos.i + i] === '\n') {
++ pos.ln;
pos.ch = 0;
}
}
pos.i += steps;
}
class TokenState {
constructor(src) {
this.src = src;
this.block = null;
this.token = null;
this.pos = {i: 0, ln: 0, ch: 0};
this.reset();
}
isOver() {
return this.pos.i > this.src.length;
}
reset() {
this.token = {s: '', v: '', q: false, b: null, e: null};
this.block = null;
}
beginToken(advance) {
this.block = advance.newBlock;
Object.assign(this.token, this.block.baseToken);
this.token.b = copyPos(this.pos);
}
endToken() {
let token = null;
if(!this.block.omit) {
this.token.e = copyPos(this.pos);
token = this.token;
}
this.reset();
return token;
}
advance() {
const advance = tokAdvance(this.src, this.pos.i, this.block);
if(advance.newBlock) {
this.beginToken(advance);
}
this.token.s += advance.appendSpace;
this.token.v += advance.appendValue;
advancePos(this.pos, this.src, advance.skip);
if(advance.end) {
return this.endToken();
} else {
return null;
}
}
}
function posStr(pos) {
return 'line ' + (pos.ln + 1) + ', character ' + pos.ch;
}
return class Tokeniser { return class Tokeniser {
tokenise(src) { tokenise(src) {
const tokens = []; const tokens = [];
let block = null; const state = new TokenState(src);
let current = {s: '', v: '', q: false}; while(!state.isOver()) {
for(let i = 0; i <= src.length;) { const token = state.advance();
const advance = tokAdvance(src, i, block); if(token) {
if(advance.newBlock) { tokens.push(token);
block = advance.newBlock;
Object.assign(current, block.baseToken);
}
current.s += advance.appendSpace;
current.v += advance.appendValue;
i += advance.skip;
if(advance.end) {
if(!block.omit) {
tokens.push(current);
}
current = {s: '', v: '', q: false};
block = null;
} }
} }
if(block) { if(state.block) {
throw new Error('Unterminated block'); throw new Error(
'Unterminated literal (began at ' +
posStr(state.token.b) + ')'
);
} }
return tokens; return tokens;
} }

View File

@ -4,14 +4,24 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const tokeniser = new Tokeniser(); const tokeniser = new Tokeniser();
describe('.tokenise', () => { describe('.tokenise', () => {
function token({
s = jasmine.anything(), // spacing
v = jasmine.anything(), // value
q = jasmine.anything(), // isQuote?
b = jasmine.anything(), // begin position
e = jasmine.anything(), // end position
}) {
return {s, v, q, b, e};
}
it('converts the source into atomic tokens', () => { it('converts the source into atomic tokens', () => {
const input = 'foo bar -> baz'; const input = 'foo bar -> baz';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: '', v: 'foo', q: false}, token({s: '', v: 'foo'}),
{s: ' ', v: 'bar', q: false}, token({s: ' ', v: 'bar'}),
{s: ' ', v: '->', q: false}, token({s: ' ', v: '->'}),
{s: ' ', v: 'baz', q: false}, token({s: ' ', v: 'baz'}),
]); ]);
}); });
@ -19,10 +29,21 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const input = 'foo bar->baz'; const input = 'foo bar->baz';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: '', v: 'foo', q: false}, token({s: '', v: 'foo'}),
{s: ' ', v: 'bar', q: false}, token({s: ' ', v: 'bar'}),
{s: '', v: '->', q: false}, token({s: '', v: '->'}),
{s: '', v: 'baz', q: false}, token({s: '', v: 'baz'}),
]);
});
it('stores character numbers', () => {
const input = 'foo bar -> baz';
const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([
token({b: {i: 0, ln: 0, ch: 0}, e: {i: 3, ln: 0, ch: 3}}),
token({b: {i: 4, ln: 0, ch: 4}, e: {i: 7, ln: 0, ch: 7}}),
token({b: {i: 8, ln: 0, ch: 8}, e: {i: 10, ln: 0, ch: 10}}),
token({b: {i: 11, ln: 0, ch: 11}, e: {i: 14, ln: 0, ch: 14}}),
]); ]);
}); });
@ -30,10 +51,21 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const input = 'foo bar\nbaz'; const input = 'foo bar\nbaz';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: '', v: 'foo', q: false}, token({s: '', v: 'foo'}),
{s: ' ', v: 'bar', q: false}, token({s: ' ', v: 'bar'}),
{s: '', v: '\n', q: false}, token({s: '', v: '\n'}),
{s: '', v: 'baz', q: false}, token({s: '', v: 'baz'}),
]);
});
it('stores line numbers', () => {
const input = 'foo bar\nbaz';
const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([
token({b: {i: 0, ln: 0, ch: 0}, e: {i: 3, ln: 0, ch: 3}}),
token({b: {i: 4, ln: 0, ch: 4}, e: {i: 7, ln: 0, ch: 7}}),
token({b: {i: 7, ln: 0, ch: 7}, e: {i: 8, ln: 1, ch: 0}}),
token({b: {i: 8, ln: 1, ch: 0}, e: {i: 11, ln: 1, ch: 3}}),
]); ]);
}); });
@ -41,9 +73,9 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const input = 'foo "\n" baz'; const input = 'foo "\n" baz';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: '', v: 'foo', q: false}, token({s: '', v: 'foo', q: false}),
{s: ' ', v: '\n', q: true}, token({s: ' ', v: '\n', q: true}),
{s: ' ', v: 'baz', q: false}, token({s: ' ', v: 'baz', q: false}),
]); ]);
}); });
@ -51,10 +83,10 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const input = ' foo \t bar\t\n baz'; const input = ' foo \t bar\t\n baz';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: ' ', v: 'foo', q: false}, token({s: ' ', v: 'foo'}),
{s: ' \t ', v: 'bar', q: false}, token({s: ' \t ', v: 'bar'}),
{s: '\t', v: '\n', q: false}, token({s: '\t', v: '\n'}),
{s: ' ', v: 'baz', q: false}, token({s: ' ', v: 'baz'}),
]); ]);
}); });
@ -62,9 +94,17 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const input = 'foo "zig zag" \'abc def\''; const input = 'foo "zig zag" \'abc def\'';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: '', v: 'foo', q: false}, token({s: '', v: 'foo', q: false}),
{s: ' ', v: 'zig zag', q: true}, token({s: ' ', v: 'zig zag', q: true}),
{s: ' ', v: 'abc def', q: true}, token({s: ' ', v: 'abc def', q: true}),
]);
});
it('stores character positions around quoted strings', () => {
const input = '"foo bar"';
const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([
token({b: {i: 0, ln: 0, ch: 0}, e: {i: 9, ln: 0, ch: 9}}),
]); ]);
}); });
@ -72,9 +112,9 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const input = 'foo # bar baz\nzig'; const input = 'foo # bar baz\nzig';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: '', v: 'foo', q: false}, token({s: '', v: 'foo'}),
{s: '', v: '\n', q: false}, token({s: '', v: '\n'}),
{s: '', v: 'zig', q: false}, token({s: '', v: 'zig'}),
]); ]);
}); });
@ -82,9 +122,9 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const input = 'foo # bar "\'baz\nzig'; const input = 'foo # bar "\'baz\nzig';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: '', v: 'foo', q: false}, token({s: '', v: 'foo'}),
{s: '', v: '\n', q: false}, token({s: '', v: '\n'}),
{s: '', v: 'zig', q: false}, token({s: '', v: 'zig'}),
]); ]);
}); });
@ -92,8 +132,8 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const input = 'foo "zig\\" zag\\n"'; const input = 'foo "zig\\" zag\\n"';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: '', v: 'foo', q: false}, token({s: '', v: 'foo', q: false}),
{s: ' ', v: 'zig" zag\n', q: true}, token({s: ' ', v: 'zig" zag\n', q: true}),
]); ]);
}); });
@ -101,13 +141,27 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
const input = 'foo " zig\n zag "'; const input = 'foo " zig\n zag "';
const tokens = tokeniser.tokenise(input); const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([ expect(tokens).toEqual([
{s: '', v: 'foo', q: false}, token({s: '', v: 'foo', q: false}),
{s: ' ', v: ' zig\n zag ', q: true}, token({s: ' ', v: ' zig\n zag ', q: true}),
]);
});
it('calculates line numbers consistently within quotes', () => {
const input = 'foo\nbar "zig\nzag\na" b';
const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([
token({b: {i: 0, ln: 0, ch: 0}, e: {i: 3, ln: 0, ch: 3}}),
token({b: {i: 3, ln: 0, ch: 3}, e: {i: 4, ln: 1, ch: 0}}),
token({b: {i: 4, ln: 1, ch: 0}, e: {i: 7, ln: 1, ch: 3}}),
token({b: {i: 8, ln: 1, ch: 4}, e: {i: 19, ln: 3, ch: 2}}),
token({b: {i: 20, ln: 3, ch: 3}, e: {i: 21, ln: 3, ch: 4}}),
]); ]);
}); });
it('rejects unterminated quoted values', () => { it('rejects unterminated quoted values', () => {
expect(() => tokeniser.tokenise('"nope')).toThrow(); expect(() => tokeniser.tokenise('"nope')).toThrow(new Error(
'Unterminated literal (began at line 1, character 0)'
));
}); });
}); });