From 5e7578c4a3a1c000e7bdf677e2131ccc7ebc64b6 Mon Sep 17 00:00:00 2001 From: Yura Dupyn <2153100+omedusyo@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:34:33 +0100 Subject: [PATCH] Update parser for signal-expressions --- src/lang/debug/repl.ts | 4 +- src/lang/expr.ts | 1 + src/lang/parser/parser.ts | 108 ++++++++++++++++++++++++++++++++++--- src/lang/parser/scanner.ts | 47 +++++++++++++++- 4 files changed, 151 insertions(+), 9 deletions(-) diff --git a/src/lang/debug/repl.ts b/src/lang/debug/repl.ts index 82fbdf5..54270cd 100644 --- a/src/lang/debug/repl.ts +++ b/src/lang/debug/repl.ts @@ -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); diff --git a/src/lang/expr.ts b/src/lang/expr.ts index cee8b5d..174ea4d 100644 --- a/src/lang/expr.ts +++ b/src/lang/expr.ts @@ -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 diff --git a/src/lang/parser/parser.ts b/src/lang/parser/parser.ts index 8fa089d..a6a29ee 100644 --- a/src/lang/parser/parser.ts +++ b/src/lang/parser/parser.ts @@ -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 { +export function parseExpr(source: SourceText): Result { const cursor = new Cursor(source); try { @@ -475,6 +549,28 @@ export function parse(source: SourceText): Result { } } +export function parseSignalExpr(source: SourceText): Result { + 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); diff --git a/src/lang/parser/scanner.ts b/src/lang/parser/scanner.ts index 4b9562d..efce63c 100644 --- a/src/lang/parser/scanner.ts +++ b/src/lang/parser/scanner.ts @@ -44,7 +44,7 @@ const DELIMITER_CHARS = ["(", ")", "{", "}", ".", ",", "@", "$", "#", '"', "\\"] export type Delimiter = typeof DELIMITER_CHARS[number]; const DELIMITER_SET: Set = 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 = 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 "!":