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 = {
|
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}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
119
src/lexer.ts
119
src/lexer.ts
|
|
@ -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;
|
||||||
|
|
|
||||||
41
src/main.ts
41
src/main.ts
|
|
@ -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 env: Env = new Map(Object.entries(builtins));
|
||||||
const appRecord = evaluate(ast, env);
|
const appRecord = evaluate(ast, env, cgCode);
|
||||||
console.log("appRecord", appRecord);
|
// console.log("appRecord", appRecord);
|
||||||
|
|
||||||
if (appRecord.kind !== 'record')
|
if (appRecord.kind !== 'record')
|
||||||
throw new Error('Expected record');
|
throw new Error('Expected record');
|
||||||
|
|
||||||
const init = appRecord.fields.init;
|
const init = appRecord.fields.init;
|
||||||
const update = appRecord.fields.update;
|
const update = appRecord.fields.update;
|
||||||
const view = appRecord.fields.view;
|
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…
Add table
Add a link
Reference in a new issue