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 = {
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}`
}
}

@ -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 { 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);
}
}

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

@ -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);
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);
console.log("appRecord", appRecord);
const env: Env = new Map(Object.entries(builtins));
const appRecord = evaluate(ast, env, cgCode);
// console.log("appRecord", appRecord);
if (appRecord.kind !== 'record')
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);
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;
}
}

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

@ -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) {
}
};
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);
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');
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);
const newState = evaluate(app.update.body, callEnv, source);
state = newState;
rerender();
} catch (error) {
if (error instanceof CGError) {
console.error(error.format());
} else {
throw error;
}
}
}
canvas.addEventListener('click', (e) => {

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

Loading…
Cancel
Save