GAL / Backend /interpreter /interpreter.py
Clarkoer's picture
add comments ehehe
e6b283b
Raw
History Blame Contribute Delete
128 kB
"""Runtime interpreter for the GAL AST.
After lexer, parser, and semantic validation succeed, server.py calls
Interpreter.interpret(ast). This file executes AST nodes and stores runtime
state such as variables, functions, scopes, loop flags, and output.
"""
# AUTO: Imports names from another module.
from shared.ast_nodes import *
# AUTO: Imports a module used by this file.
import threading
# AUTO: Imports a module used by this file.
import sys
# AUTO: Calls `sys.setrecursionlimit`.
sys.setrecursionlimit(10000)
# AUTO: Starts protected code that can catch errors.
try:
# AUTO: Imports a module used by this file.
import eventlet.event as _ev
# AUTO: Sets `_USE_EVENTLET`.
_USE_EVENTLET = True
# AUTO: Handles the matching error case.
except ImportError:
# AUTO: Sets `_USE_EVENTLET`.
_USE_EVENTLET = False
# AUTO: Imports names from another module.
from semantic.errors import SemanticError # noqa: F401 - some runtime checks raise it
# AUTO: Imports names from another module.
from interpreter.errors import ( # noqa: F401 - runtime-specific error classes
# AUTO: Executes this statement.
ReturnValue,
# AUTO: Executes this statement.
_CancelledError,
# AUTO: Executes this statement.
InterpreterError,
# AUTO: Executes this statement.
InterpreterInputRequest,
# AUTO: Closes the current grouped code/data.
)
# AUTO: Defines class `Interpreter`.
class Interpreter:
# AUTO: Defines function `__init__`.
def __init__(self, socketio=None):
# GUIDE: Output and Socket.IO are how plant() and water() communicate with UI.
# LINE: Stores output text generated by plant().
self.output = []
# LINE: Holds the UI/emitter object used to send output/input events.
self.socketio = socketio
# GUIDE: Loop state is used by prune/skip and infinite-loop protection.
# LINE: Tracks active loops so prune/skip know if they are allowed.
self.loop_stack = []
# LINE: Becomes True when prune should stop a loop.
self.break_flag = False
# LINE: Becomes True when skip should jump to the next loop iteration.
self.continue_flag = False
# GUIDE: Input state lets water() pause until the UI sends a value.
# LINE: True while water() is waiting for user input.
self.input_required = False
# LINE: Stores wait objects for interactive water() calls.
self.input_events = {}
# LINE: Stores input values received from the UI.
self.input_values = {}
# LINE: Optional current AST node pointer for runtime context.
self.current_node = None
# LINE: Optional parent AST node pointer for runtime context.
self.current_parent = None
# GUIDE: Runtime symbol storage. scopes[-1] is current active scope.
# LINE: Older/global variable map kept for compatibility.
self.variables = {}
# LINE: Stores variables declared globally.
self.global_variables = {}
# LINE: Stores declared functions by name.
self.functions = {}
# LINE: Scope stack; the last dictionary is the current active scope.
self.scopes = [{}]
# LINE: Name of the function currently executing.
self.current_func_name = None
# LINE: Per-function variable tracking storage.
self.function_variables = {}
# LINE: Stores bundle/struct type definitions.
self.bundle_types = {}
# AUTO: Defines function `declare_variable`.
def declare_variable(self, name, type_, value=None, is_list=False, is_fertile=False):
# LINE: Use the current top scope for this declaration.
scope = self.scopes[-1]
# LINE: Remember current function name if needed for future tracking.
current_func = self.current_func_name
# LINE: If name is not in current scope, create a new runtime variable entry.
if name not in self.scopes[-1]:
# AUTO: Sets `scope[name]`.
scope[name] = {
# LINE: Store GAL type such as seed/tree/vine.
"type": type_,
# LINE: Store the current runtime value.
"value": value,
# LINE: Mark whether this variable is an array/list.
"is_list": is_list,
# LINE: Mark whether this variable is fertile/constant.
"is_fertile": is_fertile
# AUTO: Closes the current grouped code/data.
}
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Duplicate global declaration is a semantic error.
if name in self.global_variables:
# AUTO: Returns this result to the caller.
return f"Semantic Error: Variable '{name}' already declared."
# LINE: Compatibility path for older global storage.
self.variables[name] = {
# AUTO: Executes this statement.
"type": type_,
# AUTO: Executes this statement.
"value": value,
# AUTO: Executes this statement.
"is_list": is_list,
# AUTO: Executes this statement.
"is_fertile": is_fertile
# AUTO: Closes the current grouped code/data.
}
# AUTO: Sets `self.global_variables[name]`.
self.global_variables[name] = self.variables[name]
# AUTO: Defines function `lookup_variable`.
def lookup_variable(self, name):
# LINE: Search from inner scope to outer scope so locals override globals.
for i, scope in enumerate(reversed(self.scopes)):
# AUTO: Checks this condition.
if name in scope:
# LINE: Return the variable info dictionary once found.
return scope[name]
# LINE: Fallback to older variable map if not found in scopes.
if name in self.variables:
# AUTO: Returns this result to the caller.
return self.variables[name]
# LINE: Returning a string means caller should raise an error.
return f"Semantic Error: Variable '{name}' used before declaration."
# AUTO: Defines function `set_variable`.
def set_variable(self, name, value):
# LINE: Search all scopes from inner to outer for assignment target.
for i in reversed(range(len(self.scopes))):
# AUTO: Sets `scope`.
scope = self.scopes[i]
# AUTO: Checks this condition.
if name in scope:
# LINE: Update only the stored value, not type/list/fertile metadata.
scope[name]["value"] = value
# AUTO: Returns this result to the caller.
return
# LINE: Assignment target was never declared.
return f"Semantic Error: Variable '{name}' not declared in any scope."
# AUTO: Defines function `declare_function`.
def declare_function(self, name, return_type, params, node=None):
# LINE: Function names must be unique.
if name in self.functions:
# AUTO: Returns this result to the caller.
return f"Semantic Error: Function '{name}' already declared."
# LINE: Save function metadata and body node for later calls.
self.functions[name] = {"return_type": return_type, "params": params, "node": node}
# AUTO: Defines function `lookup_function`.
def lookup_function(self, name):
# LINE: Return saved function metadata if it exists.
if name in self.functions:
# AUTO: Returns this result to the caller.
return self.functions[name]
# LINE: Return error string if function was never declared.
return f"Semantic Error: Function '{name}' is not defined."
# AUTO: Defines function `enter_scope`.
def enter_scope(self):
# LINE: Push a new local variable dictionary.
self.scopes.append({})
# AUTO: Defines function `exit_scope`.
def exit_scope(self):
# LINE: Never remove the global scope.
if len(self.scopes) > 1:
# LINE: Pop local variables when leaving a block/function.
self.scopes.pop()
# AUTO: Checks this condition.
if self.current_func_name:
# AUTO: Sets `current_func`.
current_func = self.current_func_name
# AUTO: Checks this condition.
if current_func in self.function_variables:
# AUTO: Calls `self.function_variables[current_func].clear`.
self.function_variables[current_func].clear()
# AUTO: Defines function `interpret`.
def interpret(self, node):
# GUIDE: Central runtime dispatcher; each AST node class is sent to its
# matching eval_* method. This is where execution branches by node type.
# LINE: ProgramNode means start the whole program execution.
if isinstance(node, ProgramNode):
# AUTO: Returns this result to the caller.
return self.eval_program(node)
# LINE: BundleDefinitionNode registers a bundle/struct type.
elif isinstance(node, BundleDefinitionNode):
# AUTO: Returns this result to the caller.
return self.eval_bundle_definition(node)
# LINE: MemberAccessNode reads obj.member.
elif isinstance(node, MemberAccessNode):
# AUTO: Returns this result to the caller.
return self.eval_member_access(node)
# LINE: ArrayMemberAccessNode reads arr[i].member.
elif isinstance(node, ArrayMemberAccessNode):
# AUTO: Returns this result to the caller.
return self.eval_array_member_access(node)
# LINE: VariableDeclarationNode creates a variable at runtime.
elif isinstance(node, VariableDeclarationNode):
# AUTO: Returns this result to the caller.
return self.eval_variable_declaration(node)
# LINE: AssignmentNode updates a variable/list/member value.
elif isinstance(node, AssignmentNode):
# AUTO: Returns this result to the caller.
return self.eval_assignment(node)
# LINE: BinaryOpNode evaluates operators like +, -, *, /, ==, &&.
elif isinstance(node, BinaryOpNode):
# AUTO: Sets `value`.
value = self.eval_binary_op(node)
# LINE: Guard against numbers larger than GAL's numeric limit.
if isinstance(value, (int, float)):
# AUTO: Checks this condition.
if value > 1000000000000000 or value < -9999999999999999:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Evaluated number exceeds maximum number of 16 digits", node.line)
# AUTO: Returns this result to the caller.
return value
# LINE: FunctionDeclarationNode is saved, not executed immediately.
elif isinstance(node, FunctionDeclarationNode):
# AUTO: Returns this result to the caller.
return self.eval_function_declaration(node)
# LINE: PrintNode executes plant().
elif isinstance(node, PrintNode):
# AUTO: Returns this result to the caller.
return self.eval_print(node)
# LINE: ListNode builds an array/list value.
elif isinstance(node, ListNode):
# AUTO: Returns this result to the caller.
return self.eval_list(node)
# LINE: ListAccessNode reads arr[index].
elif isinstance(node, ListAccessNode):
# AUTO: Returns this result to the caller.
return self.eval_list_access(node)
# LINE: ReturnNode executes reclaim.
elif isinstance(node, ReturnNode):
# AUTO: Returns this result to the caller.
return self.eval_return(node)
# LINE: FunctionCallNode executes root(), gcd(), or another function call.
elif isinstance(node, FunctionCallNode):
# AUTO: Returns this result to the caller.
return self.eval_function_call(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, AppendNode):
# AUTO: Returns this result to the caller.
return self.eval_append(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, InsertNode):
# AUTO: Returns this result to the caller.
return self.eval_insert(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, RemoveNode):
# AUTO: Returns this result to the caller.
return self.eval_remove(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, UnaryOpNode):
# AUTO: Returns this result to the caller.
return self.eval_unaryop(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, FertileDeclarationNode):
# AUTO: Returns this result to the caller.
return self.eval_sturdy_declaration(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, CastNode):
# AUTO: Returns this result to the caller.
return self.eval_cast(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, SoilNode):
# AUTO: Returns this result to the caller.
return self.eval_soil(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, BloomNode):
# AUTO: Returns this result to the caller.
return self.eval_bloom(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, IfStatementNode):
# AUTO: Returns this result to the caller.
return self.eval_if_statement(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, ForLoopNode):
# AUTO: Returns this result to the caller.
return self.eval_for_loop(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, WhileLoopNode):
# AUTO: Returns this result to the caller.
return self.eval_while_loop(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, DoWhileLoopNode):
# AUTO: Returns this result to the caller.
return self.eval_do_while_loop(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, BreakNode):
# AUTO: Returns this result to the caller.
return self.eval_break(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, ContinueNode):
# AUTO: Returns this result to the caller.
return self.eval_continue(node)
# AUTO: Checks the next alternate condition.
elif isinstance(node, SwitchNode):
# AUTO: Returns this result to the caller.
return self.eval_switch(node)
# AUTO: Checks the next alternate condition.
elif node.node_type == "Input":
# LINE: Input node executes water().
return self.eval_input(node)
# AUTO: Checks the next alternate condition.
elif node.node_type == "Value":
# LINE: Value node converts a literal token into a Python value.
value = self._parse_literal(node.value)
# AUTO: Returns this result to the caller.
return value
# AUTO: Checks the next alternate condition.
elif node.node_type == "Identifier":
# LINE: Identifier reads the stored value of a variable.
var_info = self.lookup_variable(node.value)
# AUTO: Checks this condition.
if isinstance(var_info, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(var_info, node.line)
# AUTO: Returns this result to the caller.
return var_info["value"]
# AUTO: Checks the next alternate condition.
elif node.node_type == "FormattedString":
# LINE: FormattedString removes quotes and decodes escapes.
return self.eval_formatted_string(node)
# AUTO: Checks the next alternate condition.
elif node.node_type == "VariableDeclarationList":
# LINE: Declare each variable inside a grouped declaration list.
for child in node.children:
# AUTO: Calls `self.eval_variable_declaration`.
self.eval_variable_declaration(child)
# AUTO: Checks the next alternate condition.
elif node.node_type == "AssignmentList":
# LINE: Execute each assignment/update inside a grouped assignment list.
for child in node.children:
# AUTO: Checks this condition.
if isinstance(child, AssignmentNode):
# AUTO: Calls `self.eval_assignment`.
self.eval_assignment(child)
# AUTO: Checks the next alternate condition.
elif isinstance(child, UnaryOpNode):
# AUTO: Calls `self.eval_unaryop`.
self.eval_unaryop(child)
# AUTO: Checks the next alternate condition.
elif node.node_type == "List":
# LINE: Evaluate every list child and return a Python list.
return [self.interpret(child) for child in node.children]
# AUTO: Checks the next alternate condition.
elif node.node_type == "Block":
# LINE: Execute a block of statements in order.
self.eval_block(node)
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Unknown AST node means builder/interpreter are out of sync.
raise Exception(f"Unknown AST node type: {node.node_type}")
# AUTO: Defines function `eval_program`.
def eval_program(self, node):
# GUIDE: First register top-level declarations/functions, then call root().
# LINE: Visit each top-level child under ProgramNode.
for child in node.children:
# Top-level children are usually FunctionDeclarationNode, bundle
# definitions, and global declarations. Function declarations are
# stored, not executed yet.
# LINE: This saves function declarations or executes global declarations.
self.interpret(child)
# After registration, create a fake function call node for root().
# This is how the interpreter starts the user's main program.
# LINE: Create a runtime call equivalent to root().
main_call = FunctionCallNode("root", [], node.line)
# LINE: Dispatch that root() call through interpret() like any other call.
return self.interpret(main_call)
# AUTO: Defines function `eval_variable_declaration`.
def eval_variable_declaration(self, node):
# GUIDE: Creates a runtime variable entry, using either an initializer value
# or a default value based on the GAL data type.
# LINE: First child stores the declared type, like seed/tree/vine.
var_type = node.children[0].value
# LINE: Second child stores the variable name.
var_name = node.children[1].value
# LINE: Third child is optional initializer, like = 10 or = water(seed).
value_node = node.children[2] if len(node.children) > 2 else None
# LINE: Starts false and becomes true for array/list initializers.
is_list = False
# LINE: Default runtime values when there is no initializer.
default_values = {
# If a variable has no initializer, these are the runtime defaults.
# AUTO: Executes this statement.
"seed": 0,
# AUTO: Executes this statement.
"tree": 0.0,
# AUTO: Executes this statement.
"leaf": '',
# AUTO: Executes this statement.
"vine": "",
# AUTO: Executes this statement.
"branch": False,
# AUTO: Closes the current grouped code/data.
}
# LINE: If initializer exists, evaluate it before declaring the variable.
if value_node:
# There is an initializer, so evaluate the initializer AST node now.
# LINE: List initializer means array/list value.
if value_node.node_type == "List":
# Array/list initializer: evaluate each element and store a
# Python list as the runtime value.
# AUTO: Checks this condition.
if var_type in self.bundle_types:
# AUTO: Sets `value`.
value = [self._build_bundle_defaults(var_type) for _ in value_node.children]
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Defines function `materialize`.
def materialize(list_node):
# LINE: Convert nested ListNode AST into Python list.
result = []
# AUTO: Starts a loop over these values.
for child in list_node.children:
# LINE: Recursively handle nested array values.
if isinstance(child, ListNode):
# AUTO: Appends a value to a list.
result.append(materialize(child))
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Evaluate normal element expression/literal.
item = self.interpret(child)
# AUTO: Checks this condition.
if var_type == "seed" and isinstance(item, float):
# AUTO: Sets `item`.
item = int(item)
# AUTO: Checks the next alternate condition.
elif var_type == "tree":
# AUTO: Sets `item`.
item = float(item)
# AUTO: Appends a value to a list.
result.append(item)
# AUTO: Returns this result to the caller.
return result
# LINE: Store the materialized Python list as the variable value.
value = materialize(value_node)
# LINE: Mark this declaration as a list/array.
is_list = True
# AUTO: Runs when previous condition did not pass.
else:
# Normal initializer such as seed x = 10 or vine s = "hi".
# LINE: Evaluate the initializer expression.
value = self.interpret(value_node)
# LINE: Convert tree-like float into seed integer if needed.
if var_type == "seed" and isinstance(value, float):
# AUTO: Sets `value`.
value = int(value)
# LINE: seed/tree only accept numeric values.
if var_type in {"tree", "seed"}:
# AUTO: Checks this condition.
if not isinstance(value, (int, float)):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Semantic Error: Type Mismatch! Invalid value for {var_name}", node.line)
# LINE: Prevent branch/boolean from being treated as a number.
if isinstance(value, bool):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Semantic Error: Type Mismatch! Invalid value for {var_name}", node.line)
# LINE: tree stores integer initializer as float.
if var_type == "tree" and isinstance(value, int):
# AUTO: Sets `value`.
value = float(value)
# LINE: leaf must receive a string-like character value.
if var_type == "leaf":
# AUTO: Checks this condition.
if not isinstance(value, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Semantic Error: Type Mismatch! Invalid value for {var_name}", node.line)
# LINE: vine must receive a string value.
if var_type == "vine":
# AUTO: Checks this condition.
if not isinstance(value, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Semantic Error: Type Mismatch! Invalid value for {var_name}", node.line)
# LINE: branch can convert 0/1 numeric-style values into bool.
if var_type == "branch":
# AUTO: Checks this condition.
if isinstance(value, int) or isinstance(value, float):
# AUTO: Checks this condition.
if value == 0:
# AUTO: Sets `value`.
value = False
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Sets `value`.
value = True
# AUTO: Runs when previous condition did not pass.
else:
# No initializer, so use a default value based on type.
# LINE: Bundle variables get default member dictionaries.
if var_type in self.bundle_types:
# AUTO: Sets `value`.
value = self._build_bundle_defaults(var_type)
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Built-in types get their default value from default_values.
value = default_values.get(var_type, None)
# LINE: Save the variable into the current runtime scope.
self.declare_variable(var_name, var_type, value, is_list=is_list)
# AUTO: Defines function `eval_bundle_definition`.
def eval_bundle_definition(self, node):
# LINE: Save bundle type name with its member definitions.
self.bundle_types[node.bundle_name] = node.members
# AUTO: Defines function `_build_bundle_defaults`.
def _build_bundle_defaults(self, bundle_type_name):
# LINE: Default values for each built-in GAL member type.
_member_defaults = {"seed": 0, "tree": 0.0, "leaf": '', "vine": "", "branch": False}
# LINE: Get member list for this bundle type.
members = self.bundle_types[bundle_type_name]
# LINE: Build a dictionary value for the bundle instance.
result = {}
# LINE: Visit every member name/type in the bundle.
for name, typ in members.items():
# LINE: Nested bundle member gets its own default dictionary.
if typ in self.bundle_types:
# AUTO: Sets `result[name]`.
result[name] = self._build_bundle_defaults(typ)
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Built-in member gets a simple default value.
result[name] = _member_defaults.get(typ, None)
# LINE: Return default bundle value.
return result
# AUTO: Defines function `eval_member_access`.
def eval_member_access(self, node):
# LINE: First child is object/previous access; second child is member name.
obj_child = node.children[0]
# LINE: Store member name being read.
member_name = node.children[1].value
# LINE: Nested member access, like a.b.c.
if obj_child.node_type == "MemberAccess":
# AUTO: Sets `bundle_value`.
bundle_value = self.eval_member_access(obj_child)
# LINE: Array member access, like students[i].age.
elif obj_child.node_type == "ArrayMemberAccess":
# AUTO: Sets `bundle_value`.
bundle_value = self.eval_array_member_access(obj_child)
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Simple object name, like student.age.
obj_name = obj_child.value
# LINE: Look up the object variable.
var_info = self.lookup_variable(obj_name)
# AUTO: Checks this condition.
if isinstance(var_info, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(var_info, node.line)
# LINE: Get the bundle dictionary value.
bundle_value = var_info["value"]
# LINE: Member access only works on bundle dictionaries.
if not isinstance(bundle_value, dict):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Value is not a bundle.", node.line)
# LINE: Requested member must exist in the bundle.
if member_name not in bundle_value:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Bundle has no member '{member_name}'.", node.line)
# LINE: Return the member's stored value.
return bundle_value[member_name]
# AUTO: Defines function `eval_array_member_access`.
def eval_array_member_access(self, node):
# LINE: First child is array access part, like students[i].
list_access_node = node.children[0]
# LINE: Second child is member name, like age.
member_name = node.children[1].value
# LINE: Evaluate students[i] first.
bundle_element = self.eval_list_access(list_access_node)
# LINE: Array element must be a bundle dictionary.
if not isinstance(bundle_element, dict):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Array element is not a bundle.", node.line)
# LINE: Requested bundle member must exist.
if member_name not in bundle_element:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Bundle has no member '{member_name}'.", node.line)
# LINE: Return students[i].member value.
return bundle_element[member_name]
# AUTO: Defines function `eval_sturdy_declaration`.
def eval_sturdy_declaration(self, node):
# LINE: First child is fertile variable type.
var_type = node.children[0].value
# LINE: Second child is fertile variable name.
var_name = node.children[1].value
# LINE: Third child is required initializer.
value_node = node.children[2]
# LINE: Evaluate initializer.
value = self.interpret(value_node)
# LINE: Declare variable with is_fertile=True so reassignment is blocked.
self.declare_variable(var_name, var_type, value, is_list=False, is_fertile=True)
# AUTO: Defines function `eval_assignment`.
def eval_assignment(self, node):
# GUIDE: Assignments evaluate RHS first, then write into a variable,
# array element, or bundle member target.
# LINE: Left child is the assignment target.
target_node = node.children[0]
# LINE: Right child is the value/expression being assigned.
value_node = node.children[1]
# LINE: RHS list means assign an array/list value.
if value_node.node_type == "List":
# RHS is an array/list value.
# AUTO: Sets `value`.
value = []
# AUTO: Starts a loop over these values.
for val in value_node.children:
# LINE: Evaluate each list item before storing.
item = self.interpret(val)
# AUTO: Appends a value to a list.
value.append(item)
# AUTO: Runs when previous condition did not pass.
else:
# RHS is an expression, literal, function call, water(), etc.
# LINE: Evaluate the right side first, such as a + b or water(seed).
value = self.interpret(value_node)
# AUTO: Checks this condition.
if isinstance(value_node, AppendNode) or isinstance(value_node, InsertNode) or isinstance(value_node, RemoveNode):
# LINE: append/insert/remove already changed the list themselves.
return
# LINE: If target is arr[index], write into a list element.
if target_node.node_type == "ListAccess":
# Assignment into an array/list element, e.g. arr[i] = value.
# LINE: Collect indexes for arr[i] or nested arr[i][j].
indices = []
# LINE: Start from the ListAccess target node.
current = target_node
# LINE: Walk nested ListAccess nodes from outside to inside.
while hasattr(current, 'node_type') and current.node_type == "ListAccess":
# LINE: Evaluate the index expression inside brackets.
idx = self.interpret(current.children[1].children[0])
# LINE: Index must be an integer.
if not isinstance(idx, int):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: List index must be an integer. Got '{idx}'", node.line)
# LINE: Store the index for later navigation.
indices.append(idx)
# LINE: Move toward the base list name.
current = current.children[0].value
# LINE: current now holds the base list variable name.
list_name = current
# LINE: Look up the list variable.
list_entry = self.lookup_variable(list_name)
# AUTO: Checks this condition.
if isinstance(list_entry, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(list_entry, node.line)
# LINE: Get the actual list/string value.
list_value = list_entry["value"]
# LINE: Target must be a list or string.
if not isinstance(list_value, (list, str)):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Variable '{list_name}' is not a list.", node.line)
# LINE: String index assignment path.
if isinstance(list_value, str):
# LINE: Strings only support one-dimensional indexing.
if len(indices) != 1:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Multi-dimensional indexing not supported for strings.", node.line)
# LINE: Final string index.
final_idx = indices[0]
# LINE: Check string index bounds.
if final_idx < 0 or final_idx >= len(list_value):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Index '{final_idx}' out of bounds for '{list_name}'.", node.line)
# LINE: String index assignment must receive one character.
if not isinstance(value, str) or len(value) != 1:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Can only assign a single character to a string index.", node.line)
# LINE: Create updated string with one character replaced.
list_value = list_value[:final_idx] + value + list_value[final_idx + 1:]
# LINE: Save updated string back into variable entry.
list_entry["value"] = list_value
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Reverse indexes so traversal starts from base list.
indices.reverse()
# LINE: Start navigating from the base list value.
target = list_value
# LINE: Walk all indexes except the final assignment index.
for i, idx in enumerate(indices[:-1]):
# LINE: Check each intermediate index is within bounds.
if idx < 0 or idx >= len(target):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Index '{idx}' out of bounds for list '{list_name}'.", node.line)
# LINE: Move into the nested list.
target = target[idx]
# LINE: Intermediate target must still be a list.
if not isinstance(target, list):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Cannot index into a non-list value.", node.line)
# LINE: Last index is where assignment happens.
final_idx = indices[-1]
# LINE: Check final index bounds.
if final_idx < 0 or final_idx >= len(target):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Index '{final_idx}' out of bounds for list '{list_name}'.", node.line)
# LINE: Store RHS value into final list element.
target[final_idx] = value
# AUTO: Checks the next alternate condition.
elif target_node.node_type == "MemberAccess":
# Assignment into a bundle member, e.g. student.age = 20.
# LINE: chain stores member path names, like ["info", "age"].
chain = []
# LINE: Start from full member access target.
current = target_node
# LINE: Walk member access nodes until reaching base object.
while hasattr(current, 'node_type') and current.node_type == "MemberAccess":
# LINE: Save current member name.
chain.append(current.children[1].value)
# LINE: Move left toward the base object.
current = current.children[0]
# LINE: Reverse to access members from base object outward.
chain.reverse()
# LINE: Base object might be array member access.
if hasattr(current, 'node_type') and current.node_type == "ArrayMemberAccess":
# AUTO: Sets `bundle_value`.
bundle_value = self.interpret(current)
# AUTO: Checks this condition.
if not isinstance(bundle_value, dict):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Value is not a bundle.", node.line)
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Base object is a normal variable.
obj_name = current.value
# LINE: Look up bundle variable.
var_info = self.lookup_variable(obj_name)
# AUTO: Checks this condition.
if isinstance(var_info, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(var_info, node.line)
# LINE: Get bundle dictionary from variable.
bundle_value = var_info["value"]
# AUTO: Checks this condition.
if not isinstance(bundle_value, dict):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Variable '{obj_name}' is not a bundle.", node.line)
# LINE: Navigate through nested bundle members before the final member.
for member in chain[:-1]:
# AUTO: Checks this condition.
if member not in bundle_value:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Bundle has no member '{member}'.", node.line)
# AUTO: Sets `bundle_value`.
bundle_value = bundle_value[member]
# AUTO: Checks this condition.
if not isinstance(bundle_value, dict):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Member '{member}' is not a bundle.", node.line)
# LINE: Final member is the field being assigned.
final_member = chain[-1]
# LINE: Final member must exist.
if final_member not in bundle_value:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Bundle has no member '{final_member}'.", node.line)
# LINE: Find the declared type chain for the final member.
type_chain_current = current
# AUTO: Checks this condition.
if hasattr(type_chain_current, 'node_type') and type_chain_current.node_type == "ArrayMemberAccess":
# LINE: Get base array name from array member access.
la_node = type_chain_current.children[0]
# AUTO: Repeats while this condition is true.
while hasattr(la_node, 'node_type') and la_node.node_type == "ListAccess":
# AUTO: Sets `la_node`.
la_node = la_node.children[0].value
# LINE: Read bundle type from base array variable.
var_type = self.lookup_variable(la_node)["type"] if not isinstance(self.lookup_variable(la_node), str) else None # type: ignore
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Base object type comes from the variable entry.
obj_name = type_chain_current.value
# AUTO: Sets `var_info`.
var_info = self.lookup_variable(obj_name)
# AUTO: Sets `var_type`.
var_type = var_info["type"] if not isinstance(var_info, str) else None
# LINE: Convert assigned value to match member type when needed.
if var_type and var_type in self.bundle_types:
# AUTO: Sets `cur_type`.
cur_type = var_type
# AUTO: Starts a loop over these values.
for member in chain:
# AUTO: Checks this condition.
if cur_type in self.bundle_types:
# AUTO: Sets `cur_type`.
cur_type = self.bundle_types[cur_type].get(member, cur_type)
# AUTO: Checks this condition.
if cur_type == "seed" and isinstance(value, float):
# AUTO: Sets `value`.
value = int(value)
# AUTO: Checks the next alternate condition.
elif cur_type == "tree" and isinstance(value, int):
# AUTO: Sets `value`.
value = float(value)
# AUTO: Checks the next alternate condition.
elif cur_type == "branch" and isinstance(value, int):
# AUTO: Executes this statement.
value = True if value != 0 else False
# LINE: Store value into the bundle member.
bundle_value[final_member] = value
# AUTO: Checks the next alternate condition.
elif target_node.node_type == "ArrayMemberAccess":
# Assignment into a bundle member inside an array element,
# e.g. students[i].age = 20.
# LINE: First child is list element access.
list_access_node = target_node.children[0]
# LINE: Second child is member name.
member_name = target_node.children[1].value
# LINE: Evaluate the array element first.
bundle_element = self.eval_list_access(list_access_node)
# LINE: Array element must be a bundle dictionary.
if not isinstance(bundle_element, dict):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Array element is not a bundle.", node.line)
# LINE: Requested member must exist.
if member_name not in bundle_element:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Bundle has no member '{member_name}'.", node.line)
# LINE: Find the base array variable name.
current = list_access_node
# AUTO: Repeats while this condition is true.
while hasattr(current, 'node_type') and current.node_type == "ListAccess":
# AUTO: Sets `current`.
current = current.children[0].value
# LINE: current now stores the base variable name.
var_name = current
# LINE: Look up base array variable.
var_info = self.lookup_variable(var_name)
# LINE: Convert value to member type if the base array is a bundle type.
if not isinstance(var_info, str) and var_info["type"] in self.bundle_types:
# AUTO: Sets `member_type`.
member_type = self.bundle_types[var_info["type"]].get(member_name)
# AUTO: Checks this condition.
if member_type == "seed" and isinstance(value, float):
# AUTO: Sets `value`.
value = int(value)
# AUTO: Checks the next alternate condition.
elif member_type == "tree" and isinstance(value, int):
# AUTO: Sets `value`.
value = float(value)
# AUTO: Checks the next alternate condition.
elif member_type == "branch" and isinstance(value, int):
# AUTO: Executes this statement.
value = True if value != 0 else False
# LINE: Store value into the array element's member.
bundle_element[member_name] = value
# AUTO: Runs when previous condition did not pass.
else:
# Simple variable assignment, e.g. total = x + y.
# LINE: Target node value is the variable name.
var_name = target_node.value
# LINE: Look up variable metadata and current value.
var_info = self.lookup_variable(var_name)
# AUTO: Checks this condition.
if isinstance(var_info, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(var_info, node.line)
# LINE: Read declared type for conversion.
var_type = var_info["type"]
# LINE: Assigning float to seed truncates to int.
if var_type == "seed" and isinstance(value, float):
# AUTO: Sets `value`.
value = int(value)
# LINE: Assigning int to tree converts to float.
if var_type == "tree" and isinstance(value, int):
# AUTO: Sets `value`.
value = float(value)
# LINE: Assigning int to branch converts 0 to False and nonzero to True.
if var_type == "branch" and isinstance(value, int):
# AUTO: Executes this statement.
value = True if value != 0 else False
# LINE: Save converted value into the variable.
self.set_variable(var_name, value)
# LINE: Return assigned value so assignment expression can be reused.
return value
# AUTO: Defines function `eval_binary_op`.
def eval_binary_op(self, node):
# GUIDE: Binary operations evaluate left/right child expressions before
# applying arithmetic, comparison, logical, or concat behavior.
# Example AST for x + y:
# left child = Identifier(x), right child = Identifier(y), value = "+"
# LINE: Evaluate the left operand first.
left = self.interpret(node.children[0])
# LINE: Evaluate the right operand second.
right = self.interpret(node.children[1])
# LINE: node.value stores the actual operator symbol.
operator = node.value
# LINE: Backtick is GAL string concatenation.
if operator == '`':
# GAL string concat operator.
# AUTO: Sets `result`.
result = str(left) + str(right)
# AUTO: Returns this result to the caller.
return result
# Convert token/literal strings like "10", "~5", "sunshine" into Python
# values like 10, -5, True before applying the operator.
# LINE: Convert left literal text into Python int/float/bool/string if needed.
left = self._parse_literal(left)
# LINE: Convert right literal text into Python int/float/bool/string if needed.
right = self._parse_literal(right)
# LINE: Plus with any string becomes concatenation.
if operator == '+' and (isinstance(left, str) or isinstance(right, str)):
# AUTO: Sets `result`.
result = str(left) + str(right)
# AUTO: Returns this result to the caller.
return result
# AUTO: Starts protected code that can catch errors.
try:
# LINE: Choose operation based on the operator stored in the AST node.
if operator == '+':
# Numeric addition. If both operands are non-numeric, convert
# truthy/empty values into numbers first.
# AUTO: Checks this condition.
if not isinstance(left, (int, float)) and not isinstance(right, (int, float)):
# AUTO: Checks this condition.
if isinstance(left, bool):
# AUTO: Executes this statement.
left = 1 if left == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(left, str):
# AUTO: Executes this statement.
left = 1 if left != "" else 0
# AUTO: Checks this condition.
if isinstance(right, bool):
# AUTO: Executes this statement.
right = 1 if right == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(right, str):
# AUTO: Executes this statement.
right = 1 if right != "" else 0
# AUTO: Returns this result to the caller.
return left + right # type: ignore[operator]
# AUTO: Checks the next alternate condition.
elif operator == '-':
# LINE: Subtraction path.
if not isinstance(left, (int, float)) and not isinstance(right, (int, float)):
# AUTO: Checks this condition.
if isinstance(left, bool):
# AUTO: Executes this statement.
left = 1 if left == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(left, str):
# AUTO: Executes this statement.
left = 1 if left != "" else 0
# AUTO: Checks this condition.
if isinstance(right, bool):
# AUTO: Executes this statement.
right = 1 if right == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(right, str):
# AUTO: Executes this statement.
right = 1 if right != "" else 0
# AUTO: Returns this result to the caller.
return left - right # type: ignore[operator]
# AUTO: Checks the next alternate condition.
elif operator == '*':
# LINE: Multiplication path.
if not isinstance(left, (int, float)) and not isinstance(right, (int, float)):
# AUTO: Checks this condition.
if isinstance(left, bool):
# AUTO: Executes this statement.
left = 1 if left == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(left, str):
# AUTO: Executes this statement.
left = 1 if left != "" else 0
# AUTO: Checks this condition.
if isinstance(right, bool):
# AUTO: Executes this statement.
right = 1 if right == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(right, str):
# AUTO: Executes this statement.
right = 1 if right != "" else 0
# AUTO: Returns this result to the caller.
return left * right # type: ignore[operator]
# AUTO: Checks the next alternate condition.
elif operator == '**':
# LINE: Exponent path.
if not isinstance(left, (int, float)) and not isinstance(right, (int, float)):
# AUTO: Checks this condition.
if isinstance(left, bool):
# AUTO: Executes this statement.
left = 1 if left == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(left, str):
# AUTO: Executes this statement.
left = 1 if left != "" else 0
# AUTO: Checks this condition.
if isinstance(right, bool):
# AUTO: Executes this statement.
right = 1 if right == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(right, str):
# AUTO: Executes this statement.
right = 1 if right != "" else 0
# AUTO: Returns this result to the caller.
return left ** right # type: ignore[operator]
# AUTO: Checks the next alternate condition.
elif operator == '/':
# Division includes runtime zero checking.
# LINE: Division path.
if not isinstance(left, (int, float)) and not isinstance(right, (int, float)):
# AUTO: Checks this condition.
if isinstance(left, bool):
# AUTO: Executes this statement.
left = 1 if left == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(left, str):
# AUTO: Executes this statement.
left = 1 if left != "" else 0
# AUTO: Checks this condition.
if isinstance(right, bool):
# AUTO: Executes this statement.
right = 1 if right == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(right, str):
# AUTO: Executes this statement.
right = 1 if right != "" else 0
# AUTO: Checks this condition.
if right == 0:
# LINE: Stop execution when divisor is zero.
raise InterpreterError("Runtime Error: Division by zero is undefined", node.line)
# AUTO: Returns this result to the caller.
return left / right # type: ignore[operator]
# AUTO: Checks the next alternate condition.
elif operator == '%':
# LINE: Modulo/remainder path.
if not isinstance(left, (int, float)) and not isinstance(right, (int, float)):
# AUTO: Checks this condition.
if isinstance(left, bool):
# AUTO: Executes this statement.
left = 1 if left == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(left, str):
# AUTO: Executes this statement.
left = 1 if left != "" else 0
# AUTO: Checks this condition.
if isinstance(right, bool):
# AUTO: Executes this statement.
right = 1 if right == True else 0
# AUTO: Checks the next alternate condition.
elif isinstance(right, str):
# AUTO: Executes this statement.
right = 1 if right != "" else 0
# AUTO: Checks this condition.
if right == 0:
# LINE: Modulo by zero is also invalid.
raise InterpreterError("Runtime Error: Division by zero is undefined", node.line)
# AUTO: Returns this result to the caller.
return left % right # type: ignore[operator]
# AUTO: Checks the next alternate condition.
elif operator == '==':
# Comparison operators return branch/boolean results.
# LINE: Equality comparison.
return left == right
# AUTO: Checks the next alternate condition.
elif operator == '!=':
# LINE: Not-equal comparison.
return left != right
# AUTO: Checks the next alternate condition.
elif operator == '<':
# LINE: Less-than comparison.
if isinstance(left, str):
# AUTO: Executes this statement.
left = 0 if left == "" else 1
# AUTO: Checks this condition.
if isinstance(right, str):
# AUTO: Executes this statement.
right = 0 if right == "" else 1
# AUTO: Returns this result to the caller.
return left < right
# AUTO: Checks the next alternate condition.
elif operator == '<=':
# LINE: Less-than-or-equal comparison.
if isinstance(left, str):
# AUTO: Executes this statement.
left = 0 if left == "" else 1
# AUTO: Checks this condition.
if isinstance(right, str):
# AUTO: Executes this statement.
right = 0 if right == "" else 1
# AUTO: Returns this result to the caller.
return left <= right
# AUTO: Checks the next alternate condition.
elif operator == '>':
# LINE: Greater-than comparison.
if isinstance(left, str):
# AUTO: Executes this statement.
left = 0 if left == "" else 1
# AUTO: Checks this condition.
if isinstance(right, str):
# AUTO: Executes this statement.
right = 0 if right == "" else 1
# AUTO: Returns this result to the caller.
return left > right
# AUTO: Checks the next alternate condition.
elif operator == '>=':
# LINE: Greater-than-or-equal comparison.
if isinstance(left, str):
# AUTO: Executes this statement.
left = 0 if left == "" else 1
# AUTO: Checks this condition.
if isinstance(right, str):
# AUTO: Executes this statement.
right = 0 if right == "" else 1
# AUTO: Returns this result to the caller.
return left >= right
# AUTO: Checks the next alternate condition.
elif operator == '&&':
# Logical operators convert numeric/string values into boolean
# truthiness before applying AND/OR.
# LINE: Logical AND path.
if isinstance(left, int) or isinstance(left, float):
# AUTO: Checks this condition.
if left == 0:
# AUTO: Sets `left`.
left = False
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Sets `left`.
left = True
# AUTO: Checks the next alternate condition.
elif isinstance(right, int) or isinstance(right, float):
# AUTO: Checks this condition.
if right == 0:
# AUTO: Sets `right`.
right = False
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Sets `right`.
right = True
# AUTO: Checks the next alternate condition.
elif isinstance(left, str):
# AUTO: Executes this statement.
left = False if left == "" else True
# AUTO: Checks the next alternate condition.
elif isinstance(right, str):
# AUTO: Executes this statement.
right = False if right == "" else True
# AUTO: Checks the next alternate condition.
elif isinstance(left, str) or isinstance(right, str):
# AUTO: Sets `left`.
left = bool(left)
# AUTO: Checks the next alternate condition.
elif isinstance(left, str) or isinstance(right, str):
# AUTO: Sets `right`.
right = bool(right)
# AUTO: Returns this result to the caller.
return bool(left) and bool(right)
# AUTO: Checks the next alternate condition.
elif operator == '||':
# LINE: Logical OR path.
if isinstance(left, int) or isinstance(left, float):
# AUTO: Checks this condition.
if left == 0:
# AUTO: Sets `left`.
left = False
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Sets `left`.
left = True
# AUTO: Checks the next alternate condition.
elif isinstance(right, int) or isinstance(right, float):
# AUTO: Checks this condition.
if right == 0:
# AUTO: Sets `right`.
right = False
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Sets `right`.
right = True
# AUTO: Checks the next alternate condition.
elif isinstance(left, str) or isinstance(right, str):
# AUTO: Sets `left`.
left = bool(left)
# AUTO: Checks the next alternate condition.
elif isinstance(left, str) or isinstance(right, str):
# AUTO: Sets `right`.
right = bool(right)
# AUTO: Returns this result to the caller.
return bool(left) or bool(right)
# AUTO: Checks the next alternate condition.
elif operator == '!':
# AUTO: Returns this result to the caller.
return not bool(left)
# AUTO: Checks the next alternate condition.
elif operator == 'neg':
# AUTO: Returns this result to the caller.
return -left # type: ignore
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Stops this flow by raising an error.
raise Exception(f"Unknown operator: {operator}")
# AUTO: Handles the matching error case.
except ZeroDivisionError:
# AUTO: Stops this flow by raising an error.
raise InterpreterError("Runtime Error: Division by zero", "")
# AUTO: Defines function `_parse_literal`.
def _parse_literal(self, value):
# AUTO: Checks this condition.
if isinstance(value, str):
# AUTO: Sets `var_info`.
var_info = self.lookup_variable(value)
# AUTO: Checks this condition.
if var_info is not None and not isinstance(var_info, str):
# AUTO: Returns this result to the caller.
return var_info["value"]
# AUTO: Checks this condition.
if isinstance(value, (int, float, bool)):
# AUTO: Returns this result to the caller.
return value
# AUTO: Checks this condition.
if not isinstance(value, str):
# AUTO: Returns this result to the caller.
return value
# AUTO: Sets `value`.
value = value.strip()
# AUTO: Checks this condition.
if value.startswith('"') and value.endswith('"'):
# AUTO: Returns this result to the caller.
return value[1:-1]
# AUTO: Checks this condition.
if value.startswith("'") and value.endswith("'"):
# AUTO: Returns this result to the caller.
return value[1:-1]
# AUTO: Checks this condition.
if value in ('true', 'sunshine'):
# AUTO: Returns this result to the caller.
return True
# AUTO: Checks this condition.
if value in ('false', 'frost'):
# AUTO: Returns this result to the caller.
return False
# AUTO: Sets `parse_value`.
parse_value = value
# AUTO: Checks this condition.
if parse_value.startswith('~'):
# AUTO: Sets `parse_value`.
parse_value = '-' + parse_value[1:]
# AUTO: Starts protected code that can catch errors.
try:
# AUTO: Checks this condition.
if '.' in parse_value:
# AUTO: Returns this result to the caller.
return float(parse_value)
# AUTO: Returns this result to the caller.
return int(parse_value)
# AUTO: Handles the matching error case.
except ValueError:
# AUTO: Returns this result to the caller.
return value
# AUTO: Defines function `eval_function_declaration`.
def eval_function_declaration(self, node):
# GUIDE: Function declarations are registered, not executed immediately.
# The saved body runs later when a FunctionCallNode is interpreted.
# LINE: First child stores function return type.
return_type = node.children[0].value
# LINE: Second child stores the parameter list node.
parameters_node = node.children[1]
# LINE: node.value stores the function name.
func_name = node.value
# LINE: Collect parameters into simple dictionaries.
params = []
# AUTO: Checks this condition.
if parameters_node and len(parameters_node.children) > 0:
# AUTO: Starts a loop over these values.
for param in parameters_node.children:
# LINE: Parameter nodes must have the expected AST shape.
if not hasattr(param, 'node_type') or param.node_type != 'Parameter':
# AUTO: Stops this flow by raising an error.
raise Exception(f"Invalid parameter: {param.value}")
# LINE: First parameter child is type.
param_type = param.children[0].value
# LINE: Second parameter child is name.
param_name = param.children[1].value
# LINE: Detect array/list parameter marker.
is_list = any(child.node_type == "ArrayParam" for child in param.children)
# LINE: Save this parameter metadata.
params.append({"name": param_name, "type": param_type, "is_list": is_list})
# LINE: Register the function in self.functions for later calls.
self.declare_function(func_name, return_type, params, node)
# LINE: Declaration itself produces no runtime value.
return None
# AUTO: Defines function `eval_block`.
def eval_block(self, block_node):
# GUIDE: Execute statements in order. reclaim/prune/skip can interrupt this
# normal sequence through ReturnValue or loop flags.
# LINE: Run each statement inside the block from top to bottom.
for statement in block_node.children:
# LINE: Dispatch statement to the correct eval_* method.
self.interpret(statement)
# LINE: Stop this block if prune was triggered.
if self.break_triggered():
# AUTO: Returns this result to the caller.
return
# LINE: Stop this block if skip was triggered.
if self.continue_flag:
# AUTO: Returns this result to the caller.
return
# AUTO: Defines function `plant`.
def plant(self, value):
# AUTO: Sends an event/message to the frontend.
self.socketio.emit('output', {'output': str(value)})
# AUTO: Defines function `plant_out`.
def plant_out(self, num):
# AUTO: Sends an event/message to the frontend.
self.socketio.emit('output', {'output': str(num)})
# AUTO: Appends a value to a list.
self.output.append(str(num))
# AUTO: Defines function `eval_print`.
def eval_print(self, node):
# GUIDE: plant() evaluates args, applies optional {} formatting, and
# emits the final text to the UI terminal.
# LINE: plant() with no arguments prints nothing.
if not node.children:
# AUTO: Returns this result to the caller.
return
# LINE: First plant argument can be normal text or a format string.
first = node.children[0]
# LINE: Evaluate the first argument.
evaluated_first = self.interpret(first)
# AUTO: Checks this condition.
if isinstance(evaluated_first, float):
# LINE: Limit displayed float decimals to 5 digits.
whole, dot, dec = str(evaluated_first).partition('.')
# AUTO: Sets `dec`.
dec = dec[:5]
# AUTO: Sets `evaluated_first`.
evaluated_first = float(f"{whole}.{dec}")
# LINE: If first string has {}, use Python format with remaining args.
if isinstance(evaluated_first, str) and '{}' in evaluated_first:
# AUTO: Sets `values`.
values = []
# AUTO: Starts a loop over these values.
for arg in node.children[1:]:
# LINE: Evaluate each format value.
value = self.interpret(arg)
# AUTO: Checks this condition.
if isinstance(value, str) and not isinstance(self.lookup_variable(value), str):
# AUTO: Sets `value`.
value = self.lookup_variable(value)["value"] # type: ignore[index]
# AUTO: Checks this condition.
if isinstance(value, float):
# AUTO: Sets `whole, dot, dec`.
whole, dot, dec = str(value).partition('.')
# AUTO: Sets `dec`.
dec = dec[:5]
# AUTO: Sets `value`.
value = float(f"{whole}.{dec}")
# AUTO: Appends a value to a list.
values.append(value)
# AUTO: Starts protected code that can catch errors.
try:
# LINE: Replace {} placeholders with evaluated values.
output_str = evaluated_first.format(*values)
# AUTO: Handles the matching error case.
except Exception as e:
# AUTO: Stops this flow by raising an error.
raise Exception(f"Format error in plant(): '{evaluated_first}' with {values}: {e}")
# LINE: Send formatted output to UI.
self.plant(output_str)
# AUTO: Returns this result to the caller.
return
# LINE: Multiple plant args without {} are joined with spaces.
if len(node.children) > 1:
# AUTO: Sets `parts`.
parts = [str(evaluated_first)]
# AUTO: Starts a loop over these values.
for arg in node.children[1:]:
# LINE: Evaluate each extra plant argument.
value = self.interpret(arg)
# AUTO: Checks this condition.
if isinstance(value, float):
# AUTO: Sets `whole, dot, dec`.
whole, dot, dec = str(value).partition('.')
# AUTO: Sets `dec`.
dec = dec[:5]
# AUTO: Sets `value`.
value = float(f"{whole}.{dec}")
# AUTO: Appends a value to a list.
parts.append(str(value))
# LINE: Output the combined text.
self.plant(" ".join(parts))
# AUTO: Returns this result to the caller.
return
# LINE: Single plant argument output path.
self.plant(str(evaluated_first))
# AUTO: Defines function `eval_formatted_string`.
def eval_formatted_string(self, node):
# AUTO: Sets `value`.
value = node.value
# AUTO: Checks this condition.
if value.startswith('"') and value.endswith('"'):
# AUTO: Sets `value`.
value = value[1:-1]
# AUTO: Sets `value`.
value = value.replace(r'\\', '\\')
# AUTO: Sets `value`.
value = value.replace(r'\n', '\n')
# AUTO: Sets `value`.
value = value.replace(r'\t', '\t')
# AUTO: Sets `value`.
value = value.replace(r'\"', '"')
# AUTO: Sets `value`.
value = value.replace(r'\{', '{')
# AUTO: Sets `value`.
value = value.replace(r'\}', '}')
# AUTO: Sets `value`.
value = value.replace(r'\/', '/')
# AUTO: Returns this result to the caller.
return value
# AUTO: Defines function `eval_list`.
def eval_list(self, node):
# AUTO: Sets `result`.
result = []
# AUTO: Starts a loop over these values.
for child in node.children:
# AUTO: Checks this condition.
if isinstance(child, ListNode):
# AUTO: Appends a value to a list.
result.append(self.eval_list(child))
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Appends a value to a list.
result.append(self.interpret(child))
# AUTO: Returns this result to the caller.
return result
# AUTO: Defines function `eval_list_access`.
def eval_list_access(self, node):
# AUTO: Sets `name_or_node`.
name_or_node = node.children[0].value
# AUTO: Checks this condition.
if hasattr(name_or_node, 'node_type') and name_or_node.node_type == "ListAccess":
# AUTO: Sets `list_value`.
list_value = self.eval_list_access(name_or_node)
# AUTO: Sets `display_name`.
display_name = "nested list"
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Sets `list_name`.
list_name = name_or_node
# AUTO: Sets `list_entry`.
list_entry = self.lookup_variable(list_name)
# AUTO: Sets `list_value`.
list_value = list_entry["value"] # type: ignore
# AUTO: Sets `display_name`.
display_name = list_name
# AUTO: Sets `index_node`.
index_node = node.children[1]
# AUTO: Sets `index`.
index = self.interpret(index_node.children[0])
# AUTO: Checks this condition.
if not isinstance(index, int):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: List index must be an integer. Got '{index}'", node.line)
# AUTO: Checks this condition.
if not isinstance(list_value, (list, str)):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Cannot index into a non-list value.", node.line)
# AUTO: Checks this condition.
if index < 0 or index >= len(list_value):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Index '{index}' out of bounds for '{display_name}'.", node.line)
# AUTO: Returns this result to the caller.
return list_value[index]
# AUTO: Defines function `eval_return`.
def eval_return(self, node):
# GUIDE: reclaim jumps out of the current function by raising ReturnValue.
# LINE: Evaluate reclaim value if present; root usually has none.
value = self.interpret(node.children[0]) if node.children else None
# LINE: Raise ReturnValue so nested blocks immediately exit the function.
raise ReturnValue(value)
# AUTO: Defines function `eval_function_call`.
def eval_function_call(self, node):
# GUIDE: Function call flow; evaluate args, enter scope, bind params,
# run the saved body, then leave the scope.
# LINE: node.value is the function name being called.
function_name = node.value
# Evaluate all actual arguments before entering the called function.
# Example: gcd(a, b) becomes [value_of_a, value_of_b].
# LINE: Evaluate every argument expression before binding parameters.
args = [self.interpret(arg.children[0]) for arg in node.children]
# Look up the function saved earlier by eval_function_declaration().
# LINE: Fetch function metadata from self.functions.
func_info = self.lookup_function(function_name)
# AUTO: Checks this condition.
if isinstance(func_info, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(func_info, node.line)
# LINE: Expected parameter list saved during declaration.
expected_params = func_info["params"]
# LINE: FunctionDeclarationNode containing the function body.
function_node = func_info["node"]
# LINE: Argument count must match parameter count.
if len(expected_params) != len(args):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(
# AUTO: Executes this statement.
f"Runtime Error: Function '{function_name}' expects {len(expected_params)} argument(s), got {len(args)}.",
# AUTO: Executes this statement.
node.line
# AUTO: Closes the current grouped code/data.
)
# LINE: Enter a new local function scope.
self.enter_scope()
# AUTO: Starts protected code that can catch errors.
try:
# LINE: Bind each argument value to its parameter variable.
for i, param in enumerate(expected_params):
# Bind each argument value to its parameter name in the new
# function scope. Example: parameter "a" receives 48.
# AUTO: Sets `param_name`.
param_name = param["name"]
# AUTO: Sets `param_type`.
param_type = param["type"]
# AUTO: Sets `arg_value`.
arg_value = args[i]
# AUTO: Sets `is_list`.
is_list = param.get("is_list", False)
# LINE: Parameters are stored like local variables.
self.declare_variable(param_name, param_type, arg_value, is_list=is_list)
# AUTO: Starts protected code that can catch errors.
try:
# Execute the function body block. If reclaim runs inside,
# eval_return raises ReturnValue and jumps to the except below.
# LINE: Run the saved function body.
self.eval_block(function_node.children[2])
# AUTO: Handles the matching error case.
except ReturnValue as ret:
# The reclaim value becomes the function call result.
# LINE: Return reclaim's value to the caller.
return ret.value
# LINE: If no reclaim value happened, function returns None.
return None
# AUTO: Runs cleanup code no matter what happened.
finally:
# LINE: Always leave the function scope even if an error/reclaim happens.
self.exit_scope()
# LINE: Clear active function marker.
self.current_func_name = None
# AUTO: Defines function `eval_append`.
def eval_append(self, node):
# AUTO: Sets `list_name`.
list_name = node.parent.children[0].value
# AUTO: Sets `list_info`.
list_info = self.lookup_variable(list_name)
# AUTO: Starts a loop over these values.
for child in node.children:
# AUTO: Sets `value`.
value = self.interpret(child)
# AUTO: Appends a value to a list.
list_info["value"].append(value) # type: ignore
# AUTO: Defines function `eval_insert`.
def eval_insert(self, node):
# AUTO: Sets `list_name`.
list_name = node.parent.children[0].value
# AUTO: Sets `list_info`.
list_info = self.lookup_variable(list_name)
# AUTO: Sets `index`.
index = self.interpret(node.children[0].children[0])
# AUTO: Checks this condition.
if not isinstance(index, int):
# AUTO: Stops this flow by raising an error.
raise InterpreterError("Runtime Error: Insert index must be an integer", node.line)
# AUTO: Checks this condition.
if index < 0 or index > len(list_info["value"]): # type: ignore
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Index {index} out of range for insert", node.line)
# AUTO: Starts a loop over these values.
for child in node.children[1:]:
# AUTO: Sets `value`.
value = self.interpret(child)
# AUTO: Executes this statement.
list_info["value"].insert(index, value) # type: ignore
# AUTO: Adds into `index`.
index += 1
# AUTO: Defines function `eval_remove`.
def eval_remove(self, node):
# AUTO: Sets `list_name`.
list_name = node.children[0].value
# AUTO: Sets `index_node`.
index_node = node.children[1].children[0]
# AUTO: Sets `list_info`.
list_info = self.lookup_variable(list_name)
# AUTO: Checks this condition.
if isinstance(list_info, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(list_info, node.line)
# AUTO: Sets `index`.
index = self.interpret(index_node)
# AUTO: Checks this condition.
if not isinstance(index, int):
# AUTO: Stops this flow by raising an error.
raise InterpreterError("Runtime Error: Remove index must be an integer", node.line)
# AUTO: Checks this condition.
if index < 0 or index >= len(list_info["value"]):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Index {index} out of bounds for remove", node.line)
# AUTO: Sets `removed`.
removed = list_info["value"].pop(index)
# AUTO: Defines function `eval_unaryop`.
def eval_unaryop(self, node):
# LINE: Member increment/decrement path, like student.age++.
if isinstance(node.children[0], MemberAccessNode) and node.value in {"++", "--"}:
# LINE: Target is the member access node.
target = node.children[0]
# LINE: chain stores member names from nested access.
chain = []
# LINE: Start walking from the target access.
current = target
# LINE: Collect all member names until base object.
while isinstance(current, MemberAccessNode):
# AUTO: Appends a value to a list.
chain.append(current.children[1].value)
# AUTO: Sets `current`.
current = current.children[0]
# LINE: Reverse so access starts from base object outward.
chain.reverse()
# LINE: Base object variable name.
obj_name = current.value
# LINE: Look up base object variable.
var_info = self.lookup_variable(obj_name)
# AUTO: Checks this condition.
if isinstance(var_info, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(var_info, node.line)
# LINE: Get bundle dictionary value.
bundle_value = var_info["value"]
# LINE: Member increment requires bundle object.
if not isinstance(bundle_value, dict):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Variable '{obj_name}' is not a bundle.", node.line)
# LINE: Navigate nested bundle path before final member.
for member in chain[:-1]:
# AUTO: Checks this condition.
if member not in bundle_value:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Bundle has no member '{member}'.", node.line)
# AUTO: Sets `bundle_value`.
bundle_value = bundle_value[member]
# AUTO: Checks this condition.
if not isinstance(bundle_value, dict):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Member '{member}' is not a bundle.", node.line)
# LINE: Final member is incremented/decremented.
final_member = chain[-1]
# AUTO: Checks this condition.
if final_member not in bundle_value:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Bundle has no member '{final_member}'.", node.line)
# LINE: Save old value for postfix result.
original = bundle_value[final_member]
# LINE: Compute new value depending on ++ or --.
new_value = original + 1 if node.value == "++" else original - 1
# LINE: Store updated value in bundle member.
bundle_value[final_member] = new_value
# LINE: Postfix returns old value, prefix returns new value.
return original if node.position == "post" else new_value
# LINE: Simple variable unary path, not array/list access.
if not isinstance(node.children[0], ListAccessNode):
# LINE: Operand node stores the variable/literal being changed.
operand_node = node.children[0]
# LINE: For ++/-- this is the variable name.
operand_name = operand_node.value
# LINE: Look up variable info dictionary.
var_info = self.lookup_variable(operand_name)
# LINE: Increment variable path.
if node.value == "++":
# AUTO: Checks this condition.
if isinstance(var_info, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(var_info, node.line)
# LINE: Prefix ++ updates first then returns new value.
if node.position == "pre":
# AUTO: Adds into `var_info["value"]`.
var_info["value"] += 1
# AUTO: Returns this result to the caller.
return var_info["value"]
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Postfix ++ returns old value then updates.
original = var_info["value"]
# AUTO: Adds into `var_info["value"]`.
var_info["value"] += 1
# AUTO: Returns this result to the caller.
return original
# LINE: Decrement variable path.
elif node.value == "--":
# AUTO: Checks this condition.
if isinstance(var_info, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(var_info, node.line)
# LINE: Prefix -- updates first then returns new value.
if node.position == "pre":
# AUTO: Subtracts from `var_info["value"]`.
var_info["value"] -= 1
# AUTO: Returns this result to the caller.
return var_info["value"]
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Postfix -- returns old value then updates.
original = var_info["value"]
# AUTO: Subtracts from `var_info["value"]`.
var_info["value"] -= 1
# AUTO: Returns this result to the caller.
return original
# LINE: Minus operator path.
elif node.value == "-":
# LINE: Evaluate operand then negate it.
value = self.interpret(operand_node)
# AUTO: Returns this result to the caller.
return -value
# LINE: GAL negative operator path.
elif node.value == "~":
# LINE: Evaluate operand then negate it.
value = self.interpret(operand_node)
# AUTO: Returns this result to the caller.
return -value
# LINE: Logical not path.
elif node.value == "!":
# LINE: Evaluate operand then invert boolean truth.
value = self.interpret(operand_node)
# AUTO: Returns this result to the caller.
return not value
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Array/list element ++/-- path, like arr[i]++.
operand_node = node.children[0]
# LINE: Base list variable name.
list_name = operand_node.children[0].value
# LINE: Index node inside brackets.
index_node = operand_node.children[1]
# LINE: Evaluate index expression.
index = self.interpret(index_node.children[0])
# LINE: Look up list variable.
list_entry = self.lookup_variable(list_name)
# AUTO: Checks this condition.
if isinstance(list_entry, str):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(list_entry, node.line)
# LINE: Get actual Python list value.
list_value = list_entry["value"]
# LINE: Index must be integer.
if not isinstance(index, int):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: List index must be an integer. Got '{index}'", node.line)
# LINE: Target variable must be a list.
if not isinstance(list_value, list):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Variable '{list_name}' is not a list.", node.line)
# LINE: Check index bounds.
if index < 0 or index >= len(list_value):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Index '{index}' out of bounds for list '{list_name}'.", node.line)
# LINE: Increment array element.
if node.value == "++":
# AUTO: Sets `original`.
original = list_value[index]
# AUTO: Adds into `list_value[index]`.
list_value[index] += 1
# AUTO: Returns this result to the caller.
return original if node.position == "post" else list_value[index]
# LINE: Decrement array element.
elif node.value == "--":
# AUTO: Sets `original`.
original = list_value[index]
# AUTO: Subtracts from `list_value[index]`.
list_value[index] -= 1
# AUTO: Returns this result to the caller.
return original if node.position == "post" else list_value[index]
# LINE: If no unary branch matched, this operator is unsupported.
raise InterpreterError(f"Unknown unary operator {node.value}", node.line)
# AUTO: Defines function `eval_cast`.
def eval_cast(self, node):
# LINE: Second child is the expression being converted.
value = self.interpret(node.children[1])
# LINE: First child stores target cast type.
cast_type = node.children[0].value
# LINE: Convert value to seed/int.
if cast_type == "seed":
# AUTO: Returns this result to the caller.
return int(value)
# LINE: Convert value to tree/float.
elif cast_type == "tree":
# AUTO: Returns this result to the caller.
return float(value)
# LINE: Convert value to leaf/character.
elif cast_type == "leaf":
# LINE: Integer leaf cast uses character code.
if isinstance(value, int):
# AUTO: Returns this result to the caller.
return chr(value)
# LINE: String leaf cast takes first character or null char.
return str(value)[0] if value else '\0'
# LINE: Convert value to branch/bool.
elif cast_type == "branch":
# AUTO: Returns this result to the caller.
return bool(value)
# LINE: Convert value to vine/string.
elif cast_type == "vine":
# AUTO: Returns this result to the caller.
return str(value)
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Unknown target type is runtime error.
raise InterpreterError(f"Unknown cast type: {cast_type}", node.line)
# AUTO: Defines function `eval_soil`.
def eval_soil(self, node):
# LINE: First child is the variable whose value will be lowercased.
var_name = node.children[0].value
# LINE: Look up the variable entry.
var_info = self.lookup_variable(var_name)
# LINE: Return lowercase version of the stored value.
return var_info["value"].lower() # type: ignore
# AUTO: Defines function `eval_bloom`.
def eval_bloom(self, node):
# LINE: First child is the variable whose value will be uppercased.
var_name = node.children[0].value
# LINE: Look up the variable entry.
var_info = self.lookup_variable(var_name)
# LINE: Return uppercase version of the stored value.
return var_info["value"].upper() # type: ignore
# AUTO: Defines function `eval_if_statement`.
def eval_if_statement(self, node):
# LINE: Evaluate spring condition from first child.
condition_result = self.interpret(node.children[0].children[0])
# LINE: Create local scope for this if/else chain.
self.enter_scope()
# AUTO: Starts protected code that can catch errors.
try:
# LINE: If spring condition is True, run spring block.
if condition_result:
# AUTO: Calls `self.eval_block`.
self.eval_block(node.children[1])
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Start checking children after spring condition/block.
current_node = 2
# LINE: Walk bud/wither nodes until one runs or list ends.
while current_node < len(node.children):
# LINE: Current child can be ElseIfStatement or ElseStatement.
elif_node = node.children[current_node]
# LINE: bud condition path.
if elif_node.node_type == "ElseIfStatement":
# LINE: Evaluate bud condition.
elif_condition_result = self.interpret(elif_node.children[0].children[0])
# LINE: bud condition must be branch/bool.
if not isinstance(elif_condition_result, bool):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Condition must be a boolean. Got '{condition_result}'", node.line)
# LINE: If bud is true, run its block and stop the chain.
if elif_condition_result:
# AUTO: Starts protected code that can catch errors.
try:
# LINE: bud block gets its own local scope.
self.enter_scope()
# LINE: Execute bud block.
self.eval_block(elif_node.children[1])
# AUTO: Runs cleanup code no matter what happened.
finally:
# LINE: Leave bud local scope.
self.exit_scope()
# AUTO: Returns this result to the caller.
return
# LINE: wither/else path.
elif elif_node.node_type == "ElseStatement":
# AUTO: Starts protected code that can catch errors.
try:
# LINE: wither block gets its own local scope.
self.enter_scope()
# LINE: Execute wither block.
self.eval_block(elif_node.children[0])
# AUTO: Runs cleanup code no matter what happened.
finally:
# LINE: Leave wither local scope.
self.exit_scope()
# AUTO: Returns this result to the caller.
return
# LINE: Move to next bud/wither child.
current_node += 1
# AUTO: Runs cleanup code no matter what happened.
finally:
# LINE: Always leave spring chain scope.
self.exit_scope()
# LINE: If no block ran, return no value.
return None
# AUTO: Defines function `eval_for_loop`.
def eval_for_loop(self, node):
# GUIDE: cultivate flow; initialize once, check condition, run block,
# apply update expressions, then repeat.
# LINE: Mark that execution is inside a cultivate loop.
self.enter_loop('for')
# LINE: Create loop-local scope.
self.enter_scope()
# LINE: Safety limit to prevent infinite loops.
MAX_LOOP_ITERATIONS = 10000
# LINE: Counts how many loop iterations already ran.
LOOP_COUNTER = 0
# AUTO: Starts protected code that can catch errors.
try:
# LINE: First child is the initializer part of cultivate.
instantiate_node = node.children[0]
# AUTO: Checks this condition.
if isinstance(instantiate_node, VariableDeclarationNode):
# First part of cultivate: seed i = 0
# AUTO: Sets `var_type`.
var_type = instantiate_node.children[0].value
# AUTO: Sets `var_name`.
var_name = instantiate_node.children[1].value
# AUTO: Sets `initial_value_node`.
initial_value_node = self.interpret(instantiate_node.children[2])
# AUTO: Calls `self.declare_variable`.
self.declare_variable(var_name, var_type, initial_value_node)
# AUTO: Checks the next alternate condition.
elif isinstance(instantiate_node, AssignmentNode):
# First part of cultivate: i = 0
# AUTO: Sets `var_name`.
var_name = instantiate_node.children[0].value
# AUTO: Sets `initial_value_node`.
initial_value_node = self.interpret(instantiate_node.children[1])
# AUTO: Sets `self.lookup_variable(var_name)["value"]`.
self.lookup_variable(var_name)["value"] = initial_value_node # type: ignore
# LINE: Second child is the loop condition.
condition_node = node.children[1].children[0]
# Second part of cultivate: evaluate condition such as i <= n.
# LINE: Evaluate condition before the first iteration.
condition_result = self.interpret(condition_node)
# LINE: Loop condition must evaluate to branch/bool.
if not isinstance(condition_result, bool):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Condition must be a boolean. Got '{condition_result}'", node.line)
# LINE: Keep running while condition is sunshine/True.
while condition_result:
# AUTO: Adds into `LOOP_COUNTER`.
LOOP_COUNTER += 1
# AUTO: Checks this condition.
if LOOP_COUNTER > MAX_LOOP_ITERATIONS:
# AUTO: Stops this flow by raising an error.
raise InterpreterError("Runtime Error: Infinite loop detected!", node.line)
# LINE: Execute the loop body block.
self.eval_block(node.children[3])
# AUTO: Checks this condition.
if self.continue_flag:
# LINE: skip clears here before updates/next condition.
self.continue_flag = False
# AUTO: Checks this condition.
if self.break_triggered():
# LINE: prune stops the loop immediately.
break
# LINE: Run update expressions after each iteration.
for update_expr in node.children[2].children:
# Third part of cultivate: apply update such as i++.
# AUTO: Dispatches an AST node for execution.
self.interpret(update_expr)
# Re-check the loop condition for the next iteration.
# LINE: Re-evaluate condition to decide if loop continues.
condition_result = self.interpret(condition_node)
# AUTO: Runs cleanup code no matter what happened.
finally:
# LINE: Always remove loop scope after loop ends/errors.
self.exit_scope()
# LINE: Always clear loop tracking after loop ends/errors.
self.exit_loop()
# AUTO: Defines function `eval_while_loop`.
def eval_while_loop(self, node):
# GUIDE: grow checks the branch condition before each block execution.
# LINE: Mark that execution is inside a grow loop.
self.enter_loop('while')
# LINE: Create local loop scope.
self.enter_scope()
# LINE: Safety limit to avoid infinite grow loops.
MAX_LOOP_ITERATIONS = 10000
# LINE: Count loop iterations.
LOOP_COUNTER = 0
# LINE: First child stores grow condition expression.
condition_node = node.children[0].children[0]
# AUTO: Starts protected code that can catch errors.
try:
# Evaluate grow(condition) before the first iteration.
# LINE: Evaluate condition before entering body.
condition_result = self.interpret(condition_node)
# LINE: grow condition must be branch/bool.
if not isinstance(condition_result, bool):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Condition must be a boolean. Got '{condition_result}'", node.line)
# LINE: Continue loop while condition is sunshine/True.
while condition_result:
# AUTO: Adds into `LOOP_COUNTER`.
LOOP_COUNTER += 1
# AUTO: Checks this condition.
if LOOP_COUNTER > MAX_LOOP_ITERATIONS:
# AUTO: Stops this flow by raising an error.
raise InterpreterError("Runtime Error: Infinite loop detected!", node.line)
# LINE: Second child is the grow body block.
block_node = node.children[1]
# LINE: Execute grow body once.
self.eval_block(block_node)
# AUTO: Checks this condition.
if self.continue_flag:
# LINE: skip resets before next condition check.
self.continue_flag = False
# AUTO: Checks this condition.
if self.break_triggered():
# LINE: prune exits the grow loop.
break
# Re-evaluate the condition after the block. If false, loop stops.
# LINE: Check condition again after the body.
condition_result = self.interpret(condition_node)
# AUTO: Runs cleanup code no matter what happened.
finally:
# LINE: Clear loop tracking.
self.exit_loop()
# LINE: Remove loop-local scope.
self.exit_scope()
# AUTO: Defines function `eval_do_while_loop`.
def eval_do_while_loop(self, node):
# AUTO: Calls `self.enter_loop`.
self.enter_loop('do-while')
# AUTO: Sets `MAX_LOOP_ITERATIONS`.
MAX_LOOP_ITERATIONS = 10000
# AUTO: Sets `LOOP_COUNTER`.
LOOP_COUNTER = 0
# AUTO: Sets `condition_node`.
condition_node = node.children[1].children[0]
# AUTO: Sets `block_node`.
block_node = node.children[0]
# AUTO: Starts protected code that can catch errors.
try:
# AUTO: Repeats while this condition is true.
while True:
# AUTO: Calls `self.eval_block`.
self.eval_block(block_node)
# AUTO: Adds into `LOOP_COUNTER`.
LOOP_COUNTER += 1
# AUTO: Checks this condition.
if LOOP_COUNTER > MAX_LOOP_ITERATIONS:
# AUTO: Stops this flow by raising an error.
raise InterpreterError("Runtime Error: Infinite loop detected!", node.line)
# AUTO: Checks this condition.
if self.continue_flag:
# AUTO: Sets `self.continue_flag`.
self.continue_flag = False
# AUTO: Checks this condition.
if self.break_triggered():
# AUTO: Stops the nearest loop.
break
# AUTO: Sets `condition_result`.
condition_result = self.interpret(condition_node)
# AUTO: Checks this condition.
if not isinstance(condition_result, bool):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Condition must be a boolean. Got '{condition_result}'", node.line)
# AUTO: Checks this condition.
if not condition_result:
# AUTO: Stops the nearest loop.
break
# AUTO: Runs cleanup code no matter what happened.
finally:
# AUTO: Calls `self.exit_loop`.
self.exit_loop()
# AUTO: Defines function `eval_break`.
def eval_break(self, node):
# LINE: prune is legal only when loop_stack is not empty.
if self.loop_stack:
# LINE: Set break flag so the loop can stop.
self.trigger_break()
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Using prune outside loop/switch is a runtime error.
raise InterpreterError("Runtime Error: Break statement used outside of a loop", node.line)
# AUTO: Defines function `trigger_break`.
def trigger_break(self):
# LINE: Mark that current loop should stop.
self.break_flag = True
# AUTO: Defines function `break_triggered`.
def break_triggered(self):
# LINE: Return whether prune was triggered.
return self.break_flag
# AUTO: Defines function `enter_loop`.
def enter_loop(self, loop_type):
# LINE: Push loop type so prune/skip know we are inside a loop/switch.
self.loop_stack.append(loop_type)
# LINE: Reset prune flag for new loop.
self.break_flag = False
# LINE: Reset skip flag for new loop.
self.continue_flag = False
# AUTO: Defines function `exit_loop`.
def exit_loop(self):
# LINE: Only pop when a loop/switch context exists.
if self.loop_stack:
# LINE: Remove current loop/switch context.
self.loop_stack.pop()
# LINE: Clear prune after leaving loop.
self.break_flag = False
# LINE: Clear skip after leaving loop.
self.continue_flag = False
# AUTO: Defines function `eval_continue`.
def eval_continue(self, node):
# LINE: skip is legal only inside a loop.
if self.loop_stack:
# LINE: Set skip flag.
self.trigger_continue()
# AUTO: Runs when previous condition did not pass.
else:
# LINE: skip outside loop is runtime error.
raise InterpreterError("Runtime Error: Continue statement used outside of a loop", node.line)
# AUTO: Defines function `continue_triggered`.
def continue_triggered(self):
# LINE: Return whether skip was triggered.
return self.continue_flag
# AUTO: Defines function `trigger_continue`.
def trigger_continue(self):
# LINE: Mark that current loop should skip to next iteration.
self.continue_flag = True
# AUTO: Defines function `eval_switch`.
def eval_switch(self, node):
# LINE: harvest behaves like a switch context for prune.
self.enter_loop('switch')
# LINE: Create local scope for switch execution.
self.enter_scope()
# LINE: First child is the switch expression.
switch_expr_node = node.children[0]
# LINE: Evaluate switch expression once.
switch_value = self.interpret(switch_expr_node)
# LINE: Tracks if a matching variety case has been found.
matched_case = False
# LINE: Tracks if prune stopped case execution.
break_found = False
# LINE: Stores soil/default block if present.
default_case = None
# AUTO: Starts protected code that can catch errors.
try:
# LINE: Visit every variety/soil child after switch expression.
for case_node in node.children[1:]:
# LINE: Case node type tells variety or soil/default.
label_type = case_node.node_type
# LINE: variety case path.
if label_type == "Case":
# LINE: First case child is case literal/expression.
case_value_node = case_node.children[0]
# LINE: Second case child is block to run.
block_node = case_node.children[1]
# LINE: Evaluate case value for comparison.
case_value = self.interpret(case_value_node)
# LINE: Run this block if value matches or fall-through already started.
if switch_value == case_value or matched_case:
# LINE: Mark that switch found a matching case.
matched_case = True
# AUTO: Starts protected code that can catch errors.
try:
# LINE: Each case block gets its own local scope.
self.enter_scope()
# LINE: Execute case statements.
self.eval_block(block_node)
# LINE: Stop switch if prune was triggered.
if self.break_triggered():
# AUTO: Stops the nearest loop.
break_found = True
# AUTO: Stops the nearest loop.
break
# AUTO: Runs cleanup code no matter what happened.
finally:
# LINE: Leave case scope.
self.exit_scope()
# LINE: soil/default case path.
elif label_type == "Default":
# LINE: Save default block to run later if no case matched.
default_case = case_node.children[0]
# LINE: Run soil/default only if no variety matched and no prune happened.
if not matched_case and not break_found and default_case:
# AUTO: Starts protected code that can catch errors.
try:
# LINE: Default block gets its own local scope.
self.enter_scope()
# LINE: Execute soil/default statements.
self.eval_block(default_case)
# AUTO: Runs cleanup code no matter what happened.
finally:
# LINE: Leave default scope.
self.exit_scope()
# AUTO: Runs cleanup code no matter what happened.
finally:
# LINE: Leave switch/prune context.
self.exit_loop()
# LINE: Leave switch local scope.
self.exit_scope()
# AUTO: Defines function `emit_input_request`.
def emit_input_request(self, var_name, prompt):
# LINE: Tell frontend to show input prompt for water().
self.socketio.emit('input_required', {'prompt': prompt, 'variable': var_name})
# AUTO: Defines function `provide_input`.
def provide_input(self, var_name, input_value):
# LINE: Get waiting event for this water() variable.
evt = self.input_events.get(var_name)
# LINE: If interpreter is not waiting yet, store input for later.
if evt is None:
# AUTO: Sets `self.input_values[var_name]`.
self.input_values[var_name] = input_value
# AUTO: Returns this result to the caller.
return
# LINE: Eventlet mode resumes the waiting green thread.
if _USE_EVENTLET:
# AUTO: Calls `evt.send`.
evt.send(input_value)
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Threading mode stores value and releases wait().
self.input_values[var_name] = input_value
# AUTO: Calls `evt.set`.
evt.set()
# AUTO: Defines function `wait_for_input`.
def wait_for_input(self, var_name):
# LINE: If input arrived early, consume it immediately.
if var_name in self.input_values:
# AUTO: Returns this result to the caller.
return self.input_values.pop(var_name)
# LINE: Eventlet waiting path used by Socket.IO server.
if _USE_EVENTLET:
# LINE: Create event object that pauses execution.
evt = _ev.Event()
# LINE: Store event so provide_input can resume it.
self.input_events[var_name] = evt
# LINE: Pause here until frontend sends input.
value = evt.wait()
# LINE: Remove event after input arrives.
self.input_events.pop(var_name, None)
# LINE: Stop if execution was cancelled while waiting.
if getattr(self, '_cancelled', False):
# AUTO: Stops this flow by raising an error.
raise _CancelledError()
# LINE: Return received input value.
return value
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Standard threading waiting path.
event = threading.Event()
# LINE: Store event so provide_input can set it.
self.input_events[var_name] = event
# LINE: Pause here until event.set().
event.wait()
# LINE: Stop if execution was cancelled while waiting.
if getattr(self, '_cancelled', False):
# AUTO: Stops this flow by raising an error.
raise _CancelledError()
# LINE: Read input value sent by frontend.
value = self.input_values.pop(var_name, None)
# LINE: Remove finished event.
self.input_events.pop(var_name, None)
# LINE: Return received input value.
return value
# AUTO: Defines function `eval_input`.
def eval_input(self, node):
# GUIDE: water() finds target variable/type from parent node, asks the
# UI for a value, then converts that value before assignment.
# LINE: Parent tells whether water() is declaration, assignment, or expression.
parent_node = node.parent
# LINE: Case seed n = water(seed);
if isinstance(parent_node, VariableDeclarationNode):
# Case: seed n = water(seed);
# AUTO: Sets `var_name`.
var_name = parent_node.children[1].value
# AUTO: Sets `var_type`.
var_type = parent_node.children[0].value
# LINE: Case water(n); or n = water(seed);
elif isinstance(parent_node, AssignmentNode):
# Case: water(n); or n = water(seed);
# AUTO: Sets `target`.
target = parent_node.children[0]
# LINE: Array input target path like arr[i].
if isinstance(target, ListAccessNode):
# AUTO: Sets `current`.
current = target
# AUTO: Repeats while this condition is true.
while hasattr(current, 'node_type') and current.node_type == "ListAccess":
# AUTO: Sets `current`.
current = current.children[0].value
# AUTO: Sets `var_name`.
var_name = current if isinstance(current, str) else str(current)
# AUTO: Sets `var_type`.
var_type = self.lookup_variable(var_name)["type"] # type: ignore
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Simple variable input target path.
var_name = target.value
# AUTO: Sets `var_type`.
var_type = self.lookup_variable(var_name)["type"] # type: ignore
# AUTO: Runs when previous condition did not pass.
else:
# Case: water(seed) used directly as an expression.
# LINE: Expression water() has no variable target, so use temporary name.
var_name = "_input"
# AUTO: Checks this condition.
if node.value and "(" in node.value:
# LINE: Extract requested input type from water(seed/tree/etc.).
inner = node.value.split("(")[1].rstrip(")")
# AUTO: Sets `var_type`.
var_type = inner if inner in {"seed", "tree", "leaf", "branch", "vine"} else "vine"
# AUTO: Runs when previous condition did not pass.
else:
# LINE: Plain water() defaults to vine/string input.
var_type = "vine"
# LINE: Prompt text sent to UI.
prompt = f"Input for {var_name}: "
# LINE: Mark interpreter as waiting for input.
self.input_required = True
# Ask the UI/browser for input and wait until capture_input sends it.
# LINE: Send input_required event to frontend.
self.emit_input_request(var_name, prompt)
# LINE: Pause execution until frontend sends input.
input_value = self.wait_for_input(var_name)
# LINE: Mark input wait as finished.
self.input_required = False
# LINE: Convert user text into seed integer when needed.
if var_type == "seed":
# AUTO: Sets `original_input`.
original_input = input_value
# AUTO: Checks this condition.
if isinstance(input_value, str) and input_value.startswith('-'):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: GAL uses '~' for negative numbers, not '-'. Got '{original_input}'; did you mean '~{original_input[1:]}'?", node.line) # type: ignore
# AUTO: Checks this condition.
if isinstance(input_value, str) and input_value.startswith('~'):
# AUTO: Sets `input_value`.
input_value = '-' + input_value[1:]
# AUTO: Starts protected code that can catch errors.
try:
# AUTO: Checks this condition.
if len(input_value.strip('-').lstrip('0')) > 16:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Input value exceeds maximum number of 16 digits", node.line)
# AUTO: Sets `input_value`.
input_value = int(float(input_value)) # type: ignore
# AUTO: Handles the matching error case.
except ValueError:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Expected integer value, got '{original_input}'", node.line)
# AUTO: Checks the next alternate condition.
elif var_type == "tree":
# AUTO: Sets `original_input`.
original_input = input_value
# AUTO: Checks this condition.
if isinstance(input_value, str) and input_value.startswith('-'):
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: GAL uses '~' for negative numbers, not '-'. Got '{original_input}'; did you mean '~{original_input[1:]}'?", node.line) # type: ignore
# AUTO: Checks this condition.
if isinstance(input_value, str) and input_value.startswith('~'):
# AUTO: Sets `input_value`.
input_value = '-' + input_value[1:]
# AUTO: Starts protected code that can catch errors.
try:
# AUTO: Checks this condition.
if '.' in input_value: # type: ignore
# AUTO: Sets `integer_part, decimal_part`.
integer_part, decimal_part = str(input_value).split('.')
# AUTO: Checks this condition.
if len(integer_part.strip('-').lstrip('0')) > 16:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Input value exceeds maximum number of 16 digits", node.line)
# AUTO: Checks this condition.
if len(decimal_part.rstrip('0')) > 5:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Input value exceeds maximum number of 5 decimal numbers", node.line)
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Checks this condition.
if len(input_value.strip('-').lstrip('0')) > 16:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Input value exceeds maximum number of 16 digits", node.line)
# AUTO: Sets `input_value`.
input_value = float(input_value) # type: ignore
# AUTO: Handles the matching error case.
except ValueError:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Expected float value, got '{original_input}'", node.line)
# AUTO: Checks the next alternate condition.
elif var_type == "branch":
# AUTO: Checks this condition.
if input_value == "true" or input_value == "false":
# AUTO: Executes this statement.
suggestion = "sunshine" if input_value == "true" else "frost"
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: GAL uses 'sunshine' and 'frost' for booleans, not 'true'/'false'. Got '{input_value}'; did you mean '{suggestion}'?", node.line)
# AUTO: Checks this condition.
if input_value == "sunshine":
# AUTO: Sets `input_value`.
input_value = True
# AUTO: Checks the next alternate condition.
elif input_value == "frost":
# AUTO: Sets `input_value`.
input_value = False
# AUTO: Runs when previous condition did not pass.
else:
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: expected branch value (sunshine/frost), got '{input_value}'", node.line)
# AUTO: Checks the next alternate condition.
elif var_type == "leaf":
# AUTO: Checks this condition.
if len(input_value) != 1: # type: ignore
# AUTO: Stops this flow by raising an error.
raise InterpreterError(f"Runtime Error: Expected a single character for leaf, got '{input_value}'", node.line)
# AUTO: Sets `input_value`.
input_value = str(input_value)
# AUTO: Checks the next alternate condition.
elif var_type == "vine":
# AUTO: Sets `input_value`.
input_value = str(input_value)
# AUTO: Returns this result to the caller.
return input_value