From 216fe6bd30f14e1a1be0e18d3cc0f1448af56c80 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Mon, 2 Feb 2026 20:10:32 -0700 Subject: [PATCH] Adding builtins as native-functions. desugaring symbols as native function application. except ~ which is the new pipe operator, > is now greater than --- src/builtins.ts | 447 +++++++++++++++++++++++++++++++++++++++++++++ src/counter.cg | 1 + src/interpreter.ts | 102 +++-------- src/lexer.ts | 74 ++++++-- src/main.ts | 3 +- src/parser.ts | 57 ++++-- src/types.ts | 10 +- 7 files changed, 583 insertions(+), 111 deletions(-) create mode 100644 src/builtins.ts diff --git a/src/builtins.ts b/src/builtins.ts new file mode 100644 index 0000000..7294c65 --- /dev/null +++ b/src/builtins.ts @@ -0,0 +1,447 @@ +import type { Value, NativeFunction } from './types' + +function expectInt(v: Value, name: string): number { + if (v.kind !== 'int') + throw new Error(`${name} expects int, got ${v.kind}`); + return v.value; +} + +// function expectFloat(v: Value, name: string): number { +// if (v.kind !== 'float') +// throw new Error(`${name} expects float, got ${v.kind}`); +// return v.value; +// } + +function expectNumber(v: Value, name: string): number { + if (v.kind !== 'float' && v.kind !== 'int') + throw new Error(`${name} expects number, got ${v.kind}`); + return v.value; +} + +// function expectString(v: Value, name: string): string { +// if (v.kind !== 'string') +// throw new Error(`${name} expects string, got ${v.kind}`); +// return v.value; +// } + +// function expectList(v: Value, name: string): Value[] { +// if (v.kind !== 'list') +// throw new Error(`${name} expects list, got ${v.kind}`); +// return v.elements; +// } + +export const builtins: { [name: string]: NativeFunction } = { + // Arithmetic + 'add': { + kind: 'native', + name: 'add', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'add'); + const y = expectNumber(b, 'add'); + return { kind: 'int', value: x + y }; + } + }, + + 'sub': { + kind: 'native', + name: 'sub', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'sub'); + const y = expectNumber(b, 'sub'); + return { kind: 'int', value: x - y }; + } + }, + + 'mul': { + kind: 'native', + name: 'mul', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'mul'); + const y = expectNumber(b, 'mul'); + return { kind: 'int', value: x * y }; + } + }, + + 'div': { + kind: 'native', + name: 'div', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'div'); + const y = expectNumber(b, 'div'); + return { kind: 'int', value: Math.floor(x / y) }; + } + }, + + 'mod': { + kind: 'native', + name: 'mod', + arity: 2, + fn: (a, b) => { + const x = expectInt(a, 'mod'); + const y = expectInt(b, 'mod'); + return { kind: 'int', value: x % y }; + } + }, + + 'pow': { + kind: 'native', + name: 'pow', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'add'); + const y = expectNumber(b, 'add'); + return { kind: 'int', value: Math.pow(x, y) }; + } + }, + + // Comparison + 'eq': { + kind: 'native', + name: 'eq', + arity: 2, + fn: (a, b) => { + return { + kind: 'constructor', + name: JSON.stringify(a) === JSON.stringify(b) ? 'True' : 'False', + args: [] + }; + } + }, + + 'neq': { + kind: 'native', + name: 'eq', + arity: 2, + fn: (a, b) => { + return { + kind: 'constructor', + name: JSON.stringify(a) !== JSON.stringify(b) ? 'True' : 'False', + args: [] + }; + } + }, + + 'lt': { + kind: 'native', + name: 'lt', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'lt'); + const y = expectNumber(b, 'lt'); + return { + kind: 'constructor', + name: x < y ? 'True' : 'False', + args: [] + }; + } + }, + + 'gt': { + kind: 'native', + name: 'gt', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'gt'); + const y = expectNumber(b, 'gt'); + return { + kind: 'constructor', + name: x > y ? 'True' : 'False', + args: [] + }; + } + }, + + 'lte': { + kind: 'native', + name: 'lt', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'lte'); + const y = expectNumber(b, 'lt'); + return { + kind: 'constructor', + name: x <= y ? 'True' : 'False', + args: [] + }; + } + }, + + 'gte': { + kind: 'native', + name: 'gte', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'gte'); + const y = expectNumber(b, 'gte'); + return { + kind: 'constructor', + name: x >= y ? 'True' : 'False', + args: [] + }; + } + }, + + // String & List + 'cat': { + kind: 'native', + name: 'cat', + arity: 2, + fn: (a, b) => { + if (a.kind === 'string' && b.kind === 'string') { + return { kind: 'string', value: a.value + b.value }; + } + if (a.kind === 'list' && b.kind === 'list') { + return { kind: 'list', elements: [...a.elements, ...b.elements] }; + } + throw new Error('cat requires 2 lists or 2 strings'); + } + }, + + 'len': { + kind: 'native', + name: 'len', + arity: 1, + fn: (seq) => { + if (seq.kind === 'string') { + return { kind: 'int', value: seq.value.length }; + } + if (seq.kind === 'list') { + return { kind: 'int', value: seq.elements.length }; + } + throw new Error('cat requires a list or a string'); + } + }, + + 'at': { + kind: 'native', + name: 'at', + arity: 2, + fn: (seq, idx) => { + const i = expectInt(idx, 'at'); + + if (seq.kind === 'string') { + return { kind: 'string', value: seq.value[i] || '' }; + } + if (seq.kind === 'list') { + return seq.elements[i]; + } + throw new Error('at requires a list or a string'); + } + }, + + 'slice': { + kind: 'native', + name: 'slice', + arity: 3, + fn: (seq, start, end) => { + const s = expectInt(start, 'slice'); + const e = expectInt(end, 'slice'); + + if (seq.kind === 'string') { + return { kind: 'string', value: seq.value.slice(s, e) }; + } + if (seq.kind === 'list') { + return { kind: 'list', elements: seq.elements.slice(s, e) }; + } + throw new Error('slice requires a list or a string'); + } + }, + + 'head': { + kind: 'native', + name: 'head', + arity: 1, + fn: (seq) => { + if (seq.kind === 'string') { + if (seq.value.length === 0) { + return { kind: 'constructor', name: 'None', args: [] }; + } + return { kind: 'constructor', name: 'Some', args: [{ kind: 'string', value: seq.value[0] }] }; + } + if (seq.kind === 'list') { + if (seq.elements.length === 0) { + return { kind: 'constructor', name: 'None', args: [] }; + } + return { kind: 'constructor', name: 'Some', args: [seq.elements[0]] }; + } + throw new Error('head requires a list or a string'); + } + }, + + 'tail': { + kind: 'native', + name: 'tail', + arity: 1, + fn: (seq) => { + if (seq.kind === 'string') { + if (seq.value.length === 0) { + return { kind: 'constructor', name: 'None', args: [] }; + } + return { kind: 'constructor', name: 'Some', args: [{ kind: 'string', value: seq.value.slice(1) }] }; + } + if (seq.kind === 'list') { + if (seq.elements.length === 0) { + return { kind: 'constructor', name: 'None', args: [] }; + } + return { kind: 'constructor', name: 'Some', args: [{ kind: 'list', elements: seq.elements.slice(1) }] }; + } + throw new Error('tail requires a list or a string'); + } + }, + + // Types + 'str': { + kind: 'native', + name: 'str', + arity: 1, + fn: (val) => { + if (val.kind === 'int' || val.kind === 'float') + return { kind: 'string', value: val.value.toString() } + + if (val.kind === 'string') + return val; + + throw new Error('str: cannot convert to string'); + } + }, + + 'int': { + kind: 'native', + name: 'int', + arity: 1, + fn: (val) => { + if (val.kind === 'int') + return val; + + if (val.kind === 'float') + return { kind: 'int', value: Math.floor(val.value) }; + + if (val.kind === 'string') { + const parsed = parseInt(val.value, 10); + + if (isNaN(parsed)) + throw new Error(`int: cannot parse "${val.value}"`); + + return { kind: 'int', value: parsed } + } + + throw new Error(`int: cannot convert to int`); + } + }, + + 'float': { + kind: 'native', + name: 'float', + arity: 1, + fn: (val) => { + if (val.kind === 'float') + return val; + + if (val.kind === 'int') + return { kind: 'float', value: val.value }; + + if (val.kind === 'string') { + const parsed = parseFloat(val.value, 10); + + if (isNaN(parsed)) + throw new Error(`float: cannot parse "${val.value}"`); + + return { kind: 'float', value: parsed } + } + + throw new Error(`float: cannot convert to float`); + } + }, + + // Math + 'sqrt': { + kind: 'native', + name: 'sqrt', + arity: 1, + fn: (val) => { + const x = expectNumber(val, 'sqrt'); + return { kind: 'float', value: Math.sqrt(x) }; + } + }, + + 'abs': { + kind: 'native', + name: 'abs', + arity: 1, + fn: (val) => { + if (val.kind === 'int') + return { kind: 'int', value: Math.abs(val.value) }; + + if (val.kind === 'float') + return { kind: 'float', value: Math.abs(val.value) }; + + throw new Error('abs expects a number'); + } + }, + + 'floor': { + kind: 'native', + name: 'floor', + arity: 1, + fn: (val) => { + const x = expectNumber(val, 'floor'); + return { kind: 'int', value: Math.floor(x) }; + } + }, + + 'ceil': { + kind: 'native', + name: 'ceil', + arity: 1, + fn: (val) => { + const x = expectNumber(val, 'ceil'); + return { kind: 'int', value: Math.ceil(x) }; + } + }, + + 'round': { + kind: 'native', + name: 'round', + arity: 1, + fn: (val) => { + const x = expectNumber(val, 'round'); + return { kind: 'int', value: Math.round(x) }; + } + }, + + 'min': { + kind: 'native', + name: 'min', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'min'); + const y = expectNumber(b, 'min'); + const result = Math.min(x, y); + + if (a.kind === 'float' || b.kind === 'float') + return { kind: 'float', value: result }; + + return { kind: 'int', value: result }; + } + }, + + 'max': { + kind: 'native', + name: 'max', + arity: 2, + fn: (a, b) => { + const x = expectNumber(a, 'max'); + const y = expectNumber(b, 'max'); + const result = Math.max(x, y); + + if (a.kind === 'float' || b.kind === 'float') + return { kind: 'float', value: result }; + + return { kind: 'int', value: result }; + } + }, +} diff --git a/src/counter.cg b/src/counter.cg index 33ada62..6acacf1 100644 --- a/src/counter.cg +++ b/src/counter.cg @@ -6,6 +6,7 @@ view = count \ Column { gap = 20, children = [ + Text({ content = str(count), x = 0, y = 20 }), Clickable { event = "increment", child = Rect { w = 100, h = 40, color = "blue" } diff --git a/src/interpreter.ts b/src/interpreter.ts index 684c4d8..523da37 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -87,23 +87,33 @@ export function evaluate(ast: AST, env: Env): Value { } case 'apply': { - // Operators - if (ast.func.kind === 'variable') { - const name = ast.func.name; - const builtIns = ['+', '-', '*', '/', '>', '&']; + const func = evaluate(ast.func, env); + const argValues = ast.args.map(arg => evaluate(arg, env)); - if (builtIns.includes(name)) { - const argValues = ast.args.map(arg => evaluate(arg, env)); + // Native functions + if (func.kind === 'native') { + // Exact args + if (argValues.length === func.arity) { + return func.fn(...argValues); + } - if (argValues.length !== 2) { - throw new Error(`${name} expects 2 args`); - } + // Partial application + if (argValues.length < func.arity) { + const capturedArgs = argValues; + + return { + kind: 'native', + name: func.name, + arity: func.arity - argValues.length, + fn: (...restArgs: Value[]) => { + return func.fn(...capturedArgs, ...restArgs); - return evaluateBuiltIn(name, argValues[0], argValues[1]); + } + }; } - } - const func = evaluate(ast.func, env); + throw new Error(`Function expects ${func.arity} args, but got ${argValues.length}`); + } // Constructor application if (func.kind === 'constructor') { @@ -118,8 +128,6 @@ export function evaluate(ast: AST, env: Env): Value { if (func.kind !== 'closure') throw new Error('Not a function'); - const argValues = ast.args.map(arg => evaluate(arg, env)); - // Too few args (Currying) if (argValues.length < func.params.length) { // Bind the params we have @@ -173,72 +181,6 @@ export function evaluate(ast: AST, env: Env): Value { } } -function evaluateBinaryOp(op: string, left: Value, right: Value): Value { - const leftKind = left.kind; - const rightKind = right.kind; - - const bothNumbers = ((leftKind === 'int' || leftKind === 'float')) && ((rightKind === 'int' || rightKind === 'float')); - if (!bothNumbers) - throw new Error(`Not numbers: ${left}, ${right}`); - - const leftValue = left.value; - const rightValue = right.value; - - switch (op) { - case '+': - return { value: leftValue + rightValue, kind: 'int' }; - - case '-': - return { value: leftValue - rightValue, kind: 'int' }; - - case '*': - return { value: leftValue * rightValue, kind: 'int' } - - case '/': - return { value: leftValue / rightValue, kind: 'int' } - - default: - throw new Error(`Unknown operation: ${op}`); - } -} - -function evaluateBuiltIn(op: string, left: Value, right: Value): Value { - if (op === '+' || op === '-' || op === '*' || op === '/') { - return evaluateBinaryOp(op, left, right); - } - - if (op === '>') { - // x > f means f(x) - if (right.kind !== 'closure') - throw new Error('Right side of > must be a function'); - - if (right.params.length !== 1) - throw new Error('Pipe only works with 1-arg functions for now..'); - - const callEnv = new Map(right.env); - callEnv.set(right.params[0], left); - return evaluate(right.body, callEnv); - } - - if (op === '&') { - if (left.kind == 'list' && right.kind === 'list') { - return { - kind: 'list', - elements: [...left.elements, ...right.elements] - }; - } - - if (left.kind == 'string' && right.kind === 'string') { - return { - kind: 'string', - value: left.value + right.value - }; - } - } - - throw new Error(`Unknown built-in: ${op}`); -} - type Bindings = { [key: string]: Value }; function matchPattern(value: Value, pattern: Pattern): Bindings | null { diff --git a/src/lexer.ts b/src/lexer.ts index 86a5131..f94d030 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -14,13 +14,21 @@ export type Token = | { kind: 'open-bracket' } | { kind: 'close-bracket' } - // Symbols + // Comparison | { kind: 'equals' } + | { kind: 'equals-equals' } + | { kind: 'not-equals' } + | { kind: 'greater-than' } + | { kind: 'greater-equals' } + | { kind: 'less-than' } + | { kind: 'less-equals' } + + // Symbols | { kind: 'colon' } | { kind: 'semicolon' } | { kind: 'backslash' } | { kind: 'pipe' } - | { kind: 'greater-than' } + | { kind: 'tilde' } | { kind: 'comma' } | { kind: 'ampersand' } | { kind: 'underscore' } @@ -32,6 +40,8 @@ export type Token = | { kind: 'minus' } | { kind: 'star' } | { kind: 'slash' } + | { kind: 'percent' } + | { kind: 'caret' } | { kind: 'eof' } @@ -137,25 +147,49 @@ export function tokenize(source: string): Token[] { } switch (char) { - // Brackets - case '(': tokens.push({ kind: 'open-paren' }); break; - case ')': tokens.push({ kind: 'close-paren' }); break; - case '{': tokens.push({ kind: 'open-brace' }); break; - case '}': tokens.push({ kind: 'close-brace' }); break; - case '[': tokens.push({ kind: 'open-bracket' }); break; - case ']': tokens.push({ kind: 'close-bracket' }); break; - - // Symbols - case '=': tokens.push({ kind: 'equals' }); break; + case '>': { + if (source[i + 1] == '=') { + tokens.push({ kind: 'greater-equals' }); + i++; + } else { + tokens.push({ kind: 'greater-than' }); + } + break; + } + case '<': { + if (source[i + 1] == '=') { + tokens.push({ kind: 'less-equals' }); + i++; + } else { + tokens.push({ kind: 'less-than' }); + } + break; + } + case '=': { + if (source[i + 1] == '=') { + tokens.push({ kind: 'equals-equals' }); + i++; + } else { + tokens.push({ kind: 'equals' }); + } + break; + } + case '!': { + if (source[i + 1] == '=') { + tokens.push({ kind: 'not-equals' }); + i++; + } else { + throw new Error(`Unexpected character: ${char}`) + } + break; + } case ':': tokens.push({ kind: 'colon' }); break; case ';': tokens.push({ kind: 'semicolon' }); break; case '\\': tokens.push({ kind: 'backslash' }); break; + case '~': tokens.push({ kind: 'tilde' }); break; case '|': tokens.push({ kind: 'pipe' }); break; - // case '<': tokens.push({ kind: 'less-than' }); break; - case '>': tokens.push({ kind: 'greater-than' }); break; case ',': tokens.push({ kind: 'comma' }); break; case '&': tokens.push({ kind: 'ampersand' }); break; - case '_': tokens.push({ kind: 'underscore' }); break; case '.': tokens.push({ kind: 'dot' }); break; case '@': tokens.push({ kind: 'at' }); break; @@ -164,6 +198,16 @@ export function tokenize(source: string): Token[] { case '-': tokens.push({ kind: 'minus' }); break; case '*': tokens.push({ kind: 'star' }); break; case '/': tokens.push({ kind: 'slash' }); break; + case '^': tokens.push({ kind: 'caret' }); break; + case '%': tokens.push({ kind: 'percent' }); break; + + // Brackets + case '(': tokens.push({ kind: 'open-paren' }); break; + case ')': tokens.push({ kind: 'close-paren' }); break; + case '{': tokens.push({ kind: 'open-brace' }); break; + case '}': tokens.push({ kind: 'close-brace' }); break; + case '[': tokens.push({ kind: 'open-bracket' }); break; + case ']': tokens.push({ kind: 'close-bracket' }); break; } i++; diff --git a/src/main.ts b/src/main.ts index f055548..543aa7b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { tokenize } from './lexer' import { Parser } from './parser' import cgCode from './counter.cg?raw'; import { runApp } from './runtime'; +import { builtins } from './builtins'; const canvas = document.createElement('canvas'); canvas.width = 800; @@ -15,7 +16,7 @@ const parser = new Parser(tokens); const ast = parser.parse(); console.log(ast); -const env: Env = new Map(); +const env: Env = new Map(Object.entries(builtins)); const appRecord = evaluate(ast, env); console.log(appRecord); diff --git a/src/parser.ts b/src/parser.ts index a5bafb0..bec33af 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -43,18 +43,34 @@ export class Parser { const kind = this.current().kind; return kind === 'plus' || kind === 'minus' || kind === 'star' || kind === 'slash' || - kind === 'greater-than' || kind === 'ampersand'; - + kind === 'percent' || kind === 'caret' || + kind === 'ampersand' || + kind === 'equals-equals' || kind === 'not-equals' || + kind === 'less-than' || kind === 'less-equals' || + kind === 'greater-than' || kind === 'greater-equals' || + kind === 'tilde'; } private tokenToOpName(token: Token): string { switch (token.kind) { - case 'plus': return '+'; - case 'minus': return '-'; - case 'star': return '*'; - case 'slash': return '/'; - case 'greater-than': return '>'; - case 'ampersand': return '&'; + case 'ampersand': return 'cat'; + + // Arithmetic + case 'plus': return 'add'; + case 'minus': return 'sub'; + case 'star': return 'mul'; + case 'slash': return 'div'; + case 'percent': return 'mod'; + case 'caret': return 'pow'; + + // Comparison + case 'equals-equals': return 'eq'; + case 'not-equals': return 'neq'; + case 'greater-than': return 'gt'; + case 'greater-equals': return 'gte'; + case 'less-than': return 'lt'; + case 'less-equals': return 'lte'; + default: throw new Error(`Not an operator: ${token.kind}`); } } @@ -246,13 +262,26 @@ export class Parser { while (this.isInfixOp()) { const opToken = this.advance(); - const opName = this.tokenToOpName(opToken); - const right = this.parseApplication(); - left = { - kind: 'apply', - func: { kind: 'variable', name: opName }, - args: [left, right] + if (opToken.kind === 'tilde') { + // function application operator + const right = this.parseApplication(); + + left = { + kind: 'apply', + func: right, + args: [left] + }; + } else { + // operators desugar to function calls + const opName = this.tokenToOpName(opToken); + const right = this.parseApplication(); + + left = { + kind: 'apply', + func: { kind: 'variable', name: opName }, + args: [left, right] + } } } diff --git a/src/types.ts b/src/types.ts index 195ab80..c2d3a99 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,6 +39,14 @@ export type ConstructorValue = { args: Value[] } +export type NativeFunction = { + kind: 'native' + name: string + arity: number + fn: (...args: Value[]) => Value + +} + export type UIValue = | { kind: 'rect', w: number, h: number, color: string } | { kind: 'text', content: string, x: number, y: number } @@ -47,4 +55,4 @@ export type UIValue = | { kind: 'clickable', child: UIValue, event: string } | { kind: 'padding', child: UIValue, amount: number } -export type Value = IntValue | FloatValue | StringValue | Closure | ListValue | RecordValue | ConstructorValue; +export type Value = IntValue | FloatValue | StringValue | Closure | ListValue | RecordValue | ConstructorValue | NativeFunction;