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; 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; 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; 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 := ''; tkKeyword: Result := ''; tkNumber: Result := ''; tkString: Result := ''; tkEOF: Result := ''; else Result := ''; end; end; function TToken.GetLocation: ISourceLocation; begin Result := TIdentities.Location(Line, Col); end; { EParserException } 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; { TLexer } 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; var params: TList; begin Consume(tkLeftBracket); params := TList.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; fieldName: string; fieldValue: IAstNode; keyToken: TToken; keyNode: IKeywordNode; begin var startLoc := FCurrentToken.GetLocation; Consume(tkLeftBrace); fields := TList.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; head: TExpr; tailNodes: TArray; 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.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) var count := Length(tailNodes); if (count < 1) or ((count mod 2) = 0) then Error('Syntax Error: ''?'' (cond) requires an odd number of arguments.'); var pairs: TArray; 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; // Parameter list parsing happens in ParseExpression for brackets 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.'); 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.'); // elements[1] is the parameter list node which contains the parsed Params array Result := TAst.LambdaExpr(elements[1].Params, tailNodes[1], startLoc); end else if SameText(head.Token.Text, 'do') then begin Result := TAst.Block(tailNodes, startLoc) end else if SameText(head.Token.Text, 'recur') then begin Result := TAst.Recur(tailNodes, startLoc) end 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 // --- NEW: Support for new-series --- else if SameText(head.Token.Text, 'new-series') then begin if Length(tailNodes) <> 1 then Error('Syntax Error: ''new-series'' requires exactly 1 argument (definition string).'); // Extract definition string from constant node if (tailNodes[0].Kind = akConstant) and (tailNodes[0].AsConstant.Value.Kind = vkText) then Result := TAst.CreateSeries(tailNodes[0].AsConstant.Value.AsText, startLoc) else Error('Syntax Error: ''new-series'' argument must be a string literal.'); end // --- NEW: Support for add-item --- else if SameText(head.Token.Text, 'add-item') then begin if not (Length(tailNodes) in [2, 3]) then Error('Syntax Error: ''add-item'' requires 2 or 3 arguments (series, value, [lookback]).'); var lookback: IAstNode := nil; if Length(tailNodes) = 3 then lookback := tailNodes[2]; if tailNodes[0].Kind <> akIdentifier then Error('Syntax Error: ''add-item'' first argument must be a series identifier.'); Result := TAst.AddSeriesItem(tailNodes[0].AsIdentifier, tailNodes[1], lookback, startLoc); end // --- NEW: Support for count (SeriesLength) --- else if SameText(head.Token.Text, 'count') then begin if Length(tailNodes) <> 1 then Error('Syntax Error: ''count'' requires exactly 1 argument (series).'); if tailNodes[0].Kind <> akIdentifier then Error('Syntax Error: ''count'' argument must be a series identifier.'); Result := TAst.SeriesLength(tailNodes[0].AsIdentifier, 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; { TPrettyPrintVisitor } 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; { TAstScript } class function TAstScript.Parse(const ASource: string): IAstNode; var p: TParser; begin 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.