Skip to content

Commit ef71cb1

Browse files
Add Standard Library
1 parent a2cb09f commit ef71cb1

File tree

7 files changed

+689
-110
lines changed

7 files changed

+689
-110
lines changed

asmln.exe

1.35 KB
Binary file not shown.

asmln.py

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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:

asmln.spec

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- mode: python ; coding: utf-8 -*-
2+
3+
4+
a = Analysis(
5+
['c:/Users/jaked/OneDrive/Formerly a USB/PY/ASM-Lang/asmln.py'],
6+
pathex=[],
7+
binaries=[],
8+
datas=[],
9+
hiddenimports=[],
10+
hookspath=[],
11+
hooksconfig={},
12+
runtime_hooks=[],
13+
excludes=[],
14+
noarchive=False,
15+
optimize=0,
16+
)
17+
pyz = PYZ(a.pure)
18+
19+
exe = EXE(
20+
pyz,
21+
a.scripts,
22+
a.binaries,
23+
a.datas,
24+
[],
25+
name='asmln',
26+
debug=False,
27+
bootloader_ignore_signals=False,
28+
strip=False,
29+
upx=True,
30+
upx_exclude=[],
31+
runtime_tmpdir=None,
32+
console=True,
33+
disable_windowed_traceback=False,
34+
argv_emulation=False,
35+
target_arch=None,
36+
codesign_identity=None,
37+
entitlements_file=None,
38+
)

0 commit comments

Comments
 (0)