about summary refs log tree commit diff stats
path: root/js/baba-yaga/experimental/compiler.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/baba-yaga/experimental/compiler.js')
-rw-r--r--js/baba-yaga/experimental/compiler.js728
1 files changed, 728 insertions, 0 deletions
diff --git a/js/baba-yaga/experimental/compiler.js b/js/baba-yaga/experimental/compiler.js
new file mode 100644
index 0000000..d5c70ce
--- /dev/null
+++ b/js/baba-yaga/experimental/compiler.js
@@ -0,0 +1,728 @@
+// compiler.js
+//
+// High-level compiler scaffold for Baba Yaga → JavaScript according to COMPILER.md.
+// This file intentionally contains detailed structure and JSDoc/TODOs so that
+// an engineer can implement the compiler incrementally with minimal ambiguity.
+//
+// Overview of the pipeline implemented here (stubs):
+// - lex + parse → AST (using existing lexer.js, parser.js)
+// - normalize → Core IR (operators→primitives, currying, member→get, when→cond)
+// - codegen mode:
+//   - ski: bracket abstraction to SKI combinators and emit with runtime
+//   - closure: direct JS closures with currying helpers
+//   - hybrid: heuristics to mix both
+// - emit module (UMD default, ESM/CJS available)
+// - optional CLI entry (use runner.js for project CLI; this is dev-friendly only)
+
+// NOTE: This file is deprecated; the active compiler lives at experimental/compiler/compiler.js
+// It re-exports from the new location to preserve existing imports.
+
+import { createLexer } from '../lexer.js';
+import { createParser } from '../parser.js';
+export { compile, DEFAULT_COMPILE_OPTIONS, normalizeAstToIr, lowerIrToCodeIr, applyBracketAbstraction, applyHybridLowering, emitModule, emitProgramBody, generateRuntimePrelude, wrapModule } from './compiler.js';
+
+/**
+ * Compiler options with sensible defaults. Extend as needed.
+ * @typedef {Object} CompileOptions
+ * @property {'ski'|'closure'|'hybrid'} mode - Codegen mode. Default 'ski'.
+ * @property {'umd'|'esm'|'cjs'} format - Module format. Default 'umd'.
+ * @property {'inline'|{ externalPath: string }} runtime - Runtime placement.
+ * @property {'none'|'inline'|'file'} sourcemap - Source map emission.
+ * @property {boolean} pretty - Pretty-print output.
+ * @property {string} moduleName - UMD global name for browser builds.
+ */
+
+/** @type {CompileOptions} */
+export const DEFAULT_COMPILE_OPTIONS = {
+  mode: 'ski',
+  format: 'umd',
+  runtime: 'inline',
+  sourcemap: 'none',
+  pretty: false,
+  moduleName: 'BabaYaga',
+};
+
+/**
+ * Compile Baba Yaga source text to JavaScript.
+ * Orchestrates: lex → parse → normalize → codegen → emit.
+ *
+ * TODO: implement each stage; keep interfaces stable per COMPILER.md.
+ *
+ * @param {string} source - Baba Yaga program text
+ * @param {Partial<CompileOptions>} [options] - compiler options
+ * @returns {{ code: string, map?: string, diagnostics: Array<{kind:string,message:string,span?:any}> }}
+ */
+export function compile(source, options = {}) {
+  const opts = { ...DEFAULT_COMPILE_OPTIONS, ...options };
+
+  // 1) Front-end: lex + parse
+  const lexer = createLexer(source);
+  const tokens = lexer.allTokens();
+  const parser = createParser(tokens);
+  const ast = parser.parse();
+
+  // 2) Normalization: AST → Core IR
+  const irProgram = normalizeAstToIr(ast, { mode: opts.mode });
+
+  // 3) Codegen: IR → Code IR (SKI tree or closure terms)
+  const codeIr = lowerIrToCodeIr(irProgram, { mode: opts.mode });
+
+  // 4) Emit: JS text (+ runtime prelude), UMD/ESM/CJS
+  const { code, map } = emitModule(codeIr, {
+    format: opts.format,
+    mode: opts.mode,
+    runtime: opts.runtime,
+    sourcemap: opts.sourcemap,
+    pretty: opts.pretty,
+    moduleName: opts.moduleName,
+  });
+
+  return { code, map, diagnostics: [] };
+}
+
+// =============================
+// IR definitions (documentation)
+// =============================
+
+/**
+ * Core IR after normalization (see COMPILER.md → Core IR).
+ * We represent terms as plain JS objects with a minimal tag.
+ *
+ * Term kinds:
+ * - Var:       { kind:'Var', name:string }
+ * - Lam:       { kind:'Lam', param:string, body:Term }
+ * - App:       { kind:'App', func:Term, arg:Term }
+ * - Const:     { kind:'Const', name:string }             // runtime primitive or top-level symbol
+ * - Lit:       { kind:'Lit', value:any }                 // numbers are {value,isFloat}, lists arrays, tables Map-wrapped objects
+ * - Cond:      { kind:'Cond', predicate:Term, ifTrue:Term, ifFalse:Term } // used for lowered when
+ *
+ * Program: { kind:'Program', decls:Array<Decl> }
+ * Decl: FunctionDecl | VarDecl
+ * - FunctionDecl: { kind:'FunctionDecl', name:string, arity:number, body:Term }
+ * - VarDecl:      { kind:'VarDecl', name:string, value:Term }
+ */
+
+/**
+ * Normalize parsed AST into Core IR.
+ * - Converts infix ops to `Const` calls (e.g., add/sub/...)
+ * - Curries multi-arg lambdas into nested `Lam`
+ * - Lowers member access to `get` primitive calls
+ * - Lowers `when` to `Cond` with thunked branches (thunks become `Lam(_).body` applied later)
+ * - Converts Ok/Err, lists, tables into `Lit`/`Const` per COMPILER.md
+ *
+ * @param {any} ast - AST from parser.js
+ * @param {{ mode: 'ski'|'closure'|'hybrid' }} ctx
+ * @returns {{ kind:'Program', decls:Array<any> }}
+ */
+export function normalizeAstToIr(ast, ctx) {
+  /**
+   * Transform a parser AST node into Core IR Term.
+   */
+  function lowerExpr(node) {
+    if (!node) return { kind: 'Lit', value: undefined };
+    switch (node.type) {
+      case 'NumberLiteral':
+        return { kind: 'Lit', value: { type: 'Number', value: node.value, isFloat: !!node.isFloat } };
+      case 'StringLiteral':
+        return { kind: 'Lit', value: { type: 'String', value: node.value } };
+      case 'BooleanLiteral':
+        return { kind: 'Lit', value: { type: 'Boolean', value: node.value } };
+      case 'Identifier': {
+        return { kind: 'Var', name: node.name };
+      }
+      case 'AnonymousFunction': {
+        // Desugar multi-arg anonymous function to nested Lam
+        const params = node.params.map(p => (typeof p === 'string' ? p : p.name || p.value));
+        let body = lowerExpr(node.body);
+        for (let i = params.length - 1; i >= 0; i--) {
+          body = { kind: 'Lam', param: params[i], body };
+        }
+        return body;
+      }
+      case 'FunctionCall': {
+        let func = lowerExpr(node.callee);
+        for (const arg of node.arguments) {
+          func = { kind: 'App', func, arg: lowerExpr(arg) };
+        }
+        return func;
+      }
+      case 'UnaryExpression': {
+        if (node.operator === '-') {
+          return { kind: 'App', func: { kind: 'Const', name: 'neg' }, arg: lowerExpr(node.operand) };
+        }
+        throw new Error(`Unsupported unary operator: ${node.operator}`);
+      }
+      case 'BinaryExpression': {
+        const opMap = {
+          '+': 'add',
+          '-': 'sub',
+          '*': 'mul',
+          '/': 'div',
+          '%': 'mod',
+          '..': 'concatDot',
+          '=': 'eq',
+          '!=': 'neq',
+          '>': 'gt',
+          '<': 'lt',
+          '>=': 'gte',
+          '<=': 'lte',
+          and: 'and',
+          or: 'or',
+          xor: 'xor',
+        };
+        const prim = opMap[node.operator];
+        if (!prim) throw new Error(`Unknown operator: ${node.operator}`);
+        return {
+          kind: 'App',
+          func: { kind: 'App', func: { kind: 'Const', name: prim }, arg: lowerExpr(node.left) },
+          arg: lowerExpr(node.right),
+        };
+      }
+      case 'MemberExpression': {
+        // Lower to get key obj: App(App(Const('get'), key), obj)
+        const keyNode = node.property;
+        let keyLit;
+        if (keyNode.type === 'Identifier') {
+          keyLit = { kind: 'Lit', value: { type: 'String', value: keyNode.name } };
+        } else if (keyNode.type === 'NumberLiteral') {
+          keyLit = { kind: 'Lit', value: { type: 'Number', value: keyNode.value, isFloat: !!keyNode.isFloat } };
+        } else if (keyNode.type === 'StringLiteral') {
+          keyLit = { kind: 'Lit', value: { type: 'String', value: keyNode.value } };
+        } else {
+          // Fallback: lower the property expression and hope it's literal-ish when emitted
+          keyLit = lowerExpr(keyNode);
+        }
+        const obj = lowerExpr(node.object);
+        return { kind: 'App', func: { kind: 'App', func: { kind: 'Const', name: 'get' }, arg: keyLit }, arg: obj };
+      }
+      case 'ListLiteral': {
+        const elements = node.elements.map(lowerExpr);
+        return { kind: 'Lit', value: { type: 'List', elements } };
+      }
+      case 'TableLiteral': {
+        const properties = node.properties.map(p => ({ key: p.key, value: lowerExpr(p.value) }));
+        return { kind: 'Lit', value: { type: 'Table', properties } };
+      }
+      case 'ResultExpression': {
+        return { kind: 'App', func: { kind: 'Const', name: node.variant }, arg: lowerExpr(node.value) };
+      }
+      case 'WhenExpression': {
+        const ds = node.discriminants.map(lowerExpr);
+        if (ds.length === 0) return { kind: 'Lit', value: undefined };
+        // Helpers
+        const True = { kind: 'Lit', value: { type: 'Boolean', value: true } };
+        const False = { kind: 'Lit', value: { type: 'Boolean', value: false } };
+        const mkAnd = (a, b) => ({ kind: 'App', func: { kind: 'App', func: { kind: 'Const', name: 'and' }, arg: a }, arg: b });
+        const mkEq = (a, b) => ({ kind: 'App', func: { kind: 'App', func: { kind: 'Const', name: 'eq' }, arg: a }, arg: b });
+        const mkNum = (n) => ({ kind: 'Lit', value: { type: 'Number', value: n, isFloat: false } });
+        const mkStr = (s) => ({ kind: 'Lit', value: { type: 'String', value: s } });
+        const mkGet = (keyTerm, objTerm) => ({ kind: 'App', func: { kind: 'App', func: { kind: 'Const', name: 'get' }, arg: keyTerm }, arg: objTerm });
+
+        function buildPredicateForPattern(pat, discTerm) {
+          if (!pat) return False;
+          if (pat.type === 'WildcardPattern') return True;
+          if (pat.type === 'NumberLiteral' || pat.type === 'StringLiteral' || pat.type === 'BooleanLiteral') {
+            return mkEq(discTerm, lowerExpr(pat));
+          }
+          if (pat.type === 'TypePattern') {
+            return { kind: 'App', func: { kind: 'App', func: { kind: 'Const', name: 'isType' }, arg: mkStr(pat.name) }, arg: discTerm };
+          }
+          if (pat.type === 'ResultPattern') {
+            return { kind: 'App', func: { kind: 'App', func: { kind: 'Const', name: 'isResultVariant' }, arg: mkStr(pat.variant) }, arg: discTerm };
+          }
+          if (pat.type === 'ListPattern') {
+            const n = pat.elements.length;
+            const preds = [];
+            preds.push(mkEq({ kind: 'App', func: { kind: 'Const', name: 'listLen' }, arg: discTerm }, mkNum(n)));
+            for (let j = 0; j < n; j++) {
+              const sub = pat.elements[j];
+              if (sub.type === 'WildcardPattern') continue;
+              const elem = mkGet(mkNum(j), discTerm);
+              preds.push(mkEq(elem, lowerExpr(sub)));
+            }
+            // Fold with lazy conjunction using Cond to avoid evaluating later predicates when earlier fail
+            if (preds.length === 0) return True;
+            let folded = preds[0];
+            for (let i = 1; i < preds.length; i++) {
+              folded = { kind: 'Cond', predicate: folded, ifTrue: preds[i], ifFalse: False };
+            }
+            return folded;
+          }
+          if (pat.type === 'TablePattern') {
+            const preds = [];
+            preds.push({ kind: 'App', func: { kind: 'App', func: { kind: 'Const', name: 'isType' }, arg: mkStr('Table') }, arg: discTerm });
+            for (const prop of pat.properties) {
+              const has = { kind: 'App', func: { kind: 'App', func: { kind: 'Const', name: 'has' }, arg: mkStr(prop.key) }, arg: discTerm };
+              preds.push(has);
+              if (prop.value.type !== 'WildcardPattern') {
+                const eq = mkEq(mkGet(mkStr(prop.key), discTerm), lowerExpr(prop.value));
+                preds.push(eq);
+              }
+            }
+            if (preds.length === 0) return True;
+            let folded = preds[0];
+            for (let i = 1; i < preds.length; i++) {
+              folded = { kind: 'Cond', predicate: folded, ifTrue: preds[i], ifFalse: False };
+            }
+            return folded;
+          }
+          // Fallback unsupported
+          return False;
+        }
+
+        let chain = { kind: 'Lit', value: undefined };
+        for (let i = node.cases.length - 1; i >= 0; i--) {
+          const c = node.cases[i];
+          if (!c.patterns || c.patterns.length === 0) continue;
+          // Build predicate across all discriminants, folded lazily
+          const preds = [];
+          const binders = [];
+          for (let k = 0; k < Math.min(c.patterns.length, ds.length); k++) {
+            const p = c.patterns[k];
+            preds.push(buildPredicateForPattern(p, ds[k]));
+            if (p.type === 'ResultPattern' && p.identifier && p.identifier.name) {
+              binders.push({ name: p.identifier.name, discIndex: k });
+            }
+          }
+          let pred = preds.length ? preds[0] : True;
+          for (let j = 1; j < preds.length; j++) {
+            pred = { kind: 'Cond', predicate: pred, ifTrue: preds[j], ifFalse: False };
+          }
+          // Build consequent, applying binders
+          let thenTerm = lowerExpr(c.consequent);
+          for (let b = binders.length - 1; b >= 0; b--) {
+            const valTerm = { kind: 'App', func: { kind: 'Const', name: 'resultValue' }, arg: ds[binders[b].discIndex] };
+            thenTerm = { kind: 'App', func: { kind: 'Lam', param: binders[b].name, body: thenTerm }, arg: valTerm };
+          }
+          chain = { kind: 'Cond', predicate: pred, ifTrue: thenTerm, ifFalse: chain };
+        }
+        return chain;
+      }
+      default:
+        throw new Error(`normalize: unsupported AST node type ${node.type}`);
+    }
+  }
+
+  function lowerFunctionLikeToLam(name, params, bodyNode) {
+    // Flatten curried/nested function bodies to a single final expression and parameter list
+    const flatParams = [];
+    const pushParam = (p) => flatParams.push(typeof p === 'string' ? p : (p && (p.name || p.value)));
+    params.forEach(pushParam);
+
+    let bodyExprAst = bodyNode;
+    // Unwrap nested function declaration bodies
+    while (bodyExprAst && (bodyExprAst.type === 'FunctionDeclarationBody' || bodyExprAst.type === 'CurriedFunctionBody')) {
+      if (Array.isArray(bodyExprAst.params)) bodyExprAst.params.forEach(pushParam);
+      bodyExprAst = bodyExprAst.body;
+    }
+    let term = lowerExpr(bodyExprAst);
+    for (let i = flatParams.length - 1; i >= 0; i--) {
+      term = { kind: 'Lam', param: flatParams[i], body: term };
+    }
+    return term;
+  }
+
+  /** Build Program decls preserving order (functions declared first for recursion). */
+  const funcDecls = [];
+  const otherDecls = [];
+  for (const stmt of ast.body || []) {
+    if (stmt.type === 'TypeDeclaration') {
+      continue;
+    }
+    if (stmt.type === 'FunctionDeclaration') {
+      const lam = lowerFunctionLikeToLam(stmt.name, stmt.params || [], stmt.body);
+      funcDecls.push({ kind: 'FunctionDecl', name: stmt.name, arity: (stmt.params || []).length, body: lam });
+      continue;
+    }
+    if (stmt.type === 'CurriedFunctionDeclaration') {
+      const lam = lowerFunctionLikeToLam(stmt.name, [stmt.param], stmt.body);
+      funcDecls.push({ kind: 'FunctionDecl', name: stmt.name, arity: 1, body: lam });
+      continue;
+    }
+    if (stmt.type === 'VariableDeclaration') {
+      otherDecls.push({ kind: 'VarDecl', name: stmt.name, value: lowerExpr(stmt.value) });
+      continue;
+    }
+    // Expression statement
+    otherDecls.push({ kind: 'ExprStmt', expr: lowerExpr(stmt) });
+  }
+  return { kind: 'Program', decls: [...funcDecls, ...otherDecls] };
+}
+
+/**
+ * Lower Core IR to Code IR depending on mode.
+ * - ski: apply bracket abstraction to produce SKI combinator trees
+ * - closure: keep `Lam` and emit closures later
+ * - hybrid: pick strategy per node heuristics (e.g., closure for complex Cond/when)
+ *
+ * @param {{ kind:'Program', decls:Array<any> }} irProgram
+ * @param {{ mode: 'ski'|'closure'|'hybrid' }} ctx
+ * @returns {{ kind:'Program', decls:Array<any> }}
+ */
+export function lowerIrToCodeIr(irProgram, ctx) {
+  switch (ctx.mode) {
+    case 'ski':
+      return applyBracketAbstraction(irProgram);
+    case 'closure':
+      return irProgram; // closures are emitted directly
+    case 'hybrid':
+      return applyHybridLowering(irProgram);
+    default:
+      throw new Error(`Unknown mode: ${ctx.mode}`);
+  }
+}
+
+/**
+ * Apply standard bracket abstraction to remove all Lam nodes.
+ * See COMPILER.md → Bracket Abstraction. Introduce I, K, S as Const terminals.
+ *
+ * @param {{ kind:'Program', decls:Array<any> }} irProgram
+ * @returns {{ kind:'Program', decls:Array<any> }}
+ */
+export function applyBracketAbstraction(irProgram) {
+  // TODO: Walk decl bodies and transform Lam/App/Var terms into SKI trees.
+  // Use a helper: abstract(varName, term) → term' applying rules:
+  //  - [x]x = I
+  //  - [x]M (x ∉ FV(M)) = K M
+  //  - [x](M N) = S ([x]M) ([x]N)
+  //  Multi-arg lambdas handled by nesting.
+  return irProgram;
+}
+
+/**
+ * Hybrid lowering placeholder. Use closures for complex cases, SKI for simple lambdas.
+ * @param {{ kind:'Program', decls:Array<any> }} irProgram
+ * @returns {{ kind:'Program', decls:Array<any> }}
+ */
+export function applyHybridLowering(irProgram) {
+  // TODO: Implement heuristics, e.g.,
+  // - If body contains Cond/When or deep nested applications, keep closure
+  // - Else apply abstraction elimination
+  return irProgram;
+}
+
+/**
+ * Emit a full JS module given Code IR and options.
+ * Responsible for:
+ * - Injecting runtime prelude (inline) or linking external
+ * - Emitting declarations (let-first for functions → assignment), variables
+ * - Wrapping in UMD/ESM/CJS based on options
+ * - Pretty-print toggles
+ * - Source map generation (stubbed)
+ *
+ * @param {{ kind:'Program', decls:Array<any> }} program
+ * @param {{ format:'umd'|'esm'|'cjs', mode:'ski'|'closure'|'hybrid', runtime:'inline'|{externalPath:string}, sourcemap:'none'|'inline'|'file', pretty:boolean, moduleName:string }} options
+ * @returns {{ code:string, map?:string }}
+ */
+export function emitModule(program, options) {
+  const prelude = options.runtime === 'inline' ? generateRuntimePrelude(options) : '';
+  const body = emitProgramBody(program, options);
+  const wrapped = wrapModule(prelude + '\n' + body, options);
+  // TODO: Generate real source map when implemented
+  return { code: wrapped };
+}
+
+/**
+ * Emit declarations body (no wrapper). This function should:
+ * - collect function names; emit `let` declarations
+ * - emit function assignments from Code IR
+ * - emit variables as `const`
+ * - attach exports per module format in wrapper
+ *
+ * @param {{ kind:'Program', decls:Array<any> }} program
+ * @param {{ mode:'ski'|'closure'|'hybrid' }} options
+ * @returns {string}
+ */
+export function emitProgramBody(program, options) {
+  // Closure-mode only for now.
+  function emitTerm(term) {
+    switch (term.kind) {
+      case 'Var': {
+        const rtVars = new Set(['io','str','math','map','filter','reduce','append','prepend','concat','update','removeAt','slice','set','remove','merge','keys','values','length']);
+        if (rtVars.has(term.name)) return `__rt.${term.name}`;
+        return term.name;
+      }
+      case 'Const':
+        return `__rt.${term.name}`;
+      case 'Lam':
+        return `(${sanitizeParam(term.param)})=>${emitTerm(term.body)}`;
+      case 'App': {
+        const { callee, args } = flattenApp(term);
+        // Detect get 'out' io pattern: callee is Const('get'), args[0]=lit 'out', args[1]=Var('io')
+        if (
+          callee && callee.kind === 'Const' && callee.name === 'get' &&
+          args.length >= 2 && args[0] && args[0].kind === 'Lit' && args[0].value && args[0].value.type === 'String' && args[0].value.value === 'out' &&
+          args[1] && args[1].kind === 'Var' && args[1].name === 'io'
+        ) {
+          const rest = args.slice(2).map(emitTerm).join(', ');
+          return rest.length ? `__rt.io.out(${rest})` : `__rt.io.out()`;
+        }
+        // Default: rebuild left-associative applications
+        let expr = emitTerm(callee);
+        for (const a of args) expr = `(${expr})(${emitTerm(a)})`;
+        return expr;
+      }
+      case 'Lit':
+        return emitLiteral(term.value);
+      case 'Cond':
+        return `__rt.cond(${emitTerm(term.predicate)})(()=>${emitTerm(term.ifTrue)})(() => ${emitTerm(term.ifFalse)})`;
+      default:
+        throw new Error(`emit: unknown term kind ${term.kind}`);
+    }
+  }
+
+  function flattenApp(term) {
+    const args = [];
+    let callee = term;
+    while (callee && callee.kind === 'App') {
+      args.unshift(callee.arg);
+      callee = callee.func;
+    }
+    return { callee, args };
+  }
+
+  function isGetOfIoProp(term, propName) {
+    // Match: App(App(Const('get'), keyLit), obj)
+    if (!term || term.kind !== 'App') return false;
+    const inner = term.func;
+    if (!inner || inner.kind !== 'App') return false;
+    if (!inner.func || inner.func.kind !== 'Const' || inner.func.name !== 'get') return false;
+    const key = inner.arg;
+    const obj = term.arg;
+    if (!key || key.kind !== 'Lit' || !key.value || key.value.type !== 'String') return false;
+    if (key.value.value !== propName) return false;
+    if (!obj || obj.kind !== 'Var' || obj.name !== 'io') return false;
+    return true;
+  }
+
+  function sanitizeParam(p) {
+    if (!p || typeof p !== 'string') return 'x';
+    if (p === '_') return '_';
+    return p;
+  }
+
+  function emitLiteral(lit) {
+    if (!lit || typeof lit !== 'object' || !lit.type) return 'undefined';
+    switch (lit.type) {
+      case 'Number':
+        return `__rt.num(${JSON.stringify(lit.value)}, ${lit.isFloat ? 'true' : 'false'})`;
+      case 'String':
+        return JSON.stringify(lit.value);
+      case 'Boolean':
+        return lit.value ? 'true' : 'false';
+      case 'List':
+        return `[${lit.elements.map(emitTerm).join(', ')}]`;
+      case 'Table': {
+        const props = lit.properties.map(p => `${JSON.stringify(p.key)}: ${emitTerm(p.value)}`).join(', ');
+        return `__rt.table({ ${props} })`;
+      }
+      default:
+        return 'undefined';
+    }
+  }
+
+  const lines = [];
+  lines.push(`export function run(host){`);
+  lines.push(`  __rt.installHost(host || {});`);
+  lines.push(`  let __rt_last;`);
+  // Predeclare functions for recursion
+  const userFuncDecls = program.decls.filter(d => d.kind === 'FunctionDecl');
+  const funcNameSet = new Set();
+  for (const d of userFuncDecls) funcNameSet.add(d.name);
+  const funcNames = Array.from(funcNameSet);
+  if (funcNames.length) {
+    lines.push(`  let ${funcNames.join(', ')};`);
+  }
+  for (const decl of userFuncDecls) {
+    lines.push(`  ${decl.name} = ${emitTerm(decl.body)};`);
+  }
+  let lastResultVar = '__rt_last';
+  for (const decl of program.decls) {
+    if (decl.kind === 'VarDecl') {
+      lines.push(`  const ${decl.name} = ${emitTerm(decl.value)};`);
+      lines.push(`  ${lastResultVar} = ${decl.name};`);
+    } else if (decl.kind === 'ExprStmt') {
+      lines.push(`  ${lastResultVar} = ${emitTerm(decl.expr)};`);
+    }
+  }
+  lines.push(`  return ${lastResultVar};`);
+  lines.push(`}`);
+  return lines.join('\n');
+}
+
+/**
+ * Generate the inline runtime prelude as text based on options and mode.
+ * MUST implement:
+ * - combinators I, K, S (and optionally B/C/Z later)
+ * - curry helpers (curry2, etc.)
+ * - numeric wrapper aware primitives: add, sub, mul, div, mod, neg, eq, ...
+ * - get, Ok, Err, cond; list/table ops; namespaces io/str/math (host-initialized)
+ * See COMPILER.md → Runtime Design.
+ *
+ * @param {{ mode:'ski'|'closure'|'hybrid' }} options
+ * @returns {string}
+ */
+export function generateRuntimePrelude(options) {
+  const prelude = `// Runtime prelude (closure mode minimal)\n` +
+`const __rt = { };\n` +
+`__rt.I = (a)=>a;\n` +
+`__rt.K = (a)=>(b)=>a;\n` +
+`__rt.S = (f)=>(g)=>(x)=>f(x)(g(x));\n` +
+`__rt.curry2 = (f)=>(a)=>(b)=>f(a,b);\n` +
+`__rt.curry3 = (f)=>(a)=>(b)=>(c)=>f(a,b,c);\n` +
+`__rt.num = (value, isFloat)=>({ value, isFloat: !!isFloat });\n` +
+`__rt.numValue = (x)=> (x && typeof x.value === 'number') ? x.value : Number(x);\n` +
+`__rt.isFloatLike = (x)=> (x && typeof x.value === 'number') ? !!x.isFloat : !Number.isInteger(Number(x));\n` +
+`__rt.bool = (x)=> !!(x && typeof x.value === 'number' ? x.value : x);\n` +
+`__rt.table = (plain)=>{ const m = new Map(); for (const k of Object.keys(plain||{})) m.set(k, plain[k]); return { type:'Object', properties: m }; };\n` +
+`__rt.add = __rt.curry2((a,b)=>{ const av=__rt.numValue(a), bv=__rt.numValue(b); const isF = __rt.isFloatLike(a)||__rt.isFloatLike(b); return __rt.num(av+bv, isF); });\n` +
+`__rt.sub = __rt.curry2((a,b)=>{ const av=__rt.numValue(a), bv=__rt.numValue(b); const isF = __rt.isFloatLike(a)||__rt.isFloatLike(b); return __rt.num(av-bv, isF); });\n` +
+`__rt.mul = __rt.curry2((a,b)=>{ const av=__rt.numValue(a), bv=__rt.numValue(b); const isF = __rt.isFloatLike(a)||__rt.isFloatLike(b); return __rt.num(av*bv, isF); });\n` +
+`__rt.div = __rt.curry2((a,b)=>{ const av=__rt.numValue(a), bv=__rt.numValue(b); if (bv===0) throw new Error('Division by zero'); return __rt.num(av/bv, true); });\n` +
+`__rt.mod = __rt.curry2((a,b)=>{ const av=__rt.numValue(a), bv=__rt.numValue(b); return __rt.num(av%bv, __rt.isFloatLike(a)||__rt.isFloatLike(b)); });\n` +
+`__rt.neg = (x)=>{ const v=__rt.numValue(x); return __rt.num(-v, __rt.isFloatLike(x)); };\n` +
+`__rt.eq = __rt.curry2((a,b)=>{ const an=a&&typeof a.value==='number', bn=b&&typeof b.value==='number'; if (an||bn) return __rt.numValue(a)===__rt.numValue(b); return a===b; });\n` +
+`__rt.neq = __rt.curry2((a,b)=>!__rt.eq(a)(b));\n` +
+`__rt.gt = __rt.curry2((a,b)=>__rt.numValue(a)>__rt.numValue(b));\n` +
+`__rt.lt = __rt.curry2((a,b)=>__rt.numValue(a)<__rt.numValue(b));\n` +
+`__rt.gte = __rt.curry2((a,b)=>__rt.numValue(a)>=__rt.numValue(b));\n` +
+`__rt.lte = __rt.curry2((a,b)=>__rt.numValue(a)<=__rt.numValue(b));\n` +
+`__rt.and = __rt.curry2((a,b)=>!!a && !!b);\n` +
+`__rt.or  = __rt.curry2((a,b)=>!!a || !!b);\n` +
+`__rt.xor = __rt.curry2((a,b)=>!!a !== !!b);\n` +
+`__rt.concatDot = __rt.curry2((a,b)=> String(a) + String(b));\n` +
+`__rt.Ok  = (v)=>({ type:'Result', variant:'Ok', value:v });\n` +
+`__rt.Err = (v)=>({ type:'Result', variant:'Err', value:v });\n` +
+`__rt.get = __rt.curry2((key,obj)=>{ const k = (key && typeof key.value==='number') ? key.value : (key && key.type==='Number'?key.value : (key && key.type==='String'?key.value : key)); if (obj==null) return null; if (Array.isArray(obj) && typeof k==='number') { if (k<0||k>=obj.length) return undefined; return obj[k]; } if (obj && obj.type==='Object' && obj.properties instanceof Map) { if (!obj.properties.has(String(k))) return undefined; return obj.properties.get(String(k)); } if (typeof obj==='object' && obj!==null && Object.prototype.hasOwnProperty.call(obj, k)) { return obj[k]; } return undefined; });\n` +
+`__rt.cond = (p)=>(t)=>(e)=> (p ? t() : e());\n` +
+`__rt.io = { out: (...xs)=>{ /* replaced in installHost */ }, in: ()=>'' };\n` +
+`__rt.str = { }; __rt.math = { };\n` +
+`__rt.installHost = (host)=>{ const hio = (host&&host.io)||{}; __rt.io = { out: (...xs)=>{ if (typeof hio.out==='function') { hio.out(...xs); } }, in: ()=>{ return typeof hio.in==='function' ? hio.in() : ''; } }; return __rt; };\n`;
+  // Add higher-order list ops and string namespace
+  const lib = `\n` +
+`__rt.map = __rt.curry2((fn, list)=>{ if (!Array.isArray(list)) throw new Error('map expects list'); return list.map(x=> fn(x)); });\n` +
+`__rt.filter = __rt.curry2((fn, list)=>{ if (!Array.isArray(list)) throw new Error('filter expects list'); return list.filter(x=> fn(x)); });\n` +
+`__rt.reduce = __rt.curry3((fn, acc, list)=>{ if (!Array.isArray(list)) throw new Error('reduce expects list'); let a = acc; for (const item of list) { a = fn(a)(item); } return a; });\n` +
+`__rt.length = (arg)=> { if (Array.isArray(arg)) return __rt.num(arg.length,false); if (typeof arg==='string') return __rt.num(arg.length,false); throw new Error('length expects a list or string'); };\n` +
+`__rt.append = __rt.curry2((list, element)=>{ if (!Array.isArray(list)) throw new Error('append expects list'); return [...list, element]; });\n` +
+`__rt.prepend = __rt.curry2((element, list)=>{ if (!Array.isArray(list)) throw new Error('prepend expects list'); return [element, ...list]; });\n` +
+`__rt.concat = __rt.curry2((list1, list2)=>{ if (!Array.isArray(list1) || !Array.isArray(list2)) throw new Error('concat expects lists'); return [...list1, ...list2]; });\n` +
+`__rt.update = __rt.curry3((list, index, value)=>{ if (!Array.isArray(list)) throw new Error('update expects list'); const i = (index && typeof index.value==='number') ? index.value : Number(index); if (!Number.isInteger(i) || i < 0 || i >= list.length) throw new Error('Index out of bounds: '+i); const out = [...list]; out[i] = value; return out; });\n` +
+`__rt.removeAt = __rt.curry2((list, index)=>{ if (!Array.isArray(list)) throw new Error('removeAt expects list'); const i = (index && typeof index.value==='number') ? index.value : Number(index); if (!Number.isInteger(i) || i < 0 || i >= list.length) throw new Error('Index out of bounds: '+i); return list.filter((_, idx)=> idx !== i); });\n` +
+`__rt.slice = __rt.curry3((list, start, end)=>{ if (!Array.isArray(list)) throw new Error('slice expects list'); const s = (start && typeof start.value==='number') ? start.value : Number(start); const e = (end==null) ? list.length : ((end && typeof end.value==='number') ? end.value : Number(end)); if (!Number.isInteger(s) || s < 0) throw new Error('Invalid start index: '+s); if (!Number.isInteger(e) || e < s || e > list.length) throw new Error('Invalid end index: '+e); return list.slice(s, e); });\n` +
+`__rt.set = __rt.curry3((table, key, value)=>{ if (!table || table.type!=='Object' || !(table.properties instanceof Map)) throw new Error('set expects a table'); const m = new Map(table.properties); const k = String(key && key.value ? key.value : key); m.set(k, value); return { type:'Object', properties: m }; });\n` +
+`__rt.remove = __rt.curry2((table, key)=>{ if (!table || table.type!=='Object' || !(table.properties instanceof Map)) throw new Error('remove expects a table'); const m = new Map(table.properties); const k = String(key && key.value ? key.value : key); m.delete(k); return { type:'Object', properties: m }; });\n` +
+`__rt.merge = __rt.curry2((table1, table2)=>{ if (!table1 || table1.type!=='Object' || !(table1.properties instanceof Map)) throw new Error('merge expects tables'); if (!table2 || table2.type!=='Object' || !(table2.properties instanceof Map)) throw new Error('merge expects tables'); const m = new Map(table1.properties); for (const [k,v] of table2.properties.entries()) m.set(k,v); return { type:'Object', properties: m }; });\n` +
+`__rt.keys = (table)=>{ if (!table || table.type!=='Object' || !(table.properties instanceof Map)) throw new Error('keys expects a table'); return Array.from(table.properties.keys()); };\n` +
+`__rt.values = (table)=>{ if (!table || table.type!=='Object' || !(table.properties instanceof Map)) throw new Error('values expects a table'); return Array.from(table.properties.values()); };\n` +
+`__rt.str.concat = __rt.curry2((a,b)=> String(a)+String(b));\n` +
+`__rt.str.split = __rt.curry2((s,delim)=> String(s).split(String(delim)));\n` +
+`__rt.str.join = __rt.curry2((arr,delim)=> { if (!Array.isArray(arr)) throw new Error('str.join expects array'); return arr.map(x=>String(x)).join(String(delim)); });\n` +
+`__rt.str.length = (s)=> __rt.num(String(s).length, false);\n` +
+`__rt.str.substring = __rt.curry3((s,start,end)=> String(s).substring(__rt.numValue(start), end==null? undefined : __rt.numValue(end)));\n` +
+`__rt.str.replace = __rt.curry3((s,search,repl)=> String(s).replace(new RegExp(String(search),'g'), String(repl)));\n` +
+`__rt.str.trim = (s)=> String(s).trim();\n` +
+`__rt.str.upper = (s)=> String(s).toUpperCase();\n` +
+`__rt.str.lower = (s)=> String(s).toLowerCase();\n`;
+  // Mark selected runtime functions to resemble interpreter NativeFunction shape during io.out
+  const nativeMarks = `\n` +
+`try { __rt.str.concat.type='NativeFunction'; } catch{}\n` +
+`try { __rt.str.split.type='NativeFunction'; } catch{}\n` +
+`try { __rt.str.join.type='NativeFunction'; } catch{}\n` +
+`try { __rt.str.length.type='NativeFunction'; } catch{}\n` +
+`try { __rt.str.substring.type='NativeFunction'; } catch{}\n` +
+`try { __rt.str.replace.type='NativeFunction'; } catch{}\n` +
+`try { __rt.str.trim.type='NativeFunction'; } catch{}\n` +
+`try { __rt.str.upper.type='NativeFunction'; } catch{}\n` +
+`try { __rt.str.lower.type='NativeFunction'; } catch{}\n` +
+`try { __rt.length.type='NativeFunction'; } catch{}\n`;
+  const match = `\n` +
+`__rt.isType = __rt.curry2((expected, value)=>{ const exp = String(expected); if (exp==='Int') return !!(value && typeof value.value==='number' && !value.isFloat); if (exp==='Float') return !!(value && typeof value.value==='number'); if (exp==='Number') return !!(value && typeof value.value==='number'); if (exp==='String') return typeof value === 'string'; if (exp==='Bool' || exp==='Boolean') return typeof value === 'boolean'; if (exp==='List') return Array.isArray(value); if (exp==='Table') return !!(value && value.type==='Object' && value.properties instanceof Map); if (exp==='Result') return !!(value && value.type==='Result'); return false; });\n` +
+`__rt.isResultVariant = __rt.curry2((variant, v)=> !!(v && v.type==='Result' && v.variant===String(variant)));\n` +
+`__rt.resultValue = (v)=> v && v.type==='Result' ? v.value : undefined;\n` +
+`__rt.listLen = (xs)=> Array.isArray(xs) ? __rt.num(xs.length, false) : __rt.num(0, false);\n` +
+`__rt.has = __rt.curry2((key, obj)=> { const k = String(key && key.value ? key.value : key); if (obj && obj.type==='Object' && obj.properties instanceof Map) { return obj.properties.has(k); } if (obj && typeof obj==='object') { return Object.prototype.hasOwnProperty.call(obj, k); } return false; });\n`;
+  const math = `\n` +
+`__rt.math = { };\n` +
+`__rt.math.abs = (x)=> __rt.num(Math.abs(__rt.numValue(x)), true);\n` +
+`__rt.math.sign = (x)=> __rt.num(Math.sign(__rt.numValue(x)), true);\n` +
+`__rt.math.floor = (x)=> __rt.num(Math.floor(__rt.numValue(x)), true);\n` +
+`__rt.math.ceil = (x)=> __rt.num(Math.ceil(__rt.numValue(x)), true);\n` +
+`__rt.math.round = (x)=> __rt.num(Math.round(__rt.numValue(x)), true);\n` +
+`__rt.math.trunc = (x)=> __rt.num(Math.trunc(__rt.numValue(x)), true);\n` +
+`__rt.math.min = __rt.curry2((a,b)=> __rt.num(Math.min(__rt.numValue(a), __rt.numValue(b)), true));\n` +
+`__rt.math.max = __rt.curry2((a,b)=> __rt.num(Math.max(__rt.numValue(a), __rt.numValue(b)), true));\n` +
+`__rt.math.clamp = __rt.curry3((x, lo, hi)=> { const xv=__rt.numValue(x), lv=__rt.numValue(lo), hv=__rt.numValue(hi); return __rt.num(Math.min(Math.max(xv, lv), hv), true); });\n` +
+`__rt.math.pow = __rt.curry2((x,y)=> __rt.num(Math.pow(__rt.numValue(x), __rt.numValue(y)), true));\n` +
+`__rt.math.sqrt = (x)=> { const v=__rt.numValue(x); if (v < 0) throw new Error('Domain error: sqrt expects x >= 0'); return __rt.num(Math.sqrt(v), true); };\n` +
+`__rt.math.exp = (x)=> __rt.num(Math.exp(__rt.numValue(x)), true);\n` +
+`__rt.math.log = (x)=> { const v=__rt.numValue(x); if (v <= 0) throw new Error('Domain error: log expects x > 0'); return __rt.num(Math.log(v), true); };\n` +
+`__rt.math.sin = (x)=> __rt.num(Math.sin(__rt.numValue(x)), true);\n` +
+`__rt.math.cos = (x)=> __rt.num(Math.cos(__rt.numValue(x)), true);\n` +
+`__rt.math.tan = (x)=> __rt.num(Math.tan(__rt.numValue(x)), true);\n` +
+`__rt.math.asin = (x)=> __rt.num(Math.asin(__rt.numValue(x)), true);\n` +
+`__rt.math.acos = (x)=> __rt.num(Math.acos(__rt.numValue(x)), true);\n` +
+`__rt.math.atan = (x)=> __rt.num(Math.atan(__rt.numValue(x)), true);\n` +
+`__rt.math.atan2 = __rt.curry2((y,x)=> __rt.num(Math.atan2(__rt.numValue(y), __rt.numValue(x)), true));\n` +
+`__rt.math.deg = (r)=> __rt.num(__rt.numValue(r) * (180 / Math.PI), true);\n` +
+`__rt.math.rad = (d)=> __rt.num(__rt.numValue(d) * (Math.PI / 180), true);\n` +
+`__rt.math.random = ()=> __rt.num(Math.random(), true);\n` +
+`__rt.math.randomInt = __rt.curry2((lo, hi)=> { const a = ~~(__rt.numValue(lo)); const b = ~~(__rt.numValue(hi)); if (a > b) throw new Error('Invalid range: lo > hi'); const n = a + Math.floor(Math.random() * (b - a + 1)); return __rt.num(n, false); });\n`;
+  return prelude + match + lib + nativeMarks + math;
+}
+
+/**
+ * Wrap concatenated prelude+body into selected module format (UMD/ESM/CJS).
+ * The wrapper should export named user bindings. For now, return identity.
+ *
+ * @param {string} content
+ * @param {{ format:'umd'|'esm'|'cjs', moduleName:string }} options
+ * @returns {string}
+ */
+export function wrapModule(content, options) {
+  // TODO: Implement proper wrappers. Keep this trivial to enable early testing.
+  if (options.format === 'esm') return content;
+  if (options.format === 'cjs') return content;
+  // UMD default
+  return content;
+}
+
+// =============================
+// Dev-friendly CLI (optional)
+// =============================
+
+/**
+ * Minimal CLI for direct compiler invocation.
+ * Prefer integrating flags into runner.js as the canonical CLI.
+ */
+if (typeof process !== 'undefined' && process.argv && process.argv[1] && process.argv[1].endsWith('compiler.js')) {
+  const fs = await import('fs');
+  const path = await import('path');
+
+  const args = process.argv.slice(2);
+  const inIdx = args.indexOf('--in');
+  const outIdx = args.indexOf('-o') >= 0 ? args.indexOf('-o') : args.indexOf('--out');
+  const formatIdx = args.indexOf('--format');
+  const modeIdx = args.indexOf('--mode');
+
+  if (inIdx === -1 || !args[inIdx + 1]) {
+    console.error('Usage: node compiler.js --in <input.baba> [-o out.js] [--format esm|cjs|umd] [--mode ski|closure|hybrid]');
+    process.exit(1);
+  }
+
+  const inputPath = path.resolve(process.cwd(), args[inIdx + 1]);
+  const outPath = outIdx !== -1 && args[outIdx + 1] ? path.resolve(process.cwd(), args[outIdx + 1]) : null;
+  const format = formatIdx !== -1 && args[formatIdx + 1] ? args[formatIdx + 1] : undefined;
+  const mode = modeIdx !== -1 && args[modeIdx + 1] ? args[modeIdx + 1] : undefined;
+
+  const source = fs.readFileSync(inputPath, 'utf8');
+  const { code } = compile(source, { format, mode });
+  if (outPath) {
+    fs.writeFileSync(outPath, code, 'utf8');
+  } else {
+    process.stdout.write(code);
+  }
+}
+
+
+