Files
MycLib/Src/AST/Myc.Ast.Script.pas
T
2025-12-07 12:06:29 +01:00

1173 lines
36 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 (* cond1 branch1 ... else *)
| "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;
function VisitConstant(const Node: IConstantNode): TVoid; override;
function VisitIdentifier(const Node: IIdentifierNode): TVoid; override;
function VisitKeyword(const Node: IKeywordNode): TVoid; override;
// List Visitors
function VisitParameterList(const Node: IParameterList): TVoid; override;
function VisitArgumentList(const Node: IArgumentList): TVoid; override;
function VisitExpressionList(const Node: IExpressionList): TVoid; override;
function VisitRecordFieldList(const Node: IRecordFieldList): TVoid; override;
function VisitRecordField(const Node: IRecordFieldNode): TVoid; override;
function VisitIfExpression(const Node: IIfExpressionNode): TVoid; override;
function VisitCondExpression(const Node: ICondExpressionNode): TVoid; override;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): TVoid; override;
function VisitMacroDefinition(const Node: IMacroDefinitionNode): TVoid; override;
function VisitQuasiquote(const Node: IQuasiquoteNode): TVoid; override;
function VisitUnquote(const Node: IUnquoteNode): TVoid; override;
function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TVoid; override;
function VisitFunctionCall(const Node: IFunctionCallNode): TVoid; override;
function VisitMacroExpansionNode(const Node: IMacroExpansionNode): TVoid; override;
function VisitBlockExpression(const Node: IBlockExpressionNode): TVoid; override;
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): TVoid; override;
function VisitAssignment(const Node: IAssignmentNode): TVoid; override;
function VisitIndexer(const Node: IIndexerNode): TVoid; override;
function VisitMemberAccess(const Node: IMemberAccessNode): TVoid; override;
function VisitRecordLiteral(const Node: IRecordLiteralNode): TVoid; override;
function VisitCreateSeries(const Node: ICreateSeriesNode): TVoid; override;
function VisitAddSeriesItem(const Node: IAddSeriesItemNode): TVoid; override;
function VisitSeriesLength(const Node: ISeriesLengthNode): TVoid; override;
function VisitRecurNode(const Node: IRecurNode): TVoid; override;
function VisitNop(const Node: INopNode): TVoid; 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
// (? test1 branch1 test2 branch2 ... else)
// Requires odd number of tailNodes (pairs + 1 else)
var count := Length(tailNodes);
if (count < 1) or ((count mod 2) = 0) then
Error('Syntax Error: ''?'' (cond) requires an odd number of arguments (pairs of condition/branch and one final else).');
var pairs: TArray<TCondPair>;
SetLength(pairs, count div 2);
for var i := 0 to High(pairs) do
pairs[i] := TCondPair.Create(tailNodes[i * 2], tailNodes[i * 2 + 1]);
var elseNode := tailNodes[High(tailNodes)];
Result := TAst.CondExpr(pairs, elseNode, 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;
function TPrettyPrintVisitor.VisitConstant(const Node: IConstantNode): TVoid;
var
val: TDataValue;
begin
val := Node.Value;
if val.Kind = vkText then
Append('"' + val.AsText + '"')
else
Append(val.ToString);
end;
function TPrettyPrintVisitor.VisitIdentifier(const Node: IIdentifierNode): TVoid;
begin
Append(Node.Name);
end;
function TPrettyPrintVisitor.VisitKeyword(const Node: IKeywordNode): TVoid;
begin
Append(':' + Node.Value.Name);
end;
// --- List Visitors Implementation ---
function TPrettyPrintVisitor.VisitParameterList(const Node: IParameterList): TVoid;
begin
Append('[');
for var i := 0 to Node.Count - 1 do
begin
if i > 0 then
Append(' ');
Node[i].Accept(Self);
end;
Append(']');
end;
function TPrettyPrintVisitor.VisitArgumentList(const Node: IArgumentList): TVoid;
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;
function TPrettyPrintVisitor.VisitExpressionList(const Node: IExpressionList): TVoid;
begin
Indent;
for var item in Node do
begin
NewLine;
item.Accept(Self);
end;
Unindent;
NewLine;
end;
function TPrettyPrintVisitor.VisitRecordFieldList(const Node: IRecordFieldList): TVoid;
begin
Indent;
for var item in Node do
begin
NewLine;
item.Accept(Self);
end;
Unindent;
NewLine;
end;
function TPrettyPrintVisitor.VisitRecordField(const Node: IRecordFieldNode): TVoid;
begin
Node.Key.Accept(Self);
Append(' ');
Node.Value.Accept(Self);
end;
// --- Node Visitors ---
function TPrettyPrintVisitor.VisitIfExpression(const Node: IIfExpressionNode): TVoid;
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;
function TPrettyPrintVisitor.VisitCondExpression(const Node: ICondExpressionNode): TVoid;
begin
Append('(?');
Indent;
for var pair in Node.Pairs do
begin
NewLine;
pair.Condition.Accept(Self);
Append(' ');
pair.Branch.Accept(Self);
end;
// Else
NewLine;
Node.ElseBranch.Accept(Self);
Unindent;
NewLine;
Append(')');
end;
function TPrettyPrintVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): TVoid;
begin
Append('(fn ');
Node.Parameters.Accept(Self);
Indent;
NewLine;
Node.Body.Accept(Self);
Unindent;
NewLine;
Append(')');
end;
function TPrettyPrintVisitor.VisitMacroDefinition(const Node: IMacroDefinitionNode): TVoid;
begin
Append('(defmacro ');
Node.Name.Accept(Self);
Append(' ');
Node.Parameters.Accept(Self);
Indent;
NewLine;
Node.Body.Accept(Self);
Unindent;
NewLine;
Append(')');
end;
function TPrettyPrintVisitor.VisitQuasiquote(const Node: IQuasiquoteNode): TVoid;
begin
Append('`');
Node.Expression.Accept(Self);
end;
function TPrettyPrintVisitor.VisitUnquote(const Node: IUnquoteNode): TVoid;
begin
Append('~');
Node.Expression.Accept(Self);
end;
function TPrettyPrintVisitor.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TVoid;
begin
Append('~@');
Node.Expression.Accept(Self);
end;
function TPrettyPrintVisitor.VisitFunctionCall(const Node: IFunctionCallNode): TVoid;
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;
function TPrettyPrintVisitor.VisitMacroExpansionNode(const Node: IMacroExpansionNode): TVoid;
begin
VisitFunctionCall(Node.CallNode);
end;
function TPrettyPrintVisitor.VisitRecurNode(const Node: IRecurNode): TVoid;
begin
Append('(recur');
Node.Arguments.Accept(Self);
Append(')');
end;
function TPrettyPrintVisitor.VisitBlockExpression(const Node: IBlockExpressionNode): TVoid;
begin
Append('(do');
Node.Expressions.Accept(Self);
Append(')');
end;
function TPrettyPrintVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode): TVoid;
begin
Append('(def ');
Node.Target.Accept(Self);
if Assigned(Node.Initializer) then
begin
Append(' ');
Node.Initializer.Accept(Self);
end;
Append(')');
end;
function TPrettyPrintVisitor.VisitAssignment(const Node: IAssignmentNode): TVoid;
begin
Append('(assign ');
Node.Target.Accept(Self);
Append(' ');
Node.Value.Accept(Self);
Append(')');
end;
function TPrettyPrintVisitor.VisitIndexer(const Node: IIndexerNode): TVoid;
begin
Append('(get ');
Node.Base.Accept(Self);
Append(' ');
Node.Index.Accept(Self);
Append(')');
end;
function TPrettyPrintVisitor.VisitMemberAccess(const Node: IMemberAccessNode): TVoid;
begin
Append('(.');
Node.Member.Accept(Self);
Append(' ');
Node.Base.Accept(Self);
Append(')');
end;
function TPrettyPrintVisitor.VisitRecordLiteral(const Node: IRecordLiteralNode): TVoid;
begin
if Node.Fields.Count = 0 then
begin
Append('{}');
exit;
end;
Append('{');
Node.Fields.Accept(Self);
Append('}');
end;
function TPrettyPrintVisitor.VisitCreateSeries(const Node: ICreateSeriesNode): TVoid;
begin
Append(Format('(new-series "%s")', [Node.Definition]));
end;
function TPrettyPrintVisitor.VisitAddSeriesItem(const Node: IAddSeriesItemNode): TVoid;
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;
function TPrettyPrintVisitor.VisitNop(const Node: INopNode): TVoid;
begin
Append('...');
end;
function TPrettyPrintVisitor.VisitSeriesLength(const Node: ISeriesLengthNode): TVoid;
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.