Better errors!
This commit is contained in:
parent
c44f06268f
commit
9d1b079361
8 changed files with 305 additions and 132 deletions
61
src/ast.ts
61
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}`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
src/error.ts
Normal file
44
src/error.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
119
src/lexer.ts
119
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;
|
||||
|
|
|
|||
41
src/main.ts
41
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);
|
||||
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')
|
||||
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 view = appRecord.fields.view;
|
||||
const init = appRecord.fields.init;
|
||||
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 { 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) {
|
|||
}
|
||||
};
|
||||
|
||||
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);
|
||||
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);
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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…
Add table
Add a link
Reference in a new issue