#!/usr/bin/env node import { readFileSync } from 'node:fs'; import { dirname, resolve as resolvePath } from 'node:path'; import { type CompiledFn, type CompiledPkg, CompileError, displayPath } from './compile.ts'; import { cells } from './diagram.ts'; import { fnById } from './compile.ts'; import { fmtBox } from './domain.ts'; import { short } from './hash.ts'; import { call, fmtValue, perform, RuntimeError, type Value } from './interp.ts'; import { getIR, pp } from './ir.ts'; import { SyntaxError_ } from './lexer.ts'; import { mstDiff } from './mst.ts'; import { parse } from './parser.ts'; import { Linker, loadRegistry } from './registry.ts'; import { resolve } from './resolve.ts'; const USAGE = `bhv — behavior-hashed functional language (POC) bhv check compile; list each function's id and cells (its signature) bhv cells [fn] show each function's cells: input region → expression bhv diff compare two versions function by function (MST diff) bhv run [input…] run main:input; one value, several (a tuple), or k=v pairs (a record) bhv resolve resolve diamond dependencies by behavior Dependencies are looked up in the file's directory (override with --registry ).`; function load(file: string, registryDir?: string): CompiledPkg { const mod = parse(readFileSync(file, 'utf8'), file); const reg = loadRegistry(registryDir ?? dirname(resolvePath(file))); return new Linker(reg).compileModule(mod); } /** Compile with diamonds resolved, so each dependency is linked at one version. */ function loadResolved(file: string, registryDir?: string): CompiledPkg { const mod = parse(readFileSync(file, 'utf8'), file); const reg = loadRegistry(registryDir ?? dirname(resolvePath(file))); const rep = resolve(mod, reg); const unresolved = rep.resolutions.filter((r) => !r.chosen).map((r) => r.dep); if (unresolved.length) throw new CompileError(`unresolvable diamond on ${unresolved.join(', ')} (run 'bhv resolve' for details)`); return new Linker(reg, rep.overrides).compileModule(mod); } const names = (f: CompiledFn) => (p: string) => f.names[p] ?? displayPath(p); const fnName = (id: string) => { const f = fnById.get(id); return f ? `${f.name}#${short(id).slice(0, 6)}` : '#' + short(id); }; const label = (k: string) => (k.startsWith('ext:') ? `extend ${k.slice(4)}` : k); /** A function's signature is its cells: input region → what it returns. */ function printCells(f: CompiledFn, indent: string) { for (const c of cells(f.root)) console.log(`${indent}${fmtBox(c.box, names(f)).padEnd(34)} → ${pp(getIR(c.label), names(f), fnName)}`); } function cmdCheck(pkg: CompiledPkg) { console.log(`${pkg.name}@${pkg.version}`); for (const [k, f] of pkg.fns) { console.log(` ${label(k)} id ${short(f.id)}${f.stages > 1 ? ` (${f.stages} stages)` : ''}${f.recursive ? ' recursive' : ''}`); printCells(f, ' '); } } function cmdCells(pkg: CompiledPkg, only?: string) { for (const [k, f] of pkg.fns) { if (only && f.name !== only && k !== only) continue; console.log(`${label(k)} id ${short(f.id)}`); printCells(f, ' '); } } function cmdDiff(a: CompiledPkg, b: CompiledPkg) { const all = new Set([...a.fns.keys(), ...b.fns.keys()]); for (const n of all) { const fa = a.fns.get(n), fb = b.fns.get(n); if (!fa) { console.log(`+ ${n} (new) ${short(fb!.id)}`); continue; } if (!fb) { console.log(`- ${n} (removed)`); continue; } if (fa.id === fb.id) { console.log(`= ${n} same behavior ${short(fa.id)}`); continue; } const d = mstDiff(fa.id, fb.id); console.log(`~ ${n} ${short(fa.id)} → ${short(fb.id)} (opened ${d.opened} of ${d.total} MST nodes)`); const show = (l: string, f: CompiledFn) => pp(getIR(l), names(f), fnName); for (const [k, v] of d.removed) console.log(` - ${k} → ${show(v, fa)}`); for (const [k, v] of d.added) console.log(` + ${k} → ${show(v, fb)}`); for (const [k, v1, v2] of d.changed) console.log(` ~ ${k}: ${show(v1, fa)} ⇒ ${show(v2, fb)}`); } } function parseInput(args: string[]): Value { if (!args.length) return { tag: '_', f: {} }; const one = (s: string): Value => (/^-?\d+$/.test(s) ? Number(s) : s); if (args.length === 1 && !args[0].includes('=')) return one(args[0]); if (args.every((a) => !a.includes('='))) return { tag: '_', f: Object.fromEntries(args.map((a, i) => [String(i), one(a)])) }; const f: Record = {}; for (const a of args) { const [k, ...v] = a.split('='); f[k] = one(v.join('=')); } return { tag: '_', f: Object.fromEntries(Object.entries(f).sort(([x], [y]) => (x < y ? -1 : 1))) }; } function cmdRun(pkg: CompiledPkg, args: string[]) { const main = pkg.fns.get('main'); if (!main) throw new CompileError(`${pkg.name} has no main`); const out = call(main, parseInput(args)); try { perform(out); } catch (e) { if (e instanceof RuntimeError) console.log(fmtValue(out)); else throw e; } } function cmdResolve(file: string, registryDir?: string) { const mod = parse(readFileSync(file, 'utf8'), file); const reg = loadRegistry(registryDir ?? dirname(resolvePath(file))); const rep = resolve(mod, reg); console.log('Dependency graph:'); for (const l of rep.tree) console.log(' ' + l); if (!rep.resolutions.length) { console.log('\nNo diamonds: every package uses a single version of each dependency.'); return; } for (const r of rep.resolutions) { console.log(`\nDiamond on '${r.dep}': ` + [...r.versions].map(([v, ps]) => `${v} (used by ${ps.join(', ')})`).join(', ')); console.log('\nWhat each dependent actually relies on:'); for (const q of r.requirements) { console.log(` ${q.from} → ${q.dep}.${q.fn} on ${q.boxes.map((b) => fmtBox(b, displayPath)).join(' or ')}`); console.log(` behavior there (from ${q.builtWith}): ${short(q.fingerprint)}`); } console.log('\nCandidates:'); for (const c of r.checks) { console.log(` ${r.dep}@${c.version}: ${c.ok ? 'satisfies every dependent' : 'no'}`); for (const f of c.failures) console.log(` ✗ ${f}`); } if (r.synthesized) { console.log(`\nNo published version fits; synthesizing ${r.dep}@${[...r.versions.keys()].join('+')}:`); for (const s of r.synthesized) { console.log(` ${s.fn} id ${short(s.id)} (defined only where someone calls it)`); for (const p of s.pieces) console.log(` ${p}`); } } for (const c of r.conflicts) console.log(`\n ✗ CONFLICT in ${r.dep}.${c.fn}: ${c.detail}`); if (!r.chosen) { console.log(`\nUnresolvable: dependents need different behavior for the same inputs.`); continue; } console.log(`\nChosen: ${r.dep}@${r.chosen.version}${r.chosen.synthetic ? ' (synthesized)' : ''}`); console.log('Verification (recompile each dependent against it; ids must not change):'); for (const v of r.verified) { const ok = v.fns.every((f) => f.before === f.after); console.log(` ${ok ? '✓' : '✗'} ${v.pkg}: ` + v.fns.map((f) => `${f.name} ${short(f.before)}${f.before === f.after ? '' : ' → ' + short(f.after)}`).join(', ')); } } } function main(argv: string[]) { const regIdx = argv.indexOf('--registry'); let registry: string | undefined; if (regIdx >= 0) { registry = argv[regIdx + 1]; argv.splice(regIdx, 2); } const [cmd, ...rest] = argv; try { switch (cmd) { case 'check': return cmdCheck(load(rest[0], registry)); case 'cells': return cmdCells(load(rest[0], registry), rest[1]); case 'diff': return cmdDiff(load(rest[0], registry), load(rest[1], registry)); case 'run': return cmdRun(loadResolved(rest[0], registry), rest.slice(1)); case 'resolve': return cmdResolve(rest[0], registry); default: console.log(USAGE); } } catch (e) { if (e instanceof CompileError || e instanceof SyntaxError_ || e instanceof RuntimeError) { console.error('error: ' + e.message); process.exitCode = 1; } else throw e; } } main(process.argv.slice(2));