diff --git a/src/3x5.ts b/src/3x5.ts index 7619ddb..5ed3b9c 100644 --- a/src/3x5.ts +++ b/src/3x5.ts @@ -1,4 +1,6 @@ -import { parse } from "./parser"; +import { parse } from './parser'; +import { runNoctl, Vm } from './vm'; +import { AsHtml } from './words'; /** * Basic unit of information, also an "actor" in the programming system @@ -12,25 +14,6 @@ type Card = { code: string; }; -/** - * "Mode" of the environment a script runs in; determines access to mutability features and such. - * - * "action": response to a UI action; allowed to modify card fields and access time and random numbers. - * - * "render": deterministic generation of display markup from card and workspace state; can only modify temporary variables. - */ -type ScriptType = "action" | "render"; - -/** - * State for running a script in. - */ -type Vm = { - /** Mutability status */ - mode: ScriptType; - /** Markup to render / output */ - output: string; -}; - /** * @param state VM state * @param code Script to run @@ -39,7 +22,7 @@ type Vm = { function renderCard(state: Vm, code: string) { const script = parse(code); if (script[0]) { - state.output = JSON.stringify(script[1], null, 2); + runNoctl(state, script[1], (word) => (state.output += AsHtml(word) + "\n")); } else { state.output = script[1]; } @@ -101,6 +84,7 @@ const debugDisplay = document.createElement("pre"); function render() { const vm: Vm = { mode: "render", + commands: {}, output: "", }; const html = renderCard(vm, theCard.code); diff --git a/src/vm.ts b/src/vm.ts new file mode 100644 index 0000000..fb8fa7d --- /dev/null +++ b/src/vm.ts @@ -0,0 +1,74 @@ +import { + AsHtml, AsText, Concat, EnchantedWord, HtmlWord, InterpolatedPiece, Script, TextPiece, TextWord, + Word +} from './words'; + +/** + * "Mode" of the environment a script runs in; determines access to mutability features and such. + * + * "action": response to a UI action; allowed to modify card fields and access time and random numbers. + * + * "render": deterministic generation of display markup from card and workspace state; can only modify temporary variables. + */ +export type ScriptType = "action" | "render"; + +export type Proc = (state: Vm, argv: TextPiece[]) => TextPiece; + +/** + * State for running a script in. + */ +export type Vm = { + /** Mutability status */ + mode: ScriptType; + /** Implementations of commands scripts can run */ + commands: Record; + /** Markup to render / output */ + output: string; +}; + +function evaluateWord( + state: Vm, + word: Word | InterpolatedPiece +): TextWord | EnchantedWord | HtmlWord { + if ("enchanted" in word || "text" in word || "html" in word) { + return word; + } else if ("variable" in word) { + return { text: "" }; + } else if ("script" in word) { + return runNoctl(state, word.script); + } else { + return ( + word.pieces + .map((piece) => evaluateWord(state, piece)) + .reduce(Concat, null) ?? { text: "" } + ); + } +} + +/** + * Runs a script in the context of a Noctl state. Potentially mutates the state. + * + * @param onReturn callback optionally invoked with the return word for each top-level command (not triggered by command substitutions) + * @returns the return word of the final command in the script, or empty text if the script is empty. + */ +export function runNoctl( + state: Vm, + script: Script, + onReturn?: (word: TextPiece) => void +): TextPiece { + let returnWord: TextPiece = { text: "" }; + + script.forEach((command) => { + const argv = command.map((word) => evaluateWord(state, word)); + const name = AsText(argv[0]); + if (name in state.commands) { + returnWord = state.commands[name](state, argv); + } else { + // TODO: implement error propagation + returnWord = { html: `UNKNOWN COMMAND: ${AsHtml(argv[0])}` }; + } + onReturn?.(returnWord); + }); + + return returnWord; +}