|
|
|
@ -1,13 +1,15 @@
|
|
|
|
import type { AST, Pattern, Definition } from './ast';
|
|
|
|
import type { AST, Pattern, Definition } from './ast';
|
|
|
|
import { _rt, store } from './runtime-js';
|
|
|
|
import { store } from './runtime-js';
|
|
|
|
|
|
|
|
|
|
|
|
let matchCounter = 0;
|
|
|
|
let matchCounter = 0;
|
|
|
|
|
|
|
|
|
|
|
|
export const definitions: Map<string, AST> = new Map();
|
|
|
|
export const definitions: Map<string, AST> = new Map();
|
|
|
|
export const dependencies: Map<string, Set<string>> = new Map();
|
|
|
|
export const dependencies: Map<string, Set<string>> = new Map();
|
|
|
|
export const dependents: 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, useStore = true, bound = new Set<string>()): string {
|
|
|
|
export function compile(ast: AST, useStore = true, bound = new Set<string>(), topLevel = new Set<string>()): string {
|
|
|
|
switch (ast.kind) {
|
|
|
|
switch (ast.kind) {
|
|
|
|
case 'literal':
|
|
|
|
case 'literal':
|
|
|
|
if (ast.value.kind === 'string')
|
|
|
|
if (ast.value.kind === 'string')
|
|
|
|
@ -18,61 +20,62 @@ export function compile(ast: AST, useStore = true, bound = new Set<string>()): s
|
|
|
|
|
|
|
|
|
|
|
|
case 'variable': {
|
|
|
|
case 'variable': {
|
|
|
|
if (bound.has(ast.name)) {
|
|
|
|
if (bound.has(ast.name)) {
|
|
|
|
return sanitizeName(ast.name);
|
|
|
|
return sanitizeName(ast.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return sanitize(ast.name, useStore);
|
|
|
|
return sanitize(ast.name, useStore, topLevel);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'lambda': {
|
|
|
|
case 'lambda': {
|
|
|
|
const newBound = new Set([...bound, ...ast.params]);
|
|
|
|
const newBound = new Set([...bound, ...ast.params]);
|
|
|
|
const params = ast.params.map(sanitizeName).join(') => (');
|
|
|
|
const params = ast.params.map(sanitizeName).join(') => (');
|
|
|
|
return `Object.assign((${params}) => ${compile(ast.body, useStore, newBound)}, { _ast: (${JSON.stringify(ast)}) })`;
|
|
|
|
const id = astIdCounter++;
|
|
|
|
|
|
|
|
return `Object.assign((${params}) => ${compile(ast.body, useStore, newBound, topLevel)}, { _astId: (${id}) })`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'apply':
|
|
|
|
case 'apply':
|
|
|
|
// Constructor
|
|
|
|
// Constructor
|
|
|
|
if (ast.func.kind === 'constructor') {
|
|
|
|
if (ast.func.kind === 'constructor') {
|
|
|
|
const ctorName = ast.func.name;
|
|
|
|
const ctorName = ast.func.name;
|
|
|
|
const arg = compile(ast.args[0], useStore, bound);
|
|
|
|
const arg = compile(ast.args[0], useStore, bound, topLevel);
|
|
|
|
return `((_a) => _a && typeof _a === 'object' && !Array.isArray(_a) && !_a._tag
|
|
|
|
return `((_a) => _a && typeof _a === 'object' && !Array.isArray(_a) && !_a._tag
|
|
|
|
? { _tag: "${ctorName}", ..._a }
|
|
|
|
? { _tag: "${ctorName}", ..._a }
|
|
|
|
: { _tag: "${ctorName}", _0: _a })(${arg})`;
|
|
|
|
: { _tag: "${ctorName}", _0: _a })(${arg})`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const args = ast.args.map(a => compile(a, useStore, bound)).join(')(');
|
|
|
|
const args = ast.args.map(a => compile(a, useStore, bound, topLevel)).join(')(');
|
|
|
|
return `${compile(ast.func, useStore, bound)}(${args})`;
|
|
|
|
return `${compile(ast.func, useStore, bound, topLevel)}(${args})`;
|
|
|
|
|
|
|
|
|
|
|
|
case 'record': {
|
|
|
|
case 'record': {
|
|
|
|
const parts = ast.entries.map(entry =>
|
|
|
|
const parts = ast.entries.map(entry =>
|
|
|
|
entry.kind === 'spread'
|
|
|
|
entry.kind === 'spread'
|
|
|
|
? `...${compile(entry.expr, useStore, bound)}`
|
|
|
|
? `...${compile(entry.expr, useStore, bound, topLevel)}`
|
|
|
|
: `${sanitizeName(entry.key)}: ${compile(entry.value, useStore, bound)}`
|
|
|
|
: `${sanitizeName(entry.key)}: ${compile(entry.value, useStore, bound, topLevel)}`
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return `({${parts.join(', ')}})`;
|
|
|
|
return `({${parts.join(', ')}})`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'list': {
|
|
|
|
case 'list': {
|
|
|
|
const elements = ast.elements.map(e =>
|
|
|
|
const elements = ast.elements.map(e =>
|
|
|
|
'spread' in e ? `...${compile(e.spread, useStore, bound)}` : compile(e, useStore, bound)
|
|
|
|
'spread' in e ? `...${compile(e.spread, useStore, bound, topLevel)}` : compile(e, useStore, bound, topLevel)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
return `[${elements.join(', ')}]`;
|
|
|
|
return `[${elements.join(', ')}]`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'record-access':
|
|
|
|
case 'record-access':
|
|
|
|
return `${compile(ast.record, useStore, bound)}.${sanitizeName(ast.field)}`;
|
|
|
|
return `${compile(ast.record, useStore, bound, topLevel)}.${sanitizeName(ast.field)}`;
|
|
|
|
|
|
|
|
|
|
|
|
case 'record-update':
|
|
|
|
case 'record-update':
|
|
|
|
const updates = Object.entries(ast.updates)
|
|
|
|
const updates = Object.entries(ast.updates)
|
|
|
|
.map(([k, v]) => `${sanitizeName(k)}: ${compile(v, useStore, bound)}`);
|
|
|
|
.map(([k, v]) => `${sanitizeName(k)}: ${compile(v, useStore, bound, topLevel)}`);
|
|
|
|
return `({...${compile(ast.record, useStore, bound)}, ${updates.join(', ')}})`;
|
|
|
|
return `({...${compile(ast.record, useStore, bound, topLevel)}, ${updates.join(', ')}})`;
|
|
|
|
|
|
|
|
|
|
|
|
case 'let':
|
|
|
|
case 'let':
|
|
|
|
const newBound = new Set([...bound, ast.name]);
|
|
|
|
const newBound = new Set([...bound, ast.name]);
|
|
|
|
return `((${sanitizeName(ast.name)}) =>
|
|
|
|
return `((${sanitizeName(ast.name)}) =>
|
|
|
|
${compile(ast.body, useStore, newBound)})(${compile(ast.value, useStore, bound)})`;
|
|
|
|
${compile(ast.body, useStore, newBound, topLevel)})(${compile(ast.value, useStore, bound, topLevel)})`;
|
|
|
|
|
|
|
|
|
|
|
|
case 'match':
|
|
|
|
case 'match':
|
|
|
|
return compileMatch(ast, useStore, bound);
|
|
|
|
return compileMatch(ast, useStore, bound, topLevel);
|
|
|
|
|
|
|
|
|
|
|
|
case 'constructor':
|
|
|
|
case 'constructor':
|
|
|
|
return `({ _tag: "${ast.name}" })`;
|
|
|
|
return `({ _tag: "${ast.name}" })`;
|
|
|
|
@ -85,10 +88,15 @@ export function compile(ast: AST, useStore = true, bound = new Set<string>()): s
|
|
|
|
case 'rebind': {
|
|
|
|
case 'rebind': {
|
|
|
|
const rootName = getRootName(ast.target);
|
|
|
|
const rootName = getRootName(ast.target);
|
|
|
|
const path = getPath(ast.target);
|
|
|
|
const path = getPath(ast.target);
|
|
|
|
const value = compile(ast.value, useStore, bound);
|
|
|
|
const value = compile(ast.value, useStore, bound, topLevel);
|
|
|
|
|
|
|
|
|
|
|
|
if (!rootName) throw new Error('Rebind target must be a variable');
|
|
|
|
if (!rootName) throw new Error('Rebind target must be a variable');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (bound.has(rootName)) {
|
|
|
|
|
|
|
|
const target = compile(ast.target, useStore, bound, topLevel);
|
|
|
|
|
|
|
|
return `(() => { ${target} = ${value}; return { _tag: "NoOp" }; })()`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (path.length === 0) {
|
|
|
|
if (path.length === 0) {
|
|
|
|
return `({ _tag: "Rebind", _0: "${rootName}", _1: ${value} })`;
|
|
|
|
return `({ _tag: "Rebind", _0: "${rootName}", _1: ${value} })`;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
@ -102,23 +110,9 @@ export function compile(ast: AST, useStore = true, bound = new Set<string>()): s
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sanitize(name: string, useStore = true): string {
|
|
|
|
function sanitize(name: string, useStore = true, topLevel: Set<string>): string {
|
|
|
|
const ops: Record<string, string> = {
|
|
|
|
if (!useStore && topLevel.has(name)) return sanitizeName(name);
|
|
|
|
'add': '_rt.add', 'sub': '_rt.sub', 'mul': '_rt.mul', 'div': '_rt.div',
|
|
|
|
return `store[${JSON.stringify(name)}]`
|
|
|
|
'mod': '_rt.mod', 'pow': '_rt.pow',
|
|
|
|
|
|
|
|
'eq': '_rt.eq', 'gt': '_rt.gt', 'lt': '_rt.lt', 'gte': '_rt.gte', 'lte': '_rt.lte',
|
|
|
|
|
|
|
|
'cat': '_rt.cat', 'max': '_rt.max', 'min': '_rt.min',
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (ops[name]) return ops[name];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const natives = ['eval', 'measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'nth', 'len', 'slice', 'join', 'chars', 'split', 'str', 'redefine', 'undefine', 'batch', 'noOp', 'focus', 'ui'];
|
|
|
|
|
|
|
|
if (natives.includes(name)) return `_rt.${name}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (useStore) {
|
|
|
|
|
|
|
|
return `store.${sanitizeName(name)}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return sanitizeName(name);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sanitizeName(name: string): string {
|
|
|
|
function sanitizeName(name: string): string {
|
|
|
|
@ -135,8 +129,8 @@ function sanitizeName(name: string): string {
|
|
|
|
return name.replace(/-/g, '_');
|
|
|
|
return name.replace(/-/g, '_');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new Set<string>()): string {
|
|
|
|
function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new Set<string>(), topLevel = new Set<string>()): string {
|
|
|
|
const expr = compile(ast.expr, useStore, bound);
|
|
|
|
const expr = compile(ast.expr, useStore, bound, topLevel);
|
|
|
|
const tmpVar = `_m${matchCounter++}`;
|
|
|
|
const tmpVar = `_m${matchCounter++}`;
|
|
|
|
|
|
|
|
|
|
|
|
let code = `((${tmpVar}) => { `;
|
|
|
|
let code = `((${tmpVar}) => { `;
|
|
|
|
@ -149,7 +143,7 @@ function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new
|
|
|
|
if (bindings.length > 0) {
|
|
|
|
if (bindings.length > 0) {
|
|
|
|
code += `const ${bindings.join(', ')}; `;
|
|
|
|
code += `const ${bindings.join(', ')}; `;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
code += `return ${compile(c.result, useStore, newBound)}; }`;
|
|
|
|
code += `return ${compile(c.result, useStore, newBound, topLevel)}; }`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`;
|
|
|
|
code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`;
|
|
|
|
@ -245,11 +239,11 @@ export function compileAndRun(defs: Definition[]) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const def of defs) {
|
|
|
|
for (const def of defs) {
|
|
|
|
const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, false)};`;
|
|
|
|
const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, false, new Set(), topLevel)};`;
|
|
|
|
compiledDefs.push(compiled);
|
|
|
|
compiledDefs.push(compiled);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
new Function('_rt', compiled);
|
|
|
|
new Function('store', compiled);
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
console.error(`=== BROKEN: ${def.name} ===`);
|
|
|
|
console.error(`=== BROKEN: ${def.name} ===`);
|
|
|
|
console.error(compiled);
|
|
|
|
console.error(compiled);
|
|
|
|
@ -263,8 +257,8 @@ export function compileAndRun(defs: Definition[]) {
|
|
|
|
const code = `${compiledDefs.join('\n')}
|
|
|
|
const code = `${compiledDefs.join('\n')}
|
|
|
|
return { ${defNames}, __result: ${sanitizeName(lastName)} };`;
|
|
|
|
return { ${defNames}, __result: ${sanitizeName(lastName)} };`;
|
|
|
|
|
|
|
|
|
|
|
|
const fn = new Function('_rt', 'store', code);
|
|
|
|
const fn = new Function('store', code);
|
|
|
|
const allDefs = fn(_rt, store);
|
|
|
|
const allDefs = fn(store);
|
|
|
|
|
|
|
|
|
|
|
|
// Populate store
|
|
|
|
// Populate store
|
|
|
|
for (const [name, value] of Object.entries(allDefs)) {
|
|
|
|
for (const [name, value] of Object.entries(allDefs)) {
|
|
|
|
@ -397,8 +391,8 @@ export function recompile(name: string, newAst: AST) {
|
|
|
|
const ast = definitions.get(defName)!;
|
|
|
|
const ast = definitions.get(defName)!;
|
|
|
|
const compiled = compile(ast);
|
|
|
|
const compiled = compile(ast);
|
|
|
|
|
|
|
|
|
|
|
|
const fn = new Function('_rt', 'store', `return ${compiled}`);
|
|
|
|
const fn = new Function('store', `return ${compiled}`);
|
|
|
|
store[defName] = fn(_rt, store);
|
|
|
|
store[defName] = fn(store);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|