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
|
|
@ -1,6 +1,6 @@
|
|||
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern } from '../value';
|
||||
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 { Result } from '../result';
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ export type ParseError =
|
|||
| { tag: "ExpectedPatternAssignmentSymbol", span: Span } // Expected '=' in pattern assignment
|
||||
| { tag: "ExpectedPatternBindingSymbol", span: Span } // Expected '.' in pattern binding
|
||||
| { tag: "ExpectedFunctionCallStart", span: Span } // Expected '(' after function name
|
||||
| { tag: "ExpectedRecordOpen", span: Span } // Expected '(' after ':'
|
||||
| { tag: "ExpectedLetBlockOpen", span: Span } // Expected '{' after 'let'
|
||||
| { tag: "ExpectedLetBlockClose", span: Span } // Expected '}' at end of 'let' expression
|
||||
| { tag: "ExpectedMatchBlockOpen", span: Span } // Expected '{' after 'match'
|
||||
|
|
@ -35,6 +36,7 @@ export type ParseError =
|
|||
| { tag: "ExpectedApplySeparator", span: Span } // Expected '!' inside 'apply'
|
||||
| { tag: "UnexpectedTagPattern", span: Span } // Found #tag where product pattern expected
|
||||
| { 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
|
||||
|
||||
// TODO: Delete?
|
||||
|
|
@ -44,6 +46,7 @@ export type Expectation =
|
|||
| "ExpectedPatternAssignmentSymbol"
|
||||
| "ExpectedPatternBindingSymbol"
|
||||
| "ExpectedFunctionCallStart"
|
||||
| "ExpectedRecordOpen"
|
||||
| "ExpectedLetBlockOpen"
|
||||
| "ExpectedLetBlockClose"
|
||||
| "ExpectedMatchBlockOpen"
|
||||
|
|
@ -53,6 +56,7 @@ export type Expectation =
|
|||
| "UnexpectedTagPattern"
|
||||
| "ExpectedPattern"
|
||||
| "ExpectedRecordField"
|
||||
| "ExpectedRecordPatternOpen"
|
||||
|
||||
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:
|
||||
// tuples: ( a, b, c ) -> `)`
|
||||
// records: { f0 = e0, f1 = e1 } -> `}`
|
||||
// records: :( f0 = e0, f1 = e1 ) -> `}`
|
||||
// function call: f(a, b, c) -> `)`
|
||||
// let-binding: let { p = e . body } -> `.`
|
||||
// fn-asbtraction: fn { p0, p1 . body } -> `.`
|
||||
|
|
@ -110,6 +114,7 @@ function delimitedTerminalSequence<A>(cursor: Cursor, delimiter: CodePoint, term
|
|||
}
|
||||
|
||||
while (true) {
|
||||
|
||||
const item = p(cursor); // `p` should be responsible for getting rid of whitespace after it has done its work
|
||||
items.push(item);
|
||||
|
||||
|
|
@ -154,6 +159,12 @@ function patternStartToken(cursor: Cursor): PatternStartToken {
|
|||
return token;
|
||||
}
|
||||
|
||||
function identifier(cursor: Cursor, kind: IdentifierKind): { name: string, span: Span } {
|
||||
const result = identifierScanner(cursor, kind);
|
||||
skipWhitespaceAndComments(cursor);
|
||||
return result;
|
||||
}
|
||||
|
||||
// === Expression Parsers ===
|
||||
|
||||
function expr(cursor: Cursor): Expr {
|
||||
|
|
@ -185,10 +196,6 @@ function expr(cursor: Cursor): Expr {
|
|||
// e.g. (a, b, c)
|
||||
const items = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_CLOSE_PAREN, expr);
|
||||
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":
|
||||
// e.g. my_func(arg1, arg2)
|
||||
// parse a `,` delimiter sequence of expr
|
||||
|
|
@ -203,6 +210,17 @@ function expr(cursor: Cursor): Expr {
|
|||
return Expr.call(token.name, args);
|
||||
case "keyword":
|
||||
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":
|
||||
// let { p0 = e0, p1 = e2 . body }
|
||||
if (!tryConsume(cursor, char('{'))) {
|
||||
|
|
@ -320,7 +338,7 @@ function productPatternBinding(cursor: Cursor): ExprBinding {
|
|||
|
||||
function fieldAssignment(cursor: Cursor): FieldAssignment {
|
||||
// `f = e`
|
||||
const { name, span } = identifier(cursor, 'identifier');
|
||||
const { name, span } = identifier(cursor, 'field_name');
|
||||
|
||||
if (!tryConsume(cursor, char('='))) {
|
||||
throw {
|
||||
|
|
@ -385,27 +403,42 @@ function finishProductPattern(cursor: Cursor, token: PatternStartToken): Product
|
|||
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":
|
||||
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":
|
||||
throw { tag: "ExpectedPattern", span: token.span } as ParseError;
|
||||
}
|
||||
}
|
||||
|
||||
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('='))) {
|
||||
const p = productPattern(cursor);
|
||||
return ProductPattern.fieldPattern(name, p);
|
||||
} else {
|
||||
// Punning: { a } -> { a = a }
|
||||
// Punning: :( a ) -> :( a = a )
|
||||
return ProductPattern.fieldPattern(name, ProductPattern.any(name));
|
||||
}
|
||||
}
|
||||
|
|
@ -422,7 +455,7 @@ export function parse(input: string): Result<Expr, ParseError> {
|
|||
if (!cursor.eof()) {
|
||||
return Result.error({
|
||||
tag: "UnexpectedToken",
|
||||
expected: "End of File",
|
||||
expected: "EndOfFile",
|
||||
span: cursor.makeSpan(cursor.currentLocation())
|
||||
} as ParseError);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue