33import argparse
44import json
55import math
6+ import os
67import sys
78from dataclasses import dataclass
89from typing import Any , Callable , Dict , Iterable , List , Optional
@@ -83,8 +84,11 @@ def tokenize(self) -> List[Token]:
8384 tokens .append (Token (SYMBOLS [ch ],ch ,self .line ,self .column ))
8485 self ._advance ()
8586 continue
87+ if (ch == "-" ):
88+ tokens .append (self ._consume_signed_number ())
89+ continue
8690 if (ch in "01" ):
87- tokens .append (self ._consume_number ())
91+ tokens .append (self ._consume_unsigned_number ())
8892 continue
8993 if (self ._is_identifier_start (ch )):
9094 tokens .append (self ._consume_identifier ())
@@ -95,13 +99,27 @@ def tokenize(self) -> List[Token]:
9599 def _consume_comment (self ) -> None :
96100 while (not self ._eof and self ._peek () != "\n " ):
97101 self ._advance ()
98- def _consume_number (self ) -> Token :
102+ def _consume_unsigned_number (self ) -> Token :
103+ line , col = self .line , self .column
104+ digits = self ._consume_binary_digits ()
105+ return (Token ("NUMBER" ,digits ,line ,col ))
106+
107+ def _consume_signed_number (self ) -> Token :
99108 line , col = self .line , self .column
109+ self ._advance () # consume '-'
110+ while (not self ._eof and self ._peek () in " \t \r " ):
111+ self ._advance ()
112+ if (self ._eof or self ._peek () not in "01" ):
113+ raise ASMParseError (f"Expected binary digits after '-' at { self .filename } :{ line } :{ col } " )
114+ digits = self ._consume_binary_digits ()
115+ return (Token ("NUMBER" ,"-" + digits ,line ,col ))
116+
117+ def _consume_binary_digits (self ) -> str :
100118 digits : List [str ] = []
101119 while (not self ._eof and self ._peek () in "01" ):
102120 digits .append (self ._peek ())
103121 self ._advance ()
104- return ( Token ( "NUMBER" , "" .join (digits ), line , col ) )
122+ return "" .join (digits )
105123 def _consume_identifier (self ) -> Token :
106124 line , col = self .line , self .column
107125 chars : List [str ] = []
@@ -480,6 +498,8 @@ def __init__(self) -> None:
480498 self ._register_variadic ("LEN" , 0 , lambda vals : len (vals ))
481499 self ._register_fixed ("LOG" , 1 , self ._safe_log )
482500 self ._register_fixed ("CLOG" , 1 , self ._safe_clog )
501+ self ._register_custom ("MAIN" , 0 , 0 , self ._main )
502+ self ._register_custom ("IMPORT" , 1 , 1 , self ._import )
483503 self ._register_custom ("INPUT" , 0 , 0 , self ._input )
484504 self ._register_custom ("PRINT" , 0 , None , self ._print )
485505 self ._register_custom ("ASSERT" , 1 , 1 , self ._assert )
@@ -557,6 +577,49 @@ def _safe_clog(self, value: int) -> int:
557577 return value .bit_length () - 1
558578 return value .bit_length ()
559579
580+ def _main (
581+ self ,
582+ interpreter : "Interpreter" ,
583+ _ : List [int ],
584+ __ : List [Expression ],
585+ ___ : Environment ,
586+ location : SourceLocation ,
587+ ) -> int :
588+ # Return 1 when the call originates from the primary program file, else 0.
589+ root = interpreter .entry_filename
590+ if root == "<string>" :
591+ return 1 if location .file == "<string>" else 0
592+ return 1 if os .path .abspath (location .file ) == root else 0
593+
594+ def _import (
595+ self ,
596+ interpreter : "Interpreter" ,
597+ _ : List [int ],
598+ arg_nodes : List [Expression ],
599+ env : Environment ,
600+ location : SourceLocation ,
601+ ) -> int :
602+ if len (arg_nodes ) != 1 or not isinstance (arg_nodes [0 ], Identifier ):
603+ raise ASMRuntimeError ("IMPORT expects module name identifier" , location = location , rewrite_rule = "IMPORT" )
604+
605+ module_name = arg_nodes [0 ].name
606+ base_dir = os .getcwd () if location .file == "<string>" else os .path .dirname (os .path .abspath (location .file ))
607+ module_path = os .path .join (base_dir , f"{ module_name } .asmln" )
608+
609+ try :
610+ with open (module_path , "r" , encoding = "utf-8" ) as handle :
611+ source_text = handle .read ()
612+ except OSError as exc :
613+ raise ASMRuntimeError (f"Failed to import '{ module_name } ': { exc } " , location = location , rewrite_rule = "IMPORT" )
614+
615+ lexer = Lexer (source_text , module_path )
616+ tokens = lexer .tokenize ()
617+ parser = Parser (tokens , module_path , source_text .splitlines ())
618+ program = parser .parse ()
619+
620+ interpreter ._execute_block (program .statements , env )
621+ return 0
622+
560623 def _slice (self , a : int , hi : int , lo : int ) -> int :
561624 if hi < lo :
562625 raise ASMRuntimeError ("SLICE: hi must be >= lo" , rewrite_rule = "SLICE" )
@@ -645,7 +708,9 @@ def __init__(
645708 output_sink : Optional [Callable [[str ], None ]] = None ,
646709 ) -> None :
647710 self .source = source
648- self .filename = filename
711+ normalized_filename = filename if filename == "<string>" else os .path .abspath (filename )
712+ self .filename = normalized_filename
713+ self .entry_filename = normalized_filename
649714 self .verbose = verbose
650715 self .input_provider = input_provider or (lambda : input (">>> " ))
651716 self .output_sink = output_sink or (lambda text : print (text ))
@@ -742,6 +807,17 @@ def _evaluate_expression(self, expression: Expression, env: Environment) -> int:
742807 return (result )
743808 raise
744809 if (isinstance (expression ,CallExpression )):
810+ if (expression .name == "IMPORT" ):
811+ module_label = expression .args [0 ].name if (expression .args and isinstance (expression .args [0 ], Identifier )) else None
812+ dummy_args :List [int ] = [0 ] * len (expression .args )
813+ try :
814+ result :int = self .builtins .invoke (self ,expression .name ,dummy_args ,expression .args ,env ,expression .location )
815+ except ASMRuntimeError :
816+ self ._log_step (rule = "IMPORT" ,location = expression .location ,extra = {"module" : module_label ,"status" : "error" })
817+ raise
818+ self ._log_step (rule = "IMPORT" ,location = expression .location ,extra = {"module" : module_label ,"result" : result })
819+ return (result )
820+
745821 args :List [int ] = []
746822 for arg in expression .args :
747823 args .append (self ._evaluate_expression (arg ,env ))
@@ -873,12 +949,92 @@ def to_json(self, error: ASMRuntimeError) -> str:
873949 }
874950 return json .dumps (data , indent = 2 )
875951def run_cli (argv : Optional [List [str ]] = None ) -> int :
952+ def _parse_statements_from_source (text : str , filename : str ) -> List [Statement ]:
953+ lexer = Lexer (text , filename )
954+ tokens = lexer .tokenize ()
955+ parser = Parser (tokens , filename , text .splitlines ())
956+ program = parser .parse ()
957+ return program .statements
958+
959+ def run_repl (verbose : bool ) -> int :
960+ print ("ASM-Lang REPL. Enter statements, blank line to run buffer." )
961+ interpreter = Interpreter (source = "" , filename = "<repl>" , verbose = verbose )
962+ global_env = Environment ()
963+ global_frame = interpreter ._new_frame ("<repl>" , global_env , None )
964+ interpreter .call_stack .append (global_frame )
965+ buffer : List [str ] = []
966+
967+ while True :
968+ prompt = ">>> " if not buffer else "..> "
969+ try :
970+ line = input (prompt )
971+ except EOFError :
972+ print ()
973+ break
974+
975+ stripped = line .strip ()
976+
977+ # If buffer is empty, try to execute single-line statements immediately
978+ # (so EXIT() exits without needing a blank line). Buffer multi-line
979+ # constructs that start blocks (FUNC, IF, WHILE, FOR) or explicit
980+ # bracket starts.
981+ is_block_start = False
982+ if not buffer :
983+ uc = stripped .upper ()
984+ if uc .startswith ("FUNC" ) or uc .startswith ("IF" ) or uc .startswith ("WHILE" ) or uc .startswith ("FOR" ):
985+ is_block_start = True
986+ if stripped .endswith ("[" ) or stripped .endswith ("{" ):
987+ is_block_start = True
988+
989+ if not buffer and stripped != "" and not is_block_start :
990+ # try execute this single line immediately
991+ try :
992+ statements = _parse_statements_from_source (line , "<repl>" )
993+ try :
994+ interpreter ._execute_block (statements , global_env )
995+ except ExitSignal as sig :
996+ return sig .code
997+ except ASMParseError as error :
998+ # If parse error, fall back to buffering the line to allow
999+ # multi-line input (user may be starting a block).
1000+ buffer .append (line )
1001+ continue
1002+
1003+ if stripped == "" and buffer :
1004+ source_text = "\n " .join (buffer )
1005+ buffer .clear ()
1006+ try :
1007+ statements = _parse_statements_from_source (source_text , "<repl>" )
1008+ interpreter ._execute_block (statements , global_env )
1009+ except ExitSignal as sig :
1010+ return sig .code
1011+ except ASMParseError as error :
1012+ print (f"ParseError: { error } " , file = sys .stderr )
1013+ except ASMRuntimeError as error :
1014+ if interpreter .logger .entries :
1015+ error .step_index = interpreter .logger .entries [- 1 ].step_index
1016+ formatter = TracebackFormatter (interpreter )
1017+ print (formatter .format_text (error , verbose = interpreter .verbose ), file = sys .stderr )
1018+ interpreter .call_stack = [global_frame ]
1019+ continue
1020+
1021+ buffer .append (line )
1022+
1023+ return 0
1024+
8761025 parser = argparse .ArgumentParser (description = "ASM-Lang reference interpreter" )
877- parser .add_argument ("program" , help = "Source file path or literal source with -source" )
1026+ parser .add_argument ("program" , nargs = "?" , help = "Source file path or literal source with -source" )
8781027 parser .add_argument ("-source" , "--source" , dest = "source_mode" , action = "store_true" , help = "Treat program argument as literal source text" )
8791028 parser .add_argument ("-verbose" , "--verbose" , dest = "verbose" , action = "store_true" , help = "Emit env snapshots in tracebacks" )
8801029 parser .add_argument ("--traceback-json" , action = "store_true" , help = "Also emit JSON traceback" )
8811030 args = parser .parse_args (argv )
1031+
1032+ if args .program is None :
1033+ if args .source_mode :
1034+ print ("-source requires a program string" , file = sys .stderr )
1035+ return 1
1036+ return run_repl (verbose = args .verbose )
1037+
8821038 if args .source_mode :
8831039 source_text = args .program
8841040 filename = "<string>"
@@ -906,4 +1062,4 @@ def run_cli(argv: Optional[List[str]] = None) -> int:
9061062 return 1
9071063 return 0
9081064if __name__ == "__main__" :
909- raise SystemExit (run_cli ())
1065+ raise SystemExit (run_cli ())
0 commit comments