Update parser for signal-expressions
This commit is contained in:
parent
94cb3bd721
commit
5e7578c4a3
4 changed files with 151 additions and 9 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// AI GENERATED
|
// AI GENERATED
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { parse, ParseError } from '../parser/parser';
|
import { parseExpr, ParseError } from '../parser/parser';
|
||||||
import { SourceText, renderSpan, sourceText } from '../parser/source_text';
|
import { SourceText, renderSpan, sourceText } from '../parser/source_text';
|
||||||
import { exprToString } from '../debug/expr_show';
|
import { exprToString } from '../debug/expr_show';
|
||||||
import { valueToString } from '../debug/value_show';
|
import { valueToString } from '../debug/value_show';
|
||||||
|
|
@ -30,7 +30,7 @@ function runSource(inputRaw: string, isRepl: boolean): boolean {
|
||||||
const text = sourceText(input);
|
const text = sourceText(input);
|
||||||
|
|
||||||
// === Parse ===
|
// === Parse ===
|
||||||
const parseResult = parse(text);
|
const parseResult = parseExpr(text);
|
||||||
|
|
||||||
if (parseResult.tag === "error") {
|
if (parseResult.tag === "error") {
|
||||||
printPrettyError(text, parseResult.error);
|
printPrettyError(text, parseResult.error);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export type Expr =
|
||||||
|
|
||||||
export type SignalExpr =
|
export type SignalExpr =
|
||||||
| { tag: "read", name: SignalName } & Meta
|
| { tag: "read", name: SignalName } & Meta
|
||||||
|
// TODO: Is `const` necesary?
|
||||||
| { tag: "const", arg: Expr } & Meta
|
| { tag: "const", arg: Expr } & Meta
|
||||||
| { tag: "let", bindings: SignalBinding[], body: Expr } & Meta
|
| { tag: "let", bindings: SignalBinding[], body: Expr } & Meta
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { Cursor } from './cursor';
|
import { Cursor } from './cursor';
|
||||||
import { ExprScanError, exprStart, ExprStartToken, IdentifierKind, identifierScanner, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, skipWhitespaceAndComments } from './scanner';
|
import { ExprScanError, exprStart, ExprStartToken, IdentifierKind, identifierScanner, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, signalExprStart, SignalExprStartToken, 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';
|
||||||
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern } from '../expr';
|
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern, SignalExpr } from '../expr';
|
||||||
import { UserFunctionDefinition } from '../program';
|
|
||||||
import { Product } from 'electron';
|
|
||||||
|
|
||||||
// CONVENTION: Every parser is responsible to consume whitespace/comments at the end.
|
// CONVENTION: Every parser is responsible to consume whitespace/comments at the end.
|
||||||
// Every parser is not responsible for cleaning up whitespace/comments at the start - only the final `parse` that's exposed to the public.
|
// Every parser is not responsible for cleaning up whitespace/comments at the start - only the final `parse` that's exposed to the public.
|
||||||
|
|
@ -23,6 +21,7 @@ export type ParseError =
|
||||||
|
|
||||||
// === Specific Context Errors ===
|
// === Specific Context Errors ===
|
||||||
| { tag: "ExpectedExpression", span: Span } // Expected start of expr (e.g. hit EOF or keyword)
|
| { tag: "ExpectedExpression", span: Span } // Expected start of expr (e.g. hit EOF or keyword)
|
||||||
|
| { tag: "ExpectedSignalExpression", span: Span } // Expected start of signal expr (e.g. hit EOF or keyword)
|
||||||
| { tag: "ExpectedFieldAssignmentSymbol", span: Span } // Expected '=' in field assignment
|
| { tag: "ExpectedFieldAssignmentSymbol", span: Span } // Expected '=' in field assignment
|
||||||
| { 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
|
||||||
|
|
@ -155,6 +154,12 @@ function exprStartToken(cursor: Cursor): ExprStartToken {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function signalExprStartToken(cursor: Cursor): SignalExprStartToken {
|
||||||
|
const token = signalExprStart(cursor);
|
||||||
|
skipWhitespaceAndComments(cursor);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
function patternStartToken(cursor: Cursor): PatternStartToken {
|
function patternStartToken(cursor: Cursor): PatternStartToken {
|
||||||
const token = patternStart(cursor);
|
const token = patternStart(cursor);
|
||||||
skipWhitespaceAndComments(cursor);
|
skipWhitespaceAndComments(cursor);
|
||||||
|
|
@ -172,7 +177,6 @@ function identifier(cursor: Cursor, kind: IdentifierKind): { name: string, span:
|
||||||
function expr(cursor: Cursor): Expr {
|
function expr(cursor: Cursor): Expr {
|
||||||
const start = cursor.currentLocation();
|
const start = cursor.currentLocation();
|
||||||
const token = exprStartToken(cursor);
|
const token = exprStartToken(cursor);
|
||||||
// TODO: You need to include the spans and perhaps other meta-info.
|
|
||||||
switch (token.tag) {
|
switch (token.tag) {
|
||||||
case "EOF":
|
case "EOF":
|
||||||
throw {
|
throw {
|
||||||
|
|
@ -291,6 +295,9 @@ function expr(cursor: Cursor): Expr {
|
||||||
|
|
||||||
const branches = delimitedTerminalSequence(cursor, DELIMITER_PIPE, TERMINATOR_CLOSE_BRACE, matchBranch);
|
const branches = delimitedTerminalSequence(cursor, DELIMITER_PIPE, TERMINATOR_CLOSE_BRACE, matchBranch);
|
||||||
return Expr.match(arg, branches, cursor.makeSpan(start))
|
return Expr.match(arg, branches, cursor.makeSpan(start))
|
||||||
|
case "let-signal":
|
||||||
|
case "signal":
|
||||||
|
case "fn-signal":
|
||||||
case "=":
|
case "=":
|
||||||
case "|":
|
case "|":
|
||||||
case "!":
|
case "!":
|
||||||
|
|
@ -303,6 +310,73 @@ function expr(cursor: Cursor): Expr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function signalExpr(cursor: Cursor): SignalExpr {
|
||||||
|
const start = cursor.currentLocation();
|
||||||
|
const token = signalExprStartToken(cursor);
|
||||||
|
switch (token.tag) {
|
||||||
|
case "EOF":
|
||||||
|
throw {
|
||||||
|
tag: "UnexpectedToken",
|
||||||
|
expected: "SignalExpression",
|
||||||
|
span: token.span
|
||||||
|
} as ParseError;
|
||||||
|
case "signal_read":
|
||||||
|
return SignalExpr.read(token.name, token.span);
|
||||||
|
// case "function_name":
|
||||||
|
// TODO: when components are ready
|
||||||
|
// // e.g. my_func(arg1, arg2)
|
||||||
|
// // parse a `,` delimiter sequence of expr
|
||||||
|
// // need to consume )
|
||||||
|
// if (!tryConsume(cursor, char('('))) {
|
||||||
|
// throw {
|
||||||
|
// tag: "ExpectedFunctionCallStart",
|
||||||
|
// span: cursor.makeSpan(cursor.currentLocation())
|
||||||
|
// } as ParseError;
|
||||||
|
// }
|
||||||
|
// const args = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_CLOSE_PAREN, expr);
|
||||||
|
// return Expr.call(token.name, args, cursor.makeSpan(start));
|
||||||
|
case "keyword":
|
||||||
|
switch (token.kw) {
|
||||||
|
case "let-signal":
|
||||||
|
// TODO:
|
||||||
|
// // let { p0 = e0, p1 = e2 . body }
|
||||||
|
// if (!tryConsume(cursor, char('{'))) {
|
||||||
|
// throw {
|
||||||
|
// tag: "ExpectedLetBlockOpen",
|
||||||
|
// span: cursor.makeSpan(cursor.currentLocation())
|
||||||
|
// } as ParseError;
|
||||||
|
// }
|
||||||
|
// const bindings = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_DOT, productPatternBinding);
|
||||||
|
// const body = expr(cursor);
|
||||||
|
|
||||||
|
// if (!tryConsume(cursor, TERMINATOR_CLOSE_BRACE)) {
|
||||||
|
// throw {
|
||||||
|
// tag: "ExpectedLetBlockClose",
|
||||||
|
// span: cursor.makeSpan(cursor.currentLocation())
|
||||||
|
// } as ParseError;
|
||||||
|
// }
|
||||||
|
// return Expr.let_(bindings, body, cursor.makeSpan(start));
|
||||||
|
return 0 as any;
|
||||||
|
case "let":
|
||||||
|
case "fn":
|
||||||
|
case "match":
|
||||||
|
case "apply":
|
||||||
|
case "signal":
|
||||||
|
case "fn-signal":
|
||||||
|
case "=":
|
||||||
|
case "|":
|
||||||
|
case "!":
|
||||||
|
case ":":
|
||||||
|
// These keywords CANNOT start a signal-expression.
|
||||||
|
throw {
|
||||||
|
tag: "ExpectedSignalExpression",
|
||||||
|
span: token.span
|
||||||
|
} as ParseError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function matchBranch(cursor: Cursor): MatchBranch {
|
function matchBranch(cursor: Cursor): MatchBranch {
|
||||||
// p . body
|
// p . body
|
||||||
const start = cursor.currentLocation();
|
const start = cursor.currentLocation();
|
||||||
|
|
@ -453,7 +527,7 @@ function recordPatternField(cursor: Cursor): FieldPattern {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function parse(source: SourceText): Result<Expr, ParseError> {
|
export function parseExpr(source: SourceText): Result<Expr, ParseError> {
|
||||||
const cursor = new Cursor(source);
|
const cursor = new Cursor(source);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -475,6 +549,28 @@ export function parse(source: SourceText): Result<Expr, ParseError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseSignalExpr(source: SourceText): Result<SignalExpr, ParseError> {
|
||||||
|
const cursor = new Cursor(source);
|
||||||
|
|
||||||
|
try {
|
||||||
|
skipWhitespaceAndComments(cursor);
|
||||||
|
const expression = signalExpr(cursor);
|
||||||
|
|
||||||
|
if (!cursor.eof()) {
|
||||||
|
return Result.error({
|
||||||
|
tag: "UnexpectedToken",
|
||||||
|
expected: "EndOfFile",
|
||||||
|
span: cursor.makeSpan(cursor.currentLocation())
|
||||||
|
} as ParseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(expression);
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: This is a bit sketchy. We maybe forced to have "checked" Exceptions for `ParseError` by wrapping it in something that has a proper tag.
|
||||||
|
return Result.error(e as ParseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function functionParameters(cursor: Cursor): ProductPattern[] {
|
function functionParameters(cursor: Cursor): ProductPattern[] {
|
||||||
const parameters = delimitedTerminalSequence(cursor, DELIMITER_COMMA, undefined, productPattern);
|
const parameters = delimitedTerminalSequence(cursor, DELIMITER_COMMA, undefined, productPattern);
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,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", "let-signal", "signal", "fn-signal" , "=" , "|" , "!", ":"] 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);
|
||||||
|
|
||||||
|
|
@ -62,6 +62,7 @@ export type IdentifierKind =
|
||||||
| "field_name"
|
| "field_name"
|
||||||
| "tag_construction"
|
| "tag_construction"
|
||||||
| "function_call"
|
| "function_call"
|
||||||
|
| "signal_read"
|
||||||
| "pattern_binding";
|
| "pattern_binding";
|
||||||
|
|
||||||
export type IdentifierErrorReason =
|
export type IdentifierErrorReason =
|
||||||
|
|
@ -90,6 +91,15 @@ export type PatternStartToken =
|
||||||
// TODO: ger rid of EOF
|
// TODO: ger rid of EOF
|
||||||
| { tag: "EOF", span: Span };
|
| { tag: "EOF", span: Span };
|
||||||
|
|
||||||
|
export type SignalExprStartToken =
|
||||||
|
// TODO: when we have parametrized signal-expressions
|
||||||
|
// TODO: consider naming it `component` or `parametrized_signal_name`
|
||||||
|
// | { tag: "function_name", name: string, span: Span }
|
||||||
|
| { tag: "signal_read", name: string, span: Span }
|
||||||
|
| { tag: "keyword", kw: Keyword, span: Span }
|
||||||
|
// TODO: ger rid of EOF
|
||||||
|
| { tag: "EOF", span: Span }
|
||||||
|
|
||||||
// === Identifier Scanners ===
|
// === Identifier Scanners ===
|
||||||
|
|
||||||
// Returns the raw string.
|
// Returns the raw string.
|
||||||
|
|
@ -236,6 +246,34 @@ export function exprStart(cursor: Cursor): ExprStartToken {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function signalExprStart(cursor: Cursor): SignalExprStartToken {
|
||||||
|
const start = cursor.currentLocation();
|
||||||
|
if (cursor.eof()) {
|
||||||
|
return { tag: "EOF", span: cursor.makeSpan(start) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = cursor.peek()!;
|
||||||
|
|
||||||
|
// === variable use ===
|
||||||
|
if (c === char('@')) {
|
||||||
|
cursor.next();
|
||||||
|
const { name } = identifierScanner(cursor, 'signal_read');
|
||||||
|
return { tag: "signal_read", name, span: cursor.makeSpan(start) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// === keywords & identifiers ===
|
||||||
|
// Fallthrough: it must be a keyword or a function call
|
||||||
|
const result = identifierOrKeywordScanner(cursor, 'function_call');
|
||||||
|
switch (result.tag) {
|
||||||
|
case "keyword":
|
||||||
|
return result;
|
||||||
|
case "identifier":
|
||||||
|
// TODO: when we have parametrized signal-expressions
|
||||||
|
// return { tag: "function_name", name: result.name, span: result.span };
|
||||||
|
return 0 as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function patternStart(cursor: Cursor): PatternStartToken {
|
export function patternStart(cursor: Cursor): PatternStartToken {
|
||||||
const start = cursor.currentLocation();
|
const start = cursor.currentLocation();
|
||||||
|
|
||||||
|
|
@ -300,6 +338,10 @@ export function isNextTokenExprStart(cursor: Cursor): boolean {
|
||||||
case "apply":
|
case "apply":
|
||||||
case ":":
|
case ":":
|
||||||
return true;
|
return true;
|
||||||
|
case "let-signal":
|
||||||
|
case "signal":
|
||||||
|
case "fn-signal":
|
||||||
|
case "=":
|
||||||
case "=":
|
case "=":
|
||||||
case "|":
|
case "|":
|
||||||
case "!":
|
case "!":
|
||||||
|
|
@ -333,10 +375,13 @@ export function isNextTokenProductPatternStart(cursor: Cursor): boolean {
|
||||||
switch (token.kw) {
|
switch (token.kw) {
|
||||||
case ":":
|
case ":":
|
||||||
return true;
|
return true;
|
||||||
|
case "let-signal":
|
||||||
case "let":
|
case "let":
|
||||||
case "fn":
|
case "fn":
|
||||||
case "match":
|
case "match":
|
||||||
case "apply":
|
case "apply":
|
||||||
|
case "signal":
|
||||||
|
case "fn-signal":
|
||||||
case "=":
|
case "=":
|
||||||
case "|":
|
case "|":
|
||||||
case "!":
|
case "!":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue