Update parser for signal-expressions

This commit is contained in:
Yura Dupyn 2026-02-09 11:34:33 +01:00
parent 94cb3bd721
commit 5e7578c4a3
4 changed files with 151 additions and 9 deletions

View file

@ -1,7 +1,7 @@
// AI GENERATED
import * as readline from 'readline';
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 { exprToString } from '../debug/expr_show';
import { valueToString } from '../debug/value_show';
@ -30,7 +30,7 @@ function runSource(inputRaw: string, isRepl: boolean): boolean {
const text = sourceText(input);
// === Parse ===
const parseResult = parse(text);
const parseResult = parseExpr(text);
if (parseResult.tag === "error") {
printPrettyError(text, parseResult.error);

View file

@ -25,6 +25,7 @@ export type Expr =
export type SignalExpr =
| { tag: "read", name: SignalName } & Meta
// TODO: Is `const` necesary?
| { tag: "const", arg: Expr } & Meta
| { tag: "let", bindings: SignalBinding[], body: Expr } & Meta

View file

@ -1,10 +1,8 @@
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 { Result } from '../result';
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern } from '../expr';
import { UserFunctionDefinition } from '../program';
import { Product } from 'electron';
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern, SignalExpr } from '../expr';
// 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.
@ -23,6 +21,7 @@ export type ParseError =
// === Specific Context Errors ===
| { 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: "ExpectedPatternAssignmentSymbol", span: Span } // Expected '=' in pattern assignment
| { tag: "ExpectedPatternBindingSymbol", span: Span } // Expected '.' in pattern binding
@ -155,6 +154,12 @@ function exprStartToken(cursor: Cursor): ExprStartToken {
return token;
}
function signalExprStartToken(cursor: Cursor): SignalExprStartToken {
const token = signalExprStart(cursor);
skipWhitespaceAndComments(cursor);
return token;
}
function patternStartToken(cursor: Cursor): PatternStartToken {
const token = patternStart(cursor);
skipWhitespaceAndComments(cursor);
@ -172,7 +177,6 @@ function identifier(cursor: Cursor, kind: IdentifierKind): { name: string, span:
function expr(cursor: Cursor): Expr {
const start = cursor.currentLocation();
const token = exprStartToken(cursor);
// TODO: You need to include the spans and perhaps other meta-info.
switch (token.tag) {
case "EOF":
throw {
@ -291,6 +295,9 @@ function expr(cursor: Cursor): Expr {
const branches = delimitedTerminalSequence(cursor, DELIMITER_PIPE, TERMINATOR_CLOSE_BRACE, matchBranch);
return Expr.match(arg, branches, cursor.makeSpan(start))
case "let-signal":
case "signal":
case "fn-signal":
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 {
// p . body
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);
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[] {
const parameters = delimitedTerminalSequence(cursor, DELIMITER_COMMA, undefined, productPattern);

View file

@ -44,7 +44,7 @@ const DELIMITER_CHARS = ["(", ")", "{", "}", ".", ",", "@", "$", "#", '"', "\\"]
export type Delimiter = typeof DELIMITER_CHARS[number];
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];
const KEYWORD_SET: Set<string> = new Set(KEYWORD_LIST);
@ -62,6 +62,7 @@ export type IdentifierKind =
| "field_name"
| "tag_construction"
| "function_call"
| "signal_read"
| "pattern_binding";
export type IdentifierErrorReason =
@ -90,6 +91,15 @@ export type PatternStartToken =
// TODO: ger rid of EOF
| { 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 ===
// 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 {
const start = cursor.currentLocation();
@ -300,6 +338,10 @@ export function isNextTokenExprStart(cursor: Cursor): boolean {
case "apply":
case ":":
return true;
case "let-signal":
case "signal":
case "fn-signal":
case "=":
case "=":
case "|":
case "!":
@ -333,10 +375,13 @@ export function isNextTokenProductPatternStart(cursor: Cursor): boolean {
switch (token.kw) {
case ":":
return true;
case "let-signal":
case "let":
case "fn":
case "match":
case "apply":
case "signal":
case "fn-signal":
case "=":
case "|":
case "!":