Better errors!

master
Dustin Swan 2 days ago
parent c44f06268f
commit 9d1b079361
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -5,16 +5,25 @@ import type { Value } from './types';
export type Literal = { export type Literal = {
kind: 'literal' kind: 'literal'
value: Value value: Value
line: number
column: number
start: number
} }
export type Variable = { export type Variable = {
kind: 'variable' kind: 'variable'
name: string name: string
line: number
column: number
start: number
} }
export type Constructor = { export type Constructor = {
kind: 'constructor' kind: 'constructor'
name: string name: string
line: number
column: number
start: number
} }
// Functions // Functions
@ -23,12 +32,18 @@ export type Lambda = {
kind: 'lambda' kind: 'lambda'
params: string[] params: string[]
body: AST body: AST
line: number
column: number
start: number
} }
export type Apply = { export type Apply = {
kind: 'apply' kind: 'apply'
func: AST func: AST
args: AST[] args: AST[]
line: number
column: number
start: number
} }
// Bindings // Bindings
@ -38,6 +53,9 @@ export type Let = {
name: string name: string
value: AST value: AST
body: AST body: AST
line: number
column: number
start: number
} }
// Matching // Matching
@ -46,11 +64,17 @@ export type Match = {
kind: 'match' kind: 'match'
expr: AST expr: AST
cases: MatchCase[] cases: MatchCase[]
line: number
column: number
start: number
} }
export type MatchCase = { export type MatchCase = {
pattern: Pattern pattern: Pattern
result: AST result: AST
line: number
column: number
start: number
} }
export type Pattern = export type Pattern =
@ -64,26 +88,46 @@ export type Pattern =
// Data Structures // Data Structures
export type ListSpread = {
kind: 'list-spread'
spread: AST;
line: number;
column: number;
start: number;
}
export type List = { export type List = {
kind: 'list' kind: 'list'
elements: (AST | { spread: AST })[] elements: (AST | ListSpread)[]
line: number
column: number
start: number
} }
export type Record = { export type Record = {
kind: 'record' kind: 'record'
fields: { [key: string]: AST } fields: { [key: string]: AST }
line: number
column: number
start: number
} }
export type RecordAccess = { export type RecordAccess = {
kind: 'record-access' kind: 'record-access'
record: AST record: AST
field: string field: string
line: number
column: number
start: number
} }
export type RecordUpdate = { export type RecordUpdate = {
kind: 'record-update' kind: 'record-update'
record: AST record: AST
updates: { [key: string]: AST } updates: { [key: string]: AST }
line: number
column: number
start: number
} }
// Top-level constructs // Top-level constructs
@ -92,19 +136,27 @@ export type Definition = {
kind: 'definition' kind: 'definition'
name: string name: string
body: AST body: AST
// type?: string // TODO line: number
column: number
start: number
} }
export type TypeDef = { export type TypeDef = {
kind: 'typedef' kind: 'typedef'
name: string name: string
variants: Array<{ name: string, args: string[] }> variants: Array<{ name: string, args: string[] }>
line: number
column: number
start: number
} }
export type Import = { export type Import = {
kind: 'import' kind: 'import'
module: string module: string
items: string[] | 'all' items: string[] | 'all'
line: number
column: number
start: number
} }
export type AST = export type AST =
@ -119,6 +171,7 @@ export type AST =
| RecordAccess | RecordAccess
| RecordUpdate | RecordUpdate
| List | List
| ListSpread
| Definition | Definition
| TypeDef | TypeDef
| Import | Import
@ -183,7 +236,7 @@ export function prettyPrint(ast: AST, indent = 0): string {
return `${i}match ${expr}\n${cases}`; return `${i}match ${expr}\n${cases}`;
default: default:
return `${i}${ast.kind}` return `Unknown AST kind: ${i}${(ast as any).kind}`
} }
} }
@ -215,5 +268,7 @@ function prettyPrintPattern(pattern: Pattern): string {
.join(', '); .join(', ');
return `{${fields}}`; return `{${fields}}`;
default:
return `Unknown AST kind: ${(pattern as any).kind}`
} }
} }

@ -0,0 +1,44 @@
export class CGError extends Error {
line: number;
column: number;
source: string;
errorType: 'RuntimeError' | 'ParseError';
constructor(
message: string,
line: number,
column: number,
source: string,
errorType: 'RuntimeError' | 'ParseError' = 'RuntimeError'
) {
super(message);
this.name = errorType;
this.line = line;
this.column = column;
this.source = source;
this.errorType = errorType;
}
format(): string {
const lines = this.source.split('\n');
const errorLine = lines[this.line - 1];
if (!errorLine) {
return `${this.name}: ${this.message} at line ${this.line}, column ${this.column}`;
}
const lineNumStr = `${this.line}`;
const padding = ' '.repeat(lineNumStr.length);
const pointer = ' '.repeat(this.column - 1) + '^';
return `${this.name}: ${this.message}\n\n ${lineNumStr} | ${errorLine}\n${padding} | ${pointer}`;
}
}
export function RuntimeError(message: string, line: number, column: number, source: string) {
return new CGError(message, line, column, source, 'RuntimeError');
}
export function ParseError(message: string, line: number, column: number, source: string) {
return new CGError(message, line, column, source, 'ParseError');
}

@ -1,8 +1,9 @@
import type { AST, Pattern } from './ast'; import type { AST, Pattern } from './ast';
import type { Env } from './env'; import type { Env } from './env';
import type { Value } from './types'; import type { Value } from './types';
import { RuntimeError } from './error';
export function evaluate(ast: AST, env: Env): Value { export function evaluate(ast: AST, env: Env, source: string): Value {
switch (ast.kind) { switch (ast.kind) {
case 'literal': case 'literal':
return ast.value; return ast.value;
@ -11,7 +12,12 @@ export function evaluate(ast: AST, env: Env): Value {
const val = env.get(ast.name); const val = env.get(ast.name);
if (val === undefined) if (val === undefined)
throw new Error(`Unknown variable: ${ast.name}`); throw RuntimeError(
`Unknown variable: ${ast.name}`,
ast.line,
ast.column,
source
);
return val; return val;
} }
@ -22,14 +28,14 @@ export function evaluate(ast: AST, env: Env): Value {
for (const item of ast.elements) { for (const item of ast.elements) {
// Spread // Spread
if ('spread' in item) { if ('spread' in item) {
const spreadValue = evaluate(item.spread, env); const spreadValue = evaluate(item.spread, env, source);
if (spreadValue.kind !== 'list') if (spreadValue.kind !== 'list')
throw new Error('can only spread lists'); throw RuntimeError('can only spread lists', ast.line, ast.column, source);
elements.push(...spreadValue.elements); elements.push(...spreadValue.elements);
} else { } else {
elements.push(evaluate(item, env)); elements.push(evaluate(item, env, source));
} }
} }
@ -40,36 +46,36 @@ export function evaluate(ast: AST, env: Env): Value {
const fields: { [key: string]: Value } = {}; const fields: { [key: string]: Value } = {};
Object.entries(ast.fields).forEach(([k, v]) => { Object.entries(ast.fields).forEach(([k, v]) => {
fields[k] = evaluate(v, env); fields[k] = evaluate(v, env, source);
}); });
return { kind: 'record', fields }; return { kind: 'record', fields };
case 'record-access': { case 'record-access': {
const record = evaluate(ast.record, env); const record = evaluate(ast.record, env, source);
if (record.kind !== 'record') if (record.kind !== 'record')
throw new Error('Not a record'); throw RuntimeError('Not a record', ast.line, ast.column, source);
const value = record.fields[ast.field]; const value = record.fields[ast.field];
if (value === undefined) { if (value === undefined) {
throw new Error(`Field ${ast.field} not found`); throw RuntimeError(`Field ${ast.field} not found`, ast.line, ast.column, source);
} }
return value; return value;
} }
case 'record-update': { case 'record-update': {
const record = evaluate(ast.record, env); const record = evaluate(ast.record, env, source);
if (record.kind !== 'record') if (record.kind !== 'record')
throw new Error('Not a record'); throw RuntimeError('Not a record', ast.line, ast.column, source);
const newFields: { [key: string]: Value } = { ...record.fields }; const newFields: { [key: string]: Value } = { ...record.fields };
for (const [field, expr] of Object.entries(ast.updates)) { for (const [field, expr] of Object.entries(ast.updates)) {
newFields[field] = evaluate(expr, env); newFields[field] = evaluate(expr, env, source);
} }
return { kind: 'record', fields: newFields }; return { kind: 'record', fields: newFields };
@ -85,7 +91,7 @@ export function evaluate(ast: AST, env: Env): Value {
case 'let': { case 'let': {
const newEnv = new Map(env); const newEnv = new Map(env);
const val = evaluate(ast.value, newEnv); const val = evaluate(ast.value, newEnv, source);
// Don't bind _ // Don't bind _
if (ast.name !== '_') { if (ast.name !== '_') {
@ -94,7 +100,7 @@ export function evaluate(ast: AST, env: Env): Value {
newEnv.set(ast.name, val); newEnv.set(ast.name, val);
return evaluate(ast.body, newEnv); return evaluate(ast.body, newEnv, source);
} }
case 'lambda': case 'lambda':
@ -106,8 +112,8 @@ export function evaluate(ast: AST, env: Env): Value {
} }
case 'apply': { case 'apply': {
const func = evaluate(ast.func, env); const func = evaluate(ast.func, env, source);
const argValues = ast.args.map(arg => evaluate(arg, env)); const argValues = ast.args.map(arg => evaluate(arg, env, source));
// Native functions // Native functions
if (func.kind === 'native') { if (func.kind === 'native') {
@ -131,12 +137,12 @@ export function evaluate(ast: AST, env: Env): Value {
}; };
} }
throw new Error(`Function expects ${func.arity} args, but got ${argValues.length}`); throw RuntimeError(`Function expects ${func.arity} args, but got ${argValues.length}`, ast.line, ast.column, source);
} }
// Constructor application // Constructor application
if (func.kind === 'constructor') { if (func.kind === 'constructor') {
const argValues = ast.args.map(arg => evaluate(arg, env)); const argValues = ast.args.map(arg => evaluate(arg, env, source));
return { return {
kind: 'constructor', kind: 'constructor',
name: func.name, name: func.name,
@ -145,7 +151,7 @@ export function evaluate(ast: AST, env: Env): Value {
} }
if (func.kind !== 'closure') if (func.kind !== 'closure')
throw new Error('Not a function'); throw RuntimeError('Not a function', ast.line, ast.column, source);
// Too few args (Currying) // Too few args (Currying)
if (argValues.length < func.params.length) { if (argValues.length < func.params.length) {
@ -166,7 +172,7 @@ export function evaluate(ast: AST, env: Env): Value {
// Too many args // Too many args
if (argValues.length > func.params.length) if (argValues.length > func.params.length)
throw new Error('Too many arguments'); throw RuntimeError('Too many arguments', ast.line, ast.column, source);
// Exact number of args // Exact number of args
const callEnv = new Map(func.env); const callEnv = new Map(func.env);
@ -174,11 +180,11 @@ export function evaluate(ast: AST, env: Env): Value {
callEnv.set(func.params[i], argValues[i]); callEnv.set(func.params[i], argValues[i]);
} }
return evaluate(func.body, callEnv); return evaluate(func.body, callEnv, source);
} }
case 'match': { case 'match': {
const value = evaluate(ast.expr, env); const value = evaluate(ast.expr, env, source);
for (const matchCase of ast.cases) { for (const matchCase of ast.cases) {
const bindings = matchPattern(value, matchCase.pattern); const bindings = matchPattern(value, matchCase.pattern);
@ -188,15 +194,15 @@ export function evaluate(ast: AST, env: Env): Value {
for (const [name, val] of Object.entries(bindings)) { for (const [name, val] of Object.entries(bindings)) {
newEnv.set(name, val); newEnv.set(name, val);
} }
return evaluate(matchCase.result, newEnv); return evaluate(matchCase.result, newEnv, source);
} }
} }
throw new Error('Non-exhaustive pattern match'); throw RuntimeError('Non-exhaustive pattern match', ast.line, ast.column, source);
} }
default: default:
throw new Error('Syntax Error'); throw RuntimeError('Syntax Error', ast.line, ast.column, source);
} }
} }

@ -1,4 +1,4 @@
export type Token = export type Token = (
// Literals // Literals
| { kind: 'int', value: number } | { kind: 'int', value: number }
| { kind: 'float', value: number } | { kind: 'float', value: number }
@ -45,24 +45,45 @@ export type Token =
| { kind: 'caret' } | { kind: 'caret' }
| { kind: 'eof' } | { kind: 'eof' }
) & {
line: number;
column: number;
start: number; // char offset in source
}
export function tokenize(source: string): Token[] { export function tokenize(source: string): Token[] {
const tokens: Token[] = []; const tokens: Token[] = [];
let i = 0; let i = 0;
let line = 1;
let column = 1;
function advance() {
if (source[i] === '\n') {
line++;
column = 1
} else {
column++;
}
i++;
}
while (i < source.length) { while (i < source.length) {
const char = source[i]; const char = source[i];
const start = i;
const startLine = line;
const startColumn = column;
// Whitespace // Whitespace
if (/\s/.test(char)) { if (/\s/.test(char)) {
i++; advance();
continue; continue;
} }
// Comments // Comments
if (char === '#') { if (char === '#') {
while (i < source.length && source[i] !== '\n') { while (i < source.length && source[i] !== '\n') {
i++; advance();
} }
continue; continue;
} }
@ -78,12 +99,12 @@ export function tokenize(source: string): Token[] {
hasDot = true; hasDot = true;
} }
num += source[i]; num += source[i];
i++; advance();
} }
tokens.push(hasDot tokens.push(hasDot
? { kind: 'float', value: parseFloat(num) } ? { kind: 'float', value: parseFloat(num), line: startLine, column: startColumn, start }
: { kind: 'int', value: parseInt(num) }); : { kind: 'int', value: parseInt(num), line: startLine, column: startColumn, start });
continue; continue;
} }
@ -93,18 +114,18 @@ export function tokenize(source: string): Token[] {
let str = ''; let str = '';
while (i < source.length && /[A-Za-z0-9_!-]/.test(source[i])) { while (i < source.length && /[A-Za-z0-9_!-]/.test(source[i])) {
str += source[i]; str += source[i];
i++; advance();
} }
if (str === '_') { if (str === '_') {
// Wildcards // Wildcards
tokens.push({ kind: 'underscore' }); tokens.push({ kind: 'underscore', line: startLine, column: startColumn, start });
} else { } else {
const isType = /[A-Z]/.test(str[0]); const isType = /[A-Z]/.test(str[0]);
tokens.push(isType tokens.push(isType
? { kind: 'type-ident', value: str } ? { kind: 'type-ident', value: str, line: startLine, column: startColumn, start }
: { kind: 'ident', value: str }); : { kind: 'ident', value: str, line: startLine, column: startColumn, start });
} }
continue; continue;
@ -112,12 +133,12 @@ export function tokenize(source: string): Token[] {
// Strings // Strings
if (char === '"') { if (char === '"') {
i++; advance();
let str = ''; let str = '';
while (i < source.length && source[i] !== '"') { while (i < source.length && source[i] !== '"') {
if (source[i] === '\\') { if (source[i] === '\\') {
i++; advance();
if (i >= source.length) { if (i >= source.length) {
throw new Error('Unterminated string'); throw new Error('Unterminated string');
@ -133,16 +154,16 @@ export function tokenize(source: string): Token[] {
str += source[i]; str += source[i];
} }
i++; advance();
} }
if (i >= source.length) { if (i >= source.length) {
throw new Error('Unterminated string'); throw new Error('Unterminated string');
} }
tokens.push({ kind: 'string', value: str }); tokens.push({ kind: 'string', value: str, line: startLine, column: startColumn, start });
i++; advance();
continue; continue;
} }
@ -150,35 +171,35 @@ export function tokenize(source: string): Token[] {
switch (char) { switch (char) {
case '>': { case '>': {
if (source[i + 1] === '=') { if (source[i + 1] === '=') {
tokens.push({ kind: 'greater-equals' }); tokens.push({ kind: 'greater-equals', line: startLine, column: startColumn, start });
i++; advance();
} else { } else {
tokens.push({ kind: 'greater-than' }); tokens.push({ kind: 'greater-than', line: startLine, column: startColumn, start });
} }
break; break;
} }
case '<': { case '<': {
if (source[i + 1] === '=') { if (source[i + 1] === '=') {
tokens.push({ kind: 'less-equals' }); tokens.push({ kind: 'less-equals', line: startLine, column: startColumn, start });
i++; advance();
} else { } else {
tokens.push({ kind: 'less-than' }); tokens.push({ kind: 'less-than', line: startLine, column: startColumn, start });
} }
break; break;
} }
case '=': { case '=': {
if (source[i + 1] === '=') { if (source[i + 1] === '=') {
tokens.push({ kind: 'equals-equals' }); tokens.push({ kind: 'equals-equals', line: startLine, column: startColumn, start });
i++; advance();
} else { } else {
tokens.push({ kind: 'equals' }); tokens.push({ kind: 'equals', line: startLine, column: startColumn, start });
} }
break; break;
} }
case '!': { case '!': {
if (source[i + 1] === '=') { if (source[i + 1] === '=') {
tokens.push({ kind: 'not-equals' }); tokens.push({ kind: 'not-equals', line: startLine, column: startColumn, start });
i++; advance();
} else { } else {
throw new Error(`Unexpected character: ${char}`) throw new Error(`Unexpected character: ${char}`)
} }
@ -186,40 +207,40 @@ export function tokenize(source: string): Token[] {
} }
case '.': { case '.': {
if (source[i + 1] === '.' && source[i + 2] === '.') { if (source[i + 1] === '.' && source[i + 2] === '.') {
tokens.push({ kind: 'dot-dot-dot' }) tokens.push({ kind: 'dot-dot-dot', line: startLine, column: startColumn, start })
i += 2; i += 2;
} else { } else {
tokens.push({ kind: 'dot' }); tokens.push({ kind: 'dot', line: startLine, column: startColumn, start });
} }
break; break;
} }
case ':': tokens.push({ kind: 'colon' }); break; case ':': tokens.push({ kind: 'colon', line: startLine, column: startColumn, start }); break;
case ';': tokens.push({ kind: 'semicolon' }); break; case ';': tokens.push({ kind: 'semicolon', line: startLine, column: startColumn, start }); break;
case '\\': tokens.push({ kind: 'backslash' }); break; case '\\': tokens.push({ kind: 'backslash', line: startLine, column: startColumn, start }); break;
case '~': tokens.push({ kind: 'tilde' }); break; case '~': tokens.push({ kind: 'tilde', line: startLine, column: startColumn, start }); break;
case '|': tokens.push({ kind: 'pipe' }); break; case '|': tokens.push({ kind: 'pipe', line: startLine, column: startColumn, start }); break;
case ',': tokens.push({ kind: 'comma' }); break; case ',': tokens.push({ kind: 'comma', line: startLine, column: startColumn, start }); break;
case '&': tokens.push({ kind: 'ampersand' }); break; case '&': tokens.push({ kind: 'ampersand', line: startLine, column: startColumn, start }); break;
case '@': tokens.push({ kind: 'at' }); break; case '@': tokens.push({ kind: 'at', line: startLine, column: startColumn, start }); break;
// Arithmetic // Arithmetic
case '+': tokens.push({ kind: 'plus' }); break; case '+': tokens.push({ kind: 'plus', line: startLine, column: startColumn, start }); break;
case '-': tokens.push({ kind: 'minus' }); break; case '-': tokens.push({ kind: 'minus', line: startLine, column: startColumn, start }); break;
case '*': tokens.push({ kind: 'star' }); break; case '*': tokens.push({ kind: 'star', line: startLine, column: startColumn, start }); break;
case '/': tokens.push({ kind: 'slash' }); break; case '/': tokens.push({ kind: 'slash', line: startLine, column: startColumn, start }); break;
case '^': tokens.push({ kind: 'caret' }); break; case '^': tokens.push({ kind: 'caret', line: startLine, column: startColumn, start }); break;
case '%': tokens.push({ kind: 'percent' }); break; case '%': tokens.push({ kind: 'percent', line: startLine, column: startColumn, start }); break;
// Brackets // Brackets
case '(': tokens.push({ kind: 'open-paren' }); break; case '(': tokens.push({ kind: 'open-paren', line: startLine, column: startColumn, start }); break;
case ')': tokens.push({ kind: 'close-paren' }); break; case ')': tokens.push({ kind: 'close-paren', line: startLine, column: startColumn, start }); break;
case '{': tokens.push({ kind: 'open-brace' }); break; case '{': tokens.push({ kind: 'open-brace', line: startLine, column: startColumn, start }); break;
case '}': tokens.push({ kind: 'close-brace' }); break; case '}': tokens.push({ kind: 'close-brace', line: startLine, column: startColumn, start }); break;
case '[': tokens.push({ kind: 'open-bracket' }); break; case '[': tokens.push({ kind: 'open-bracket', line: startLine, column: startColumn, start }); break;
case ']': tokens.push({ kind: 'close-bracket' }); break; case ']': tokens.push({ kind: 'close-bracket', line: startLine, column: startColumn, start }); break;
} }
i++; advance();
} }
return tokens; return tokens;

@ -4,34 +4,45 @@ import { tokenize } from './lexer'
import { Parser } from './parser' import { Parser } from './parser'
import { runApp } from './runtime'; import { runApp } from './runtime';
import { builtins } from './builtins'; import { builtins } from './builtins';
import { CGError } from './error';
import stdlibCode from './stdlib.cg?raw'; import stdlibCode from './stdlib.cg?raw';
import designTokensCode from './design-tokens.cg?raw'; import designTokensCode from './design-tokens.cg?raw';
import uiComponentsCode from './ui-components.cg?raw'; import uiComponentsCode from './ui-components.cg?raw';
import textInputCode from './textinput-test.cg?raw'; import textInputCode from './textinput-test.cg?raw';
import testCode from './test.cg?raw'; // import testCode from './test.cg?raw';
import counterApp from './counter.cg?raw'; // import counterApp from './counter.cg?raw';
const canvas = document.createElement('canvas') as HTMLCanvasElement; const canvas = document.createElement('canvas') as HTMLCanvasElement;
document.body.appendChild(canvas); document.body.appendChild(canvas);
const cgCode = stdlibCode + '\n' + designTokensCode + '\n' + uiComponentsCode + '\n' + textInputCode; const cgCode = stdlibCode + '\n' + designTokensCode + '\n' + uiComponentsCode + '\n' + textInputCode;
const tokens = tokenize(cgCode); try {
const parser = new Parser(tokens); const tokens = tokenize(cgCode);
const ast = parser.parse(); const parser = new Parser(tokens, cgCode);
console.log(ast); const ast = parser.parse();
// console.log(ast);
const env: Env = new Map(Object.entries(builtins));
const appRecord = evaluate(ast, env); const env: Env = new Map(Object.entries(builtins));
console.log("appRecord", appRecord); const appRecord = evaluate(ast, env, cgCode);
// console.log("appRecord", appRecord);
if (appRecord.kind !== 'record')
throw new Error('Expected record'); if (appRecord.kind !== 'record')
throw new Error('Expected record');
const init = appRecord.fields.init;
const update = appRecord.fields.update; const init = appRecord.fields.init;
const view = appRecord.fields.view; const update = appRecord.fields.update;
const view = appRecord.fields.view;
runApp({ init, update, view }, canvas);
runApp({ init, update, view }, canvas, cgCode);
} catch(error) {
console.log('CAUGHT ERROR:', error);
console.log('Is CGError??', error instanceof CGError);
if (error instanceof CGError) {
console.error(error.format());
} else {
throw error;
}
}

@ -1,12 +1,15 @@
import type { Token } from './lexer' import type { Token } from './lexer'
import type { AST, MatchCase, Pattern } from './ast' import type { AST, MatchCase, Pattern } from './ast'
import { ParseError } from './error'
export class Parser { export class Parser {
private tokens: Token[] private tokens: Token[]
private pos: number = 0 private pos: number = 0
private source: string
constructor(tokens: Token[]) { constructor(tokens: Token[], source: string) {
this.tokens = tokens; this.tokens = tokens;
this.source = source;
} }
private current(): Token { private current(): Token {
@ -33,12 +36,20 @@ export class Parser {
const token = this.current(); const token = this.current();
if (token.kind !== kind) { if (token.kind !== kind) {
throw new Error(`Expected ${kind}, got ${token.kind}`); throw ParseError(`Expected ${kind}, got ${token.kind}`, token.line, token.column, this.source);
} }
return this.advance(); return this.advance();
} }
private getPos(token: Token) {
return {
line: token.line,
column: token.column,
start: token.start
};
}
private isInfixOp(): boolean { private isInfixOp(): boolean {
const kind = this.current().kind; const kind = this.current().kind;
return kind === 'plus' || kind === 'minus' || return kind === 'plus' || kind === 'minus' ||
@ -71,7 +82,7 @@ export class Parser {
case 'less-than': return 'lt'; case 'less-than': return 'lt';
case 'less-equals': return 'lte'; case 'less-equals': return 'lte';
default: throw new Error(`Not an operator: ${token.kind}`); default: throw ParseError(`Not an operator: ${token.kind}`, token.line, token.column, this.source);
} }
} }
@ -145,6 +156,7 @@ export class Parser {
private parseMatch(expr: AST): AST { private parseMatch(expr: AST): AST {
const token = this.current();
const cases: MatchCase[] = []; const cases: MatchCase[] = [];
while(this.current().kind === 'pipe') { while(this.current().kind === 'pipe') {
@ -152,10 +164,10 @@ export class Parser {
const pattern = this.parsePattern(); const pattern = this.parsePattern();
this.expect('backslash'); this.expect('backslash');
const result = this.parseExpressionNoMatch(); const result = this.parseExpressionNoMatch();
cases.push({ pattern, result }) cases.push({ pattern, result, ...this.getPos(token) })
} }
return { kind: 'match', expr, cases }; return { kind: 'match', expr, cases, ...this.getPos(token) };
} }
private parsePattern(): Pattern { private parsePattern(): Pattern {
@ -217,7 +229,7 @@ export class Parser {
this.expect('close-bracket'); this.expect('close-bracket');
if (spreadName !== null) { if (spreadName !== null) {
return { kind: 'list-spread', head: elements, spread: spreadName }; return { kind: 'list-spread', head: elements, spread: spreadName, ...this.getPos(token) };
} }
return { kind: 'list', elements }; return { kind: 'list', elements };
@ -251,7 +263,7 @@ export class Parser {
return pattern; return pattern;
} }
throw new Error(`Unexpected token in pattern: ${token.kind}`) throw ParseError(`Unexpected token in pattern: ${token.kind}`, token.line, token.column, this.source);
} }
private canStartPattern(): boolean { private canStartPattern(): boolean {
@ -264,6 +276,7 @@ export class Parser {
} }
private parseLambda(): AST { private parseLambda(): AST {
const token = this.current();
const params: string[] = []; const params: string[] = [];
while (this.current().kind === 'ident') { while (this.current().kind === 'ident') {
@ -274,7 +287,7 @@ export class Parser {
this.expect('backslash'); this.expect('backslash');
const body = this.parseExpression(); const body = this.parseExpression();
return { kind: 'lambda', params, body }; return { kind: 'lambda', params, body, ...this.getPos(token) };
} }
private parseLet(): AST { private parseLet(): AST {
@ -288,7 +301,7 @@ export class Parser {
name = (nameToken as { value: string }).value; name = (nameToken as { value: string }).value;
this.advance(); this.advance();
} else { } else {
throw new Error(`Expected ident or underscore, got ${nameToken.kind}`); throw ParseError(`Expected ident or underscore, got ${nameToken.kind}`, nameToken.line, nameToken.column, this.source);
} }
@ -297,10 +310,11 @@ export class Parser {
this.expect('semicolon'); this.expect('semicolon');
const body = this.parseExpressionNoMatch(); const body = this.parseExpressionNoMatch();
return { kind: 'let', name, value, body }; return { kind: 'let', name, value, body, ...this.getPos(nameToken) };
} }
private parseInfix(): AST { private parseInfix(): AST {
const token = this.current();
let left = this.parseApplication(); let left = this.parseApplication();
while (this.isInfixOp()) { while (this.isInfixOp()) {
@ -313,7 +327,8 @@ export class Parser {
left = { left = {
kind: 'apply', kind: 'apply',
func: right, func: right,
args: [left] args: [left],
...this.getPos(token)
}; };
} else { } else {
// operators desugar to function calls // operators desugar to function calls
@ -322,8 +337,9 @@ export class Parser {
left = { left = {
kind: 'apply', kind: 'apply',
func: { kind: 'variable', name: opName }, func: { kind: 'variable', name: opName, ...this.getPos(token) },
args: [left, right] args: [left, right],
...this.getPos(token)
} }
} }
} }
@ -332,17 +348,19 @@ export class Parser {
} }
private parseApplication(): AST { private parseApplication(): AST {
const token = this.current();
let func = this.parsePostfix(); let func = this.parsePostfix();
while (this.canStartPrimary()) { while (this.canStartPrimary()) {
const arg = this.parsePostfix(); const arg = this.parsePostfix();
func = { kind: 'apply', func, args: [arg] }; func = { kind: 'apply', func, args: [arg], ...this.getPos(token) };
} }
return func; return func;
} }
private parsePostfix(): AST { private parsePostfix(): AST {
const token = this.current();
let expr = this.parsePrimary(); let expr = this.parsePrimary();
while (true) { while (true) {
@ -368,13 +386,13 @@ export class Parser {
} }
this.expect('close-brace'); this.expect('close-brace');
expr = { kind: 'record-update', record: expr, updates } expr = { kind: 'record-update', record: expr, updates, ...this.getPos(token) }
} else { } else {
// Record access // Record access
const fieldToken = this.expect('ident'); const fieldToken = this.expect('ident');
const field = (fieldToken as { value: string }).value; const field = (fieldToken as { value: string }).value;
expr = { kind: 'record-access', record: expr, field }; expr = { kind: 'record-access', record: expr, field, ...this.getPos(token) };
} }
} else { } else {
break; break;
@ -397,7 +415,7 @@ export class Parser {
if (token.kind === 'open-bracket') { if (token.kind === 'open-bracket') {
this.advance(); this.advance();
const items: (AST | { spread: AST })[] = []; const items: AST[] = [];
let first = true; let first = true;
while (this.current().kind !== 'close-bracket') { while (this.current().kind !== 'close-bracket') {
@ -408,16 +426,17 @@ export class Parser {
// Spread // Spread
if (this.current().kind === 'dot-dot-dot') { if (this.current().kind === 'dot-dot-dot') {
const spreadToken = this.current();
this.advance(); this.advance();
const expr = this.parseExpression(); const expr = this.parseExpression();
items.push({ spread: expr }) items.push({ kind: 'list-spread', spread: expr, ...this.getPos(spreadToken) })
} else { } else {
items.push(this.parseExpression()); items.push(this.parseExpression());
} }
} }
this.expect('close-bracket'); this.expect('close-bracket');
return { kind: 'list', elements: items }; return { kind: 'list', elements: items, ...this.getPos(token) };
} }
if (token.kind === 'open-brace') { if (token.kind === 'open-brace') {
@ -439,34 +458,34 @@ export class Parser {
} }
this.expect('close-brace'); this.expect('close-brace');
return { kind: 'record', fields }; return { kind: 'record', fields, ...this.getPos(token) };
} }
if (token.kind === 'int') { if (token.kind === 'int') {
this.advance(); this.advance();
return { kind: 'literal', value: { kind: 'int', value: token.value } }; return { kind: 'literal', value: { kind: 'int', value: token.value }, ...this.getPos(token) };
} }
if (token.kind === 'float') { if (token.kind === 'float') {
this.advance(); this.advance();
return { kind: 'literal', value: { kind: 'float', value: token.value } }; return { kind: 'literal', value: { kind: 'float', value: token.value }, ...this.getPos(token) };
} }
if (token.kind === 'string') { if (token.kind === 'string') {
this.advance(); this.advance();
return { kind: 'literal', value: { kind: 'string', value: token.value } }; return { kind: 'literal', value: { kind: 'string', value: token.value }, ...this.getPos(token) };
} }
if (token.kind === 'ident') { if (token.kind === 'ident') {
this.advance(); this.advance();
return { kind: 'variable', name: token.value }; return { kind: 'variable', name: token.value, ...this.getPos(token) };
} }
if (token.kind === 'type-ident') { if (token.kind === 'type-ident') {
this.advance(); this.advance();
return { kind: 'constructor', name: token.value }; return { kind: 'constructor', name: token.value, ...this.getPos(token) };
} }
throw new Error(`Unexpected token: ${token.kind}`); throw ParseError(`Unexpected token: ${token.kind}`, token.line, token.column, this.source);
} }
} }

@ -2,6 +2,7 @@ import type { Value } from './types';
import { valueToUI } from './valueToUI'; import { valueToUI } from './valueToUI';
import { render, hitTest, hitTestTextInput } from './ui'; import { render, hitTest, hitTestTextInput } from './ui';
import { evaluate } from './interpreter'; import { evaluate } from './interpreter';
import { CGError } from './error';
export type App = { export type App = {
init: Value; init: Value;
@ -9,7 +10,7 @@ export type App = {
view: Value; // State / UI view: Value; // State / UI
} }
export function runApp(app: App, canvas: HTMLCanvasElement) { export function runApp(app: App, canvas: HTMLCanvasElement, source: string) {
let state = app.init; let state = app.init;
function setupCanvas() { function setupCanvas() {
@ -36,13 +37,21 @@ export function runApp(app: App, canvas: HTMLCanvasElement) {
} }
}; };
const callEnv = new Map(app.view.env); try {
callEnv.set(app.view.params[0], state); const callEnv = new Map(app.view.env);
callEnv.set(app.view.params[1], viewport); callEnv.set(app.view.params[0], state);
const uiValue = evaluate(app.view.body, callEnv); callEnv.set(app.view.params[1], viewport);
const ui = valueToUI(uiValue); const uiValue = evaluate(app.view.body, callEnv, source);
const ui = valueToUI(uiValue);
render(ui, canvas);
render(ui, canvas);
} catch (error) {
if (error instanceof CGError) {
console.error(error.format());
} else {
throw error;
}
}
} }
function handleEvent(event: Value) { function handleEvent(event: Value) {
@ -52,13 +61,21 @@ export function runApp(app: App, canvas: HTMLCanvasElement) {
if (app.update.params.length !== 2) if (app.update.params.length !== 2)
throw new Error('update must have 2 parameters'); throw new Error('update must have 2 parameters');
const callEnv = new Map(app.update.env); try {
callEnv.set(app.update.params[0], state); const callEnv = new Map(app.update.env);
callEnv.set(app.update.params[1], event); callEnv.set(app.update.params[0], state);
const newState = evaluate(app.update.body, callEnv); callEnv.set(app.update.params[1], event);
const newState = evaluate(app.update.body, callEnv, source);
state = newState; state = newState;
rerender(); rerender();
} catch (error) {
if (error instanceof CGError) {
console.error(error.format());
} else {
throw error;
}
}
} }
canvas.addEventListener('click', (e) => { canvas.addEventListener('click', (e) => {

@ -11,7 +11,7 @@ button = config \
}; };
insertChar = text pos char \ insertChar = text pos char \
_ = debug "insertChar" char; # _ = debug "insertChar" char;
before = slice text 0 pos; before = slice text 0 pos;
after = slice text pos (len text); after = slice text pos (len text);
before & char & after; before & char & after;

Loading…
Cancel
Save