182 lines
5.4 KiB
TypeScript
182 lines
5.4 KiB
TypeScript
import { RunScriptBlock } from '../blocks/run-script-block.js';
|
|
import type { BlockNode, CaseBlockNode } from '../schema/blocks-file.js';
|
|
import { evalExpression, runRunScriptBlock } from '../runtime/run-script-vm.js';
|
|
import { coerceNumber } from './coerce.js';
|
|
|
|
export type ExecState = {
|
|
/** Legacy ForBlock `settings.override.args[0]` — loop index. */
|
|
loopIndex?: number;
|
|
/** Set by `BreakBlock` for parent `SwitchBlock` (legacy `switchblock.stop()`). */
|
|
switchCtl?: { stopped: boolean };
|
|
};
|
|
|
|
function delay(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function isTruthy(value: unknown): boolean {
|
|
return Boolean(value);
|
|
}
|
|
|
|
function forCondition(counter: number | string, comparator: string, finalStr: string, ctx: object): boolean {
|
|
const condStr = `${counter}${comparator}${finalStr}`;
|
|
return isTruthy(evalExpression(condStr, ctx));
|
|
}
|
|
|
|
function forStep(counter: number | string, modifier: string, ctx: object): unknown {
|
|
const expr = `${counter}${modifier}`;
|
|
return evalExpression(expr, ctx);
|
|
}
|
|
|
|
async function executeFor(node: Extract<BlockNode, { kind: 'for' }>, ctx: object, state?: ExecState): Promise<unknown> {
|
|
let counter = coerceNumber(evalExpression(node.initial, ctx));
|
|
if (Number.isNaN(counter)) {
|
|
counter = 0;
|
|
}
|
|
let last: unknown;
|
|
while (forCondition(counter, node.comparator, node.final, ctx)) {
|
|
const childState: ExecState = { ...state, loopIndex: counter };
|
|
last = await runBlockList(node.items, ctx, childState);
|
|
const nextRaw = forStep(counter, node.modifier, ctx);
|
|
const next = coerceNumber(nextRaw);
|
|
if (Number.isNaN(next)) {
|
|
break;
|
|
}
|
|
if (next === counter) {
|
|
break;
|
|
}
|
|
counter = next;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
async function executeWhile(node: Extract<BlockNode, { kind: 'while' }>, ctx: object, state?: ExecState): Promise<unknown> {
|
|
const limit = node.loopLimit ?? 1500;
|
|
let iterations = 0;
|
|
let last: unknown;
|
|
while (isTruthy(evalExpression(node.condition, ctx)) && iterations < limit) {
|
|
last = await runBlockList(node.items, ctx, state);
|
|
iterations++;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
/** Legacy CaseBlock.solve comparison: `switchValue + comparator + expression` evaluated as one expression. */
|
|
function matchCase(ctx: object, variable: string, item: Pick<CaseBlockNode, 'comparator' | 'expression'>): boolean {
|
|
const sv = evalExpression(`this.${variable}`, ctx);
|
|
const rhs = evalExpression(item.expression, ctx);
|
|
const combined = `${String(sv)}${item.comparator}${String(rhs)}`;
|
|
return isTruthy(evalExpression(combined, ctx));
|
|
}
|
|
|
|
async function executeSwitch(node: Extract<BlockNode, { kind: 'switch' }>, ctx: object, state?: ExecState): Promise<unknown> {
|
|
const switchCtl = state?.switchCtl ?? { stopped: false };
|
|
const childState: ExecState = { ...state, switchCtl };
|
|
let anyMatched = false;
|
|
let last: unknown;
|
|
|
|
for (const item of node.items) {
|
|
if (item.kind === 'case') {
|
|
if (matchCase(ctx, node.variable, item)) {
|
|
last = await runBlockList(item.consequent, ctx, childState);
|
|
anyMatched = true;
|
|
break;
|
|
}
|
|
}
|
|
if (switchCtl.stopped) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!anyMatched) {
|
|
for (const item of node.items) {
|
|
if (item.kind === 'switchDefault') {
|
|
last = await runBlockList(item.consequent, ctx, childState);
|
|
}
|
|
}
|
|
}
|
|
|
|
return last;
|
|
}
|
|
|
|
export async function runBlockList(blocks: BlockNode[], ctx: object, state?: ExecState): Promise<unknown> {
|
|
let last: unknown;
|
|
for (const b of blocks) {
|
|
last = await executeBlock(b, ctx, state);
|
|
}
|
|
return last;
|
|
}
|
|
|
|
/**
|
|
* Execute one block node with `ctx` as `this` in expressions and runScript blocks.
|
|
*/
|
|
export async function executeBlock(node: BlockNode, ctx: object, state?: ExecState): Promise<unknown> {
|
|
if (node.kind === 'runScript') {
|
|
return runRunScriptBlock(new RunScriptBlock({ method: node.method }), ctx, []);
|
|
}
|
|
|
|
if (node.kind === 'wait') {
|
|
await delay(node.ms);
|
|
return undefined;
|
|
}
|
|
|
|
if (node.kind === 'break') {
|
|
if (state?.switchCtl) {
|
|
state.switchCtl.stopped = true;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
if (node.kind === 'for') {
|
|
return executeFor(node, ctx, state);
|
|
}
|
|
|
|
if (node.kind === 'while') {
|
|
return executeWhile(node, ctx, state);
|
|
}
|
|
|
|
if (node.kind === 'switch') {
|
|
return executeSwitch(node, ctx, state);
|
|
}
|
|
|
|
if (node.kind === 'if') {
|
|
if (isTruthy(evalExpression(node.condition, ctx))) {
|
|
return runBlockList(node.consequent, ctx, state);
|
|
}
|
|
|
|
if (node.elseIfBlocks) {
|
|
for (const branch of node.elseIfBlocks) {
|
|
if (isTruthy(evalExpression(branch.condition, ctx))) {
|
|
return runBlockList(branch.consequent, ctx, state);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node.alternate?.length) {
|
|
return runBlockList(node.alternate, ctx, state);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export type ExecuteRootsResult = {
|
|
/** Return value of each top-level block, in order. */
|
|
results: unknown[];
|
|
/** Last top-level return (same as `results[results.length - 1]`). */
|
|
result: unknown;
|
|
};
|
|
|
|
/** Run many top-level blocks in order (shared `ctx`), e.g. UI “run all” / script-like flow. */
|
|
export async function executeRoots(roots: BlockNode[], ctx: object): Promise<ExecuteRootsResult> {
|
|
const results: unknown[] = [];
|
|
let result: unknown;
|
|
for (const block of roots) {
|
|
result = await executeBlock(block, ctx);
|
|
results.push(result);
|
|
}
|
|
return { results, result };
|
|
}
|