1158 lines
34 KiB
ObjectPascal
1158 lines
34 KiB
ObjectPascal
unit Myc.Ast.Script;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils,
|
|
Myc.Data.Value,
|
|
Myc.Ast.Nodes,
|
|
Myc.Ast.Visitor;
|
|
|
|
type
|
|
// Exception for parsing errors containing location information
|
|
EParserException = class(EAstException)
|
|
strict private
|
|
FLine: Integer;
|
|
FCol: Integer;
|
|
public
|
|
constructor Create(const AMessage: string; ALine, ACol: Integer);
|
|
property Line: Integer read FLine;
|
|
property Col: Integer read FCol;
|
|
end;
|
|
|
|
var
|
|
// This BNF should always be kept up to date and valid.
|
|
BNF: String =
|
|
'''
|
|
(* ---------------------------------------------------------------------- *)
|
|
(* ---- Main Productions (Start Symbols) ---- *)
|
|
(* ---------------------------------------------------------------------- *)
|
|
program ::= expression
|
|
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) ---- *)
|
|
(* ---------------------------------------------------------------------- *)
|
|
list ::= "(" list_content ")"
|
|
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
|
|
|
|
function_call ::= expression expression*
|
|
|
|
(* ---------------------------------------------------------------------- *)
|
|
(* ---- Parameter Lists and Records ---- *)
|
|
(* ---------------------------------------------------------------------- *)
|
|
parameter_list ::= "[" identifier* "]"
|
|
record_literal ::= "{" (keyword expression)* "}"
|
|
|
|
(* ---------------------------------------------------------------------- *)
|
|
(* ---- Terminals (Lexer Tokens) ---- *)
|
|
(* ---------------------------------------------------------------------- *)
|
|
number ::= ["-"] digit+ ["." digit+]
|
|
string ::= '"' ( ? any char except \ or " ? | '\' ( '"' | '\' | 'n' | 'r' | 't' ) )* '"'
|
|
keyword ::= ":" identifier
|
|
dot_identifier ::= "." identifier
|
|
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
|
|
public
|
|
class function Parse(const ASource: string): IAstNode; static;
|
|
class function Print(const ANode: IAstNode): string; static;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
System.Classes,
|
|
System.Generics.Collections,
|
|
System.Character,
|
|
System.Math,
|
|
Myc.Data.Scalar,
|
|
Myc.Data.Keyword,
|
|
Myc.Ast,
|
|
Myc.Ast.Identities;
|
|
|
|
type
|
|
// --- Internal Parser Implementation ---
|
|
|
|
TTokenKind = (
|
|
tkLeftParen, // (
|
|
tkRightParen, // )
|
|
tkLeftBracket, // [
|
|
tkRightBracket, // ]
|
|
tkLeftBrace, // {
|
|
tkRightBrace, // }
|
|
tkQuote, // '
|
|
tkBacktick, // `
|
|
tkTilde, // ~
|
|
tkAt, // @
|
|
tkIdentifier,
|
|
tkKeyword,
|
|
tkNumber,
|
|
tkString,
|
|
tkEOF,
|
|
tkError
|
|
);
|
|
|
|
TTokenKindHelper = record helper for TTokenKind
|
|
function ToString: String;
|
|
end;
|
|
|
|
TToken = record
|
|
Kind: TTokenKind;
|
|
Text: string;
|
|
Line: Integer;
|
|
Col: Integer;
|
|
function GetLocation: ISourceLocation;
|
|
end;
|
|
|
|
TLexer = class
|
|
private
|
|
FSource: string;
|
|
FCurrentPos: Integer;
|
|
FLine: Integer;
|
|
FLineStart: Integer;
|
|
function Peek: Char;
|
|
procedure Advance;
|
|
function ReadNumber: string;
|
|
function ReadString: string;
|
|
function ReadIdentifier: string;
|
|
function GetCurrentLine: Integer;
|
|
function GetCurrentCol: Integer;
|
|
procedure Error(const Msg: string);
|
|
public
|
|
constructor Create(const ASource: string);
|
|
function GetNextToken: TToken;
|
|
property CurrentLine: Integer read GetCurrentLine;
|
|
property CurrentCol: Integer read GetCurrentCol;
|
|
end;
|
|
|
|
TExpr = record
|
|
Token: TToken;
|
|
Node: IAstNode;
|
|
Params: TArray<IIdentifierNode>;
|
|
end;
|
|
|
|
TParser = class
|
|
private
|
|
FLexer: TLexer;
|
|
FCurrentToken: TToken;
|
|
procedure Error(const Msg: string);
|
|
procedure ErrorFmt(const Msg: string; const Args: array of const);
|
|
procedure Consume(AExpectedKind: TTokenKind);
|
|
procedure NextToken;
|
|
function ParseList: IAstNode;
|
|
function ParseRecordLiteral: IAstNode;
|
|
function ParseParameterList: TArray<IIdentifierNode>;
|
|
function ParseExpression: TExpr;
|
|
public
|
|
constructor Create(const ASource: string);
|
|
destructor Destroy; override;
|
|
function Parse: IAstNode;
|
|
end;
|
|
|
|
// --- Internal Printer Implementation ---
|
|
|
|
TPrettyPrintVisitor = class(TAstVisitor)
|
|
private
|
|
FBuilder: TStringBuilder;
|
|
FIndentLevel: Integer;
|
|
procedure Indent;
|
|
procedure Unindent;
|
|
procedure Append(const S: string);
|
|
procedure NewLine;
|
|
public
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
function GetResult: string;
|
|
function Execute(const RootNode: IAstNode): TDataValue;
|
|
|
|
procedure VisitConstant(const Node: IConstantNode); override;
|
|
procedure VisitIdentifier(const Node: IIdentifierNode); override;
|
|
procedure VisitKeyword(const Node: IKeywordNode); override;
|
|
|
|
// List Visitors
|
|
procedure VisitParameterList(const Node: IParameterList); override;
|
|
procedure VisitArgumentList(const Node: IArgumentList); override;
|
|
procedure VisitExpressionList(const Node: IExpressionList); override;
|
|
procedure VisitRecordFieldList(const Node: IRecordFieldList); override;
|
|
procedure VisitRecordField(const Node: IRecordFieldNode); override;
|
|
|
|
procedure VisitIfExpression(const Node: IIfExpressionNode); override;
|
|
procedure VisitTernaryExpression(const Node: ITernaryExpressionNode); override;
|
|
procedure VisitLambdaExpression(const Node: ILambdaExpressionNode); override;
|
|
procedure VisitMacroDefinition(const Node: IMacroDefinitionNode); override;
|
|
procedure VisitQuasiquote(const Node: IQuasiquoteNode); override;
|
|
procedure VisitUnquote(const Node: IUnquoteNode); override;
|
|
procedure VisitUnquoteSplicing(const Node: IUnquoteSplicingNode); override;
|
|
procedure VisitFunctionCall(const Node: IFunctionCallNode); override;
|
|
procedure VisitMacroExpansionNode(const Node: IMacroExpansionNode); override;
|
|
procedure VisitBlockExpression(const Node: IBlockExpressionNode); override;
|
|
procedure VisitVariableDeclaration(const Node: IVariableDeclarationNode); override;
|
|
procedure VisitAssignment(const Node: IAssignmentNode); override;
|
|
procedure VisitIndexer(const Node: IIndexerNode); override;
|
|
procedure VisitMemberAccess(const Node: IMemberAccessNode); override;
|
|
procedure VisitRecordLiteral(const Node: IRecordLiteralNode); override;
|
|
procedure VisitCreateSeries(const Node: ICreateSeriesNode); override;
|
|
procedure VisitAddSeriesItem(const Node: IAddSeriesItemNode); override;
|
|
procedure VisitSeriesLength(const Node: ISeriesLengthNode); override;
|
|
procedure VisitRecurNode(const Node: IRecurNode); override;
|
|
procedure VisitNop(const Node: INopNode); override;
|
|
end;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// IMPLEMENTATION: Helpers & Basic Types
|
|
// -----------------------------------------------------------------------------
|
|
|
|
function TTokenKindHelper.ToString: String;
|
|
begin
|
|
case Self of
|
|
tkLeftParen: Result := '(';
|
|
tkRightParen: Result := ')';
|
|
tkLeftBracket: Result := '[';
|
|
tkRightBracket: Result := ']';
|
|
tkLeftBrace: Result := '{';
|
|
tkRightBrace: Result := '}';
|
|
tkQuote: Result := '''';
|
|
tkBacktick: Result := '`';
|
|
tkTilde: Result := '~';
|
|
tkAt: Result := '@';
|
|
tkIdentifier: Result := '<identifier>';
|
|
tkKeyword: Result := '<keyword>';
|
|
tkNumber: Result := '<number>';
|
|
tkString: Result := '<string>';
|
|
tkEOF: Result := '<EOF>';
|
|
else
|
|
Result := '<error>';
|
|
end;
|
|
end;
|
|
|
|
function TToken.GetLocation: ISourceLocation;
|
|
begin
|
|
Result := TIdentities.Location(Line, Col);
|
|
end;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// IMPLEMENTATION: Lexer
|
|
// -----------------------------------------------------------------------------
|
|
|
|
constructor EParserException.Create(const AMessage: string; ALine, ACol: Integer);
|
|
begin
|
|
inherited CreateFmt('[Line %d, Col %d] %s', [ALine, ACol, AMessage]);
|
|
FLine := ALine;
|
|
FCol := ACol;
|
|
end;
|
|
|
|
constructor TLexer.Create(const ASource: string);
|
|
begin
|
|
inherited Create;
|
|
FSource := ASource;
|
|
FCurrentPos := 1;
|
|
FLine := 1;
|
|
FLineStart := 1;
|
|
end;
|
|
|
|
function TLexer.GetCurrentLine: Integer;
|
|
begin
|
|
Result := FLine;
|
|
end;
|
|
|
|
function TLexer.GetCurrentCol: Integer;
|
|
begin
|
|
Result := FCurrentPos - FLineStart + 1;
|
|
end;
|
|
|
|
procedure TLexer.Error(const Msg: string);
|
|
begin
|
|
raise EParserException.Create(Msg, FLine, GetCurrentCol);
|
|
end;
|
|
|
|
function TLexer.Peek: Char;
|
|
begin
|
|
if FCurrentPos > Length(FSource) then
|
|
Result := #0
|
|
else
|
|
Result := FSource[FCurrentPos];
|
|
end;
|
|
|
|
procedure TLexer.Advance;
|
|
var
|
|
c: Char;
|
|
begin
|
|
if FCurrentPos <= Length(FSource) then
|
|
begin
|
|
c := FSource[FCurrentPos];
|
|
if c = #10 then
|
|
begin
|
|
Inc(FLine);
|
|
FLineStart := FCurrentPos + 1;
|
|
end;
|
|
end;
|
|
Inc(FCurrentPos);
|
|
end;
|
|
|
|
function TLexer.ReadIdentifier: string;
|
|
var
|
|
startPos: Integer;
|
|
begin
|
|
startPos := FCurrentPos;
|
|
while (Peek <> #0) and (not (Peek.IsWhiteSpace or CharInSet(Peek, ['(', ')', '[', ']', '{', '}', '''', '`', '~', ';']))) do
|
|
Advance;
|
|
Result := Copy(FSource, startPos, FCurrentPos - startPos);
|
|
end;
|
|
|
|
function TLexer.ReadNumber: string;
|
|
var
|
|
startPos: Integer;
|
|
begin
|
|
startPos := FCurrentPos;
|
|
if (Peek = '-') then
|
|
Advance;
|
|
|
|
while (Peek <> #0) and (Peek.IsDigit or (Peek = '.')) do
|
|
Advance;
|
|
Result := Copy(FSource, startPos, FCurrentPos - startPos);
|
|
end;
|
|
|
|
function TLexer.ReadString: string;
|
|
var
|
|
builder: TStringBuilder;
|
|
c: Char;
|
|
begin
|
|
Advance; // Skip opening "
|
|
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
|
|
Error('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);
|
|
't': builder.Append(#9);
|
|
'r': builder.Append(#13);
|
|
else
|
|
builder.Append(c);
|
|
end;
|
|
Advance; // Consume escaped char
|
|
end
|
|
else
|
|
begin
|
|
if c = #0 then
|
|
break;
|
|
builder.Append(c);
|
|
Advance;
|
|
end;
|
|
end;
|
|
Error('Syntax Error: Unterminated string literal.');
|
|
finally
|
|
builder.Free;
|
|
end;
|
|
end;
|
|
|
|
function TLexer.GetNextToken: TToken;
|
|
begin
|
|
while FCurrentPos <= Length(FSource) do
|
|
begin
|
|
if FSource[FCurrentPos].IsWhiteSpace then
|
|
begin
|
|
Advance;
|
|
continue;
|
|
end;
|
|
|
|
if FSource[FCurrentPos] = ';' then
|
|
begin
|
|
while (FCurrentPos <= Length(FSource)) and (not CharInSet(FSource[FCurrentPos], [#10, #13])) do
|
|
Advance;
|
|
continue;
|
|
end;
|
|
break;
|
|
end;
|
|
|
|
Result.Line := CurrentLine;
|
|
Result.Col := CurrentCol;
|
|
|
|
if FCurrentPos > Length(FSource) then
|
|
begin
|
|
Result.Kind := tkEOF;
|
|
exit;
|
|
end;
|
|
|
|
var c := Peek;
|
|
case c of
|
|
'(':
|
|
begin
|
|
Result.Kind := tkLeftParen;
|
|
Advance;
|
|
end;
|
|
')':
|
|
begin
|
|
Result.Kind := tkRightParen;
|
|
Advance;
|
|
end;
|
|
'[':
|
|
begin
|
|
Result.Kind := tkLeftBracket;
|
|
Advance;
|
|
end;
|
|
']':
|
|
begin
|
|
Result.Kind := tkRightBracket;
|
|
Advance;
|
|
end;
|
|
'{':
|
|
begin
|
|
Result.Kind := tkLeftBrace;
|
|
Advance;
|
|
end;
|
|
'}':
|
|
begin
|
|
Result.Kind := tkRightBrace;
|
|
Advance;
|
|
end;
|
|
'''':
|
|
begin
|
|
Result.Kind := tkQuote;
|
|
Advance;
|
|
end;
|
|
'`':
|
|
begin
|
|
Result.Kind := tkBacktick;
|
|
Advance;
|
|
end;
|
|
'~':
|
|
begin
|
|
Result.Kind := tkTilde;
|
|
Advance;
|
|
end;
|
|
'@':
|
|
begin
|
|
Result.Kind := tkAt;
|
|
Advance;
|
|
end;
|
|
'"':
|
|
begin
|
|
Result.Kind := tkString;
|
|
Result.Text := ReadString;
|
|
end;
|
|
':':
|
|
begin
|
|
Advance;
|
|
Result.Kind := tkKeyword;
|
|
Result.Text := ReadIdentifier;
|
|
if Result.Text.IsEmpty then
|
|
Result.Kind := tkError;
|
|
end;
|
|
else
|
|
if c.IsDigit or ((c = '-') and (FCurrentPos < Length(FSource)) and FSource[FCurrentPos + 1].IsDigit) then
|
|
begin
|
|
Result.Kind := tkNumber;
|
|
Result.Text := ReadNumber;
|
|
end
|
|
else
|
|
begin
|
|
Result.Kind := tkIdentifier;
|
|
Result.Text := ReadIdentifier;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// IMPLEMENTATION: Parser
|
|
// -----------------------------------------------------------------------------
|
|
|
|
constructor TParser.Create(const ASource: string);
|
|
begin
|
|
inherited Create;
|
|
FLexer := TLexer.Create(ASource);
|
|
NextToken;
|
|
end;
|
|
|
|
destructor TParser.Destroy;
|
|
begin
|
|
FLexer.Free;
|
|
inherited;
|
|
end;
|
|
|
|
procedure TParser.Error(const Msg: string);
|
|
begin
|
|
raise EParserException.Create(Msg, FCurrentToken.Line, FCurrentToken.Col);
|
|
end;
|
|
|
|
procedure TParser.ErrorFmt(const Msg: string; const Args: array of const);
|
|
begin
|
|
raise EParserException.Create(Format(Msg, Args), FCurrentToken.Line, FCurrentToken.Col);
|
|
end;
|
|
|
|
procedure TParser.NextToken;
|
|
begin
|
|
FCurrentToken := FLexer.GetNextToken;
|
|
end;
|
|
|
|
procedure TParser.Consume(AExpectedKind: TTokenKind);
|
|
begin
|
|
if FCurrentToken.Kind <> AExpectedKind then
|
|
ErrorFmt('Syntax Error: Expected token %s, but found %s', [AExpectedKind.ToString, FCurrentToken.Kind.ToString]);
|
|
NextToken;
|
|
end;
|
|
|
|
function TParser.ParseParameterList: TArray<IIdentifierNode>;
|
|
var
|
|
params: TList<IIdentifierNode>;
|
|
begin
|
|
Consume(tkLeftBracket);
|
|
params := TList<IIdentifierNode>.Create;
|
|
try
|
|
while FCurrentToken.Kind <> tkRightBracket do
|
|
begin
|
|
if FCurrentToken.Kind = tkEOF then
|
|
Error('Syntax Error: Unexpected end of file.');
|
|
if FCurrentToken.Kind <> tkIdentifier then
|
|
Error('Syntax Error: Expected identifier in parameter list.');
|
|
|
|
params.Add(TAst.Identifier(FCurrentToken.Text, FCurrentToken.GetLocation));
|
|
NextToken;
|
|
end;
|
|
Result := params.ToArray;
|
|
finally
|
|
params.Free;
|
|
end;
|
|
Consume(tkRightBracket);
|
|
end;
|
|
|
|
function TParser.ParseRecordLiteral: IAstNode;
|
|
var
|
|
fields: TList<IRecordFieldNode>;
|
|
fieldName: string;
|
|
fieldValue: IAstNode;
|
|
keyToken: TToken;
|
|
keyNode: IKeywordNode;
|
|
begin
|
|
var startLoc := FCurrentToken.GetLocation;
|
|
Consume(tkLeftBrace);
|
|
fields := TList<IRecordFieldNode>.Create;
|
|
try
|
|
while FCurrentToken.Kind <> tkRightBrace do
|
|
begin
|
|
if FCurrentToken.Kind = tkEOF then
|
|
Error('Syntax Error: Unexpected end of file in record literal.');
|
|
|
|
if FCurrentToken.Kind <> tkKeyword then
|
|
Error('Syntax Error: Expected keyword (e.g., :key) as field name in record literal.');
|
|
|
|
keyToken := FCurrentToken;
|
|
fieldName := FCurrentToken.Text;
|
|
keyNode := TAst.Keyword(fieldName, keyToken.GetLocation);
|
|
NextToken;
|
|
|
|
if FCurrentToken.Kind = tkRightBrace then
|
|
Error('Syntax Error: Missing value for key ' + fieldName + ' in record literal.');
|
|
|
|
fieldValue := ParseExpression.Node;
|
|
|
|
fields.Add(TAst.RecordField(keyNode, fieldValue, keyToken.GetLocation));
|
|
end;
|
|
|
|
Result := TAst.RecordLiteral(fields.ToArray, startLoc);
|
|
finally
|
|
fields.Free;
|
|
end;
|
|
Consume(tkRightBrace);
|
|
end;
|
|
|
|
function TParser.ParseList: IAstNode;
|
|
var
|
|
elements: TList<TExpr>;
|
|
head: TExpr;
|
|
tailNodes: TArray<IAstNode>;
|
|
initializer: IAstNode;
|
|
startLoc: ISourceLocation;
|
|
begin
|
|
startLoc := FCurrentToken.GetLocation;
|
|
Consume(tkLeftParen);
|
|
if FCurrentToken.Kind = tkRightParen then
|
|
Error('Syntax Error: Empty list () is not a valid expression.');
|
|
|
|
elements := TList<TExpr>.Create;
|
|
try
|
|
while FCurrentToken.Kind <> tkRightParen do
|
|
begin
|
|
if FCurrentToken.Kind = tkEOF then
|
|
Error('Syntax Error: Unexpected end of file.');
|
|
elements.Add(ParseExpression);
|
|
end;
|
|
|
|
head := elements[0];
|
|
|
|
SetLength(tailNodes, elements.Count - 1);
|
|
for var i := 0 to High(tailNodes) do
|
|
tailNodes[i] := elements[i + 1].Node;
|
|
|
|
if head.Token.Kind = tkIdentifier then
|
|
begin
|
|
if SameText(head.Token.Text, 'if') then
|
|
begin
|
|
if not (Length(tailNodes) in [2, 3]) then
|
|
Error('Syntax Error in if statement.');
|
|
var elseBranch: IAstNode := nil;
|
|
if Length(tailNodes) = 3 then
|
|
elseBranch := tailNodes[2];
|
|
Result := TAst.IfExpr(tailNodes[0], tailNodes[1], elseBranch, startLoc);
|
|
end
|
|
else if SameText(head.Token.Text, '?') then
|
|
begin
|
|
if Length(tailNodes) <> 3 then
|
|
Error('Syntax Error in ternary statement.');
|
|
Result := TAst.TernaryExpr(tailNodes[0], tailNodes[1], tailNodes[2], startLoc);
|
|
end
|
|
else if SameText(head.Token.Text, 'def') then
|
|
begin
|
|
if not (Length(tailNodes) in [1, 2]) then
|
|
Error('Syntax Error: ''def'' requires a target and an optional initializer.');
|
|
initializer := nil;
|
|
if Length(tailNodes) = 2 then
|
|
initializer := tailNodes[1];
|
|
Result := TAst.VarDecl(tailNodes[0], initializer, startLoc);
|
|
end
|
|
else if SameText(head.Token.Text, 'defmacro') then
|
|
begin
|
|
if (Length(tailNodes) <> 3) or (elements[1].Token.Kind <> tkIdentifier) then
|
|
Error('Syntax Error: ''defmacro'' requires a name, a parameter list, and a body.');
|
|
|
|
var macroName := tailNodes[0].AsIdentifier;
|
|
var macroParams := elements[2].Params;
|
|
|
|
if tailNodes[2].Kind <> akQuasiquote then
|
|
Error('Syntax Error: Expected a quasiquote as macro body.');
|
|
|
|
Result := TAst.MacroDef(macroName, macroParams, tailNodes[2], startLoc);
|
|
end
|
|
else if SameText(head.Token.Text, 'assign') then
|
|
begin
|
|
if Length(tailNodes) <> 2 then
|
|
Error('Syntax Error: ''assign'' requires exactly 2 arguments (target and value).');
|
|
Result := TAst.Assign(tailNodes[0], tailNodes[1], startLoc);
|
|
end
|
|
else if SameText(head.Token.Text, 'fn') then
|
|
begin
|
|
if Length(tailNodes) <> 2 then
|
|
Error('Syntax Error: ''fn'' requires a parameter list and a body.');
|
|
Result := TAst.LambdaExpr(elements[1].Params, tailNodes[1], startLoc);
|
|
end
|
|
else if SameText(head.Token.Text, 'do') then
|
|
Result := TAst.Block(tailNodes, startLoc)
|
|
else if SameText(head.Token.Text, 'recur') then
|
|
Result := TAst.Recur(tailNodes, startLoc)
|
|
else if SameText(head.Token.Text, 'get') then
|
|
begin
|
|
if Length(tailNodes) <> 2 then
|
|
Error('Syntax Error: ''get'' requires exactly 2 arguments (base and index).');
|
|
Result := TAst.Indexer(tailNodes[0], tailNodes[1], startLoc)
|
|
end
|
|
else if (Length(head.Token.Text) > 1) and (head.Token.Text.StartsWith('.')) then
|
|
begin
|
|
if Length(tailNodes) <> 1 then
|
|
ErrorFmt(
|
|
'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), head.Token.GetLocation), startLoc);
|
|
end;
|
|
end;
|
|
|
|
if Result = nil then
|
|
Result := TAst.FunctionCall(head.Node, tailNodes, startLoc);
|
|
|
|
finally
|
|
elements.Free;
|
|
end;
|
|
|
|
Consume(tkRightParen);
|
|
end;
|
|
|
|
function TParser.ParseExpression: TExpr;
|
|
var
|
|
i64: Int64;
|
|
dbl: Double;
|
|
expr: TExpr;
|
|
startLoc: ISourceLocation;
|
|
begin
|
|
Result.Token := FCurrentToken;
|
|
startLoc := FCurrentToken.GetLocation;
|
|
|
|
case FCurrentToken.Kind of
|
|
tkBacktick:
|
|
begin
|
|
NextToken;
|
|
expr := ParseExpression;
|
|
Result.Node := TAst.Quasiquote(expr.Node, startLoc);
|
|
exit;
|
|
end;
|
|
tkTilde:
|
|
begin
|
|
NextToken;
|
|
if FCurrentToken.Kind = tkAt then
|
|
begin
|
|
NextToken;
|
|
expr := ParseExpression;
|
|
Result.Node := TAst.UnquoteSplicing(expr.Node.AsQuasiquote, startLoc);
|
|
end
|
|
else
|
|
begin
|
|
expr := ParseExpression;
|
|
Result.Node := TAst.Unquote(expr.Node, startLoc);
|
|
end;
|
|
exit;
|
|
end;
|
|
tkQuote:
|
|
begin
|
|
NextToken;
|
|
expr := ParseExpression;
|
|
Result.Node := TAst.FunctionCall(TAst.Identifier('quote', startLoc), [expr.Node], startLoc);
|
|
exit;
|
|
end;
|
|
end;
|
|
|
|
Result.Token := FCurrentToken;
|
|
case FCurrentToken.Kind of
|
|
tkNumber:
|
|
begin
|
|
if TryStrToInt64(FCurrentToken.Text, i64) then
|
|
Result.Node := TAst.Constant(i64, startLoc)
|
|
else if TryStrToFloat(FCurrentToken.Text, dbl, TFormatSettings.Invariant) then
|
|
Result.Node := TAst.Constant(dbl, startLoc)
|
|
else
|
|
ErrorFmt('Syntax Error: Invalid number format "%s"', [FCurrentToken.Text]);
|
|
NextToken;
|
|
end;
|
|
tkString:
|
|
begin
|
|
Result.Node := TAst.Constant(FCurrentToken.Text, startLoc);
|
|
NextToken;
|
|
end;
|
|
tkKeyword:
|
|
begin
|
|
Result.Node := TAst.Keyword(FCurrentToken.Text, startLoc);
|
|
NextToken;
|
|
end;
|
|
tkIdentifier:
|
|
begin
|
|
if FCurrentToken.Text = '...' then
|
|
Result.Node := TAst.Nop(startLoc)
|
|
else
|
|
Result.Node := TAst.Identifier(FCurrentToken.Text, startLoc);
|
|
NextToken;
|
|
end;
|
|
tkLeftParen: Result.Node := ParseList;
|
|
tkLeftBracket: Result.Params := ParseParameterList;
|
|
tkLeftBrace: Result.Node := ParseRecordLiteral;
|
|
tkEOF: {nop};
|
|
else
|
|
ErrorFmt('Syntax Error: Unexpected token %s', [FCurrentToken.Kind.ToString]);
|
|
end;
|
|
end;
|
|
|
|
function TParser.Parse: IAstNode;
|
|
begin
|
|
var expr := ParseExpression;
|
|
if FCurrentToken.Kind <> tkEOF then
|
|
Error('Syntax Error: Unexpected characters after end of expression.');
|
|
Result := expr.Node;
|
|
end;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// IMPLEMENTATION: PrettyPrintVisitor
|
|
// -----------------------------------------------------------------------------
|
|
|
|
constructor TPrettyPrintVisitor.Create;
|
|
begin
|
|
inherited Create;
|
|
FBuilder := TStringBuilder.Create;
|
|
FIndentLevel := 0;
|
|
end;
|
|
|
|
destructor TPrettyPrintVisitor.Destroy;
|
|
begin
|
|
FBuilder.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
function TPrettyPrintVisitor.GetResult: string;
|
|
begin
|
|
Result := FBuilder.ToString;
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.Indent;
|
|
begin
|
|
inc(FIndentLevel, 2);
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.Unindent;
|
|
begin
|
|
dec(FIndentLevel, 2);
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.Append(const S: string);
|
|
begin
|
|
FBuilder.Append(S);
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.NewLine;
|
|
begin
|
|
FBuilder.AppendLine;
|
|
FBuilder.Append(''.PadLeft(FIndentLevel));
|
|
end;
|
|
|
|
function TPrettyPrintVisitor.Execute(const RootNode: IAstNode): TDataValue;
|
|
begin
|
|
if Assigned(RootNode) then
|
|
RootNode.Accept(Self);
|
|
Result := TDataValue.Void;
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitConstant(const Node: IConstantNode);
|
|
var
|
|
val: TDataValue;
|
|
begin
|
|
val := Node.Value;
|
|
if val.Kind = vkText then
|
|
Append('"' + val.AsText + '"')
|
|
else
|
|
Append(val.ToString);
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitIdentifier(const Node: IIdentifierNode);
|
|
begin
|
|
Append(Node.Name);
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitKeyword(const Node: IKeywordNode);
|
|
begin
|
|
Append(':' + Node.Value.Name);
|
|
end;
|
|
|
|
// --- List Visitors Implementation ---
|
|
|
|
procedure TPrettyPrintVisitor.VisitParameterList(const Node: IParameterList);
|
|
begin
|
|
Append('[');
|
|
for var i := 0 to Node.Count - 1 do
|
|
begin
|
|
if i > 0 then
|
|
Append(' ');
|
|
Node[i].Accept(Self);
|
|
end;
|
|
Append(']');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitArgumentList(const Node: IArgumentList);
|
|
begin
|
|
// Argument list does not have brackets of its own in Lisp call syntax
|
|
for var i := 0 to Node.Count - 1 do
|
|
begin
|
|
Append(' '); // Space before each argument
|
|
Node[i].Accept(Self);
|
|
end;
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitExpressionList(const Node: IExpressionList);
|
|
begin
|
|
Indent;
|
|
for var item in Node do
|
|
begin
|
|
NewLine;
|
|
item.Accept(Self);
|
|
end;
|
|
Unindent;
|
|
NewLine;
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitRecordFieldList(const Node: IRecordFieldList);
|
|
begin
|
|
Indent;
|
|
for var item in Node do
|
|
begin
|
|
NewLine;
|
|
item.Accept(Self);
|
|
end;
|
|
Unindent;
|
|
NewLine;
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitRecordField(const Node: IRecordFieldNode);
|
|
begin
|
|
Node.Key.Accept(Self);
|
|
Append(' ');
|
|
Node.Value.Accept(Self);
|
|
end;
|
|
|
|
// --- Node Visitors ---
|
|
|
|
procedure TPrettyPrintVisitor.VisitIfExpression(const Node: IIfExpressionNode);
|
|
begin
|
|
Append('(if ');
|
|
Node.Condition.Accept(Self);
|
|
Indent;
|
|
NewLine;
|
|
Node.ThenBranch.Accept(Self);
|
|
if Assigned(Node.ElseBranch) then
|
|
begin
|
|
NewLine;
|
|
Node.ElseBranch.Accept(Self);
|
|
end;
|
|
Unindent;
|
|
NewLine;
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitTernaryExpression(const Node: ITernaryExpressionNode);
|
|
begin
|
|
Append('(? ');
|
|
Node.Condition.Accept(Self);
|
|
Indent;
|
|
NewLine;
|
|
Node.ThenBranch.Accept(Self);
|
|
NewLine;
|
|
Node.ElseBranch.Accept(Self);
|
|
Unindent;
|
|
NewLine;
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode);
|
|
begin
|
|
Append('(fn ');
|
|
Node.Parameters.Accept(Self);
|
|
Indent;
|
|
NewLine;
|
|
Node.Body.Accept(Self);
|
|
Unindent;
|
|
NewLine;
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitMacroDefinition(const Node: IMacroDefinitionNode);
|
|
begin
|
|
Append('(defmacro ');
|
|
Node.Name.Accept(Self);
|
|
Append(' ');
|
|
Node.Parameters.Accept(Self);
|
|
Indent;
|
|
NewLine;
|
|
Node.Body.Accept(Self);
|
|
Unindent;
|
|
NewLine;
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitQuasiquote(const Node: IQuasiquoteNode);
|
|
begin
|
|
Append('`');
|
|
Node.Expression.Accept(Self);
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitUnquote(const Node: IUnquoteNode);
|
|
begin
|
|
Append('~');
|
|
Node.Expression.Accept(Self);
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode);
|
|
begin
|
|
Append('~@');
|
|
Node.Expression.Accept(Self);
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitFunctionCall(const Node: IFunctionCallNode);
|
|
begin
|
|
if (Node.Callee.Kind = akIdentifier) and (Node.Callee.AsIdentifier.Name = 'quote') and (Node.Arguments.Count = 1) then
|
|
begin
|
|
Append('''');
|
|
Node.Arguments[0].Accept(Self);
|
|
exit;
|
|
end;
|
|
|
|
Append('(');
|
|
Node.Callee.Accept(Self);
|
|
Node.Arguments.Accept(Self); // Prints separated by space
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitMacroExpansionNode(const Node: IMacroExpansionNode);
|
|
begin
|
|
VisitFunctionCall(Node.CallNode);
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitRecurNode(const Node: IRecurNode);
|
|
begin
|
|
Append('(recur');
|
|
Node.Arguments.Accept(Self);
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitBlockExpression(const Node: IBlockExpressionNode);
|
|
begin
|
|
Append('(do');
|
|
Node.Expressions.Accept(Self);
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode);
|
|
begin
|
|
Append('(def ');
|
|
Node.Target.Accept(Self);
|
|
if Assigned(Node.Initializer) then
|
|
begin
|
|
Append(' ');
|
|
Node.Initializer.Accept(Self);
|
|
end;
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitAssignment(const Node: IAssignmentNode);
|
|
begin
|
|
Append('(assign ');
|
|
Node.Target.Accept(Self);
|
|
Append(' ');
|
|
Node.Value.Accept(Self);
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitIndexer(const Node: IIndexerNode);
|
|
begin
|
|
Append('(get ');
|
|
Node.Base.Accept(Self);
|
|
Append(' ');
|
|
Node.Index.Accept(Self);
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitMemberAccess(const Node: IMemberAccessNode);
|
|
begin
|
|
Append('(.');
|
|
Node.Member.Accept(Self);
|
|
Append(' ');
|
|
Node.Base.Accept(Self);
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitRecordLiteral(const Node: IRecordLiteralNode);
|
|
begin
|
|
if Node.Fields.Count = 0 then
|
|
begin
|
|
Append('{}');
|
|
exit;
|
|
end;
|
|
|
|
Append('{');
|
|
Node.Fields.Accept(Self);
|
|
Append('}');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitCreateSeries(const Node: ICreateSeriesNode);
|
|
begin
|
|
Append(Format('(new-series "%s")', [Node.Definition]));
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitAddSeriesItem(const Node: IAddSeriesItemNode);
|
|
begin
|
|
Append('(add-item ');
|
|
Node.Series.Accept(Self);
|
|
Append(' ');
|
|
Node.Value.Accept(Self);
|
|
if Assigned(Node.Lookback) then
|
|
begin
|
|
Append(' ');
|
|
Node.Lookback.Accept(Self);
|
|
end;
|
|
Append(')');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitNop(const Node: INopNode);
|
|
begin
|
|
Append('...');
|
|
end;
|
|
|
|
procedure TPrettyPrintVisitor.VisitSeriesLength(const Node: ISeriesLengthNode);
|
|
begin
|
|
Append('(count ');
|
|
Node.Series.Accept(Self);
|
|
Append(')');
|
|
end;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// IMPLEMENTATION: TAstScript Facade
|
|
// -----------------------------------------------------------------------------
|
|
|
|
class function TAstScript.Parse(const ASource: string): IAstNode;
|
|
var
|
|
p: TParser;
|
|
begin
|
|
if ASource.Trim.IsEmpty then
|
|
exit(nil);
|
|
p := TParser.Create(ASource);
|
|
try
|
|
Result := p.Parse;
|
|
finally
|
|
p.Free;
|
|
end;
|
|
end;
|
|
|
|
class function TAstScript.Print(const ANode: IAstNode): string;
|
|
var
|
|
visitor: TPrettyPrintVisitor;
|
|
begin
|
|
if not Assigned(ANode) then
|
|
exit('');
|
|
visitor := TPrettyPrintVisitor.Create;
|
|
try
|
|
visitor.Execute(ANode);
|
|
Result := visitor.GetResult;
|
|
finally
|
|
visitor.Free;
|
|
end;
|
|
end;
|
|
|
|
end.
|