You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
236 lines
6.5 KiB
236 lines
6.5 KiB
import { withLog } from './debug';
|
|
|
|
let index = 0;
|
|
|
|
export abstract class Term {
|
|
protected id = index++;
|
|
protected cache: Term | null = null;
|
|
|
|
public constructor(protected bindings: Record<string, Term>) { }
|
|
|
|
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<string, Term>) => Term,
|
|
bindings: Record<string, Term> = {},
|
|
) { 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<string, Term> = {},
|
|
) { 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<string, Term> = {},
|
|
) { 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<string, Term> = {},
|
|
) { 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<string, Term> = {};
|
|
|
|
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<Call>((f: Call, a: RefTerm) => new Call(f, termify(a)), new Call(termify(func), termify(arg)));
|
|
export const d = (func: (bindings: Record<string, Term>) => Term) => new Deferred(func);
|
|
|
|
export const $ = <T>(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'))))));
|
|
|