prototype-3x5/src/vm.ts

97 lines
2.9 KiB
TypeScript
Raw Normal View History

2023-09-08 04:03:48 +00:00
import {
2023-11-21 02:13:56 +00:00
AsText, Concat, ErrorResult, InterpolatedPiece, ProcResult, Script, SourcePos, TextPiece, Word
} from './words';
2023-09-08 04:03:48 +00:00
/**
* "Mode" of the environment a script runs in; determines access to mutability features and such.
*
2023-11-21 02:13:56 +00:00
* "findingAction": preparing a response to a UI action, which can involve recalculating variables, but has no side-effects itself.
*
2023-09-08 04:03:48 +00:00
* "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.
*/
2023-11-21 02:13:56 +00:00
export type ScriptType = ["findingAction", SourcePos] | ["action"] | ["render"];
2023-09-08 04:03:48 +00:00
export type Proc<Context> = (
state: Vm<Context>,
argv: TextPiece[]
) => ProcResult;
2023-09-08 04:03:48 +00:00
/**
* State for running a script in.
*/
export type Vm<Context = {}> = {
2023-09-08 04:03:48 +00:00
/** Mutability status */
mode: ScriptType;
/** Implementations of commands scripts can run */
commands: Record<string, Proc<Context>>;
2023-09-08 04:03:48 +00:00
/** Markup to render / output */
output: string;
} & Context;
2023-09-08 04:03:48 +00:00
function evaluateWord<Context>(
state: Vm<Context>,
2023-09-08 04:03:48 +00:00
word: Word | InterpolatedPiece
): TextPiece | ErrorResult {
2023-10-19 00:06:52 +00:00
if ("bare" in word || "text" in word || "html" in word) {
2023-09-08 04:03:48 +00:00
return word;
} else if ("variable" in word) {
return { text: "" };
} else if ("script" in word) {
return runNoctl(state, word.script);
} else {
let fullWord = null;
for (const piece of word.pieces) {
const result = evaluateWord(state, piece);
if ("error" in result) {
return result;
} else {
fullWord = Concat(fullWord, result);
}
}
return fullWord ?? { text: "" };
2023-09-08 04:03:48 +00:00
}
}
2023-10-21 01:02:27 +00:00
const NUMBER = /^\d+$/;
2023-09-08 04:03:48 +00:00
/**
* 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<Context>(
state: Vm<Context>,
2023-09-08 04:03:48 +00:00
script: Script,
onReturn?: (word: TextPiece | ErrorResult) => void
): TextPiece | ErrorResult {
let returnWord: TextPiece | ErrorResult = { text: "" };
for (const command of script) {
const argv: TextPiece[] = [];
for (const word of command) {
const processedWord = evaluateWord(state, word);
if ("error" in processedWord) {
onReturn?.(processedWord);
return processedWord;
} else {
argv.push(processedWord);
}
}
2023-09-08 04:03:48 +00:00
const name = AsText(argv[0]);
if (name in state.commands) {
returnWord = state.commands[name](state, argv);
2023-10-21 01:02:27 +00:00
} else if (NUMBER.test(name) && "expr" in state.commands) {
returnWord = state.commands.expr(state, argv);
2023-09-08 04:03:48 +00:00
} else {
returnWord = { error: `Unknown Command: ${name}` };
2023-09-08 04:03:48 +00:00
}
onReturn?.(returnWord);
}
2023-09-08 04:03:48 +00:00
return returnWord;
}