Skip to content

Commit 54d7b23

Browse files
Add control-flow statement ASYNC, make the endiannes (?) of BYTES configureable via optional argument
1 parent 26afc9c commit 54d7b23

File tree

5 files changed

+84
-14
lines changed

5 files changed

+84
-14
lines changed

SPECIFICATION.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@
137137
138138
Blocks group one or more statements and are enclosed in curly brackets: `{ statement1 ... statementN }`. Curly braces must match (that is, `{` closes with `}`). Blocks serve as the bodies of control-flow constructs and functions.
139139
140+
ASYNC blocks: The `ASYNC` statement introduces a background task whose body is a regular block: `ASYNC{ ... }`. The statements inside the block execute synchronously with respect to each other but asynchronously relative to the rest of the program: the interpreter begins executing the block in a background task and immediately continues with the next statement in the current thread. The block shares the main program namespace (it executes with the same lexical environment), so assignments and mutations performed inside an `ASYNC` block are visible to the rest of the program immediately. Runtime errors raised inside an `ASYNC` block are reported via the interpreter's error hooks and recorded in the state log; they do not synchronously abort the main thread. Note that language invariants still apply inside the `ASYNC` block (for example, `RETURN` outside a function is a runtime error).
141+
140142
Assignments have the syntax `TYPE : identifier = expression` on first use, where TYPE is `INT`, `STR`, or `TNS`. Spaces around the colon and equals sign are optional. Subsequent assignments to an existing identifier may omit the type but must preserve the original type. Variables are deallocated only when `DEL(identifier)` is executed.
141143
142144
Tensor elements can be reassigned with the indexed form `identifier[i1,...,iN] = expression`. The base must be a previously-declared `TNS` binding. The indices must match the tensor's dimensionality, follow the same one-based/negative-index rules as ordinary indexing, and must reference anexisting position. The element's original type cannot change: attempting to store a different type at that position is a runtime error. Indexed assignment mutates the `TNS`.
@@ -166,7 +168,7 @@
166168
167169
## 6. Variables and Memory Model
168170
169-
A variable is created only when it is first assigned with an explicit type annotation of the form `TYPE : name = expression`, where TYPE is `INT`, `STR`, or `TNS`. For example: `INT` : counter = 0` or `STR` : message = "hi"`. Subsequent assignments to the same name must match the declared type and may omit the type annotation (`counter = ADD(counter,1)`). Assigning to an undeclared name without a type annotation is a runtime error. A variable exists until `DEL(name)` is executed. Referencing a variable that has never been declared, or that has been deleted, is a runtime error.
171+
A variable is created only when it is first assigned with an explicit type annotation of the form `TYPE : name = expression`, where TYPE is `INT`, `STR`, or `TNS`. For example: `INT : counter = 0` or `STR : message = "hi"`. Subsequent assignments to the same name must match the declared type and may omit the type annotation (`counter = ADD(counter,1)`). Assigning to an undeclared name without a type annotation is a runtime error. A variable exists until `DEL(name)` is executed. Referencing a variable that has never been declared, or that has been deleted, is a runtime error.
170172
171173
The language assumes at least a global typed environment mapping identifiers to (type, value) pairs. Function calls create new environments for parameters and local variables, as described in Section 6.2; the precise details of name resolution depend on the chosen scoping rules.
172174
@@ -343,7 +345,7 @@
343345
- `EXIT()` or `EXIT(INT:code)` Requests immediate termination of the interpreter. If an integer `code` is supplied, it is used as the interpreter's process exit code; otherwise `0` is used. Execution stops immediately when `EXIT` is executed (no further statements run), and an entry is recorded in the state log to make deterministic replay possible. Using `EXIT` inside a function terminates the entire program (not just the function).
344346
- `INT(STR: x):INT` ; `STR` -> `INT` using empty/01/other rules; `INT` passthrough
345347
- `STR(INT: x):STR` ; `INT` -> binary-spelled `STR`; `STR` passthrough
346-
- `BYTES(INT: n):TNS` Converts a non-negative integer into its big-endian byte representation. The result is a one-dimensional `TNS` whose elements are `INT` values in the range `0..11111111` (0-255 decimal), ordered most-significant byte first. The tensor length is `max(1, ceil(bit_length(n)/8))`; `BYTES(0)` returns a single zero byte. Supplying a negative integer is a runtime error.
348+
- `BYTES(INT: n, endian = "big"):TNS` Converts a non-negative integer into its byte representation. The optional `endian` parameter controls byte order: if `endian` is `"little"` the result is little-endian (least-significant byte first); if `endian` is `"big"` the result is big-endian (most-significant byte first). The result is a one-dimensional `TNS` whose elements are `INT` values in the range `0..11111111` (0-255 decimal). The tensor length is `max(1, ceil(bit_length(n)/8))`; `BYTES(0)` returns a single zero byte. Supplying a negative integer is a runtime error. Supplying an `endian` value other than `"big"` or `"little"` is a runtime error.
347349
- Binary literal: optional leading `-` (spaces/tabs/CR allowed after the dash) then `{0,1}+`
348350
- String literal: `"` characters `"` (no escapes, no newlines)
349351
- `EXIST(SYMBOL: x):INT` ; returns `1` if the identifier `x` exists in the current environment (searching enclosing environments), else `0`. The argument must be an identifier; supplying an expression that is not a plain identifier is a runtime error.

asm-lang.exe

2.56 KB
Binary file not shown.

interpreter.py

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import tempfile
99
import codecs
1010
import numpy as np
11+
import threading
1112
from dataclasses import dataclass, field
1213
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
1314
from numpy.typing import NDArray
@@ -24,6 +25,7 @@
2425
ExpressionStatement,
2526
ForStatement,
2627
FuncDef,
28+
AsyncStatement,
2729
GotoStatement,
2830
GotopointStatement,
2931
Identifier,
@@ -338,7 +340,6 @@ def _as_bool(value: int) -> int:
338340
class Builtins:
339341
def __init__(self) -> None:
340342
self.table: Dict[str, BuiltinFunction] = {}
341-
# Numeric operators that support INT and FLT (no mixing).
342343
self._register_custom("ADD", 2, 2, self._add)
343344
self._register_custom("SUB", 2, 2, self._sub)
344345
self._register_custom("MUL", 2, 2, self._mul)
@@ -412,7 +413,7 @@ def __init__(self) -> None:
412413
self._register_custom("TYPE", 1, 1, self._type)
413414
self._register_custom("ROUND", 1, 3, self._round)
414415
self._register_custom("READFILE", 1, 2, self._readfile)
415-
self._register_custom("BYTES", 1, 1, self._bytes)
416+
self._register_custom("BYTES", 1, 2, self._bytes)
416417
self._register_custom("WRITEFILE", 2, 3, self._writefile)
417418
self._register_custom("EXISTFILE", 1, 1, self._existfile)
418419
self._register_custom("CL", 1, 1, self._cl)
@@ -1916,6 +1917,17 @@ def _bytes(
19161917
number = self._expect_int(args[0], "BYTES", location)
19171918
if number < 0:
19181919
raise ASMRuntimeError("BYTES expects a non-negative integer", location=location, rewrite_rule="BYTES")
1920+
# Optional endian argument: default is big-endian
1921+
endian = "big"
1922+
if len(args) >= 2:
1923+
endian_val = args[1]
1924+
if endian_val.type != TYPE_STR:
1925+
raise ASMRuntimeError("BYTES expects string endian argument", location=location, rewrite_rule="BYTES")
1926+
endian = str(endian_val.value).strip().lower()
1927+
# Validate endian
1928+
if endian not in {"big", "little"}:
1929+
raise ASMRuntimeError("BYTES endian must be 'big' or 'little'", location=location, rewrite_rule="BYTES")
1930+
19191931
if number == 0:
19201932
data = np.array([Value(TYPE_INT, 0)], dtype=object)
19211933
return Value(TYPE_TNS, Tensor(shape=[1], data=data))
@@ -1925,7 +1937,9 @@ def _bytes(
19251937
while temp > 0:
19261938
octets.append(temp & 0xFF)
19271939
temp >>= 8
1928-
octets.reverse()
1940+
# octets currently little-endian (LSB first); reverse for big-endian
1941+
if endian == "big":
1942+
octets.reverse()
19291943

19301944
data = np.array([Value(TYPE_INT, b) for b in octets], dtype=object)
19311945
return Value(TYPE_TNS, Tensor(shape=[len(octets)], data=data))
@@ -3105,6 +3119,38 @@ def _execute_statement(self, statement: Statement, env: Environment) -> None:
31053119
if isinstance(statement, GotoStatement):
31063120
target = self._evaluate_expression(statement.expression, env)
31073121
raise JumpSignal(target)
3122+
if isinstance(statement, AsyncStatement):
3123+
# Execute the block synchronously inside a background thread
3124+
block = statement.block
3125+
loc = statement.location
3126+
3127+
def _async_worker() -> None:
3128+
frame = self._new_frame("<async>", env, loc)
3129+
self.call_stack.append(frame)
3130+
try:
3131+
self._emit_event("async_start", self, frame, env)
3132+
try:
3133+
self._execute_block(block.statements, env)
3134+
except Exception as exc:
3135+
# Notify hooks about the error so it can be recorded/handled.
3136+
try:
3137+
self._emit_event("on_error", self, exc)
3138+
except Exception:
3139+
pass
3140+
finally:
3141+
# Pop the async frame and emit end event.
3142+
try:
3143+
self.call_stack.pop()
3144+
except Exception:
3145+
pass
3146+
try:
3147+
self._emit_event("async_end", self, frame, env)
3148+
except Exception:
3149+
pass
3150+
3151+
t = threading.Thread(target=_async_worker, daemon=True, name=f"asm_async_{self.frame_counter}")
3152+
t.start()
3153+
return
31083154
raise ASMRuntimeError("Unsupported statement", location=statement.location)
31093155

31103156
def _execute_if(self, statement: IfStatement, env: Environment) -> None:
@@ -3495,17 +3541,26 @@ def _evaluate_expression(self, expression: Expression, env: Environment) -> Valu
34953541
)
34963542
try:
34973543
if keyword_args:
3544+
# Allow specific named keywords for certain builtins.
34983545
if expression.name in {"READFILE", "WRITEFILE"}:
34993546
allowed = {"coding"}
3500-
unexpected = [k for k in keyword_args if k not in allowed]
3501-
if unexpected:
3502-
raise ASMRuntimeError(
3503-
f"Unexpected keyword arguments: {', '.join(sorted(unexpected))}",
3504-
location=expression.location,
3505-
rewrite_rule=expression.name,
3506-
)
3507-
if "coding" in keyword_args:
3508-
positional_args.append(keyword_args.pop("coding"))
3547+
key = "coding"
3548+
elif expression.name == "BYTES":
3549+
allowed = {"endian"}
3550+
key = "endian"
3551+
else:
3552+
allowed = set()
3553+
key = None
3554+
3555+
unexpected = [k for k in keyword_args if k not in allowed]
3556+
if unexpected:
3557+
raise ASMRuntimeError(
3558+
f"Unexpected keyword arguments: {', '.join(sorted(unexpected))}",
3559+
location=expression.location,
3560+
rewrite_rule=expression.name,
3561+
)
3562+
if key and key in keyword_args:
3563+
positional_args.append(keyword_args.pop(key))
35093564
if keyword_args:
35103565
raise ASMRuntimeError(
35113566
f"{expression.name} does not accept keyword arguments",

lexer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class Token:
2626
"WHILE",
2727
"FOR",
2828
"FUNC",
29+
"ASYNC",
2930
"RETURN",
3031
"POP",
3132
"BREAK",

parser.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ class ContinueStatement(Statement):
118118
pass
119119

120120

121+
@dataclass
122+
class AsyncStatement(Statement):
123+
block: Block
124+
125+
121126
class Expression(Node):
122127
pass
123128

@@ -231,6 +236,8 @@ def _parse_statement(self) -> Statement:
231236
return self._parse_func()
232237
if token.type == "IF":
233238
return self._parse_if()
239+
if token.type == "ASYNC":
240+
return self._parse_async()
234241
if token.type == "WHILE":
235242
return self._parse_while()
236243
if token.type == "FOR":
@@ -364,6 +371,11 @@ def _parse_continue(self) -> ContinueStatement:
364371
self._consume("RPAREN")
365372
return ContinueStatement(location=self._location_from_token(keyword))
366373

374+
def _parse_async(self) -> AsyncStatement:
375+
keyword = self._consume("ASYNC")
376+
block: Block = self._parse_block()
377+
return AsyncStatement(location=self._location_from_token(keyword), block=block)
378+
367379
def _parse_block(self) -> Block:
368380
opening = self._peek().type
369381
if opening == "LBRACE":

0 commit comments

Comments
 (0)