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 = {
|
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 =
|
||||||
|
|
|
||||||
20
src/error.ts
20
src/error.ts
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
29
src/main.ts
29
src/main.ts
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
|
||||||
store.set(def.name, {
|
// Load persisted store entries
|
||||||
value,
|
if (persisted) {
|
||||||
body: def.body,
|
for (const [name, data] of Object.entries(persisted)) {
|
||||||
dependencies: deps
|
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);
|
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 { 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 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
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