Skip to content

Commit 4c7f5af

Browse files
Improve tensor implementation, add Waveforms lib & Windows CL fallback
- Switch tensor storage to NumPy arrays (object dtype) and iterate via .flat - Add in-place tensor mutation support (_mutate_tensor_value) and enforce frozen ids on element assignment - Implement Windows long-command workaround for CL (temp .ps1/.cmd files) and safely cleanup - Add numpy/tempfile imports and typing hints; minor REPL/argparse whitespace fix - Add new lib/waveforms.asmln and accompanying tests; update spec.txt to document mutable indexed assignment - Update asmln.exe binary
1 parent 79d8b40 commit 4c7f5af

File tree

6 files changed

+507
-37
lines changed

6 files changed

+507
-37
lines changed

asmln.exe

6.34 MB
Binary file not shown.

asmln.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""ASM-Lang entry point and REPL wiring."""
2+
23
from __future__ import annotations
34
import argparse
45
import sys

interpreter.py

Lines changed: 121 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
import os
66
import sys
77
import platform
8+
import tempfile
89
import codecs
10+
import numpy as np
911
from dataclasses import dataclass, field
1012
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
13+
from numpy.typing import NDArray
1114

1215
from lexer import ASMError, ASMParseError, Lexer
1316
from parser import (
@@ -44,11 +47,13 @@
4447
TYPE_STR = "STR"
4548
TYPE_TNS = "TNS"
4649

50+
# On Windows, command lines over a certain length cause CreateProcess errors
51+
WINDOWS_COMMAND_LENGTH_LIMIT = 8000
4752

4853
@dataclass(frozen=True)
4954
class Tensor:
5055
shape: List[int]
51-
data: List["Value"]
56+
data: NDArray[Any]
5257

5358

5459
@dataclass
@@ -445,14 +450,38 @@ def _expect_tns(self, value: Value, rule: str, location: SourceLocation) -> Tens
445450
assert isinstance(value.value, Tensor)
446451
return value.value
447452

453+
def _extract_powershell_body(self, cmd: str) -> Optional[str]:
454+
"""Best-effort extraction of the script text passed to -Command.
455+
456+
This keeps the heavy script contents out of the CreateProcess command
457+
line by allowing us to emit it into a temporary .ps1 file instead.
458+
"""
459+
lower = cmd.lower()
460+
idx = lower.find("-command")
461+
if idx == -1:
462+
return None
463+
tail = cmd[idx + len("-command") :].lstrip()
464+
if not tail:
465+
return None
466+
if tail[0] in ('"', "'"):
467+
quote = tail[0]
468+
tail = tail[1:]
469+
end = tail.find(quote)
470+
if end == -1:
471+
return None
472+
return tail[:end]
473+
# Fallback: take the next token
474+
parts = tail.split(None, 1)
475+
return parts[0] if parts else None
476+
448477
def _as_bool_value(self, value: Value) -> int:
449478
if value.type == TYPE_INT:
450479
return 0 if value.value == 0 else 1
451480
if value.type == TYPE_STR:
452481
return 0 if value.value == "" else 1
453482
if value.type == TYPE_TNS:
454483
assert isinstance(value.value, Tensor)
455-
return 1 if any(self._as_bool_value(item) for item in value.value.data) else 0
484+
return 1 if any(self._as_bool_value(item) for item in value.value.data.flat) else 0
456485
return 0
457486

458487
def _condition_from_value(self, value: Value) -> int:
@@ -1237,7 +1266,7 @@ def _shape_from_tensor(self, tensor: Tensor, rule: str, location: SourceLocation
12371266
if len(tensor.shape) != 1:
12381267
raise ASMRuntimeError(f"{rule} shape must be a 1D tensor", location=location, rewrite_rule=rule)
12391268
dims: List[int] = []
1240-
for entry in tensor.data:
1269+
for entry in tensor.data.flat:
12411270
dim = self._expect_int(entry, rule, location)
12421271
if dim <= 0:
12431272
raise ASMRuntimeError("Tensor dimensions must be positive", location=location, rewrite_rule=rule)
@@ -1249,22 +1278,24 @@ def _shape_from_tensor(self, tensor: Tensor, rule: str, location: SourceLocation
12491278
def _map_tensor_int_binary(self, x: Tensor, y: Tensor, rule: str, location: SourceLocation, op: Callable[[int, int], int]) -> Tensor:
12501279
if x.shape != y.shape:
12511280
raise ASMRuntimeError(f"{rule} requires tensors with identical shapes", location=location, rewrite_rule=rule)
1252-
data: List[Value] = []
1253-
for a, b in zip(x.data, y.data):
1254-
ai = self._expect_int(a, rule, location)
1255-
bi = self._expect_int(b, rule, location)
1256-
data.append(Value(TYPE_INT, op(ai, bi)))
1281+
data = np.array(
1282+
[
1283+
Value(TYPE_INT, op(self._expect_int(a, rule, location), self._expect_int(b, rule, location)))
1284+
for a, b in zip(x.data.flat, y.data.flat)
1285+
],
1286+
dtype=object,
1287+
)
12571288
return Tensor(shape=list(x.shape), data=data)
12581289

12591290
def _map_tensor_int_scalar(self, tensor: Tensor, scalar: int, rule: str, location: SourceLocation, op: Callable[[int, int], int]) -> Tensor:
1260-
data: List[Value] = []
1261-
for entry in tensor.data:
1262-
val = self._expect_int(entry, rule, location)
1263-
data.append(Value(TYPE_INT, op(val, scalar)))
1291+
data = np.array(
1292+
[Value(TYPE_INT, op(self._expect_int(entry, rule, location), scalar)) for entry in tensor.data.flat],
1293+
dtype=object,
1294+
)
12641295
return Tensor(shape=list(tensor.shape), data=data)
12651296

12661297
def _ensure_tensor_ints(self, tensor: Tensor, rule: str, location: SourceLocation) -> None:
1267-
for entry in tensor.data:
1298+
for entry in tensor.data.flat:
12681299
self._expect_int(entry, rule, location)
12691300

12701301
# Tensor built-ins
@@ -1277,7 +1308,7 @@ def _shape(
12771308
location: SourceLocation,
12781309
) -> Value:
12791310
tensor = self._expect_tns(args[0], "SHAPE", location)
1280-
shape_data = [Value(TYPE_INT, dim) for dim in tensor.shape]
1311+
shape_data = np.array([Value(TYPE_INT, dim) for dim in tensor.shape], dtype=object)
12811312
return Value(TYPE_TNS, Tensor(shape=[len(shape_data)], data=shape_data))
12821313

12831314
def _tlen(
@@ -1304,9 +1335,12 @@ def _fill(
13041335
) -> Value:
13051336
tensor = self._expect_tns(args[0], "FILL", location)
13061337
fill_value = args[1]
1307-
if any(entry.type != fill_value.type for entry in tensor.data):
1338+
if any(entry.type != fill_value.type for entry in tensor.data.flat):
13081339
raise ASMRuntimeError("FILL value type must match existing tensor element types", location=location, rewrite_rule="FILL")
1309-
new_data = [Value(fill_value.type, fill_value.value) for _ in tensor.data]
1340+
new_data = np.array(
1341+
[Value(fill_value.type, fill_value.value) for _ in range(tensor.data.size)],
1342+
dtype=object,
1343+
)
13101344
return Value(TYPE_TNS, Tensor(shape=list(tensor.shape), data=new_data))
13111345

13121346
def _tns(
@@ -1473,18 +1507,62 @@ def _cl(
14731507
# Capture stdout/stderr so the REPL can display results without
14741508
# spawning a visible console window on Windows.
14751509
cmd = self._expect_str(args[0], "CL", location)
1510+
cleanup_script: Optional[str] = None
14761511
try:
14771512
# Use text mode for captured output.
1478-
run_kwargs = {"shell": True, "stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "text": True}
1513+
run_kwargs: Dict[str, object] = {"stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "text": True}
1514+
# Default to shell=True to preserve existing behaviour for string commands.
1515+
run_kwargs["shell"] = True
1516+
14791517
# On Windows, avoid creating a visible console window for subprocesses.
14801518
if platform.system().lower().startswith("win"):
14811519
run_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
1482-
completed = subprocess.run(cmd, **run_kwargs)
1520+
1521+
command_to_run: Union[str, List[str]] = cmd
1522+
if platform.system().lower().startswith("win") and len(cmd) > WINDOWS_COMMAND_LENGTH_LIMIT:
1523+
stripped = cmd.lstrip()
1524+
prefix = stripped.split(None, 1)[0].lower() if stripped else ""
1525+
1526+
# If this is a PowerShell command, lift the -Command payload into
1527+
# a temporary .ps1 to avoid CreateProcess length limits.
1528+
if prefix in {"powershell", "pwsh"}:
1529+
ps_body = self._extract_powershell_body(cmd)
1530+
if ps_body is not None:
1531+
fd, script_path = tempfile.mkstemp(suffix=".ps1", text=True)
1532+
with os.fdopen(fd, "w", encoding="utf-8") as f:
1533+
f.write(ps_body)
1534+
cleanup_script = script_path
1535+
command_to_run = [prefix, "-NoProfile", "-File", script_path]
1536+
run_kwargs["shell"] = False
1537+
else:
1538+
# Fall back to the generic .cmd approach if parsing failed.
1539+
fd, script_path = tempfile.mkstemp(suffix=".cmd", text=True)
1540+
with os.fdopen(fd, "w", encoding="utf-8") as f:
1541+
f.write(cmd)
1542+
cleanup_script = script_path
1543+
command_to_run = ["cmd", "/c", script_path]
1544+
run_kwargs["shell"] = False
1545+
else:
1546+
# Generic long-command fallback: write to .cmd and execute it.
1547+
fd, script_path = tempfile.mkstemp(suffix=".cmd", text=True)
1548+
with os.fdopen(fd, "w", encoding="utf-8") as f:
1549+
f.write(cmd)
1550+
cleanup_script = script_path
1551+
command_to_run = ["cmd", "/c", script_path]
1552+
run_kwargs["shell"] = False
1553+
1554+
completed = subprocess.run(command_to_run, **run_kwargs)
14831555
code = completed.returncode
14841556
out = completed.stdout or ""
14851557
err = completed.stderr or ""
14861558
except OSError as exc:
14871559
raise ASMRuntimeError(f"CL failed: {exc}", location=location, rewrite_rule="CL")
1560+
finally:
1561+
if cleanup_script:
1562+
try:
1563+
os.unlink(cleanup_script)
1564+
except OSError:
1565+
pass
14881566

14891567
# Record the CL event for deterministic logging/replay, including captured output.
14901568
interpreter.io_log.append({"event": "CL", "cmd": cmd, "code": code, "stdout": out, "stderr": err})
@@ -1662,6 +1740,16 @@ def _execute_statement(self, statement: Statement, env: Environment) -> None:
16621740
location=statement.location,
16631741
rewrite_rule="ASSIGN",
16641742
)
1743+
1744+
# Respect identifier freezing: element assignment is still a form of reassignment.
1745+
env_found = env._find_env(base_expr.name)
1746+
if env_found is not None and (base_expr.name in env_found.frozen or base_expr.name in env_found.permafrozen):
1747+
raise ASMRuntimeError(
1748+
f"Identifier '{base_expr.name}' is frozen and cannot be reassigned",
1749+
location=statement.location,
1750+
rewrite_rule="ASSIGN",
1751+
)
1752+
16651753
base_val = self._evaluate_expression(base_expr, env)
16661754
if base_val.type != TYPE_TNS:
16671755
raise ASMRuntimeError(
@@ -1672,8 +1760,7 @@ def _execute_statement(self, statement: Statement, env: Environment) -> None:
16721760
assert isinstance(base_val.value, Tensor)
16731761
indices = [self._expect_int(self._evaluate_expression(node, env), "ASSIGN", statement.location) for node in index_nodes]
16741762
new_value = self._evaluate_expression(statement.value, env)
1675-
updated = self._set_tensor_value(base_val.value, indices, new_value, statement.location)
1676-
env.set(base_expr.name, Value(TYPE_TNS, updated))
1763+
self._mutate_tensor_value(base_val.value, indices, new_value, statement.location)
16771764
return
16781765
if isinstance(statement, ExpressionStatement):
16791766
self._evaluate_expression(statement.expression, env)
@@ -1820,7 +1907,7 @@ def _tensor_total_size(self, shape: List[int]) -> int:
18201907
return size
18211908

18221909
def _tensor_truthy(self, tensor: Tensor) -> bool:
1823-
for item in tensor.data:
1910+
for item in tensor.data.flat:
18241911
if item.type == TYPE_INT and item.value != 0:
18251912
return True
18261913
if item.type == TYPE_STR and item.value != "":
@@ -1841,7 +1928,7 @@ def _values_equal(self, left: Value, right: Value) -> bool:
18411928
def _tensor_equal(self, left: Tensor, right: Tensor) -> bool:
18421929
if left.shape != right.shape:
18431930
return False
1844-
return all(self._values_equal(a, b) for a, b in zip(left.data, right.data))
1931+
return all(self._values_equal(a, b) for a, b in zip(left.data.flat, right.data.flat))
18451932

18461933
def _validate_tensor_shape(self, shape: List[int], rule: str, location: SourceLocation) -> None:
18471934
if not shape:
@@ -1877,17 +1964,25 @@ def _clone(val: Value) -> Value:
18771964
return Value(TYPE_TNS, val.value)
18781965
return Value(val.type, val.value)
18791966

1880-
return Tensor(shape=list(shape), data=[_clone(fill_value) for _ in range(total)])
1967+
data = np.array([_clone(fill_value) for _ in range(total)], dtype=object)
1968+
return Tensor(shape=list(shape), data=data)
18811969

18821970
def _set_tensor_value(self, tensor: Tensor, indices: List[int], value: Value, location: SourceLocation) -> Tensor:
18831971
offset = self._tensor_flat_index(tensor, indices, "ASSIGN", location)
18841972
current = tensor.data[offset]
18851973
if current.type != value.type:
18861974
raise ASMRuntimeError("Tensor element type mismatch", location=location, rewrite_rule="ASSIGN")
1887-
new_data = list(tensor.data)
1975+
new_data = tensor.data.copy()
18881976
new_data[offset] = value
18891977
return Tensor(shape=list(tensor.shape), data=new_data)
18901978

1979+
def _mutate_tensor_value(self, tensor: Tensor, indices: List[int], value: Value, location: SourceLocation) -> None:
1980+
offset = self._tensor_flat_index(tensor, indices, "ASSIGN", location)
1981+
current = tensor.data[offset]
1982+
if current.type != value.type:
1983+
raise ASMRuntimeError("Tensor element type mismatch", location=location, rewrite_rule="ASSIGN")
1984+
tensor.data[offset] = value
1985+
18911986
def _build_tensor_from_literal(self, literal: TensorLiteral, env: Environment) -> Tensor:
18921987
items = literal.items
18931988
if not items:
@@ -1901,7 +1996,7 @@ def _build_tensor_from_literal(self, literal: TensorLiteral, env: Environment) -
19011996
subshape = nested.shape
19021997
elif subshape != nested.shape:
19031998
raise ASMRuntimeError("Inconsistent tensor shape", location=item.location, rewrite_rule="TNS")
1904-
flat.extend(nested.data)
1999+
flat.extend(list(nested.data.flat))
19052000
else:
19062001
val = self._evaluate_expression(item, env)
19072002
if subshape is None:
@@ -1914,7 +2009,7 @@ def _build_tensor_from_literal(self, literal: TensorLiteral, env: Environment) -
19142009
expected = self._tensor_total_size(shape)
19152010
if len(flat) != expected:
19162011
raise ASMRuntimeError("Tensor literal size mismatch", location=literal.location, rewrite_rule="TNS")
1917-
return Tensor(shape=shape, data=flat)
2012+
return Tensor(shape=shape, data=np.array(flat, dtype=object))
19182013

19192014
def _gather_index_chain(self, expr: Expression) -> Tuple[Expression, List[Expression]]:
19202015
indices: List[Expression] = []

0 commit comments

Comments
 (0)