UNPKG

jssm

Version:

A Javascript finite state machine (FSM) with a terse DSL and a simple API. Most FSMs are one-liners. Fast, easy, powerful, well tested, typed with TypeScript, and visualizations. MIT License.

367 lines (364 loc) 13.3 kB
import { JssmError } from './jssm_error'; import { parse } from './fsl_parser'; import { arrow_left_kind, arrow_right_kind } from './jssm_arrow'; import { find_repeated, name_bind_prop_and_state } from './jssm_util'; import { reduce as reduce_to_639 } from 'reduce-to-639-1'; /********* * * Internal method meant to perform factory assembly of an edge. Not meant for * external use. * * @internal * * @typeparam mDT The type of the machine data member; usually omitted * */ // TODO add at-param to docblock function makeTransition(this_se, from, to, isRight, _wasList, _wasIndex) { const kind = isRight ? arrow_right_kind(this_se.kind) : arrow_left_kind(this_se.kind), edge = { from, to, kind, after_time: isRight ? this_se.r_after : this_se.l_after, forced_only: kind === 'forced', main_path: kind === 'main' }; // if ((wasList !== undefined) && (wasIndex === undefined)) { throw new JssmError(undefined, `Must have an index if transition was in a list"); } // if ((wasIndex !== undefined) && (wasList === undefined)) { throw new JssmError(undefined, `Must be in a list if transition has an index"); } /* if (typeof edge.to === 'object') { if (edge.to.key === 'cycle') { if (wasList === undefined) { throw new JssmError(undefined, "Must have a waslist if a to is type cycle"); } const nextIndex = wrapBy(wasIndex, edge.to.value, wasList.length); edge.to = wasList[nextIndex]; } } */ const action = isRight ? 'r_action' : 'l_action', probability = isRight ? 'r_probability' : 'l_probability'; if (this_se[action]) { edge.action = this_se[action]; } if (this_se[probability]) { edge.probability = this_se[probability]; } return edge; } /********* * * This method wraps the parser call that comes from the peg grammar, * {@link parse}. Generally neither this nor that should be used directly * unless you mean to develop plugins or extensions for the machine. * * Parses the intermediate representation of a compiled string down to a * machine configuration object. If you're using this (probably don't,) you're * probably also using {@link compile} and {@link Machine.constructor}. * * ```typescript * import { parse, compile, Machine } from 'jssm'; * * const intermediate = wrap_parse('a -> b;', {}); * // [ {key:'transition', from:'a', se:{kind:'->',to:'b'}} ] * * const cfg = compile(intermediate); * // { start_states:['a'], transitions: [{ from:'a', to:'b', kind:'legal', forced_only:false, main_path:false }] } * * const machine = new Machine(cfg); * // Machine { _instance_name: undefined, _state: 'a', ... * ``` * * This method is mostly for plugin and intermediate tool authors, or people * who need to work with the machine's intermediate representation. * * # Hey! * * Most people looking at this want either the `sm` operator or method `from`, * which perform all the steps in the chain. The library's author mostly uses * operator `sm`, and mostly falls back to `.from` when needing to parse * strings dynamically instead of from template literals. * * Operator {@link sm}: * * ```typescript * import { sm } from 'jssm'; * * const lswitch = sm`on <=> off;`; * ``` * * Method {@link from}: * * ```typescript * import * as jssm from 'jssm'; * * const toggle = jssm.from('up <=> down;'); * ``` * * `wrap_parse` itself is an internal convenience method for alting out an * object as the options call. Not generally meant for external use. * * @param input The FSL code to be evaluated * * @param options Things to control about the instance * */ function wrap_parse(input, options) { return parse(input, options || {}); } /********* * * Internal method performing one step in compiling rules for transitions. Not * generally meant for external use. * * @internal * * @typeparam mDT The type of the machine data member; usually omitted * */ function compile_rule_transition_step(acc, from, to, this_se, next_se) { const edges = []; const uFrom = (Array.isArray(from) ? from : [from]), uTo = (Array.isArray(to) ? to : [to]); uFrom.map((f) => { uTo.map((t) => { const right = makeTransition(this_se, f, t, true); if (right.kind !== 'none') { edges.push(right); } const left = makeTransition(this_se, t, f, false); if (left.kind !== 'none') { edges.push(left); } }); }); const new_acc = acc.concat(edges); if (next_se) { return compile_rule_transition_step(new_acc, to, next_se.to, next_se, next_se.se); } else { return new_acc; } } /********* * * Internal method performing one step in compiling rules for transitions. Not * generally meant for external use. * * @internal * */ function compile_rule_handle_transition(rule) { return compile_rule_transition_step([], rule.from, rule.se.to, rule.se, rule.se.se); } /********* * * Internal method performing one step in compiling rules for transitions. Not * generally meant for external use. * * @internal * */ function compile_rule_handler(rule) { if (rule.key === 'transition') { return { agg_as: 'transition', val: compile_rule_handle_transition(rule) }; } if (rule.key === 'machine_language') { return { agg_as: 'machine_language', val: reduce_to_639(rule.value) }; } // manually rehandled to make `undefined` as a property safe if (rule.key === 'property_definition') { const ret = { agg_as: 'property_definition', val: { name: rule.name } }; if (rule.hasOwnProperty('default_value')) { ret.val.default_value = rule.default_value; } if (rule.hasOwnProperty('required')) { ret.val.required = rule.required; } return ret; } // state properties are in here if (rule.key === 'state_declaration') { if (!rule.name) { throw new JssmError(undefined, 'State declarations must have a name'); } return { agg_as: 'state_declaration', val: { state: rule.name, declarations: rule.value } }; } if (['arrange_declaration', 'arrange_start_declaration', 'arrange_end_declaration'].includes(rule.key)) { return { agg_as: rule.key, val: [rule.value] }; } // things that can only exist once and are just a value under their own name const tautologies = [ 'graph_layout', 'start_states', 'end_states', 'machine_name', 'machine_version', 'machine_comment', 'machine_author', 'machine_contributor', 'machine_definition', 'machine_reference', 'machine_license', 'fsl_version', 'state_config', 'theme', 'flow', 'dot_preamble', 'allows_override', 'default_state_config', 'default_start_state_config', 'default_end_state_config', 'default_hooked_state_config', 'default_active_state_config', 'default_terminal_state_config' ]; if (tautologies.includes(rule.key)) { return { agg_as: rule.key, val: rule.value }; } throw new JssmError(undefined, `compile_rule_handler: Unknown rule: ${JSON.stringify(rule)}`); } /********* * * Compile a machine's JSON intermediate representation to a config object. If * you're using this (probably don't,) you're probably also using * {@link parse} to get the IR, and the object constructor * {@link Machine.construct} to turn the config object into a workable machine. * * ```typescript * import { parse, compile, Machine } from 'jssm'; * * const intermediate = parse('a -> b;'); * // [ {key:'transition', from:'a', se:{kind:'->',to:'b'}} ] * * const cfg = compile(intermediate); * // { start_states:['a'], transitions: [{ from:'a', to:'b', kind:'legal', forced_only:false, main_path:false }] } * * const machine = new Machine(cfg); * // Machine { _instance_name: undefined, _state: 'a', ... * ``` * * This method is mostly for plugin and intermediate tool authors, or people * who need to work with the machine's intermediate representation. * * # Hey! * * Most people looking at this want either the `sm` operator or method `from`, * which perform all the steps in the chain. The library's author mostly uses * operator `sm`, and mostly falls back to `.from` when needing to parse * strings dynamically instead of from template literals. * * Operator {@link sm}: * * ```typescript * import { sm } from 'jssm'; * * const lswitch = sm`on <=> off;`; * ``` * * Method {@link from}: * * ```typescript * import * as jssm from 'jssm'; * * const toggle = jssm.from('up <=> down;'); * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param tree The parse tree to be boiled down into a machine config * */ function compile(tree) { const results = { graph_layout: [], transition: [], start_states: [], end_states: [], state_config: [], state_declaration: [], fsl_version: [], machine_author: [], machine_comment: [], machine_contributor: [], machine_definition: [], machine_language: [], machine_license: [], machine_name: [], machine_reference: [], property_definition: [], state_property: {}, theme: [], flow: [], dot_preamble: [], arrange_declaration: [], arrange_start_declaration: [], arrange_end_declaration: [], machine_version: [], default_state_config: [], default_active_state_config: [], default_hooked_state_config: [], default_terminal_state_config: [], default_start_state_config: [], default_end_state_config: [], allows_override: [] }; tree.map((tr) => { const rule = compile_rule_handler(tr), agg_as = rule.agg_as, val = rule.val; // TODO FIXME no any results[agg_as] = results[agg_as].concat(val); }); const property_keys = results['property_definition'].map(pd => pd.name), repeat_props = find_repeated(property_keys); if (repeat_props.length) { throw new JssmError(undefined, `Cannot repeat property definitions. Saw ${JSON.stringify(repeat_props)}`); } const assembled_transitions = [].concat(...results['transition']); const result_cfg = { start_states: results.start_states.length ? results.start_states : [assembled_transitions[0].from], end_states: results.end_states, transitions: assembled_transitions, state_property: [], }; const oneOnlyKeys = [ 'graph_layout', 'machine_name', 'machine_version', 'machine_comment', 'fsl_version', 'machine_license', 'machine_definition', 'machine_language', 'flow', 'dot_preamble', 'allows_override' ]; oneOnlyKeys.map((oneOnlyKey) => { if (results[oneOnlyKey].length > 1) { throw new JssmError(undefined, `May only have one ${oneOnlyKey} statement maximum: ${JSON.stringify(results[oneOnlyKey])}`); } else { if (results[oneOnlyKey].length) { result_cfg[oneOnlyKey] = results[oneOnlyKey][0]; } } }); ['arrange_declaration', 'arrange_start_declaration', 'arrange_end_declaration', 'machine_author', 'machine_contributor', 'machine_reference', 'theme', 'state_declaration', 'property_definition', 'default_state_config', 'default_start_state_config', 'default_end_state_config', 'default_hooked_state_config', 'default_terminal_state_config', 'default_active_state_config'].map((multiKey) => { if (results[multiKey].length) { result_cfg[multiKey] = results[multiKey]; } }); // re-walk state declarations, already wrapped up, to get state properties, // which go out in a different datastructure results.state_declaration.forEach(sd => { sd.declarations.forEach(decl => { if (decl.key === 'state_property') { const label = name_bind_prop_and_state(decl.name, sd.state); if (result_cfg.state_property.findIndex(c => c.name === label) !== -1) { throw new JssmError(undefined, `A state may only bind a property once (${sd.state} re-binds ${decl.name})`); } else { result_cfg.state_property.push({ name: label, default_value: decl.value }); } } }); }); return result_cfg; } /********* * * An internal convenience wrapper for parsing then compiling a machine string. * Not generally meant for external use. Please see {@link compile} or * {@link sm}. * * @typeparam mDT The type of the machine data member; usually omitted * * @param plan The FSL code to be evaluated and built into a machine config * */ function make(plan) { return compile(wrap_parse(plan)); } export { compile, // compile_rule_handler, // compile_rule_transition_step, // compile_rule_handle_transition, make, makeTransition, wrap_parse };