Adding builtins as native-functions. desugaring symbols as native function application. except ~ which is the new pipe operator, > is now greater than
This commit is contained in:
parent
5b40e9d298
commit
216fe6bd30
7 changed files with 587 additions and 115 deletions
447
src/builtins.ts
Normal file
447
src/builtins.ts
Normal file
|
|
@ -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 {
|
||||
gap = 20,
|
||||
children = [
|
||||
Text({ content = str(count), x = 0, y = 20 }),
|
||||
Clickable {
|
||||
event = "increment",
|
||||
child = Rect { w = 100, h = 40, color = "blue" }
|
||||
|
|
|
|||
|
|
@ -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 = ['+', '-', '*', '/', '>', '&'];
|
||||
|
||||
if (builtIns.includes(name)) {
|
||||
const argValues = ast.args.map(arg => evaluate(arg, env));
|
||||
|
||||
if (argValues.length !== 2) {
|
||||
throw new Error(`${name} expects 2 args`);
|
||||
}
|
||||
|
||||
return evaluateBuiltIn(name, argValues[0], argValues[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const func = evaluate(ast.func, env);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
74
src/lexer.ts
74
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++;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
10
src/types.ts
10
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue