Adding builtins as native-functions. desugaring symbols as native function application. except ~ which is the new pipe operator, > is now greater than

master
Dustin Swan 4 days ago
parent 5b40e9d298
commit 216fe6bd30
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -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 };
}
},
}

@ -6,6 +6,7 @@ view = count \
Column { Column {
gap = 20, gap = 20,
children = [ children = [
Text({ content = str(count), x = 0, y = 20 }),
Clickable { Clickable {
event = "increment", event = "increment",
child = Rect { w = 100, h = 40, color = "blue" } child = Rect { w = 100, h = 40, color = "blue" }

@ -87,23 +87,33 @@ export function evaluate(ast: AST, env: Env): Value {
} }
case 'apply': { case 'apply': {
// Operators const func = evaluate(ast.func, env);
if (ast.func.kind === 'variable') {
const name = ast.func.name;
const builtIns = ['+', '-', '*', '/', '>', '&'];
if (builtIns.includes(name)) {
const argValues = ast.args.map(arg => evaluate(arg, env)); const argValues = ast.args.map(arg => evaluate(arg, env));
if (argValues.length !== 2) { // Native functions
throw new Error(`${name} expects 2 args`); if (func.kind === 'native') {
// Exact args
if (argValues.length === func.arity) {
return func.fn(...argValues);
} }
return evaluateBuiltIn(name, argValues[0], argValues[1]); // 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);
} }
};
} }
const func = evaluate(ast.func, env); throw new Error(`Function expects ${func.arity} args, but got ${argValues.length}`);
}
// Constructor application // Constructor application
if (func.kind === 'constructor') { if (func.kind === 'constructor') {
@ -118,8 +128,6 @@ export function evaluate(ast: AST, env: Env): Value {
if (func.kind !== 'closure') if (func.kind !== 'closure')
throw new Error('Not a function'); throw new Error('Not a function');
const argValues = ast.args.map(arg => evaluate(arg, env));
// Too few args (Currying) // Too few args (Currying)
if (argValues.length < func.params.length) { if (argValues.length < func.params.length) {
// Bind the params we have // 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 }; type Bindings = { [key: string]: Value };
function matchPattern(value: Value, pattern: Pattern): Bindings | null { function matchPattern(value: Value, pattern: Pattern): Bindings | null {

@ -14,13 +14,21 @@ export type Token =
| { kind: 'open-bracket' } | { kind: 'open-bracket' }
| { kind: 'close-bracket' } | { kind: 'close-bracket' }
// Symbols // Comparison
| { kind: 'equals' } | { kind: 'equals' }
| { kind: 'equals-equals' }
| { kind: 'not-equals' }
| { kind: 'greater-than' }
| { kind: 'greater-equals' }
| { kind: 'less-than' }
| { kind: 'less-equals' }
// Symbols
| { kind: 'colon' } | { kind: 'colon' }
| { kind: 'semicolon' } | { kind: 'semicolon' }
| { kind: 'backslash' } | { kind: 'backslash' }
| { kind: 'pipe' } | { kind: 'pipe' }
| { kind: 'greater-than' } | { kind: 'tilde' }
| { kind: 'comma' } | { kind: 'comma' }
| { kind: 'ampersand' } | { kind: 'ampersand' }
| { kind: 'underscore' } | { kind: 'underscore' }
@ -32,6 +40,8 @@ export type Token =
| { kind: 'minus' } | { kind: 'minus' }
| { kind: 'star' } | { kind: 'star' }
| { kind: 'slash' } | { kind: 'slash' }
| { kind: 'percent' }
| { kind: 'caret' }
| { kind: 'eof' } | { kind: 'eof' }
@ -137,25 +147,49 @@ export function tokenize(source: string): Token[] {
} }
switch (char) { switch (char) {
// Brackets case '>': {
case '(': tokens.push({ kind: 'open-paren' }); break; if (source[i + 1] == '=') {
case ')': tokens.push({ kind: 'close-paren' }); break; tokens.push({ kind: 'greater-equals' });
case '{': tokens.push({ kind: 'open-brace' }); break; i++;
case '}': tokens.push({ kind: 'close-brace' }); break; } else {
case '[': tokens.push({ kind: 'open-bracket' }); break; tokens.push({ kind: 'greater-than' });
case ']': tokens.push({ kind: 'close-bracket' }); break; }
break;
// Symbols }
case '=': tokens.push({ kind: 'equals' }); 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: 'colon' }); break;
case ';': tokens.push({ kind: 'semicolon' }); break; case ';': tokens.push({ kind: 'semicolon' }); break;
case '\\': tokens.push({ kind: 'backslash' }); break; case '\\': tokens.push({ kind: 'backslash' }); break;
case '~': tokens.push({ kind: 'tilde' }); break;
case '|': tokens.push({ kind: 'pipe' }); 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: 'comma' }); break;
case '&': tokens.push({ kind: 'ampersand' }); break; case '&': tokens.push({ kind: 'ampersand' }); break;
case '_': tokens.push({ kind: 'underscore' }); break;
case '.': tokens.push({ kind: 'dot' }); break; case '.': tokens.push({ kind: 'dot' }); break;
case '@': tokens.push({ kind: 'at' }); 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: 'minus' }); break;
case '*': tokens.push({ kind: 'star' }); break; case '*': tokens.push({ kind: 'star' }); break;
case '/': tokens.push({ kind: 'slash' }); 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++; i++;

@ -4,6 +4,7 @@ import { tokenize } from './lexer'
import { Parser } from './parser' import { Parser } from './parser'
import cgCode from './counter.cg?raw'; import cgCode from './counter.cg?raw';
import { runApp } from './runtime'; import { runApp } from './runtime';
import { builtins } from './builtins';
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = 800; canvas.width = 800;
@ -15,7 +16,7 @@ const parser = new Parser(tokens);
const ast = parser.parse(); const ast = parser.parse();
console.log(ast); console.log(ast);
const env: Env = new Map(); const env: Env = new Map(Object.entries(builtins));
const appRecord = evaluate(ast, env); const appRecord = evaluate(ast, env);
console.log(appRecord); console.log(appRecord);

@ -43,18 +43,34 @@ export class Parser {
const kind = this.current().kind; const kind = this.current().kind;
return kind === 'plus' || kind === 'minus' || return kind === 'plus' || kind === 'minus' ||
kind === 'star' || kind === 'slash' || 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 { private tokenToOpName(token: Token): string {
switch (token.kind) { switch (token.kind) {
case 'plus': return '+'; case 'ampersand': return 'cat';
case 'minus': return '-';
case 'star': return '*'; // Arithmetic
case 'slash': return '/'; case 'plus': return 'add';
case 'greater-than': return '>'; case 'minus': return 'sub';
case 'ampersand': return '&'; 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}`); default: throw new Error(`Not an operator: ${token.kind}`);
} }
} }
@ -246,6 +262,18 @@ export class Parser {
while (this.isInfixOp()) { while (this.isInfixOp()) {
const opToken = this.advance(); const opToken = this.advance();
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 opName = this.tokenToOpName(opToken);
const right = this.parseApplication(); const right = this.parseApplication();
@ -255,6 +283,7 @@ export class Parser {
args: [left, right] args: [left, right]
} }
} }
}
return left; return left;
} }

@ -39,6 +39,14 @@ export type ConstructorValue = {
args: Value[] args: Value[]
} }
export type NativeFunction = {
kind: 'native'
name: string
arity: number
fn: (...args: Value[]) => Value
}
export type UIValue = export type UIValue =
| { kind: 'rect', w: number, h: number, color: string } | { kind: 'rect', w: number, h: number, color: string }
| { kind: 'text', content: string, x: number, y: number } | { kind: 'text', content: string, x: number, y: number }
@ -47,4 +55,4 @@ export type UIValue =
| { kind: 'clickable', child: UIValue, event: string } | { kind: 'clickable', child: UIValue, event: string }
| { kind: 'padding', child: UIValue, amount: number } | { 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;

Loading…
Cancel
Save