Skip to content

Commit 7386677

Browse files
Enhance IMPORT functionality to support optional aliasing and improve extension loading mechanism
1 parent 6c7f69b commit 7386677

File tree

4 files changed

+70
-6
lines changed

4 files changed

+70
-6
lines changed

SPECIFICATION.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@
247247
- 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.
248248
- 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.
249249
- If a `.asmx` file is supplied as an argument, all of the linked extensions are loaded.
250+
- If no explicit extension arguments are provided, the interpreter will automatically look for a pointer file named `.asmx` in the current working directory and, when a program path is being executed (not when `-source` is used), in the program's directory; if found, the extensions listed in that pointer file are loaded as if supplied on the command line.
250251
- Extensions are loaded before parsing so that extension-defined type names are recognized in typed assignments and function signatures.
251252
- If the only supplied positional inputs are extensions (and no program is supplied), the interpreter runs the REPL with the loaded extensions.
252253
- Hook surfaces exposed by the reference implementation include:
@@ -392,13 +393,13 @@
392393
- `CLOG(INT: a):INT` ; ceil(log2(a)) for a > 0
393394
394395
### Module operations:
395-
- `IMPORT(MODULE: name)` — Loads another source file and provides it as a distinct module namespace.
396+
- `IMPORT(MODULE: name)` or `IMPORT(MODULE: name, SYMBOL: alias)` — Loads another source file and exposes it as a distinct module namespace. When an optional alias identifier is supplied, the imported module's bindings are exposed under the `alias` prefix rather than the module's own name (for example, `IMPORT(mod, ali)` makes `ali.F()` valid while `mod.F()` is not).
396397
397-
The argument must be an identifier naming a module; the interpreter first looks for a file named `<name>.asmln` in the same directory as the referring source file. When the referring source is provided via the `-source` string literal mode, the primary search directory is the process's current working directory. If the module file is not found there, the interpreter will additionally attempt to load the file from a `lib` subdirectory located alongside the interpreter implementation (that is, `<interpreter_dir>/lib/<name>.asmln`, where `<interpreter_dir>` is the directory containing the interpreter script or executable).
398+
The first argument must be an identifier naming a module; the interpreter first looks for a file named `<name>.asmln` in the same directory as the referring source file. When the referring source is provided via the `-source` string literal mode, the primary search directory is the process's current working directory. If the module file is not found there, the interpreter will additionally attempt to load the file from a `lib` subdirectory located alongside the interpreter implementation (that is, `<interpreter_dir>/lib/<name>.asmln`, where `<interpreter_dir>` is the directory containing the interpreter script or executable).
398399
399400
The imported file is parsed and executed in its own isolated top-level environment on the first import during a given interpreter invocation: top-level assignments and function definitions in the imported module do not directly mutate the caller's environment during execution. During that execution unqualified identifiers (for example, `x` or `helper`) refer to names in the module's own top-level namespace. Qualified identifiers (for example, `other.FOO`) refer only to the dotted names that the module itself has created or imported; those qualified bindings are scoped to the module's namespace.
400401
401-
After the module finishes executing the first time, the interpreter caches the module's top-level environment and the module-qualified function objects. Subsequent `IMPORT` calls for the same module identifier within the same interpreter process reuse that cached namespace/instance and do not re-execute the module source. Callers importing the same module later will observe the same shared module environment (that is, the same binding objects and the same function objects) exposed under the dotted names (`module.FOO`, `module.bar`, etc.). If the module imported other modules during its execution, those nested qualified bindings are preserved in the cached namespace and remain accessible via the same dotted paths (for example, `module.other.SYM`).
402+
After the module finishes executing the first time, the interpreter caches the module's top-level environment and the module-qualified function objects. Subsequent `IMPORT` calls for the same module identifier within the same interpreter process reuse that cached namespace/instance and do not re-execute the module source. Callers importing the same module later will observe the same shared module environment (that is, the same binding objects and the same function objects). By default bindings are exposed under the module's own dotted prefix (`module.FOO`, `module.bar`, etc.); however, if the importer supplied an alias the bindings are instead exposed under the alias prefix (`alias.FOO`, `alias.bar`). Multiple different aliases for the same module identifier will each get their own dotted view into the same cached module instance. If the module imported other modules during its execution, those nested qualified bindings are preserved in the cached namespace and remain accessible via the same dotted paths (for example, `module.other.SYM` or `alias.other.SYM` or `module.alias.SYM`).
402403
403404
This caching behavior ensures that importing a module multiple times produces the same shared module namespace instance for all importers. The interpreter does not automatically perform cycle detection beyond using the cached instance once execution has completed; careful module design should avoid import cycles where possible.
404405
- `EXPORT(SYMBOL: symbol, MODULE: module):INT` Adds the caller's binding for the identifier `symbol` into the namespace of the imported module named by the identifier `module`. The first argument must be an identifier (not a string literal); its current value in the caller's environment is copied into the imported module's namespace and becomes available as the qualified name `module.symbol` in the caller's environment. The second argument must be an identifier naming a previously-imported module; if the module has not been imported yet, the interpreter raises a runtime error (rewrite: EXPORT). `EXPORT` returns `INT` 0 on success.

asm-lang.exe

1.29 KB
Binary file not shown.

asm-lang.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44
import argparse
55
import sys
6+
import os
67
from typing import List, Optional
78

89
from extensions import ASMExtensionError, ReplContext, load_runtime_services
@@ -138,6 +139,26 @@ def run_cli(argv: Optional[List[str]] = None) -> int:
138139
else:
139140
remaining.append(item)
140141

142+
# Initialize `program` early so subsequent checks can reference it safely.
143+
program: Optional[str] = remaining[0] if remaining else None
144+
145+
# If the caller didn't specify any extensions, look for a pointer file named
146+
# ".asmx" in the current working directory or (when a program file was
147+
# provided) in the program's directory. If found, use it as the extension
148+
# pointer file so the interpreter loads the extensions it points to.
149+
if not ext_paths:
150+
cwd_asmx = os.path.abspath(".asmx")
151+
if os.path.exists(cwd_asmx):
152+
ext_paths.append(cwd_asmx)
153+
else:
154+
# If a program path was given (and isn't literal source text),
155+
# also check the program's directory for a .asmx pointer file.
156+
if program and not args.source_mode:
157+
program_dir = os.path.dirname(os.path.abspath(program))
158+
program_asmx = os.path.join(program_dir, ".asmx")
159+
if os.path.exists(program_asmx):
160+
ext_paths.append(program_asmx)
161+
141162
try:
142163
services = load_runtime_services(ext_paths) if ext_paths else load_runtime_services([])
143164
except ASMExtensionError as exc:

interpreter.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def __init__(self) -> None:
365365
self._register_custom("REPLACE", 3, 3, self._replace)
366366
self._register_custom("MAIN", 0, 0, self._main)
367367
self._register_custom("OS", 0, 0, self._os)
368-
self._register_custom("IMPORT", 1, 1, self._import)
368+
self._register_custom("IMPORT", 1, 2, self._import)
369369
self._register_custom("RUN", 1, 1, self._run)
370370
self._register_custom("INPUT", 0, 1, self._input)
371371
self._register_custom("PRINT", 0, None, self._print)
@@ -900,10 +900,17 @@ def _import(
900900
env: Environment,
901901
location: SourceLocation,
902902
) -> Value:
903-
if len(arg_nodes) != 1 or not isinstance(arg_nodes[0], Identifier):
903+
# Accept either IMPORT(module) or IMPORT(module, alias)
904+
if len(arg_nodes) not in (1, 2) or not isinstance(arg_nodes[0], Identifier):
904905
raise ASMRuntimeError("IMPORT expects module name identifier", location=location, rewrite_rule="IMPORT")
905906

906907
module_name = arg_nodes[0].name
908+
if len(arg_nodes) == 2:
909+
if not isinstance(arg_nodes[1], Identifier):
910+
raise ASMRuntimeError("IMPORT alias must be an identifier", location=location, rewrite_rule="IMPORT")
911+
export_prefix = arg_nodes[1].name
912+
else:
913+
export_prefix = module_name
907914
# If module was already imported earlier in this interpreter instance,
908915
# reuse the same Environment and function objects so all importers
909916
# observe the same namespace/instance.
@@ -913,9 +920,25 @@ def _import(
913920
for fn in interpreter.module_functions.get(module_name, []):
914921
if fn.name not in interpreter.functions:
915922
interpreter.functions[fn.name] = fn
923+
# Also register alias-qualified function names when an alias was requested
924+
if export_prefix != module_name:
925+
# fn.name is module_name.func; produce alias.func
926+
parts = fn.name.split(".", 1)
927+
if len(parts) == 2:
928+
_, unqualified = parts
929+
alias_name = f"{export_prefix}.{unqualified}"
930+
if alias_name not in interpreter.functions:
931+
created = Function(
932+
name=alias_name,
933+
params=fn.params,
934+
return_type=fn.return_type,
935+
body=fn.body,
936+
closure=cached_env,
937+
)
938+
interpreter.functions[alias_name] = created
916939

917940
for k, v in cached_env.values.items():
918-
dotted = f"{module_name}.{k}"
941+
dotted = f"{export_prefix}.{k}"
919942
env.set(dotted, v, declared_type=v.type)
920943
return Value(TYPE_INT, 0)
921944

@@ -981,6 +1004,25 @@ def _import(
9811004
interpreter.functions[dotted_name] = created
9821005
registered_functions.append(created)
9831006

1007+
# If the caller requested an alias different from the module name,
1008+
# also register alias-qualified function entries pointing to the
1009+
# same function bodies so callers can invoke e.g. ALIAS.F().
1010+
if export_prefix != module_name:
1011+
for fn in registered_functions:
1012+
parts = fn.name.split(".", 1)
1013+
if len(parts) == 2:
1014+
_, unqualified = parts
1015+
alias_name = f"{export_prefix}.{unqualified}"
1016+
if alias_name not in interpreter.functions:
1017+
alias_fn = Function(
1018+
name=alias_name,
1019+
params=fn.params,
1020+
return_type=fn.return_type,
1021+
body=fn.body,
1022+
closure=module_env,
1023+
)
1024+
interpreter.functions[alias_name] = alias_fn
1025+
9841026
# Store module env and functions into the interpreter cache for reuse
9851027
interpreter.module_cache[module_name] = module_env
9861028
interpreter.module_functions[module_name] = registered_functions

0 commit comments

Comments
 (0)