Skip to content

Commit afdbe75

Browse files
gh-22: add package support.
1 parent d61fd24 commit afdbe75

File tree

10 files changed

+139
-21
lines changed

10 files changed

+139
-21
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# Ignore Python bytecode cache directories
22
__pycache__/
3-
ext/__pycache__/
43
# Ignore MyPy cache directory
54
.mypy_cache/

asm-lang.exe

3.12 KB
Binary file not shown.

docs/SPECIFICATION.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,8 @@
774774
775775
- `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).
776776
777+
Package imports: ASM-Lang supports package namespaces using `..` as the package separator. The canonical form is `package..subpackage..module`. When `IMPORT(pkg)` is used the interpreter will prefer a package layout and attempt to load `pkg/init.asmln` from the referring source's directory (falling back to the interpreter `lib/` directory). If a directory named `pkg` exists but contains no `init.asmln` the import raises a clear error. When `IMPORT(pkg..mod)` is used the interpreter resolves to `pkg/mod.asmln` (or the corresponding file under `lib/`). When a package directory and a same-named module file both exist in the same search location, the package takes precedence. Items within the package are still imported using their full dotted names (for example, `pkg..mod.FOO`).
778+
777779
- `IMPORT_PATH(STR: path)` — Loads an ASM-Lang module from the absolute filesystem `path` provided as a `STR`. The argument must be an absolute path to a `.asmln` source file. The imported module is treated the same as `IMPORT` with respect to parsing, execution isolation, function/module caching, and companion extension loading (companion `<module>.asmxt` alongside the resolved file and a built-in `ext/<module>.py` are both checked and loaded if present). The module's basename (filename without extension) is used as the module identifier for qualifying exported bindings.
778780
779781
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).

interpreter.py

Lines changed: 137 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,6 +1707,121 @@ def _os(
17071707
fam = "unix"
17081708
return Value(TYPE_STR, fam)
17091709

1710+
def _resolve_module_path(
1711+
self,
1712+
module_name: str,
1713+
base_dir: str,
1714+
location: SourceLocation,
1715+
) -> Tuple[str, str]:
1716+
"""Resolve a module name to a filesystem path.
1717+
1718+
Supports package notation using '..' as the package separator:
1719+
- IMPORT(pkg) -> looks for pkg/init.asmln first, then pkg.asmln
1720+
- IMPORT(pkg..mod) -> looks for pkg/mod.asmln
1721+
- IMPORT(pkg..sub..mod) -> looks for pkg/sub/mod.asmln
1722+
1723+
When a package and module with the same name exist at the same location,
1724+
the package is preferred.
1725+
1726+
Returns a tuple of (resolved_path, source_text).
1727+
Raises ASMRuntimeError if the module cannot be found.
1728+
"""
1729+
interpreter_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
1730+
lib_dir = os.path.join(interpreter_dir, "lib")
1731+
1732+
# Parse the module name for package structure
1733+
# '..' is the package separator
1734+
if ".." in module_name:
1735+
# Package-qualified import: pkg..mod or pkg..sub..mod
1736+
parts = module_name.split("..")
1737+
# Build the path: pkg/sub/mod.asmln
1738+
relative_path = os.path.join(*parts[:-1], f"{parts[-1]}.asmln")
1739+
else:
1740+
# Simple module name - could be a module or a package
1741+
# First try as a package: look for pkg/init.asmln
1742+
pkg_init_path = os.path.join(base_dir, module_name, "init.asmln")
1743+
lib_pkg_init_path = os.path.join(lib_dir, module_name, "init.asmln")
1744+
1745+
# Check for package in base_dir
1746+
if os.path.isdir(os.path.join(base_dir, module_name)):
1747+
if os.path.exists(pkg_init_path):
1748+
try:
1749+
with open(pkg_init_path, "r", encoding="utf-8") as handle:
1750+
return pkg_init_path, handle.read()
1751+
except OSError:
1752+
pass
1753+
else:
1754+
raise ASMRuntimeError(
1755+
f"Package '{module_name}' exists but has no init.asmln",
1756+
location=location,
1757+
rewrite_rule="IMPORT"
1758+
)
1759+
1760+
# Check for package in lib_dir
1761+
if os.path.isdir(os.path.join(lib_dir, module_name)):
1762+
if os.path.exists(lib_pkg_init_path):
1763+
try:
1764+
with open(lib_pkg_init_path, "r", encoding="utf-8") as handle:
1765+
return lib_pkg_init_path, handle.read()
1766+
except OSError:
1767+
pass
1768+
else:
1769+
raise ASMRuntimeError(
1770+
f"Package '{module_name}' exists but has no init.asmln",
1771+
location=location,
1772+
rewrite_rule="IMPORT"
1773+
)
1774+
1775+
# Fall back to module file: pkg.asmln
1776+
relative_path = f"{module_name}.asmln"
1777+
1778+
# Try base_dir first
1779+
candidate = os.path.join(base_dir, relative_path)
1780+
if os.path.exists(candidate):
1781+
try:
1782+
with open(candidate, "r", encoding="utf-8") as handle:
1783+
return candidate, handle.read()
1784+
except OSError:
1785+
pass
1786+
1787+
# Try lib_dir
1788+
lib_candidate = os.path.join(lib_dir, relative_path)
1789+
if os.path.exists(lib_candidate):
1790+
try:
1791+
with open(lib_candidate, "r", encoding="utf-8") as handle:
1792+
return lib_candidate, handle.read()
1793+
except OSError:
1794+
pass
1795+
1796+
raise ASMRuntimeError(
1797+
f"Failed to import '{module_name}': module not found",
1798+
location=location,
1799+
rewrite_rule="IMPORT"
1800+
)
1801+
1802+
def _split_module_name(self, fn_name: str, module_name: str) -> str:
1803+
"""Split a function name from its module prefix.
1804+
1805+
For package-qualified modules (using '..'), we need to handle
1806+
the separator properly.
1807+
"""
1808+
# Module name may contain package separators ('..'). Regardless,
1809+
# function/symbol names are joined to the module using a single
1810+
# dot. Strip the module+dot prefix if present.
1811+
prefix = module_name + "."
1812+
if fn_name.startswith(prefix):
1813+
return fn_name[len(prefix):]
1814+
return fn_name
1815+
1816+
def _qualify_name(self, base: str, name: str) -> str:
1817+
"""Create a qualified name by joining base and name.
1818+
1819+
Uses '..' for package-qualified modules, '.' for simple modules.
1820+
"""
1821+
# Always use a single dot to separate a module (which may itself
1822+
# contain package '..' separators) from the symbol name.
1823+
return f"{base}.{name}"
1824+
17101825
def _import(
17111826
self,
17121827
interpreter: "Interpreter",
@@ -1727,6 +1842,12 @@ def _import(
17271842
export_prefix = arg_nodes[1].name
17281843
else:
17291844
export_prefix = module_name
1845+
1846+
# Use '.' to join a module name to its symbols; '..' remains
1847+
# the package separator inside module names for filesystem layout.
1848+
name_sep = "."
1849+
export_sep = "."
1850+
17301851
# If module was already imported earlier in this interpreter instance,
17311852
# reuse the same Environment and function objects so all importers
17321853
# observe the same namespace/instance.
@@ -1741,7 +1862,7 @@ def _import(
17411862
# fn.name is module_name.func; produce alias.func
17421863
if "." in fn.name:
17431864
unqualified = fn.name.split(".", 1)[1]
1744-
alias_name = f"{export_prefix}.{unqualified}"
1865+
alias_name = f"{export_prefix}{export_sep}{unqualified}"
17451866
if alias_name not in interpreter.functions:
17461867
created = Function(
17471868
name=alias_name,
@@ -1753,28 +1874,24 @@ def _import(
17531874
interpreter.functions[alias_name] = created
17541875

17551876
for k, v in cached_env.values.items():
1756-
dotted = f"{export_prefix}.{k}"
1877+
dotted = f"{export_prefix}{export_sep}{k}"
17571878
env.set(dotted, v, declared_type=v.type)
17581879

17591880
if set(interpreter.functions.keys()) != pre_function_keys:
17601881
interpreter._mark_functions_changed()
17611882
return Value(TYPE_INT, 0)
17621883

17631884
base_dir = os.getcwd() if location.file == "<repl>" else os.path.dirname(os.path.abspath(location.file))
1764-
module_path = os.path.join(base_dir, f"{module_name}.asmln")
1765-
1766-
try:
1767-
with open(module_path, "r", encoding="utf-8") as handle:
1768-
source_text = handle.read()
1769-
except OSError as exc:
1770-
interpreter_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
1771-
lib_module_path = os.path.join(interpreter_dir, "lib", f"{module_name}.asmln")
1772-
try:
1773-
with open(lib_module_path, "r", encoding="utf-8") as handle:
1774-
source_text = handle.read()
1775-
module_path = lib_module_path
1776-
except OSError:
1777-
raise ASMRuntimeError(f"Failed to import '{module_name}': {exc}", location=location, rewrite_rule="IMPORT")
1885+
1886+
# Use package-aware module resolution
1887+
module_path, source_text = self._resolve_module_path(module_name, base_dir, location)
1888+
1889+
# For extension loading, get the base name of the actual module file
1890+
# (handles package..module -> module.py extension lookup)
1891+
if ".." in module_name:
1892+
ext_module_name = module_name.split("..")[-1]
1893+
else:
1894+
ext_module_name = module_name
17781895

17791896
# --- IMPORT-time extension loading ---
17801897
# When importing a module, attempt to load any companion extensions so
@@ -1820,7 +1937,7 @@ def _import(
18201937
raise ASMExtensionError(f"Failed to load extensions from {companion_asmxt}: {exc}") from exc
18211938

18221939
# Next, check for a single-file built-in extension named <module>.py
1823-
builtin = _extmod._resolve_in_builtin_ext(f"{module_name}.py")
1940+
builtin = _extmod._resolve_in_builtin_ext(f"{ext_module_name}.py")
18241941
if builtin is not None and os.path.exists(builtin):
18251942
mod = _extmod.load_extension_module(builtin)
18261943
api_version = getattr(mod, "ASM_LANG_EXTENSION_API_VERSION", _extmod.EXTENSION_API_VERSION)
@@ -1875,7 +1992,7 @@ def _import(
18751992
new_funcs = {n: f for n, f in interpreter.functions.items() if n not in prev_functions}
18761993
registered_functions: List[Function] = []
18771994
for name, fn in new_funcs.items():
1878-
dotted_name = f"{module_name}.{name}"
1995+
dotted_name = f"{module_name}{name_sep}{name}"
18791996
if "." in name:
18801997
created = Function(
18811998
name=dotted_name,
@@ -1906,7 +2023,7 @@ def _import(
19062023
for fn in registered_functions:
19072024
if "." in fn.name:
19082025
unqualified = fn.name.split(".", 1)[1]
1909-
alias_name = f"{export_prefix}.{unqualified}"
2026+
alias_name = f"{export_prefix}{export_sep}{unqualified}"
19102027
if alias_name not in interpreter.functions:
19112028
alias_fn = Function(
19122029
name=alias_name,
@@ -1926,7 +2043,7 @@ def _import(
19262043

19272044
# Export top-level bindings from the module under the dotted namespace
19282045
for k, v in module_env.values.items():
1929-
dotted = f"{module_name}.{k}"
2046+
dotted = f"{module_name}{name_sep}{k}"
19302047
env.set(dotted, v, declared_type=v.type)
19312048
return Value(TYPE_INT, 0)
19322049

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)