Skip to content

Commit 6c7f69b

Browse files
Add language extensions
1 parent 6daced5 commit 6c7f69b

File tree

7 files changed

+645
-105
lines changed

7 files changed

+645
-105
lines changed

.jekyllignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
!CONTRIBUTING.html
99
!SPECIFICATION.html
1010
!UNLICENSE.html
11+
!icon.png

SPECIFICATION.html

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,24 @@
241241
242242
This section documents how the reference interpreter accepts program input and how to enable verbose tracebacks that include environment snapshots.
243243
244-
- Invocation arguments: the interpreter reads its first program argument (that is, `argv[1]`) as the program source. If the `-source` flag is not present, `argv[1]` is interpreted as a path to a source file and the interpreter loads and parses that file. If the `-source` flag is present, `argv[1]` is treated as the source text itself (a string containing the program) and is parsed directly without reading a file.
244+
- Program argument: the interpreter reads a single *program* argument as its input program. If the `-source` flag is not present, the program argument is interpreted as a path to a source file and the interpreter loads and parses that file. If the `-source` flag is present, the program argument is treated as the source text itself (a string containing the program) and is parsed directly without reading a file.
245+
246+
- Extensions: the interpreter may also accept zero or more *extension* arguments that load Python extension modules before parsing and execution. Extensions may add new operators, new runtime types, and runtime hooks (including custom REPL implementations), but MUST NOT replace or modify existing built-in operators or types.
247+
- A Python extension is a `.py` file that defines `ASM_LANG_EXTENSION_API_VERSION = 1` (optional; defaults to 1) and a callable `asm_lang_register(ext)` entrypoint.
248+
- A pointer file is a `.asmx` text file containing one extension path per line. Lines are trimmed; blank lines are ignored; lines beginning with `#` are comments. Relative paths are resolved relative to the `.asmx` file's directory.
249+
- If a `.asmx` file is supplied as an argument, all of the linked extensions are loaded.
250+
- Extensions are loaded before parsing so that extension-defined type names are recognized in typed assignments and function signatures.
251+
- If the only supplied positional inputs are extensions (and no program is supplied), the interpreter runs the REPL with the loaded extensions.
252+
- Hook surfaces exposed by the reference implementation include:
253+
- Operators: additional call names dispatched like built-ins.
254+
- Per-N-steps rules: `every_n_steps(N, handler)` runs the handler after state-log step indices where `step_index % N == 0`.
255+
- Event-bound rules: `on_event(name, handler)` for the following event names:
256+
- `program_start(interpreter, program, env)`
257+
- `program_end(interpreter, exit_code)`
258+
- `on_error(interpreter, error)`
259+
- `before_statement(interpreter, stmt, env)` / `after_statement(interpreter, stmt, env)`
260+
- `before_call(interpreter, name, args, env, location)` / `after_call(interpreter, name, result, env, location)`
261+
- REPL: extensions may provide a replacement REPL implementation.
245262
246263
- Verbose tracebacks: if the `-verbose` flag is supplied on the command line, tracebacks include the environment snapshots described in Section 10.8. In concise traceback mode the `-verbose` flag causes the interpreter to attach an `env_snapshot` entry to each frame shown; in verbose traceback mode the same flag expands the printed `State snapshot: blocks to include the selected local environment and any small set of globals included by policy. The snapshot contents follow the requirements in Section 10.2 and are suitable for deterministic replay.
247264

@@ -251,14 +268,16 @@
251268
Example invocations (illustrative):
252269
- File mode: `asm-lang program.asmln`
253270
- Source-string mode: `asm-lang -source "foo = INPUT\nPRINT(foo)" -verbose`
271+
- With extensions (file mode): `asm-lang myext.py program.asmln`
272+
- With pointer file: `asm-lang extensions.asmx program.asmln`
254273
255-
- REPL / Interactive mode: `asm-lang` (no program argument)
274+
- REPL / Interactive mode: `asm-lang` (no program argument), or `asm-lang myext.py` (extensions only)
256275
257276
## 11. REPL (Interactive Mode)
258277
259278
When the interpreter is invoked without a program path or a `-source` string argument it enters an interactive read–eval–print loop (REPL). The REPL is a convenient development and exploration environment that executes ASM-Lang statements using the same parser, runtime, built-ins, and state-logging semantics as file-mode execution. The following rules describe REPL behaviour:
260279
261-
- Invocation: running `interpreter` with no positional `program` argument launches the REPL. The `-verbose` and `--traceback-json` flags keep the same meanings in the REPL as in batch mode.
280+
- Invocation: running `asm-lang` with no program argument launches the REPL. If one or more extensions are supplied and no program argument is supplied, the interpreter launches a REPL with those extensions loaded; if an extension provides a replacement REPL, that REPL may be used.
262281
- Prompting and input: the REPL presents a primary prompt for new top-level input and a continuation prompt while the user is entering a multi-line block (for example, the body of `FUNC`, `IF`, `WHILE`, or `FOR`).
263282
- Single-line execution: when the user enters a single complete top-level statement (for example `x = 1010` or `PRINT(x)`), the REPL parses and executes that statement immediately and prints any side-effect output. This means `EXIT()` typed as a single-line statement will terminate the REPL immediately and return the supplied exit code (or `0` if omitted), identical to the behaviour when `EXIT()` is executed in a file.
264283
- Multi-line buffering: if a statement begins a block (for example a `FUNC` definition or an `IF(...){` that spans multiple lines), the REPL buffers lines until the block is complete (a blank line may be used to indicate end-of-entry when appropriate). When the buffer is complete the REPL parses and executes the collected statements as a unit.
@@ -275,7 +294,7 @@
275294
276295
This section lists built-in functions, operators and statement signatures, along with a (somewhat) brief description of their behaviour.
277296
278-
Type notation: union signatures such as `INT|STR` restrict arguments to the listed types. `ANY` now means `INT`, `STR`, or `TNS` unless explicitly narrowed below.
297+
Type notation: union signatures such as `INT|STR` restrict arguments to the listed types. `ANY` means "any runtime value"; in the base language this includes `INT`, `STR`, and `TNS`, and when extensions are enabled it also includes extension-defined types (unless a signature explicitly narrows the set).
279298
280299
### Expression syntax
281300

asm-lang.exe

14.6 KB
Binary file not shown.

asm-lang.py

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@
55
import sys
66
from typing import List, Optional
77

8+
from extensions import ASMExtensionError, ReplContext, load_runtime_services
89
from interpreter import ASMRuntimeError, Environment, ExitSignal, Interpreter, TracebackFormatter
910
from lexer import ASMParseError, Lexer
1011
from parser import Parser, Statement
1112

1213

13-
def _parse_statements_from_source(text: str, filename: str) -> List[Statement]:
14+
def _parse_statements_from_source(text: str, filename: str, *, type_names: Optional[set[str]] = None) -> List[Statement]:
1415
lexer = Lexer(text, filename)
1516
tokens = lexer.tokenize()
16-
parser = Parser(tokens, filename, text.splitlines())
17+
parser = Parser(tokens, filename, text.splitlines(), type_names=type_names)
1718
program = parser.parse()
1819
return program.statements
1920

2021

21-
def run_repl(verbose: bool) -> int:
22+
def run_repl(*, verbose: bool, services) -> int:
2223
print("\x1b[38;2;153;221;255mASM-Lang\033[0m REPL. Enter statements, blank line to run buffer.") # "ASM-Lang" in light blue
2324
# Use "<string>" as the REPL's effective source filename so that MAIN() and imports behave
2425
had_output = False
@@ -27,7 +28,35 @@ def _output_sink(text: str) -> None:
2728
had_output = True
2829
print(text, end="")
2930

30-
interpreter = Interpreter(source="", filename="<string>", verbose=verbose, input_provider=(lambda: input()), output_sink=_output_sink)
31+
picked = services.hook_registry.pick_repl() if services is not None else None
32+
if picked is not None:
33+
_name, runner, _ext = picked
34+
ctx = ReplContext(
35+
verbose=verbose,
36+
services=services,
37+
make_interpreter=lambda source, filename: Interpreter(
38+
source=source,
39+
filename=filename,
40+
verbose=verbose,
41+
services=services,
42+
input_provider=(lambda: input()),
43+
output_sink=_output_sink,
44+
),
45+
)
46+
return runner(ctx)
47+
48+
try:
49+
interpreter = Interpreter(
50+
source="",
51+
filename="<string>",
52+
verbose=verbose,
53+
services=services,
54+
input_provider=(lambda: input()),
55+
output_sink=_output_sink,
56+
)
57+
except ASMExtensionError as exc:
58+
print(f"ExtensionError: {exc}", file=sys.stderr)
59+
return 1
3160
global_env = Environment()
3261
# Make the REPL top-level frame mimic script top-level frame
3362
global_frame = interpreter._new_frame("<top-level>", global_env, None)
@@ -58,7 +87,7 @@ def _output_sink(text: str) -> None:
5887

5988
if not buffer and stripped != "" and not is_block_start:
6089
try:
61-
statements = _parse_statements_from_source(line, "<string>")
90+
statements = _parse_statements_from_source(line, "<string>", type_names=interpreter.type_registry.names())
6291
try:
6392
interpreter._execute_block(statements, global_env)
6493
except ExitSignal as sig:
@@ -72,7 +101,7 @@ def _output_sink(text: str) -> None:
72101
source_text = "\n".join(buffer)
73102
buffer.clear()
74103
try:
75-
statements = _parse_statements_from_source(source_text, "<string>")
104+
statements = _parse_statements_from_source(source_text, "<string>", type_names=interpreter.type_registry.names())
76105
interpreter._execute_block(statements, global_env)
77106
except ExitSignal as sig:
78107
return sig.code
@@ -92,31 +121,60 @@ def _output_sink(text: str) -> None:
92121

93122
def run_cli(argv: Optional[List[str]] = None) -> int:
94123
parser = argparse.ArgumentParser(description="ASM-Lang reference interpreter")
95-
parser.add_argument("program", nargs="?", help="Source file path or literal source with -source")
124+
parser.add_argument("inputs", nargs="*", help="Program path/source and/or extension files (.py/.asmx)")
125+
parser.add_argument("--ext", action="append", default=[], help="Extension path (.py) or pointer file (.asmx)")
96126
parser.add_argument("-source", "--source", dest="source_mode", action="store_true", help="Treat program argument as literal source text")
97127
parser.add_argument("-verbose", "--verbose", dest="verbose", action="store_true", help="Emit env snapshots in tracebacks")
98128
parser.add_argument("--traceback-json", action="store_true", help="Also emit JSON traceback")
99129
args = parser.parse_args(argv)
100130

101-
if args.program is None:
102-
if args.source_mode:
131+
inputs: List[str] = list(args.inputs or [])
132+
ext_paths: List[str] = list(args.ext or [])
133+
remaining: List[str] = []
134+
for item in inputs:
135+
lower = item.lower()
136+
if lower.endswith(".py") or lower.endswith(".asmx"):
137+
ext_paths.append(item)
138+
else:
139+
remaining.append(item)
140+
141+
try:
142+
services = load_runtime_services(ext_paths) if ext_paths else load_runtime_services([])
143+
except ASMExtensionError as exc:
144+
print(f"ExtensionError: {exc}", file=sys.stderr)
145+
return 1
146+
147+
program: Optional[str] = None
148+
if remaining:
149+
if len(remaining) > 1:
150+
print("Too many non-extension inputs; expected a single program argument", file=sys.stderr)
151+
return 1
152+
program = remaining[0]
153+
154+
if program is None:
155+
if args.source_mode and not ext_paths:
103156
print("-source requires a program string", file=sys.stderr)
104157
return 1
105-
return run_repl(verbose=args.verbose)
158+
# If only extensions are present, run REPL with the loaded extensions.
159+
return run_repl(verbose=args.verbose, services=services)
106160

107161
if args.source_mode:
108-
source_text = args.program
162+
source_text = program
109163
filename = "<string>"
110164
else:
111-
filename = args.program
165+
filename = program
112166
try:
113167
with open(filename, "r", encoding="utf-8") as handle:
114168
source_text = handle.read()
115169
except OSError as exc:
116170
print(f"Failed to read {filename}: {exc}", file=sys.stderr)
117171
return 1
118172

119-
interpreter = Interpreter(source=source_text, filename=filename, verbose=args.verbose)
173+
try:
174+
interpreter = Interpreter(source=source_text, filename=filename, verbose=args.verbose, services=services)
175+
except ASMExtensionError as exc:
176+
print(f"ExtensionError: {exc}", file=sys.stderr)
177+
return 1
120178
try:
121179
interpreter.run()
122180
except ASMParseError as error:

0 commit comments

Comments
 (0)