Keywords as basic scalar type

Script enhancements
This commit is contained in:
Michael Schimmel
2025-11-01 00:56:59 +01:00
parent 689dede600
commit 957171f089
7 changed files with 498 additions and 78 deletions
+153 -13
View File
@@ -7,6 +7,90 @@ uses
Myc.Ast.Nodes,
Myc.Ast.Visitor;
var
// This BNF should always be kept up to date and valid. It is used to comunicate with LLMs.
BNF: String =
'''
(* ---------------------------------------------------------------------- *)
(* ---- Main Productions (Start Symbols) ---- *)
(* ---------------------------------------------------------------------- *)
(* A program is a single expression, which can be followed by comments *)
program ::= expression
(* An expression is either an atom, a list, a record, or a macro character *)
expression ::= atom | list | record_literal | reader_macro
(* ---------------------------------------------------------------------- *)
(* ---- Atoms (Basic Values) ---- *)
(* ---------------------------------------------------------------------- *)
atom ::= number | string | identifier | keyword
(* ---------------------------------------------------------------------- *)
(* ---- Reader-Macros (Syntactic Sugar) ---- *)
(* ---------------------------------------------------------------------- *)
reader_macro ::= "'" expression (* (quote ...) *)
| "`" expression (* (quasiquote ...) *)
| "~" expression (* (unquote ...) *)
| "~@" expression (* (unquote-splicing ...) *)
(* ---------------------------------------------------------------------- *)
(* ---- Lists (S-Expressions) ---- *)
(* ---------------------------------------------------------------------- *)
(* A list is the primary structure for code. Empty lists () are invalid. *)
list ::= "(" list_content ")"
(* The parser distinguishes between special forms (like 'if') and
general function calls based on the first element. *)
list_content ::= special_form | function_call
special_form ::= "if" expression expression expression?
| "?" expression expression expression
| "def" identifier expression?
| "defmacro" identifier parameter_list expression
| "assign" identifier expression
| "fn" parameter_list expression
| "do" expression*
| "recur" expression*
| "get" expression expression
| dot_identifier expression
(* A function call is any list not recognized as a special form.
The first 'expression' is the callee, the rest are the arguments. *)
function_call ::= expression expression*
(* ---------------------------------------------------------------------- *)
(* ---- Parameter Lists and Records ---- *)
(* ---------------------------------------------------------------------- *)
(* A parameter list is *only* valid inside 'fn' and 'defmacro' *)
parameter_list ::= "[" identifier* "]"
(* A record literal consists of 0 or more keyword/value pairs *)
record_literal ::= "{" (keyword expression)* "}"
(* ---------------------------------------------------------------------- *)
(* ---- Terminals (Lexer Tokens) ---- *)
(* ---------------------------------------------------------------------- *)
number ::= ["-"] digit+ ["." digit+]
string ::= '"' ( ? any char except \ or " ? | '\' ( '"' | '\' | 'n' | 'r' | 't' ) )* '"'
keyword ::= ":" identifier
(* An identifier for member access, handled specially by the parser *)
dot_identifier ::= "." identifier
(* An identifier is a sequence of characters that are not delimiters.
The lexer is very permissive. *)
identifier ::= ? any sequence of chars not containing whitespace, '()[]{}'`~:; ?
digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
''';
type
// Provides a high-level facade for parsing and printing the AST.
TAstScript = record
@@ -164,8 +248,8 @@ var
startPos: Integer;
begin
startPos := FCurrentPos;
// Reader macro characters are now also delimiters for identifiers.
while (Peek <> #0) and (not (Peek.IsWhiteSpace or CharInSet(Peek, ['(', ')', '[', ']', '{', '}', '''', '`', '~']))) do
// Reader macro characters and comments are now also delimiters for identifiers.
while (Peek <> #0) and (not (Peek.IsWhiteSpace or CharInSet(Peek, ['(', ')', '[', ']', '{', '}', '''', '`', '~', ';']))) do
Advance;
Result := Copy(FSource, startPos, FCurrentPos - startPos);
end;
@@ -175,6 +259,11 @@ var
startPos: Integer;
begin
startPos := FCurrentPos;
// Handle the optional leading minus sign
if (Peek = '-') then
Advance;
while (Peek <> #0) and (Peek.IsDigit or (Peek = '.')) do
Advance;
Result := Copy(FSource, startPos, FCurrentPos - startPos);
@@ -182,15 +271,57 @@ end;
function TLexer.ReadString: string;
var
startPos: Integer;
builder: TStringBuilder;
c: Char;
begin
Advance; // Skip opening "
startPos := FCurrentPos;
while (Peek <> #0) and (Peek <> '"') do
Advance;
Result := Copy(FSource, startPos, FCurrentPos - startPos);
if Peek = '"' then
Advance; // Skip closing "
builder := TStringBuilder.Create;
try
while (FCurrentPos <= Length(FSource)) do
begin
c := Peek;
if c = '"' then
begin
Advance; // Consume closing "
Result := builder.ToString;
exit;
end;
if c = '\' then
begin
Advance; // Consume '\'
if FCurrentPos > Length(FSource) then
raise Exception.Create('Syntax Error: String literal ends with an escape character.');
c := Peek; // Get escaped char
case c of
'"': builder.Append('"');
'\': builder.Append('\');
'n': builder.Append(sLineBreak); // Use system's line break
't': builder.Append(#9);
'r': builder.Append(#13);
else
// Pass through unknown escapes (e.g., "\z" becomes "z")
builder.Append(c);
end;
Advance; // Consume escaped char
end
else
begin
// Append regular char (including literal newlines)
if c = #0 then // Check for unexpected EOF
break;
builder.Append(c);
Advance;
end;
end;
// If we fall out of the loop, we hit EOF without a closing quote.
raise Exception.Create('Syntax Error: Unterminated string literal.');
finally
builder.Free;
end;
end;
function TLexer.GetNextToken: TToken;
@@ -473,6 +604,8 @@ begin
end
else if SameText(head.Token.Text, 'assign') then
begin
if Length(tailNodes) <> 2 then
raise Exception.Create('Syntax Error: ''assign'' requires exactly 2 arguments (identifier and value).');
if tailTokens[0].Kind <> tkIdentifier then
raise Exception.Create('Syntax Error: Expected an identifier for assignment.');
Result := TAst.Assign(IIdentifierNode(tailNodes[0]), tailNodes[1]);
@@ -488,9 +621,19 @@ begin
else if SameText(head.Token.Text, 'recur') then
Result := TAst.Recur(tailNodes)
else if SameText(head.Token.Text, 'get') then
begin
if Length(tailNodes) <> 2 then
raise Exception.Create('Syntax Error: ''get'' requires exactly 2 arguments (base and index).');
Result := TAst.Indexer(tailNodes[0], tailNodes[1])
end
else if (Length(head.Token.Text) > 1) and (head.Token.Text.StartsWith('.')) then
begin
if Length(tailNodes) <> 1 then
raise Exception.CreateFmt(
'Syntax Error: Member access (e.g., ''%s'') requires exactly 1 argument (the base object).',
[head.Token.Text]);
Result := TAst.MemberAccess(tailNodes[0], TAst.Keyword(head.Token.Text.Substring(1)));
end;
end;
// If no special form matched, treat it as a generic function call.
@@ -553,7 +696,7 @@ begin
Result.Node :=
TAst.Constant(i64)
// Use invariant format settings for float parsing.
else if TryStrToFloat(FCurrentToken.Text, dbl) then
else if TryStrToFloat(FCurrentToken.Text, dbl, TFormatSettings.Invariant) then
Result.Node := TAst.Constant(dbl)
else
raise Exception.CreateFmt('Syntax Error: Invalid number format "%s"', [FCurrentToken.Text]);
@@ -1009,7 +1152,4 @@ begin
end;
end;
initialization
FormatSettings.DecimalSeparator := '.';
end.