diff --git a/src/ast.ts b/src/ast.ts index 7ffd39e..c1ced95 100644 --- a/src/ast.ts +++ b/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 = diff --git a/src/error.ts b/src/error.ts index 0c81736..e9e14cd 100644 --- a/src/error.ts +++ b/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'); } diff --git a/src/interpreter.ts b/src/interpreter.ts index 3ed55c4..850f4d8 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -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); diff --git a/src/main.ts b/src/main.ts index 90d4211..413665c 100644 --- a/src/main.ts +++ b/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 }); + } + + // 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, { - value, - body: def.body, - dependencies: deps - }); + store.set(name, { value, body: data.body, dependencies: deps }); + } + } } const dependents = buildDependents(store); diff --git a/src/persistence.ts b/src/persistence.ts new file mode 100644 index 0000000..72faebe --- /dev/null +++ b/src/persistence.ts @@ -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 = {}; + for (const [name, entry] of store) { + data[name] = { + body: entry.body, + source: 'file' + }; + } + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); +} + +export function loadStore(): Record | 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); +} diff --git a/src/runtime.ts b/src/runtime.ts index 5d9164f..5fd7932 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -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; } diff --git a/src/textinput-test.cg b/src/textinput-test.cg index ad4eaf4..412c62c 100644 --- a/src/textinput-test.cg +++ b/src/textinput-test.cg @@ -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 } ] } }; diff --git a/src/valueToAST.ts b/src/valueToAST.ts new file mode 100644 index 0000000..dd1992d --- /dev/null +++ b/src/valueToAST.ts @@ -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`); + } +}