we have persistence. persisting all store values' ASTs to localStorage

master
Dustin Swan 5 hours ago
parent 1029b1671f
commit 8c3237e0db
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -5,25 +5,25 @@ import type { Value } from './types';
export type Literal = { export type Literal = {
kind: 'literal' kind: 'literal'
value: Value value: Value
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type Variable = { export type Variable = {
kind: 'variable' kind: 'variable'
name: string name: string
line: number line?: number
column: number column: number
start: number start?: number
} }
export type Constructor = { export type Constructor = {
kind: 'constructor' kind: 'constructor'
name: string name: string
line: number line?: number
column: number column?: number
start: number start?: number
} }
// Functions // Functions
@ -32,18 +32,18 @@ export type Lambda = {
kind: 'lambda' kind: 'lambda'
params: string[] params: string[]
body: AST body: AST
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type Apply = { export type Apply = {
kind: 'apply' kind: 'apply'
func: AST func: AST
args: AST[] args: AST[]
line: number line?: number
column: number column?: number
start: number start?: number
} }
// Bindings // Bindings
@ -53,9 +53,9 @@ export type Let = {
name: string name: string
value: AST value: AST
body: AST body: AST
line: number line?: number
column: number column?: number
start: number start?: number
} }
// Matching // Matching
@ -64,17 +64,17 @@ export type Match = {
kind: 'match' kind: 'match'
expr: AST expr: AST
cases: MatchCase[] cases: MatchCase[]
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type MatchCase = { export type MatchCase = {
pattern: Pattern pattern: Pattern
result: AST result: AST
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type Pattern = export type Pattern =
@ -91,43 +91,43 @@ export type Pattern =
export type ListSpread = { export type ListSpread = {
kind: 'list-spread' kind: 'list-spread'
spread: AST; spread: AST;
line: number; line?: number;
column: number; column?: number;
start: number; start?: number;
} }
export type List = { export type List = {
kind: 'list' kind: 'list'
elements: (AST | ListSpread)[] elements: (AST | ListSpread)[]
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type Record = { export type Record = {
kind: 'record' kind: 'record'
fields: { [key: string]: AST } fields: { [key: string]: AST }
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type RecordAccess = { export type RecordAccess = {
kind: 'record-access' kind: 'record-access'
record: AST record: AST
field: string field: string
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type RecordUpdate = { export type RecordUpdate = {
kind: 'record-update' kind: 'record-update'
record: AST record: AST
updates: { [key: string]: AST } updates: { [key: string]: AST }
line: number line?: number
column: number column?: number
start: number start?: number
} }
// Top-level constructs // Top-level constructs
@ -136,36 +136,36 @@ export type Definition = {
kind: 'definition' kind: 'definition'
name: string name: string
body: AST body: AST
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type TypeDef = { export type TypeDef = {
kind: 'typedef' kind: 'typedef'
name: string name: string
variants: Array<{ name: string, args: string[] }> variants: Array<{ name: string, args: string[] }>
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type Import = { export type Import = {
kind: 'import' kind: 'import'
module: string module: string
items: string[] | 'all' items: string[] | 'all'
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type Rebind = { export type Rebind = {
kind: 'rebind' kind: 'rebind'
target: AST target: AST
value: AST value: AST
line: number line?: number
column: number column?: number
start: number start?: number
} }
export type AST = export type AST =

@ -1,14 +1,14 @@
export class CGError extends Error { export class CGError extends Error {
line: number; line?: number;
column: number; column?: number;
source: string; source?: string;
errorType: 'RuntimeError' | 'ParseError'; errorType: 'RuntimeError' | 'ParseError';
constructor( constructor(
message: string, message: string,
line: number, line?: number,
column: number, column?: number,
source: string, source?: string,
errorType: 'RuntimeError' | 'ParseError' = 'RuntimeError' errorType: 'RuntimeError' | 'ParseError' = 'RuntimeError'
) { ) {
super(message); super(message);
@ -20,6 +20,10 @@ export class CGError extends Error {
} }
format(): string { format(): string {
if (this.line === undefined || this.column === undefined || !this.source) {
return `${this.name}: ${this.message}`;
}
const lines = this.source.split('\n'); const lines = this.source.split('\n');
const errorLine = lines[this.line - 1]; const errorLine = lines[this.line - 1];
@ -35,10 +39,10 @@ export class CGError extends Error {
} }
} }
export function RuntimeError(message: string, line: number, column: number, source: string) { export function RuntimeError(message: string, line?: number, column?: number, source?: string) {
return new CGError(message, line, column, source, 'RuntimeError'); return new CGError(message, line, column, source, 'RuntimeError');
} }
export function ParseError(message: string, line: number, column: number, source: string) { export function ParseError(message: string, line?: number, column?: number, source?: string) {
return new CGError(message, line, column, source, 'ParseError'); return new CGError(message, line, column, source, 'ParseError');
} }

@ -13,7 +13,7 @@ export function evaluate(ast: AST, env: Env, source: string): Value {
const val = env.get(ast.name); const val = env.get(ast.name);
if (val === undefined) if (val === undefined)
throw RuntimeError( `Unknown variable: ${ast.name}`, ast.line, ast.column, source); throw RuntimeError(`Unknown variable: ${ast.name}`, ast.line, ast.column, source);
recordDependency(ast.name); recordDependency(ast.name);

@ -6,8 +6,7 @@ import { runApp } from './runtime';
import { builtins } from './builtins'; import { builtins } from './builtins';
import { CGError } from './error'; import { CGError } from './error';
import { createStore, startTracking, stopTracking, buildDependents } from './store'; import { createStore, startTracking, stopTracking, buildDependents } from './store';
// import type { Store } from './store' import { loadStore } from './persistence';
import stdlibCode from './stdlib.cg?raw'; import stdlibCode from './stdlib.cg?raw';
import designTokensCode from './design-tokens.cg?raw'; import designTokensCode from './design-tokens.cg?raw';
@ -33,18 +32,32 @@ try {
const env: Env = new Map(Object.entries(builtins)); const env: Env = new Map(Object.entries(builtins));
const store = createStore(); const store = createStore();
const persisted = loadStore();
for (const def of definitions) { for (const def of definitions) {
const body = persisted?.[def.name]?.body ?? def.body;
const deps = startTracking(def.name); const deps = startTracking(def.name);
const value = evaluate(def.body, env, cgCode); const value = evaluate(body, env, cgCode);
stopTracking(); stopTracking();
env.set(def.name, value); env.set(def.name, value);
store.set(def.name, { value, body, dependencies: deps });
}
// Load persisted store entries
if (persisted) {
for (const [name, data] of Object.entries(persisted)) {
if (!store.has(name) && data.source === 'runtime') {
const deps = startTracking(name);
const value = evaluate(data.body, env, "");
stopTracking();
env.set(name, value);
store.set(def.name, { store.set(name, { value, body: data.body, dependencies: deps });
value, }
body: def.body, }
dependencies: deps
});
} }
const dependents = buildDependents(store); const dependents = buildDependents(store);

@ -0,0 +1,29 @@
import type { AST } from './ast'
import type { Store } from './store'
const STORAGE_KEY = 'cg_store';
export function saveStore(store: Store) {
const data: Record<string, any> = {};
for (const [name, entry] of store) {
data[name] = {
body: entry.body,
source: 'file'
};
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
export function loadStore(): Record<string, { body: AST, source: string }> | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch(e) {
return null;
}
}
export function clearStore() {
localStorage.remove(STORAGE_KEY);
}

@ -6,6 +6,8 @@ import { CGError } from './error';
import type { AST } from './ast'; import type { AST } from './ast';
import type { Env } from './env'; import type { Env } from './env';
import type { Store } from './store'; import type { Store } from './store';
import { saveStore } from './persistence';
import { valueToAST } from './valueToAST';
export type App = { export type App = {
init: Value; init: Value;
@ -284,9 +286,12 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
const entry = store.get(name); const entry = store.get(name);
if (entry) { if (entry) {
entry.value = newValue; entry.value = newValue;
entry.body = valueToAST(newValue);
} }
recomputeDependents(name); recomputeDependents(name);
saveStore(store);
return; return;
} }

@ -6,6 +6,8 @@ testApp = {
combinedText = email & " " & password combinedText = email & " " & password
}; };
topLevelText = "";
update = state event \ event update = state event \ event
| _ \ state; | _ \ state;
@ -18,7 +20,7 @@ view = state viewport \
children = [ children = [
textInput { textInput {
key = "email", key = "email",
initialValue = "", initialValue = testApp.email,
initialFocus = True, initialFocus = True,
w = 300, w = 300,
h = 40, h = 40,
@ -26,7 +28,7 @@ view = state viewport \
}, },
textInput { textInput {
key = "password", key = "password",
initialValue = "", initialValue = testApp.password,
initialFocus = False, initialFocus = False,
w = 300, w = 300,
h = 40, h = 40,
@ -34,7 +36,16 @@ view = state viewport \
}, },
Text { content = "Username: " & testApp.email, x = 8, y = 16 }, Text { content = "Username: " & testApp.email, x = 8, y = 16 },
Text { content = "Password: " & testApp.password, x = 8, y = 16 }, Text { content = "Password: " & testApp.password, x = 8, y = 16 },
Text { content = "Combined: " & testApp.combinedText, x = 8, y = 16 } Text { content = "Combined: " & testApp.combinedText, x = 8, y = 16 },
textInput {
key = "top-level-text",
initialValue = topLevelText,
initialFocus = False,
w = 300,
h = 40,
onChange = text \ topLevelText := text
},
Text { content = "Top Level: " & topLevelText, x = 8, y = 16 }
] ]
} }
}; };

@ -0,0 +1,45 @@
import type { AST } from './ast'
import type { Value } from './types'
export function valueToAST(value: Value): AST {
switch (value.kind) {
case 'int':
case 'float':
case 'string':
return { kind: 'literal', value };
case 'list':
return {
kind: 'list',
elements: value.elements.map(valueToAST)
};
case 'record':
const fields: { [key: string]: AST } = {};
for (const [k, v] of Object.entries(value.fields)) {
if (value.fieldMeta && value.fieldMeta?.[k]?.dependencies?.size > 0) {
fields[k] = value.fieldMeta[k].body;
} else {
fields[k] = valueToAST(v);
}
}
return { kind: 'record', fields };
case 'constructor':
return {
kind: 'constructor',
name: value.name
};
case 'closure':
return {
kind: 'lambda',
params: value.params,
body: value.body
};
default:
throw new Error(`Cannot convert ${(value as any).kind} to AST`);
}
}
Loading…
Cancel
Save