diff options
Diffstat (limited to 'js/baba-yaga/experimental/compiler.js')
-rw-r--r-- | js/baba-yaga/experimental/compiler.js | 728 |
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); + } +} + + + |