@@ -122,27 +122,35 @@ def _consume_binary_digits(self) -> str:
122122 return "" .join (digits )
123123 def _consume_identifier (self ) -> Token :
124124 line , col = self .line , self .column
125+ # Defensive check: identifiers must not start with the digits '0' or '1'.
126+ # Normally this is enforced by _is_identifier_start, but check here
127+ # so that the lexer produces a clear parse error if invoked incorrectly.
128+ if not self ._eof and self ._peek () in "01" :
129+ raise ASMParseError (
130+ f"Identifiers must not start with '0' or '1' at { self .filename } :{ line } :{ col } "
131+ )
125132 chars : List [str ] = []
126133 while (not self ._eof and self ._is_identifier_part (self ._peek ())):
127134 chars .append (self ._peek ())
128135 self ._advance ()
129136 value = "" .join (chars )
130- if (any (ch in "01" for ch in value )):
131- raise ASMParseError (f"Identifiers may not contain '0' or '1' at { line } :{ col } ({ value } )" )
132- token_type :str = value if value in KEYWORDS else "IDENT"
133- return (Token (token_type ,value ,line ,col ))
137+ token_type : str = value if value in KEYWORDS else "IDENT"
138+ return Token (token_type , value , line , col )
134139 def _is_identifier_start (self , ch : str ) -> bool :
135140 # Only allow ASCII letters and underscore as the start of an identifier.
136141 # This enforces the spec requirement that identifiers must be ASCII-only.
137142 return (ch == "_" ) or ("A" <= ch <= "Z" ) or ("a" <= ch <= "z" )
138143 def _is_identifier_part (self , ch : str ) -> bool :
139- # Allow ASCII letters, underscore, and digits 2-9 (digits '0' and '1' are disallowed
140- # per the language spec because they would collide with binary-literal syntax).
144+ # Allow ASCII letters, underscore, and digits (including '0' and '1').
145+ # The first character of an identifier must not be '0' or '1'
146+ # (this is enforced by _is_identifier_start and by a defensive
147+ # check in _consume_identifier). Characters '0' and '1' are permitted
148+ # in subsequent positions inside an identifier.
141149 return (
142150 (ch == "_" )
143151 or ("A" <= ch <= "Z" )
144152 or ("a" <= ch <= "z" )
145- or ("2 " <= ch <= "9" )
153+ or ("0 " <= ch <= "9" )
146154 )
147155 @property
148156 def _eof (self ) -> bool :
@@ -368,6 +376,15 @@ def __init__(self, parent: Optional["Environment"] = None) -> None:
368376 self .parent = parent
369377 self .values : Dict [str , int ] = {}
370378 def set (self , name : str , value : int ) -> None :
379+ # Assign to the nearest environment that already contains the name.
380+ # If the name does not exist in any parent, create it in this env.
381+ env : Optional [Environment ] = self
382+ while env is not None :
383+ if name in env .values :
384+ env .values [name ] = value
385+ return
386+ env = env .parent
387+ # Not found in parents: bind in the current environment
371388 self .values [name ] = value
372389 def get (self , name : str ) -> int :
373390 if name in self .values :
@@ -377,10 +394,21 @@ def get(self, name: str) -> int:
377394 raise ASMRuntimeError (f"Undefined identifier '{ name } '" , rewrite_rule = "IDENT" )
378395
379396 def delete (self , name : str ) -> None :
380- if name in self .values :
381- del self .values [name ]
382- return
397+ # Delete the identifier from the nearest environment that defines it.
398+ env : Optional [Environment ] = self
399+ while env is not None :
400+ if name in env .values :
401+ del env .values [name ]
402+ return
403+ env = env .parent
383404 raise ASMRuntimeError (f"Cannot delete undefined identifier '{ name } '" , rewrite_rule = "DEL" )
405+ def has (self , name : str ) -> bool :
406+ """Return True if 'name' exists in this environment or any parent."""
407+ if name in self .values :
408+ return True
409+ if self .parent is not None :
410+ return self .parent .has (name )
411+ return False
384412 def snapshot (self ) -> Dict [str , int ]:
385413 return dict (self .values )
386414@dataclass
@@ -464,7 +492,7 @@ def __init__(self) -> None:
464492 self ._register_fixed ("SUB" , 2 , lambda a , b : a - b )
465493 self ._register_fixed ("MUL" , 2 , lambda a , b : a * b )
466494 self ._register_fixed ("DIV" , 2 , self ._safe_div )
467- self ._register_fixed ("CEIL " , 2 , self ._safe_ceil )
495+ self ._register_fixed ("CDIV " , 2 , self ._safe_cdiv )
468496 self ._register_fixed ("MOD" , 2 , self ._safe_mod )
469497 self ._register_fixed ("POW" , 2 , self ._safe_pow )
470498 self ._register_fixed ("NEG" , 1 , lambda a : - a )
@@ -496,6 +524,7 @@ def __init__(self) -> None:
496524 self ._register_variadic ("ANY" , 1 , lambda vals : 1 if any (_as_bool (v ) for v in vals ) else 0 )
497525 self ._register_variadic ("ALL" , 1 , lambda vals : 1 if all (_as_bool (v ) for v in vals ) else 0 )
498526 self ._register_variadic ("LEN" , 0 , lambda vals : len (vals ))
527+ self ._register_variadic ("JOIN" , 1 , self ._join )
499528 self ._register_fixed ("LOG" , 1 , self ._safe_log )
500529 self ._register_fixed ("CLOG" , 1 , self ._safe_clog )
501530 self ._register_custom ("MAIN" , 0 , 0 , self ._main )
@@ -504,6 +533,7 @@ def __init__(self) -> None:
504533 self ._register_custom ("PRINT" , 0 , None , self ._print )
505534 self ._register_custom ("ASSERT" , 1 , 1 , self ._assert )
506535 self ._register_custom ("DEL" , 1 , 1 , self ._delete )
536+ self ._register_custom ("EXIST" , 1 , 1 , self ._exist )
507537 self ._register_custom ("EXIT" , 0 , 1 , self ._exit )
508538 def _register_fixed (self , name : str , arity : int , func : Callable [..., int ]) -> None :
509539 def impl (interpreter : "Interpreter" , args : List [int ], _ : List [Expression ], __ : Environment , ___ : SourceLocation ) -> int :
@@ -539,10 +569,10 @@ def _safe_div(self, a: int, b: int) -> int:
539569 if b == 0 :
540570 raise ASMRuntimeError ("Division by zero" , rewrite_rule = "DIV" )
541571 return a // b
542- def _safe_ceil (self , a : int , b : int ) -> int :
572+ def _safe_cdiv (self , a : int , b : int ) -> int :
543573 if b == 0 :
544- raise ASMRuntimeError ("Division by zero" , rewrite_rule = "CEIL " )
545- # Compute ceil (a / b) using integer operations only.
574+ raise ASMRuntimeError ("Division by zero" , rewrite_rule = "CDIV " )
575+ # Compute ceiling division CDIV (a / b) using integer operations only.
546576 # Use Python's floor division to get the floor quotient `q`.
547577 q = a // b
548578 # If remainder is zero then the division is exact; otherwise ceil is q+1.
@@ -566,6 +596,19 @@ def _prod(self, values: List[int]) -> int:
566596 for value in values :
567597 result *= value
568598 return result
599+ def _join (self , values : List [int ]) -> int :
600+ # Allow either all non-negative arguments or all negative arguments.
601+ if any (value < 0 for value in values ):
602+ if not all (value < 0 for value in values ):
603+ raise ASMRuntimeError ("JOIN arguments must not mix positive and negative values" , rewrite_rule = "JOIN" )
604+ # All values are negative: concatenate the binary spellings of their absolute values,
605+ # then return the negated integer value.
606+ abs_vals = [abs (v ) for v in values ]
607+ bits = "" .join ("0" if v == 0 else format (v , "b" ) for v in abs_vals )
608+ return - int (bits or "0" , 2 )
609+ # All values non-negative: concatenate their binary spellings in call order.
610+ bits = "" .join ("0" if value == 0 else format (value , "b" ) for value in values )
611+ return int (bits or "0" , 2 )
569612 def _safe_log (self , value : int ) -> int :
570613 if value <= 0 :
571614 raise ASMRuntimeError ("LOG argument must be > 0" , rewrite_rule = "LOG" )
@@ -610,7 +653,17 @@ def _import(
610653 with open (module_path , "r" , encoding = "utf-8" ) as handle :
611654 source_text = handle .read ()
612655 except OSError as exc :
613- raise ASMRuntimeError (f"Failed to import '{ module_name } ': { exc } " , location = location , rewrite_rule = "IMPORT" )
656+ # Fallback: also search for a `lib` subdirectory adjacent to the
657+ # interpreter implementation (useful when modules are shipped
658+ # alongside the interpreter in a `lib/` directory).
659+ interpreter_dir = os .path .dirname (os .path .abspath (sys .argv [0 ]))
660+ lib_module_path = os .path .join (interpreter_dir , "lib" , f"{ module_name } .asmln" )
661+ try :
662+ with open (lib_module_path , "r" , encoding = "utf-8" ) as handle :
663+ source_text = handle .read ()
664+ module_path = lib_module_path
665+ except OSError :
666+ raise ASMRuntimeError (f"Failed to import '{ module_name } ': { exc } " , location = location , rewrite_rule = "IMPORT" )
614667
615668 lexer = Lexer (source_text , module_path )
616669 tokens = lexer .tokenize ()
@@ -686,6 +739,21 @@ def _delete(
686739 err .location = location
687740 raise
688741 return 0
742+ def _exist (
743+ self ,
744+ interpreter : "Interpreter" ,
745+ args : List [int ],
746+ arg_nodes : List [Expression ],
747+ env : Environment ,
748+ location : SourceLocation ,
749+ ) -> int :
750+ # EXIST requires a plain identifier argument and returns 1 if that
751+ # identifier exists in the current environment (searching parents),
752+ # otherwise 0. This is intended for LBYL-style checks.
753+ if not arg_nodes or not isinstance (arg_nodes [0 ], Identifier ):
754+ raise ASMRuntimeError ("EXIST requires an identifier argument" , location = location , rewrite_rule = "EXIST" )
755+ name = arg_nodes [0 ].name
756+ return 1 if env .has (name ) else 0
689757 def _exit (
690758 self ,
691759 interpreter : "Interpreter" ,
@@ -818,6 +886,20 @@ def _evaluate_expression(self, expression: Expression, env: Environment) -> int:
818886 self ._log_step (rule = "IMPORT" ,location = expression .location ,extra = {"module" : module_label ,"result" : result })
819887 return (result )
820888
889+ # Builtins that operate on identifier nodes (DEL, EXIST) must not
890+ # cause evaluation of the identifier expression (which would raise
891+ # if the identifier is undefined). Pass dummy numeric args and the
892+ # raw arg nodes to the builtin so it can inspect names itself.
893+ if expression .name in ("DEL" , "EXIST" ):
894+ dummy_args :List [int ] = [0 ] * len (expression .args )
895+ try :
896+ result :int = self .builtins .invoke (self , expression .name , dummy_args , expression .args , env , expression .location )
897+ except ASMRuntimeError as err :
898+ self ._log_step (rule = expression .name , location = expression .location , extra = {"args" : None , "status" : "error" })
899+ raise
900+ self ._log_step (rule = expression .name , location = expression .location , extra = {"args" : None , "result" : result })
901+ return result
902+
821903 args :List [int ] = []
822904 for arg in expression .args :
823905 args .append (self ._evaluate_expression (arg ,env ))
@@ -965,7 +1047,7 @@ def run_repl(verbose: bool) -> int:
9651047 buffer : List [str ] = []
9661048
9671049 while True :
968- prompt = ">>> " if not buffer else "..> "
1050+ prompt = "\x1b [38;2;153;221;255m >>>\033 [0m " if not buffer else "\x1b [38;2;153;221;255m ..>\033 [0m "
9691051 try :
9701052 line = input (prompt )
9711053 except EOFError :
0 commit comments