diff --git a/src/ast.ts b/src/ast.ts index 801d4e5..9a09b38 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -5,16 +5,25 @@ import type { Value } from './types'; export type Literal = { kind: 'literal' value: Value + line: number + column: number + start: number } export type Variable = { kind: 'variable' name: string + line: number + column: number + start: number } export type Constructor = { kind: 'constructor' name: string + line: number + column: number + start: number } // Functions @@ -23,12 +32,18 @@ export type Lambda = { kind: 'lambda' params: string[] body: AST + line: number + column: number + start: number } export type Apply = { kind: 'apply' func: AST args: AST[] + line: number + column: number + start: number } // Bindings @@ -38,6 +53,9 @@ export type Let = { name: string value: AST body: AST + line: number + column: number + start: number } // Matching @@ -46,11 +64,17 @@ export type Match = { kind: 'match' expr: AST cases: MatchCase[] + line: number + column: number + start: number } export type MatchCase = { pattern: Pattern result: AST + line: number + column: number + start: number } export type Pattern = @@ -64,26 +88,46 @@ export type Pattern = // Data Structures +export type ListSpread = { + kind: 'list-spread' + spread: AST; + line: number; + column: number; + start: number; +} + export type List = { kind: 'list' - elements: (AST | { spread: AST })[] + elements: (AST | ListSpread)[] + line: number + column: number + start: number } export type Record = { kind: 'record' fields: { [key: string]: AST } + line: number + column: number + start: number } export type RecordAccess = { kind: 'record-access' record: AST field: string + line: number + column: number + start: number } export type RecordUpdate = { kind: 'record-update' record: AST updates: { [key: string]: AST } + line: number + column: number + start: number } // Top-level constructs @@ -92,19 +136,27 @@ export type Definition = { kind: 'definition' name: string body: AST - // type?: string // TODO + line: number + column: number + start: number } export type TypeDef = { kind: 'typedef' name: string variants: Array<{ name: string, args: string[] }> + line: number + column: number + start: number } export type Import = { kind: 'import' module: string items: string[] | 'all' + line: number + column: number + start: number } export type AST = @@ -119,6 +171,7 @@ export type AST = | RecordAccess | RecordUpdate | List + | ListSpread | Definition | TypeDef | Import @@ -183,7 +236,7 @@ export function prettyPrint(ast: AST, indent = 0): string { return `${i}match ${expr}\n${cases}`; default: - return `${i}${ast.kind}` + return `Unknown AST kind: ${i}${(ast as any).kind}` } } @@ -215,5 +268,7 @@ function prettyPrintPattern(pattern: Pattern): string { .join(', '); return `{${fields}}`; + default: + return `Unknown AST kind: ${(pattern as any).kind}` } } diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..0c81736 --- /dev/null +++ b/src/error.ts @@ -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'); +} diff --git a/src/interpreter.ts b/src/interpreter.ts index a856707..3880c4f 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,8 +1,9 @@ import type { AST, Pattern } from './ast'; import type { Env } from './env'; 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) { case 'literal': return ast.value; @@ -11,7 +12,12 @@ export function evaluate(ast: AST, env: Env): Value { const val = env.get(ast.name); if (val === undefined) - throw new Error(`Unknown variable: ${ast.name}`); + throw RuntimeError( + `Unknown variable: ${ast.name}`, + ast.line, + ast.column, + source + ); return val; } @@ -22,14 +28,14 @@ export function evaluate(ast: AST, env: Env): Value { for (const item of ast.elements) { // Spread if ('spread' in item) { - const spreadValue = evaluate(item.spread, env); + const spreadValue = evaluate(item.spread, env, source); 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); } 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 } = {}; Object.entries(ast.fields).forEach(([k, v]) => { - fields[k] = evaluate(v, env); + fields[k] = evaluate(v, env, source); }); return { kind: 'record', fields }; case 'record-access': { - const record = evaluate(ast.record, env); + const record = evaluate(ast.record, env, source); 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]; 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; } case 'record-update': { - const record = evaluate(ast.record, env); + const record = evaluate(ast.record, env, source); 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 }; 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 }; @@ -85,7 +91,7 @@ export function evaluate(ast: AST, env: Env): Value { case 'let': { const newEnv = new Map(env); - const val = evaluate(ast.value, newEnv); + const val = evaluate(ast.value, newEnv, source); // Don't bind _ if (ast.name !== '_') { @@ -94,7 +100,7 @@ export function evaluate(ast: AST, env: Env): Value { newEnv.set(ast.name, val); - return evaluate(ast.body, newEnv); + return evaluate(ast.body, newEnv, source); } case 'lambda': @@ -106,8 +112,8 @@ export function evaluate(ast: AST, env: Env): Value { } case 'apply': { - const func = evaluate(ast.func, env); - const argValues = ast.args.map(arg => evaluate(arg, env)); + const func = evaluate(ast.func, env, source); + const argValues = ast.args.map(arg => evaluate(arg, env, source)); // Native functions 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 if (func.kind === 'constructor') { - const argValues = ast.args.map(arg => evaluate(arg, env)); + const argValues = ast.args.map(arg => evaluate(arg, env, source)); return { kind: 'constructor', name: func.name, @@ -145,7 +151,7 @@ export function evaluate(ast: AST, env: Env): Value { } if (func.kind !== 'closure') - throw new Error('Not a function'); + throw RuntimeError('Not a function', ast.line, ast.column, source); // Too few args (Currying) if (argValues.length < func.params.length) { @@ -166,7 +172,7 @@ export function evaluate(ast: AST, env: Env): Value { // Too many args 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 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]); } - return evaluate(func.body, callEnv); + return evaluate(func.body, callEnv, source); } case 'match': { - const value = evaluate(ast.expr, env); + const value = evaluate(ast.expr, env, source); for (const matchCase of ast.cases) { 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)) { 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: - throw new Error('Syntax Error'); + throw RuntimeError('Syntax Error', ast.line, ast.column, source); } } diff --git a/src/lexer.ts b/src/lexer.ts index 6294079..158d6d6 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -1,4 +1,4 @@ -export type Token = +export type Token = ( // Literals | { kind: 'int', value: number } | { kind: 'float', value: number } @@ -45,24 +45,45 @@ export type Token = | { kind: 'caret' } | { kind: 'eof' } +) & { + line: number; + column: number; + start: number; // char offset in source +} export function tokenize(source: string): Token[] { const tokens: Token[] = []; 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) { const char = source[i]; + const start = i; + const startLine = line; + const startColumn = column; // Whitespace if (/\s/.test(char)) { - i++; + advance(); continue; } // Comments if (char === '#') { while (i < source.length && source[i] !== '\n') { - i++; + advance(); } continue; } @@ -78,12 +99,12 @@ export function tokenize(source: string): Token[] { hasDot = true; } num += source[i]; - i++; + advance(); } tokens.push(hasDot - ? { kind: 'float', value: parseFloat(num) } - : { kind: 'int', value: parseInt(num) }); + ? { kind: 'float', value: parseFloat(num), line: startLine, column: startColumn, start } + : { kind: 'int', value: parseInt(num), line: startLine, column: startColumn, start }); continue; } @@ -93,18 +114,18 @@ export function tokenize(source: string): Token[] { let str = ''; while (i < source.length && /[A-Za-z0-9_!-]/.test(source[i])) { str += source[i]; - i++; + advance(); } if (str === '_') { // Wildcards - tokens.push({ kind: 'underscore' }); + tokens.push({ kind: 'underscore', line: startLine, column: startColumn, start }); } else { const isType = /[A-Z]/.test(str[0]); tokens.push(isType - ? { kind: 'type-ident', value: str } - : { kind: 'ident', value: str }); + ? { kind: 'type-ident', value: str, line: startLine, column: startColumn, start } + : { kind: 'ident', value: str, line: startLine, column: startColumn, start }); } continue; @@ -112,12 +133,12 @@ export function tokenize(source: string): Token[] { // Strings if (char === '"') { - i++; + advance(); let str = ''; while (i < source.length && source[i] !== '"') { if (source[i] === '\\') { - i++; + advance(); if (i >= source.length) { throw new Error('Unterminated string'); @@ -133,16 +154,16 @@ export function tokenize(source: string): Token[] { str += source[i]; } - i++; + advance(); } if (i >= source.length) { 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; } @@ -150,35 +171,35 @@ export function tokenize(source: string): Token[] { switch (char) { case '>': { if (source[i + 1] === '=') { - tokens.push({ kind: 'greater-equals' }); - i++; + tokens.push({ kind: 'greater-equals', line: startLine, column: startColumn, start }); + advance(); } else { - tokens.push({ kind: 'greater-than' }); + tokens.push({ kind: 'greater-than', line: startLine, column: startColumn, start }); } break; } case '<': { if (source[i + 1] === '=') { - tokens.push({ kind: 'less-equals' }); - i++; + tokens.push({ kind: 'less-equals', line: startLine, column: startColumn, start }); + advance(); } else { - tokens.push({ kind: 'less-than' }); + tokens.push({ kind: 'less-than', line: startLine, column: startColumn, start }); } break; } case '=': { if (source[i + 1] === '=') { - tokens.push({ kind: 'equals-equals' }); - i++; + tokens.push({ kind: 'equals-equals', line: startLine, column: startColumn, start }); + advance(); } else { - tokens.push({ kind: 'equals' }); + tokens.push({ kind: 'equals', line: startLine, column: startColumn, start }); } break; } case '!': { if (source[i + 1] === '=') { - tokens.push({ kind: 'not-equals' }); - i++; + tokens.push({ kind: 'not-equals', line: startLine, column: startColumn, start }); + advance(); } else { throw new Error(`Unexpected character: ${char}`) } @@ -186,40 +207,40 @@ export function tokenize(source: string): Token[] { } case '.': { 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; } else { - tokens.push({ kind: 'dot' }); + tokens.push({ kind: 'dot', line: startLine, column: startColumn, start }); } 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: 'comma' }); break; - case '&': tokens.push({ kind: 'ampersand' }); break; - case '@': tokens.push({ kind: 'at' }); break; + case ':': tokens.push({ kind: 'colon', line: startLine, column: startColumn, start }); break; + case ';': tokens.push({ kind: 'semicolon', line: startLine, column: startColumn, start }); break; + case '\\': tokens.push({ kind: 'backslash', line: startLine, column: startColumn, start }); break; + case '~': tokens.push({ kind: 'tilde', line: startLine, column: startColumn, start }); break; + case '|': tokens.push({ kind: 'pipe', line: startLine, column: startColumn, start }); break; + case ',': tokens.push({ kind: 'comma', line: startLine, column: startColumn, start }); break; + case '&': tokens.push({ kind: 'ampersand', line: startLine, column: startColumn, start }); break; + case '@': tokens.push({ kind: 'at', line: startLine, column: startColumn, start }); break; // Arithmetic - case '+': tokens.push({ kind: 'plus' }); break; - 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; + case '+': tokens.push({ kind: 'plus', line: startLine, column: startColumn, start }); break; + case '-': tokens.push({ kind: 'minus', line: startLine, column: startColumn, start }); break; + case '*': tokens.push({ kind: 'star', line: startLine, column: startColumn, start }); break; + case '/': tokens.push({ kind: 'slash', line: startLine, column: startColumn, start }); break; + case '^': tokens.push({ kind: 'caret', line: startLine, column: startColumn, start }); break; + case '%': tokens.push({ kind: 'percent', line: startLine, column: startColumn, start }); 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; + case '(': tokens.push({ kind: 'open-paren', line: startLine, column: startColumn, start }); break; + case ')': tokens.push({ kind: 'close-paren', line: startLine, column: startColumn, start }); break; + case '{': tokens.push({ kind: 'open-brace', line: startLine, column: startColumn, start }); break; + case '}': tokens.push({ kind: 'close-brace', line: startLine, column: startColumn, start }); break; + case '[': tokens.push({ kind: 'open-bracket', line: startLine, column: startColumn, start }); break; + case ']': tokens.push({ kind: 'close-bracket', line: startLine, column: startColumn, start }); break; } - i++; + advance(); } return tokens; diff --git a/src/main.ts b/src/main.ts index 915463c..f622c14 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,34 +4,45 @@ import { tokenize } from './lexer' import { Parser } from './parser' import { runApp } from './runtime'; import { builtins } from './builtins'; +import { CGError } from './error'; import stdlibCode from './stdlib.cg?raw'; import designTokensCode from './design-tokens.cg?raw'; import uiComponentsCode from './ui-components.cg?raw'; import textInputCode from './textinput-test.cg?raw'; -import testCode from './test.cg?raw'; -import counterApp from './counter.cg?raw'; +// import testCode from './test.cg?raw'; +// import counterApp from './counter.cg?raw'; const canvas = document.createElement('canvas') as HTMLCanvasElement; document.body.appendChild(canvas); const cgCode = stdlibCode + '\n' + designTokensCode + '\n' + uiComponentsCode + '\n' + textInputCode; -const tokens = tokenize(cgCode); -const parser = new Parser(tokens); -const ast = parser.parse(); -console.log(ast); - -const env: Env = new Map(Object.entries(builtins)); -const appRecord = evaluate(ast, env); -console.log("appRecord", appRecord); - -if (appRecord.kind !== 'record') - throw new Error('Expected record'); - -const init = appRecord.fields.init; -const update = appRecord.fields.update; -const view = appRecord.fields.view; - -runApp({ init, update, view }, canvas); +try { + const tokens = tokenize(cgCode); + const parser = new Parser(tokens, cgCode); + const ast = parser.parse(); + // console.log(ast); + + const env: Env = new Map(Object.entries(builtins)); + const appRecord = evaluate(ast, env, cgCode); + // console.log("appRecord", appRecord); + + if (appRecord.kind !== 'record') + throw new Error('Expected record'); + + const init = appRecord.fields.init; + const update = appRecord.fields.update; + const view = appRecord.fields.view; + + 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; + } +} diff --git a/src/parser.ts b/src/parser.ts index 9c7032b..724bcb2 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,12 +1,15 @@ import type { Token } from './lexer' import type { AST, MatchCase, Pattern } from './ast' +import { ParseError } from './error' export class Parser { private tokens: Token[] private pos: number = 0 + private source: string - constructor(tokens: Token[]) { + constructor(tokens: Token[], source: string) { this.tokens = tokens; + this.source = source; } private current(): Token { @@ -33,12 +36,20 @@ export class Parser { const token = this.current(); 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(); } + private getPos(token: Token) { + return { + line: token.line, + column: token.column, + start: token.start + }; + } + private isInfixOp(): boolean { const kind = this.current().kind; return kind === 'plus' || kind === 'minus' || @@ -71,7 +82,7 @@ export class Parser { case 'less-than': return 'lt'; 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 { + const token = this.current(); const cases: MatchCase[] = []; while(this.current().kind === 'pipe') { @@ -152,10 +164,10 @@ export class Parser { const pattern = this.parsePattern(); this.expect('backslash'); 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 { @@ -217,7 +229,7 @@ export class Parser { this.expect('close-bracket'); 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 }; @@ -251,7 +263,7 @@ export class Parser { 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 { @@ -264,6 +276,7 @@ export class Parser { } private parseLambda(): AST { + const token = this.current(); const params: string[] = []; while (this.current().kind === 'ident') { @@ -274,7 +287,7 @@ export class Parser { this.expect('backslash'); const body = this.parseExpression(); - return { kind: 'lambda', params, body }; + return { kind: 'lambda', params, body, ...this.getPos(token) }; } private parseLet(): AST { @@ -288,7 +301,7 @@ export class Parser { name = (nameToken as { value: string }).value; this.advance(); } 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'); const body = this.parseExpressionNoMatch(); - return { kind: 'let', name, value, body }; + return { kind: 'let', name, value, body, ...this.getPos(nameToken) }; } private parseInfix(): AST { + const token = this.current(); let left = this.parseApplication(); while (this.isInfixOp()) { @@ -313,7 +327,8 @@ export class Parser { left = { kind: 'apply', func: right, - args: [left] + args: [left], + ...this.getPos(token) }; } else { // operators desugar to function calls @@ -322,8 +337,9 @@ export class Parser { left = { kind: 'apply', - func: { kind: 'variable', name: opName }, - args: [left, right] + func: { kind: 'variable', name: opName, ...this.getPos(token) }, + args: [left, right], + ...this.getPos(token) } } } @@ -332,17 +348,19 @@ export class Parser { } private parseApplication(): AST { + const token = this.current(); let func = this.parsePostfix(); while (this.canStartPrimary()) { const arg = this.parsePostfix(); - func = { kind: 'apply', func, args: [arg] }; + func = { kind: 'apply', func, args: [arg], ...this.getPos(token) }; } return func; } private parsePostfix(): AST { + const token = this.current(); let expr = this.parsePrimary(); while (true) { @@ -368,13 +386,13 @@ export class Parser { } this.expect('close-brace'); - expr = { kind: 'record-update', record: expr, updates } + expr = { kind: 'record-update', record: expr, updates, ...this.getPos(token) } } else { // Record access const fieldToken = this.expect('ident'); 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 { break; @@ -397,7 +415,7 @@ export class Parser { if (token.kind === 'open-bracket') { this.advance(); - const items: (AST | { spread: AST })[] = []; + const items: AST[] = []; let first = true; while (this.current().kind !== 'close-bracket') { @@ -408,16 +426,17 @@ export class Parser { // Spread if (this.current().kind === 'dot-dot-dot') { + const spreadToken = this.current(); this.advance(); const expr = this.parseExpression(); - items.push({ spread: expr }) + items.push({ kind: 'list-spread', spread: expr, ...this.getPos(spreadToken) }) } else { items.push(this.parseExpression()); } } this.expect('close-bracket'); - return { kind: 'list', elements: items }; + return { kind: 'list', elements: items, ...this.getPos(token) }; } if (token.kind === 'open-brace') { @@ -439,34 +458,34 @@ export class Parser { } this.expect('close-brace'); - return { kind: 'record', fields }; + return { kind: 'record', fields, ...this.getPos(token) }; } if (token.kind === 'int') { 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') { 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') { 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') { this.advance(); - return { kind: 'variable', name: token.value }; + return { kind: 'variable', name: token.value, ...this.getPos(token) }; } if (token.kind === 'type-ident') { 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); } } diff --git a/src/runtime.ts b/src/runtime.ts index 7295062..cabe77e 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -2,6 +2,7 @@ import type { Value } from './types'; import { valueToUI } from './valueToUI'; import { render, hitTest, hitTestTextInput } from './ui'; import { evaluate } from './interpreter'; +import { CGError } from './error'; export type App = { init: Value; @@ -9,7 +10,7 @@ export type App = { view: Value; // State / UI } -export function runApp(app: App, canvas: HTMLCanvasElement) { +export function runApp(app: App, canvas: HTMLCanvasElement, source: string) { let state = app.init; function setupCanvas() { @@ -36,13 +37,21 @@ export function runApp(app: App, canvas: HTMLCanvasElement) { } }; - const callEnv = new Map(app.view.env); - callEnv.set(app.view.params[0], state); - callEnv.set(app.view.params[1], viewport); - const uiValue = evaluate(app.view.body, callEnv); - const ui = valueToUI(uiValue); - - render(ui, canvas); + try { + const callEnv = new Map(app.view.env); + callEnv.set(app.view.params[0], state); + callEnv.set(app.view.params[1], viewport); + const uiValue = evaluate(app.view.body, callEnv, source); + const ui = valueToUI(uiValue); + + render(ui, canvas); + } catch (error) { + if (error instanceof CGError) { + console.error(error.format()); + } else { + throw error; + } + } } function handleEvent(event: Value) { @@ -52,13 +61,21 @@ export function runApp(app: App, canvas: HTMLCanvasElement) { if (app.update.params.length !== 2) throw new Error('update must have 2 parameters'); - const callEnv = new Map(app.update.env); - callEnv.set(app.update.params[0], state); - callEnv.set(app.update.params[1], event); - const newState = evaluate(app.update.body, callEnv); + try { + const callEnv = new Map(app.update.env); + callEnv.set(app.update.params[0], state); + callEnv.set(app.update.params[1], event); + const newState = evaluate(app.update.body, callEnv, source); - state = newState; - rerender(); + state = newState; + rerender(); + } catch (error) { + if (error instanceof CGError) { + console.error(error.format()); + } else { + throw error; + } + } } canvas.addEventListener('click', (e) => { diff --git a/src/ui-components.cg b/src/ui-components.cg index eabb3c5..675d03b 100644 --- a/src/ui-components.cg +++ b/src/ui-components.cg @@ -11,7 +11,7 @@ button = config \ }; insertChar = text pos char \ - _ = debug "insertChar" char; + # _ = debug "insertChar" char; before = slice text 0 pos; after = slice text pos (len text); before & char & after;