Fixing lots of stuff. pretty printer. saving to localStorage again. store stuff. can't remember now
This commit is contained in:
parent
01d8a6d67c
commit
b1696499e5
9 changed files with 300 additions and 102 deletions
|
|
@ -227,7 +227,7 @@ export function prettyPrint(ast: AST, indent = 0): string {
|
|||
? `...${prettyPrint(entry.expr, )}`
|
||||
: `${entry.key} = ${prettyPrint(entry.value, 0)}`
|
||||
);
|
||||
return `{ ${parts.join(', ') }`;
|
||||
return `{ ${parts.join(', ')} }`;
|
||||
|
||||
case 'lambda': {
|
||||
const params = ast.params.join(' ');
|
||||
|
|
@ -264,7 +264,7 @@ export function prettyPrint(ast: AST, indent = 0): string {
|
|||
return `...${prettyPrint(ast.spread, 0)}`;
|
||||
|
||||
case 'definition':
|
||||
return `${ast.name} = ${prettyPrint(ast.body, indent)}`;
|
||||
return `${ast.name} = ${prettyPrint(ast.body, indent)};`;
|
||||
|
||||
default:
|
||||
return `Unknown AST kind: ${i}${(ast as any).kind}`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# nth : Int \ List a \ Maybe a
|
||||
# in host at the moment, until we get typeclasses or something
|
||||
# in host at the moment, until we get typeclasses or something and this can work on strings too
|
||||
# nth = i list \ [i, list]
|
||||
# | [_, []] \ None
|
||||
# | [0, [x, ...xs]] \ (Some x)
|
||||
|
|
|
|||
|
|
@ -168,8 +168,6 @@ textInput = config \ ui.stateful {
|
|||
cursorX = ui.measureText textBeforeCursor;
|
||||
padding = 8;
|
||||
|
||||
_ = debug "focused" state.focused;
|
||||
|
||||
ui.clip {
|
||||
w = config.w,
|
||||
h = config.h,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ inspector = config \
|
|||
windowWidth = 600;
|
||||
|
||||
source = getSource config.name;
|
||||
_ = debug "inspector source" source;
|
||||
sourceLines = split "\n" source;
|
||||
_ = debug "source" source;
|
||||
_ = debug "sourceLines" sourceLines;
|
||||
|
||||
dialogPadding = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
osState = {
|
||||
palette = {
|
||||
visible = False,
|
||||
visible = True,
|
||||
query = "",
|
||||
focusedIndex = 0,
|
||||
},
|
||||
|
|
@ -10,6 +10,22 @@ osState = {
|
|||
}
|
||||
};
|
||||
|
||||
run = cmd \
|
||||
_ = debug "run" cmd;
|
||||
result = eval cmd;
|
||||
_ = debug "result" result;
|
||||
result
|
||||
| Defined name \ debug "defined" name
|
||||
| Value v \ debug "result" v
|
||||
| Err msg \ debug "error" msg
|
||||
| _ \ noOp;
|
||||
|
||||
onSelect = item \
|
||||
_ = debug "onSelect" item;
|
||||
item
|
||||
| "" \ run osState.palette.query
|
||||
| a \ inspect a;
|
||||
|
||||
inspect = item \
|
||||
batch [
|
||||
osState.palette.visible := False,
|
||||
|
|
@ -41,7 +57,7 @@ view = state viewport \
|
|||
| True \ palette {
|
||||
state = osState.palette,
|
||||
search = storeSearch,
|
||||
onSelect = item \ inspect item,
|
||||
onSelect = onSelect,
|
||||
viewport = viewport,
|
||||
}
|
||||
| False \ empty,
|
||||
|
|
|
|||
113
src/compiler.ts
113
src/compiler.ts
|
|
@ -2,10 +2,10 @@ import type { AST, Pattern, Definition } from './ast';
|
|||
import { _rt, store } from './runtime-js';
|
||||
|
||||
export const definitions: Map<string, AST> = new Map();
|
||||
const dependencies: Map<string, Set<string>> = new Map();
|
||||
const dependents: 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 function compile(ast: AST): string {
|
||||
export function compile(ast: AST, useStore = true, bound = new Set<string>()): string {
|
||||
switch (ast.kind) {
|
||||
case 'literal':
|
||||
if (ast.value.kind === 'string')
|
||||
|
|
@ -14,56 +14,63 @@ export function compile(ast: AST): string {
|
|||
return JSON.stringify(ast.value.value);
|
||||
throw new Error(`Cannot compile literal`); // of kind ${ast.value.kind}`);
|
||||
|
||||
case 'variable':
|
||||
return sanitize(ast.name);
|
||||
case 'variable': {
|
||||
if (bound.has(ast.name)) {
|
||||
return sanitizeName(ast.name);
|
||||
}
|
||||
return sanitize(ast.name, useStore);
|
||||
}
|
||||
|
||||
case 'lambda':
|
||||
case 'lambda': {
|
||||
const newBound = new Set([...bound, ...ast.params]);
|
||||
const params = ast.params.map(sanitizeName).join(') => (');
|
||||
return `Object.assign((${params}) => ${compile(ast.body)}, { _ast: (${JSON.stringify(ast)}) })`;
|
||||
return `Object.assign((${params}) => ${compile(ast.body, useStore, newBound)}, { _ast: (${JSON.stringify(ast)}) })`;
|
||||
}
|
||||
|
||||
case 'apply':
|
||||
// Constructor
|
||||
if (ast.func.kind === 'constructor') {
|
||||
const ctorName = ast.func.name;
|
||||
const arg = compile(ast.args[0]);
|
||||
const arg = compile(ast.args[0], useStore, bound);
|
||||
return `((_a) => _a && typeof _a === 'object' && !Array.isArray(_a) && !_a._tag
|
||||
? { _tag: "${ctorName}", ..._a }
|
||||
: { _tag: "${ctorName}", _0: _a })(${arg})`;
|
||||
}
|
||||
|
||||
const args = ast.args.map(compile).join(')(');
|
||||
return `${compile(ast.func)}(${args})`;
|
||||
const args = ast.args.map(a => compile(a, useStore, bound)).join(')(');
|
||||
return `${compile(ast.func, useStore, bound)}(${args})`;
|
||||
|
||||
case 'record': {
|
||||
const parts = ast.entries.map(entry =>
|
||||
entry.kind === 'spread'
|
||||
? `...${compile(entry.expr)}`
|
||||
: `${sanitizeName(entry.key)}: ${compile(entry.value)}`
|
||||
? `...${compile(entry.expr, useStore, bound)}`
|
||||
: `${sanitizeName(entry.key)}: ${compile(entry.value, useStore, bound)}`
|
||||
)
|
||||
return `({${parts.join(', ')}})`;
|
||||
}
|
||||
|
||||
case 'list': {
|
||||
const elements = ast.elements.map(e =>
|
||||
'spread' in e ? `...${compile(e.spread)}` : compile(e)
|
||||
'spread' in e ? `...${compile(e.spread, useStore, bound)}` : compile(e, useStore, bound)
|
||||
);
|
||||
return `[${elements.join(', ')}]`;
|
||||
}
|
||||
|
||||
case 'record-access':
|
||||
return `${compile(ast.record)}.${sanitizeName(ast.field)}`;
|
||||
return `${compile(ast.record, useStore, bound)}.${sanitizeName(ast.field)}`;
|
||||
|
||||
case 'record-update':
|
||||
const updates = Object.entries(ast.updates)
|
||||
.map(([k, v]) => `${sanitizeName(k)}: ${compile(v)}`);
|
||||
return `({...${compile(ast.record)}, ${updates.join(', ')}})`;
|
||||
.map(([k, v]) => `${sanitizeName(k)}: ${compile(v, useStore, bound)}`);
|
||||
return `({...${compile(ast.record, useStore, bound)}, ${updates.join(', ')}})`;
|
||||
|
||||
case 'let':
|
||||
const newBound = new Set([...bound, ast.name]);
|
||||
return `((${sanitizeName(ast.name)}) =>
|
||||
${compile(ast.body)})(${compile(ast.value)})`;
|
||||
${compile(ast.body, useStore, newBound)})(${compile(ast.value, useStore, bound)})`;
|
||||
|
||||
case 'match':
|
||||
return compileMatch(ast);
|
||||
return compileMatch(ast, useStore, bound);
|
||||
|
||||
case 'constructor':
|
||||
return `({ _tag: "${ast.name}" })`;
|
||||
|
|
@ -74,15 +81,20 @@ export function compile(ast: AST): string {
|
|||
*/
|
||||
|
||||
case 'rebind': {
|
||||
if (ast.target.kind === 'variable') {
|
||||
return `({ _tag: "Rebind", _0: "${ast.target.name}", _1: ${compile(ast.value)} })`;
|
||||
} else if (ast.target.kind === 'record-access') {
|
||||
const field = ast.target.field;
|
||||
const obj = compile(ast.target.record);
|
||||
const value = compile(ast.value);
|
||||
return `(() => { ${obj}.${sanitize(field)} = ${value}; return { _tag: "Rerender" }; })()`;
|
||||
const rootName = getRootName(ast.target);
|
||||
const path = getPath(ast.target);
|
||||
const value = compile(ast.value, useStore, bound);
|
||||
|
||||
if (!useStore || !rootName) {
|
||||
const target = compile(ast.target, useStore, bound);
|
||||
return `(() => { ${target} = ${value}; return { _tag: "Rerender" }; })()`;
|
||||
}
|
||||
|
||||
if (path.length === 0) {
|
||||
return `({ _tag: "Rebind", _0: "${rootName}", _1: ${value} })`;
|
||||
} else {
|
||||
return `({ _tag: "Rebind", _0: "${rootName}", _1: ${JSON.stringify(path)}, _2: ${value} })`;
|
||||
}
|
||||
throw new Error('Invalid rebind target');
|
||||
}
|
||||
|
||||
default:
|
||||
|
|
@ -91,7 +103,7 @@ export function compile(ast: AST): string {
|
|||
}
|
||||
}
|
||||
|
||||
function sanitize(name: string): string {
|
||||
function sanitize(name: string, useStore = true): string {
|
||||
const ops: Record<string, string> = {
|
||||
'add': '_rt.add', 'sub': '_rt.sub', 'mul': '_rt.mul',
|
||||
'div': '_rt.div', 'mod': '_rt.mod', 'eq': '_rt.eq',
|
||||
|
|
@ -101,9 +113,12 @@ function sanitize(name: string): string {
|
|||
|
||||
if (ops[name]) return ops[name];
|
||||
|
||||
const natives = ['measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'nth', 'len', 'slice', 'join', 'chars', 'split', 'str', 'redefine', 'stateful', 'batch', 'noOp', 'rerender', 'focus', 'ui'];
|
||||
const natives = ['eval', 'measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'nth', 'len', 'slice', 'join', 'chars', 'split', 'str', 'redefine', 'undefine', 'stateful', 'batch', 'noOp', 'rerender', 'focus', 'ui'];
|
||||
if (natives.includes(name)) return `_rt.${name}`;
|
||||
|
||||
if (useStore) {
|
||||
return `store.${sanitizeName(name)}`;
|
||||
}
|
||||
return sanitizeName(name);
|
||||
}
|
||||
|
||||
|
|
@ -113,26 +128,29 @@ function sanitizeName(name: string): string {
|
|||
'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'
|
||||
'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'}): string {
|
||||
const expr = compile(ast.expr);
|
||||
function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new Set<string>()): string {
|
||||
const expr = compile(ast.expr, useStore, bound);
|
||||
const tmpVar = `_m${Math.floor(Math.random() * 10000)}`;
|
||||
|
||||
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([...bound, ...patternBound]);
|
||||
code += `if (${condition}) { `;
|
||||
if (bindings.length > 0) {
|
||||
code += `const ${bindings.join(', ')}; `;
|
||||
}
|
||||
code += `return ${compile(c.result)}; }`;
|
||||
code += `return ${compile(c.result, useStore, newBound)}; }`;
|
||||
}
|
||||
|
||||
code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`;
|
||||
|
|
@ -182,7 +200,7 @@ function compilePattern(pattern: Pattern, expr: string): { condition: string, bi
|
|||
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
|
||||
bindings.push(...sub.bindings);
|
||||
});
|
||||
bindings.push(`${sanitize(pattern.spread)} = ${expr}.slice(${pattern.head.length})`);
|
||||
bindings.push(`${sanitizeName(pattern.spread)} = ${expr}.slice(${pattern.head.length})`);
|
||||
|
||||
return { condition, bindings };
|
||||
}
|
||||
|
|
@ -191,7 +209,7 @@ function compilePattern(pattern: Pattern, expr: string): { condition: string, bi
|
|||
let condition = 'true';
|
||||
const bindings: string[] = [];
|
||||
for (const [field, fieldPattern] of Object.entries(pattern.fields)) {
|
||||
const sub = compilePattern(fieldPattern, `${expr}.${sanitize(field)}`);
|
||||
const sub = compilePattern(fieldPattern, `${expr}.${sanitizeName(field)}`);
|
||||
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
|
||||
bindings.push(...sub.bindings);
|
||||
}
|
||||
|
|
@ -227,9 +245,8 @@ export function compileAndRun(defs: Definition[]) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
for (const def of defs) {
|
||||
const compiled = `const ${sanitize(def.name)} = ${compile(def.body)};`;
|
||||
const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, false)};`;
|
||||
compiledDefs.push(compiled);
|
||||
|
||||
try {
|
||||
|
|
@ -242,13 +259,13 @@ export function compileAndRun(defs: Definition[]) {
|
|||
}
|
||||
|
||||
const lastName = defs[defs.length - 1].name;
|
||||
const defNames = defs.map(d => sanitize(d.name)).join(', ');
|
||||
const defNames = defs.map(d => sanitizeName(d.name)).join(', ');
|
||||
|
||||
const code = `${compiledDefs.join('\n')}
|
||||
return { ${defNames}, __result: ${sanitize(lastName)} };`;
|
||||
return { ${defNames}, __result: ${sanitizeName(lastName)} };`;
|
||||
|
||||
const fn = new Function('_rt', code);
|
||||
const allDefs = fn(_rt);
|
||||
const fn = new Function('_rt', 'store', code);
|
||||
const allDefs = fn(_rt, store);
|
||||
|
||||
// Populate store
|
||||
for (const [name, value] of Object.entries(allDefs)) {
|
||||
|
|
@ -260,7 +277,7 @@ return { ${defNames}, __result: ${sanitize(lastName)} };`;
|
|||
return allDefs.__result;
|
||||
}
|
||||
|
||||
function freeVars(ast: AST, bound: Set<string> = new Set()): Set<string> {
|
||||
export function freeVars(ast: AST, bound: Set<string> = new Set()): Set<string> {
|
||||
switch (ast.kind) {
|
||||
case 'literal':
|
||||
case 'constructor':
|
||||
|
|
@ -385,3 +402,17 @@ export function recompile(name: string, newAst: AST) {
|
|||
store[defName] = fn(_rt, 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 [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { compileAndRun } from './compiler'
|
|||
import { tokenize } from './lexer'
|
||||
import { Parser } from './parser'
|
||||
import { runAppCompiled } from './runtime-compiled'
|
||||
import { _rt } from './runtime-js'
|
||||
import { _rt, loadDefinitions, saveDefinitions } from './runtime-js'
|
||||
|
||||
const modules = import.meta.glob('./cg/*.cg', { query: 'raw', import: 'default', eager: true });
|
||||
const cgCode = Object.keys(modules)
|
||||
|
|
@ -17,7 +17,9 @@ try {
|
|||
const tokens = tokenize(cgCode);
|
||||
const parser = new Parser(tokens, cgCode);
|
||||
const defs = parser.parse();
|
||||
loadDefinitions();
|
||||
const os = compileAndRun(defs);
|
||||
saveDefinitions();
|
||||
|
||||
runAppCompiled(
|
||||
{ init: os.init, update: os.update, view: os.view },
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ export class Parser {
|
|||
}
|
||||
|
||||
this.expect('equals');
|
||||
const value = this.parseExpressionNoMatch();
|
||||
const value = this.parseExpression();
|
||||
this.expect('semicolon');
|
||||
const body = this.parseExpressionNoMatch();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { tokenize } from './lexer'
|
||||
import { Parser } from './parser'
|
||||
import { recompile, definitions } from './compiler'
|
||||
import { compile, recompile, definitions, freeVars, dependencies, dependents } from './compiler'
|
||||
import { prettyPrint } from './ast'
|
||||
import type { AST } from './ast'
|
||||
|
||||
const STORAGE_KEY = 'cg-definitions';
|
||||
|
||||
export const store: Record<string, any> = {};
|
||||
|
||||
|
|
@ -46,55 +49,6 @@ export const _rt = {
|
|||
},
|
||||
},
|
||||
|
||||
batch: (events: any[]) => ({ _tag: 'Batch', _0: events }),
|
||||
noOp: { _tag: 'NoOp' },
|
||||
rerender: { _tag: 'Rerender' },
|
||||
focus: (key: string) => ({ _tag: 'Focus', _0: key }),
|
||||
|
||||
nth: (i: number) => (xs: any[] | string) => i >= 0 && i < xs.length
|
||||
? { _tag: 'Some', _0: xs[i] }
|
||||
: { _tag: 'None' },
|
||||
len: (xs: any[] | string) => xs.length,
|
||||
str: (x: any) => String(x),
|
||||
chars: (s: string) => s.split(''),
|
||||
join: (delim: string) => (xs: string[]) => xs.join(delim),
|
||||
split: (delim: string) => (xs: string) => xs.split(delim),
|
||||
slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end),
|
||||
debug: (label: string) => (value: any) => { console.log(label, value); return value; },
|
||||
fuzzyMatch: (query: string) => (target: string) => {
|
||||
const q = query.toLowerCase();
|
||||
const t = target.toLowerCase();
|
||||
let qi = 0;
|
||||
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
||||
if (t[ti] === q[qi]) qi++;
|
||||
}
|
||||
return { _tag: qi === q.length ? 'True' : 'False' };
|
||||
},
|
||||
storeSearch: (query: string) => {
|
||||
return Object.keys(store).filter(name => _rt.fuzzyMatch(query)(name)._tag === 'True');
|
||||
},
|
||||
getSource: (name: string) => {
|
||||
const ast = definitions.get(name);
|
||||
if (!ast) return "";
|
||||
const printed = prettyPrint(ast);
|
||||
return printed;
|
||||
},
|
||||
rebind: (name: string, pathOrValue: any, maybeValue?: any) => {
|
||||
if (maybeValue === undefined) {
|
||||
if (pathOrValue && pathOrValue.ast) {
|
||||
recompile(name, pathOrValue._ast);
|
||||
} else {
|
||||
store[name] = pathOrValue;
|
||||
}
|
||||
} else {
|
||||
const path = pathOrValue as string[];
|
||||
let obj = store[name];
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
obj = obj[path[i]];
|
||||
}
|
||||
obj[path[path.length - 1]] = maybeValue;
|
||||
}
|
||||
},
|
||||
measure: (ui: any): { width: number, height: number } => {
|
||||
switch (ui._kind) {
|
||||
case 'rect': return { width: ui.w, height: ui.h };
|
||||
|
|
@ -153,11 +107,209 @@ export const _rt = {
|
|||
return { width: 0, height: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
batch: (events: any[]) => ({ _tag: 'Batch', _0: events }),
|
||||
noOp: { _tag: 'NoOp' },
|
||||
rerender: { _tag: 'Rerender' },
|
||||
focus: (key: string) => ({ _tag: 'Focus', _0: key }),
|
||||
|
||||
nth: (i: number) => (xs: any[] | string) => i >= 0 && i < xs.length
|
||||
? { _tag: 'Some', _0: xs[i] }
|
||||
: { _tag: 'None' },
|
||||
len: (xs: any[] | string) => xs.length,
|
||||
str: (x: any) => String(x),
|
||||
chars: (s: string) => s.split(''),
|
||||
join: (delim: string) => (xs: string[]) => xs.join(delim),
|
||||
split: (delim: string) => (xs: string) => xs.split(delim),
|
||||
slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end),
|
||||
debug: (label: string) => (value: any) => { console.log(label, value); return value; },
|
||||
fuzzyMatch: (query: string) => (target: string) => {
|
||||
const q = query.toLowerCase();
|
||||
const t = target.toLowerCase();
|
||||
let qi = 0;
|
||||
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
||||
if (t[ti] === q[qi]) qi++;
|
||||
}
|
||||
return { _tag: qi === q.length ? 'True' : 'False' };
|
||||
},
|
||||
storeSearch: (query: string) => {
|
||||
return Object.keys(store).filter(name => _rt.fuzzyMatch(query)(name)._tag === 'True');
|
||||
},
|
||||
getSource: (name: string) => {
|
||||
const ast = definitions.get(name);
|
||||
if (!ast) return "";
|
||||
const printed = prettyPrint(ast);
|
||||
return printed;
|
||||
},
|
||||
rebind: (name: string, pathOrValue: any, maybeValue?: any) => {
|
||||
if (maybeValue === undefined) {
|
||||
store[name] = pathOrValue;
|
||||
} else {
|
||||
const path = pathOrValue as string[];
|
||||
let obj = store[name];
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
obj = obj[path[i]];
|
||||
}
|
||||
obj[path[path.length - 1]] = maybeValue;
|
||||
}
|
||||
syncToAst(name);
|
||||
},
|
||||
redefine: (name: string) => (code: string) => {
|
||||
const tokens = tokenize(`_tmp = ${code};`);
|
||||
const parser = new Parser(tokens, "");
|
||||
const defs = parser.parse();
|
||||
recompile(name, defs[0]. body);
|
||||
return { _tag: 'Ok' };
|
||||
},
|
||||
undefine: (name: string) => {
|
||||
delete store[name];
|
||||
definitions.delete(name);
|
||||
dependencies.delete(name);
|
||||
dependents.delete(name);
|
||||
saveDefinitions();
|
||||
return { _tag: 'Ok' };
|
||||
},
|
||||
|
||||
eval: (code: string) => {
|
||||
const trimmed = code.trim();
|
||||
|
||||
// is it a definition
|
||||
const defMatch = /^([a-z_][a-zA-Z0-9_]*)\s*=(?![=])/.exec(trimmed);
|
||||
|
||||
if (defMatch) {
|
||||
try {
|
||||
const fullCode = trimmed.endsWith(';') ? trimmed : trimmed + ';';
|
||||
const tokens = tokenize(fullCode);
|
||||
const parser = new Parser(tokens, fullCode);
|
||||
const defs = parser.parse();
|
||||
|
||||
if (defs.length > 0) {
|
||||
const def = defs[0];
|
||||
recompile(def.name, def.body);
|
||||
saveDefinitions();
|
||||
return { _tag: 'Defined', _0: def.name };
|
||||
}
|
||||
} catch (e: any) {
|
||||
return { _tag: 'Err', _0: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
// its an expression
|
||||
try {
|
||||
const wrapped = `_expr = ${trimmed};`;
|
||||
const tokens = tokenize(wrapped);
|
||||
const parser = new Parser(tokens, wrapped);
|
||||
const defs = parser.parse();
|
||||
const ast = defs[0].body;
|
||||
// validate free vars
|
||||
const free = freeVars(ast);
|
||||
const allowed = new Set([
|
||||
...Object.keys(store),
|
||||
...Object.keys(_rt),
|
||||
'True'
|
||||
])
|
||||
|
||||
const compiled = compile(defs[0].body);
|
||||
const fn = new Function('_rt', 'store', `return ${compiled}`);
|
||||
const result = fn(_rt, store);
|
||||
return { _tag: 'Value', _0: result };
|
||||
} catch (e: any) {
|
||||
return { _tag: 'Err', _0: e.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function saveDefinitions() {
|
||||
const saved: Record<string, string> = {};
|
||||
for (const [name, ast] of definitions) {
|
||||
const source = prettyPrint({ kind: 'definition', name, body: ast });
|
||||
saved[name] = source;
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(saved));
|
||||
|
||||
}
|
||||
|
||||
export function loadDefinitions() {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
if (!data) return;
|
||||
|
||||
try {
|
||||
const saved = JSON.parse(data);
|
||||
for (const [name, source] of Object.entries(saved)) {
|
||||
const tokens = tokenize(source as string);
|
||||
const parser = new Parser(tokens, source as string);
|
||||
const defs = parser.parse();
|
||||
if (defs.length > 0) {
|
||||
recompile(defs[0].name, defs[0].body);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load definitions:', e);
|
||||
console.log(data);
|
||||
}
|
||||
}
|
||||
|
||||
function valueToAst(value: any): AST {
|
||||
// Numbers
|
||||
if (typeof value === 'number') {
|
||||
return {
|
||||
kind: 'literal',
|
||||
value: Number.isInteger(value)
|
||||
? { kind: 'int', value }
|
||||
: { kind: 'float', value}
|
||||
};
|
||||
}
|
||||
|
||||
// Strings
|
||||
if (typeof value === 'string') {
|
||||
return { kind: 'literal', value: { kind: 'string', value } };
|
||||
}
|
||||
|
||||
// Arrays
|
||||
if (Array.isArray(value)) {
|
||||
return { kind: 'list', elements: value.map(valueToAst) };
|
||||
}
|
||||
|
||||
// Constructor
|
||||
if (typeof value === 'object' && value !== null && value._tag) {
|
||||
const tag = value._tag;
|
||||
|
||||
if ('_0' in value) {
|
||||
return {
|
||||
kind: 'apply',
|
||||
func: { kind: 'constructor', name: tag },
|
||||
args: [valueToAst(value._0)]
|
||||
};
|
||||
}
|
||||
|
||||
return { kind: 'constructor', name: tag };
|
||||
}
|
||||
|
||||
// Records
|
||||
if (typeof value === 'object' && value !== null && !value._tag) {
|
||||
const entries = Object.entries(value).map(([k, v]) => ({
|
||||
kind: 'field' as const,
|
||||
key: k,
|
||||
value: valueToAst(v)
|
||||
}));
|
||||
return { kind: 'record', entries };
|
||||
}
|
||||
|
||||
// Functions
|
||||
if (typeof value === 'function') {
|
||||
if (value._ast) {
|
||||
return value._ast;
|
||||
}
|
||||
|
||||
throw new Error('Cannot serialize function without _ast');
|
||||
}
|
||||
|
||||
throw new Error(`Cannot convert to AST: ${typeof value}`);
|
||||
}
|
||||
|
||||
export function syncToAst(name: string) {
|
||||
if (definitions.has(name)) {
|
||||
definitions.set(name, valueToAst(store[name]));
|
||||
saveDefinitions();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue