diff --git a/src/ast.ts b/src/ast.ts index fb028dc..7ffd39e 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -2,7 +2,6 @@ import type { Value } from './types'; // Literals and Variables - export type Literal = { kind: 'literal' value: Value @@ -160,6 +159,15 @@ export type Import = { start: number } +export type Rebind = { + kind: 'rebind' + target: AST + value: AST + line: number + column: number + start: number +} + export type AST = | Literal | Variable @@ -176,6 +184,7 @@ export type AST = | Definition | TypeDef | Import + | Rebind export function prettyPrint(ast: AST, indent = 0): string { const i = ' '.repeat(indent); diff --git a/src/builtins.ts b/src/builtins.ts index b1193fd..79f27f2 100644 --- a/src/builtins.ts +++ b/src/builtins.ts @@ -471,44 +471,5 @@ export const builtins: { [name: string]: Value } = { const l = expectString(label, 'debug'); return value; } - }, - - 'store': { - kind: 'record', - fields: { - 'ref': { - kind: 'native', - name: 'Store.ref', - arity: 1, - fn: (initialValue) => { - return { kind: 'ref', value: initialValue }; - } - }, - - 'get': { - kind: 'native', - name: 'Store.get', - arity: 1, - fn: (ref) => { - if (ref.kind !== 'ref') - throw new Error('get expects a Ref'); - - return ref.value; - } - }, - - 'set': { - kind: 'native', - name: 'Store.set', - arity: 2, - fn: (ref, transformFn) => { - return { - kind: 'constructor', - name: 'Update', - args: [ref, transformFn] - }; - } - } - } } } diff --git a/src/interpreter.ts b/src/interpreter.ts index 3880c4f..2eb0f4b 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -201,6 +201,20 @@ export function evaluate(ast: AST, env: Env, source: string): Value { throw RuntimeError('Non-exhaustive pattern match', ast.line, ast.column, source); } + 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] + }; + } + default: throw RuntimeError('Syntax Error', ast.line, ast.column, source); } diff --git a/src/lexer.ts b/src/lexer.ts index 158d6d6..0789c58 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -25,6 +25,7 @@ export type Token = ( // Symbols | { kind: 'colon' } + | { kind: 'colon-equals' } | { kind: 'semicolon' } | { kind: 'backslash' } | { kind: 'pipe' } @@ -214,7 +215,15 @@ export function tokenize(source: string): Token[] { } break; } - case ':': tokens.push({ kind: 'colon', line: startLine, column: startColumn, start }); break; + case ':': { + if (source[i + 1] === '=') { + tokens.push({ kind: 'colon-equals', line: startLine, column: startColumn, start }); + advance(); + } else { + tokens.push({ kind: 'colon', line: startLine, column: startColumn, start }); + } + break; + } case ';': tokens.push({ kind: 'semicolon', line: startLine, column: startColumn, start }); break; case '\\': tokens.push({ kind: 'backslash', line: startLine, column: startColumn, start }); break; case '~': tokens.push({ kind: 'tilde', line: startLine, column: startColumn, start }); break; diff --git a/src/main.ts b/src/main.ts index 59cfc50..12a3c26 100644 --- a/src/main.ts +++ b/src/main.ts @@ -45,7 +45,7 @@ try { const update = appRecord.fields.update; const view = appRecord.fields.view; - runApp({ init, update, view }, canvas, cgCode); + runApp({ init, update, view }, canvas, cgCode, env); } catch(error) { console.log('CAUGHT ERROR:', error); console.log('Is CGError??', error instanceof CGError); diff --git a/src/parser.ts b/src/parser.ts index 181199a..9b9ec8c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -152,6 +152,14 @@ export class Parser { let expr = this.parseInfix(); + // Rebind + if (this.current().kind == 'colon-equals') { + const token = this.current(); + this.advance(); + const value = this.parseExpression(); + return { kind: 'rebind', target: expr, value, ...this.getPos(token) }; + } + // Match if (this.current().kind === 'pipe') { return this.parseMatch(expr); diff --git a/src/runtime.ts b/src/runtime.ts index fa14471..ba49f63 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -3,6 +3,7 @@ import { valueToUI } from './valueToUI'; import { render, hitTest } from './ui'; import { evaluate } from './interpreter'; import { CGError } from './error'; +import type { Env } from './env'; export type App = { init: Value; @@ -10,7 +11,7 @@ export type App = { view: Value; // State / UI } -export function runApp(app: App, canvas: HTMLCanvasElement, source: string) { +export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env: Env) { let state = app.init; type ComponentInstance = { @@ -221,23 +222,11 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string) { } } - if (event.kind === 'constructor' && event.name === 'Update') { - if (event.args.length === 2) { - const ref = event.args[0]; - const transformFn = event.args[1]; - - if (ref.kind !== 'ref') - throw new Error('Update event expects a Ref') - - if (transformFn.kind !== 'closure') - throw new Error('Update event expects a Ref') - - const callEnv = new Map(transformFn.env); - callEnv.set(transformFn.params[0], ref.value); - const newValue = evaluate(transformFn.body, callEnv, source); - - ref.value = newValue; - + 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; } @@ -275,7 +264,7 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string) { if (hitResult) { const { event, relativeX, relativeY } = hitResult; - if (event.kind === 'constructor' && (event.name === 'Focus' || event.name === 'Update')) { + if (event.kind === 'constructor' && (event.name === 'Focus' || event.name === 'Rebind')) { handleEvent(event); } else if (event.kind === 'constructor' && event.name === 'FocusAndClick') { const eventWithCoords: Value = { diff --git a/src/textinput-test.cg b/src/textinput-test.cg index d979487..447159e 100644 --- a/src/textinput-test.cg +++ b/src/textinput-test.cg @@ -1,14 +1,12 @@ init = {}; -email = store.ref ""; -password = store.ref ""; +email = ""; +password = ""; update = state event \ event | _ \ state; view = state viewport \ - emailText = store.get email; - Positioned { x = 30, y = 30, @@ -21,7 +19,7 @@ view = state viewport \ initialFocus = True, w = 300, h = 40, - onChange = text \ store.set email (a \ text) + onChange = text \ email := text }, textInput { key = "password", @@ -29,10 +27,10 @@ view = state viewport \ initialFocus = False, w = 300, h = 40, - onChange = text \ store.set password (a \ text) + onChange = text \ password := text }, - Text { content = "Username: " & emailText, x = 8, y = 16 }, - Text { content = "Password: " & store.get password, x = 8, y = 16 } + Text { content = "Username: " & email, x = 8, y = 16 }, + Text { content = "Password: " & password, x = 8, y = 16 } ] } }; diff --git a/src/types.ts b/src/types.ts index b86b7ed..b62a5cb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,10 +46,6 @@ export type NativeFunction = { fn: (...args: Value[]) => Value } -export type RefValue = { - kind: 'ref' - value: Value -} export type UIValue = | { kind: 'rect', w: number, h: number, color: string, radius?: number } @@ -64,4 +60,4 @@ export type UIValue = | { kind: 'stack', children: UIValue[] } | { kind: 'stateful', key: string, focusable: boolean, init: Value, update: Value, view: Value } -export type Value = IntValue | FloatValue | StringValue | Closure | ListValue | RecordValue | ConstructorValue | NativeFunction | RefValue; +export type Value = IntValue | FloatValue | StringValue | Closure | ListValue | RecordValue | ConstructorValue | NativeFunction;