Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
pr_sha: ${{ github.event.pull_request.head.sha }}

package-tests:
timeout-minutes: 10
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -79,7 +79,7 @@ jobs:
run: makim ${{ matrix.project }}.unittests

arx-language-tests:
timeout-minutes: 10
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -131,7 +131,7 @@ jobs:

linter:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 15

defaults:
run:
Expand Down
60 changes: 60 additions & 0 deletions docs/library/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,66 @@ Use absolute dotted paths for public imports across package boundaries:
import circle_area from geometry.shapes.area
```

## Installed Package Imports

Arx can also resolve absolute imports from installed Arx source packages
declared in `[project].dependencies` in `.arxproject.toml`.

For example, if the current project declares an installed dependency that
exposes a package root named `local_lib`, this import resolves from that
installed package when no local project source shadows it:

```arx
import sum2 from local_lib.stats
```

Installed package lookup is scoped to the current Python environment running
`arx`. The compiler reads installed distribution metadata, follows transitive
dependency metadata, and uses packaged `.arxproject.toml` files to find Arx
source roots. It does not install packages, access the network, or change
generated IR/codegen behavior.

For installed packages with the normal project layout, place `.arxproject.toml`
at the packaged project root and declare both `[build].src_dir` and
`[build].package`:

```text
site-packages/local_lib_project/
├── .arxproject.toml
└── src
└── local_lib
├── __init__.x
└── stats.x
```

```toml
[build]
src_dir = "src"
package = "local_lib"
```

With that layout, `local_lib.stats` resolves to
`site-packages/local_lib_project/src/local_lib/stats.x`.

Flat package layouts are also supported for packages that place
`.arxproject.toml` and `.x` files directly in the package directory:

```text
site-packages/local_lib/
├── .arxproject.toml
├── __init__.x
└── stats.x
```

Resolution precedence is:

1. local/current project source files
2. installed Arx dependency packages
3. unresolved import error

The reserved `stdlib` and internal `builtins` namespaces always remain owned by
the compiler and cannot be replaced by installed packages.

## Bundled `stdlib`

Arx ships a first-party standard library namespace called `stdlib`.
Expand Down
1 change: 1 addition & 0 deletions packages/arx/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies = [
"astx == 1.21.0", # semantic-release
"pyirx == 1.21.0", # semantic-release
"jsonschema (>=4.0.0)",
"packaging >=23",
"types-pyyaml (>=6.0.12.20250516)",
"tomli >=2.0.0 ; python_version < \"3.11\""
]
Expand Down
112 changes: 112 additions & 0 deletions packages/arx/src/arx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from irx.analysis.module_interfaces import ParsedModule

from arx import builtins as arx_builtins
from arx import package_index
from arx import settings as arx_settings
from arx.codegen import ArxBuilder
from arx.io import ArxIO
Expand Down Expand Up @@ -260,11 +261,16 @@ class FileImportResolver:
type: dict[str, ParsedModule]
_source_root_cache:
type: dict[Path, Path | None]
_installed_package_index:
type: package_index.InstalledArxPackageIndex | None
"""

input_files: tuple[str, ...]
cache: dict[str, ParsedModule] = field(default_factory=dict)
_source_root_cache: dict[Path, Path | None] = field(default_factory=dict)
_installed_package_index: package_index.InstalledArxPackageIndex | None = (
None
)

def _project_source_root(self, directory: Path) -> Path | None:
"""
Expand Down Expand Up @@ -404,8 +410,114 @@ def _resolve_module_file(self, requested_specifier: str) -> Path:
return init_path
if has_file:
return file_path
installed_path = self._resolve_installed_module_file(
requested_specifier
)
if installed_path is not None:
return installed_path
raise LookupError(requested_specifier)

def _installed_package_start(self) -> Path:
"""
title: Return the manifest search start for installed dependencies.
returns:
type: Path
"""
if not self.input_files:
return Path.cwd()

input_root = Path(self.input_files[0]).resolve().parent
if arx_settings.find_config_file(start=input_root) is not None:
return input_root
return Path.cwd()

def _installed_packages(
self,
) -> package_index.InstalledArxPackageIndex:
"""
title: Lazily discover installed Arx package dependencies.
returns:
type: package_index.InstalledArxPackageIndex
"""
if self._installed_package_index is None:
self._installed_package_index = (
package_index.discover_installed_arx_packages(
start=self._installed_package_start()
)
)
return self._installed_package_index

def _resolve_installed_module_file(
self,
requested_specifier: str,
) -> Path | None:
"""
title: Resolve one module specifier from installed Arx packages.
parameters:
requested_specifier:
type: str
returns:
type: Path | None
"""
specifier_parts = tuple(
part for part in requested_specifier.split(".") if part
)
if not specifier_parts:
return None

index = self._installed_packages()
package_name = specifier_parts[0]
conflicts = index.conflicts.get(package_name)
if conflicts is not None:
locations = ", ".join(
f"{package.distribution_name} at {package.source_root}"
for package in conflicts
)
raise LookupError(
"ambiguous installed Arx package module "
f"'{package_name}': provided by {locations}"
)

package = index.packages.get(package_name)
if package is None:
missing_distribution = index.missing_distribution_for_module(
package_name
)
if missing_distribution is None:
return None
raise LookupError(
"declared Arx dependency "
f"'{missing_distribution}' is not installed in the "
"current Python environment"
)

if len(specifier_parts) == 1:
init_path = (package.source_root / "__init__.x").resolve()
if init_path.is_file():
return init_path
return None

relative_path = Path(*specifier_parts[1:])
file_path = (
(package.source_root / relative_path).with_suffix(".x").resolve()
)
init_path = (
package.source_root / relative_path / "__init__.x"
).resolve()
has_init = init_path.is_file()
has_file = file_path.is_file()
if has_init and has_file:
raise LookupError(
"ambiguous module specifier "
f"'{requested_specifier}': both "
f"'{file_path}' and '{init_path}' exist"
)
if has_init:
return init_path
if has_file:
return file_path
return None

def _shadowing_reserved_path(
self,
requested_specifier: str,
Expand Down
Loading
Loading