/** * Ball TypeScript Engine — interprets Ball programs directly from JSON. * * This is a compatibility wrapper around the compiled (self-hosted) * Ball engine. The compiled engine is generated by compiling * `dart/self_host/engine.ball.json` through `@ball-lang/compiler`. * * Usage: * import { BallEngine } from '@ball-lang/engine'; * const engine = new BallEngine(programJson); * await engine.run(); * console.log(engine.getOutput()); */ import { BallEngine as CompiledEngine, } from './compiled_engine.ts'; import * as _compiled from './compiled_engine.ts'; import { createEngineSetup } from './engine_setup.ts'; import { unwrapBallFile } from './ball_file.ts'; // All proto3-JSON normalization, the method-dispatch handler, the extra // std-function registrations, and the compiled-engine patches live in the // shared `engine_setup` module so the Phase 2.7b conformance harness (which // runs against a freshly compiled engine) can reuse the exact same setup. const _setup = createEngineSetup(_compiled as any); const protoWrap = _setup.protoWrap; const MethodDispatchHandler = _setup.MethodDispatchHandler; const StdModuleHandler = _compiled.StdModuleHandler; const registerExtraStdFunctions = _setup.registerExtraStdFunctions; const seedGlobalScope = _setup.seedGlobalScope; const patchCompiledEngine = _setup.patchCompiledEngine; const patchScopeBindings = _setup.patchScopeBindings; // ── Compatibility wrapper ────────────────────────────────────────────────── export interface BallEngineOptions { stdout?: (msg: string) => void; stderr?: (msg: string) => void; /** Maximum execution time in milliseconds (null = unbounded). */ timeoutMs?: number | null; /** Maximum memory usage in bytes (null = unbounded). */ maxMemoryBytes?: number | null; /** Maximum number of modules allowed in the program (default: 1000000). */ maxModules?: number; /** Maximum expression nesting depth (default: 1000000). */ maxExpressionDepth?: number; /** Maximum program JSON size in bytes (null = skip check). */ maxProgramSizeBytes?: number | null; /** Whether to run in sandbox mode (blocks file I/O, env access, etc.). */ sandbox?: boolean; /** Maximum recursion depth (default: 100000). */ maxRecursionDepth?: number; } export class BallEngine { private _compiledEngine: CompiledEngine; private _output: string[] = []; constructor(program: any, options: BallEngineOptions = {}) { // Ball files are self-describing `google.protobuf.Any` envelopes. Unwrap // the `@type` envelope (if present) before normalizing; callers passing an // already-unwrapped Program object are still supported. const parsed = typeof program === 'string' ? JSON.parse(program) : program; const unwrapped = unwrapBallFile(parsed); const normalized = protoWrap(unwrapped); // The compiled engine's program-size validation (run() → // _validateProgramLimits) calls `program.writeToBuffer()` — the Dart // protobuf binary encoding. Plain-JSON programs have no protobuf runtime // here, so expose the UTF-8 JSON encoding of the (unwrapped) input as the // byte-size source. Non-enumerable so program traversal never sees it. if (typeof normalized === 'object' && normalized !== null) { Object.defineProperty(normalized, 'writeToBuffer', { value: () => new TextEncoder().encode(JSON.stringify(unwrapped)), enumerable: false, writable: true, configurable: true, }); } const stdHandler = new StdModuleHandler(); const methodHandler = new MethodDispatchHandler(); const outputCapture = this._output; const stdoutFn = options.stdout ?? ((msg: string) => { outputCapture.push(msg); }); const stderrFn = options.stderr ?? (() => {}); // The self-hosted engine constructor takes 16 positional parameters: // program, stdout, stderr, stdinReader, envGet, args, enableProfiling, // maxRecursionDepth, timeoutMs, maxMemoryBytes, maxModules, // maxExpressionDepth, maxProgramSizeBytes, sandbox, moduleHandlers, // resolver // (older IR revisions only had 9). Options default to permissive values // unless the caller explicitly sets them. this._compiledEngine = new CompiledEngine( normalized, stdoutFn, stderrFn, null, // stdinReader null, // envGet [], // args false, // enableProfiling options.maxRecursionDepth ?? 100000, // maxRecursionDepth options.timeoutMs ?? null, // timeoutMs (null = unbounded) options.maxMemoryBytes ?? null, // maxMemoryBytes (null = unbounded) options.maxModules ?? 1000000, // maxModules options.maxExpressionDepth ?? 1000000, // maxExpressionDepth options.maxProgramSizeBytes ?? null, // maxProgramSizeBytes (null = skip) options.sandbox ?? false, // sandbox [methodHandler as any, stdHandler], // moduleHandlers null, // resolver ); // Patch scope bindings to use null-prototype objects (avoids // Object.prototype.values/entries/keys getters polluting `in` checks). if (typeof (globalThis as any)._patchScopeBindings === 'function') { (globalThis as any)._patchScopeBindings(this._compiledEngine._globalScope); } else { patchScopeBindings(this._compiledEngine._globalScope); } registerExtraStdFunctions(stdHandler, this._compiledEngine); seedGlobalScope(this._compiledEngine); patchCompiledEngine(this._compiledEngine); // Note: double formatting (12 vs 12.0) handled by BallDouble in preamble. // The compiled engine returns raw numbers for literals; BallDouble wrapping // happens in arithmetic operations (_stdAdd, _stdBinary, etc.). } /** * Run the program. Returns a promise that resolves to the captured * output lines (same content as `getOutput()`). * * NOTE: The compiled engine is async internally. If you were relying * on synchronous `run()`, wrap your call in `await`. */ async run(): Promise { await this._compiledEngine.run(); return this._output; } /** Retrieve lines printed via `std.print` (when no custom stdout was given). */ getOutput(): string[] { return this._output; } } // Re-export useful compiled-engine types for advanced consumers. export { StdModuleHandler } from './compiled_engine.ts';