diff --git a/src/builtins.ts b/src/builtins.ts index 79f27f2..5128d7b 100644 --- a/src/builtins.ts +++ b/src/builtins.ts @@ -1,4 +1,4 @@ -import type { Value, NativeFunction } from './types' +import type { Value } from './types' const measureCanvas = document.createElement('canvas'); const measureCtx = measureCanvas.getContext('2d')!; @@ -468,7 +468,7 @@ export const builtins: { [name: string]: Value } = { name: 'debug', arity: 2, fn: (label, value) => { - const l = expectString(label, 'debug'); + expectString(label, 'debug'); return value; } } diff --git a/src/interpreter.ts b/src/interpreter.ts index 2eb0f4b..a87b424 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -202,17 +202,49 @@ export function evaluate(ast: AST, env: Env, source: string): Value { } case 'rebind': { - if (ast.target.kind !== 'variable') - throw new Error('Rebind target must be a variable'); - - const name = ast.target.name; const value = evaluate(ast.value, env, source); - return { - kind: 'constructor', - name: 'Rebind', - args: [{ kind: 'string', value: name }, value] - }; + if (ast.target.kind === 'variable') { + const name = ast.target.name; + return { + kind: 'constructor', + name: 'Rebind', + args: [{ kind: 'string', value: name }, value] + }; + } + + if (ast.target.kind === 'record-access') { + let current: AST = ast.target; + const path: string[] = []; + + while (current.kind === 'record-access') { + path.unshift(current.field); + current = current.record; + } + + if (current.kind !== 'variable') + throw RuntimeError('Rebind target must be a variable or field access', ast.line, ast.column, source); + + const rootName = current.name; + const rootValue = env.get(rootName); + + if (!rootValue) + throw RuntimeError(`Unknown variable: ${rootName}`, ast.line, ast.column, source); + + // const newRoot = updatePath(rootValue, path, value, ast.line, ast.column, source); + + return { + kind: 'constructor', + name: 'Rebind', + args: [ + { kind: 'string', value: rootName }, + { kind: 'list', elements: path.map(p => ({ kind: 'string', value: p })) }, + value + ] + }; + } + + throw RuntimeError('Rebind target must be a variable or field access', ast.line, ast.column, source); } default: @@ -297,5 +329,4 @@ function matchPattern(value: Value, pattern: Pattern): Bindings | null { return bindings; } } - } diff --git a/src/runtime.ts b/src/runtime.ts index ba49f63..aaaf284 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -187,7 +187,7 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env: }; try { - const callEnv = new Map(app.view.env); + const callEnv = new Map(env); callEnv.set(app.view.params[0], state); callEnv.set(app.view.params[1], viewport); const uiValue = evaluate(app.view.body, callEnv, source); @@ -223,13 +223,25 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env: } if (event.kind === 'constructor' && event.name === 'Rebind') { - if (event.args.length === 2 && event.args[0].kind === 'string') { - const name = event.args[0].value; - const value = event.args[1]; - env.set(name, value); - rerender(); - return; + if (event.args[0].kind !== 'string') return; + const name = event.args[0].value; + + if (event.args.length === 2) { + // Rebind "name" value + env.set(name, event.args[1]); + } else if (event.args.length === 3 && event.args[1].kind === 'list') { + // Rebind "name" ["path"] + const pathList = (event.args[1] as { elements: Value[] }); + const path = pathList.elements.map((e: Value) => e.kind === 'string' ? e.value : ''); + const currentValue = env.get(name); + if (currentValue) { + const newValue = updatePath(currentValue, path, event.args[2]); + env.set(name, newValue); + } } + + rerender(); + return; } if (app.update.kind !== 'closure') @@ -321,3 +333,19 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env: rerender(); } + +function updatePath(obj: Value, path: string[], value: Value): Value { + if (path.length === 0) return value; + + if (obj.kind !== 'record') + throw new Error('Cannot access field on non-record'); + + const [field, ...rest] = path; + return { + kind: 'record', + fields: { + ...obj.fields, + [field]: updatePath(obj.fields[field], rest, value) + } + }; +} diff --git a/src/textinput-test.cg b/src/textinput-test.cg index 447159e..a6d85f0 100644 --- a/src/textinput-test.cg +++ b/src/textinput-test.cg @@ -1,7 +1,9 @@ init = {}; -email = ""; -password = ""; +testApp = { + email = "", + password = "" +}; update = state event \ event | _ \ state; @@ -19,7 +21,7 @@ view = state viewport \ initialFocus = True, w = 300, h = 40, - onChange = text \ email := text + onChange = text \ testApp.email := text }, textInput { key = "password", @@ -27,10 +29,10 @@ view = state viewport \ initialFocus = False, w = 300, h = 40, - onChange = text \ password := text + onChange = text \ testApp.password := text }, - Text { content = "Username: " & email, x = 8, y = 16 }, - Text { content = "Password: " & password, x = 8, y = 16 } + Text { content = "Username: " & testApp.email, x = 8, y = 16 }, + Text { content = "Password: " & testApp.password, x = 8, y = 16 } ] } };