we have persistence. persisting all store values' ASTs to localStorage
This commit is contained in:
parent
1029b1671f
commit
8c3237e0db
8 changed files with 177 additions and 70 deletions
100
src/ast.ts
100
src/ast.ts
|
|
@ -5,25 +5,25 @@ import type { Value } from './types';
|
|||
export type Literal = {
|
||||
kind: 'literal'
|
||||
value: Value
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type Variable = {
|
||||
kind: 'variable'
|
||||
name: string
|
||||
line: number
|
||||
line?: number
|
||||
column: number
|
||||
start: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type Constructor = {
|
||||
kind: 'constructor'
|
||||
name: string
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
// Functions
|
||||
|
|
@ -32,18 +32,18 @@ export type Lambda = {
|
|||
kind: 'lambda'
|
||||
params: string[]
|
||||
body: AST
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type Apply = {
|
||||
kind: 'apply'
|
||||
func: AST
|
||||
args: AST[]
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
// Bindings
|
||||
|
|
@ -53,9 +53,9 @@ export type Let = {
|
|||
name: string
|
||||
value: AST
|
||||
body: AST
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
// Matching
|
||||
|
|
@ -64,17 +64,17 @@ export type Match = {
|
|||
kind: 'match'
|
||||
expr: AST
|
||||
cases: MatchCase[]
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type MatchCase = {
|
||||
pattern: Pattern
|
||||
result: AST
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type Pattern =
|
||||
|
|
@ -91,43 +91,43 @@ export type Pattern =
|
|||
export type ListSpread = {
|
||||
kind: 'list-spread'
|
||||
spread: AST;
|
||||
line: number;
|
||||
column: number;
|
||||
start: number;
|
||||
line?: number;
|
||||
column?: number;
|
||||
start?: number;
|
||||
}
|
||||
|
||||
export type List = {
|
||||
kind: 'list'
|
||||
elements: (AST | ListSpread)[]
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type Record = {
|
||||
kind: 'record'
|
||||
fields: { [key: string]: AST }
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type RecordAccess = {
|
||||
kind: 'record-access'
|
||||
record: AST
|
||||
field: string
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type RecordUpdate = {
|
||||
kind: 'record-update'
|
||||
record: AST
|
||||
updates: { [key: string]: AST }
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
// Top-level constructs
|
||||
|
|
@ -136,36 +136,36 @@ export type Definition = {
|
|||
kind: 'definition'
|
||||
name: string
|
||||
body: AST
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type TypeDef = {
|
||||
kind: 'typedef'
|
||||
name: string
|
||||
variants: Array<{ name: string, args: string[] }>
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type Import = {
|
||||
kind: 'import'
|
||||
module: string
|
||||
items: string[] | 'all'
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type Rebind = {
|
||||
kind: 'rebind'
|
||||
target: AST
|
||||
value: AST
|
||||
line: number
|
||||
column: number
|
||||
start: number
|
||||
line?: number
|
||||
column?: number
|
||||
start?: number
|
||||
}
|
||||
|
||||
export type AST =
|
||||
|
|
|
|||
20
src/error.ts
20
src/error.ts
|
|
@ -1,14 +1,14 @@
|
|||
export class CGError extends Error {
|
||||
line: number;
|
||||
column: number;
|
||||
source: string;
|
||||
line?: number;
|
||||
column?: number;
|
||||
source?: string;
|
||||
errorType: 'RuntimeError' | 'ParseError';
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
line: number,
|
||||
column: number,
|
||||
source: string,
|
||||
line?: number,
|
||||
column?: number,
|
||||
source?: string,
|
||||
errorType: 'RuntimeError' | 'ParseError' = 'RuntimeError'
|
||||
) {
|
||||
super(message);
|
||||
|
|
@ -20,6 +20,10 @@ export class CGError extends Error {
|
|||
}
|
||||
|
||||
format(): string {
|
||||
if (this.line === undefined || this.column === undefined || !this.source) {
|
||||
return `${this.name}: ${this.message}`;
|
||||
}
|
||||
|
||||
const lines = this.source.split('\n');
|
||||
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');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function evaluate(ast: AST, env: Env, source: string): Value {
|
|||
const val = env.get(ast.name);
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
|||
29
src/main.ts
29
src/main.ts
|
|
@ -6,8 +6,7 @@ import { runApp } from './runtime';
|
|||
import { builtins } from './builtins';
|
||||
import { CGError } from './error';
|
||||
import { createStore, startTracking, stopTracking, buildDependents } from './store';
|
||||
// import type { Store } from './store'
|
||||
|
||||
import { loadStore } from './persistence';
|
||||
|
||||
import stdlibCode from './stdlib.cg?raw';
|
||||
import designTokensCode from './design-tokens.cg?raw';
|
||||
|
|
@ -33,18 +32,32 @@ try {
|
|||
const env: Env = new Map(Object.entries(builtins));
|
||||
const store = createStore();
|
||||
|
||||
const persisted = loadStore();
|
||||
|
||||
for (const def of definitions) {
|
||||
const body = persisted?.[def.name]?.body ?? def.body;
|
||||
|
||||
const deps = startTracking(def.name);
|
||||
const value = evaluate(def.body, env, cgCode);
|
||||
const value = evaluate(body, env, cgCode);
|
||||
stopTracking();
|
||||
|
||||
env.set(def.name, value);
|
||||
store.set(def.name, { value, body, dependencies: deps });
|
||||
}
|
||||
|
||||
store.set(def.name, {
|
||||
value,
|
||||
body: def.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(name, { value, body: data.body, dependencies: deps });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dependents = buildDependents(store);
|
||||
|
|
|
|||
29
src/persistence.ts
Normal file
29
src/persistence.ts
Normal file
|
|
@ -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 { Env } from './env';
|
||||
import type { Store } from './store';
|
||||
import { saveStore } from './persistence';
|
||||
import { valueToAST } from './valueToAST';
|
||||
|
||||
export type App = {
|
||||
init: Value;
|
||||
|
|
@ -284,9 +286,12 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
|
|||
const entry = store.get(name);
|
||||
if (entry) {
|
||||
entry.value = newValue;
|
||||
entry.body = valueToAST(newValue);
|
||||
}
|
||||
|
||||
recomputeDependents(name);
|
||||
|
||||
saveStore(store);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ testApp = {
|
|||
combinedText = email & " " & password
|
||||
};
|
||||
|
||||
topLevelText = "";
|
||||
|
||||
update = state event \ event
|
||||
| _ \ state;
|
||||
|
||||
|
|
@ -18,7 +20,7 @@ view = state viewport \
|
|||
children = [
|
||||
textInput {
|
||||
key = "email",
|
||||
initialValue = "",
|
||||
initialValue = testApp.email,
|
||||
initialFocus = True,
|
||||
w = 300,
|
||||
h = 40,
|
||||
|
|
@ -26,7 +28,7 @@ view = state viewport \
|
|||
},
|
||||
textInput {
|
||||
key = "password",
|
||||
initialValue = "",
|
||||
initialValue = testApp.password,
|
||||
initialFocus = False,
|
||||
w = 300,
|
||||
h = 40,
|
||||
|
|
@ -34,7 +36,16 @@ view = state viewport \
|
|||
},
|
||||
Text { content = "Username: " & testApp.email, 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 }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
|
|
|||
45
src/valueToAST.ts
Normal file
45
src/valueToAST.ts
Normal file
|
|
@ -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…
Add table
Add a link
Reference in a new issue