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
|
|
@ -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