mono/packages/xblox/src/engine/execute-block.ts
2026-04-07 19:41:32 +02:00

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 };
}