Source: EvowareCompiler.js

/**
 * Roboliq: Automation for liquid-handling robots
 * @copyright 2017, ETH Zurich, Ellis Whitehead
 * @license GPL-3.0
 */

/**
 * Module for compiling an instruction list (created by roboliq) to
 * an Evoware script.
 * @module
 */
import _ from 'lodash';
import naturalSort from 'javascript-natural-sort';
import path from 'path';
import M from './Medley.js';
import commandHelper from 'roboliq-processor/dist/commandHelper.js';
import * as EvowareTableFile from './EvowareTableFile.js';
import * as evowareHelper from './commands/evowareHelper.js';
import * as evoware from './commands/evoware.js';
import * as pipetter from './commands/pipetter.js';
import * as system from './commands/system.js';
import * as timer from './commands/timer.js';
import * as transporter from './commands/transporter.js';

const commandHandlers = {
	"evoware._execute": evoware._execute,
	"evoware._facts": evoware._facts,
	"evoware._raw": evoware._raw,
	"evoware._subroutine": evoware._subroutine,
	"evoware._userPrompt": evoware._userPrompt,
	"evoware._variable": evoware._variable,
	"pipetter._aspirate": pipetter._aspirate,
	"pipetter._dispense": pipetter._dispense,
	"pipetter._measureVolume": pipetter._measureVolume,
	"pipetter._mix": pipetter._mix,
	"pipetter._pipette": pipetter._pipette,
	"pipetter._washTips": pipetter._washTips,
	"system.runtimeExitLoop": system.runtimeExitLoop,
	"system.runtimeLoadVariables": system.runtimeLoadVariables,
	"system.runtimeSteps": system.runtimeSteps,
	"timer._sleep": timer._sleep,
	"timer._start": timer._start,
	"timer._wait": timer._wait,
	"transporter._moveLidFromContainerToSite": transporter._moveLidFromContainerToSite,
	"transporter._moveLidFromSiteToContainer": transporter._moveLidFromSiteToContainer,
	"transporter._movePlate": transporter._movePlate
};

/**
 * Compile a protocol for a given evoware setup.
 *
 * @param  {EvowareCarrierData} carrierData
 * @param  {object} table - table object (see EvowareTableFile.load)
 * @param  {roboliq:Protocol} protocol
 * @param  {array} agents - string array of agent names that this script should generate script(s) for
 * @param  {object} options - an optional map of options; e.g. set timing=false to avoid outputting time-logging instructions
 * @return {array} an array of {table, lines} items; one item is generated per required table layout.  lines is an array of strings.
 */
export function compile(table, protocol, agents, options = {}) {
	// console.log(`compile:`)
	// console.log({options})
	options = _.defaults(options, _.get(protocol.config, "evowareCompiler", {}));
	// console.log({options})
	table = _.cloneDeep(table);
	const objects = _.cloneDeep(protocol.objects);
	const evowareVariables = {}
	const results = compileStep(table, protocol, agents, [], objects, evowareVariables, [], options);

	let lines = [];
	if (!_.isEmpty(results)) {
		// Prepend variables
		// console.log({evowareVariables})
		const variableList = _.reverse(_.sortBy(_.toPairs(evowareVariables), x => x[0]));
		// console.log({variableList})
		_.forEach(variableList, ([name, value]) => {
			// console.log({name, value, line: evowareHelper.createVariableLine(name, value)})
			results.unshift({line: evowareHelper.createVariableLine(name, value)});
		});
		// Prepend token to call 'initRun'
		results.unshift({line: evowareHelper.createExecuteLine(options.variables.ROBOLIQ, ["initRun", options.variables.SCRIPTFILE], true)});
		// Append token to reset the last moved ROMA
		results.push(transporter.moveLastRomaHome({objects}));

		lines = _(results).flattenDeep().map(x => x.line).compact().value();
		if (!_.isEmpty(lines)) {
			// Prepend token to create the TEMPDIR
			if (_.some(lines, line => line.indexOf(options.variables.TEMPDIR) >= 0)) {
				lines.unshift(evowareHelper.createExecuteLine("cmd", ["/c", "mkdir", options.variables.TEMPDIR], true));
			}

			// Prepend token to open HTML
			if (_.get(options, "checkBench", true)) {
				lines.unshift(evowareHelper.createUserPromptLine("Please check the bench setup and then confirm this dialog when you're done"));
				lines.unshift(evowareHelper.createExecuteLine(options.variables.BROWSER, [path.win32.dirname(options.variables.SCRIPTFILE)+"\\index.html"], false));
			}
		}
	}

	return [{table, lines, tokenTree: results}];
}

export function compileStep(table, protocol, agents, path, objects, evowareVariables, loopEndStack = [], options = {}) {
	// console.log(`compileStep: ${path.join(".")}`)
	// console.log({options})
	try {
		const results = compileStepSub(table, protocol, agents, path, objects, evowareVariables, loopEndStack, options);
		return results;
	} catch (e) {
		console.log("ERROR: "+path.join("."));
		console.log(JSON.stringify(_.get(protocol.steps, path)))
		console.log(e)
		console.log(e.stack)
	}
	return [];
}

function compileStepSub(table, protocol, agents, path, objects, evowareVariables, loopEndStack, options) {
	// console.log(`compileStepSub: ${path.join(".")}`)
	// console.log({options})
	if (_.isUndefined(objects)) {
		objects = _.cloneDeep(protocol.objects);
	}

	const stepId = path.join(".");
	//console.log({steps: protocol.steps})
	const step = (_.isEmpty(path)) ? protocol.steps : _.get(protocol.steps, path);
	// console.log({step})
	if (_.isUndefined(step))
		return undefined;

	if (stepId !== "" && protocol.COMPILER) {
		// Handle suspending
		if (protocol.COMPILER.suspendStepId) {
			const cmp = naturalSort(stepId, protocol.COMPILER.suspendStepId);
			// console.log({stepId, suspendStepId: protocol.COMPILER.suspendStepId, cmp})
			// If we've passed the suspend step, quit compiling
			if (cmp > 0) {
				return undefined;
			}
			// If we're before the suspend step, skip unless the current step is a parent step
			else if (cmp < 0 && !_.startsWith(protocol.COMPILER.suspendStepId, stepId+".")) {
				return undefined;
			}
		}
		// Handle resuming
		if (protocol.COMPILER.resumeStepId) {
			const cmp = naturalSort(stepId, protocol.COMPILER.resumeStepId);
			// console.log({stepId, resumeStepId: protocol.COMPILER.resumeStepId, cmp})
			// Skip until we've passed the resume step
			if (cmp <= 0) {
				return undefined;
			}
		}
	}

	const results = [];

	const commandHandler = commandHandlers[step.command];
	let generatedCommandLines = false;
	let generatedTimingLogs = false;
	const agentMatch = _.isUndefined(step.agent) || _.includes(agents, step.agent);
	// If there is no command handler for this step, then handle sub-steps
	if (_.isUndefined(commandHandler) || !agentMatch) {
		// Find all sub-steps (properties that start with a digit)
		var keys = _.filter(_.keys(step), function(key) {
			var c = key[0];
			return (c >= '0' && c <= '9');
		});
		// Sort them in "natural" order
		keys.sort(naturalSort);
		// console.log({keys})

		const isLoop = _.includes(["system.repeat", "experiment.forEachGroup", "experiment.forEachRow"], step.command);
		const loopEndName = `_${stepId}End`;
		const loopEndStack2 = (isLoop) ? [loopEndName].concat(loopEndStack) : loopEndStack;
		let needLoopLabel = false;

		// Try to expand the substeps
		for (const key of keys) {
			const result1 = compileStep(table, protocol, agents, path.concat(key), objects, evowareVariables, loopEndStack2, options);
			// Possibly check whether we need a loop label
			if (isLoop && !needLoopLabel) {
				const result2 = _.flattenDeep(result1);
				needLoopLabel = _.some(result2, result => (result.line || "").indexOf(loopEndName) >= 0);
			}
			if (!_.isUndefined(result1)) {
				results.push(result1);
			}
		}

		if (needLoopLabel) {
			results.push({line: `Comment("${loopEndName}");`});
		}
	}
	// Else, handle the step's command:
	else {
		const data = {
			objects,
			//predicates,
			//planHandlers: protocol.planHandlers,
			schemas: protocol.schemas,
			accesses: [],
			//files: filecache,
			protocol,
			path,
			loopEndStack,
			evowareVariables
		};
		// Parse command options
		const schema = protocol.schemas[step.command];
		const parsed = (schema)
			? commandHelper.parseParams(step, data, schema)
			: {orig: step};
		// Handle the command
		const result0 = commandHandler(step, parsed, data);
		// For all returned results:
		_.forEach(_.compact(result0), result1 => {
			// console.log("result1: "+JSON.stringify(result1));
			results.push(result1);
			if (result1.effects) {
				_.forEach(result1.effects, (effect, path2) => {
					M.setMut(objects, path2, effect);
				});
			}
			if (!_.isEmpty(result1.tableEffects)) {
				//console.log("tableEffects: "+JSON.stringify(result1.tableEffects))
				_.forEach(result1.tableEffects, ([path2, value]) => {
					//console.log({path2, value})
					// TODO: Need to check whether a table change is required because a different labware model is now used at a given site
					M.setMut(table, path2, value);
				});
			}
		});

		if (_.isEmpty(results)) {
			return [];
		}

		// Check whether command produced any output lines
		generatedCommandLines = _.find(_.flattenDeep(results), x => _.has(x, "line"));

		// Possibly wrap the instructions in calls to pathToRoboliqRuntimeCli in order to check timing
		// console.log({options, timing: _.get(options, "timing", true)})
		if (generatedCommandLines && _.get(options, "timing", false) === true) {
			const agent = _.get(objects, step.agent);
			const exePath = "%{ROBOLIQ}";//options.variables.ROBOLIQ;
			// console.log({agent})
			results.unshift({line: evowareHelper.createExecuteLine(exePath, ["begin", "--step", stepId, "--script", "%{SCRIPTFILE}"], false)});
			results.push({line: evowareHelper.createExecuteLine(exePath, ["end", "--step", stepId, "--script", "%{SCRIPTFILE}"], false)});
			generatedTimingLogs = true;
		}

	}

	// Process the protocol's effects
	const effects = _.get(protocol, ["effects", stepId]);
	if (!_.isUndefined(effects)) {
		_.forEach(effects, (effect, path2) => {
			M.setMut(objects, path2, effect);
		});
	}

	const generatedComment = !_.isEmpty(step.description);
	if (generatedComment) {
		const hasInstruction = _.find(_.flattenDeep(results), x => _.has(x, "line"));
		const text = `${stepId}) ${step.description}`;
		results.unshift({line: `Comment("${text}");`});
	}

	// Possibly wrap the instructions in a group
	const generatedGroup =
		(generatedTimingLogs) ||
		(!generatedCommandLines && generatedComment && results.length > 1);
	if (generatedGroup) {
		const text = `Step ${stepId}`;
		results.unshift({line: `Group("${text}");`});
		results.push({line: `GroupEnd();`});
	}

	// console.log({stepId, results})
	substitutePathVariables(results, options);

	return results;
}

function substitutePathVariables(results, options) {
	// console.log({results, options})
	if (!options.variables)
		return;

	for (let i = 0; i < results.length; i++) {
		const result = results[i];
		if (_.isArray(result)) {
			substitutePathVariables(result, options);
		}
		else {
			let line = results[i].line;
			if (line) {
				line = line.replace("%{ROBOLIQ}", options.variables.ROBOLIQ);
				line = line.replace("%{SCRIPTFILE}", options.variables.SCRIPTFILE);
				line = line.replace("%{SCRIPTDIR}", options.variables.SCRIPTDIR);
				line = line.replace("%{TEMPDIR}", options.variables.TEMPDIR);
				results[i].line = line;
			}
		}
	}
}