Source code for yapcad.dsl.ast

"""
Abstract Syntax Tree (AST) node definitions for the yapCAD DSL v2 (Pythonic Syntax).

The AST represents the structure of a parsed DSL program, which can then
be type-checked and compiled/interpreted.

Changes from v1:
- FunctionDef replaces Command (uses 'def' keyword)
- VarDecl replaces LetStatement (no 'let' keyword, optional type annotation)
- AssertStatement replaces RequireStatement
- NativeFunction for @native decorated functions
- Added PassStatement, WhileStatement, ElifBranch
"""

from dataclasses import dataclass, field
from typing import Optional, List, Union, Any
from abc import ABC, abstractmethod
from .tokens import SourceSpan, TokenType


# =============================================================================
# Base Classes
# =============================================================================

[docs] @dataclass class AstNode(ABC): """Base class for all AST nodes.""" span: SourceSpan # Source location for error reporting
[docs] def accept(self, visitor: "AstVisitor") -> Any: """Accept a visitor for traversal.""" method_name = f"visit_{self.__class__.__name__}" method = getattr(visitor, method_name, visitor.generic_visit) return method(self)
[docs] class AstVisitor(ABC): """Base class for AST visitors."""
[docs] def generic_visit(self, node: AstNode) -> Any: """Default visit method.""" raise NotImplementedError(f"No visitor for {node.__class__.__name__}")
# ============================================================================= # Type Nodes # =============================================================================
[docs] @dataclass class TypeNode(AstNode): """Base class for type annotations.""" pass
[docs] @dataclass class SimpleType(TypeNode): """A simple type like 'int', 'float', 'solid', etc.""" name: str # The type name
[docs] @dataclass class GenericType(TypeNode): """A generic type like 'list[point3d]' or 'dict[str, int]'.""" name: str # 'list' or 'dict' type_args: List[TypeNode] # Type arguments
[docs] @dataclass class OptionalType(TypeNode): """An optional type, e.g., 'point3d?'.""" inner: TypeNode
# ============================================================================= # Expression Nodes # =============================================================================
[docs] @dataclass class Expression(AstNode): """Base class for all expressions.""" pass
[docs] @dataclass class Literal(Expression): """A literal value (int, float, string, bool).""" value: Union[int, float, str, bool] literal_type: TokenType # INT_LITERAL, FLOAT_LITERAL, STRING_LITERAL, BOOL_LITERAL
[docs] @dataclass class Identifier(Expression): """A variable or function name reference.""" name: str
[docs] @dataclass class BinaryOp(Expression): """A binary operation (e.g., a + b, x and y).""" left: Expression operator: TokenType # Includes AND, OR for logical operators right: Expression
[docs] @dataclass class UnaryOp(Expression): """A unary operation (e.g., not x, -n).""" operator: TokenType # Includes NOT for logical negation operand: Expression
[docs] @dataclass class FunctionCall(Expression): """A function or constructor call (e.g., point(1, 2, 3)).""" callee: Expression # Identifier or member access arguments: List[Expression] named_arguments: dict[str, Expression] = field(default_factory=dict)
[docs] @dataclass class MethodCall(Expression): """A method call (e.g., curve.at(0.5)).""" object: Expression method: str arguments: List[Expression] named_arguments: dict[str, Expression] = field(default_factory=dict)
[docs] @dataclass class MemberAccess(Expression): """Member access (e.g., point.x).""" object: Expression member: str
[docs] @dataclass class IndexAccess(Expression): """Index access (e.g., list[0]).""" object: Expression index: Expression
[docs] @dataclass class ListLiteral(Expression): """A list literal (e.g., [1, 2, 3]).""" elements: List[Expression]
[docs] @dataclass class ListComprehension(Expression): """A list comprehension (e.g., [f(x) for x in items if cond]).""" element_expr: Expression variable: str iterable: Expression condition: Optional[Expression] = None # Optional 'if' filter
[docs] @dataclass class RangeExpr(Expression): """A range expression (e.g., 0..10 or range(10)).""" start: Expression end: Expression step: Optional[Expression] = None # For range(start, end, step)
[docs] @dataclass class IfExpr(Expression): """An if-else expression (returns a value).""" condition: Expression then_branch: "Block" elif_branches: List["ElifBranch"] = field(default_factory=list) else_branch: Optional["Block"] = None
[docs] @dataclass class ElifBranch(AstNode): """An elif branch in an if expression/statement.""" condition: Expression body: "Block"
[docs] @dataclass class MatchExpr(Expression): """A match expression.""" subject: Expression arms: List["MatchArm"]
[docs] @dataclass class MatchArm(AstNode): """A single arm of a match expression.""" pattern: "Pattern" body: Expression
[docs] @dataclass class Pattern(AstNode): """Base class for match patterns.""" pass
[docs] @dataclass class LiteralPattern(Pattern): """A literal pattern (e.g., 'match x { case 42: ... }').""" value: Literal
[docs] @dataclass class IdentifierPattern(Pattern): """A binding pattern (e.g., 'match x { case n: ... }').""" name: str
[docs] @dataclass class WildcardPattern(Pattern): """The wildcard pattern '_'.""" pass
[docs] @dataclass class LambdaExpr(Expression): """A lambda/anonymous function (e.g., (x) => x * 2).""" parameters: List[str] body: Expression
[docs] @dataclass class PythonExpr(Expression): """A python block that returns a value (legacy support).""" code: str return_type: "TypeNode" # The 'as <type>' annotation
# ============================================================================= # Statement Nodes # =============================================================================
[docs] @dataclass class Statement(AstNode): """Base class for all statements.""" pass
[docs] @dataclass class VarDecl(Statement): """A variable declaration (Pythonic style, no 'let' keyword). Syntax options: x = 42 # Type inferred x: int = 42 # Explicit type x: int # Declaration without initialization (rare) """ name: str type_annotation: Optional[TypeNode] initializer: Optional[Expression]
# Keep LetStatement as alias for backward compatibility during transition LetStatement = VarDecl
[docs] @dataclass class AssignmentStatement(Statement): """An assignment to an existing variable (e.g., x = 5).""" target: Expression # Identifier or member/index access value: Expression
[docs] @dataclass class AssertStatement(Statement): """An assert statement (e.g., assert x > 0, "x must be positive").""" condition: Expression message: Optional[Expression] = None # String literal message
# Keep RequireStatement as alias for backward compatibility RequireStatement = AssertStatement
[docs] @dataclass class EmitStatement(Statement): """An emit statement with optional metadata kwargs. Syntax: emit gear # Simple emit emit gear, name="spur", material="steel" # With metadata """ value: Expression metadata: dict[str, Expression] = field(default_factory=dict)
[docs] @dataclass class ReturnStatement(Statement): """A return statement.""" value: Optional[Expression] = None
[docs] @dataclass class PassStatement(Statement): """A pass statement (placeholder for empty blocks).""" pass
[docs] @dataclass class DictLiteral(Expression): """A dictionary literal (e.g., {"key": value, ...}).""" entries: dict[str, Expression]
[docs] @dataclass class ForStatement(Statement): """A for loop (e.g., for i in range(n):).""" variable: str iterable: Expression body: "Block"
[docs] @dataclass class WhileStatement(Statement): """A while loop (e.g., while condition:).""" condition: Expression body: "Block"
[docs] @dataclass class IfStatement(Statement): """An if statement (doesn't return a value). Syntax: if condition: ... elif condition: ... else: ... """ condition: Expression then_branch: "Block" elif_branches: List[ElifBranch] = field(default_factory=list) else_branch: Optional["Block"] = None
[docs] @dataclass class ExpressionStatement(Statement): """An expression used as a statement.""" expression: Expression
[docs] @dataclass class Block(AstNode): """A block of statements (indented block). In Pythonic syntax, blocks are delimited by indentation (INDENT/DEDENT) rather than braces. """ statements: List[Statement] # The last statement may be an expression that produces a value final_expression: Optional[Expression] = None
[docs] @dataclass class PythonBlock(Statement): """An inline Python block (legacy support).""" code: str
# ============================================================================= # Decorators and Function Definitions # =============================================================================
[docs] @dataclass class Decorator(AstNode): """A decorator (e.g., @native).""" name: str arguments: List[Expression] = field(default_factory=list)
[docs] @dataclass class Parameter(AstNode): """A function parameter.""" name: str type_annotation: Optional[TypeNode] = None # Optional for type inference default_value: Optional[Expression] = None
[docs] @dataclass class FunctionDef(AstNode): """A function definition (uses 'def' keyword). Syntax: def function_name(param1: type1, param2: type2) -> return_type: ... Replaces the old 'command' syntax. """ name: str parameters: List[Parameter] return_type: Optional[TypeNode] # Optional for type inference body: Block decorators: List[Decorator] = field(default_factory=list)
# Keep Command as alias for backward compatibility Command = FunctionDef
[docs] @dataclass class NativeFunction(AstNode): """A native function (Python code with DSL type signature). Syntax: @native def function_name(param1: type1, param2: type2) -> return_type: '''Python code here''' ... The function body contains Python code that is executed directly. """ name: str parameters: List[Parameter] return_type: TypeNode python_code: str # The Python code in the function body
# ============================================================================= # Legacy Native Block Support (for backward compatibility) # =============================================================================
[docs] @dataclass class NativeFunctionDecl(AstNode): """A function declaration in a native block's exports section (legacy). Represents: fn name(param1: type1, param2: type2) -> return_type; """ name: str parameters: List["Parameter"] return_type: TypeNode
[docs] @dataclass class NativeBlock(AstNode): """A native Python block with exported function declarations (legacy). Syntax: native python { # Python code here } exports { fn func_name(param: type) -> return_type; } The Python code is executed to define functions, which are then made available to the DSL with the declared type signatures. Note: This is the legacy syntax. New code should use @native decorator. """ code: str exports: List[NativeFunctionDecl]
# ============================================================================= # Module-Level Declarations # =============================================================================
[docs] @dataclass class UseStatement(AstNode): """A use/import statement (e.g., use yapcad.stdlib.transforms). Syntax: use module.path use module.path as alias use module.path.{item1, item2} # (future: selective imports) """ module_path: List[str] # ['yapcad', 'stdlib', 'transforms'] alias: Optional[str] = None # 'as' alias
[docs] @dataclass class ExportStatement(AstNode): """An export statement (e.g., export function_name).""" name: str
[docs] @dataclass class ExportUseStatement(AstNode): """An export use statement (e.g., export use other.module).""" module_path: List[str]
[docs] @dataclass class Module(AstNode): """A complete DSL module. Syntax: module module_name use other.module @native def native_func(...): ... def my_function(...): ... """ name: Optional[str] # module name, or None for scripts uses: List[Union[UseStatement, ExportUseStatement]] = field(default_factory=list) native_blocks: List[NativeBlock] = field(default_factory=list) # Legacy support native_functions: List[NativeFunction] = field(default_factory=list) # New style functions: List[FunctionDef] = field(default_factory=list) # All non-native functions exports: List[ExportStatement] = field(default_factory=list) @property def commands(self) -> List[FunctionDef]: """Backward compatibility alias for functions.""" return self.functions
# ============================================================================= # Visitor Helpers # =============================================================================
[docs] class PrintVisitor(AstVisitor): """Debug visitor that prints the AST structure.""" def __init__(self, indent: int = 0): self.indent = indent def _print(self, text: str) -> None: print(" " * self.indent + text)
[docs] def generic_visit(self, node: AstNode) -> None: self._print(f"{node.__class__.__name__}") for name, value in node.__dict__.items(): if name == "span": continue if isinstance(value, AstNode): self._print(f" {name}:") PrintVisitor(self.indent + 2).generic_visit(value) elif isinstance(value, list): self._print(f" {name}: [") for item in value: if isinstance(item, AstNode): PrintVisitor(self.indent + 2).generic_visit(item) else: self._print(f" {item!r}") self._print(" ]") else: self._print(f" {name}: {value!r}")