Add virtual side agents for blocks, simplify generator logic

This commit is contained in:
David Evans 2017-10-24 21:26:17 +01:00
parent 1449d73194
commit 8cbdddec20
2 changed files with 315 additions and 196 deletions

View File

@ -17,178 +17,218 @@ define(() => {
} }
return class Generator { return class Generator {
findAgents(stages) { constructor() {
const agents = ['[']; this.agentStates = new Map();
stages.forEach((stage) => { this.blockCount = 0;
if(stage.agents) { this.nesting = [];
mergeSets(agents, stage.agents);
}
});
if(agents.indexOf(']') !== -1) { this.stageHandlers = {
agents.splice(agents.indexOf(']'), 1); 'agent define': this.handleAgentDefine.bind(this),
} 'agent begin': this.handleAgentBegin.bind(this),
agents.push(']'); 'agent end': this.handleAgentEnd.bind(this),
'block begin': this.handleBlockBegin.bind(this),
return agents; 'block split': this.handleBlockSplit.bind(this),
'block end': this.handleBlockEnd.bind(this),
};
this.handleStage = this.handleStage.bind(this);
} }
generate({meta = {}, stages}) { addStage(stage) {
const agents = this.findAgents(stages); this.currentSection.stages.push(stage);
mergeSets(this.currentNest.agents, stage.agents);
}
const agentStates = new Map(); addColumnBounds(target, agentL, agentR, involvedAgents = []) {
agents.forEach((agent) => { const oldL = target.indexOf(agentL);
agentStates.set(agent, {visible: false, locked: false}); if(oldL !== -1) {
}); target.splice(oldL, 1);
agentStates.get('[').locked = true; }
agentStates.get(']').locked = true; const oldR = target.indexOf(agentR);
if(oldR !== -1) {
const rootStages = []; target.splice(oldR, 1);
let currentSection = {
mode: 'global',
label: '',
stages: rootStages,
};
let currentNest = {
type: 'block',
agents: [],
root: true,
sections: [currentSection],
};
const nesting = [currentNest];
function beginNested(stage) {
currentSection = {
mode: stage.mode,
label: stage.label,
stages: [],
};
currentNest = {
type: 'block',
agents: [],
sections: [currentSection],
};
nesting.push(currentNest);
} }
function splitNested(stage) { let indexL = 0;
if(currentNest.sections[0].mode !== 'if') { let indexR = target.length;
throw new Error('Invalid block nesting'); if(involvedAgents.length > 0) {
} const found = (involvedAgents
currentSection = { .map((agent) => target.indexOf(agent))
mode: stage.mode, .filter((p) => (p !== -1))
label: stage.label, );
stages: [], indexL = found.reduce((a, b) => Math.min(a, b), target.length);
}; indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1;
currentNest.sections.push(currentSection);
} }
function addStage(stage) { target.splice(indexL, 0, agentL);
currentSection.stages.push(stage); target.splice(indexR + 1, 0, agentR);
mergeSets(currentNest.agents, stage.agents); }
}
function endNested() { addAgentMod(stageAgents, markVisible, mode) {
if(currentNest.root) { if(stageAgents.length === 0) {
throw new Error('Invalid block nesting'); return;
}
const subNest = nesting.pop();
currentNest = lastElement(nesting);
currentSection = lastElement(currentNest.sections);
if(subNest.agents.length > 0) {
addStage(subNest);
}
} }
stageAgents.forEach((agent) => {
function addAgentMod(stageAgents, markVisible, mode) { const state = this.agentStates.get(agent);
if(stageAgents.length === 0) { if(state) {
return; state.visible = markVisible;
}
stageAgents.forEach((agent) => {
agentStates.get(agent).visible = markVisible;
});
const type = (markVisible ? 'agent begin' : 'agent end');
const existing = lastElement(currentSection.stages) || {};
if(existing.type === type && existing.mode === mode) {
mergeSets(existing.agents, stageAgents);
mergeSets(currentNest.agents, stageAgents);
} else { } else {
addStage({ this.agentStates.set(agent, {
type, visible: markVisible,
agents: stageAgents, locked: false,
mode,
}); });
} }
} });
const type = (markVisible ? 'agent begin' : 'agent end');
function filterVis(stageAgents, visible, implicit = false) { const existing = lastElement(this.currentSection.stages) || {};
return stageAgents.filter((agent) => { if(existing.type === type && existing.mode === mode) {
const state = agentStates.get(agent); mergeSets(existing.agents, stageAgents);
if(!state.locked) { mergeSets(this.currentNest.agents, stageAgents);
return state.visible === visible; } else {
} else if(!implicit) { this.addStage({
throw new Error('Cannot modify agent ' + agent); type,
} else { agents: stageAgents,
return false; mode,
}
}); });
} }
}
stages.forEach((stage) => { filterVis(stageAgents, visible, implicit = false) {
/* jshint -W074 */ // It's only a switch statement return stageAgents.filter((agent) => {
switch(stage.type) { const state = this.agentStates.get(agent);
case 'agent define': if(!state) {
break; return !visible;
case 'agent begin': } else if(!state.locked) {
addAgentMod( return state.visible === visible;
filterVis(stage.agents, false), } else if(!implicit) {
true, throw new Error('Cannot modify agent ' + agent);
stage.mode } else {
); return false;
break;
case 'agent end':
addAgentMod(
filterVis(stage.agents, true),
false,
stage.mode
);
break;
case 'block begin':
beginNested(stage);
break;
case 'block split':
splitNested(stage);
break;
case 'block end':
endNested(stage);
break;
default:
addAgentMod(
filterVis(stage.agents, false, true),
true,
'box'
);
addStage(stage);
break;
} }
}); });
}
if(nesting.length !== 1) { beginNested(mode, label, name) {
const agents = [];
const stages = [];
this.currentSection = {
mode,
label,
stages,
};
this.currentNest = {
type: 'block',
agents,
sections: [this.currentSection],
leftColumn: name + '[',
rightColumn: name + ']',
};
this.agentStates.set(name + '[', {visible: false, locked: true});
this.agentStates.set(name + ']', {visible: false, locked: true});
this.nesting.push(this.currentNest);
return {agents, stages};
}
handleAgentDefine() {
}
handleAgentBegin({agents, mode}) {
this.addAgentMod(this.filterVis(agents, false), true, mode);
}
handleAgentEnd({agents, mode}) {
this.addAgentMod(this.filterVis(agents, true), false, mode);
}
handleBlockBegin({mode, label}) {
const name = '__BLOCK' + this.blockCount;
this.beginNested(mode, label, name);
++ this.blockCount;
}
handleBlockSplit({mode, label}) {
if(this.currentNest.sections[0].mode !== 'if') {
throw new Error('Invalid block nesting');
}
this.currentSection = {
mode,
label,
stages: [],
};
this.currentNest.sections.push(this.currentSection);
}
handleBlockEnd() {
if(this.nesting.length <= 1) {
throw new Error('Invalid block nesting');
}
const subNest = this.nesting.pop();
this.currentNest = lastElement(this.nesting);
this.currentSection = lastElement(this.currentNest.sections);
if(subNest.agents.length > 0) {
this.addStage(subNest);
this.addColumnBounds(
this.currentNest.agents,
subNest.leftColumn,
subNest.rightColumn,
subNest.agents
);
this.addColumnBounds(
subNest.agents,
subNest.leftColumn,
subNest.rightColumn
);
}
}
handleUnknownStage(stage) {
this.addAgentMod(
this.filterVis(stage.agents, false, true),
true,
'box'
);
this.addStage(stage);
}
handleStage(stage) {
const handler = this.stageHandlers[stage.type];
if(handler) {
handler(stage);
} else {
this.handleUnknownStage(stage);
}
}
generate({stages, meta = {}}) {
this.agentStates.clear();
this.blockCount = 0;
this.nesting.length = 0;
const globals = this.beginNested('global', '', '');
stages.forEach(this.handleStage);
if(this.nesting.length !== 1) {
throw new Error('Invalid block nesting'); throw new Error('Invalid block nesting');
} }
addAgentMod( this.addAgentMod(
filterVis(agents, true, true), this.filterVis(globals.agents, true, true),
false, false,
meta.terminators || 'none' meta.terminators || 'none'
); );
this.addColumnBounds(
globals.agents,
this.currentNest.leftColumn,
this.currentNest.rightColumn
);
return { return {
meta: { meta: {
title: meta.title, title: meta.title,
}, },
agents, agents: globals.agents,
stages: rootStages, stages: globals.stages,
}; };
} }
}; };

View File

@ -162,7 +162,6 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('does not merge different modes of end', () => { it('does not merge different modes of end', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: AGENT_DEFINE, agents: ['E']},
{type: AGENT_BEGIN, agents: ['C', 'D'], mode: 'box'}, {type: AGENT_BEGIN, agents: ['C', 'D'], mode: 'box'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: ['A', 'B']},
{type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'cross'}, {type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'cross'},
@ -175,7 +174,62 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
}); });
it('records all involved agents in block begin statements', () => { it('creates virtual agents for block statements', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: BLOCK_END},
]});
expect(sequence.agents).toEqual(
['[', '__BLOCK0[', 'A', 'B', '__BLOCK0]', ']']
);
});
it('positions virtual block agents near involved agents', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['C', 'D']},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['E', 'F']},
{type: BLOCK_END},
{type: BLOCK_END},
{type: '->', agents: ['G', 'H']},
]});
expect(sequence.agents).toEqual([
'[',
'A',
'B',
'__BLOCK0[',
'C',
'D',
'__BLOCK1[',
'E',
'F',
'__BLOCK1]',
'__BLOCK0]',
'G',
'H',
']',
]);
});
it('records virtual block column names in blocks', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: BLOCK_END},
]});
const block0 = sequence.stages[0];
expect(block0.type).toEqual('block');
expect(block0.leftColumn).toEqual('__BLOCK0[');
expect(block0.rightColumn).toEqual('__BLOCK0]');
});
it('records all involved agents in blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: ['A', 'B']},
@ -184,18 +238,31 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
expect(sequence.stages).toEqual([ const block0 = sequence.stages[0];
{type: 'block', agents: ['A', 'B', 'C'], sections: [ expect(block0.agents).toEqual(
{mode: 'if', label: 'abc', stages: [ ['__BLOCK0[', 'A', 'B', 'C', '__BLOCK0]']
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, );
{type: '->', agents: ['A', 'B']}, });
]},
{mode: 'else', label: 'xyz', stages: [ it('records all sections within blocks', () => {
{type: AGENT_BEGIN, agents: ['C'], mode: 'box'}, const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'C']}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
]}, {type: '->', agents: ['A', 'B']},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: '->', agents: ['A', 'C']},
{type: BLOCK_END},
]});
const block0 = sequence.stages[0];
expect(block0.sections).toEqual([
{mode: 'if', label: 'abc', stages: [
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
{type: '->', agents: ['A', 'B']},
]},
{mode: 'else', label: 'xyz', stages: [
{type: AGENT_BEGIN, agents: ['C'], mode: 'box'},
{type: '->', agents: ['A', 'C']},
]}, ]},
{type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'none'},
]); ]);
}); });
@ -210,23 +277,30 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
expect(sequence.stages).toEqual([ const block0 = sequence.stages[0];
{type: 'block', agents: ['A', 'B', 'C'], sections: [ expect(block0.type).toEqual('block');
{mode: 'if', label: 'abc', stages: [ expect(block0.agents).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, '__BLOCK0[',
{type: '->', agents: ['A', 'B']}, '__BLOCK1[',
]}, 'A',
{mode: 'else', label: 'xyz', stages: [ 'B',
{type: 'block', agents: ['C', 'A'], sections: [ 'C',
{mode: 'if', label: 'def', stages: [ '__BLOCK1]',
{type: AGENT_BEGIN, agents: ['C'], mode: 'box'}, '__BLOCK0]',
{type: '->', agents: ['A', 'C']},
]},
]},
]},
]},
{type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'none'},
]); ]);
expect(block0.leftColumn).toEqual('__BLOCK0[');
expect(block0.rightColumn).toEqual('__BLOCK0]');
const block1 = block0.sections[1].stages[0];
expect(block1.type).toEqual('block');
expect(block1.agents).toEqual([
'__BLOCK1[',
'C',
'A',
'__BLOCK1]',
]);
expect(block1.leftColumn).toEqual('__BLOCK1[');
expect(block1.rightColumn).toEqual('__BLOCK1]');
}); });
it('allows empty block parts after split', () => { it('allows empty block parts after split', () => {
@ -237,15 +311,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
expect(sequence.stages).toEqual([ const block0 = sequence.stages[0];
{type: 'block', agents: ['A', 'B'], sections: [ expect(block0.sections).toEqual([
{mode: 'if', label: 'abc', stages: [ {mode: 'if', label: 'abc', stages: [
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: ['A', 'B']},
]},
{mode: 'else', label: 'xyz', stages: []},
]}, ]},
{type: AGENT_END, agents: ['A', 'B'], mode: 'none'}, {mode: 'else', label: 'xyz', stages: []},
]); ]);
}); });
@ -257,15 +329,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
expect(sequence.stages).toEqual([ const block0 = sequence.stages[0];
{type: 'block', agents: ['A', 'B'], sections: [ expect(block0.sections).toEqual([
{mode: 'if', label: 'abc', stages: []}, {mode: 'if', label: 'abc', stages: []},
{mode: 'else', label: 'xyz', stages: [ {mode: 'else', label: 'xyz', stages: [
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: ['A', 'B']},
]},
]}, ]},
{type: AGENT_END, agents: ['A', 'B'], mode: 'none'},
]); ]);
}); });
@ -281,6 +351,18 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(sequence.stages).toEqual([]); expect(sequence.stages).toEqual([]);
}); });
it('does not create virtual agents for empty blocks', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_END},
{type: BLOCK_END},
]});
expect(sequence.agents).toEqual(['[', ']']);
});
it('removes entirely empty nested blocks', () => { it('removes entirely empty nested blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
@ -291,16 +373,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
const block0 = sequence.stages[0];
expect(sequence.stages).toEqual([ expect(block0.sections).toEqual([
{type: 'block', agents: ['A', 'B'], sections: [ {mode: 'if', label: 'abc', stages: [
{mode: 'if', label: 'abc', stages: [ {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, {type: '->', agents: ['A', 'B']},
{type: '->', agents: ['A', 'B']},
]},
{mode: 'else', label: 'xyz', stages: []},
]}, ]},
{type: AGENT_END, agents: ['A', 'B'], mode: 'none'}, {mode: 'else', label: 'xyz', stages: []},
]); ]);
}); });