Friday, August 28, 2026

IMPLEMENTING A DYNAMICALLY TYPED PROGRAMMING LANGUAGE: A COMPLETE PRACTICAL WALKTHROUGH



In this third part of my mini series on programmiung language design, It‘ll focus on the design of a dynamically typed language called DynaLang. An interpreter will be the runtime environment for DynaLang programs.

INTRODUCTION

This article demonstrates the complete implementation of a dynamically typed programming language called "DynaLang". Unlike the statically typed MiniLang, DynaLang performs all type checking at runtime, offering greater flexibility and faster prototyping at the cost of runtime type errors.

DynaLang supports dynamic typing where variables can hold values of any type and change types during execution. The language includes first-class functions, closures, objects with prototype-based inheritance, dynamic property access, exceptions for error handling, and built-in collections like lists and maps. We will implement a complete interpreter from grammar through execution.

The implementation follows an interpreter architecture where source code flows through the lexer to produce tokens, the parser builds an abstract syntax tree, and the interpreter directly executes the AST while maintaining runtime type information and managing dynamic scoping.

PART ONE: LANGUAGE SPECIFICATION

1.1 DYNALANG OVERVIEW

DynaLang is a dynamically typed language with the following characteristics. Variables do not have declared types and can hold values of any type. Type checking occurs at runtime when operations are performed. The language supports multiple data types including numbers, strings, booleans, null, functions, lists, and objects. Functions are first-class values that can capture variables from their enclosing scope, creating closures. Objects use prototype-based inheritance similar to JavaScript.

Here is a complete example program demonstrating DynaLang's features:

// Example DynaLang program demonstrating all features

// Variables can hold any type
x = 42;
x = "now a string";
x = true;

// Functions are first-class values
function greet(name) {
    return "Hello, " + name;
}

message = greet("World");
print(message);

// Closures capture enclosing scope
function makeCounter() {
    count = 0;
    return function() {
        count = count + 1;
        return count;
    };
}

counter1 = makeCounter();
counter2 = makeCounter();
print(counter1());  // Prints 1
print(counter1());  // Prints 2
print(counter2());  // Prints 1

// Objects with dynamic properties
person = {
    name: "Alice",
    age: 30,
    greet: function() {
        return "Hi, I'm " + this.name;
    }
};

print(person.name);
print(person.greet());

// Prototype-based inheritance
employee = Object.create(person);
employee.salary = 50000;
employee.work = function() {
    return this.name + " is working";
};

print(employee.name);  // Inherited from person
print(employee.work());

// Lists
numbers = [1, 2, 3, 4, 5];
numbers.push(6);

for i = 0; i < numbers.length(); i = i + 1 {
    print(numbers[i]);
}

// For-each loop
foreach num in numbers {
    print(num * 2);
}

// While loop
count = 0;
while count < 3 {
    print(count);
    count = count + 1;
}

// If-else
if person.age >= 18 {
    print("Adult");
} else {
    print("Minor");
}

// Exception handling
try {
    result = 10 / 0;
} catch error {
    print("Error: " + error);
} finally {
    print("Cleanup");
}

// Switch statement
switch person.age {
    case 30:
        print("Thirty");
        break;
    case 40:
        print("Forty");
        break;
    default:
        print("Other age");
}

This example shows dynamic typing, first-class functions, closures, objects, lists, all control flow constructs, and exception handling.

1.2 TYPE SYSTEM SPECIFICATION

DynaLang uses dynamic typing with the following runtime types. The number type represents both integers and floating-point values. The string type represents text. The boolean type has values true and false. The null type represents absence of value. The function type represents callable functions including closures. The list type represents ordered collections. The object type represents key-value mappings with prototype chains.

Type checking happens at runtime when operations are performed. Adding a number and a string attempts automatic conversion. Calling a non-function value raises a runtime error. Accessing properties on null raises a runtime error. The language provides type checking functions like isNumber, isString, isFunction, isList, and isObject.

Type coercion follows these rules. Numbers convert to strings for concatenation. Strings convert to numbers for arithmetic when possible. Any value converts to boolean for conditionals where null and false are falsy and everything else is truthy. Objects and lists are always truthy.

1.3 SCOPING AND CLOSURES

DynaLang uses lexical scoping with dynamic variable creation. Variables are created on first assignment and looked up through the scope chain. Functions capture their enclosing scope, creating closures that maintain references to outer variables even after the outer function returns.

Each function execution creates a new environment that chains to its parent. Variable lookup searches the current environment first, then parent environments up to the global scope. Assignment to an existing variable modifies it in place. Assignment to a new variable creates it in the current scope.

The this keyword in object methods refers to the object on which the method was called. Arrow functions do not have their own this binding and inherit it from the enclosing scope.

PART TWO: LEXICAL AND SYNTACTIC SPECIFICATION

2.1 ANTLR GRAMMAR DEFINITION

We define the complete DynaLang grammar using ANTLR version 4 notation. The grammar file is named DynaLang.g4.

grammar DynaLang;

// Parser Rules

program
    : statement* EOF
    ;

statement
    : expressionStatement
    | variableDeclaration
    | functionDeclaration
    | ifStatement
    | whileStatement
    | forStatement
    | foreachStatement
    | switchStatement
    | tryStatement
    | returnStatement
    | breakStatement
    | continueStatement
    | block
    ;

expressionStatement
    : expression ';'
    ;

variableDeclaration
    : IDENTIFIER '=' expression ';'
    ;

functionDeclaration
    : 'function' IDENTIFIER '(' parameterList? ')' block
    ;

parameterList
    : IDENTIFIER (',' IDENTIFIER)*
    ;

block
    : '{' statement* '}'
    ;

ifStatement
    : 'if' expression block ('else' (ifStatement | block))?
    ;

whileStatement
    : 'while' expression block
    ;

forStatement
    : 'for' IDENTIFIER '=' expression ';' expression ';' 
      IDENTIFIER '=' expression block
    ;

foreachStatement
    : 'foreach' IDENTIFIER 'in' expression block
    ;

switchStatement
    : 'switch' expression '{' caseClause* defaultClause? '}'
    ;

caseClause
    : 'case' expression ':' statement* ('break' ';')?
    ;

defaultClause
    : 'default' ':' statement*
    ;

tryStatement
    : 'try' block 'catch' IDENTIFIER block ('finally' block)?
    ;

returnStatement
    : 'return' expression? ';'
    ;

breakStatement
    : 'break' ';'
    ;

continueStatement
    : 'continue' ';'
    ;

expression
    : primary
    | expression '(' argumentList? ')'                    // Function call
    | expression '[' expression ']'                       // Index access
    | expression '.' IDENTIFIER                           // Property access
    | expression '.' IDENTIFIER '(' argumentList? ')'     // Method call
    | '!' expression                                      // Logical not
    | '-' expression                                      // Unary minus
    | expression op=('*' | '/' | '%') expression         // Multiplicative
    | expression op=('+' | '-') expression               // Additive
    | expression op=('<' | '>' | '<=' | '>=') expression // Relational
    | expression op=('==' | '!=') expression             // Equality
    | expression '&&' expression                          // Logical and
    | expression '||' expression                          // Logical or
    | expression '=' expression                           // Assignment
    | '(' expression ')'                                  // Parenthesized
    ;

primary
    : NUMBER
    | STRING
    | BOOLEAN
    | NULL
    | IDENTIFIER
    | functionExpression
    | listLiteral
    | objectLiteral
    | THIS
    ;

functionExpression
    : 'function' '(' parameterList? ')' block
    ;

listLiteral
    : '[' (expression (',' expression)*)? ']'
    ;

objectLiteral
    : '{' (objectProperty (',' objectProperty)*)? '}'
    ;

objectProperty
    : IDENTIFIER ':' expression
    ;

argumentList
    : expression (',' expression)*
    ;

// Lexer Rules

IDENTIFIER
    : [a-zA-Z_][a-zA-Z0-9_]*
    ;

NUMBER
    : [0-9]+ ('.' [0-9]+)?
    ;

STRING
    : '"' (~["\r\n] | '\\' .)* '"'
    | '\'' (~['\r\n] | '\\' .)* '\''
    ;

BOOLEAN
    : 'true'
    | 'false'
    ;

NULL
    : 'null'
    ;

THIS
    : 'this'
    ;

WHITESPACE
    : [ \t\r\n]+ -> skip
    ;

COMMENT
    : '//' ~[\r\n]* -> skip
    ;

BLOCK_COMMENT
    : '/*' .*? '*/' -> skip
    ;

This grammar defines the complete syntax of DynaLang. The expression rule uses precedence climbing to handle operator precedence correctly. Assignment is right-associative while other binary operators are left-associative.

2.2 ANTLR CONFIGURATION AND GENERATION

Create a project directory structure as follows:

dynalang/
    grammar/
        DynaLang.g4
    src/
        main/
            java/
                com/
                    dynalang/
                        ast/
                        interpreter/
                        runtime/
    lib/
        antlr-4.13.1-complete.jar

Generate the lexer and parser using ANTLR:

java -jar lib/antlr-4.13.1-complete.jar -o src/main/java/com/dynalang/parser -package com.dynalang.parser -visitor grammar/DynaLang.g4

This generates the DynaLangLexer, DynaLangParser, and visitor classes needed for our interpreter.

PART THREE: ABSTRACT SYNTAX TREE DESIGN

3.1 AST NODE HIERARCHY

We design a clean AST representation independent of the ANTLR parse tree.

package com.dynalang.ast;

/**
 * Base class for all AST nodes.
 */
public abstract class ASTNode {
    private int line;
    private int column;
    
    public ASTNode(int line, int column) {
        this.line = line;
        this.column = column;
    }
    
    public int getLine() {
        return line;
    }
    
    public int getColumn() {
        return column;
    }
    
    /**
     * Accept method for visitor pattern.
     */
    public abstract <T> T accept(ASTVisitor<T> visitor);
}

The base class tracks source location for runtime error reporting.

Define the program node:

package com.dynalang.ast;

import java.util.List;
import java.util.ArrayList;

/**
 * Root node representing a complete program.
 */
public class ProgramNode extends ASTNode {
    private List<StatementNode> statements;
    
    public ProgramNode(int line, int column) {
        super(line, column);
        this.statements = new ArrayList<>();
    }
    
    public void addStatement(StatementNode stmt) {
        statements.add(stmt);
    }
    
    public List<StatementNode> getStatements() {
        return statements;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitProgram(this);
    }
}

The program node contains a sequence of top-level statements.

3.2 STATEMENT NODES

Define nodes for all statement types:

package com.dynalang.ast;

import java.util.List;
import java.util.ArrayList;

/**
 * Base class for statement nodes.
 */
public abstract class StatementNode extends ASTNode {
    public StatementNode(int line, int column) {
        super(line, column);
    }
}

/**
 * Block statement containing multiple statements.
 */
public class BlockNode extends StatementNode {
    private List<StatementNode> statements;
    
    public BlockNode(int line, int column) {
        super(line, column);
        this.statements = new ArrayList<>();
    }
    
    public void addStatement(StatementNode stmt) {
        statements.add(stmt);
    }
    
    public List<StatementNode> getStatements() {
        return statements;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitBlock(this);
    }
}

/**
 * Expression used as a statement.
 */
public class ExpressionStatementNode extends StatementNode {
    private ExpressionNode expression;
    
    public ExpressionStatementNode(int line, int column, 
                                  ExpressionNode expression) {
        super(line, column);
        this.expression = expression;
    }
    
    public ExpressionNode getExpression() {
        return expression;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitExpressionStatement(this);
    }
}

/**
 * Variable declaration and initialization.
 */
public class VariableDeclarationNode extends StatementNode {
    private String name;
    private ExpressionNode initializer;
    
    public VariableDeclarationNode(int line, int column, String name,
                                  ExpressionNode initializer) {
        super(line, column);
        this.name = name;
        this.initializer = initializer;
    }
    
    public String getName() {
        return name;
    }
    
    public ExpressionNode getInitializer() {
        return initializer;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitVariableDeclaration(this);
    }
}

/**
 * Function declaration.
 */
public class FunctionDeclarationNode extends StatementNode {
    private String name;
    private List<String> parameters;
    private BlockNode body;
    
    public FunctionDeclarationNode(int line, int column, String name,
                                  BlockNode body) {
        super(line, column);
        this.name = name;
        this.body = body;
        this.parameters = new ArrayList<>();
    }
    
    public String getName() {
        return name;
    }
    
    public void addParameter(String param) {
        parameters.add(param);
    }
    
    public List<String> getParameters() {
        return parameters;
    }
    
    public BlockNode getBody() {
        return body;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitFunctionDeclaration(this);
    }
}

/**
 * If statement with optional else clause.
 */
public class IfStatementNode extends StatementNode {
    private ExpressionNode condition;
    private StatementNode thenBranch;
    private StatementNode elseBranch;  // May be null
    
    public IfStatementNode(int line, int column, 
                          ExpressionNode condition,
                          StatementNode thenBranch,
                          StatementNode elseBranch) {
        super(line, column);
        this.condition = condition;
        this.thenBranch = thenBranch;
        this.elseBranch = elseBranch;
    }
    
    public ExpressionNode getCondition() {
        return condition;
    }
    
    public StatementNode getThenBranch() {
        return thenBranch;
    }
    
    public StatementNode getElseBranch() {
        return elseBranch;
    }
    
    public boolean hasElse() {
        return elseBranch != null;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitIfStatement(this);
    }
}

/**
 * While loop statement.
 */
public class WhileStatementNode extends StatementNode {
    private ExpressionNode condition;
    private StatementNode body;
    
    public WhileStatementNode(int line, int column, 
                             ExpressionNode condition,
                             StatementNode body) {
        super(line, column);
        this.condition = condition;
        this.body = body;
    }
    
    public ExpressionNode getCondition() {
        return condition;
    }
    
    public StatementNode getBody() {
        return body;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitWhileStatement(this);
    }
}

/**
 * For loop statement.
 */
public class ForStatementNode extends StatementNode {
    private String variable;
    private ExpressionNode init;
    private ExpressionNode condition;
    private ExpressionNode update;
    private StatementNode body;
    
    public ForStatementNode(int line, int column, String variable,
                           ExpressionNode init, ExpressionNode condition,
                           ExpressionNode update, StatementNode body) {
        super(line, column);
        this.variable = variable;
        this.init = init;
        this.condition = condition;
        this.update = update;
        this.body = body;
    }
    
    public String getVariable() {
        return variable;
    }
    
    public ExpressionNode getInit() {
        return init;
    }
    
    public ExpressionNode getCondition() {
        return condition;
    }
    
    public ExpressionNode getUpdate() {
        return update;
    }
    
    public StatementNode getBody() {
        return body;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitForStatement(this);
    }
}

/**
 * Foreach loop statement.
 */
public class ForeachStatementNode extends StatementNode {
    private String variable;
    private ExpressionNode iterable;
    private StatementNode body;
    
    public ForeachStatementNode(int line, int column, String variable,
                               ExpressionNode iterable, 
                               StatementNode body) {
        super(line, column);
        this.variable = variable;
        this.iterable = iterable;
        this.body = body;
    }
    
    public String getVariable() {
        return variable;
    }
    
    public ExpressionNode getIterable() {
        return iterable;
    }
    
    public StatementNode getBody() {
        return body;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitForeachStatement(this);
    }
}

/**
 * Switch statement.
 */
public class SwitchStatementNode extends StatementNode {
    private ExpressionNode expression;
    private List<CaseClauseNode> cases;
    private DefaultClauseNode defaultClause;  // May be null
    
    public SwitchStatementNode(int line, int column, 
                              ExpressionNode expression) {
        super(line, column);
        this.expression = expression;
        this.cases = new ArrayList<>();
    }
    
    public ExpressionNode getExpression() {
        return expression;
    }
    
    public void addCase(CaseClauseNode caseClause) {
        cases.add(caseClause);
    }
    
    public List<CaseClauseNode> getCases() {
        return cases;
    }
    
    public void setDefaultClause(DefaultClauseNode defaultClause) {
        this.defaultClause = defaultClause;
    }
    
    public DefaultClauseNode getDefaultClause() {
        return defaultClause;
    }
    
    public boolean hasDefault() {
        return defaultClause != null;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitSwitchStatement(this);
    }
}

/**
 * Case clause within a switch statement.
 */
public class CaseClauseNode extends ASTNode {
    private ExpressionNode value;
    private List<StatementNode> statements;
    private boolean hasBreak;
    
    public CaseClauseNode(int line, int column, ExpressionNode value,
                         boolean hasBreak) {
        super(line, column);
        this.value = value;
        this.statements = new ArrayList<>();
        this.hasBreak = hasBreak;
    }
    
    public ExpressionNode getValue() {
        return value;
    }
    
    public void addStatement(StatementNode stmt) {
        statements.add(stmt);
    }
    
    public List<StatementNode> getStatements() {
        return statements;
    }
    
    public boolean hasBreak() {
        return hasBreak;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitCaseClause(this);
    }
}

/**
 * Default clause within a switch statement.
 */
public class DefaultClauseNode extends ASTNode {
    private List<StatementNode> statements;
    
    public DefaultClauseNode(int line, int column) {
        super(line, column);
        this.statements = new ArrayList<>();
    }
    
    public void addStatement(StatementNode stmt) {
        statements.add(stmt);
    }
    
    public List<StatementNode> getStatements() {
        return statements;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitDefaultClause(this);
    }
}

/**
 * Try-catch-finally statement.
 */
public class TryStatementNode extends StatementNode {
    private BlockNode tryBlock;
    private String catchVariable;
    private BlockNode catchBlock;
    private BlockNode finallyBlock;  // May be null
    
    public TryStatementNode(int line, int column, BlockNode tryBlock,
                           String catchVariable, BlockNode catchBlock,
                           BlockNode finallyBlock) {
        super(line, column);
        this.tryBlock = tryBlock;
        this.catchVariable = catchVariable;
        this.catchBlock = catchBlock;
        this.finallyBlock = finallyBlock;
    }
    
    public BlockNode getTryBlock() {
        return tryBlock;
    }
    
    public String getCatchVariable() {
        return catchVariable;
    }
    
    public BlockNode getCatchBlock() {
        return catchBlock;
    }
    
    public BlockNode getFinallyBlock() {
        return finallyBlock;
    }
    
    public boolean hasFinally() {
        return finallyBlock != null;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitTryStatement(this);
    }
}

/**
 * Return statement.
 */
public class ReturnStatementNode extends StatementNode {
    private ExpressionNode value;  // May be null
    
    public ReturnStatementNode(int line, int column, 
                              ExpressionNode value) {
        super(line, column);
        this.value = value;
    }
    
    public ExpressionNode getValue() {
        return value;
    }
    
    public boolean hasValue() {
        return value != null;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitReturnStatement(this);
    }
}

/**
 * Break statement.
 */
public class BreakStatementNode extends StatementNode {
    public BreakStatementNode(int line, int column) {
        super(line, column);
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitBreakStatement(this);
    }
}

/**
 * Continue statement.
 */
public class ContinueStatementNode extends StatementNode {
    public ContinueStatementNode(int line, int column) {
        super(line, column);
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitContinueStatement(this);
    }
}

Statement nodes represent executable code including control flow and declarations.

3.3 EXPRESSION NODES

Define nodes for expressions:

package com.dynalang.ast;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.LinkedHashMap;

/**
 * Base class for expression nodes.
 */
public abstract class ExpressionNode extends ASTNode {
    public ExpressionNode(int line, int column) {
        super(line, column);
    }
}

/**
 * Number literal.
 */
public class NumberLiteralNode extends ExpressionNode {
    private double value;
    
    public NumberLiteralNode(int line, int column, double value) {
        super(line, column);
        this.value = value;
    }
    
    public double getValue() {
        return value;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitNumberLiteral(this);
    }
}

/**
 * String literal.
 */
public class StringLiteralNode extends ExpressionNode {
    private String value;
    
    public StringLiteralNode(int line, int column, String value) {
        super(line, column);
        this.value = value;
    }
    
    public String getValue() {
        return value;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitStringLiteral(this);
    }
}

/**
 * Boolean literal.
 */
public class BooleanLiteralNode extends ExpressionNode {
    private boolean value;
    
    public BooleanLiteralNode(int line, int column, boolean value) {
        super(line, column);
        this.value = value;
    }
    
    public boolean getValue() {
        return value;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitBooleanLiteral(this);
    }
}

/**
 * Null literal.
 */
public class NullLiteralNode extends ExpressionNode {
    public NullLiteralNode(int line, int column) {
        super(line, column);
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitNullLiteral(this);
    }
}

/**
 * Identifier reference.
 */
public class IdentifierNode extends ExpressionNode {
    private String name;
    
    public IdentifierNode(int line, int column, String name) {
        super(line, column);
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitIdentifier(this);
    }
}

/**
 * This reference.
 */
public class ThisNode extends ExpressionNode {
    public ThisNode(int line, int column) {
        super(line, column);
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitThis(this);
    }
}

/**
 * Binary operation.
 */
public class BinaryOpNode extends ExpressionNode {
    public enum Operator {
        ADD, SUBTRACT, MULTIPLY, DIVIDE, MODULO,
        LESS_THAN, GREATER_THAN, LESS_EQUAL, GREATER_EQUAL,
        EQUAL, NOT_EQUAL,
        LOGICAL_AND, LOGICAL_OR,
        ASSIGN
    }
    
    private Operator operator;
    private ExpressionNode left;
    private ExpressionNode right;
    
    public BinaryOpNode(int line, int column, Operator operator,
                       ExpressionNode left, ExpressionNode right) {
        super(line, column);
        this.operator = operator;
        this.left = left;
        this.right = right;
    }
    
    public Operator getOperator() {
        return operator;
    }
    
    public ExpressionNode getLeft() {
        return left;
    }
    
    public ExpressionNode getRight() {
        return right;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitBinaryOp(this);
    }
}

/**
 * Unary operation.
 */
public class UnaryOpNode extends ExpressionNode {
    public enum Operator {
        LOGICAL_NOT, NEGATE
    }
    
    private Operator operator;
    private ExpressionNode operand;
    
    public UnaryOpNode(int line, int column, Operator operator,
                      ExpressionNode operand) {
        super(line, column);
        this.operator = operator;
        this.operand = operand;
    }
    
    public Operator getOperator() {
        return operator;
    }
    
    public ExpressionNode getOperand() {
        return operand;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitUnaryOp(this);
    }
}

/**
 * Function call.
 */
public class FunctionCallNode extends ExpressionNode {
    private ExpressionNode function;
    private List<ExpressionNode> arguments;
    
    public FunctionCallNode(int line, int column, 
                           ExpressionNode function) {
        super(line, column);
        this.function = function;
        this.arguments = new ArrayList<>();
    }
    
    public ExpressionNode getFunction() {
        return function;
    }
    
    public void addArgument(ExpressionNode arg) {
        arguments.add(arg);
    }
    
    public List<ExpressionNode> getArguments() {
        return arguments;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitFunctionCall(this);
    }
}

/**
 * Index access (array subscript).
 */
public class IndexAccessNode extends ExpressionNode {
    private ExpressionNode object;
    private ExpressionNode index;
    
    public IndexAccessNode(int line, int column, ExpressionNode object,
                          ExpressionNode index) {
        super(line, column);
        this.object = object;
        this.index = index;
    }
    
    public ExpressionNode getObject() {
        return object;
    }
    
    public ExpressionNode getIndex() {
        return index;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitIndexAccess(this);
    }
}

/**
 * Property access.
 */
public class PropertyAccessNode extends ExpressionNode {
    private ExpressionNode object;
    private String property;
    
    public PropertyAccessNode(int line, int column, 
                             ExpressionNode object, String property) {
        super(line, column);
        this.object = object;
        this.property = property;
    }
    
    public ExpressionNode getObject() {
        return object;
    }
    
    public String getProperty() {
        return property;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitPropertyAccess(this);
    }
}

/**
 * Method call.
 */
public class MethodCallNode extends ExpressionNode {
    private ExpressionNode object;
    private String method;
    private List<ExpressionNode> arguments;
    
    public MethodCallNode(int line, int column, ExpressionNode object,
                         String method) {
        super(line, column);
        this.object = object;
        this.method = method;
        this.arguments = new ArrayList<>();
    }
    
    public ExpressionNode getObject() {
        return object;
    }
    
    public String getMethod() {
        return method;
    }
    
    public void addArgument(ExpressionNode arg) {
        arguments.add(arg);
    }
    
    public List<ExpressionNode> getArguments() {
        return arguments;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitMethodCall(this);
    }
}

/**
 * Function expression (anonymous function/lambda).
 */
public class FunctionExpressionNode extends ExpressionNode {
    private List<String> parameters;
    private BlockNode body;
    
    public FunctionExpressionNode(int line, int column, BlockNode body) {
        super(line, column);
        this.body = body;
        this.parameters = new ArrayList<>();
    }
    
    public void addParameter(String param) {
        parameters.add(param);
    }
    
    public List<String> getParameters() {
        return parameters;
    }
    
    public BlockNode getBody() {
        return body;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitFunctionExpression(this);
    }
}

/**
 * List literal.
 */
public class ListLiteralNode extends ExpressionNode {
    private List<ExpressionNode> elements;
    
    public ListLiteralNode(int line, int column) {
        super(line, column);
        this.elements = new ArrayList<>();
    }
    
    public void addElement(ExpressionNode element) {
        elements.add(element);
    }
    
    public List<ExpressionNode> getElements() {
        return elements;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitListLiteral(this);
    }
}

/**
 * Object literal.
 */
public class ObjectLiteralNode extends ExpressionNode {
    private Map<String, ExpressionNode> properties;
    
    public ObjectLiteralNode(int line, int column) {
        super(line, column);
        this.properties = new LinkedHashMap<>();
    }
    
    public void addProperty(String name, ExpressionNode value) {
        properties.put(name, value);
    }
    
    public Map<String, ExpressionNode> getProperties() {
        return properties;
    }
    
    @Override
    public <T> T accept(ASTVisitor<T> visitor) {
        return visitor.visitObjectLiteral(this);
    }
}

Expression nodes represent computations that produce values at runtime.

3.4 AST VISITOR INTERFACE

Define the visitor interface:

package com.dynalang.ast;

/**
 * Visitor interface for traversing the AST.
 */
public interface ASTVisitor<T> {
    T visitProgram(ProgramNode node);
    T visitBlock(BlockNode node);
    T visitExpressionStatement(ExpressionStatementNode node);
    T visitVariableDeclaration(VariableDeclarationNode node);
    T visitFunctionDeclaration(FunctionDeclarationNode node);
    T visitIfStatement(IfStatementNode node);
    T visitWhileStatement(WhileStatementNode node);
    T visitForStatement(ForStatementNode node);
    T visitForeachStatement(ForeachStatementNode node);
    T visitSwitchStatement(SwitchStatementNode node);
    T visitCaseClause(CaseClauseNode node);
    T visitDefaultClause(DefaultClauseNode node);
    T visitTryStatement(TryStatementNode node);
    T visitReturnStatement(ReturnStatementNode node);
    T visitBreakStatement(BreakStatementNode node);
    T visitContinueStatement(ContinueStatementNode node);
    T visitNumberLiteral(NumberLiteralNode node);
    T visitStringLiteral(StringLiteralNode node);
    T visitBooleanLiteral(BooleanLiteralNode node);
    T visitNullLiteral(NullLiteralNode node);
    T visitIdentifier(IdentifierNode node);
    T visitThis(ThisNode node);
    T visitBinaryOp(BinaryOpNode node);
    T visitUnaryOp(UnaryOpNode node);
    T visitFunctionCall(FunctionCallNode node);
    T visitIndexAccess(IndexAccessNode node);
    T visitPropertyAccess(PropertyAccessNode node);
    T visitMethodCall(MethodCallNode node);
    T visitFunctionExpression(FunctionExpressionNode node);
    T visitListLiteral(ListLiteralNode node);
    T visitObjectLiteral(ObjectLiteralNode node);
}

The visitor interface enables clean separation between tree structure and operations.

PART FOUR: PARSE TREE TO AST CONVERSION

4.1 AST BUILDER IMPLEMENTATION

Implement the ANTLR visitor that converts parse tree to AST:

package com.dynalang.parser;

import com.dynalang.ast.*;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.TerminalNode;

/**
 * Converts ANTLR parse tree to custom AST.
 */
public class ASTBuilder extends DynaLangBaseVisitor<ASTNode> {
    
    @Override
    public ASTNode visitProgram(DynaLangParser.ProgramContext ctx) {
        ProgramNode program = new ProgramNode(1, 0);
        
        for (DynaLangParser.StatementContext stmtCtx : ctx.statement()) {
            StatementNode stmt = (StatementNode) visit(stmtCtx);
            program.addStatement(stmt);
        }
        
        return program;
    }
    
    @Override
    public ASTNode visitBlock(DynaLangParser.BlockContext ctx) {
        Token startToken = ctx.getStart();
        BlockNode block = new BlockNode(
            startToken.getLine(),
            startToken.getCharPositionInLine()
        );
        
        for (DynaLangParser.StatementContext stmtCtx : ctx.statement()) {
            StatementNode stmt = (StatementNode) visit(stmtCtx);
            block.addStatement(stmt);
        }
        
        return block;
    }
    
    @Override
    public ASTNode visitExpressionStatement(
            DynaLangParser.ExpressionStatementContext ctx) {
        Token startToken = ctx.getStart();
        ExpressionNode expr = 
            (ExpressionNode) visitExpression(ctx.expression());
        
        return new ExpressionStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            expr
        );
    }
    
    @Override
    public ASTNode visitVariableDeclaration(
            DynaLangParser.VariableDeclarationContext ctx) {
        Token nameToken = ctx.IDENTIFIER().getSymbol();
        ExpressionNode initializer = 
            (ExpressionNode) visitExpression(ctx.expression());
        
        return new VariableDeclarationNode(
            nameToken.getLine(),
            nameToken.getCharPositionInLine(),
            nameToken.getText(),
            initializer
        );
    }
    
    @Override
    public ASTNode visitFunctionDeclaration(
            DynaLangParser.FunctionDeclarationContext ctx) {
        Token nameToken = ctx.IDENTIFIER().getSymbol();
        BlockNode body = (BlockNode) visitBlock(ctx.block());
        
        FunctionDeclarationNode func = new FunctionDeclarationNode(
            nameToken.getLine(),
            nameToken.getCharPositionInLine(),
            nameToken.getText(),
            body
        );
        
        if (ctx.parameterList() != null) {
            for (TerminalNode paramNode : 
                 ctx.parameterList().IDENTIFIER()) {
                func.addParameter(paramNode.getText());
            }
        }
        
        return func;
    }
    
    @Override
    public ASTNode visitIfStatement(
            DynaLangParser.IfStatementContext ctx) {
        Token startToken = ctx.getStart();
        ExpressionNode condition = 
            (ExpressionNode) visitExpression(ctx.expression());
        StatementNode thenBranch = (StatementNode) visitBlock(ctx.block());
        StatementNode elseBranch = null;
        
        if (ctx.ifStatement() != null) {
            elseBranch = (StatementNode) visitIfStatement(
                ctx.ifStatement());
        } else if (ctx.block().size() > 1) {
            elseBranch = (StatementNode) visitBlock(ctx.block(1));
        }
        
        return new IfStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            condition,
            thenBranch,
            elseBranch
        );
    }
    
    @Override
    public ASTNode visitWhileStatement(
            DynaLangParser.WhileStatementContext ctx) {
        Token startToken = ctx.getStart();
        ExpressionNode condition = 
            (ExpressionNode) visitExpression(ctx.expression());
        StatementNode body = (StatementNode) visitBlock(ctx.block());
        
        return new WhileStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            condition,
            body
        );
    }
    
    @Override
    public ASTNode visitForStatement(
            DynaLangParser.ForStatementContext ctx) {
        Token startToken = ctx.getStart();
        String variable = ctx.IDENTIFIER(0).getText();
        ExpressionNode init = 
            (ExpressionNode) visitExpression(ctx.expression(0));
        ExpressionNode condition = 
            (ExpressionNode) visitExpression(ctx.expression(1));
        ExpressionNode update = 
            (ExpressionNode) visitExpression(ctx.expression(2));
        StatementNode body = (StatementNode) visitBlock(ctx.block());
        
        return new ForStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            variable,
            init,
            condition,
            update,
            body
        );
    }
    
    @Override
    public ASTNode visitForeachStatement(
            DynaLangParser.ForeachStatementContext ctx) {
        Token startToken = ctx.getStart();
        String variable = ctx.IDENTIFIER().getText();
        ExpressionNode iterable = 
            (ExpressionNode) visitExpression(ctx.expression());
        StatementNode body = (StatementNode) visitBlock(ctx.block());
        
        return new ForeachStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            variable,
            iterable,
            body
        );
    }
    
    @Override
    public ASTNode visitSwitchStatement(
            DynaLangParser.SwitchStatementContext ctx) {
        Token startToken = ctx.getStart();
        ExpressionNode expression = 
            (ExpressionNode) visitExpression(ctx.expression());
        
        SwitchStatementNode switchStmt = new SwitchStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            expression
        );
        
        for (DynaLangParser.CaseClauseContext caseCtx : 
             ctx.caseClause()) {
            CaseClauseNode caseNode = 
                (CaseClauseNode) visitCaseClause(caseCtx);
            switchStmt.addCase(caseNode);
        }
        
        if (ctx.defaultClause() != null) {
            DefaultClauseNode defaultNode = 
                (DefaultClauseNode) visitDefaultClause(
                    ctx.defaultClause());
            switchStmt.setDefaultClause(defaultNode);
        }
        
        return switchStmt;
    }
    
    @Override
    public ASTNode visitCaseClause(
            DynaLangParser.CaseClauseContext ctx) {
        Token startToken = ctx.getStart();
        ExpressionNode value = 
            (ExpressionNode) visitExpression(ctx.expression());
        boolean hasBreak = ctx.getText().contains("break");
        
        CaseClauseNode caseNode = new CaseClauseNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            value,
            hasBreak
        );
        
        for (DynaLangParser.StatementContext stmtCtx : 
             ctx.statement()) {
            StatementNode stmt = (StatementNode) visit(stmtCtx);
            caseNode.addStatement(stmt);
        }
        
        return caseNode;
    }
    
    @Override
    public ASTNode visitDefaultClause(
            DynaLangParser.DefaultClauseContext ctx) {
        Token startToken = ctx.getStart();
        DefaultClauseNode defaultNode = new DefaultClauseNode(
            startToken.getLine(),
            startToken.getCharPositionInLine()
        );
        
        for (DynaLangParser.StatementContext stmtCtx : 
             ctx.statement()) {
            StatementNode stmt = (StatementNode) visit(stmtCtx);
            defaultNode.addStatement(stmt);
        }
        
        return defaultNode;
    }
    
    @Override
    public ASTNode visitTryStatement(
            DynaLangParser.TryStatementContext ctx) {
        Token startToken = ctx.getStart();
        BlockNode tryBlock = (BlockNode) visitBlock(ctx.block(0));
        String catchVariable = ctx.IDENTIFIER().getText();
        BlockNode catchBlock = (BlockNode) visitBlock(ctx.block(1));
        BlockNode finallyBlock = null;
        
        if (ctx.block().size() > 2) {
            finallyBlock = (BlockNode) visitBlock(ctx.block(2));
        }
        
        return new TryStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            tryBlock,
            catchVariable,
            catchBlock,
            finallyBlock
        );
    }
    
    @Override
    public ASTNode visitReturnStatement(
            DynaLangParser.ReturnStatementContext ctx) {
        Token startToken = ctx.getStart();
        ExpressionNode value = null;
        
        if (ctx.expression() != null) {
            value = (ExpressionNode) visitExpression(ctx.expression());
        }
        
        return new ReturnStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            value
        );
    }
    
    @Override
    public ASTNode visitBreakStatement(
            DynaLangParser.BreakStatementContext ctx) {
        Token startToken = ctx.getStart();
        return new BreakStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine()
        );
    }
    
    @Override
    public ASTNode visitContinueStatement(
            DynaLangParser.ContinueStatementContext ctx) {
        Token startToken = ctx.getStart();
        return new ContinueStatementNode(
            startToken.getLine(),
            startToken.getCharPositionInLine()
        );
    }
    
    @Override
    public ASTNode visitExpression(DynaLangParser.ExpressionContext ctx) {
        if (ctx.primary() != null) {
            return visitPrimary(ctx.primary());
        }
        
        // Function call
        if (ctx.getChildCount() >= 3 && 
            ctx.getChild(1).getText().equals("(")) {
            Token startToken = ctx.getStart();
            ExpressionNode function = 
                (ExpressionNode) visitExpression(
                    (DynaLangParser.ExpressionContext) ctx.getChild(0));
            
            FunctionCallNode call = new FunctionCallNode(
                startToken.getLine(),
                startToken.getCharPositionInLine(),
                function
            );
            
            if (ctx.argumentList() != null) {
                for (DynaLangParser.ExpressionContext argCtx : 
                     ctx.argumentList().expression()) {
                    ExpressionNode arg = 
                        (ExpressionNode) visitExpression(argCtx);
                    call.addArgument(arg);
                }
            }
            
            return call;
        }
        
        // Index access
        if (ctx.getChildCount() == 4 && 
            ctx.getChild(1).getText().equals("[")) {
            Token startToken = ctx.getStart();
            ExpressionNode object = 
                (ExpressionNode) visitExpression(
                    (DynaLangParser.ExpressionContext) ctx.getChild(0));
            ExpressionNode index = 
                (ExpressionNode) visitExpression(
                    (DynaLangParser.ExpressionContext) ctx.getChild(2));
            
            return new IndexAccessNode(
                startToken.getLine(),
                startToken.getCharPositionInLine(),
                object,
                index
            );
        }
        
        // Property access
        if (ctx.getChildCount() == 3 && 
            ctx.getChild(1).getText().equals(".") &&
            ctx.getChild(2) instanceof TerminalNode) {
            Token startToken = ctx.getStart();
            ExpressionNode object = 
                (ExpressionNode) visitExpression(
                    (DynaLangParser.ExpressionContext) ctx.getChild(0));
            String property = ctx.getChild(2).getText();
            
            return new PropertyAccessNode(
                startToken.getLine(),
                startToken.getCharPositionInLine(),
                object,
                property
            );
        }
        
        // Method call
        if (ctx.getChildCount() >= 4 && 
            ctx.getChild(1).getText().equals(".") &&
            ctx.getChild(3).getText().equals("(")) {
            Token startToken = ctx.getStart();
            ExpressionNode object = 
                (ExpressionNode) visitExpression(
                    (DynaLangParser.ExpressionContext) ctx.getChild(0));
            String method = ctx.getChild(2).getText();
            
            MethodCallNode call = new MethodCallNode(
                startToken.getLine(),
                startToken.getCharPositionInLine(),
                object,
                method
            );
            
            if (ctx.argumentList() != null) {
                for (DynaLangParser.ExpressionContext argCtx : 
                     ctx.argumentList().expression()) {
                    ExpressionNode arg = 
                        (ExpressionNode) visitExpression(argCtx);
                    call.addArgument(arg);
                }
            }
            
            return call;
        }
        
        // Unary operations
        if (ctx.getChildCount() == 2) {
            Token startToken = ctx.getStart();
            String opText = ctx.getChild(0).getText();
            ExpressionNode operand = 
                (ExpressionNode) visitExpression(
                    (DynaLangParser.ExpressionContext) ctx.getChild(1));
            
            UnaryOpNode.Operator operator;
            if (opText.equals("!")) {
                operator = UnaryOpNode.Operator.LOGICAL_NOT;
            } else {
                operator = UnaryOpNode.Operator.NEGATE;
            }
            
            return new UnaryOpNode(
                startToken.getLine(),
                startToken.getCharPositionInLine(),
                operator,
                operand
            );
        }
        
        // Binary operations
        if (ctx.getChildCount() == 3 && 
            ctx.getChild(1) instanceof TerminalNode) {
            Token startToken = ctx.getStart();
            ExpressionNode left = 
                (ExpressionNode) visitExpression(
                    (DynaLangParser.ExpressionContext) ctx.getChild(0));
            String opText = ctx.getChild(1).getText();
            ExpressionNode right = 
                (ExpressionNode) visitExpression(
                    (DynaLangParser.ExpressionContext) ctx.getChild(2));
            
            BinaryOpNode.Operator operator = parseBinaryOperator(opText);
            
            return new BinaryOpNode(
                startToken.getLine(),
                startToken.getCharPositionInLine(),
                operator,
                left,
                right
            );
        }
        
        // Parenthesized expression
        if (ctx.getChildCount() == 3 && 
            ctx.getChild(0).getText().equals("(")) {
            return visitExpression(
                (DynaLangParser.ExpressionContext) ctx.getChild(1));
        }
        
        throw new RuntimeException("Unexpected expression structure");
    }
    
    private BinaryOpNode.Operator parseBinaryOperator(String opText) {
        switch (opText) {
            case "+": return BinaryOpNode.Operator.ADD;
            case "-": return BinaryOpNode.Operator.SUBTRACT;
            case "*": return BinaryOpNode.Operator.MULTIPLY;
            case "/": return BinaryOpNode.Operator.DIVIDE;
            case "%": return BinaryOpNode.Operator.MODULO;
            case "<": return BinaryOpNode.Operator.LESS_THAN;
            case ">": return BinaryOpNode.Operator.GREATER_THAN;
            case "<=": return BinaryOpNode.Operator.LESS_EQUAL;
            case ">=": return BinaryOpNode.Operator.GREATER_EQUAL;
            case "==": return BinaryOpNode.Operator.EQUAL;
            case "!=": return BinaryOpNode.Operator.NOT_EQUAL;
            case "&&": return BinaryOpNode.Operator.LOGICAL_AND;
            case "||": return BinaryOpNode.Operator.LOGICAL_OR;
            case "=": return BinaryOpNode.Operator.ASSIGN;
            default:
                throw new RuntimeException("Unknown operator: " + opText);
        }
    }
    
    @Override
    public ASTNode visitPrimary(DynaLangParser.PrimaryContext ctx) {
        Token firstToken = ctx.getStart();
        
        if (ctx.NUMBER() != null) {
            double value = Double.parseDouble(ctx.NUMBER().getText());
            return new NumberLiteralNode(
                firstToken.getLine(),
                firstToken.getCharPositionInLine(),
                value
            );
        }
        
        if (ctx.STRING() != null) {
            String text = ctx.STRING().getText();
            String value = text.substring(1, text.length() - 1);
            value = unescapeString(value);
            return new StringLiteralNode(
                firstToken.getLine(),
                firstToken.getCharPositionInLine(),
                value
            );
        }
        
        if (ctx.BOOLEAN() != null) {
            boolean value = ctx.BOOLEAN().getText().equals("true");
            return new BooleanLiteralNode(
                firstToken.getLine(),
                firstToken.getCharPositionInLine(),
                value
            );
        }
        
        if (ctx.NULL() != null) {
            return new NullLiteralNode(
                firstToken.getLine(),
                firstToken.getCharPositionInLine()
            );
        }
        
        if (ctx.THIS() != null) {
            return new ThisNode(
                firstToken.getLine(),
                firstToken.getCharPositionInLine()
            );
        }
        
        if (ctx.IDENTIFIER() != null) {
            return new IdentifierNode(
                firstToken.getLine(),
                firstToken.getCharPositionInLine(),
                ctx.IDENTIFIER().getText()
            );
        }
        
        if (ctx.functionExpression() != null) {
            return visitFunctionExpression(ctx.functionExpression());
        }
        
        if (ctx.listLiteral() != null) {
            return visitListLiteral(ctx.listLiteral());
        }
        
        if (ctx.objectLiteral() != null) {
            return visitObjectLiteral(ctx.objectLiteral());
        }
        
        throw new RuntimeException("Unexpected primary expression");
    }
    
    @Override
    public ASTNode visitFunctionExpression(
            DynaLangParser.FunctionExpressionContext ctx) {
        Token startToken = ctx.getStart();
        BlockNode body = (BlockNode) visitBlock(ctx.block());
        
        FunctionExpressionNode func = new FunctionExpressionNode(
            startToken.getLine(),
            startToken.getCharPositionInLine(),
            body
        );
        
        if (ctx.parameterList() != null) {
            for (TerminalNode paramNode : 
                 ctx.parameterList().IDENTIFIER()) {
                func.addParameter(paramNode.getText());
            }
        }
        
        return func;
    }
    
    @Override
    public ASTNode visitListLiteral(
            DynaLangParser.ListLiteralContext ctx) {
        Token startToken = ctx.getStart();
        ListLiteralNode list = new ListLiteralNode(
            startToken.getLine(),
            startToken.getCharPositionInLine()
        );
        
        for (DynaLangParser.ExpressionContext exprCtx : 
             ctx.expression()) {
            ExpressionNode element = 
                (ExpressionNode) visitExpression(exprCtx);
            list.addElement(element);
        }
        
        return list;
    }
    
    @Override
    public ASTNode visitObjectLiteral(
            DynaLangParser.ObjectLiteralContext ctx) {
        Token startToken = ctx.getStart();
        ObjectLiteralNode object = new ObjectLiteralNode(
            startToken.getLine(),
            startToken.getCharPositionInLine()
        );
        
        for (DynaLangParser.ObjectPropertyContext propCtx : 
             ctx.objectProperty()) {
            String name = propCtx.IDENTIFIER().getText();
            ExpressionNode value = 
                (ExpressionNode) visitExpression(propCtx.expression());
            object.addProperty(name, value);
        }
        
        return object;
    }
    
    private String unescapeString(String str) {
        return str.replace("\\n", "\n")
                 .replace("\\t", "\t")
                 .replace("\\r", "\r")
                 .replace("\\\"", "\"")
                 .replace("\\'", "'")
                 .replace("\\\\", "\\");
    }
}

The AST builder converts the ANTLR parse tree into our custom AST representation.

PART FIVE: RUNTIME VALUE SYSTEM

5.1 VALUE REPRESENTATION

Define runtime value types:

package com.dynalang.runtime;

import com.dynalang.ast.FunctionDeclarationNode;
import com.dynalang.ast.FunctionExpressionNode;
import com.dynalang.ast.BlockNode;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.LinkedHashMap;

/**
 * Base class for all runtime values.
 */
public abstract class Value {
    public abstract ValueType getType();
    public abstract String toString();
    
    public boolean isNumber() {
        return getType() == ValueType.NUMBER;
    }
    
    public boolean isString() {
        return getType() == ValueType.STRING;
    }
    
    public boolean isBoolean() {
        return getType() == ValueType.BOOLEAN;
    }
    
    public boolean isNull() {
        return getType() == ValueType.NULL;
    }
    
    public boolean isFunction() {
        return getType() == ValueType.FUNCTION;
    }
    
    public boolean isList() {
        return getType() == ValueType.LIST;
    }
    
    public boolean isObject() {
        return getType() == ValueType.OBJECT;
    }
    
    public boolean isTruthy() {
        if (isNull() || (isBoolean() && !((BooleanValue) this).getValue())) {
            return false;
        }
        return true;
    }
}

/**
 * Enumeration of value types.
 */
public enum ValueType {
    NUMBER, STRING, BOOLEAN, NULL, FUNCTION, LIST, OBJECT
}

/**
 * Number value.
 */
public class NumberValue extends Value {
    private double value;
    
    public NumberValue(double value) {
        this.value = value;
    }
    
    public double getValue() {
        return value;
    }
    
    @Override
    public ValueType getType() {
        return ValueType.NUMBER;
    }
    
    @Override
    public String toString() {
        if (value == (long) value) {
            return String.valueOf((long) value);
        }
        return String.valueOf(value);
    }
}

/**
 * String value.
 */
public class StringValue extends Value {
    private String value;
    
    public StringValue(String value) {
        this.value = value;
    }
    
    public String getValue() {
        return value;
    }
    
    @Override
    public ValueType getType() {
        return ValueType.STRING;
    }
    
    @Override
    public String toString() {
        return value;
    }
}

/**
 * Boolean value.
 */
public class BooleanValue extends Value {
    private boolean value;
    
    public BooleanValue(boolean value) {
        this.value = value;
    }
    
    public boolean getValue() {
        return value;
    }
    
    @Override
    public ValueType getType() {
        return ValueType.BOOLEAN;
    }
    
    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

/**
 * Null value.
 */
public class NullValue extends Value {
    private static final NullValue INSTANCE = new NullValue();
    
    private NullValue() {}
    
    public static NullValue getInstance() {
        return INSTANCE;
    }
    
    @Override
    public ValueType getType() {
        return ValueType.NULL;
    }
    
    @Override
    public String toString() {
        return "null";
    }
}

/**
 * Function value with closure.
 */
public class FunctionValue extends Value {
    private List<String> parameters;
    private BlockNode body;
    private Environment closure;  // Captured environment
    private String name;  // Optional name for debugging
    
    public FunctionValue(List<String> parameters, BlockNode body,
                       Environment closure, String name) {
        this.parameters = parameters;
        this.body = body;
        this.closure = closure;
        this.name = name;
    }
    
    public List<String> getParameters() {
        return parameters;
    }
    
    public BlockNode getBody() {
        return body;
    }
    
    public Environment getClosure() {
        return closure;
    }
    
    public String getName() {
        return name;
    }
    
    @Override
    public ValueType getType() {
        return ValueType.FUNCTION;
    }
    
    @Override
    public String toString() {
        if (name != null) {
            return "<function " + name + ">";
        }
        return "<function>";
    }
}

/**
 * List value.
 */
public class ListValue extends Value {
    private List<Value> elements;
    
    public ListValue() {
        this.elements = new ArrayList<>();
    }
    
    public ListValue(List<Value> elements) {
        this.elements = new ArrayList<>(elements);
    }
    
    public List<Value> getElements() {
        return elements;
    }
    
    public Value get(int index) {
        if (index < 0 || index >= elements.size()) {
            throw new RuntimeException("List index out of bounds: " + 
                                     index);
        }
        return elements.get(index);
    }
    
    public void set(int index, Value value) {
        if (index < 0 || index >= elements.size()) {
            throw new RuntimeException("List index out of bounds: " + 
                                     index);
        }
        elements.set(index, value);
    }
    
    public void add(Value value) {
        elements.add(value);
    }
    
    public int size() {
        return elements.size();
    }
    
    @Override
    public ValueType getType() {
        return ValueType.LIST;
    }
    
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < elements.size(); i++) {
            if (i > 0) sb.append(", ");
            sb.append(elements.get(i).toString());
        }
        sb.append("]");
        return sb.toString();
    }
}

/**
 * Object value with prototype chain.
 */
public class ObjectValue extends Value {
    private Map<String, Value> properties;
    private ObjectValue prototype;  // May be null
    
    public ObjectValue() {
        this.properties = new LinkedHashMap<>();
        this.prototype = null;
    }
    
    public ObjectValue(ObjectValue prototype) {
        this.properties = new LinkedHashMap<>();
        this.prototype = prototype;
    }
    
    public Value getProperty(String name) {
        if (properties.containsKey(name)) {
            return properties.get(name);
        }
        
        // Search prototype chain
        if (prototype != null) {
            return prototype.getProperty(name);
        }
        
        return NullValue.getInstance();
    }
    
    public void setProperty(String name, Value value) {
        properties.put(name, value);
    }
    
    public boolean hasOwnProperty(String name) {
        return properties.containsKey(name);
    }
    
    public ObjectValue getPrototype() {
        return prototype;
    }
    
    public void setPrototype(ObjectValue prototype) {
        this.prototype = prototype;
    }
    
    public Map<String, Value> getProperties() {
        return properties;
    }
    
    @Override
    public ValueType getType() {
        return ValueType.OBJECT;
    }
    
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("{");
        boolean first = true;
        for (Map.Entry<String, Value> entry : properties.entrySet()) {
            if (!first) sb.append(", ");
            first = false;
            sb.append(entry.getKey()).append(": ");
            sb.append(entry.getValue().toString());
        }
        sb.append("}");
        return sb.toString();
    }
}

Runtime values represent all data types in DynaLang. Each value knows its type and can convert to string for output.

5.2 ENVIRONMENT IMPLEMENTATION

Define the environment for variable storage and scoping:

package com.dynalang.runtime;

import java.util.HashMap;
import java.util.Map;

/**
 * Environment for variable storage with lexical scoping.
 * Environments chain to parent environments for nested scopes.
 */
public class Environment {
    private Map<String, Value> variables;
    private Environment parent;
    
    /**
     * Create global environment with no parent.
     */
    public Environment() {
        this.variables = new HashMap<>();
        this.parent = null;
    }
    
    /**
     * Create nested environment with parent.
     */
    public Environment(Environment parent) {
        this.variables = new HashMap<>();
        this.parent = parent;
    }
    
    /**
     * Define a variable in this environment.
     */
    public void define(String name, Value value) {
        variables.put(name, value);
    }
    
    /**
     * Get a variable value, searching up the scope chain.
     */
    public Value get(String name) {
        if (variables.containsKey(name)) {
            return variables.get(name);
        }
        
        if (parent != null) {
            return parent.get(name);
        }
        
        throw new RuntimeException("Undefined variable: " + name);
    }
    
    /**
     * Set a variable value, searching up the scope chain.
     * Creates the variable in current scope if not found.
     */
    public void set(String name, Value value) {
        if (variables.containsKey(name)) {
            variables.put(name, value);
            return;
        }
        
        if (parent != null) {
            try {
                parent.set(name, value);
                return;
            } catch (RuntimeException e) {
                // Variable not found in parent chain
            }
        }
        
        // Create new variable in current scope
        variables.put(name, value);
    }
    
    /**
     * Check if variable exists in this environment or parents.
     */
    public boolean has(String name) {
        if (variables.containsKey(name)) {
            return true;
        }
        
        if (parent != null) {
            return parent.has(name);
        }
        
        return false;
    }
    
    public Environment getParent() {
        return parent;
    }
}

The environment implements lexical scoping with variable lookup through the parent chain. Variables can be created dynamically on first assignment.

PART SIX: INTERPRETER IMPLEMENTATION

6.1 INTERPRETER CORE

Implement the interpreter that executes the AST:

package com.dynalang.interpreter;

import com.dynalang.ast.*;
import com.dynalang.runtime.*;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;

/**
 * Interpreter that executes DynaLang AST.
 * Uses visitor pattern to traverse and execute nodes.
 */
public class Interpreter implements ASTVisitor<Value> {
    private Environment globalEnvironment;
    private Environment currentEnvironment;
    private ObjectValue currentThis;  // Current 'this' binding
    
    // Control flow exceptions
    private static class ReturnException extends RuntimeException {
        Value value;
        ReturnException(Value value) {
            this.value = value;
        }
    }
    
    private static class BreakException extends RuntimeException {}
    
    private static class ContinueException extends RuntimeException {}
    
    public Interpreter() {
        this.globalEnvironment = new Environment();
        this.currentEnvironment = globalEnvironment;
        this.currentThis = null;
        
        // Register built-in functions
        registerBuiltins();
    }
    
    private void registerBuiltins() {
        // print function
        globalEnvironment.define("print", new BuiltinFunction("print") {
            @Override
            public Value call(List<Value> arguments) {
                for (Value arg : arguments) {
                    System.out.println(arg.toString());
                }
                return NullValue.getInstance();
            }
        });
        
        // Object.create function for prototype-based inheritance
        ObjectValue objectConstructor = new ObjectValue();
        objectConstructor.setProperty("create", 
            new BuiltinFunction("create") {
            @Override
            public Value call(List<Value> arguments) {
                if (arguments.size() != 1) {
                    throw new RuntimeException(
                        "Object.create requires 1 argument");
                }
                
                Value proto = arguments.get(0);
                if (!proto.isObject()) {
                    throw new RuntimeException(
                        "Object.create requires object argument");
                }
                
                return new ObjectValue((ObjectValue) proto);
            }
        });
        
        globalEnvironment.define("Object", objectConstructor);
    }
    
    /**
     * Execute a program.
     */
    public void execute(ProgramNode program) {
        visitProgram(program);
    }
    
    @Override
    public Value visitProgram(ProgramNode node) {
        Value result = NullValue.getInstance();
        
        for (StatementNode stmt : node.getStatements()) {
            result = stmt.accept(this);
        }
        
        return result;
    }
    
    @Override
    public Value visitBlock(BlockNode node) {
        // Create new scope for block
        Environment previousEnv = currentEnvironment;
        currentEnvironment = new Environment(currentEnvironment);
        
        try {
            Value result = NullValue.getInstance();
            for (StatementNode stmt : node.getStatements()) {
                result = stmt.accept(this);
            }
            return result;
        } finally {
            currentEnvironment = previousEnv;
        }
    }
    
    @Override
    public Value visitExpressionStatement(ExpressionStatementNode node) {
        return node.getExpression().accept(this);
    }
    
    @Override
    public Value visitVariableDeclaration(VariableDeclarationNode node) {
        Value value = node.getInitializer().accept(this);
        currentEnvironment.define(node.getName(), value);
        return value;
    }
    
    @Override
    public Value visitFunctionDeclaration(FunctionDeclarationNode node) {
        FunctionValue function = new FunctionValue(
            node.getParameters(),
            node.getBody(),
            currentEnvironment,  // Capture current environment
            node.getName()
        );
        
        currentEnvironment.define(node.getName(), function);
        return function;
    }
    
    @Override
    public Value visitIfStatement(IfStatementNode node) {
        Value condition = node.getCondition().accept(this);
        
        if (condition.isTruthy()) {
            return node.getThenBranch().accept(this);
        } else if (node.hasElse()) {
            return node.getElseBranch().accept(this);
        }
        
        return NullValue.getInstance();
    }
    
    @Override
    public Value visitWhileStatement(WhileStatementNode node) {
        Value result = NullValue.getInstance();
        
        try {
            while (true) {
                Value condition = node.getCondition().accept(this);
                if (!condition.isTruthy()) {
                    break;
                }
                
                try {
                    result = node.getBody().accept(this);
                } catch (ContinueException e) {
                    // Continue to next iteration
                    continue;
                }
            }
        } catch (BreakException e) {
            // Break out of loop
        }
        
        return result;
    }
    
    @Override
    public Value visitForStatement(ForStatementNode node) {
        // Create new scope for loop variable
        Environment previousEnv = currentEnvironment;
        currentEnvironment = new Environment(currentEnvironment);
        
        try {
            // Initialize loop variable
            Value initValue = node.getInit().accept(this);
            currentEnvironment.define(node.getVariable(), initValue);
            
            Value result = NullValue.getInstance();
            
            try {
                while (true) {
                    // Check condition
                    Value condition = node.getCondition().accept(this);
                    if (!condition.isTruthy()) {
                        break;
                    }
                    
                    // Execute body
                    try {
                        result = node.getBody().accept(this);
                    } catch (ContinueException e) {
                        // Continue to update
                    }
                    
                    // Update loop variable
                    Value updateValue = node.getUpdate().accept(this);
                    currentEnvironment.set(node.getVariable(), updateValue);
                }
            } catch (BreakException e) {
                // Break out of loop
            }
            
            return result;
        } finally {
            currentEnvironment = previousEnv;
        }
    }
    
    @Override
    public Value visitForeachStatement(ForeachStatementNode node) {
        Value iterable = node.getIterable().accept(this);
        
        if (!iterable.isList()) {
            throw new RuntimeException(
                "Foreach requires list, got " + iterable.getType());
        }
        
        ListValue list = (ListValue) iterable;
        
        // Create new scope for loop variable
        Environment previousEnv = currentEnvironment;
        currentEnvironment = new Environment(currentEnvironment);
        
        try {
            Value result = NullValue.getInstance();
            
            try {
                for (Value element : list.getElements()) {
                    currentEnvironment.define(node.getVariable(), element);
                    
                    try {
                        result = node.getBody().accept(this);
                    } catch (ContinueException e) {
                        // Continue to next iteration
                        continue;
                    }
                }
            } catch (BreakException e) {
                // Break out of loop
            }
            
            return result;
        } finally {
            currentEnvironment = previousEnv;
        }
    }
    
    @Override
    public Value visitSwitchStatement(SwitchStatementNode node) {
        Value switchValue = node.getExpression().accept(this);
        
        boolean matched = false;
        Value result = NullValue.getInstance();
        
        try {
            for (CaseClauseNode caseNode : node.getCases()) {
                Value caseValue = caseNode.getValue().accept(this);
                
                if (!matched && valuesEqual(switchValue, caseValue)) {
                    matched = true;
                }
                
                if (matched) {
                    for (StatementNode stmt : caseNode.getStatements()) {
                        result = stmt.accept(this);
                    }
                    
                    if (caseNode.hasBreak()) {
                        break;
                    }
                }
            }
            
            // Execute default if no case matched
            if (!matched && node.hasDefault()) {
                for (StatementNode stmt : 
                     node.getDefaultClause().getStatements()) {
                    result = stmt.accept(this);
                }
            }
        } catch (BreakException e) {
            // Break out of switch
        }
        
        return result;
    }
    
    @Override
    public Value visitCaseClause(CaseClauseNode node) {
        // Handled in visitSwitchStatement
        return NullValue.getInstance();
    }
    
    @Override
    public Value visitDefaultClause(DefaultClauseNode node) {
        // Handled in visitSwitchStatement
        return NullValue.getInstance();
    }
    
    @Override
    public Value visitTryStatement(TryStatementNode node) {
        Value result = NullValue.getInstance();
        
        try {
            result = node.getTryBlock().accept(this);
        } catch (RuntimeException e) {
            // Catch block
            Environment previousEnv = currentEnvironment;
            currentEnvironment = new Environment(currentEnvironment);
            
            try {
                currentEnvironment.define(
                    node.getCatchVariable(),
                    new StringValue(e.getMessage())
                );
                result = node.getCatchBlock().accept(this);
            } finally {
                currentEnvironment = previousEnv;
            }
        } finally {
            // Finally block
            if (node.hasFinally()) {
                node.getFinallyBlock().accept(this);
            }
        }
        
        return result;
    }
    
    @Override
    public Value visitReturnStatement(ReturnStatementNode node) {
        Value value = NullValue.getInstance();
        if (node.hasValue()) {
            value = node.getValue().accept(this);
        }
        throw new ReturnException(value);
    }
    
    @Override
    public Value visitBreakStatement(BreakStatementNode node) {
        throw new BreakException();
    }
    
    @Override
    public Value visitContinueStatement(ContinueStatementNode node) {
        throw new ContinueException();
    }
    
    @Override
    public Value visitNumberLiteral(NumberLiteralNode node) {
        return new NumberValue(node.getValue());
    }
    
    @Override
    public Value visitStringLiteral(StringLiteralNode node) {
        return new StringValue(node.getValue());
    }
    
    @Override
    public Value visitBooleanLiteral(BooleanLiteralNode node) {
        return new BooleanValue(node.getValue());
    }
    
    @Override
    public Value visitNullLiteral(NullLiteralNode node) {
        return NullValue.getInstance();
    }
    
    @Override
    public Value visitIdentifier(IdentifierNode node) {
        return currentEnvironment.get(node.getName());
    }
    
    @Override
    public Value visitThis(ThisNode node) {
        if (currentThis == null) {
            throw new RuntimeException("'this' used outside object method");
        }
        return currentThis;
    }
    
    @Override
    public Value visitBinaryOp(BinaryOpNode node) {
        BinaryOpNode.Operator op = node.getOperator();
        
        // Handle assignment specially
        if (op == BinaryOpNode.Operator.ASSIGN) {
            Value value = node.getRight().accept(this);
            
            if (node.getLeft() instanceof IdentifierNode) {
                String name = ((IdentifierNode) node.getLeft()).getName();
                currentEnvironment.set(name, value);
            } else if (node.getLeft() instanceof PropertyAccessNode) {
                PropertyAccessNode prop = 
                    (PropertyAccessNode) node.getLeft();
                Value object = prop.getObject().accept(this);
                
                if (!object.isObject()) {
                    throw new RuntimeException(
                        "Cannot set property on non-object");
                }
                
                ((ObjectValue) object).setProperty(prop.getProperty(), 
                                                  value);
            } else if (node.getLeft() instanceof IndexAccessNode) {
                IndexAccessNode index = (IndexAccessNode) node.getLeft();
                Value object = index.getObject().accept(this);
                Value indexValue = index.getIndex().accept(this);
                
                if (object.isList()) {
                    if (!indexValue.isNumber()) {
                        throw new RuntimeException(
                            "List index must be number");
                    }
                    int idx = (int) ((NumberValue) indexValue).getValue();
                    ((ListValue) object).set(idx, value);
                } else if (object.isObject()) {
                    String key = indexValue.toString();
                    ((ObjectValue) object).setProperty(key, value);
                } else {
                    throw new RuntimeException(
                        "Cannot index non-list/object");
                }
            } else {
                throw new RuntimeException("Invalid assignment target");
            }
            
            return value;
        }
        
        // Evaluate operands
        Value left = node.getLeft().accept(this);
        Value right = node.getRight().accept(this);
        
        // Arithmetic operations
        if (op == BinaryOpNode.Operator.ADD) {
            if (left.isNumber() && right.isNumber()) {
                return new NumberValue(
                    ((NumberValue) left).getValue() + 
                    ((NumberValue) right).getValue()
                );
            } else if (left.isString() || right.isString()) {
                return new StringValue(
                    left.toString() + right.toString()
                );
            } else {
                throw new RuntimeException(
                    "Cannot add " + left.getType() + " and " + 
                    right.getType());
            }
        }
        
        if (op == BinaryOpNode.Operator.SUBTRACT ||
            op == BinaryOpNode.Operator.MULTIPLY ||
            op == BinaryOpNode.Operator.DIVIDE ||
            op == BinaryOpNode.Operator.MODULO) {
            
            if (!left.isNumber() || !right.isNumber()) {
                throw new RuntimeException(
                    "Arithmetic requires numbers");
            }
            
            double leftVal = ((NumberValue) left).getValue();
            double rightVal = ((NumberValue) right).getValue();
            
            switch (op) {
                case SUBTRACT:
                    return new NumberValue(leftVal - rightVal);
                case MULTIPLY:
                    return new NumberValue(leftVal * rightVal);
                case DIVIDE:
                    if (rightVal == 0) {
                        throw new RuntimeException("Division by zero");
                    }
                    return new NumberValue(leftVal / rightVal);
                case MODULO:
                    return new NumberValue(leftVal % rightVal);
            }
        }
        
        // Comparison operations
        if (op == BinaryOpNode.Operator.LESS_THAN ||
            op == BinaryOpNode.Operator.GREATER_THAN ||
            op == BinaryOpNode.Operator.LESS_EQUAL ||
            op == BinaryOpNode.Operator.GREATER_EQUAL) {
            
            if (!left.isNumber() || !right.isNumber()) {
                throw new RuntimeException(
                    "Comparison requires numbers");
            }
            
            double leftVal = ((NumberValue) left).getValue();
            double rightVal = ((NumberValue) right).getValue();
            
            boolean result;
            switch (op) {
                case LESS_THAN:
                    result = leftVal < rightVal;
                    break;
                case GREATER_THAN:
                    result = leftVal > rightVal;
                    break;
                case LESS_EQUAL:
                    result = leftVal <= rightVal;
                    break;
                case GREATER_EQUAL:
                    result = leftVal >= rightVal;
                    break;
                default:
                    result = false;
            }
            
            return new BooleanValue(result);
        }
        
        // Equality operations
        if (op == BinaryOpNode.Operator.EQUAL) {
            return new BooleanValue(valuesEqual(left, right));
        }
        
        if (op == BinaryOpNode.Operator.NOT_EQUAL) {
            return new BooleanValue(!valuesEqual(left, right));
        }
        
        // Logical operations
        if (op == BinaryOpNode.Operator.LOGICAL_AND) {
            if (!left.isTruthy()) {
                return left;
            }
            return right;
        }
        
        if (op == BinaryOpNode.Operator.LOGICAL_OR) {
            if (left.isTruthy()) {
                return left;
            }
            return right;
        }
        
        throw new RuntimeException("Unknown operator: " + op);
    }
    
    @Override
    public Value visitUnaryOp(UnaryOpNode node) {
        Value operand = node.getOperand().accept(this);
        
        if (node.getOperator() == UnaryOpNode.Operator.LOGICAL_NOT) {
            return new BooleanValue(!operand.isTruthy());
        }
        
        if (node.getOperator() == UnaryOpNode.Operator.NEGATE) {
            if (!operand.isNumber()) {
                throw new RuntimeException(
                    "Cannot negate non-number");
            }
            return new NumberValue(-((NumberValue) operand).getValue());
        }
        
        throw new RuntimeException("Unknown unary operator");
    }
    
    @Override
    public Value visitFunctionCall(FunctionCallNode node) {
        Value function = node.getFunction().accept(this);
        
        if (!function.isFunction() && 
            !(function instanceof BuiltinFunction)) {
            throw new RuntimeException(
                "Cannot call non-function: " + function.getType());
        }
        
        // Evaluate arguments
        List<Value> arguments = new ArrayList<>();
        for (ExpressionNode argExpr : node.getArguments()) {
            arguments.add(argExpr.accept(this));
        }
        
        // Call builtin function
        if (function instanceof BuiltinFunction) {
            return ((BuiltinFunction) function).call(arguments);
        }
        
        // Call user-defined function
        FunctionValue func = (FunctionValue) function;
        
        // Check argument count
        if (arguments.size() != func.getParameters().size()) {
            throw new RuntimeException(
                "Function expects " + func.getParameters().size() + 
                " arguments, got " + arguments.size());
        }
        
        // Create new environment for function execution
        Environment previousEnv = currentEnvironment;
        currentEnvironment = new Environment(func.getClosure());
        
        try {
            // Bind parameters
            for (int i = 0; i < func.getParameters().size(); i++) {
                currentEnvironment.define(
                    func.getParameters().get(i),
                    arguments.get(i)
                );
            }
            
            // Execute function body
            func.getBody().accept(this);
            
            // If no return statement, return null
            return NullValue.getInstance();
            
        } catch (ReturnException e) {
            return e.value;
        } finally {
            currentEnvironment = previousEnv;
        }
    }
    
    @Override
    public Value visitIndexAccess(IndexAccessNode node) {
        Value object = node.getObject().accept(this);
        Value index = node.getIndex().accept(this);
        
        if (object.isList()) {
            if (!index.isNumber()) {
                throw new RuntimeException("List index must be number");
            }
            int idx = (int) ((NumberValue) index).getValue();
            return ((ListValue) object).get(idx);
        } else if (object.isObject()) {
            String key = index.toString();
            return ((ObjectValue) object).getProperty(key);
        } else {
            throw new RuntimeException(
                "Cannot index " + object.getType());
        }
    }
    
    @Override
    public Value visitPropertyAccess(PropertyAccessNode node) {
        Value object = node.getObject().accept(this);
        
        if (!object.isObject() && !object.isList()) {
            throw new RuntimeException(
                "Cannot access property on " + object.getType());
        }
        
        // Handle list methods
        if (object.isList()) {
            ListValue list = (ListValue) object;
            String property = node.getProperty();
            
            if (property.equals("length")) {
                return new BuiltinFunction("length") {
                    @Override
                    public Value call(List<Value> arguments) {
                        return new NumberValue(list.size());
                    }
                };
            } else if (property.equals("push")) {
                return new BuiltinFunction("push") {
                    @Override
                    public Value call(List<Value> arguments) {
                        for (Value arg : arguments) {
                            list.add(arg);
                        }
                        return NullValue.getInstance();
                    }
                };
            }
        }
        
        return ((ObjectValue) object).getProperty(node.getProperty());
    }
    
    @Override
    public Value visitMethodCall(MethodCallNode node) {
        Value object = node.getObject().accept(this);
        
        if (!object.isObject()) {
            throw new RuntimeException(
                "Cannot call method on " + object.getType());
        }
        
        ObjectValue obj = (ObjectValue) object;
        Value method = obj.getProperty(node.getMethod());
        
        if (!method.isFunction() && !(method instanceof BuiltinFunction)) {
            throw new RuntimeException(
                "Property is not a function: " + node.getMethod());
        }
        
        // Evaluate arguments
        List<Value> arguments = new ArrayList<>();
        for (ExpressionNode argExpr : node.getArguments()) {
            arguments.add(argExpr.accept(this));
        }
        
        // Set 'this' binding
        ObjectValue previousThis = currentThis;
        currentThis = obj;
        
        try {
            // Call builtin method
            if (method instanceof BuiltinFunction) {
                return ((BuiltinFunction) method).call(arguments);
            }
            
            // Call user-defined method
            FunctionValue func = (FunctionValue) method;
            
            // Check argument count
            if (arguments.size() != func.getParameters().size()) {
                throw new RuntimeException(
                    "Method expects " + func.getParameters().size() + 
                    " arguments, got " + arguments.size());
            }
            
            // Create new environment for method execution
            Environment previousEnv = currentEnvironment;
            currentEnvironment = new Environment(func.getClosure());
            
            try {
                // Bind parameters
                for (int i = 0; i < func.getParameters().size(); i++) {
                    currentEnvironment.define(
                        func.getParameters().get(i),
                        arguments.get(i)
                    );
                }
                
                // Execute method body
                func.getBody().accept(this);
                
                return NullValue.getInstance();
                
            } catch (ReturnException e) {
                return e.value;
            } finally {
                currentEnvironment = previousEnv;
            }
        } finally {
            currentThis = previousThis;
        }
    }
    
    @Override
    public Value visitFunctionExpression(FunctionExpressionNode node) {
        return new FunctionValue(
            node.getParameters(),
            node.getBody(),
            currentEnvironment,  // Capture current environment (closure)
            null  // Anonymous function
        );
    }
    
    @Override
    public Value visitListLiteral(ListLiteralNode node) {
        ListValue list = new ListValue();
        
        for (ExpressionNode elementExpr : node.getElements()) {
            Value element = elementExpr.accept(this);
            list.add(element);
        }
        
        return list;
    }
    
    @Override
    public Value visitObjectLiteral(ObjectLiteralNode node) {
        ObjectValue object = new ObjectValue();
        
        for (Map.Entry<String, ExpressionNode> entry : 
             node.getProperties().entrySet()) {
            Value value = entry.getValue().accept(this);
            object.setProperty(entry.getKey(), value);
        }
        
        return object;
    }
    
    // Helper methods
    
    private boolean valuesEqual(Value left, Value right) {
        if (left.getType() != right.getType()) {
            return false;
        }
        
        if (left.isNull()) {
            return true;
        }
        
        if (left.isNumber()) {
            return ((NumberValue) left).getValue() == 
                   ((NumberValue) right).getValue();
        }
        
        if (left.isString()) {
            return ((StringValue) left).getValue().equals(
                ((StringValue) right).getValue());
        }
        
        if (left.isBoolean()) {
            return ((BooleanValue) left).getValue() == 
                   ((BooleanValue) right).getValue();
        }
        
        // For objects, lists, and functions, use reference equality
        return left == right;
    }
}

/**
 * Base class for built-in functions.
 */
abstract class BuiltinFunction extends Value {
    private String name;
    
    public BuiltinFunction(String name) {
        this.name = name;
    }
    
    public abstract Value call(List<Value> arguments);
    
    @Override
    public ValueType getType() {
        return ValueType.FUNCTION;
    }
    
    @Override
    public String toString() {
        return "<builtin " + name + ">";
    }
}

The interpreter executes the AST directly by visiting nodes and performing the corresponding operations. It maintains environments for variable storage and handles control flow through exceptions.

PART SEVEN: PUTTING IT ALL TOGETHER

7.1 COMPILER DRIVER

Create the main driver that coordinates all phases:

package com.dynalang;

import com.dynalang.ast.*;
import com.dynalang.parser.*;
import com.dynalang.interpreter.*;
import org.antlr.v4.runtime.*;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * Main driver for DynaLang interpreter.
 */
public class DynaLang {
    
    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: dynalang <source-file>");
            System.exit(1);
        }
        
        String sourceFile = args[0];
        
        try {
            // Phase 1: Parsing
            System.out.println("Parsing " + sourceFile + "...");
            ProgramNode ast = parse(sourceFile);
            if (ast == null) {
                System.err.println("Parsing failed");
                System.exit(1);
            }
            System.out.println("Parsing successful\n");
            
            // Phase 2: Interpretation
            System.out.println("Executing program:");
            System.out.println("--- Output ---");
            Interpreter interpreter = new Interpreter();
            interpreter.execute(ast);
            System.out.println("--- End ---");
            
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
            System.exit(1);
        } catch (RuntimeException e) {
            System.err.println("Runtime error: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }
    
    private static ProgramNode parse(String sourceFile) 
            throws IOException {
        CharStream input = CharStreams.fromFileName(sourceFile);
        
        DynaLangLexer lexer = new DynaLangLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        DynaLangParser parser = new DynaLangParser(tokens);
        
        parser.removeErrorListeners();
        parser.addErrorListener(new BaseErrorListener() {
            @Override
            public void syntaxError(Recognizer<?, ?> recognizer,
                                  Object offendingSymbol,
                                  int line,
                                  int charPositionInLine,
                                  String msg,
                                  RecognitionException e) {
                System.err.println("Syntax error at line " + line + 
                                 ", column " + charPositionInLine + 
                                 ": " + msg);
            }
        });
        
        DynaLangParser.ProgramContext parseTree = parser.program();
        
        if (parser.getNumberOfSyntaxErrors() > 0) {
            return null;
        }
        
        ASTBuilder astBuilder = new ASTBuilder();
        ASTNode ast = astBuilder.visit(parseTree);
        
        return (ProgramNode) ast;
    }
}

The driver coordinates parsing and interpretation phases with clear error reporting.

7.2 EXAMPLE PROGRAMS

Here is a comprehensive example demonstrating all DynaLang features:

// example.dyna - Comprehensive DynaLang example

// Variables can hold any type
x = 42;
print(x);

x = "now a string";
print(x);

x = true;
print(x);

// Functions
function add(a, b) {
    return a + b;
}

result = add(10, 20);
print(result);

// Closures
function makeCounter() {
    count = 0;
    return function() {
        count = count + 1;
        return count;
    };
}

counter1 = makeCounter();
counter2 = makeCounter();

print(counter1());  // 1
print(counter1());  // 2
print(counter2());  // 1

// Objects
person = {
    name: "Alice",
    age: 30,
    greet: function() {
        return "Hello, I'm " + this.name;
    }
};

print(person.name);
print(person.greet());

// Prototype-based inheritance
employee = Object.create(person);
employee.salary = 50000;
employee.work = function() {
    return this.name + " is working";
};

print(employee.name);    // Inherited
print(employee.salary);  // Own property
print(employee.work());

// Lists
numbers = [1, 2, 3, 4, 5];
print(numbers);

numbers.push(6);
print(numbers);

// For loop
sum = 0;
for i = 0; i < numbers.length(); i = i + 1 {
    sum = sum + numbers[i];
}
print(sum);

// Foreach loop
foreach num in numbers {
    print(num * 2);
}

// While loop
count = 0;
while count < 3 {
    print(count);
    count = count + 1;
}

// If-else
if person.age >= 18 {
    print("Adult");
} else {
    print("Minor");
}

// Switch statement
value = 2;
switch value {
    case 1:
        print("One");
        break;
    case 2:
        print("Two");
        break;
    default:
        print("Other");
}

// Exception handling
try {
    result = 10 / 0;
} catch error {
    print("Caught error: " + error);
} finally {
    print("Cleanup executed");
}

// Higher-order functions
function map(list, fn) {
    result = [];
    foreach item in list {
        result.push(fn(item));
    }
    return result;
}

doubled = map([1, 2, 3], function(x) {
    return x * 2;
});
print(doubled);

To compile and run:

java -jar lib/antlr-4.13.1-complete.jar -o src/main/java/com/dynalang/parser -package com.dynalang.parser -visitor grammar/DynaLang.g4

javac -cp "lib/antlr-4.13.1-complete.jar:src/main/java" src/main/java/com/dynalang/*.java src/main/java/com/dynalang/ast/*.java src/main/java/com/dynalang/parser/*.java src/main/java/com/dynalang/interpreter/*.java src/main/java/com/dynalang/runtime/*.java

java -cp "lib/antlr-4.13.1-complete.jar:src/main/java" com.dynalang.DynaLang example.dyna

CONCLUSION

This article demonstrated the complete implementation of a dynamically typed programming language from specification through execution. We covered grammar definition using ANTLR, abstract syntax tree design, runtime value representation, environment management for scoping, and interpreter implementation.

DynaLang includes all requested features including dynamic typing, first-class functions with closures, prototype-based objects, lists, control flow constructs, exception handling, and switch statements. The interpreter directly executes the AST while maintaining runtime type information.

The implementation demonstrates key differences from statically typed languages. Type checking occurs at runtime rather than compile time. Variables can change types during execution. The environment system supports dynamic variable creation and lexical scoping with closures. Objects use prototype chains for inheritance rather than class hierarchies.

This practical example provides a foundation for understanding dynamically typed language implementation. The techniques demonstrated here extend to more complex features like modules, iterators, generators, and metaprogramming capabilities.