465 lines
16 KiB
TypeScript
465 lines
16 KiB
TypeScript
import type { AST, Pattern, Definition, TypeDefinition, ClassDefinition, InstanceDeclaration } from './ast';
|
|
import { store } from './runtime-js';
|
|
import { typecheck } from './typechecker';
|
|
|
|
let matchCounter = 0;
|
|
|
|
type CompileCtx = {
|
|
useStore: boolean;
|
|
bound: Set<string>;
|
|
topLevel: Set<string>;
|
|
};
|
|
const defaultCtx: CompileCtx = { useStore: true, bound: new Set(), topLevel: new Set() };
|
|
|
|
export const definitions: Map<string, Definition> = new Map();
|
|
export const dependencies: Map<string, Set<string>> = new Map();
|
|
export const dependents: Map<string, Set<string>> = new Map();
|
|
export const astRegistry = new Map<number, AST>();
|
|
let astIdCounter = 0;
|
|
|
|
export function compile(ast: AST, ctx: CompileCtx = defaultCtx): string {
|
|
switch (ast.kind) {
|
|
case 'literal':
|
|
if (ast.value.kind === 'string')
|
|
return JSON.stringify(ast.value.value);
|
|
if (ast.value.kind === 'int' || ast.value.kind === 'float')
|
|
return JSON.stringify(ast.value.value);
|
|
throw new Error(`Cannot compile literal`); // of kind ${ast.value.kind}`);
|
|
|
|
case 'variable': {
|
|
if (ctx.bound.has(ast.name)) {
|
|
return sanitizeName(ast.name);
|
|
}
|
|
return sanitize(ast.name, ctx);
|
|
}
|
|
|
|
case 'lambda': {
|
|
const newBound = new Set([...ctx.bound, ...ast.params]);
|
|
const newCtx = { ...ctx, bound: newBound };
|
|
const params = ast.params.map(sanitizeName).join(') => (');
|
|
const id = astIdCounter++;
|
|
astRegistry.set(id, ast);
|
|
return `Object.assign((${params}) => ${compile(ast.body, newCtx)}, { _astId: (${id}) })`;
|
|
}
|
|
|
|
case 'apply':
|
|
// Collect constructor args from nested applie
|
|
let node: AST = ast;
|
|
const ctorArgs: AST[] = [];
|
|
while (node.kind === 'apply' && node.func.kind !== 'constructor') {
|
|
// Check if inner func is a constructor chain
|
|
if (node.func.kind === 'apply') {
|
|
ctorArgs.unshift(node.args[0]);
|
|
node = node.func;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Constructor
|
|
if (node.kind === 'apply' && node.func.kind === 'constructor') {
|
|
ctorArgs.unshift(node.args[0]);
|
|
const ctorName = node.func.name;
|
|
const compiledArgs = ctorArgs.map((a, i) => `_${i}: ${compile(a, ctx)}`).join(', ');
|
|
return `({ _tag: "${ctorName}", ${compiledArgs} })`;
|
|
}
|
|
|
|
// Function application
|
|
const args = ast.args.map(a => compile(a, ctx)).join(')(');
|
|
return `${compile(ast.func, ctx)}(${args})`;
|
|
|
|
case 'record': {
|
|
const fieldNames = new Set(
|
|
ast.entries.filter(e => e.kind === 'field').map(e => e.key)
|
|
);
|
|
|
|
// Check if any field references another
|
|
const needsHoist = ast.entries.some(entry =>
|
|
entry.kind === 'field' &&
|
|
[...freeVars(entry.value)].some(v =>
|
|
fieldNames.has(v) && v !== entry.key && !ctx.bound.has(v) && !ctx.topLevel.has(v)
|
|
)
|
|
);
|
|
|
|
if (needsHoist) {
|
|
const bindings: string[] = [];
|
|
const names: string[] = [];
|
|
for (const entry of ast.entries) {
|
|
if (entry.kind === 'spread') continue;
|
|
const safe = sanitizeName(entry.key);
|
|
bindings.push(`const ${safe} = ${compile(entry.value, { ...ctx, bound: new Set([...ctx.bound, ...fieldNames]) })};`);
|
|
names.push(`${JSON.stringify(entry.key)}: ${safe}`);
|
|
}
|
|
return `(() => { ${bindings.join(' ')} return {${names.join(', ')}}; })()`;
|
|
}
|
|
|
|
// Simple record, no hoisting
|
|
const parts = ast.entries.map(entry =>
|
|
entry.kind === 'spread'
|
|
? `...${compile(entry.expr, ctx)}`
|
|
: `${JSON.stringify(entry.key)}: ${compile(entry.value, ctx)}`
|
|
)
|
|
return `({${parts.join(', ')}})`;
|
|
}
|
|
|
|
case 'list': {
|
|
const elements = ast.elements.map(e =>
|
|
'spread' in e ? `...${compile(e.spread, ctx)}` : compile(e, ctx)
|
|
);
|
|
return `[${elements.join(', ')}]`;
|
|
}
|
|
|
|
case 'record-access':
|
|
return `${compile(ast.record, ctx)}[${JSON.stringify(ast.field)}]`;
|
|
|
|
case 'record-update':
|
|
const updates = Object.entries(ast.updates)
|
|
.map(([k, v]) => `${JSON.stringify(k)}: ${compile(v, ctx)}`);
|
|
return `({...${compile(ast.record, ctx)}, ${updates.join(', ')}})`;
|
|
|
|
case 'let': {
|
|
const newBound = new Set([...ctx.bound, ast.name]);
|
|
const newCtx = { ...ctx, bound: newBound };
|
|
return `(() => { let ${sanitizeName(ast.name)} = ${compile(ast.value, newCtx)};
|
|
return ${compile(ast.body, newCtx)}; })()`;
|
|
}
|
|
|
|
case 'match':
|
|
return compileMatch(ast, ctx);
|
|
|
|
case 'constructor':
|
|
return `({ _tag: "${ast.name}" })`;
|
|
|
|
case 'rebind': {
|
|
const rootName = getRootName(ast.target);
|
|
const path = getPath(ast.target);
|
|
const value = compile(ast.value, ctx);
|
|
|
|
if (!rootName) throw new Error('Rebind target must be a variable');
|
|
|
|
if (ctx.bound.has(rootName)) {
|
|
const target = compile(ast.target, ctx);
|
|
return `(() => { ${target} = ${value}; return { _tag: "NoOp" }; })()`;
|
|
}
|
|
|
|
if (path.length === 0) {
|
|
return `({ _tag: "Rebind", _0: "${rootName}", _1: ${value} })`;
|
|
} else {
|
|
return `({ _tag: "Rebind", _0: "${rootName}", _1: ${JSON.stringify(path)}, _2: ${value} })`;
|
|
}
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Cannot compile ${ast.kind}`);
|
|
|
|
}
|
|
}
|
|
|
|
function sanitize(name: string, { useStore, topLevel }: CompileCtx): string {
|
|
if (!useStore && topLevel.has(name)) return sanitizeName(name);
|
|
return `store[${JSON.stringify(name)}]`
|
|
}
|
|
|
|
function sanitizeName(name: string): string {
|
|
const reserved = [
|
|
'default','class','function','return','const','let','var',
|
|
'if','else','switch','case','for','while','do','break',
|
|
'continue','new','delete','typeof','in','this','super',
|
|
'import','export','extends','static','yield','await','async',
|
|
'try','catch','finally','throw','null','true','false',
|
|
'eval','Function','window','document','console'
|
|
];
|
|
if (reserved.includes(name)) return `_${name}`;
|
|
|
|
return name.replace(/-/g, '_');
|
|
}
|
|
|
|
function compileMatch(ast: AST & { kind: 'match'}, ctx: CompileCtx): string {
|
|
const expr = compile(ast.expr, ctx);
|
|
const tmpVar = `_m${matchCounter++}`;
|
|
|
|
let code = `((${tmpVar}) => { `;
|
|
|
|
for (const c of ast.cases) {
|
|
const { condition, bindings } = compilePattern(c.pattern, tmpVar);
|
|
const patternBound = patternVars(c.pattern);
|
|
const newBound = new Set([...ctx.bound, ...patternBound]);
|
|
const newCtx = { ...ctx, bound: newBound };
|
|
code += `if (${condition}) { `;
|
|
if (bindings.length > 0) {
|
|
code += `const ${bindings.join(', ')}; `;
|
|
}
|
|
code += `return ${compile(c.result, newCtx)}; }`;
|
|
}
|
|
|
|
code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`;
|
|
return code;
|
|
}
|
|
|
|
function compilePattern(pattern: Pattern, expr: string): { condition: string, bindings: string[] } {
|
|
switch (pattern.kind) {
|
|
case 'wildcard':
|
|
return { condition: 'true', bindings: [] };
|
|
|
|
case 'var':
|
|
return { condition: 'true', bindings: [`${sanitizeName(pattern.name)} = ${expr}`] };
|
|
|
|
case 'literal':
|
|
return { condition: `${expr} === ${JSON.stringify(pattern.value)}`, bindings: [] };
|
|
|
|
case 'constructor': {
|
|
let condition = `${expr}?._tag === "${pattern.name}"`;
|
|
const bindings: string[] = [];
|
|
pattern.args.forEach((argPattern, i) => {
|
|
const sub = compilePattern(argPattern, `${expr}._${i}`);
|
|
condition += ` && ${sub.condition}`;
|
|
bindings.push(...sub.bindings);
|
|
});
|
|
|
|
return { condition, bindings };
|
|
}
|
|
|
|
case 'list': {
|
|
let condition = `Array.isArray(${expr}) && ${expr}.length === ${pattern.elements.length}`;
|
|
const bindings: string[] = [];
|
|
pattern.elements.forEach((elemPattern, i) => {
|
|
const sub = compilePattern(elemPattern, `${expr}[${i}]`);
|
|
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
|
|
bindings.push(...sub.bindings);
|
|
});
|
|
|
|
return { condition, bindings };
|
|
}
|
|
|
|
case 'list-spread': {
|
|
let condition = `Array.isArray(${expr}) && ${expr}.length >= ${pattern.head.length}`;
|
|
const bindings: string[] = [];
|
|
pattern.head.forEach((elemPattern, i) => {
|
|
const sub = compilePattern(elemPattern, `${expr}[${i}]`);
|
|
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
|
|
bindings.push(...sub.bindings);
|
|
});
|
|
bindings.push(`${sanitizeName(pattern.spread)} = ${expr}.slice(${pattern.head.length})`);
|
|
|
|
return { condition, bindings };
|
|
}
|
|
|
|
case 'record': {
|
|
let condition = 'true';
|
|
const bindings: string[] = [];
|
|
for (const [field, fieldPattern] of Object.entries(pattern.fields)) {
|
|
const sub = compilePattern(fieldPattern, `${expr}[${JSON.stringify(field)}]`);
|
|
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
|
|
bindings.push(...sub.bindings);
|
|
}
|
|
|
|
return { condition, bindings };
|
|
}
|
|
|
|
default:
|
|
return { condition: 'true', bindings: [] };
|
|
}
|
|
}
|
|
|
|
export function compileAndRun(defs: Definition[], typeDefs: TypeDefinition[] = [], classDefs: ClassDefinition[] = [], instances: InstanceDeclaration[] = []) {
|
|
typecheck(defs, typeDefs, classDefs, instances);
|
|
|
|
const compiledDefs: string[] = [];
|
|
|
|
const topLevel = new Set(defs.filter(d => d.body).map(d => d.name));
|
|
|
|
for (const def of defs) {
|
|
if (!def.body) continue; // type declaration only
|
|
definitions.set(def.name, def);
|
|
const free = freeVars(def.body);
|
|
const deps = new Set([...free].filter(v => topLevel.has(v)));
|
|
dependencies.set(def.name, deps);
|
|
}
|
|
|
|
dependents.clear();
|
|
for (const name of topLevel) {
|
|
dependents.set(name, new Set());
|
|
}
|
|
|
|
for (const [name, deps] of dependencies) {
|
|
for (const dep of deps) {
|
|
dependents.get(dep)?.add(name);
|
|
}
|
|
}
|
|
|
|
for (const def of defs) {
|
|
if (!def.body) continue;
|
|
const ctx: CompileCtx = { useStore: false, topLevel, bound: new Set() };
|
|
const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, ctx)};`;
|
|
compiledDefs.push(compiled);
|
|
|
|
try {
|
|
new Function('store', compiled);
|
|
} catch (e) {
|
|
console.error(`=== BROKEN: ${def.name} ===`);
|
|
console.error(compiled);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
const defsWithBody = defs.filter(d => d.body);
|
|
const lastName = defs[defs.length - 1].name;
|
|
const defNames = defsWithBody.map(d => sanitizeName(d.name)).join(', ');
|
|
|
|
const code = `${compiledDefs.join('\n')}
|
|
return { ${defNames}, __result: ${sanitizeName(lastName)} };`;
|
|
|
|
const fn = new Function('store', code);
|
|
const allDefs = fn(store);
|
|
|
|
// Populate store
|
|
for (const [name, value] of Object.entries(allDefs)) {
|
|
if (name !== '__result') {
|
|
store[name] = value;
|
|
}
|
|
}
|
|
|
|
return allDefs.__result;
|
|
}
|
|
|
|
export function freeVars(ast: AST, bound: Set<string> = new Set()): Set<string> {
|
|
switch (ast.kind) {
|
|
case 'literal':
|
|
case 'constructor':
|
|
return new Set();
|
|
|
|
case 'variable':
|
|
return bound.has(ast.name) ? new Set() : new Set([ast.name]);
|
|
|
|
case 'lambda': {
|
|
const newBound = new Set([...bound, ...ast.params]);
|
|
return freeVars(ast.body, newBound);
|
|
}
|
|
|
|
case 'let': {
|
|
const valueVars = freeVars(ast.value, bound);
|
|
const bodyVars = freeVars(ast.body, new Set([...bound, ast.name]));
|
|
return new Set([...valueVars, ...bodyVars]);
|
|
}
|
|
|
|
case 'apply': {
|
|
const funcVars = freeVars(ast.func, bound);
|
|
const argVars = ast.args.flatMap(a => [...freeVars(a, bound)])
|
|
return new Set([...funcVars, ...argVars]);
|
|
}
|
|
|
|
case 'record': {
|
|
const allVars = ast.entries.flatMap(entry =>
|
|
entry.kind === 'spread'
|
|
? [...freeVars(entry.expr, bound)]
|
|
: [...freeVars(entry.value, bound)]
|
|
);
|
|
return new Set(allVars);
|
|
}
|
|
|
|
case 'list': {
|
|
const allVars = ast.elements.flatMap(e =>
|
|
'spread' in e ? [...freeVars(e.spread, bound)]
|
|
: [...freeVars(e, bound)]);
|
|
return new Set(allVars);
|
|
}
|
|
|
|
case 'record-access':
|
|
return freeVars(ast.record, bound);
|
|
|
|
case 'record-update': {
|
|
const recordVars = freeVars(ast.record, bound);
|
|
const updateVars = Object.values(ast.updates).flatMap(v => [...freeVars(v, bound)]);
|
|
return new Set([...recordVars, ...updateVars]);
|
|
}
|
|
|
|
case 'match': {
|
|
const exprVars = freeVars(ast.expr, bound);
|
|
const caseVars = ast.cases.flatMap(c => {
|
|
const patternBindings = patternVars(c.pattern);
|
|
const newBound = new Set([...bound, ...patternBindings]);
|
|
return [...freeVars(c.result, newBound)];
|
|
})
|
|
return new Set([...exprVars, ...caseVars]);
|
|
}
|
|
|
|
case 'rebind':
|
|
return freeVars(ast.value, bound);
|
|
|
|
default:
|
|
return new Set();
|
|
}
|
|
}
|
|
|
|
function patternVars(pattern: Pattern): string[] {
|
|
switch (pattern.kind) {
|
|
case 'var':
|
|
return [pattern.name];
|
|
case 'constructor':
|
|
return pattern.args.flatMap(patternVars);
|
|
case 'list':
|
|
return pattern.elements.flatMap(patternVars);
|
|
case 'list-spread':
|
|
return [...pattern.head.flatMap(patternVars), pattern.spread];
|
|
case 'record':
|
|
return Object.values(pattern.fields).flatMap(patternVars);
|
|
default:
|
|
return [];
|
|
}
|
|
|
|
}
|
|
|
|
export function recompile(name: string, newAst: AST) {
|
|
const existing = definitions.get(name);
|
|
definitions.set(name, { kind: 'definition', name, body: newAst, annotation: existing?.annotation });;
|
|
// definitions.set(name, newAst);
|
|
|
|
const topLevel = new Set(definitions.keys());
|
|
const free = freeVars(newAst);
|
|
const newDeps = new Set([...free].filter(v => topLevel.has(v)));
|
|
|
|
const oldDeps = dependencies.get(name) || new Set();
|
|
for (const dep of oldDeps) {
|
|
dependents.get(dep)?.delete(name);
|
|
}
|
|
|
|
for (const dep of newDeps) {
|
|
dependents.get(dep)?.add(name);
|
|
}
|
|
dependencies.set(name, newDeps);
|
|
|
|
const toRecompile: string[] = [];
|
|
const visited = new Set<string>();
|
|
|
|
function collectDependents(n: string) {
|
|
if (visited.has(n)) return;
|
|
visited.add(n);
|
|
toRecompile.push(n);
|
|
for (const dep of dependents.get(n) || []) {
|
|
collectDependents(dep);
|
|
}
|
|
}
|
|
collectDependents(name);
|
|
|
|
for (const defName of toRecompile) {
|
|
const ast = definitions.get(defName)!.body!;
|
|
const compiled = compile(ast);
|
|
|
|
const fn = new Function('store', `return ${compiled}`);
|
|
store[defName] = fn(store);
|
|
}
|
|
}
|
|
|
|
function getRootName(ast: AST): string | null {
|
|
if (ast.kind === 'variable') return ast.name;
|
|
if (ast.kind === 'record-access') return getRootName(ast.record);
|
|
return null;
|
|
}
|
|
|
|
function getPath(ast: AST): string[] {
|
|
if (ast.kind === 'variable') return [];
|
|
if (ast.kind === 'record-access') {
|
|
return [...getPath(ast.record), ast.field];
|
|
}
|
|
return [];
|
|
}
|