diff --git a/src/ast.ts b/src/ast.ts index 77d74f4..801d4e5 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -59,13 +59,14 @@ export type Pattern = | { kind: 'literal', value: number | string } | { kind: 'constructor', name: string, args: Pattern[] } | { kind: 'list', elements: Pattern[] } + | { kind: 'list-spread', head: Pattern[], spread: string } | { kind: 'record', fields: { [key: string]: Pattern } } // Data Structures export type List = { kind: 'list' - elements: AST[] + elements: (AST | { spread: AST })[] } export type Record = { diff --git a/src/interpreter.ts b/src/interpreter.ts index 523da37..1175b6a 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -16,11 +16,25 @@ export function evaluate(ast: AST, env: Env): Value { return val; } - case 'list': - return { - kind: 'list', - elements: ast.elements.map(el => evaluate(el, env)) - }; + case 'list': { + const elements: Value[] = []; + + for (const item of ast.elements) { + // Spread + if ('spread' in item) { + const spreadValue = evaluate(item.spread, env); + + if (spreadValue.kind !== 'list') + throw new Error('can only spread lists'); + + elements.push(...spreadValue.elements); + } else { + elements.push(evaluate(item, env)); + } + } + + return { kind: 'list', elements }; + } case 'record': const fields: { [key: string]: Value } = {}; @@ -226,6 +240,23 @@ function matchPattern(value: Value, pattern: Pattern): Bindings | null { return bindings; } + case 'list-spread': { + if (value.kind !== 'list') return null; + if (value.elements.length < pattern.head.length) return null; + + const bindings: Bindings = {}; + for (let i = 0; i < pattern.head.length; i++) { + const elemBindings = matchPattern(value.elements[i], pattern.head[i]); + if (elemBindings === null) return null; + Object.assign(bindings, elemBindings); + } + + const rest = value.elements.slice(pattern.head.length); + bindings[pattern.spread] = { kind: 'list', elements: rest }; + + return bindings; + } + case 'record': { if (value.kind !== 'record') return null; diff --git a/src/lexer.ts b/src/lexer.ts index f94d030..6294079 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -33,6 +33,7 @@ export type Token = | { kind: 'ampersand' } | { kind: 'underscore' } | { kind: 'dot' } + | { kind: 'dot-dot-dot' } | { kind: 'at' } // Anithmetic @@ -148,7 +149,7 @@ export function tokenize(source: string): Token[] { switch (char) { case '>': { - if (source[i + 1] == '=') { + if (source[i + 1] === '=') { tokens.push({ kind: 'greater-equals' }); i++; } else { @@ -157,7 +158,7 @@ export function tokenize(source: string): Token[] { break; } case '<': { - if (source[i + 1] == '=') { + if (source[i + 1] === '=') { tokens.push({ kind: 'less-equals' }); i++; } else { @@ -166,7 +167,7 @@ export function tokenize(source: string): Token[] { break; } case '=': { - if (source[i + 1] == '=') { + if (source[i + 1] === '=') { tokens.push({ kind: 'equals-equals' }); i++; } else { @@ -175,7 +176,7 @@ export function tokenize(source: string): Token[] { break; } case '!': { - if (source[i + 1] == '=') { + if (source[i + 1] === '=') { tokens.push({ kind: 'not-equals' }); i++; } else { @@ -183,6 +184,15 @@ export function tokenize(source: string): Token[] { } break; } + case '.': { + if (source[i + 1] === '.' && source[i + 2] === '.') { + tokens.push({ kind: 'dot-dot-dot' }) + i += 2; + } else { + tokens.push({ kind: 'dot' }); + } + break; + } case ':': tokens.push({ kind: 'colon' }); break; case ';': tokens.push({ kind: 'semicolon' }); break; case '\\': tokens.push({ kind: 'backslash' }); break; @@ -190,7 +200,6 @@ export function tokenize(source: string): Token[] { case '|': tokens.push({ kind: 'pipe' }); break; case ',': tokens.push({ kind: 'comma' }); break; case '&': tokens.push({ kind: 'ampersand' }); break; - case '.': tokens.push({ kind: 'dot' }); break; case '@': tokens.push({ kind: 'at' }); break; // Arithmetic diff --git a/src/main.ts b/src/main.ts index 543aa7b..3aacc27 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,21 +2,29 @@ import { evaluate } from './interpreter' import type { Env } from './env' import { tokenize } from './lexer' import { Parser } from './parser' -import cgCode from './counter.cg?raw'; import { runApp } from './runtime'; import { builtins } from './builtins'; +import counterApp from './counter.cg?raw'; +import stdlibCode from './stdlib.cg?raw'; +import testCode from './test.cg?raw'; + const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 600; document.body.appendChild(canvas); +const cgCode = stdlibCode + '\n' + testCode; + const tokens = tokenize(cgCode); const parser = new Parser(tokens); const ast = parser.parse(); console.log(ast); const env: Env = new Map(Object.entries(builtins)); +const res = evaluate(ast, env); +console.log(res); +/* const appRecord = evaluate(ast, env); console.log(appRecord); @@ -29,3 +37,4 @@ const update = appRecord.fields.update; const view = appRecord.fields.view; runApp({ init, update, view }, canvas); +*/ diff --git a/src/parser.ts b/src/parser.ts index bec33af..abfcf78 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -181,14 +181,29 @@ export class Parser { this.advance(); const elements: Pattern[] = []; let first = true; + let spreadName: string | null = null; while (this.current().kind !== 'close-bracket') { if (!first) this.expect('comma'); first = false; + + // Spread + if (this.current().kind === 'dot-dot-dot') { + this.advance(); + const nameToken = this.expect('ident'); + spreadName = (nameToken as { value: string }).value; + break; + } + elements.push(this.parsePattern()); } this.expect('close-bracket'); + + if (spreadName !== null) { + return { kind: 'list-spread', head: elements, spread: spreadName }; + } + return { kind: 'list', elements }; } @@ -356,7 +371,7 @@ export class Parser { if (token.kind === 'open-bracket') { this.advance(); - const items: AST[] = []; + const items: (AST | { spread: AST })[] = []; let first = true; while (this.current().kind !== 'close-bracket') { @@ -365,7 +380,14 @@ export class Parser { } first = false; - items.push(this.parseExpression()); + // Spread + if (this.current().kind === 'dot-dot-dot') { + this.advance(); + const expr = this.parseExpression(); + items.push({ spread: expr }) + } else { + items.push(this.parseExpression()); + } } this.expect('close-bracket'); diff --git a/src/stdlib.cg b/src/stdlib.cg new file mode 100644 index 0000000..fd8f575 --- /dev/null +++ b/src/stdlib.cg @@ -0,0 +1,9 @@ +map = f list \ list + | [] \ [] + | [x, ...xs] \ [f x, ...map f xs]; + +filter = f list \ list + | [] \ [] + | [x, ...xs] \ (f x + | True \ [x, ...filter f xs] + | False \ filter f xs); diff --git a/src/test.cg b/src/test.cg new file mode 100644 index 0000000..4795b8c --- /dev/null +++ b/src/test.cg @@ -0,0 +1,7 @@ +add1 = x \ x + 1; + +mapped = map add1 [1, 2, 3, 4, 5]; + +isEven = x \ x % 2 == 0; + +filter isEven mapped