Change record syntax, fix a few bugs
This commit is contained in:
parent
13a66f2d16
commit
e389e46852
7 changed files with 212 additions and 114 deletions
|
|
@ -44,8 +44,9 @@ fn
|
||||||
match
|
match
|
||||||
=
|
=
|
||||||
|
|
|
|
||||||
|
:
|
||||||
```
|
```
|
||||||
Note that `=` and `|` are treated as keywords, not symbols. So these can occur in identifiers.
|
Note that `=`, `|`, and `:` are treated as keywords, not symbols. So these can occur in identifiers.
|
||||||
|
|
||||||
This has strange consequences, for example
|
This has strange consequences, for example
|
||||||
```
|
```
|
||||||
|
|
@ -105,9 +106,9 @@ let {
|
||||||
( , "hello" , " ", "world" , ) // this is also technically valid syntax - the commas at the start and end are optional.
|
( , "hello" , " ", "world" , ) // this is also technically valid syntax - the commas at the start and end are optional.
|
||||||
|
|
||||||
// records
|
// records
|
||||||
{ x = 123, y = 512 }
|
:( x = 123, y = 512 ) // you can read `:` as `record`. We could have adopted verbose syntax `record ( x = 123, y =512 )`, but we shortened it to `:`.
|
||||||
{} // different from (). Perhaps in the future I'll make them equivalent or... disallow one of them. But right now these are different.
|
:() // different from (). Perhaps in the future I'll make them equivalent or... disallow one of them. But right now these are different.
|
||||||
{ name = "Conan", position = { x = 5, y = 6 } }
|
:( name = "Conan", position = :( x = 5, y = 6 ) )
|
||||||
|
|
||||||
// tags (zero-ary constructors)
|
// tags (zero-ary constructors)
|
||||||
#true
|
#true
|
||||||
|
|
@ -140,9 +141,9 @@ fn is-some?(xs) {
|
||||||
// patterns
|
// patterns
|
||||||
x
|
x
|
||||||
(x, y, z)
|
(x, y, z)
|
||||||
{ foo , bar }
|
:( foo , bar )
|
||||||
{ foo = x, bar } // equivalent to { foo = x, bar = bar }
|
:( foo = x, bar ) // equivalent to :( foo = x, bar = bar )
|
||||||
{ foo = _, bar = (x, y, z) }
|
:( foo = _, bar = (x, y, z) )
|
||||||
|
|
||||||
// lambdas/anonymous-functions
|
// lambdas/anonymous-functions
|
||||||
fn { x . $x } // identity function
|
fn { x . $x } // identity function
|
||||||
|
|
@ -195,7 +196,7 @@ top-fn-call := identifier`(` args `)`
|
||||||
|
|
||||||
tuple-expr := `(` args `)`
|
tuple-expr := `(` args `)`
|
||||||
|
|
||||||
record-expr := `{` list-sep-by(field, `,`) `}`
|
record-expr := `:` `(` list-sep-by(field, `,`) `)`
|
||||||
field := variable-identifier `=` expr
|
field := variable-identifier `=` expr
|
||||||
|
|
||||||
tag-expr := `#`tag-identifier // note how we don't allow a space between # and the identifier
|
tag-expr := `#`tag-identifier // note how we don't allow a space between # and the identifier
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export function exprToString(expr: Expr): string {
|
||||||
return literalToString(expr.literal);
|
return literalToString(expr.literal);
|
||||||
|
|
||||||
case "var_use":
|
case "var_use":
|
||||||
return expr.name;
|
return `\$${expr.name}`;
|
||||||
|
|
||||||
case "call":
|
case "call":
|
||||||
return `${expr.name}(${expr.args.map(exprToString).join(", ")})`;
|
return `${expr.name}(${expr.args.map(exprToString).join(", ")})`;
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,63 @@
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import { parse } from '../parser/parser';
|
import { parse } from '../parser/parser';
|
||||||
import { exprToString } from '../debug/expr_show';
|
import { exprToString } from '../debug/expr_show';
|
||||||
|
import { valueToString } from '../debug/value_show';
|
||||||
|
import { eval_start, Program } from '../value';
|
||||||
import { Result } from '../result';
|
import { Result } from '../result';
|
||||||
|
|
||||||
// Helper to calculate line/col from an absolute offset
|
|
||||||
function getLineCol(text: string, offset: number) {
|
|
||||||
let line = 1;
|
|
||||||
let col = 1;
|
|
||||||
for (let i = 0; i < offset && i < text.length; i++) {
|
|
||||||
if (text[i] === '\n') {
|
|
||||||
line++;
|
|
||||||
col = 1;
|
|
||||||
} else {
|
|
||||||
col++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { line, col };
|
|
||||||
}
|
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout,
|
output: process.stdout,
|
||||||
prompt: 'expr> '
|
prompt: '> '
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("=== Quick & Dirty Parser REPL ===");
|
// We create one persistent program context
|
||||||
console.log("Type an expression to parse and verify round-trip stringification.");
|
const program = Program.makeEmpty();
|
||||||
|
|
||||||
|
console.log("=== Evaluator REPL ===");
|
||||||
|
console.log("Input -> Parse -> Eval -> Value");
|
||||||
console.log("Ctrl+C to exit.\n");
|
console.log("Ctrl+C to exit.\n");
|
||||||
|
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
|
|
||||||
rl.on('line', (lineInput) => {
|
rl.on('line', (lineInput) => {
|
||||||
const trimmed = lineInput.trim();
|
const trimmed = lineInput.trim();
|
||||||
if (trimmed) {
|
if (!trimmed) {
|
||||||
|
rl.prompt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = parse(trimmed);
|
// === 1. PARSE ===
|
||||||
|
const parseResult = parse(trimmed);
|
||||||
|
|
||||||
if (result.tag === "ok") {
|
if (parseResult.tag === "error") {
|
||||||
const ast = result.value;
|
const err = parseResult.error;
|
||||||
const reconstructed = exprToString(ast);
|
console.log(`\n❌ [Parse Error]:`, err);
|
||||||
|
// (Optional: Reuse your line/col logic here)
|
||||||
|
rl.prompt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`\n✅ Parsed Successfully:`);
|
const ast = parseResult.value;
|
||||||
console.log(` ${reconstructed}`);
|
console.log(`\nAST: ${exprToString(ast)}`);
|
||||||
|
|
||||||
|
// === 2. EVALUATE ===
|
||||||
|
const evalResult = eval_start(program, ast);
|
||||||
|
|
||||||
|
if (evalResult.tag === "ok") {
|
||||||
|
console.log(`VAL: ${valueToString(evalResult.value)}`);
|
||||||
} else {
|
} else {
|
||||||
const err = result.error;
|
const err = evalResult.error;
|
||||||
// FIX: Calculate line/col manually using the input string
|
console.log(`\n🔥 [Runtime Error]:`, err);
|
||||||
const { line, col } = getLineCol(trimmed, err.span.start);
|
}
|
||||||
const loc = `${line}:${col}`;
|
|
||||||
|
|
||||||
console.log(`\n❌ Parse Error [${err.tag}] at ${loc}`);
|
|
||||||
|
|
||||||
if ('expected' in err) {
|
|
||||||
console.log(` Expected: ${(err as any).expected}`);
|
|
||||||
}
|
|
||||||
if ('reason' in err) {
|
|
||||||
console.log(` Reason: ${(err as any).reason}`);
|
|
||||||
}
|
|
||||||
if ('received' in err) {
|
|
||||||
// Useful to see what char we actually got (print char code if needed)
|
|
||||||
console.log(` Received CodePoint: ${(err as any).received}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`\n🔥 CRASH (Uncaught Exception):`);
|
console.log(`\n💥 [System Crash]:`);
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.log();
|
console.log("");
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
}).on('close', () => {
|
|
||||||
console.log('Bye!');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
55
src/debug/value_show.ts
Normal file
55
src/debug/value_show.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
// src/debug/value_string.ts
|
||||||
|
|
||||||
|
import { Value, Env, Closure, EnvFrame } from '../value';
|
||||||
|
import { exprToString, productPatternToString } from './expr_show';
|
||||||
|
|
||||||
|
export function valueToString(val: Value): string {
|
||||||
|
switch (val.tag) {
|
||||||
|
case "number": return val.value.toString();
|
||||||
|
case "string": return `"${val.value}"`;
|
||||||
|
case "tag": return `#${val.tag_name}`;
|
||||||
|
case "tagged": return `#${val.tag_name} ${valueToString(val.value)}`;
|
||||||
|
case "tuple": return `(${val.values.map(valueToString).join(", ")})`;
|
||||||
|
case "record": {
|
||||||
|
const entries = Array.from(val.fields.entries())
|
||||||
|
.map(([k, v]) => `${k} = ${valueToString(v)}`)
|
||||||
|
.join(", ");
|
||||||
|
return `{ ${entries} }`;
|
||||||
|
}
|
||||||
|
case "closure": return closureToString(val.closure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closureToString(c: Closure): string {
|
||||||
|
const params = c.parameters.map(productPatternToString).join(", ");
|
||||||
|
const envStr = envToString(c.env);
|
||||||
|
// We represent the closure as the code + a summary of its captured scope
|
||||||
|
return `fn { ${params} . ${exprToString(c.body)} } [captured: ${envStr}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function envToString(env: Env): string {
|
||||||
|
if (env.tag === "nil") return "∅";
|
||||||
|
|
||||||
|
const frames: string[] = [];
|
||||||
|
let current: Env = env;
|
||||||
|
|
||||||
|
while (current.tag === "frame") {
|
||||||
|
frames.push(frameToString(current.frame));
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows stack from inner-most to outer-most
|
||||||
|
return frames.join(" ⮕ ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function frameToString(frame: EnvFrame): string {
|
||||||
|
const entries = Array.from(frame.entries());
|
||||||
|
if (entries.length === 0) return "{}";
|
||||||
|
|
||||||
|
const formattedEntries = entries.map(([name, val]) => {
|
||||||
|
// We call valueToString here to show the actual data
|
||||||
|
return `${name} = ${valueToString(val)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `{ ${formattedEntries.join(", ")} }`;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern } from '../value';
|
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern } from '../value';
|
||||||
import { Cursor } from './cursor';
|
import { Cursor } from './cursor';
|
||||||
import { ExprScanError, exprStart, ExprStartToken, identifier, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, skipWhitespaceAndComments } from './scanner';
|
import { ExprScanError, exprStart, ExprStartToken, IdentifierKind, identifierScanner, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, skipWhitespaceAndComments } from './scanner';
|
||||||
import { char, CodePoint, SourceText, Span } from './source_text';
|
import { char, CodePoint, SourceText, Span } from './source_text';
|
||||||
import { Result } from '../result';
|
import { Result } from '../result';
|
||||||
|
|
||||||
|
|
@ -25,6 +25,7 @@ export type ParseError =
|
||||||
| { tag: "ExpectedPatternAssignmentSymbol", span: Span } // Expected '=' in pattern assignment
|
| { tag: "ExpectedPatternAssignmentSymbol", span: Span } // Expected '=' in pattern assignment
|
||||||
| { tag: "ExpectedPatternBindingSymbol", span: Span } // Expected '.' in pattern binding
|
| { tag: "ExpectedPatternBindingSymbol", span: Span } // Expected '.' in pattern binding
|
||||||
| { tag: "ExpectedFunctionCallStart", span: Span } // Expected '(' after function name
|
| { tag: "ExpectedFunctionCallStart", span: Span } // Expected '(' after function name
|
||||||
|
| { tag: "ExpectedRecordOpen", span: Span } // Expected '(' after ':'
|
||||||
| { tag: "ExpectedLetBlockOpen", span: Span } // Expected '{' after 'let'
|
| { tag: "ExpectedLetBlockOpen", span: Span } // Expected '{' after 'let'
|
||||||
| { tag: "ExpectedLetBlockClose", span: Span } // Expected '}' at end of 'let' expression
|
| { tag: "ExpectedLetBlockClose", span: Span } // Expected '}' at end of 'let' expression
|
||||||
| { tag: "ExpectedMatchBlockOpen", span: Span } // Expected '{' after 'match'
|
| { tag: "ExpectedMatchBlockOpen", span: Span } // Expected '{' after 'match'
|
||||||
|
|
@ -35,6 +36,7 @@ export type ParseError =
|
||||||
| { tag: "ExpectedApplySeparator", span: Span } // Expected '!' inside 'apply'
|
| { tag: "ExpectedApplySeparator", span: Span } // Expected '!' inside 'apply'
|
||||||
| { tag: "UnexpectedTagPattern", span: Span } // Found #tag where product pattern expected
|
| { tag: "UnexpectedTagPattern", span: Span } // Found #tag where product pattern expected
|
||||||
| { tag: "ExpectedPattern", span: Span } // EOF or invalid start of pattern
|
| { tag: "ExpectedPattern", span: Span } // EOF or invalid start of pattern
|
||||||
|
| { tag: "ExpectedRecordPatternOpen", span: Span } // Expected '(' at start of record pattern
|
||||||
| { tag: "ExpectedRecordField", span: Span }; // Expected identifier in record pattern
|
| { tag: "ExpectedRecordField", span: Span }; // Expected identifier in record pattern
|
||||||
|
|
||||||
// TODO: Delete?
|
// TODO: Delete?
|
||||||
|
|
@ -44,6 +46,7 @@ export type Expectation =
|
||||||
| "ExpectedPatternAssignmentSymbol"
|
| "ExpectedPatternAssignmentSymbol"
|
||||||
| "ExpectedPatternBindingSymbol"
|
| "ExpectedPatternBindingSymbol"
|
||||||
| "ExpectedFunctionCallStart"
|
| "ExpectedFunctionCallStart"
|
||||||
|
| "ExpectedRecordOpen"
|
||||||
| "ExpectedLetBlockOpen"
|
| "ExpectedLetBlockOpen"
|
||||||
| "ExpectedLetBlockClose"
|
| "ExpectedLetBlockClose"
|
||||||
| "ExpectedMatchBlockOpen"
|
| "ExpectedMatchBlockOpen"
|
||||||
|
|
@ -53,6 +56,7 @@ export type Expectation =
|
||||||
| "UnexpectedTagPattern"
|
| "UnexpectedTagPattern"
|
||||||
| "ExpectedPattern"
|
| "ExpectedPattern"
|
||||||
| "ExpectedRecordField"
|
| "ExpectedRecordField"
|
||||||
|
| "ExpectedRecordPatternOpen"
|
||||||
|
|
||||||
export type Parser<T> = (cursor: Cursor) => T
|
export type Parser<T> = (cursor: Cursor) => T
|
||||||
|
|
||||||
|
|
@ -92,7 +96,7 @@ function delimitedTerminalSequence<A>(cursor: Cursor, delimiter: CodePoint, term
|
||||||
//
|
//
|
||||||
// All our use-cases always have a well-defined terminator character:
|
// All our use-cases always have a well-defined terminator character:
|
||||||
// tuples: ( a, b, c ) -> `)`
|
// tuples: ( a, b, c ) -> `)`
|
||||||
// records: { f0 = e0, f1 = e1 } -> `}`
|
// records: :( f0 = e0, f1 = e1 ) -> `}`
|
||||||
// function call: f(a, b, c) -> `)`
|
// function call: f(a, b, c) -> `)`
|
||||||
// let-binding: let { p = e . body } -> `.`
|
// let-binding: let { p = e . body } -> `.`
|
||||||
// fn-asbtraction: fn { p0, p1 . body } -> `.`
|
// fn-asbtraction: fn { p0, p1 . body } -> `.`
|
||||||
|
|
@ -110,6 +114,7 @@ function delimitedTerminalSequence<A>(cursor: Cursor, delimiter: CodePoint, term
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
const item = p(cursor); // `p` should be responsible for getting rid of whitespace after it has done its work
|
const item = p(cursor); // `p` should be responsible for getting rid of whitespace after it has done its work
|
||||||
items.push(item);
|
items.push(item);
|
||||||
|
|
||||||
|
|
@ -154,6 +159,12 @@ function patternStartToken(cursor: Cursor): PatternStartToken {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function identifier(cursor: Cursor, kind: IdentifierKind): { name: string, span: Span } {
|
||||||
|
const result = identifierScanner(cursor, kind);
|
||||||
|
skipWhitespaceAndComments(cursor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// === Expression Parsers ===
|
// === Expression Parsers ===
|
||||||
|
|
||||||
function expr(cursor: Cursor): Expr {
|
function expr(cursor: Cursor): Expr {
|
||||||
|
|
@ -185,10 +196,6 @@ function expr(cursor: Cursor): Expr {
|
||||||
// e.g. (a, b, c)
|
// e.g. (a, b, c)
|
||||||
const items = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_CLOSE_PAREN, expr);
|
const items = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_CLOSE_PAREN, expr);
|
||||||
return Expr.tuple(items);
|
return Expr.tuple(items);
|
||||||
case "record_start":
|
|
||||||
// e.g. { x = 1, y = 2 }
|
|
||||||
const fields = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_CLOSE_BRACE, fieldAssignment);
|
|
||||||
return Expr.record(fields);
|
|
||||||
case "function_name":
|
case "function_name":
|
||||||
// e.g. my_func(arg1, arg2)
|
// e.g. my_func(arg1, arg2)
|
||||||
// parse a `,` delimiter sequence of expr
|
// parse a `,` delimiter sequence of expr
|
||||||
|
|
@ -203,6 +210,17 @@ function expr(cursor: Cursor): Expr {
|
||||||
return Expr.call(token.name, args);
|
return Expr.call(token.name, args);
|
||||||
case "keyword":
|
case "keyword":
|
||||||
switch (token.kw) {
|
switch (token.kw) {
|
||||||
|
case ":":
|
||||||
|
// e.g. :( x = 1, y = 2 )
|
||||||
|
// or : ( x = 1, y = 2 )
|
||||||
|
if (!tryConsume(cursor, char('('))) {
|
||||||
|
throw {
|
||||||
|
tag: "ExpectedRecordOpen",
|
||||||
|
span: cursor.makeSpan(cursor.currentLocation())
|
||||||
|
} as ParseError;
|
||||||
|
}
|
||||||
|
const fields = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_CLOSE_PAREN, fieldAssignment);
|
||||||
|
return Expr.record(fields);
|
||||||
case "let":
|
case "let":
|
||||||
// let { p0 = e0, p1 = e2 . body }
|
// let { p0 = e0, p1 = e2 . body }
|
||||||
if (!tryConsume(cursor, char('{'))) {
|
if (!tryConsume(cursor, char('{'))) {
|
||||||
|
|
@ -320,7 +338,7 @@ function productPatternBinding(cursor: Cursor): ExprBinding {
|
||||||
|
|
||||||
function fieldAssignment(cursor: Cursor): FieldAssignment {
|
function fieldAssignment(cursor: Cursor): FieldAssignment {
|
||||||
// `f = e`
|
// `f = e`
|
||||||
const { name, span } = identifier(cursor, 'identifier');
|
const { name, span } = identifier(cursor, 'field_name');
|
||||||
|
|
||||||
if (!tryConsume(cursor, char('='))) {
|
if (!tryConsume(cursor, char('='))) {
|
||||||
throw {
|
throw {
|
||||||
|
|
@ -385,27 +403,42 @@ function finishProductPattern(cursor: Cursor, token: PatternStartToken): Product
|
||||||
return ProductPattern.tuple(items);
|
return ProductPattern.tuple(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "record_start": {
|
|
||||||
// { a = p, b }
|
|
||||||
const fields = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_CLOSE_BRACE, recordPatternField);
|
|
||||||
return ProductPattern.record(fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "tag":
|
case "tag":
|
||||||
throw { tag: "UnexpectedTagPattern", span: token.span } as ParseError;
|
throw { tag: "UnexpectedTagPattern", span: token.span } as ParseError;
|
||||||
|
|
||||||
|
case "keyword": {
|
||||||
|
switch (token.kw) {
|
||||||
|
case ":": {
|
||||||
|
// :( a = p, b )
|
||||||
|
// TODO: parse open-paren
|
||||||
|
if (!tryConsume(cursor, char('{'))) {
|
||||||
|
throw {
|
||||||
|
tag: "ExpectedRecordPatternOpen",
|
||||||
|
span: cursor.makeSpan(cursor.currentLocation())
|
||||||
|
} as ParseError;
|
||||||
|
}
|
||||||
|
const fields = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_CLOSE_PAREN, recordPatternField);
|
||||||
|
return ProductPattern.record(fields);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// These keywords CANNOT start a pattern.
|
||||||
|
throw { tag: "ExpectedPattern", span: token.span } as ParseError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
case "EOF":
|
case "EOF":
|
||||||
throw { tag: "ExpectedPattern", span: token.span } as ParseError;
|
throw { tag: "ExpectedPattern", span: token.span } as ParseError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordPatternField(cursor: Cursor): FieldPattern {
|
function recordPatternField(cursor: Cursor): FieldPattern {
|
||||||
const { name, span } = identifier(cursor, 'identifier'); // Reuse existing identifier scanner
|
const { name, span } = identifier(cursor, 'field_name');
|
||||||
if (tryConsume(cursor, char('='))) {
|
if (tryConsume(cursor, char('='))) {
|
||||||
const p = productPattern(cursor);
|
const p = productPattern(cursor);
|
||||||
return ProductPattern.fieldPattern(name, p);
|
return ProductPattern.fieldPattern(name, p);
|
||||||
} else {
|
} else {
|
||||||
// Punning: { a } -> { a = a }
|
// Punning: :( a ) -> :( a = a )
|
||||||
return ProductPattern.fieldPattern(name, ProductPattern.any(name));
|
return ProductPattern.fieldPattern(name, ProductPattern.any(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -422,7 +455,7 @@ export function parse(input: string): Result<Expr, ParseError> {
|
||||||
if (!cursor.eof()) {
|
if (!cursor.eof()) {
|
||||||
return Result.error({
|
return Result.error({
|
||||||
tag: "UnexpectedToken",
|
tag: "UnexpectedToken",
|
||||||
expected: "End of File",
|
expected: "EndOfFile",
|
||||||
span: cursor.makeSpan(cursor.currentLocation())
|
span: cursor.makeSpan(cursor.currentLocation())
|
||||||
} as ParseError);
|
} as ParseError);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ const DELIMITER_CHARS = ["(", ")", "{", "}", ".", ",", "@", "$", "#", '"', "\\"]
|
||||||
export type Delimiter = typeof DELIMITER_CHARS[number];
|
export type Delimiter = typeof DELIMITER_CHARS[number];
|
||||||
const DELIMITER_SET: Set<CodePoint> = new Set(DELIMITER_CHARS.map(c => char(c)));
|
const DELIMITER_SET: Set<CodePoint> = new Set(DELIMITER_CHARS.map(c => char(c)));
|
||||||
|
|
||||||
const KEYWORD_LIST = ["let" , "fn" , "match" , "apply" , "=" , "|" , "!"] as const;
|
const KEYWORD_LIST = ["let" , "fn" , "match" , "apply" , "=" , "|" , "!", ":"] as const;
|
||||||
export type Keyword = typeof KEYWORD_LIST[number];
|
export type Keyword = typeof KEYWORD_LIST[number];
|
||||||
const KEYWORD_SET: Set<string> = new Set(KEYWORD_LIST);
|
const KEYWORD_SET: Set<string> = new Set(KEYWORD_LIST);
|
||||||
|
|
||||||
|
|
@ -61,13 +61,14 @@ export type ExprScanError =
|
||||||
|
|
||||||
// What kind of identifier were we trying to parse?
|
// What kind of identifier were we trying to parse?
|
||||||
export type IdentifierKind =
|
export type IdentifierKind =
|
||||||
| "identifier"
|
|
||||||
| "variable_use"
|
| "variable_use"
|
||||||
|
| "field_name"
|
||||||
| "tag_construction"
|
| "tag_construction"
|
||||||
| "function_call"
|
| "function_call"
|
||||||
| "pattern_binding";
|
| "pattern_binding";
|
||||||
|
|
||||||
export type IdentifierErrorReason =
|
export type IdentifierErrorReason =
|
||||||
|
| { tag: "Empty" }
|
||||||
| { tag: "StartsWithDigit" }
|
| { tag: "StartsWithDigit" }
|
||||||
| { tag: "IsKeyword", kw: Keyword }
|
| { tag: "IsKeyword", kw: Keyword }
|
||||||
|
|
||||||
|
|
@ -80,7 +81,6 @@ export type ExprStartToken =
|
||||||
| { tag: "variable_use", name: string, span: Span }
|
| { tag: "variable_use", name: string, span: Span }
|
||||||
| { tag: "tag", name: string, span: Span }
|
| { tag: "tag", name: string, span: Span }
|
||||||
| { tag: "tuple_start", span: Span }
|
| { tag: "tuple_start", span: Span }
|
||||||
| { tag: "record_start", span: Span }
|
|
||||||
| { tag: "keyword", kw: Keyword, span: Span }
|
| { tag: "keyword", kw: Keyword, span: Span }
|
||||||
// TODO: ger rid of EOF
|
// TODO: ger rid of EOF
|
||||||
| { tag: "EOF", span: Span }
|
| { tag: "EOF", span: Span }
|
||||||
|
|
@ -89,7 +89,7 @@ export type PatternStartToken =
|
||||||
| { tag: "pattern_binding", name: string, span: Span }
|
| { tag: "pattern_binding", name: string, span: Span }
|
||||||
| { tag: "tag", name: string, span: Span }
|
| { tag: "tag", name: string, span: Span }
|
||||||
| { tag: "tuple_start", span: Span }
|
| { tag: "tuple_start", span: Span }
|
||||||
| { tag: "record_start", span: Span }
|
| { tag: "keyword", kw: Keyword, span: Span }
|
||||||
// TODO: ger rid of EOF
|
// TODO: ger rid of EOF
|
||||||
| { tag: "EOF", span: Span };
|
| { tag: "EOF", span: Span };
|
||||||
|
|
||||||
|
|
@ -116,7 +116,7 @@ function rawIdentifier(cursor: Cursor): string {
|
||||||
// Scans raw identifier,
|
// Scans raw identifier,
|
||||||
// checks if it is a keyword,
|
// checks if it is a keyword,
|
||||||
// if it ain't, validates it into a proper identifier.
|
// if it ain't, validates it into a proper identifier.
|
||||||
function identifierOrKeyword(
|
function identifierOrKeywordScanner(
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
kind: IdentifierKind,
|
kind: IdentifierKind,
|
||||||
): { tag: "keyword", kw: Keyword, span: Span }
|
): { tag: "keyword", kw: Keyword, span: Span }
|
||||||
|
|
@ -124,6 +124,15 @@ function identifierOrKeyword(
|
||||||
const start = cursor.currentLocation();
|
const start = cursor.currentLocation();
|
||||||
const text = rawIdentifier(cursor);
|
const text = rawIdentifier(cursor);
|
||||||
const span = cursor.makeSpan(start);
|
const span = cursor.makeSpan(start);
|
||||||
|
if (text.length === 0) {
|
||||||
|
throw ({
|
||||||
|
tag: "InvalidIdentifier",
|
||||||
|
text,
|
||||||
|
kind,
|
||||||
|
reason: { tag: "Empty" },
|
||||||
|
span
|
||||||
|
} as ExprScanError);
|
||||||
|
}
|
||||||
|
|
||||||
if (KEYWORD_SET.has(text)) {
|
if (KEYWORD_SET.has(text)) {
|
||||||
return { tag: "keyword", kw: text as Keyword, span };
|
return { tag: "keyword", kw: text as Keyword, span };
|
||||||
|
|
@ -142,8 +151,8 @@ function identifierOrKeyword(
|
||||||
return { tag: "identifier", name: text, span };
|
return { tag: "identifier", name: text, span };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function identifier(cursor: Cursor, kind: IdentifierKind): { name: string, span: Span } {
|
export function identifierScanner(cursor: Cursor, kind: IdentifierKind): { name: string, span: Span } {
|
||||||
const res = identifierOrKeyword(cursor, kind);
|
const res = identifierOrKeywordScanner(cursor, kind);
|
||||||
|
|
||||||
if (res.tag === "keyword") {
|
if (res.tag === "keyword") {
|
||||||
throw ({
|
throw ({
|
||||||
|
|
@ -201,14 +210,14 @@ export function exprStart(cursor: Cursor): ExprStartToken {
|
||||||
// === variable use ===
|
// === variable use ===
|
||||||
if (c === char('$')) {
|
if (c === char('$')) {
|
||||||
cursor.next();
|
cursor.next();
|
||||||
const { name } = identifier(cursor, 'variable_use');
|
const { name } = identifierScanner(cursor, 'variable_use');
|
||||||
return { tag: "variable_use", name, span: cursor.makeSpan(start) };
|
return { tag: "variable_use", name, span: cursor.makeSpan(start) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// === tags ===
|
// === tags ===
|
||||||
if (c === char('#')) {
|
if (c === char('#')) {
|
||||||
cursor.next();
|
cursor.next();
|
||||||
const { name } = identifier(cursor, 'tag_construction');
|
const { name } = identifierScanner(cursor, 'tag_construction');
|
||||||
return { tag: "tag", name, span: cursor.makeSpan(start) };
|
return { tag: "tag", name, span: cursor.makeSpan(start) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,15 +228,9 @@ export function exprStart(cursor: Cursor): ExprStartToken {
|
||||||
return { tag: "tuple_start", span: cursor.makeSpan(start) };
|
return { tag: "tuple_start", span: cursor.makeSpan(start) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// === records ===
|
|
||||||
if (c === char('{')) {
|
|
||||||
cursor.next();
|
|
||||||
return { tag: "record_start", span: cursor.makeSpan(start) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// === keywords & identifiers ===
|
// === keywords & identifiers ===
|
||||||
// Fallthrough: it must be a keyword or a function call
|
// Fallthrough: it must be a keyword or a function call
|
||||||
const result = identifierOrKeyword(cursor, 'function_call');
|
const result = identifierOrKeywordScanner(cursor, 'function_call');
|
||||||
switch (result.tag) {
|
switch (result.tag) {
|
||||||
case "keyword":
|
case "keyword":
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -250,16 +253,10 @@ export function patternStart(cursor: Cursor): PatternStartToken {
|
||||||
return { tag: "tuple_start", span: cursor.makeSpan(start) };
|
return { tag: "tuple_start", span: cursor.makeSpan(start) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// === record ===
|
|
||||||
if (c === char('{')) {
|
|
||||||
cursor.next();
|
|
||||||
return { tag: "record_start", span: cursor.makeSpan(start) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// === tag ===
|
// === tag ===
|
||||||
if (c === char('#')) {
|
if (c === char('#')) {
|
||||||
cursor.next();
|
cursor.next();
|
||||||
const { name } = identifier(cursor, 'tag_construction');
|
const { name } = identifierScanner(cursor, 'tag_construction');
|
||||||
return { tag: "tag", name, span: cursor.makeSpan(start) };
|
return { tag: "tag", name, span: cursor.makeSpan(start) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,8 +271,14 @@ export function patternStart(cursor: Cursor): PatternStartToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
// === pattern binding ===
|
// === pattern binding ===
|
||||||
const { name } = identifier(cursor, 'pattern_binding');
|
// Fallthrough: it must be a keyword or a pattern-variable
|
||||||
return { tag: "pattern_binding", name, span: cursor.makeSpan(start) };
|
const result = identifierOrKeywordScanner(cursor, 'function_call');
|
||||||
|
switch (result.tag) {
|
||||||
|
case "keyword":
|
||||||
|
return result;
|
||||||
|
case "identifier":
|
||||||
|
return { tag: "pattern_binding", name: result.name, span: result.span };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isNextTokenExprStart(cursor: Cursor): boolean {
|
export function isNextTokenExprStart(cursor: Cursor): boolean {
|
||||||
|
|
@ -289,7 +292,6 @@ export function isNextTokenExprStart(cursor: Cursor): boolean {
|
||||||
case "variable_use":
|
case "variable_use":
|
||||||
case "tag":
|
case "tag":
|
||||||
case "tuple_start":
|
case "tuple_start":
|
||||||
case "record_start":
|
|
||||||
case "function_name": // e.g. my_func(x)
|
case "function_name": // e.g. my_func(x)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
@ -299,6 +301,7 @@ export function isNextTokenExprStart(cursor: Cursor): boolean {
|
||||||
case "fn":
|
case "fn":
|
||||||
case "match":
|
case "match":
|
||||||
case "apply":
|
case "apply":
|
||||||
|
case ":":
|
||||||
return true;
|
return true;
|
||||||
case "=":
|
case "=":
|
||||||
case "|":
|
case "|":
|
||||||
|
|
@ -327,8 +330,21 @@ export function isNextTokenProductPatternStart(cursor: Cursor): boolean {
|
||||||
switch (token.tag) {
|
switch (token.tag) {
|
||||||
case "pattern_binding":
|
case "pattern_binding":
|
||||||
case "tuple_start":
|
case "tuple_start":
|
||||||
case "record_start":
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case "keyword":
|
||||||
|
switch (token.kw) {
|
||||||
|
case ":":
|
||||||
|
return true;
|
||||||
|
case "let":
|
||||||
|
case "fn":
|
||||||
|
case "match":
|
||||||
|
case "apply":
|
||||||
|
case "=":
|
||||||
|
case "|":
|
||||||
|
case "!":
|
||||||
|
return false;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
src/value.ts
16
src/value.ts
|
|
@ -75,6 +75,12 @@ export namespace Program {
|
||||||
export function error<T>(error: Error): Result<T> { return { tag: "error", error } }
|
export function error<T>(error: Error): Result<T> { return { tag: "error", error } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeEmpty(): Program {
|
||||||
|
return {
|
||||||
|
function_definitions: new Map(),
|
||||||
|
function_definition_order: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
// TODO: Primitive functions like +, -, *, div, <, <=, ==, mod
|
// TODO: Primitive functions like +, -, *, div, <, <=, ==, mod
|
||||||
|
|
||||||
// TODO: function to create initial program (with the above primitive functions otherwise empty)
|
// TODO: function to create initial program (with the above primitive functions otherwise empty)
|
||||||
|
|
@ -181,7 +187,7 @@ export type Pattern =
|
||||||
|
|
||||||
// === Values ===
|
// === Values ===
|
||||||
|
|
||||||
type Value =
|
export type Value =
|
||||||
| { tag: "string", value: string }
|
| { tag: "string", value: string }
|
||||||
| { tag: "number", value: number }
|
| { tag: "number", value: number }
|
||||||
| { tag: "tag", tag_name: Tag }
|
| { tag: "tag", tag_name: Tag }
|
||||||
|
|
@ -190,7 +196,7 @@ type Value =
|
||||||
| { tag: "record", fields: Map<FieldName, Value> }
|
| { tag: "record", fields: Map<FieldName, Value> }
|
||||||
| { tag: "closure", closure: Closure }
|
| { tag: "closure", closure: Closure }
|
||||||
|
|
||||||
type ValueTag =
|
export type ValueTag =
|
||||||
| "string"
|
| "string"
|
||||||
| "number"
|
| "number"
|
||||||
| "tag"
|
| "tag"
|
||||||
|
|
@ -200,13 +206,13 @@ type ValueTag =
|
||||||
| "closure"
|
| "closure"
|
||||||
|
|
||||||
// Used as a Stack of frames. Basically a linked list.
|
// Used as a Stack of frames. Basically a linked list.
|
||||||
type Env =
|
export type Env =
|
||||||
| { tag: "nil" }
|
| { tag: "nil" }
|
||||||
| { tag: "frame", frame: EnvFrame, parent: Env }
|
| { tag: "frame", frame: EnvFrame, parent: Env }
|
||||||
|
|
||||||
type EnvFrame = Map<VariableName, Value>;
|
export type EnvFrame = Map<VariableName, Value>;
|
||||||
|
|
||||||
type Closure = {
|
export type Closure = {
|
||||||
env: Env,
|
env: Env,
|
||||||
parameters: ProductPattern[],
|
parameters: ProductPattern[],
|
||||||
body: Expr,
|
body: Expr,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue