Add support for parallel actions using & syntax [#11]

This commit is contained in:
David Evans 2018-05-03 23:22:51 +01:00
parent a1caf2b16a
commit 918c62f049
14 changed files with 1694 additions and 1030 deletions

View File

@ -1643,6 +1643,87 @@
} }
} }
function extractParallel(target, stages) {
for(const stage of stages) {
if(!stage) {
continue;
}
if(stage.type === 'parallel') {
extractParallel(target, stage.stages);
} else {
target.push(stage);
}
}
}
function checkAgentConflicts(allStages) {
const createIDs = flatMap(
allStages
.filter((stage) => (stage.type === 'agent begin')),
(stage) => stage.agentIDs
);
for(const stage of allStages) {
if(stage.type !== 'agent end') {
continue;
}
for(const id of stage.agentIDs) {
if(createIDs.indexOf(id) !== -1) {
return 'Cannot create and destroy ' + id + ' simultaneously';
}
}
}
return null;
}
function checkReferenceConflicts(allStages) {
const leftIDs = allStages
.filter((stage) => (stage.type === 'block begin'))
.map((stage) => stage.left);
for(const stage of allStages) {
if(stage.type !== 'block end') {
continue;
}
if(leftIDs.indexOf(stage.left) !== -1) {
return 'Cannot create and destroy reference simultaneously';
}
}
return null;
}
function checkDelayedConflicts(allStages) {
const tags = allStages
.filter((stage) => (stage.type === 'connect-delay-begin'))
.map((stage) => stage.tag);
for(const stage of allStages) {
if(stage.type !== 'connect-delay-end') {
continue;
}
if(tags.indexOf(stage.tag) !== -1) {
return 'Cannot start and finish delayed connection simultaneously';
}
}
return null;
}
function errorForParallel(existing, latest) {
if(!existing) {
return 'Nothing to run statement in parallel with';
}
const allStages = [];
extractParallel(allStages, [existing]);
extractParallel(allStages, [latest]);
return (
checkAgentConflicts(allStages) ||
checkReferenceConflicts(allStages) ||
checkDelayedConflicts(allStages)
);
}
function swapBegin(stage, mode) { function swapBegin(stage, mode) {
if(stage.type === 'agent begin') { if(stage.type === 'agent begin') {
stage.mode = mode; stage.mode = mode;
@ -1758,37 +1839,75 @@
}); });
} }
addStage(stage, isVisible = true) { addStage(stage, {isVisible = true, parallel = false} = {}) {
if(!stage) {
return;
}
if(isVisible) {
this.currentNest.hasContent = true;
}
if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine;
}
const {stages} = this.currentSection;
if(parallel) {
const target = last(stages);
const err = errorForParallel(target, stage);
if(err) {
throw new Error(err);
}
const pstage = this.makeParallel([target, stage]);
pstage.ln = stage.ln;
-- stages.length;
stages.push(pstage);
} else {
stages.push(stage);
}
}
addImpStage(stage, {parallel = false} = {}) {
if(!stage) { if(!stage) {
return; return;
} }
if(typeof stage.ln === 'undefined') { if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine; stage.ln = this.latestLine;
} }
this.currentSection.stages.push(stage); const {stages} = this.currentSection;
if(isVisible) { if(parallel) {
this.currentNest.hasContent = true; const target = stages[stages.length - 2];
if(!target) {
throw new Error('Nothing to run statement in parallel with');
}
if(errorForParallel(target, stage)) {
stages.splice(stages.length - 1, 0, stage);
} else {
const pstage = this.makeParallel([target, stage]);
pstage.ln = stage.ln;
stages.splice(stages.length - 2, 1, pstage);
}
} else {
stages.push(stage);
} }
} }
addParallelStages(stages) { makeParallel(stages) {
const viableStages = stages.filter((stage) => Boolean(stage)); const viableStages = [];
extractParallel(viableStages, stages);
if(viableStages.length === 0) { if(viableStages.length === 0) {
return; return null;
} }
if(viableStages.length === 1) { if(viableStages.length === 1) {
this.addStage(viableStages[0]); return viableStages[0];
return;
} }
viableStages.forEach((stage) => { viableStages.forEach((stage) => {
if(typeof stage.ln === 'undefined') { if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine; stage.ln = this.latestLine;
} }
}); });
this.addStage({ return {
stages: viableStages, stages: viableStages,
type: 'parallel', type: 'parallel',
}); };
} }
defineGAgents(gAgents) { defineGAgents(gAgents) {
@ -2004,7 +2123,10 @@
this.currentNest = last(this.nesting); this.currentNest = last(this.nesting);
this.currentSection = last(this.currentNest.sections); this.currentSection = last(this.currentNest.sections);
if(nested.hasContent) { if(!nested.hasContent) {
throw new Error('Empty block');
}
this.defineGAgents(nested.gAgents); this.defineGAgents(nested.gAgents);
addBounds( addBounds(
this.gAgents, this.gAgents,
@ -2021,9 +2143,6 @@
right: nested.rightGAgent.id, right: nested.rightGAgent.id,
type: 'block end', type: 'block end',
}); });
} else {
throw new Error('Empty block');
}
} }
makeGroupDetails(pAgents, alias) { makeGroupDetails(pAgents, alias) {
@ -2065,7 +2184,7 @@
}; };
} }
handleGroupBegin({agents, blockType, tag, label, alias}) { handleGroupBegin({agents, blockType, tag, label, alias, parallel}) {
const details = this.makeGroupDetails(agents, alias); const details = this.makeGroupDetails(agents, alias);
details.gAgentsContained.forEach((gAgent) => { details.gAgentsContained.forEach((gAgent) => {
@ -2075,7 +2194,10 @@
this.updateGAgentState(gAgent, {covered: true}); this.updateGAgentState(gAgent, {covered: true});
}); });
this.activeGroups.set(alias, details); this.activeGroups.set(alias, details);
this.addStage(this.setGAgentVis(details.gAgents, true, 'box')); this.addImpStage(
this.setGAgentVis(details.gAgents, true, 'box'),
{parallel}
);
this.addStage({ this.addStage({
blockType, blockType,
canHide: false, canHide: false,
@ -2084,7 +2206,7 @@
right: details.rightGAgent.id, right: details.rightGAgent.id,
tag: this.textFormatter(tag), tag: this.textFormatter(tag),
type: 'block begin', type: 'block begin',
}); }, {parallel});
} }
endGroup({name}) { endGroup({name}) {
@ -2111,7 +2233,7 @@
handleMark({name}) { handleMark({name}) {
this.markers.add(name); this.markers.add(name);
this.addStage({name, type: 'mark'}, false); this.addStage({name, type: 'mark'}, {isVisible: false});
} }
handleDivider({mode, height, label}) { handleDivider({mode, height, label}) {
@ -2120,14 +2242,14 @@
height, height,
mode, mode,
type: 'divider', type: 'divider',
}, false); }, {isVisible: false});
} }
handleAsync({target}) { handleAsync({target}) {
if(target !== '' && !this.markers.has(target)) { if(target !== '' && !this.markers.has(target)) {
throw new Error('Unknown marker: ' + target); throw new Error('Unknown marker: ' + target);
} }
this.addStage({target, type: 'async'}, false); this.addStage({target, type: 'async'}, {isVisible: false});
} }
handleLabelPattern({pattern}) { handleLabelPattern({pattern}) {
@ -2281,7 +2403,7 @@
return gAgents; return gAgents;
} }
_handlePartialConnect(agents) { _handlePartialConnect(agents, parallel) {
const flags = this.filterConnectFlags(agents); const flags = this.filterConnectFlags(agents);
const gAgents = agents.map(this.toGAgent); const gAgents = agents.map(this.toGAgent);
@ -2298,19 +2420,22 @@
.map(this.toGAgent) .map(this.toGAgent)
.filter((gAgent) => !gAgent.isVirtualSource) .filter((gAgent) => !gAgent.isVirtualSource)
); );
this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box')); this.addImpStage(
this.setGAgentVis(implicitBeginGAgents, true, 'box'),
{parallel}
);
return {flags, gAgents}; return {flags, gAgents};
} }
_makeConnectParallelStages(flags, connectStage) { _makeConnectParallelStages(flags, connectStage) {
return [ return this.makeParallel([
this.setGAgentVis(flags.beginGAgents, true, 'box', true), this.setGAgentVis(flags.beginGAgents, true, 'box', true),
this.setGAgentHighlight(flags.startGAgents, true, true), this.setGAgentHighlight(flags.startGAgents, true, true),
connectStage, connectStage,
this.setGAgentHighlight(flags.stopGAgents, false, true), this.setGAgentHighlight(flags.stopGAgents, false, true),
this.setGAgentVis(flags.endGAgents, false, 'cross', true), this.setGAgentVis(flags.endGAgents, false, 'cross', true),
]; ]);
} }
_isSelfConnect(agents) { _isSelfConnect(agents) {
@ -2325,7 +2450,7 @@
return true; return true;
} }
handleConnect({agents, label, options}) { handleConnect({agents, label, options, parallel}) {
if(this._isSelfConnect(agents)) { if(this._isSelfConnect(agents)) {
const tag = {}; const tag = {};
this.handleConnectDelayBegin({ this.handleConnectDelayBegin({
@ -2333,6 +2458,7 @@
ln: 0, ln: 0,
options, options,
tag, tag,
parallel,
}); });
this.handleConnectDelayEnd({ this.handleConnectDelayEnd({
agent: agents[1], agent: agents[1],
@ -2343,7 +2469,7 @@
return; return;
} }
let {flags, gAgents} = this._handlePartialConnect(agents); let {flags, gAgents} = this._handlePartialConnect(agents, parallel);
gAgents = this.expandGroupedGAgentConnection(gAgents); gAgents = this.expandGroupedGAgentConnection(gAgents);
gAgents = this.expandVirtualSourceAgents(gAgents); gAgents = this.expandVirtualSourceAgents(gAgents);
@ -2355,19 +2481,19 @@
type: 'connect', type: 'connect',
}; };
this.addParallelStages(this._makeConnectParallelStages( this.addStage(
flags, this._makeConnectParallelStages(flags, connectStage),
connectStage {parallel}
)); );
} }
handleConnectDelayBegin({agent, tag, options, ln}) { handleConnectDelayBegin({agent, tag, options, ln, parallel}) {
const dcs = this.currentSection.delayedConnections; const dcs = this.currentSection.delayedConnections;
if(dcs.has(tag)) { if(dcs.has(tag)) {
throw new Error('Duplicate delayed connection "' + tag + '"'); throw new Error('Duplicate delayed connection "' + tag + '"');
} }
const {flags, gAgents} = this._handlePartialConnect([agent]); const {flags, gAgents} = this._handlePartialConnect([agent], parallel);
const uniqueTag = this.nextVirtualAgentName(); const uniqueTag = this.nextVirtualAgentName();
const connectStage = { const connectStage = {
@ -2380,20 +2506,20 @@
dcs.set(tag, {connectStage, gAgents, ln, tag, uniqueTag}); dcs.set(tag, {connectStage, gAgents, ln, tag, uniqueTag});
this.addParallelStages(this._makeConnectParallelStages( this.addStage(
flags, this._makeConnectParallelStages(flags, connectStage),
connectStage {parallel}
)); );
} }
handleConnectDelayEnd({agent, tag, label, options}) { handleConnectDelayEnd({agent, tag, label, options, parallel}) {
const dcs = this.currentSection.delayedConnections; const dcs = this.currentSection.delayedConnections;
const dcInfo = dcs.get(tag); const dcInfo = dcs.get(tag);
if(!dcInfo) { if(!dcInfo) {
throw new Error('Unknown delayed connection "' + tag + '"'); throw new Error('Unknown delayed connection "' + tag + '"');
} }
let {flags, gAgents} = this._handlePartialConnect([agent]); let {flags, gAgents} = this._handlePartialConnect([agent], parallel);
gAgents = this.expandGroupedGAgentConnection([ gAgents = this.expandGroupedGAgentConnection([
...dcInfo.gAgents, ...dcInfo.gAgents,
@ -2422,15 +2548,15 @@
type: 'connect-delay-end', type: 'connect-delay-end',
}; };
this.addParallelStages(this._makeConnectParallelStages( this.addStage(
flags, this._makeConnectParallelStages(flags, connectEndStage),
connectEndStage {parallel}
)); );
dcs.delete(tag); dcs.delete(tag);
} }
handleNote({type, agents, mode, label}) { handleNote({type, agents, mode, label, parallel}) {
let gAgents = null; let gAgents = null;
if(agents.length === 0) { if(agents.length === 0) {
gAgents = NOTE_DEFAULT_G_AGENTS[type] || []; gAgents = NOTE_DEFAULT_G_AGENTS[type] || [];
@ -2446,15 +2572,14 @@
throw new Error('note between requires at least 2 agents'); throw new Error('note between requires at least 2 agents');
} }
this.addStage(this.setGAgentVis(gAgents, true, 'box'));
this.defineGAgents(gAgents); this.defineGAgents(gAgents);
this.addImpStage(this.setGAgentVis(gAgents, true, 'box'), {parallel});
this.addStage({ this.addStage({
agentIDs, agentIDs,
label: this.textFormatter(label), label: this.textFormatter(label),
mode, mode,
type, type,
}); }, {parallel});
} }
handleAgentDefine({agents}) { handleAgentDefine({agents}) {
@ -2482,13 +2607,13 @@
}); });
} }
handleAgentBegin({agents, mode}) { handleAgentBegin({agents, mode, parallel}) {
const gAgents = agents.map(this.toGAgent); const gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents); this.validateGAgents(gAgents);
this.addStage(this.setGAgentVis(gAgents, true, mode, true)); this.addStage(this.setGAgentVis(gAgents, true, mode, true), {parallel});
} }
handleAgentEnd({agents, mode}) { handleAgentEnd({agents, mode, parallel}) {
const groupPAgents = (agents const groupPAgents = (agents
.filter((pAgent) => this.activeGroups.has(pAgent.name)) .filter((pAgent) => this.activeGroups.has(pAgent.name))
); );
@ -2497,11 +2622,11 @@
.map(this.toGAgent) .map(this.toGAgent)
); );
this.validateGAgents(gAgents); this.validateGAgents(gAgents);
this.addParallelStages([ this.addStage(this.makeParallel([
this.setGAgentHighlight(gAgents, false), this.setGAgentHighlight(gAgents, false),
this.setGAgentVis(gAgents, false, mode, true), this.setGAgentVis(gAgents, false, mode, true),
...groupPAgents.map(this.endGroup), ...groupPAgents.map(this.endGroup),
]); ]), {parallel});
} }
handleStage(stage) { handleStage(stage) {
@ -2570,10 +2695,10 @@
this._checkSectionEnd(); this._checkSectionEnd();
const terminators = meta.terminators || 'none'; const terminators = meta.terminators || 'none';
this.addParallelStages([ this.addStage(this.makeParallel([
this.setGAgentHighlight(this.gAgents, false), this.setGAgentHighlight(this.gAgents, false),
this.setGAgentVis(this.gAgents, false, terminators), this.setGAgentVis(this.gAgents, false, terminators),
]); ]));
this._finalise(globals); this._finalise(globals);
@ -3026,6 +3151,14 @@
'red', 'red',
]; ];
const PARALLEL_TASKS = [
'begin',
'end',
'note',
'state',
'text',
];
const makeCommands = ((() => { const makeCommands = ((() => {
function agentListTo(exit, next = 1) { function agentListTo(exit, next = 1) {
return { return {
@ -3386,10 +3519,25 @@
}}, }},
}; };
return (arrows) => ({ return (arrows) => {
const arrowConnect = makeCMConnect(arrows);
const parallel = {};
for(const task of PARALLEL_TASKS) {
parallel[task] = BASE_THEN[task];
}
Object.assign(parallel, arrowConnect);
return {
type: 'error line-error', type: 'error line-error',
then: Object.assign({}, BASE_THEN, makeCMConnect(arrows)), then: Object.assign(
}); {},
BASE_THEN,
{'&': {type: 'keyword', then: parallel}},
arrowConnect
),
};
};
})()); })());
/* eslint-enable sort-keys */ /* eslint-enable sort-keys */
@ -3637,6 +3785,7 @@
if(state.currentType === NO_TOKEN) { if(state.currentType === NO_TOKEN) {
if(stream.sol()) { if(stream.sol()) {
state.line.length = 0; state.line.length = 0;
state.valid = true;
} }
if(!this._tokenBegin(stream, state)) { if(!this._tokenBegin(stream, state)) {
return ''; return '';
@ -4070,36 +4219,40 @@
return result; return result;
} }
/* eslint-disable sort-keys */ // Maybe later const BLOCK_TYPES = new Map();
BLOCK_TYPES.set('if', {
const BLOCK_TYPES = {
'if': {
type: 'block begin',
blockType: 'if', blockType: 'if',
skip: [],
tag: 'if', tag: 'if',
skip: [], type: 'block begin',
}, });
'else': { BLOCK_TYPES.set('else', {
type: 'block split',
blockType: 'else', blockType: 'else',
tag: 'else',
skip: ['if'], skip: ['if'],
}, tag: 'else',
'repeat': { type: 'block split',
type: 'block begin', });
BLOCK_TYPES.set('repeat', {
blockType: 'repeat', blockType: 'repeat',
skip: [],
tag: 'repeat', tag: 'repeat',
skip: [],
},
'group': {
type: 'block begin', type: 'block begin',
});
BLOCK_TYPES.set('group', {
blockType: 'group', blockType: 'group',
tag: '',
skip: [], skip: [],
}, tag: '',
}; type: 'block begin',
});
const CONNECT = { const CONNECT = {
agentFlags: {
'!': {flag: 'end'},
'*': {allowBlankName: true, blankNameFlag: 'source', flag: 'begin'},
'+': {flag: 'start'},
'-': {flag: 'stop'},
},
types: ((() => { types: ((() => {
const lTypes = [ const lTypes = [
{tok: '', type: 0}, {tok: '', type: 0},
@ -4125,21 +4278,14 @@
arrows.forEach((arrow) => { arrows.forEach((arrow) => {
types.set(arrow.map((part) => part.tok).join(''), { types.set(arrow.map((part) => part.tok).join(''), {
line: arrow[1].type,
left: arrow[0].type, left: arrow[0].type,
line: arrow[1].type,
right: arrow[2].type, right: arrow[2].type,
}); });
}); });
return types; return types;
})()), })()),
agentFlags: {
'*': {flag: 'begin', allowBlankName: true, blankNameFlag: 'source'},
'+': {flag: 'start'},
'-': {flag: 'stop'},
'!': {flag: 'end'},
},
}; };
const TERMINATOR_TYPES = [ const TERMINATOR_TYPES = [
@ -4150,73 +4296,70 @@
'bar', 'bar',
]; ];
const NOTE_TYPES = { const NOTE_TYPES = new Map();
'text': { NOTE_TYPES.set('text', {
mode: 'text', mode: 'text',
types: { types: {
'left': { 'left': {
type: 'note left',
skip: ['of'],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note left',
}, },
'right': { 'right': {
type: 'note right',
skip: ['of'],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note right',
}, },
}, },
}, });
'note': { NOTE_TYPES.set('note', {
mode: 'note', mode: 'note',
types: { types: {
'over': { 'between': {
type: 'note over',
skip: [],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 2,
skip: [],
type: 'note between',
}, },
'left': { 'left': {
type: 'note left',
skip: ['of'],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note left',
},
'over': {
max: Number.POSITIVE_INFINITY,
min: 0,
skip: [],
type: 'note over',
}, },
'right': { 'right': {
type: 'note right', max: Number.POSITIVE_INFINITY,
skip: ['of'],
min: 0, min: 0,
max: Number.POSITIVE_INFINITY, skip: ['of'],
}, type: 'note right',
'between': {
type: 'note between',
skip: [],
min: 2,
max: Number.POSITIVE_INFINITY,
}, },
}, },
}, });
'state': { NOTE_TYPES.set('state', {
mode: 'state', mode: 'state',
types: { types: {
'over': {type: 'note over', skip: [], min: 1, max: 1}, 'over': {max: 1, min: 1, skip: [], type: 'note over'},
}, },
}, });
};
const DIVIDER_TYPES = { const DIVIDER_TYPES = new Map();
'line': {defaultHeight: 6}, DIVIDER_TYPES.set('line', {defaultHeight: 6});
'space': {defaultHeight: 6}, DIVIDER_TYPES.set('space', {defaultHeight: 6});
'delay': {defaultHeight: 30}, DIVIDER_TYPES.set('delay', {defaultHeight: 30});
'tear': {defaultHeight: 6}, DIVIDER_TYPES.set('tear', {defaultHeight: 6});
};
const AGENT_MANIPULATION_TYPES = { const AGENT_MANIPULATION_TYPES = new Map();
'define': {type: 'agent define'}, AGENT_MANIPULATION_TYPES.set('define', {type: 'agent define'});
'begin': {type: 'agent begin', mode: 'box'}, AGENT_MANIPULATION_TYPES.set('begin', {mode: 'box', type: 'agent begin'});
'end': {type: 'agent end', mode: 'cross'}, AGENT_MANIPULATION_TYPES.set('end', {mode: 'cross', type: 'agent end'});
};
function makeError(message, token = null) { function makeError(message, token = null) {
let suffix = ''; let suffix = '';
@ -4316,8 +4459,8 @@
throw makeError('Missing agent name', errPosToken); throw makeError('Missing agent name', errPosToken);
} }
return { return {
name: joinLabel(line, start, aliasSep),
alias: joinLabel(line, aliasSep + 1, end), alias: joinLabel(line, aliasSep + 1, end),
name: joinLabel(line, start, aliasSep),
}; };
} }
@ -4344,13 +4487,13 @@
blankNameFlags.push(flag.blankNameFlag); blankNameFlags.push(flag.blankNameFlag);
} }
const {name, alias} = readAgentAlias(line, p, end, { const {name, alias} = readAgentAlias(line, p, end, {
enableAlias: aliases,
allowBlankName, allowBlankName,
enableAlias: aliases,
}); });
return { return {
name,
alias, alias,
flags: name ? flags : blankNameFlags, flags: name ? flags : blankNameFlags,
name,
}; };
} }
@ -4417,7 +4560,7 @@
}); });
const mode = joinLabel(line, 1, heightSep) || 'line'; const mode = joinLabel(line, 1, heightSep) || 'line';
if(!DIVIDER_TYPES[mode]) { if(!DIVIDER_TYPES.has(mode)) {
throw makeError('Unknown divider type', line[1]); throw makeError('Unknown divider type', line[1]);
} }
@ -4425,17 +4568,17 @@
line, line,
heightSep + 2, heightSep + 2,
labelSep, labelSep,
DIVIDER_TYPES[mode].defaultHeight DIVIDER_TYPES.get(mode).defaultHeight
); );
if(Number.isNaN(height) || height < 0) { if(Number.isNaN(height) || height < 0) {
throw makeError('Invalid divider height', line[heightSep + 2]); throw makeError('Invalid divider height', line[heightSep + 2]);
} }
return { return {
type: 'divider',
mode,
height, height,
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
mode,
type: 'divider',
}; };
}}, }},
@ -4447,8 +4590,8 @@
raw = joinLabel(line, 1); raw = joinLabel(line, 1);
} }
return { return {
type: 'label pattern',
pattern: parsePattern(raw), pattern: parsePattern(raw),
type: 'label pattern',
}; };
}}, }},
@ -4460,7 +4603,7 @@
}}, }},
{begin: [], fn: (line) => { // Block {begin: [], fn: (line) => { // Block
const type = BLOCK_TYPES[tokenKeyword(line[0])]; const type = BLOCK_TYPES.get(tokenKeyword(line[0]));
if(!type) { if(!type) {
return null; return null;
} }
@ -4470,10 +4613,10 @@
} }
skip = skipOver(line, skip, [':']); skip = skipOver(line, skip, [':']);
return { return {
type: type.type,
blockType: type.blockType, blockType: type.blockType,
tag: type.tag,
label: joinLabel(line, skip), label: joinLabel(line, skip),
tag: type.tag,
type: type.type,
}; };
}}, }},
@ -4495,17 +4638,17 @@
throw makeError('Reference must have an alias', line[labelSep]); throw makeError('Reference must have an alias', line[labelSep]);
} }
return { return {
type: 'group begin',
agents, agents,
blockType: 'ref',
tag: 'ref',
label: def.name,
alias: def.alias, alias: def.alias,
blockType: 'ref',
label: def.name,
tag: 'ref',
type: 'group begin',
}; };
}}, }},
{begin: [], fn: (line) => { // Agent {begin: [], fn: (line) => { // Agent
const type = AGENT_MANIPULATION_TYPES[tokenKeyword(line[0])]; const type = AGENT_MANIPULATION_TYPES.get(tokenKeyword(line[0]));
if(!type || line.length <= 1) { if(!type || line.length <= 1) {
return null; return null;
} }
@ -4526,13 +4669,13 @@
target = joinLabel(line, 2, line.length - 1); target = joinLabel(line, 2, line.length - 1);
} }
return { return {
type: 'async',
target, target,
type: 'async',
}; };
}}, }},
{begin: [], fn: (line) => { // Note {begin: [], fn: (line) => { // Note
const mode = NOTE_TYPES[tokenKeyword(line[0])]; const mode = NOTE_TYPES.get(tokenKeyword(line[0]));
const labelSep = findTokens(line, [':']); const labelSep = findTokens(line, [':']);
if(!mode || labelSep === -1) { if(!mode || labelSep === -1) {
return null; return null;
@ -4551,10 +4694,10 @@
throw makeError('Too many agents for ' + mode.mode, line[0]); throw makeError('Too many agents for ' + mode.mode, line[0]);
} }
return { return {
type: type.type,
agents, agents,
mode: mode.mode,
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
mode: mode.mode,
type: type.type,
}; };
}}, }},
@ -4563,7 +4706,7 @@
const connectionToken = findFirstToken( const connectionToken = findFirstToken(
line, line,
CONNECT.types, CONNECT.types,
{start: 0, limit: labelSep - 1} {limit: labelSep - 1, start: 0}
); );
if(!connectionToken) { if(!connectionToken) {
return null; return null;
@ -4577,11 +4720,11 @@
if(tokenKeyword(line[0]) === '...') { if(tokenKeyword(line[0]) === '...') {
return { return {
type: 'connect-delay-end',
tag: joinLabel(line, 1, connectPos),
agent: readAgent(line, connectPos + 1, labelSep, readOpts), agent: readAgent(line, connectPos + 1, labelSep, readOpts),
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
options: connectionToken.value, options: connectionToken.value,
tag: joinLabel(line, 1, connectPos),
type: 'connect-delay-end',
}; };
} else if(tokenKeyword(line[connectPos + 1]) === '...') { } else if(tokenKeyword(line[connectPos + 1]) === '...') {
if(labelSep !== line.length) { if(labelSep !== line.length) {
@ -4591,20 +4734,20 @@
); );
} }
return { return {
type: 'connect-delay-begin',
tag: joinLabel(line, connectPos + 2, labelSep),
agent: readAgent(line, 0, connectPos, readOpts), agent: readAgent(line, 0, connectPos, readOpts),
options: connectionToken.value, options: connectionToken.value,
tag: joinLabel(line, connectPos + 2, labelSep),
type: 'connect-delay-begin',
}; };
} else { } else {
return { return {
type: 'connect',
agents: [ agents: [
readAgent(line, 0, connectPos, readOpts), readAgent(line, 0, connectPos, readOpts),
readAgent(line, connectPos + 1, labelSep, readOpts), readAgent(line, connectPos + 1, labelSep, readOpts),
], ],
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
options: connectionToken.value, options: connectionToken.value,
type: 'connect',
}; };
} }
}}, }},
@ -4614,8 +4757,8 @@
return null; return null;
} }
return { return {
type: 'mark',
name: joinLabel(line, 0, line.length - 1), name: joinLabel(line, 0, line.length - 1),
type: 'mark',
}; };
}}, }},
@ -4638,33 +4781,44 @@
options.push(line[i].v); options.push(line[i].v);
} }
return { return {
type: 'agent options',
agent, agent,
options, options,
type: 'agent options',
}; };
}}, }},
]; ];
function parseLine(line, {meta, stages}) { function stageFromLine(line, meta) {
let stage = null;
for(const {begin, fn} of PARSERS) { for(const {begin, fn} of PARSERS) {
if(skipOver(line, 0, begin) !== begin.length) { if(skipOver(line, 0, begin) !== begin.length) {
continue; continue;
} }
stage = fn(line, meta); const stage = fn(line, meta);
if(stage) { if(stage) {
break; return stage;
} }
} }
return null;
}
function parseLine(line, {meta, stages}) {
let parallel = false;
const [start] = line;
if(tokenKeyword(start) === '&') {
parallel = true;
line.splice(0, 1);
}
const stage = stageFromLine(line, meta);
if(!stage) { if(!stage) {
throw makeError( throw makeError('Unrecognised command: ' + joinLabel(line), line[0]);
'Unrecognised command: ' + joinLabel(line), } else if(typeof stage === 'object') {
line[0] stage.ln = start.b.ln;
); stage.parallel = parallel;
}
if(typeof stage === 'object') {
stage.ln = line[0].b.ln;
stages.push(stage); stages.push(stage);
} else if(parallel) {
throw makeError('Metadata cannot be parallel', start);
} }
} }
@ -4680,12 +4834,12 @@
parseLines(lines, src) { parseLines(lines, src) {
const result = { const result = {
meta: { meta: {
title: '',
theme: '',
code: src, code: src,
terminators: 'none',
headers: 'box', headers: 'box',
terminators: 'none',
textFormatter: parseMarkdown, textFormatter: parseMarkdown,
theme: '',
title: '',
}, },
stages: [], stages: [],
}; };

File diff suppressed because one or more lines are too long

View File

@ -1643,6 +1643,87 @@
} }
} }
function extractParallel(target, stages) {
for(const stage of stages) {
if(!stage) {
continue;
}
if(stage.type === 'parallel') {
extractParallel(target, stage.stages);
} else {
target.push(stage);
}
}
}
function checkAgentConflicts(allStages) {
const createIDs = flatMap(
allStages
.filter((stage) => (stage.type === 'agent begin')),
(stage) => stage.agentIDs
);
for(const stage of allStages) {
if(stage.type !== 'agent end') {
continue;
}
for(const id of stage.agentIDs) {
if(createIDs.indexOf(id) !== -1) {
return 'Cannot create and destroy ' + id + ' simultaneously';
}
}
}
return null;
}
function checkReferenceConflicts(allStages) {
const leftIDs = allStages
.filter((stage) => (stage.type === 'block begin'))
.map((stage) => stage.left);
for(const stage of allStages) {
if(stage.type !== 'block end') {
continue;
}
if(leftIDs.indexOf(stage.left) !== -1) {
return 'Cannot create and destroy reference simultaneously';
}
}
return null;
}
function checkDelayedConflicts(allStages) {
const tags = allStages
.filter((stage) => (stage.type === 'connect-delay-begin'))
.map((stage) => stage.tag);
for(const stage of allStages) {
if(stage.type !== 'connect-delay-end') {
continue;
}
if(tags.indexOf(stage.tag) !== -1) {
return 'Cannot start and finish delayed connection simultaneously';
}
}
return null;
}
function errorForParallel(existing, latest) {
if(!existing) {
return 'Nothing to run statement in parallel with';
}
const allStages = [];
extractParallel(allStages, [existing]);
extractParallel(allStages, [latest]);
return (
checkAgentConflicts(allStages) ||
checkReferenceConflicts(allStages) ||
checkDelayedConflicts(allStages)
);
}
function swapBegin(stage, mode) { function swapBegin(stage, mode) {
if(stage.type === 'agent begin') { if(stage.type === 'agent begin') {
stage.mode = mode; stage.mode = mode;
@ -1758,37 +1839,75 @@
}); });
} }
addStage(stage, isVisible = true) { addStage(stage, {isVisible = true, parallel = false} = {}) {
if(!stage) {
return;
}
if(isVisible) {
this.currentNest.hasContent = true;
}
if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine;
}
const {stages} = this.currentSection;
if(parallel) {
const target = last(stages);
const err = errorForParallel(target, stage);
if(err) {
throw new Error(err);
}
const pstage = this.makeParallel([target, stage]);
pstage.ln = stage.ln;
-- stages.length;
stages.push(pstage);
} else {
stages.push(stage);
}
}
addImpStage(stage, {parallel = false} = {}) {
if(!stage) { if(!stage) {
return; return;
} }
if(typeof stage.ln === 'undefined') { if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine; stage.ln = this.latestLine;
} }
this.currentSection.stages.push(stage); const {stages} = this.currentSection;
if(isVisible) { if(parallel) {
this.currentNest.hasContent = true; const target = stages[stages.length - 2];
if(!target) {
throw new Error('Nothing to run statement in parallel with');
}
if(errorForParallel(target, stage)) {
stages.splice(stages.length - 1, 0, stage);
} else {
const pstage = this.makeParallel([target, stage]);
pstage.ln = stage.ln;
stages.splice(stages.length - 2, 1, pstage);
}
} else {
stages.push(stage);
} }
} }
addParallelStages(stages) { makeParallel(stages) {
const viableStages = stages.filter((stage) => Boolean(stage)); const viableStages = [];
extractParallel(viableStages, stages);
if(viableStages.length === 0) { if(viableStages.length === 0) {
return; return null;
} }
if(viableStages.length === 1) { if(viableStages.length === 1) {
this.addStage(viableStages[0]); return viableStages[0];
return;
} }
viableStages.forEach((stage) => { viableStages.forEach((stage) => {
if(typeof stage.ln === 'undefined') { if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine; stage.ln = this.latestLine;
} }
}); });
this.addStage({ return {
stages: viableStages, stages: viableStages,
type: 'parallel', type: 'parallel',
}); };
} }
defineGAgents(gAgents) { defineGAgents(gAgents) {
@ -2004,7 +2123,10 @@
this.currentNest = last(this.nesting); this.currentNest = last(this.nesting);
this.currentSection = last(this.currentNest.sections); this.currentSection = last(this.currentNest.sections);
if(nested.hasContent) { if(!nested.hasContent) {
throw new Error('Empty block');
}
this.defineGAgents(nested.gAgents); this.defineGAgents(nested.gAgents);
addBounds( addBounds(
this.gAgents, this.gAgents,
@ -2021,9 +2143,6 @@
right: nested.rightGAgent.id, right: nested.rightGAgent.id,
type: 'block end', type: 'block end',
}); });
} else {
throw new Error('Empty block');
}
} }
makeGroupDetails(pAgents, alias) { makeGroupDetails(pAgents, alias) {
@ -2065,7 +2184,7 @@
}; };
} }
handleGroupBegin({agents, blockType, tag, label, alias}) { handleGroupBegin({agents, blockType, tag, label, alias, parallel}) {
const details = this.makeGroupDetails(agents, alias); const details = this.makeGroupDetails(agents, alias);
details.gAgentsContained.forEach((gAgent) => { details.gAgentsContained.forEach((gAgent) => {
@ -2075,7 +2194,10 @@
this.updateGAgentState(gAgent, {covered: true}); this.updateGAgentState(gAgent, {covered: true});
}); });
this.activeGroups.set(alias, details); this.activeGroups.set(alias, details);
this.addStage(this.setGAgentVis(details.gAgents, true, 'box')); this.addImpStage(
this.setGAgentVis(details.gAgents, true, 'box'),
{parallel}
);
this.addStage({ this.addStage({
blockType, blockType,
canHide: false, canHide: false,
@ -2084,7 +2206,7 @@
right: details.rightGAgent.id, right: details.rightGAgent.id,
tag: this.textFormatter(tag), tag: this.textFormatter(tag),
type: 'block begin', type: 'block begin',
}); }, {parallel});
} }
endGroup({name}) { endGroup({name}) {
@ -2111,7 +2233,7 @@
handleMark({name}) { handleMark({name}) {
this.markers.add(name); this.markers.add(name);
this.addStage({name, type: 'mark'}, false); this.addStage({name, type: 'mark'}, {isVisible: false});
} }
handleDivider({mode, height, label}) { handleDivider({mode, height, label}) {
@ -2120,14 +2242,14 @@
height, height,
mode, mode,
type: 'divider', type: 'divider',
}, false); }, {isVisible: false});
} }
handleAsync({target}) { handleAsync({target}) {
if(target !== '' && !this.markers.has(target)) { if(target !== '' && !this.markers.has(target)) {
throw new Error('Unknown marker: ' + target); throw new Error('Unknown marker: ' + target);
} }
this.addStage({target, type: 'async'}, false); this.addStage({target, type: 'async'}, {isVisible: false});
} }
handleLabelPattern({pattern}) { handleLabelPattern({pattern}) {
@ -2281,7 +2403,7 @@
return gAgents; return gAgents;
} }
_handlePartialConnect(agents) { _handlePartialConnect(agents, parallel) {
const flags = this.filterConnectFlags(agents); const flags = this.filterConnectFlags(agents);
const gAgents = agents.map(this.toGAgent); const gAgents = agents.map(this.toGAgent);
@ -2298,19 +2420,22 @@
.map(this.toGAgent) .map(this.toGAgent)
.filter((gAgent) => !gAgent.isVirtualSource) .filter((gAgent) => !gAgent.isVirtualSource)
); );
this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box')); this.addImpStage(
this.setGAgentVis(implicitBeginGAgents, true, 'box'),
{parallel}
);
return {flags, gAgents}; return {flags, gAgents};
} }
_makeConnectParallelStages(flags, connectStage) { _makeConnectParallelStages(flags, connectStage) {
return [ return this.makeParallel([
this.setGAgentVis(flags.beginGAgents, true, 'box', true), this.setGAgentVis(flags.beginGAgents, true, 'box', true),
this.setGAgentHighlight(flags.startGAgents, true, true), this.setGAgentHighlight(flags.startGAgents, true, true),
connectStage, connectStage,
this.setGAgentHighlight(flags.stopGAgents, false, true), this.setGAgentHighlight(flags.stopGAgents, false, true),
this.setGAgentVis(flags.endGAgents, false, 'cross', true), this.setGAgentVis(flags.endGAgents, false, 'cross', true),
]; ]);
} }
_isSelfConnect(agents) { _isSelfConnect(agents) {
@ -2325,7 +2450,7 @@
return true; return true;
} }
handleConnect({agents, label, options}) { handleConnect({agents, label, options, parallel}) {
if(this._isSelfConnect(agents)) { if(this._isSelfConnect(agents)) {
const tag = {}; const tag = {};
this.handleConnectDelayBegin({ this.handleConnectDelayBegin({
@ -2333,6 +2458,7 @@
ln: 0, ln: 0,
options, options,
tag, tag,
parallel,
}); });
this.handleConnectDelayEnd({ this.handleConnectDelayEnd({
agent: agents[1], agent: agents[1],
@ -2343,7 +2469,7 @@
return; return;
} }
let {flags, gAgents} = this._handlePartialConnect(agents); let {flags, gAgents} = this._handlePartialConnect(agents, parallel);
gAgents = this.expandGroupedGAgentConnection(gAgents); gAgents = this.expandGroupedGAgentConnection(gAgents);
gAgents = this.expandVirtualSourceAgents(gAgents); gAgents = this.expandVirtualSourceAgents(gAgents);
@ -2355,19 +2481,19 @@
type: 'connect', type: 'connect',
}; };
this.addParallelStages(this._makeConnectParallelStages( this.addStage(
flags, this._makeConnectParallelStages(flags, connectStage),
connectStage {parallel}
)); );
} }
handleConnectDelayBegin({agent, tag, options, ln}) { handleConnectDelayBegin({agent, tag, options, ln, parallel}) {
const dcs = this.currentSection.delayedConnections; const dcs = this.currentSection.delayedConnections;
if(dcs.has(tag)) { if(dcs.has(tag)) {
throw new Error('Duplicate delayed connection "' + tag + '"'); throw new Error('Duplicate delayed connection "' + tag + '"');
} }
const {flags, gAgents} = this._handlePartialConnect([agent]); const {flags, gAgents} = this._handlePartialConnect([agent], parallel);
const uniqueTag = this.nextVirtualAgentName(); const uniqueTag = this.nextVirtualAgentName();
const connectStage = { const connectStage = {
@ -2380,20 +2506,20 @@
dcs.set(tag, {connectStage, gAgents, ln, tag, uniqueTag}); dcs.set(tag, {connectStage, gAgents, ln, tag, uniqueTag});
this.addParallelStages(this._makeConnectParallelStages( this.addStage(
flags, this._makeConnectParallelStages(flags, connectStage),
connectStage {parallel}
)); );
} }
handleConnectDelayEnd({agent, tag, label, options}) { handleConnectDelayEnd({agent, tag, label, options, parallel}) {
const dcs = this.currentSection.delayedConnections; const dcs = this.currentSection.delayedConnections;
const dcInfo = dcs.get(tag); const dcInfo = dcs.get(tag);
if(!dcInfo) { if(!dcInfo) {
throw new Error('Unknown delayed connection "' + tag + '"'); throw new Error('Unknown delayed connection "' + tag + '"');
} }
let {flags, gAgents} = this._handlePartialConnect([agent]); let {flags, gAgents} = this._handlePartialConnect([agent], parallel);
gAgents = this.expandGroupedGAgentConnection([ gAgents = this.expandGroupedGAgentConnection([
...dcInfo.gAgents, ...dcInfo.gAgents,
@ -2422,15 +2548,15 @@
type: 'connect-delay-end', type: 'connect-delay-end',
}; };
this.addParallelStages(this._makeConnectParallelStages( this.addStage(
flags, this._makeConnectParallelStages(flags, connectEndStage),
connectEndStage {parallel}
)); );
dcs.delete(tag); dcs.delete(tag);
} }
handleNote({type, agents, mode, label}) { handleNote({type, agents, mode, label, parallel}) {
let gAgents = null; let gAgents = null;
if(agents.length === 0) { if(agents.length === 0) {
gAgents = NOTE_DEFAULT_G_AGENTS[type] || []; gAgents = NOTE_DEFAULT_G_AGENTS[type] || [];
@ -2446,15 +2572,14 @@
throw new Error('note between requires at least 2 agents'); throw new Error('note between requires at least 2 agents');
} }
this.addStage(this.setGAgentVis(gAgents, true, 'box'));
this.defineGAgents(gAgents); this.defineGAgents(gAgents);
this.addImpStage(this.setGAgentVis(gAgents, true, 'box'), {parallel});
this.addStage({ this.addStage({
agentIDs, agentIDs,
label: this.textFormatter(label), label: this.textFormatter(label),
mode, mode,
type, type,
}); }, {parallel});
} }
handleAgentDefine({agents}) { handleAgentDefine({agents}) {
@ -2482,13 +2607,13 @@
}); });
} }
handleAgentBegin({agents, mode}) { handleAgentBegin({agents, mode, parallel}) {
const gAgents = agents.map(this.toGAgent); const gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents); this.validateGAgents(gAgents);
this.addStage(this.setGAgentVis(gAgents, true, mode, true)); this.addStage(this.setGAgentVis(gAgents, true, mode, true), {parallel});
} }
handleAgentEnd({agents, mode}) { handleAgentEnd({agents, mode, parallel}) {
const groupPAgents = (agents const groupPAgents = (agents
.filter((pAgent) => this.activeGroups.has(pAgent.name)) .filter((pAgent) => this.activeGroups.has(pAgent.name))
); );
@ -2497,11 +2622,11 @@
.map(this.toGAgent) .map(this.toGAgent)
); );
this.validateGAgents(gAgents); this.validateGAgents(gAgents);
this.addParallelStages([ this.addStage(this.makeParallel([
this.setGAgentHighlight(gAgents, false), this.setGAgentHighlight(gAgents, false),
this.setGAgentVis(gAgents, false, mode, true), this.setGAgentVis(gAgents, false, mode, true),
...groupPAgents.map(this.endGroup), ...groupPAgents.map(this.endGroup),
]); ]), {parallel});
} }
handleStage(stage) { handleStage(stage) {
@ -2570,10 +2695,10 @@
this._checkSectionEnd(); this._checkSectionEnd();
const terminators = meta.terminators || 'none'; const terminators = meta.terminators || 'none';
this.addParallelStages([ this.addStage(this.makeParallel([
this.setGAgentHighlight(this.gAgents, false), this.setGAgentHighlight(this.gAgents, false),
this.setGAgentVis(this.gAgents, false, terminators), this.setGAgentVis(this.gAgents, false, terminators),
]); ]));
this._finalise(globals); this._finalise(globals);
@ -3026,6 +3151,14 @@
'red', 'red',
]; ];
const PARALLEL_TASKS = [
'begin',
'end',
'note',
'state',
'text',
];
const makeCommands = ((() => { const makeCommands = ((() => {
function agentListTo(exit, next = 1) { function agentListTo(exit, next = 1) {
return { return {
@ -3386,10 +3519,25 @@
}}, }},
}; };
return (arrows) => ({ return (arrows) => {
const arrowConnect = makeCMConnect(arrows);
const parallel = {};
for(const task of PARALLEL_TASKS) {
parallel[task] = BASE_THEN[task];
}
Object.assign(parallel, arrowConnect);
return {
type: 'error line-error', type: 'error line-error',
then: Object.assign({}, BASE_THEN, makeCMConnect(arrows)), then: Object.assign(
}); {},
BASE_THEN,
{'&': {type: 'keyword', then: parallel}},
arrowConnect
),
};
};
})()); })());
/* eslint-enable sort-keys */ /* eslint-enable sort-keys */
@ -3637,6 +3785,7 @@
if(state.currentType === NO_TOKEN) { if(state.currentType === NO_TOKEN) {
if(stream.sol()) { if(stream.sol()) {
state.line.length = 0; state.line.length = 0;
state.valid = true;
} }
if(!this._tokenBegin(stream, state)) { if(!this._tokenBegin(stream, state)) {
return ''; return '';
@ -4070,36 +4219,40 @@
return result; return result;
} }
/* eslint-disable sort-keys */ // Maybe later const BLOCK_TYPES = new Map();
BLOCK_TYPES.set('if', {
const BLOCK_TYPES = {
'if': {
type: 'block begin',
blockType: 'if', blockType: 'if',
skip: [],
tag: 'if', tag: 'if',
skip: [], type: 'block begin',
}, });
'else': { BLOCK_TYPES.set('else', {
type: 'block split',
blockType: 'else', blockType: 'else',
tag: 'else',
skip: ['if'], skip: ['if'],
}, tag: 'else',
'repeat': { type: 'block split',
type: 'block begin', });
BLOCK_TYPES.set('repeat', {
blockType: 'repeat', blockType: 'repeat',
skip: [],
tag: 'repeat', tag: 'repeat',
skip: [],
},
'group': {
type: 'block begin', type: 'block begin',
});
BLOCK_TYPES.set('group', {
blockType: 'group', blockType: 'group',
tag: '',
skip: [], skip: [],
}, tag: '',
}; type: 'block begin',
});
const CONNECT = { const CONNECT = {
agentFlags: {
'!': {flag: 'end'},
'*': {allowBlankName: true, blankNameFlag: 'source', flag: 'begin'},
'+': {flag: 'start'},
'-': {flag: 'stop'},
},
types: ((() => { types: ((() => {
const lTypes = [ const lTypes = [
{tok: '', type: 0}, {tok: '', type: 0},
@ -4125,21 +4278,14 @@
arrows.forEach((arrow) => { arrows.forEach((arrow) => {
types.set(arrow.map((part) => part.tok).join(''), { types.set(arrow.map((part) => part.tok).join(''), {
line: arrow[1].type,
left: arrow[0].type, left: arrow[0].type,
line: arrow[1].type,
right: arrow[2].type, right: arrow[2].type,
}); });
}); });
return types; return types;
})()), })()),
agentFlags: {
'*': {flag: 'begin', allowBlankName: true, blankNameFlag: 'source'},
'+': {flag: 'start'},
'-': {flag: 'stop'},
'!': {flag: 'end'},
},
}; };
const TERMINATOR_TYPES = [ const TERMINATOR_TYPES = [
@ -4150,73 +4296,70 @@
'bar', 'bar',
]; ];
const NOTE_TYPES = { const NOTE_TYPES = new Map();
'text': { NOTE_TYPES.set('text', {
mode: 'text', mode: 'text',
types: { types: {
'left': { 'left': {
type: 'note left',
skip: ['of'],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note left',
}, },
'right': { 'right': {
type: 'note right',
skip: ['of'],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note right',
}, },
}, },
}, });
'note': { NOTE_TYPES.set('note', {
mode: 'note', mode: 'note',
types: { types: {
'over': { 'between': {
type: 'note over',
skip: [],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 2,
skip: [],
type: 'note between',
}, },
'left': { 'left': {
type: 'note left',
skip: ['of'],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note left',
},
'over': {
max: Number.POSITIVE_INFINITY,
min: 0,
skip: [],
type: 'note over',
}, },
'right': { 'right': {
type: 'note right', max: Number.POSITIVE_INFINITY,
skip: ['of'],
min: 0, min: 0,
max: Number.POSITIVE_INFINITY, skip: ['of'],
}, type: 'note right',
'between': {
type: 'note between',
skip: [],
min: 2,
max: Number.POSITIVE_INFINITY,
}, },
}, },
}, });
'state': { NOTE_TYPES.set('state', {
mode: 'state', mode: 'state',
types: { types: {
'over': {type: 'note over', skip: [], min: 1, max: 1}, 'over': {max: 1, min: 1, skip: [], type: 'note over'},
}, },
}, });
};
const DIVIDER_TYPES = { const DIVIDER_TYPES = new Map();
'line': {defaultHeight: 6}, DIVIDER_TYPES.set('line', {defaultHeight: 6});
'space': {defaultHeight: 6}, DIVIDER_TYPES.set('space', {defaultHeight: 6});
'delay': {defaultHeight: 30}, DIVIDER_TYPES.set('delay', {defaultHeight: 30});
'tear': {defaultHeight: 6}, DIVIDER_TYPES.set('tear', {defaultHeight: 6});
};
const AGENT_MANIPULATION_TYPES = { const AGENT_MANIPULATION_TYPES = new Map();
'define': {type: 'agent define'}, AGENT_MANIPULATION_TYPES.set('define', {type: 'agent define'});
'begin': {type: 'agent begin', mode: 'box'}, AGENT_MANIPULATION_TYPES.set('begin', {mode: 'box', type: 'agent begin'});
'end': {type: 'agent end', mode: 'cross'}, AGENT_MANIPULATION_TYPES.set('end', {mode: 'cross', type: 'agent end'});
};
function makeError(message, token = null) { function makeError(message, token = null) {
let suffix = ''; let suffix = '';
@ -4316,8 +4459,8 @@
throw makeError('Missing agent name', errPosToken); throw makeError('Missing agent name', errPosToken);
} }
return { return {
name: joinLabel(line, start, aliasSep),
alias: joinLabel(line, aliasSep + 1, end), alias: joinLabel(line, aliasSep + 1, end),
name: joinLabel(line, start, aliasSep),
}; };
} }
@ -4344,13 +4487,13 @@
blankNameFlags.push(flag.blankNameFlag); blankNameFlags.push(flag.blankNameFlag);
} }
const {name, alias} = readAgentAlias(line, p, end, { const {name, alias} = readAgentAlias(line, p, end, {
enableAlias: aliases,
allowBlankName, allowBlankName,
enableAlias: aliases,
}); });
return { return {
name,
alias, alias,
flags: name ? flags : blankNameFlags, flags: name ? flags : blankNameFlags,
name,
}; };
} }
@ -4417,7 +4560,7 @@
}); });
const mode = joinLabel(line, 1, heightSep) || 'line'; const mode = joinLabel(line, 1, heightSep) || 'line';
if(!DIVIDER_TYPES[mode]) { if(!DIVIDER_TYPES.has(mode)) {
throw makeError('Unknown divider type', line[1]); throw makeError('Unknown divider type', line[1]);
} }
@ -4425,17 +4568,17 @@
line, line,
heightSep + 2, heightSep + 2,
labelSep, labelSep,
DIVIDER_TYPES[mode].defaultHeight DIVIDER_TYPES.get(mode).defaultHeight
); );
if(Number.isNaN(height) || height < 0) { if(Number.isNaN(height) || height < 0) {
throw makeError('Invalid divider height', line[heightSep + 2]); throw makeError('Invalid divider height', line[heightSep + 2]);
} }
return { return {
type: 'divider',
mode,
height, height,
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
mode,
type: 'divider',
}; };
}}, }},
@ -4447,8 +4590,8 @@
raw = joinLabel(line, 1); raw = joinLabel(line, 1);
} }
return { return {
type: 'label pattern',
pattern: parsePattern(raw), pattern: parsePattern(raw),
type: 'label pattern',
}; };
}}, }},
@ -4460,7 +4603,7 @@
}}, }},
{begin: [], fn: (line) => { // Block {begin: [], fn: (line) => { // Block
const type = BLOCK_TYPES[tokenKeyword(line[0])]; const type = BLOCK_TYPES.get(tokenKeyword(line[0]));
if(!type) { if(!type) {
return null; return null;
} }
@ -4470,10 +4613,10 @@
} }
skip = skipOver(line, skip, [':']); skip = skipOver(line, skip, [':']);
return { return {
type: type.type,
blockType: type.blockType, blockType: type.blockType,
tag: type.tag,
label: joinLabel(line, skip), label: joinLabel(line, skip),
tag: type.tag,
type: type.type,
}; };
}}, }},
@ -4495,17 +4638,17 @@
throw makeError('Reference must have an alias', line[labelSep]); throw makeError('Reference must have an alias', line[labelSep]);
} }
return { return {
type: 'group begin',
agents, agents,
blockType: 'ref',
tag: 'ref',
label: def.name,
alias: def.alias, alias: def.alias,
blockType: 'ref',
label: def.name,
tag: 'ref',
type: 'group begin',
}; };
}}, }},
{begin: [], fn: (line) => { // Agent {begin: [], fn: (line) => { // Agent
const type = AGENT_MANIPULATION_TYPES[tokenKeyword(line[0])]; const type = AGENT_MANIPULATION_TYPES.get(tokenKeyword(line[0]));
if(!type || line.length <= 1) { if(!type || line.length <= 1) {
return null; return null;
} }
@ -4526,13 +4669,13 @@
target = joinLabel(line, 2, line.length - 1); target = joinLabel(line, 2, line.length - 1);
} }
return { return {
type: 'async',
target, target,
type: 'async',
}; };
}}, }},
{begin: [], fn: (line) => { // Note {begin: [], fn: (line) => { // Note
const mode = NOTE_TYPES[tokenKeyword(line[0])]; const mode = NOTE_TYPES.get(tokenKeyword(line[0]));
const labelSep = findTokens(line, [':']); const labelSep = findTokens(line, [':']);
if(!mode || labelSep === -1) { if(!mode || labelSep === -1) {
return null; return null;
@ -4551,10 +4694,10 @@
throw makeError('Too many agents for ' + mode.mode, line[0]); throw makeError('Too many agents for ' + mode.mode, line[0]);
} }
return { return {
type: type.type,
agents, agents,
mode: mode.mode,
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
mode: mode.mode,
type: type.type,
}; };
}}, }},
@ -4563,7 +4706,7 @@
const connectionToken = findFirstToken( const connectionToken = findFirstToken(
line, line,
CONNECT.types, CONNECT.types,
{start: 0, limit: labelSep - 1} {limit: labelSep - 1, start: 0}
); );
if(!connectionToken) { if(!connectionToken) {
return null; return null;
@ -4577,11 +4720,11 @@
if(tokenKeyword(line[0]) === '...') { if(tokenKeyword(line[0]) === '...') {
return { return {
type: 'connect-delay-end',
tag: joinLabel(line, 1, connectPos),
agent: readAgent(line, connectPos + 1, labelSep, readOpts), agent: readAgent(line, connectPos + 1, labelSep, readOpts),
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
options: connectionToken.value, options: connectionToken.value,
tag: joinLabel(line, 1, connectPos),
type: 'connect-delay-end',
}; };
} else if(tokenKeyword(line[connectPos + 1]) === '...') { } else if(tokenKeyword(line[connectPos + 1]) === '...') {
if(labelSep !== line.length) { if(labelSep !== line.length) {
@ -4591,20 +4734,20 @@
); );
} }
return { return {
type: 'connect-delay-begin',
tag: joinLabel(line, connectPos + 2, labelSep),
agent: readAgent(line, 0, connectPos, readOpts), agent: readAgent(line, 0, connectPos, readOpts),
options: connectionToken.value, options: connectionToken.value,
tag: joinLabel(line, connectPos + 2, labelSep),
type: 'connect-delay-begin',
}; };
} else { } else {
return { return {
type: 'connect',
agents: [ agents: [
readAgent(line, 0, connectPos, readOpts), readAgent(line, 0, connectPos, readOpts),
readAgent(line, connectPos + 1, labelSep, readOpts), readAgent(line, connectPos + 1, labelSep, readOpts),
], ],
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
options: connectionToken.value, options: connectionToken.value,
type: 'connect',
}; };
} }
}}, }},
@ -4614,8 +4757,8 @@
return null; return null;
} }
return { return {
type: 'mark',
name: joinLabel(line, 0, line.length - 1), name: joinLabel(line, 0, line.length - 1),
type: 'mark',
}; };
}}, }},
@ -4638,33 +4781,44 @@
options.push(line[i].v); options.push(line[i].v);
} }
return { return {
type: 'agent options',
agent, agent,
options, options,
type: 'agent options',
}; };
}}, }},
]; ];
function parseLine(line, {meta, stages}) { function stageFromLine(line, meta) {
let stage = null;
for(const {begin, fn} of PARSERS) { for(const {begin, fn} of PARSERS) {
if(skipOver(line, 0, begin) !== begin.length) { if(skipOver(line, 0, begin) !== begin.length) {
continue; continue;
} }
stage = fn(line, meta); const stage = fn(line, meta);
if(stage) { if(stage) {
break; return stage;
} }
} }
return null;
}
function parseLine(line, {meta, stages}) {
let parallel = false;
const [start] = line;
if(tokenKeyword(start) === '&') {
parallel = true;
line.splice(0, 1);
}
const stage = stageFromLine(line, meta);
if(!stage) { if(!stage) {
throw makeError( throw makeError('Unrecognised command: ' + joinLabel(line), line[0]);
'Unrecognised command: ' + joinLabel(line), } else if(typeof stage === 'object') {
line[0] stage.ln = start.b.ln;
); stage.parallel = parallel;
}
if(typeof stage === 'object') {
stage.ln = line[0].b.ln;
stages.push(stage); stages.push(stage);
} else if(parallel) {
throw makeError('Metadata cannot be parallel', start);
} }
} }
@ -4680,12 +4834,12 @@
parseLines(lines, src) { parseLines(lines, src) {
const result = { const result = {
meta: { meta: {
title: '',
theme: '',
code: src, code: src,
terminators: 'none',
headers: 'box', headers: 'box',
terminators: 'none',
textFormatter: parseMarkdown, textFormatter: parseMarkdown,
theme: '',
title: '',
}, },
stages: [], stages: [],
}; };

View File

@ -34,6 +34,14 @@ const AGENT_INFO_TYPES = [
'red', 'red',
]; ];
const PARALLEL_TASKS = [
'begin',
'end',
'note',
'state',
'text',
];
const makeCommands = ((() => { const makeCommands = ((() => {
function agentListTo(exit, next = 1) { function agentListTo(exit, next = 1) {
return { return {
@ -394,10 +402,25 @@ const makeCommands = ((() => {
}}, }},
}; };
return (arrows) => ({ return (arrows) => {
const arrowConnect = makeCMConnect(arrows);
const parallel = {};
for(const task of PARALLEL_TASKS) {
parallel[task] = BASE_THEN[task];
}
Object.assign(parallel, arrowConnect);
return {
type: 'error line-error', type: 'error line-error',
then: Object.assign({}, BASE_THEN, makeCMConnect(arrows)), then: Object.assign(
}); {},
BASE_THEN,
{'&': {type: 'keyword', then: parallel}},
arrowConnect
),
};
};
})()); })());
/* eslint-enable sort-keys */ /* eslint-enable sort-keys */
@ -645,6 +668,7 @@ export default class Mode {
if(state.currentType === NO_TOKEN) { if(state.currentType === NO_TOKEN) {
if(stream.sol()) { if(stream.sol()) {
state.line.length = 0; state.line.length = 0;
state.valid = true;
} }
if(!this._tokenBegin(stream, state)) { if(!this._tokenBegin(stream, state)) {
return ''; return '';

View File

@ -92,6 +92,23 @@ describe('Code Mirror Mode', () => {
]); ]);
}); });
it('highlights parallel statements', () => {
cm.getDoc().setValue('& A -> B');
expect(getTokens(0)).toEqual([
{type: 'keyword', v: '&'},
{type: 'variable', v: ' A'},
{type: 'keyword', v: ' ->'},
{type: 'variable', v: ' B'},
]);
});
it('highlights invalid parallel statements', () => {
cm.getDoc().setValue('& terminators cross');
expect(getTokens(0)[2].type).toContain('line-error');
});
it('does not consider quoted tokens as keywords', () => { it('does not consider quoted tokens as keywords', () => {
cm.getDoc().setValue('A "->" -> B'); cm.getDoc().setValue('A "->" -> B');
@ -449,6 +466,13 @@ describe('Code Mirror Mode', () => {
expect(getTokens(0)[3].type).toContain('line-error'); expect(getTokens(0)[3].type).toContain('line-error');
}); });
it('resets error handling on new lines with comments', () => {
cm.getDoc().setValue('nope\n#foo');
expect(getTokens(0)[0].type).toContain('line-error');
expect(getTokens(1)[0].type).not.toContain('line-error');
});
}); });
describe('autocomplete', () => { describe('autocomplete', () => {

View File

@ -193,6 +193,87 @@ function optimiseStages(stages) {
} }
} }
function extractParallel(target, stages) {
for(const stage of stages) {
if(!stage) {
continue;
}
if(stage.type === 'parallel') {
extractParallel(target, stage.stages);
} else {
target.push(stage);
}
}
}
function checkAgentConflicts(allStages) {
const createIDs = flatMap(
allStages
.filter((stage) => (stage.type === 'agent begin')),
(stage) => stage.agentIDs
);
for(const stage of allStages) {
if(stage.type !== 'agent end') {
continue;
}
for(const id of stage.agentIDs) {
if(createIDs.indexOf(id) !== -1) {
return 'Cannot create and destroy ' + id + ' simultaneously';
}
}
}
return null;
}
function checkReferenceConflicts(allStages) {
const leftIDs = allStages
.filter((stage) => (stage.type === 'block begin'))
.map((stage) => stage.left);
for(const stage of allStages) {
if(stage.type !== 'block end') {
continue;
}
if(leftIDs.indexOf(stage.left) !== -1) {
return 'Cannot create and destroy reference simultaneously';
}
}
return null;
}
function checkDelayedConflicts(allStages) {
const tags = allStages
.filter((stage) => (stage.type === 'connect-delay-begin'))
.map((stage) => stage.tag);
for(const stage of allStages) {
if(stage.type !== 'connect-delay-end') {
continue;
}
if(tags.indexOf(stage.tag) !== -1) {
return 'Cannot start and finish delayed connection simultaneously';
}
}
return null;
}
function errorForParallel(existing, latest) {
if(!existing) {
return 'Nothing to run statement in parallel with';
}
const allStages = [];
extractParallel(allStages, [existing]);
extractParallel(allStages, [latest]);
return (
checkAgentConflicts(allStages) ||
checkReferenceConflicts(allStages) ||
checkDelayedConflicts(allStages)
);
}
function swapBegin(stage, mode) { function swapBegin(stage, mode) {
if(stage.type === 'agent begin') { if(stage.type === 'agent begin') {
stage.mode = mode; stage.mode = mode;
@ -308,37 +389,75 @@ export default class Generator {
}); });
} }
addStage(stage, isVisible = true) { addStage(stage, {isVisible = true, parallel = false} = {}) {
if(!stage) {
return;
}
if(isVisible) {
this.currentNest.hasContent = true;
}
if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine;
}
const {stages} = this.currentSection;
if(parallel) {
const target = last(stages);
const err = errorForParallel(target, stage);
if(err) {
throw new Error(err);
}
const pstage = this.makeParallel([target, stage]);
pstage.ln = stage.ln;
-- stages.length;
stages.push(pstage);
} else {
stages.push(stage);
}
}
addImpStage(stage, {parallel = false} = {}) {
if(!stage) { if(!stage) {
return; return;
} }
if(typeof stage.ln === 'undefined') { if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine; stage.ln = this.latestLine;
} }
this.currentSection.stages.push(stage); const {stages} = this.currentSection;
if(isVisible) { if(parallel) {
this.currentNest.hasContent = true; const target = stages[stages.length - 2];
if(!target) {
throw new Error('Nothing to run statement in parallel with');
}
if(errorForParallel(target, stage)) {
stages.splice(stages.length - 1, 0, stage);
} else {
const pstage = this.makeParallel([target, stage]);
pstage.ln = stage.ln;
stages.splice(stages.length - 2, 1, pstage);
}
} else {
stages.push(stage);
} }
} }
addParallelStages(stages) { makeParallel(stages) {
const viableStages = stages.filter((stage) => Boolean(stage)); const viableStages = [];
extractParallel(viableStages, stages);
if(viableStages.length === 0) { if(viableStages.length === 0) {
return; return null;
} }
if(viableStages.length === 1) { if(viableStages.length === 1) {
this.addStage(viableStages[0]); return viableStages[0];
return;
} }
viableStages.forEach((stage) => { viableStages.forEach((stage) => {
if(typeof stage.ln === 'undefined') { if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine; stage.ln = this.latestLine;
} }
}); });
this.addStage({ return {
stages: viableStages, stages: viableStages,
type: 'parallel', type: 'parallel',
}); };
} }
defineGAgents(gAgents) { defineGAgents(gAgents) {
@ -554,7 +673,10 @@ export default class Generator {
this.currentNest = last(this.nesting); this.currentNest = last(this.nesting);
this.currentSection = last(this.currentNest.sections); this.currentSection = last(this.currentNest.sections);
if(nested.hasContent) { if(!nested.hasContent) {
throw new Error('Empty block');
}
this.defineGAgents(nested.gAgents); this.defineGAgents(nested.gAgents);
addBounds( addBounds(
this.gAgents, this.gAgents,
@ -571,9 +693,6 @@ export default class Generator {
right: nested.rightGAgent.id, right: nested.rightGAgent.id,
type: 'block end', type: 'block end',
}); });
} else {
throw new Error('Empty block');
}
} }
makeGroupDetails(pAgents, alias) { makeGroupDetails(pAgents, alias) {
@ -615,7 +734,7 @@ export default class Generator {
}; };
} }
handleGroupBegin({agents, blockType, tag, label, alias}) { handleGroupBegin({agents, blockType, tag, label, alias, parallel}) {
const details = this.makeGroupDetails(agents, alias); const details = this.makeGroupDetails(agents, alias);
details.gAgentsContained.forEach((gAgent) => { details.gAgentsContained.forEach((gAgent) => {
@ -625,7 +744,10 @@ export default class Generator {
this.updateGAgentState(gAgent, {covered: true}); this.updateGAgentState(gAgent, {covered: true});
}); });
this.activeGroups.set(alias, details); this.activeGroups.set(alias, details);
this.addStage(this.setGAgentVis(details.gAgents, true, 'box')); this.addImpStage(
this.setGAgentVis(details.gAgents, true, 'box'),
{parallel}
);
this.addStage({ this.addStage({
blockType, blockType,
canHide: false, canHide: false,
@ -634,7 +756,7 @@ export default class Generator {
right: details.rightGAgent.id, right: details.rightGAgent.id,
tag: this.textFormatter(tag), tag: this.textFormatter(tag),
type: 'block begin', type: 'block begin',
}); }, {parallel});
} }
endGroup({name}) { endGroup({name}) {
@ -661,7 +783,7 @@ export default class Generator {
handleMark({name}) { handleMark({name}) {
this.markers.add(name); this.markers.add(name);
this.addStage({name, type: 'mark'}, false); this.addStage({name, type: 'mark'}, {isVisible: false});
} }
handleDivider({mode, height, label}) { handleDivider({mode, height, label}) {
@ -670,14 +792,14 @@ export default class Generator {
height, height,
mode, mode,
type: 'divider', type: 'divider',
}, false); }, {isVisible: false});
} }
handleAsync({target}) { handleAsync({target}) {
if(target !== '' && !this.markers.has(target)) { if(target !== '' && !this.markers.has(target)) {
throw new Error('Unknown marker: ' + target); throw new Error('Unknown marker: ' + target);
} }
this.addStage({target, type: 'async'}, false); this.addStage({target, type: 'async'}, {isVisible: false});
} }
handleLabelPattern({pattern}) { handleLabelPattern({pattern}) {
@ -831,7 +953,7 @@ export default class Generator {
return gAgents; return gAgents;
} }
_handlePartialConnect(agents) { _handlePartialConnect(agents, parallel) {
const flags = this.filterConnectFlags(agents); const flags = this.filterConnectFlags(agents);
const gAgents = agents.map(this.toGAgent); const gAgents = agents.map(this.toGAgent);
@ -848,19 +970,22 @@ export default class Generator {
.map(this.toGAgent) .map(this.toGAgent)
.filter((gAgent) => !gAgent.isVirtualSource) .filter((gAgent) => !gAgent.isVirtualSource)
); );
this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box')); this.addImpStage(
this.setGAgentVis(implicitBeginGAgents, true, 'box'),
{parallel}
);
return {flags, gAgents}; return {flags, gAgents};
} }
_makeConnectParallelStages(flags, connectStage) { _makeConnectParallelStages(flags, connectStage) {
return [ return this.makeParallel([
this.setGAgentVis(flags.beginGAgents, true, 'box', true), this.setGAgentVis(flags.beginGAgents, true, 'box', true),
this.setGAgentHighlight(flags.startGAgents, true, true), this.setGAgentHighlight(flags.startGAgents, true, true),
connectStage, connectStage,
this.setGAgentHighlight(flags.stopGAgents, false, true), this.setGAgentHighlight(flags.stopGAgents, false, true),
this.setGAgentVis(flags.endGAgents, false, 'cross', true), this.setGAgentVis(flags.endGAgents, false, 'cross', true),
]; ]);
} }
_isSelfConnect(agents) { _isSelfConnect(agents) {
@ -875,7 +1000,7 @@ export default class Generator {
return true; return true;
} }
handleConnect({agents, label, options}) { handleConnect({agents, label, options, parallel}) {
if(this._isSelfConnect(agents)) { if(this._isSelfConnect(agents)) {
const tag = {}; const tag = {};
this.handleConnectDelayBegin({ this.handleConnectDelayBegin({
@ -883,6 +1008,7 @@ export default class Generator {
ln: 0, ln: 0,
options, options,
tag, tag,
parallel,
}); });
this.handleConnectDelayEnd({ this.handleConnectDelayEnd({
agent: agents[1], agent: agents[1],
@ -893,7 +1019,7 @@ export default class Generator {
return; return;
} }
let {flags, gAgents} = this._handlePartialConnect(agents); let {flags, gAgents} = this._handlePartialConnect(agents, parallel);
gAgents = this.expandGroupedGAgentConnection(gAgents); gAgents = this.expandGroupedGAgentConnection(gAgents);
gAgents = this.expandVirtualSourceAgents(gAgents); gAgents = this.expandVirtualSourceAgents(gAgents);
@ -905,19 +1031,19 @@ export default class Generator {
type: 'connect', type: 'connect',
}; };
this.addParallelStages(this._makeConnectParallelStages( this.addStage(
flags, this._makeConnectParallelStages(flags, connectStage),
connectStage {parallel}
)); );
} }
handleConnectDelayBegin({agent, tag, options, ln}) { handleConnectDelayBegin({agent, tag, options, ln, parallel}) {
const dcs = this.currentSection.delayedConnections; const dcs = this.currentSection.delayedConnections;
if(dcs.has(tag)) { if(dcs.has(tag)) {
throw new Error('Duplicate delayed connection "' + tag + '"'); throw new Error('Duplicate delayed connection "' + tag + '"');
} }
const {flags, gAgents} = this._handlePartialConnect([agent]); const {flags, gAgents} = this._handlePartialConnect([agent], parallel);
const uniqueTag = this.nextVirtualAgentName(); const uniqueTag = this.nextVirtualAgentName();
const connectStage = { const connectStage = {
@ -930,20 +1056,20 @@ export default class Generator {
dcs.set(tag, {connectStage, gAgents, ln, tag, uniqueTag}); dcs.set(tag, {connectStage, gAgents, ln, tag, uniqueTag});
this.addParallelStages(this._makeConnectParallelStages( this.addStage(
flags, this._makeConnectParallelStages(flags, connectStage),
connectStage {parallel}
)); );
} }
handleConnectDelayEnd({agent, tag, label, options}) { handleConnectDelayEnd({agent, tag, label, options, parallel}) {
const dcs = this.currentSection.delayedConnections; const dcs = this.currentSection.delayedConnections;
const dcInfo = dcs.get(tag); const dcInfo = dcs.get(tag);
if(!dcInfo) { if(!dcInfo) {
throw new Error('Unknown delayed connection "' + tag + '"'); throw new Error('Unknown delayed connection "' + tag + '"');
} }
let {flags, gAgents} = this._handlePartialConnect([agent]); let {flags, gAgents} = this._handlePartialConnect([agent], parallel);
gAgents = this.expandGroupedGAgentConnection([ gAgents = this.expandGroupedGAgentConnection([
...dcInfo.gAgents, ...dcInfo.gAgents,
@ -972,15 +1098,15 @@ export default class Generator {
type: 'connect-delay-end', type: 'connect-delay-end',
}; };
this.addParallelStages(this._makeConnectParallelStages( this.addStage(
flags, this._makeConnectParallelStages(flags, connectEndStage),
connectEndStage {parallel}
)); );
dcs.delete(tag); dcs.delete(tag);
} }
handleNote({type, agents, mode, label}) { handleNote({type, agents, mode, label, parallel}) {
let gAgents = null; let gAgents = null;
if(agents.length === 0) { if(agents.length === 0) {
gAgents = NOTE_DEFAULT_G_AGENTS[type] || []; gAgents = NOTE_DEFAULT_G_AGENTS[type] || [];
@ -996,15 +1122,14 @@ export default class Generator {
throw new Error('note between requires at least 2 agents'); throw new Error('note between requires at least 2 agents');
} }
this.addStage(this.setGAgentVis(gAgents, true, 'box'));
this.defineGAgents(gAgents); this.defineGAgents(gAgents);
this.addImpStage(this.setGAgentVis(gAgents, true, 'box'), {parallel});
this.addStage({ this.addStage({
agentIDs, agentIDs,
label: this.textFormatter(label), label: this.textFormatter(label),
mode, mode,
type, type,
}); }, {parallel});
} }
handleAgentDefine({agents}) { handleAgentDefine({agents}) {
@ -1032,13 +1157,13 @@ export default class Generator {
}); });
} }
handleAgentBegin({agents, mode}) { handleAgentBegin({agents, mode, parallel}) {
const gAgents = agents.map(this.toGAgent); const gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents); this.validateGAgents(gAgents);
this.addStage(this.setGAgentVis(gAgents, true, mode, true)); this.addStage(this.setGAgentVis(gAgents, true, mode, true), {parallel});
} }
handleAgentEnd({agents, mode}) { handleAgentEnd({agents, mode, parallel}) {
const groupPAgents = (agents const groupPAgents = (agents
.filter((pAgent) => this.activeGroups.has(pAgent.name)) .filter((pAgent) => this.activeGroups.has(pAgent.name))
); );
@ -1047,11 +1172,11 @@ export default class Generator {
.map(this.toGAgent) .map(this.toGAgent)
); );
this.validateGAgents(gAgents); this.validateGAgents(gAgents);
this.addParallelStages([ this.addStage(this.makeParallel([
this.setGAgentHighlight(gAgents, false), this.setGAgentHighlight(gAgents, false),
this.setGAgentVis(gAgents, false, mode, true), this.setGAgentVis(gAgents, false, mode, true),
...groupPAgents.map(this.endGroup), ...groupPAgents.map(this.endGroup),
]); ]), {parallel});
} }
handleStage(stage) { handleStage(stage) {
@ -1120,10 +1245,10 @@ export default class Generator {
this._checkSectionEnd(); this._checkSectionEnd();
const terminators = meta.terminators || 'none'; const terminators = meta.terminators || 'none';
this.addParallelStages([ this.addStage(this.makeParallel([
this.setGAgentHighlight(this.gAgents, false), this.setGAgentHighlight(this.gAgents, false),
this.setGAgentVis(this.gAgents, false, terminators), this.setGAgentVis(this.gAgents, false, terminators),
]); ]));
this._finalise(globals); this._finalise(globals);

View File

@ -9,10 +9,11 @@ describe('Sequence Generator', () => {
function makeParsedAgents(source) { function makeParsedAgents(source) {
return source.map((item) => { return source.map((item) => {
const base = {alias: '', flags: [], name: ''};
if(typeof item === 'object') { if(typeof item === 'object') {
return item; return Object.assign(base, item);
} else { } else {
return {name: item, alias: '', flags: []}; return Object.assign(base, {name: item});
} }
}); });
} }
@ -26,31 +27,42 @@ describe('Sequence Generator', () => {
const PARSED = { const PARSED = {
sourceAgent: {name: '', alias: '', flags: ['source']}, sourceAgent: {name: '', alias: '', flags: ['source']},
blockBegin: (tag, label, {ln = 0} = {}) => ({ blockBegin: (tag, label, {ln = 0, parallel = false} = {}) => ({
type: 'block begin', type: 'block begin',
blockType: tag, blockType: tag,
tag, tag,
label, label,
ln, ln,
parallel,
}), }),
blockSplit: (tag, label, {ln = 0} = {}) => ({ blockSplit: (tag, label, {ln = 0, parallel = false} = {}) => ({
type: 'block split', type: 'block split',
blockType: tag, blockType: tag,
tag, tag,
label, label,
ln, ln,
parallel,
}), }),
blockEnd: ({ln = 0} = {}) => ({type: 'block end', ln}), blockEnd: ({ln = 0, parallel = false} = {}) => ({
type: 'block end',
ln,
parallel,
}),
labelPattern: (pattern, {ln = 0} = {}) => ({ labelPattern: (pattern, {ln = 0, parallel = false} = {}) => ({
type: 'label pattern', type: 'label pattern',
pattern, pattern,
ln, ln,
parallel,
}), }),
groupBegin: (alias, agentIDs, {label = '', ln = 0} = {}) => ({ groupBegin: (alias, agentIDs, {
label = '',
ln = 0,
parallel = false,
} = {}) => ({
type: 'group begin', type: 'group begin',
agents: makeParsedAgents(agentIDs), agents: makeParsedAgents(agentIDs),
blockType: 'ref', blockType: 'ref',
@ -58,33 +70,46 @@ describe('Sequence Generator', () => {
label, label,
alias, alias,
ln, ln,
parallel,
}), }),
defineAgents: (agentIDs, {ln = 0} = {}) => ({ defineAgents: (agentIDs, {ln = 0, parallel = false} = {}) => ({
type: 'agent define', type: 'agent define',
agents: makeParsedAgents(agentIDs), agents: makeParsedAgents(agentIDs),
ln, ln,
parallel,
}), }),
agentOptions: (agentID, options, {ln = 0} = {}) => ({ agentOptions: (agentID, options, {ln = 0, parallel = false} = {}) => ({
type: 'agent options', type: 'agent options',
agent: makeParsedAgents([agentID])[0], agent: makeParsedAgents([agentID])[0],
options, options,
ln, ln,
parallel,
}), }),
beginAgents: (agentIDs, {mode = 'box', ln = 0} = {}) => ({ beginAgents: (agentIDs, {
mode = 'box',
ln = 0,
parallel = false,
} = {}) => ({
type: 'agent begin', type: 'agent begin',
agents: makeParsedAgents(agentIDs), agents: makeParsedAgents(agentIDs),
mode, mode,
ln, ln,
parallel,
}), }),
endAgents: (agentIDs, {mode = 'cross', ln = 0} = {}) => ({ endAgents: (agentIDs, {
mode = 'cross',
ln = 0,
parallel = false,
} = {}) => ({
type: 'agent end', type: 'agent end',
agents: makeParsedAgents(agentIDs), agents: makeParsedAgents(agentIDs),
mode, mode,
ln, ln,
parallel,
}), }),
connect: (agentIDs, { connect: (agentIDs, {
@ -93,6 +118,7 @@ describe('Sequence Generator', () => {
left = 0, left = 0,
right = 0, right = 0,
ln = 0, ln = 0,
parallel = false,
} = {}) => ({ } = {}) => ({
type: 'connect', type: 'connect',
agents: makeParsedAgents(agentIDs), agents: makeParsedAgents(agentIDs),
@ -103,6 +129,7 @@ describe('Sequence Generator', () => {
right, right,
}, },
ln, ln,
parallel,
}), }),
connectDelayBegin: (agentID, { connectDelayBegin: (agentID, {
@ -111,6 +138,7 @@ describe('Sequence Generator', () => {
left = 0, left = 0,
right = 0, right = 0,
ln = 0, ln = 0,
parallel = false,
} = {}) => ({ } = {}) => ({
type: 'connect-delay-begin', type: 'connect-delay-begin',
ln, ln,
@ -121,6 +149,7 @@ describe('Sequence Generator', () => {
left, left,
right, right,
}, },
parallel,
}), }),
connectDelayEnd: (agentID, { connectDelayEnd: (agentID, {
@ -130,6 +159,7 @@ describe('Sequence Generator', () => {
left = 0, left = 0,
right = 0, right = 0,
ln = 0, ln = 0,
parallel = false,
} = {}) => ({ } = {}) => ({
type: 'connect-delay-end', type: 'connect-delay-end',
ln, ln,
@ -141,18 +171,21 @@ describe('Sequence Generator', () => {
left, left,
right, right,
}, },
parallel,
}), }),
note: (type, agentIDs, { note: (type, agentIDs, {
mode = '', mode = '',
label = '', label = '',
ln = 0, ln = 0,
parallel = false,
} = {}) => ({ } = {}) => ({
type, type,
agents: makeParsedAgents(agentIDs), agents: makeParsedAgents(agentIDs),
mode, mode,
label, label,
ln, ln,
parallel,
}), }),
}; };
@ -2053,6 +2086,151 @@ describe('Sequence Generator', () => {
]); ]);
}); });
it('combines parallel statements', () => {
const sequence = invoke([
PARSED.connect(['A', 'B']),
PARSED.note('note right', ['B'], {parallel: true}),
]);
expect(sequence.stages).toEqual([
any(),
GENERATED.parallel([
GENERATED.connect(['A', 'B']),
GENERATED.note('note right', ['B']),
]),
any(),
]);
});
it('combines parallel creation and destruction', () => {
const sequence = invoke([
PARSED.beginAgents(['A']),
PARSED.beginAgents(['B']),
PARSED.endAgents(['A'], {parallel: true}),
PARSED.endAgents(['B']),
PARSED.beginAgents(['A'], {parallel: true}),
]);
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A']),
GENERATED.parallel([
GENERATED.beginAgents(['B']),
GENERATED.endAgents(['A']),
]),
GENERATED.parallel([
GENERATED.endAgents(['B']),
GENERATED.beginAgents(['A']),
]),
GENERATED.endAgents(['A']),
]);
});
it('combines parallel connects and implicit begins', () => {
const sequence = invoke([
PARSED.connect(['A', 'B']),
PARSED.connect(['B', 'C'], {parallel: true}),
]);
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A', 'B', 'C']),
GENERATED.parallel([
GENERATED.connect(['A', 'B']),
GENERATED.connect(['B', 'C']),
]),
any(),
]);
});
it('combines parallel delayed connections', () => {
const sequence = invoke([
PARSED.beginAgents(['A', 'B', 'C']),
PARSED.connectDelayBegin('B', {tag: 'foo'}),
PARSED.connectDelayBegin('B', {tag: 'bar', parallel: true}),
PARSED.connectDelayEnd('A', {tag: 'foo'}),
PARSED.connectDelayEnd('C', {tag: 'bar', parallel: true}),
]);
expect(sequence.stages).toEqual([
any(),
GENERATED.parallel([
GENERATED.connectDelayBegin(['B', 'A'], {tag: '__0'}),
GENERATED.connectDelayBegin(['B', 'C'], {tag: '__1'}),
]),
GENERATED.parallel([
GENERATED.connectDelayEnd({tag: '__0'}),
GENERATED.connectDelayEnd({tag: '__1'}),
]),
any(),
]);
});
it('combines parallel references', () => {
const sequence = invoke([
PARSED.beginAgents(['A', 'B', 'C', 'D']),
PARSED.groupBegin('AB', ['A', 'B']),
PARSED.groupBegin('CD', ['C', 'D'], {parallel: true}),
PARSED.endAgents(['AB']),
PARSED.endAgents(['CD'], {parallel: true}),
]);
expect(sequence.stages).toEqual([
any(),
GENERATED.parallel([
GENERATED.blockBegin('ref'),
GENERATED.blockBegin('ref'),
]),
GENERATED.parallel([
GENERATED.blockEnd(),
GENERATED.blockEnd(),
]),
any(),
]);
});
it('rejects parallel marks on initial statements', () => {
expect(() => invoke([
PARSED.connect(['A', 'B'], {parallel: true}),
])).toThrow(new Error(
'Nothing to run statement in parallel with at line 1'
));
expect(() => invoke([
PARSED.note('note over', ['A'], {parallel: true}),
])).toThrow(new Error(
'Nothing to run statement in parallel with at line 1'
));
});
it('rejects parallel creation and destruction of an agent', () => {
expect(() => invoke([
PARSED.beginAgents(['A']),
PARSED.endAgents(['A'], {parallel: true}),
])).toThrow(new Error(
'Cannot create and destroy A simultaneously at line 1'
));
});
it('rejects parallel begin and end of a delayed communication', () => {
expect(() => invoke([
PARSED.beginAgents(['A', 'B']),
PARSED.connectDelayBegin('A', {tag: 'foo'}),
PARSED.connectDelayEnd('B', {tag: 'foo', parallel: true}),
])).toThrow(new Error(
'Cannot start and finish delayed connection simultaneously' +
' at line 1'
));
});
it('rejects parallel creation and destruction of a reference', () => {
expect(() => invoke([
PARSED.beginAgents(['A', 'B', 'C', 'D']),
PARSED.groupBegin('AB', ['A', 'B'], {label: 'Foo'}),
PARSED.endAgents(['AB'], {parallel: true}),
])).toThrow(new Error(
'Cannot create and destroy reference simultaneously at line 1'
));
});
it('rejects note between with a repeated agent', () => { it('rejects note between with a repeated agent', () => {
expect(() => invoke([ expect(() => invoke([
PARSED.note('note between', ['A', 'A'], { PARSED.note('note between', ['A', 'A'], {

View File

@ -1,38 +1,42 @@
/* eslint-disable sort-keys */ // Maybe later
import {combine, last} from '../../core/ArrayUtilities.mjs'; import {combine, last} from '../../core/ArrayUtilities.mjs';
import Tokeniser from './Tokeniser.mjs'; import Tokeniser from './Tokeniser.mjs';
import labelPatternParser from './LabelPatternParser.mjs'; import labelPatternParser from './LabelPatternParser.mjs';
import markdownParser from './MarkdownParser.mjs'; import markdownParser from './MarkdownParser.mjs';
const BLOCK_TYPES = { const BLOCK_TYPES = new Map();
'if': { BLOCK_TYPES.set('if', {
type: 'block begin',
blockType: 'if', blockType: 'if',
skip: [],
tag: 'if', tag: 'if',
skip: [], type: 'block begin',
}, });
'else': { BLOCK_TYPES.set('else', {
type: 'block split',
blockType: 'else', blockType: 'else',
tag: 'else',
skip: ['if'], skip: ['if'],
}, tag: 'else',
'repeat': { type: 'block split',
type: 'block begin', });
BLOCK_TYPES.set('repeat', {
blockType: 'repeat', blockType: 'repeat',
skip: [],
tag: 'repeat', tag: 'repeat',
skip: [],
},
'group': {
type: 'block begin', type: 'block begin',
});
BLOCK_TYPES.set('group', {
blockType: 'group', blockType: 'group',
tag: '',
skip: [], skip: [],
}, tag: '',
}; type: 'block begin',
});
const CONNECT = { const CONNECT = {
agentFlags: {
'!': {flag: 'end'},
'*': {allowBlankName: true, blankNameFlag: 'source', flag: 'begin'},
'+': {flag: 'start'},
'-': {flag: 'stop'},
},
types: ((() => { types: ((() => {
const lTypes = [ const lTypes = [
{tok: '', type: 0}, {tok: '', type: 0},
@ -58,21 +62,14 @@ const CONNECT = {
arrows.forEach((arrow) => { arrows.forEach((arrow) => {
types.set(arrow.map((part) => part.tok).join(''), { types.set(arrow.map((part) => part.tok).join(''), {
line: arrow[1].type,
left: arrow[0].type, left: arrow[0].type,
line: arrow[1].type,
right: arrow[2].type, right: arrow[2].type,
}); });
}); });
return types; return types;
})()), })()),
agentFlags: {
'*': {flag: 'begin', allowBlankName: true, blankNameFlag: 'source'},
'+': {flag: 'start'},
'-': {flag: 'stop'},
'!': {flag: 'end'},
},
}; };
const TERMINATOR_TYPES = [ const TERMINATOR_TYPES = [
@ -83,73 +80,70 @@ const TERMINATOR_TYPES = [
'bar', 'bar',
]; ];
const NOTE_TYPES = { const NOTE_TYPES = new Map();
'text': { NOTE_TYPES.set('text', {
mode: 'text', mode: 'text',
types: { types: {
'left': { 'left': {
type: 'note left',
skip: ['of'],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note left',
}, },
'right': { 'right': {
type: 'note right',
skip: ['of'],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note right',
}, },
}, },
}, });
'note': { NOTE_TYPES.set('note', {
mode: 'note', mode: 'note',
types: { types: {
'over': { 'between': {
type: 'note over',
skip: [],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 2,
skip: [],
type: 'note between',
}, },
'left': { 'left': {
type: 'note left',
skip: ['of'],
min: 0,
max: Number.POSITIVE_INFINITY, max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note left',
},
'over': {
max: Number.POSITIVE_INFINITY,
min: 0,
skip: [],
type: 'note over',
}, },
'right': { 'right': {
type: 'note right', max: Number.POSITIVE_INFINITY,
skip: ['of'],
min: 0, min: 0,
max: Number.POSITIVE_INFINITY, skip: ['of'],
}, type: 'note right',
'between': {
type: 'note between',
skip: [],
min: 2,
max: Number.POSITIVE_INFINITY,
}, },
}, },
}, });
'state': { NOTE_TYPES.set('state', {
mode: 'state', mode: 'state',
types: { types: {
'over': {type: 'note over', skip: [], min: 1, max: 1}, 'over': {max: 1, min: 1, skip: [], type: 'note over'},
}, },
}, });
};
const DIVIDER_TYPES = { const DIVIDER_TYPES = new Map();
'line': {defaultHeight: 6}, DIVIDER_TYPES.set('line', {defaultHeight: 6});
'space': {defaultHeight: 6}, DIVIDER_TYPES.set('space', {defaultHeight: 6});
'delay': {defaultHeight: 30}, DIVIDER_TYPES.set('delay', {defaultHeight: 30});
'tear': {defaultHeight: 6}, DIVIDER_TYPES.set('tear', {defaultHeight: 6});
};
const AGENT_MANIPULATION_TYPES = { const AGENT_MANIPULATION_TYPES = new Map();
'define': {type: 'agent define'}, AGENT_MANIPULATION_TYPES.set('define', {type: 'agent define'});
'begin': {type: 'agent begin', mode: 'box'}, AGENT_MANIPULATION_TYPES.set('begin', {mode: 'box', type: 'agent begin'});
'end': {type: 'agent end', mode: 'cross'}, AGENT_MANIPULATION_TYPES.set('end', {mode: 'cross', type: 'agent end'});
};
function makeError(message, token = null) { function makeError(message, token = null) {
let suffix = ''; let suffix = '';
@ -249,8 +243,8 @@ function readAgentAlias(line, start, end, {enableAlias, allowBlankName}) {
throw makeError('Missing agent name', errPosToken); throw makeError('Missing agent name', errPosToken);
} }
return { return {
name: joinLabel(line, start, aliasSep),
alias: joinLabel(line, aliasSep + 1, end), alias: joinLabel(line, aliasSep + 1, end),
name: joinLabel(line, start, aliasSep),
}; };
} }
@ -277,13 +271,13 @@ function readAgent(line, start, end, {
blankNameFlags.push(flag.blankNameFlag); blankNameFlags.push(flag.blankNameFlag);
} }
const {name, alias} = readAgentAlias(line, p, end, { const {name, alias} = readAgentAlias(line, p, end, {
enableAlias: aliases,
allowBlankName, allowBlankName,
enableAlias: aliases,
}); });
return { return {
name,
alias, alias,
flags: name ? flags : blankNameFlags, flags: name ? flags : blankNameFlags,
name,
}; };
} }
@ -350,7 +344,7 @@ const PARSERS = [
}); });
const mode = joinLabel(line, 1, heightSep) || 'line'; const mode = joinLabel(line, 1, heightSep) || 'line';
if(!DIVIDER_TYPES[mode]) { if(!DIVIDER_TYPES.has(mode)) {
throw makeError('Unknown divider type', line[1]); throw makeError('Unknown divider type', line[1]);
} }
@ -358,17 +352,17 @@ const PARSERS = [
line, line,
heightSep + 2, heightSep + 2,
labelSep, labelSep,
DIVIDER_TYPES[mode].defaultHeight DIVIDER_TYPES.get(mode).defaultHeight
); );
if(Number.isNaN(height) || height < 0) { if(Number.isNaN(height) || height < 0) {
throw makeError('Invalid divider height', line[heightSep + 2]); throw makeError('Invalid divider height', line[heightSep + 2]);
} }
return { return {
type: 'divider',
mode,
height, height,
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
mode,
type: 'divider',
}; };
}}, }},
@ -380,8 +374,8 @@ const PARSERS = [
raw = joinLabel(line, 1); raw = joinLabel(line, 1);
} }
return { return {
type: 'label pattern',
pattern: labelPatternParser(raw), pattern: labelPatternParser(raw),
type: 'label pattern',
}; };
}}, }},
@ -393,7 +387,7 @@ const PARSERS = [
}}, }},
{begin: [], fn: (line) => { // Block {begin: [], fn: (line) => { // Block
const type = BLOCK_TYPES[tokenKeyword(line[0])]; const type = BLOCK_TYPES.get(tokenKeyword(line[0]));
if(!type) { if(!type) {
return null; return null;
} }
@ -403,10 +397,10 @@ const PARSERS = [
} }
skip = skipOver(line, skip, [':']); skip = skipOver(line, skip, [':']);
return { return {
type: type.type,
blockType: type.blockType, blockType: type.blockType,
tag: type.tag,
label: joinLabel(line, skip), label: joinLabel(line, skip),
tag: type.tag,
type: type.type,
}; };
}}, }},
@ -428,17 +422,17 @@ const PARSERS = [
throw makeError('Reference must have an alias', line[labelSep]); throw makeError('Reference must have an alias', line[labelSep]);
} }
return { return {
type: 'group begin',
agents, agents,
blockType: 'ref',
tag: 'ref',
label: def.name,
alias: def.alias, alias: def.alias,
blockType: 'ref',
label: def.name,
tag: 'ref',
type: 'group begin',
}; };
}}, }},
{begin: [], fn: (line) => { // Agent {begin: [], fn: (line) => { // Agent
const type = AGENT_MANIPULATION_TYPES[tokenKeyword(line[0])]; const type = AGENT_MANIPULATION_TYPES.get(tokenKeyword(line[0]));
if(!type || line.length <= 1) { if(!type || line.length <= 1) {
return null; return null;
} }
@ -459,13 +453,13 @@ const PARSERS = [
target = joinLabel(line, 2, line.length - 1); target = joinLabel(line, 2, line.length - 1);
} }
return { return {
type: 'async',
target, target,
type: 'async',
}; };
}}, }},
{begin: [], fn: (line) => { // Note {begin: [], fn: (line) => { // Note
const mode = NOTE_TYPES[tokenKeyword(line[0])]; const mode = NOTE_TYPES.get(tokenKeyword(line[0]));
const labelSep = findTokens(line, [':']); const labelSep = findTokens(line, [':']);
if(!mode || labelSep === -1) { if(!mode || labelSep === -1) {
return null; return null;
@ -484,10 +478,10 @@ const PARSERS = [
throw makeError('Too many agents for ' + mode.mode, line[0]); throw makeError('Too many agents for ' + mode.mode, line[0]);
} }
return { return {
type: type.type,
agents, agents,
mode: mode.mode,
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
mode: mode.mode,
type: type.type,
}; };
}}, }},
@ -496,7 +490,7 @@ const PARSERS = [
const connectionToken = findFirstToken( const connectionToken = findFirstToken(
line, line,
CONNECT.types, CONNECT.types,
{start: 0, limit: labelSep - 1} {limit: labelSep - 1, start: 0}
); );
if(!connectionToken) { if(!connectionToken) {
return null; return null;
@ -510,11 +504,11 @@ const PARSERS = [
if(tokenKeyword(line[0]) === '...') { if(tokenKeyword(line[0]) === '...') {
return { return {
type: 'connect-delay-end',
tag: joinLabel(line, 1, connectPos),
agent: readAgent(line, connectPos + 1, labelSep, readOpts), agent: readAgent(line, connectPos + 1, labelSep, readOpts),
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
options: connectionToken.value, options: connectionToken.value,
tag: joinLabel(line, 1, connectPos),
type: 'connect-delay-end',
}; };
} else if(tokenKeyword(line[connectPos + 1]) === '...') { } else if(tokenKeyword(line[connectPos + 1]) === '...') {
if(labelSep !== line.length) { if(labelSep !== line.length) {
@ -524,20 +518,20 @@ const PARSERS = [
); );
} }
return { return {
type: 'connect-delay-begin',
tag: joinLabel(line, connectPos + 2, labelSep),
agent: readAgent(line, 0, connectPos, readOpts), agent: readAgent(line, 0, connectPos, readOpts),
options: connectionToken.value, options: connectionToken.value,
tag: joinLabel(line, connectPos + 2, labelSep),
type: 'connect-delay-begin',
}; };
} else { } else {
return { return {
type: 'connect',
agents: [ agents: [
readAgent(line, 0, connectPos, readOpts), readAgent(line, 0, connectPos, readOpts),
readAgent(line, connectPos + 1, labelSep, readOpts), readAgent(line, connectPos + 1, labelSep, readOpts),
], ],
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
options: connectionToken.value, options: connectionToken.value,
type: 'connect',
}; };
} }
}}, }},
@ -547,8 +541,8 @@ const PARSERS = [
return null; return null;
} }
return { return {
type: 'mark',
name: joinLabel(line, 0, line.length - 1), name: joinLabel(line, 0, line.length - 1),
type: 'mark',
}; };
}}, }},
@ -571,33 +565,44 @@ const PARSERS = [
options.push(line[i].v); options.push(line[i].v);
} }
return { return {
type: 'agent options',
agent, agent,
options, options,
type: 'agent options',
}; };
}}, }},
]; ];
function parseLine(line, {meta, stages}) { function stageFromLine(line, meta) {
let stage = null;
for(const {begin, fn} of PARSERS) { for(const {begin, fn} of PARSERS) {
if(skipOver(line, 0, begin) !== begin.length) { if(skipOver(line, 0, begin) !== begin.length) {
continue; continue;
} }
stage = fn(line, meta); const stage = fn(line, meta);
if(stage) { if(stage) {
break; return stage;
} }
} }
return null;
}
function parseLine(line, {meta, stages}) {
let parallel = false;
const [start] = line;
if(tokenKeyword(start) === '&') {
parallel = true;
line.splice(0, 1);
}
const stage = stageFromLine(line, meta);
if(!stage) { if(!stage) {
throw makeError( throw makeError('Unrecognised command: ' + joinLabel(line), line[0]);
'Unrecognised command: ' + joinLabel(line), } else if(typeof stage === 'object') {
line[0] stage.ln = start.b.ln;
); stage.parallel = parallel;
}
if(typeof stage === 'object') {
stage.ln = line[0].b.ln;
stages.push(stage); stages.push(stage);
} else if(parallel) {
throw makeError('Metadata cannot be parallel', start);
} }
} }
@ -613,12 +618,12 @@ export default class Parser {
parseLines(lines, src) { parseLines(lines, src) {
const result = { const result = {
meta: { meta: {
title: '',
theme: '',
code: src, code: src,
terminators: 'none',
headers: 'box', headers: 'box',
terminators: 'none',
textFormatter: markdownParser, textFormatter: markdownParser,
theme: '',
title: '',
}, },
stages: [], stages: [],
}; };

View File

@ -1,66 +1,230 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
/* eslint-disable max-statements */ /* eslint-disable max-statements */
/* eslint-disable sort-keys */ // Maybe later
import Parser from './Parser.mjs'; import Parser from './Parser.mjs';
describe('Sequence Parser', () => { describe('Sequence Parser', () => {
const parser = new Parser(); const parser = new Parser();
function makeParsedAgents(source) {
return source.map((item) => {
const base = {alias: '', flags: [], name: ''};
if(typeof item === 'object') {
return Object.assign(base, item);
} else {
return Object.assign(base, {name: item});
}
});
}
const PARSED = { const PARSED = {
blockBegin: ({ agentBegin: (agents, {
ln = jasmine.anything(), ln = jasmine.anything(),
blockType = jasmine.anything(), mode = jasmine.anything(),
tag = jasmine.anything(), parallel = false,
label = jasmine.anything(),
} = {}) => ({ } = {}) => ({
type: 'block begin', agents: makeParsedAgents(agents),
ln, ln,
blockType, mode,
tag, parallel,
label, type: 'agent begin',
}), }),
blockSplit: ({ agentDefine: (agents, {
ln = jasmine.anything(), ln = jasmine.anything(),
blockType = jasmine.anything(), parallel = false,
tag = jasmine.anything(),
label = jasmine.anything(),
} = {}) => ({ } = {}) => ({
type: 'block split', agents: makeParsedAgents(agents),
ln, ln,
parallel,
type: 'agent define',
}),
agentEnd: (agents, {
ln = jasmine.anything(),
mode = jasmine.anything(),
parallel = false,
} = {}) => ({
agents: makeParsedAgents(agents),
ln,
mode,
parallel,
type: 'agent end',
}),
agentOptions: (agent, options, {
ln = jasmine.anything(),
parallel = false,
} = {}) => ({
agent: makeParsedAgents([agent])[0],
ln,
options,
parallel,
type: 'agent options',
}),
async: (target, {
ln = jasmine.anything(),
parallel = false,
} = {}) => ({
ln,
parallel,
target,
type: 'async',
}),
blockBegin: ({
blockType = jasmine.anything(),
label = jasmine.anything(),
ln = jasmine.anything(),
parallel = false,
tag = jasmine.anything(),
} = {}) => ({
blockType, blockType,
tag,
label, label,
ln,
parallel,
tag,
type: 'block begin',
}), }),
blockEnd: ({ blockEnd: ({
ln = jasmine.anything(), ln = jasmine.anything(),
parallel = false,
} = {}) => ({ } = {}) => ({
type: 'block end',
ln, ln,
parallel,
type: 'block end',
}), }),
connect: (agentNames, { blockSplit: ({
ln = jasmine.anything(), blockType = jasmine.anything(),
line = jasmine.anything(),
left = jasmine.anything(),
right = jasmine.anything(),
label = jasmine.anything(), label = jasmine.anything(),
ln = jasmine.anything(),
parallel = false,
tag = jasmine.anything(),
} = {}) => ({ } = {}) => ({
type: 'connect', blockType,
ln,
agents: agentNames.map((name) => ({
name,
alias: '',
flags: [],
})),
label, label,
options: { ln,
line, parallel,
left, tag,
right, type: 'block split',
}, }),
connect: (agents, {
label = jasmine.anything(),
left = jasmine.anything(),
line = jasmine.anything(),
ln = jasmine.anything(),
parallel = false,
right = jasmine.anything(),
} = {}) => ({
agents: makeParsedAgents(agents),
label,
ln,
options: {left, line, right},
parallel,
type: 'connect',
}),
connectBegin: (agent, tag, {
left = jasmine.anything(),
line = jasmine.anything(),
ln = jasmine.anything(),
parallel = false,
right = jasmine.anything(),
} = {}) => ({
agent: makeParsedAgents([agent])[0],
ln,
options: {left, line, right},
parallel,
tag,
type: 'connect-delay-begin',
}),
connectEnd: (agent, tag, {
label = jasmine.anything(),
left = jasmine.anything(),
line = jasmine.anything(),
ln = jasmine.anything(),
parallel = false,
right = jasmine.anything(),
} = {}) => ({
agent: makeParsedAgents([agent])[0],
label,
ln,
options: {left, line, right},
parallel,
tag,
type: 'connect-delay-end',
}),
divider: ({
height = jasmine.anything(),
label = jasmine.anything(),
ln = jasmine.anything(),
mode = jasmine.anything(),
parallel = false,
} = {}) => ({
height,
label,
ln,
mode,
parallel,
type: 'divider',
}),
groupBegin: (agents, {
alias = jasmine.anything(),
blockType = jasmine.anything(),
label = jasmine.anything(),
ln = jasmine.anything(),
parallel = false,
tag = jasmine.anything(),
} = {}) => ({
agents: makeParsedAgents(agents),
alias,
blockType,
label,
ln,
parallel,
tag,
type: 'group begin',
}),
labelPattern: (pattern, {
ln = jasmine.anything(),
parallel = false,
} = {}) => ({
ln,
parallel,
pattern,
type: 'label pattern',
}),
mark: (name, {
ln = jasmine.anything(),
parallel = false,
} = {}) => ({
ln,
name,
parallel,
type: 'mark',
}),
note: (position, agents, {
label = jasmine.anything(),
ln = jasmine.anything(),
mode = 'note',
parallel = false,
} = {}) => ({
agents: makeParsedAgents(agents),
label,
ln,
mode,
parallel,
type: 'note ' + position,
}), }),
}; };
@ -143,9 +307,9 @@ describe('Sequence Parser', () => {
const parsed = parser.parse('define Foo Bar as A'); const parsed = parser.parse('define Foo Bar as A');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: 'agent define', ln: jasmine.anything(), agents: [ PARSED.agentDefine([
{name: 'Foo Bar', alias: 'A', flags: []}, {alias: 'A', name: 'Foo Bar'},
]}, ]),
]); ]);
}); });
@ -153,9 +317,9 @@ describe('Sequence Parser', () => {
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', ln: jasmine.anything(), agents: [ PARSED.agentDefine([
{name: 'Foo Bar', alias: 'A B', flags: []}, {alias: 'A B', name: 'Foo Bar'},
]}, ]),
]); ]);
}); });
@ -163,9 +327,7 @@ describe('Sequence Parser', () => {
const parsed = parser.parse('define Foo Bar as'); const parsed = parser.parse('define Foo Bar as');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{type: 'agent define', ln: jasmine.anything(), agents: [ PARSED.agentDefine(['Foo Bar']),
{name: 'Foo Bar', alias: '', flags: []},
]},
]); ]);
}); });
@ -173,16 +335,7 @@ describe('Sequence Parser', () => {
const parsed = parser.parse('Foo bar is zig zag'); const parsed = parser.parse('Foo bar is zig zag');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.agentOptions('Foo bar', ['zig', 'zag']),
type: 'agent options',
ln: jasmine.anything(),
agent: {
name: 'Foo bar',
alias: '',
flags: [],
},
options: ['zig', 'zag'],
},
]); ]);
}); });
@ -190,26 +343,8 @@ describe('Sequence Parser', () => {
const parsed = parser.parse('Foo is a zig\nBar is an oom'); const parsed = parser.parse('Foo is a zig\nBar is an oom');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.agentOptions('Foo', ['zig']),
type: 'agent options', PARSED.agentOptions('Bar', ['oom']),
ln: jasmine.anything(),
agent: {
name: 'Foo',
alias: '',
flags: [],
},
options: ['zig'],
},
{
type: 'agent options',
ln: jasmine.anything(),
agent: {
name: 'Bar',
alias: '',
flags: [],
},
options: ['oom'],
},
]); ]);
}); });
@ -243,20 +378,10 @@ describe('Sequence Parser', () => {
const parsed = parser.parse('+A -> -*!B'); const parsed = parser.parse('+A -> -*!B');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.connect([
type: 'connect', {flags: ['start'], name: 'A'},
ln: jasmine.anything(), {flags: ['stop', 'begin', 'end'], name: 'B'},
agents: [ ]),
{name: 'A', alias: '', flags: ['start']},
{name: 'B', alias: '', flags: [
'stop',
'begin',
'end',
]},
],
label: jasmine.anything(),
options: jasmine.anything(),
},
]); ]);
}); });
@ -286,16 +411,7 @@ describe('Sequence Parser', () => {
const parsed = parser.parse('A -> *'); const parsed = parser.parse('A -> *');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.connect(['A', {flags: ['source'], name: ''}]),
type: 'connect',
ln: jasmine.anything(),
agents: [
{name: 'A', alias: '', flags: []},
{name: '', alias: '', flags: ['source']},
],
label: jasmine.anything(),
options: jasmine.anything(),
},
]); ]);
}); });
@ -303,16 +419,9 @@ describe('Sequence Parser', () => {
const parsed = parser.parse('A -> *: foo'); const parsed = parser.parse('A -> *: foo');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.connect(['A', {flags: ['source'], name: ''}], {
type: 'connect',
ln: jasmine.anything(),
agents: [
{name: 'A', alias: '', flags: []},
{name: '', alias: '', flags: ['source']},
],
label: 'foo', label: 'foo',
options: jasmine.anything(), }),
},
]); ]);
}); });
@ -376,37 +485,37 @@ describe('Sequence Parser', () => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
PARSED.connect(['A', 'B'], { PARSED.connect(['A', 'B'], {
line: 'solid',
left: 0,
right: 1,
label: '', label: '',
left: 0,
line: 'solid',
right: 1,
}), }),
PARSED.connect(['A', 'B'], {line: 'solid', left: 0, right: 2}), PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 2}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 0}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 0}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 1}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 1}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 1, right: 2}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 2}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 0}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 0}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 1}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 1}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 2, right: 2}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 2}),
PARSED.connect(['A', 'B'], {line: 'solid', left: 0, right: 3}), PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 3}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 1}), PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 1}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 2}), PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 2}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 0}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 0}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 1}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 1}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 1, right: 2}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 2}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 0}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 0}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 1}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 1}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 2, right: 2}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 2}),
PARSED.connect(['A', 'B'], {line: 'dash', left: 0, right: 3}), PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 3}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 0, right: 1}), PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 1}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 0, right: 2}), PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 2}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 1, right: 0}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 0}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 1, right: 1}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 1}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 1, right: 2}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 2}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 2, right: 0}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 0}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 2, right: 1}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 1}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 2, right: 2}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 2}),
PARSED.connect(['A', 'B'], {line: 'wave', left: 0, right: 3}), PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 3}),
]); ]);
}); });
@ -418,16 +527,16 @@ describe('Sequence Parser', () => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
PARSED.connect(['A', 'B'], { PARSED.connect(['A', 'B'], {
line: 'solid',
left: 1,
right: 0,
label: 'B -> A', label: 'B -> A',
left: 1,
line: 'solid',
right: 0,
}), }),
PARSED.connect(['A', 'B'], { PARSED.connect(['A', 'B'], {
line: 'solid',
left: 0,
right: 1,
label: 'B <- A', label: 'B <- A',
left: 0,
line: 'solid',
right: 1,
}), }),
]); ]);
}); });
@ -436,50 +545,25 @@ describe('Sequence Parser', () => {
const parsed = parser.parse('+A <- ...foo\n...foo -> -B: woo'); const parsed = parser.parse('+A <- ...foo\n...foo -> -B: woo');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.connectBegin(
type: 'connect-delay-begin', {flags: ['start'], name: 'A'},
ln: jasmine.anything(), 'foo',
tag: 'foo', {left: 1, line: 'solid', right: 0}
agent: { ),
name: 'A', PARSED.connectEnd(
alias: '', {flags: ['stop'], name: 'B'},
flags: ['start'], 'foo',
}, {label: 'woo', left: 0, line: 'solid', right: 1}
options: { ),
line: 'solid',
left: 1,
right: 0,
},
},
{
type: 'connect-delay-end',
ln: jasmine.anything(),
tag: 'foo',
agent: {
name: 'B',
alias: '',
flags: ['stop'],
},
label: 'woo',
options: {
line: 'solid',
left: 0,
right: 1,
},
},
]); ]);
}); });
it('converts notes', () => { it('converts notes', () => {
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', PARSED.note('over', ['A'], {label: 'hello there'}),
ln: jasmine.anything(), ]);
agents: [{name: 'A', alias: '', flags: []}],
mode: 'note',
label: 'hello there',
}]);
}); });
it('converts different note types', () => { it('converts different note types', () => {
@ -492,60 +576,20 @@ describe('Sequence Parser', () => {
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.note('left', ['A'], {label: 'hello there'}),
type: 'note left', PARSED.note('left', ['A'], {label: 'hello there'}),
ln: jasmine.anything(), PARSED.note('right', ['A'], {label: 'hello there'}),
agents: [{name: 'A', alias: '', flags: []}], PARSED.note('right', ['A'], {label: 'hello there'}),
mode: 'note', PARSED.note('between', ['A', 'B'], {label: 'hi'}),
label: 'hello there',
},
{
type: 'note left',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}],
mode: 'note',
label: 'hello there',
},
{
type: 'note right',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}],
mode: 'note',
label: 'hello there',
},
{
type: 'note right',
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}],
mode: 'note',
label: 'hello there',
},
{
type: 'note between',
ln: jasmine.anything(),
agents: [
{name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []},
],
mode: 'note',
label: 'hi',
},
]); ]);
}); });
it('allows multiple agents for notes', () => { it('allows multiple agents for notes', () => {
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', PARSED.note('over', ['A B', 'C D'], {label: 'hi'}),
ln: jasmine.anything(), ]);
agents: [
{name: 'A B', alias: '', flags: []},
{name: 'C D', alias: '', flags: []},
],
mode: 'note',
label: 'hi',
}]);
}); });
it('rejects note between for a single agent', () => { it('rejects note between for a single agent', () => {
@ -557,13 +601,12 @@ describe('Sequence Parser', () => {
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', PARSED.note('over', ['A'], {
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}],
mode: 'state',
label: 'doing stuff', label: 'doing stuff',
}]); mode: 'state',
}),
]);
}); });
it('rejects multiple agents for state', () => { it('rejects multiple agents for state', () => {
@ -575,13 +618,12 @@ describe('Sequence Parser', () => {
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', PARSED.note('right', ['A'], {
ln: jasmine.anything(),
agents: [{name: 'A', alias: '', flags: []}],
mode: 'text',
label: 'doing stuff', label: 'doing stuff',
}]); mode: 'text',
}),
]);
}); });
it('converts agent commands', () => { it('converts agent commands', () => {
@ -592,45 +634,18 @@ describe('Sequence Parser', () => {
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.agentDefine(['A', 'B']),
type: 'agent define', PARSED.agentBegin(['A', 'B'], {mode: 'box'}),
ln: jasmine.anything(), PARSED.agentEnd(['A', 'B'], {mode: 'cross'}),
agents: [
{name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []},
],
},
{
type: 'agent begin',
ln: jasmine.anything(),
agents: [
{name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []},
],
mode: 'box',
},
{
type: 'agent end',
ln: jasmine.anything(),
agents: [
{name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []},
],
mode: 'cross',
},
]); ]);
}); });
it('converts dividers', () => { it('converts dividers', () => {
const parsed = parser.parse('divider'); const parsed = parser.parse('divider');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([
type: 'divider', PARSED.divider({height: 6, label: '', mode: 'line'}),
ln: jasmine.anything(), ]);
mode: 'line',
height: 6,
label: '',
}]);
}); });
it('converts different divider types', () => { it('converts different divider types', () => {
@ -642,34 +657,10 @@ describe('Sequence Parser', () => {
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.divider({height: 6, mode: 'line'}),
type: 'divider', PARSED.divider({height: 6, mode: 'space'}),
ln: jasmine.anything(), PARSED.divider({height: 30, mode: 'delay'}),
mode: 'line', PARSED.divider({height: 6, mode: 'tear'}),
height: 6,
label: jasmine.anything(),
},
{
type: 'divider',
ln: jasmine.anything(),
mode: 'space',
height: 6,
label: jasmine.anything(),
},
{
type: 'divider',
ln: jasmine.anything(),
mode: 'delay',
height: 30,
label: jasmine.anything(),
},
{
type: 'divider',
ln: jasmine.anything(),
mode: 'tear',
height: 6,
label: jasmine.anything(),
},
]); ]);
}); });
@ -680,20 +671,8 @@ describe('Sequence Parser', () => {
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.divider({height: 40, label: '', mode: 'line'}),
type: 'divider', PARSED.divider({height: 0, label: '', mode: 'delay'}),
ln: jasmine.anything(),
mode: 'line',
height: 40,
label: '',
},
{
type: 'divider',
ln: jasmine.anything(),
mode: 'delay',
height: 0,
label: '',
},
]); ]);
}); });
@ -705,27 +684,9 @@ describe('Sequence Parser', () => {
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.divider({label: 'message 1'}),
type: 'divider', PARSED.divider({label: 'message 2'}),
ln: jasmine.anything(), PARSED.divider({label: 'message 3'}),
mode: jasmine.anything(),
height: jasmine.anything(),
label: 'message 1',
},
{
type: 'divider',
ln: jasmine.anything(),
mode: jasmine.anything(),
height: jasmine.anything(),
label: 'message 2',
},
{
type: 'divider',
ln: jasmine.anything(),
mode: jasmine.anything(),
height: jasmine.anything(),
label: 'message 3',
},
]); ]);
}); });
@ -736,53 +697,34 @@ describe('Sequence Parser', () => {
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.groupBegin([], {
type: 'group begin',
ln: jasmine.anything(),
agents: [],
blockType: 'ref',
tag: 'ref',
label: 'Foo bar',
alias: 'baz', alias: 'baz',
},
{
type: 'group begin',
ln: jasmine.anything(),
agents: [
{name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []},
],
blockType: 'ref', blockType: 'ref',
tag: 'ref',
label: 'Foo bar', label: 'Foo bar',
tag: 'ref',
}),
PARSED.groupBegin(['A', 'B'], {
alias: 'baz', alias: 'baz',
}, blockType: 'ref',
label: 'Foo bar',
tag: 'ref',
}),
]); ]);
}); });
it('converts markers', () => { it('converts markers', () => {
const parsed = parser.parse('abc:'); const parsed = parser.parse('abc:');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([
type: 'mark', PARSED.mark('abc'),
ln: jasmine.anything(), ]);
name: 'abc',
}]);
}); });
it('converts autolabel commands', () => { it('converts autolabel commands', () => {
const parsed = parser.parse('autolabel "foo <label> bar"'); const parsed = parser.parse('autolabel "foo <label> bar"');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.labelPattern(['foo ', {token: 'label'}, ' bar']),
type: 'label pattern',
ln: jasmine.anything(),
pattern: [
'foo ',
{token: 'label'},
' bar',
],
},
]); ]);
}); });
@ -790,32 +732,24 @@ describe('Sequence Parser', () => {
const parsed = parser.parse('autolabel off'); const parsed = parser.parse('autolabel off');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ PARSED.labelPattern([{token: 'label'}]),
type: 'label pattern',
ln: jasmine.anything(),
pattern: [{token: 'label'}],
},
]); ]);
}); });
it('converts "simultaneously" flow commands', () => { it('converts "simultaneously" flow commands', () => {
const parsed = parser.parse('simultaneously:'); const parsed = parser.parse('simultaneously:');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([
type: 'async', PARSED.async(''),
ln: jasmine.anything(), ]);
target: '',
}]);
}); });
it('converts named "simultaneously" flow commands', () => { it('converts named "simultaneously" flow commands', () => {
const parsed = parser.parse('simultaneously with abc:'); const parsed = parser.parse('simultaneously with abc:');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([
type: 'async', PARSED.async('abc'),
ln: jasmine.anything(), ]);
target: 'abc',
}]);
}); });
it('converts conditional blocks', () => { it('converts conditional blocks', () => {
@ -833,21 +767,21 @@ describe('Sequence Parser', () => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
PARSED.blockBegin({ PARSED.blockBegin({
blockType: 'if', blockType: 'if',
tag: 'if',
label: 'something happens', label: 'something happens',
tag: 'if',
}), }),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
PARSED.blockSplit({ PARSED.blockSplit({
blockType: 'else', blockType: 'else',
tag: 'else',
label: 'something else', label: 'something else',
tag: 'else',
}), }),
PARSED.connect(['A', 'C']), PARSED.connect(['A', 'C']),
PARSED.connect(['C', 'B']), PARSED.connect(['C', 'B']),
PARSED.blockSplit({ PARSED.blockSplit({
blockType: 'else', blockType: 'else',
tag: 'else',
label: '', label: '',
tag: 'else',
}), }),
PARSED.connect(['A', 'D']), PARSED.connect(['A', 'D']),
PARSED.blockEnd(), PARSED.blockEnd(),
@ -860,8 +794,8 @@ describe('Sequence Parser', () => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
PARSED.blockBegin({ PARSED.blockBegin({
blockType: 'repeat', blockType: 'repeat',
tag: 'repeat',
label: 'until something', label: 'until something',
tag: 'repeat',
}), }),
]); ]);
}); });
@ -872,12 +806,26 @@ describe('Sequence Parser', () => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
PARSED.blockBegin({ PARSED.blockBegin({
blockType: 'group', blockType: 'group',
tag: '',
label: 'something', label: 'something',
tag: '',
}), }),
]); ]);
}); });
it('propagates parallel markers', () => {
const parsed = parser.parse('& A -> B');
expect(parsed.stages).toEqual([
PARSED.connect(['A', 'B'], {parallel: true}),
]);
});
it('rejects parallel markers on metadata', () => {
expect(() => parser.parse('& title foo')).toThrow(new Error(
'Metadata cannot be parallel at line 1, character 0'
));
});
it('rejects quoted keywords', () => { it('rejects quoted keywords', () => {
expect(() => parser.parse('"repeat" until something')).toThrow(); expect(() => parser.parse('"repeat" until something')).toThrow();
}); });

23
spec/images/Parallel.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -10,4 +10,5 @@ export default [
'ReferenceLayering.svg', 'ReferenceLayering.svg',
'Markdown.svg', 'Markdown.svg',
'AgentOptions.svg', 'AgentOptions.svg',
'Parallel.svg',
]; ];

View File

@ -39,6 +39,13 @@
code: '* -> {Agent1}: {Message}', code: '* -> {Agent1}: {Message}',
title: 'Found message', title: 'Found message',
}, },
{
code: (
'{Agent1} -> {Agent2}\n' +
'& {Agent1} -> {Agent3}: {Broadcast}'
),
title: 'Broadcast message',
},
{ {
code: ( code: (
'{Agent1} -> +{Agent2}: {Request}\n' + '{Agent1} -> +{Agent2}: {Request}\n' +
@ -178,6 +185,13 @@
code: 'note between {Agent1}, {Agent2}: {Message}', code: 'note between {Agent1}, {Agent2}: {Message}',
title: 'Note between agents', title: 'Note between agents',
}, },
{
code: (
'{Agent1} -> {Agent2}\n' +
'& note right of {Agent2}: {Message}'
),
title: 'Inline note',
},
{ {
code: 'state over {Agent1}: {State}', code: 'state over {Agent1}: {State}',
title: 'State over agent', title: 'State over agent',

File diff suppressed because one or more lines are too long

View File

@ -36,6 +36,13 @@ export default [
code: '* -> {Agent1}: {Message}', code: '* -> {Agent1}: {Message}',
title: 'Found message', title: 'Found message',
}, },
{
code: (
'{Agent1} -> {Agent2}\n' +
'& {Agent1} -> {Agent3}: {Broadcast}'
),
title: 'Broadcast message',
},
{ {
code: ( code: (
'{Agent1} -> +{Agent2}: {Request}\n' + '{Agent1} -> +{Agent2}: {Request}\n' +
@ -175,6 +182,13 @@ export default [
code: 'note between {Agent1}, {Agent2}: {Message}', code: 'note between {Agent1}, {Agent2}: {Message}',
title: 'Note between agents', title: 'Note between agents',
}, },
{
code: (
'{Agent1} -> {Agent2}\n' +
'& note right of {Agent2}: {Message}'
),
title: 'Inline note',
},
{ {
code: 'state over {Agent1}: {State}', code: 'state over {Agent1}: {State}',
title: 'State over agent', title: 'State over agent',