import { withLog } from './debug'; let index = 0; export abstract class Term { protected id = index++; protected cache: Term | null = null; public constructor(protected bindings: Record) { } public refs(_name: string): boolean { return false; }; public bind(_name: string, _term: Term): Term { return this; } public reduce(): Term { return this; } public context(): string { return !Object.keys(this.bindings).length ? `<${this.id}>` : `<${this.id}>[bound ${Object.keys(this.bindings).join(', ')}] `; } public stringify(context?: boolean): string { return `${context ? this.context() : ''}${this.constructor.name}`; } public _(arg: RefTerm): Term { return c(this, arg); } } export class Undef extends Term { constructor( public from: string, ) { super({}); } override reduce(): Term { throw new Error(`${this.from} = undef`); } } export class Native extends Term { constructor( public value: any, ) { super({}); } public override stringify(): string { return `\`${JSON.stringify(this.value)}\``; } } export class Deferred extends Term { constructor( public func: (bindings: Record) => Term, bindings: Record = {}, ) { super(bindings); } override refs(_name: string): boolean { return true; } public override bind(name: string, term: Term): Term { return this.bindings[name] || !this.refs(name) ? this : new Deferred(this.func, { ...this.bindings, [name]: term }); } public override reduce(): Term { const term = this.cache ? withLog( `Executing ${this.stringify()}`, () => this.cache!, r => `got ${r.stringify()} (cached)`, ) : this.cache = withLog( `Executing ${this.stringify()}`, () => this.func({ ...globals, ...this.bindings }), r => `got ${r.stringify()}`, ); return Object.entries(this.bindings).reduce((t, b) => t.bind(...b), term).reduce(); } public override stringify(context: boolean = true): string { return `${context ? this.context() : ''}\`deferred\``; } } export class Ref extends Term { constructor( public name: string, bindings: Record = {}, ) { super(bindings); } override refs(name: string): boolean { return this.name === name; } public override bind(name: string, term: Term): Term { return this.bindings[name] || !this.refs(name) ? this : new Ref(this.name, { [name]: term }); } public override reduce(): Term { if (this.cache) { return withLog( `Substituting ${this.stringify()}`, () => this.cache!, r => `with ${r.stringify()} (cached)`, ); } else { return this.cache = withLog( `Substituting ${this.stringify()}`, () => (this.bindings[this.name] ?? globals[this.name] ?? new Undef(this.name)).reduce(), r => `with ${r.stringify()}`, ).reduce(); } } public override stringify(context = true): string { return `${context ? this.context() : ''}${this.name}`; } } export class Lambda extends Term { constructor( public param: string, public body: Term, bindings: Record = {}, ) { super(bindings); } override refs(name: string): boolean { return this.param !== name && this.body.refs(name); } override bind(name: string, term: Term): Term { return this.bindings[name] || !this.refs(name) ? this : new Lambda(this.param, this.body.bind(name, term), { ...this.bindings, [name]: term }); } public override stringify(context = true): string { return this.body instanceof Lambda ? `${context ? this.context() : ''}\\ ${this.param} ${this.body.stringify(false).slice(2)}` : `${context ? this.context() : ''}\\ ${this.param} . ${this.body.stringify(false)}`; } } export class Call extends Term { constructor( public func: Term, public arg: Term, bindings: Record = {}, ) { super(bindings); } override refs(name: string): boolean { return this.func.refs(name) || this.arg.refs(name); } override bind(name: string, term: Term): Term { return this.bindings[name] || !this.refs(name) ? this : new Call(this.func.bind(name, term), this.arg.bind(name, term), { ...this.bindings, [name]: term }); } override reduce(): Term { if (this.cache) { return withLog( `Reducing ${this.stringify()}`, () => this.cache!, r => `to ${r.stringify()} (cached)`, ); } else { return this.cache = withLog( `Reducing ${this.stringify()}`, () => { const func = this.func.reduce(); if (func instanceof Lambda) { return withLog( `Applying ${func.stringify()} to ${this.arg.stringify()}`, () => func.body.bind(func.param, this.arg), r => `got ${r.stringify()}`, ).reduce(); } return this; }, r => `to ${r.stringify()}`, ); } } public override stringify(context = true): string { return `${context ? this.context() : ''}${this.func instanceof Lambda ? `(${this.func.stringify(false)})` : this.func.stringify(false)} ${this.arg instanceof Ref || this.arg instanceof Native ? this.arg.stringify(false) : `(${this.arg.stringify(false)})`}`; } } const globals: Record = {}; type RefTerm = string | Term; const termify = (t: RefTerm) => typeof t === 'string' ? r(t) : t; export const _ = (v: any) => new Native(v); export const r = (name: string): Ref => new Ref(name); export const l = (param: string, body: RefTerm): Lambda => new Lambda(param, termify(body)); export const c = (func: RefTerm, arg: RefTerm, ...args: RefTerm[]): Call => args.reduce((f: Call, a: RefTerm) => new Call(f, termify(a)), new Call(termify(func), termify(arg))); export const d = (func: (bindings: Record) => Term) => new Deferred(func); export const $ = (t: Term): T => { const rt = t.reduce(); return rt instanceof Native ? rt.value : rt.stringify(); }; let cm: string = 'core'; export const m = (name: string) => cm = name; export const g = (name: string, term: RefTerm) => { name = `${cm}::${name}`; if (globals[name]) { throw new Error(`Duplicate global: "${name}"`); } globals[name] = termify(term); return new Ref(name); }; export const undef = g('undef', new Undef('undef')); export const id = g('id', l('f', 'f')); export const cnst = g('cnst', l('x', l('_', 'x'))); export const pipe = g('pipe', l('f', l('g', l('x', c('f', c('g', 'x'))))));