fix(ae): binary-import prepass walks the full import graph (transitive imports) - #2106
Merged
Merged
Conversation
paul-hammant
added a commit
that referenced
this pull request
Sep 19, 2026
#2106's graph-walk followed only FLAT imports. In ae_scan_binary_imports the per-import line did `if (mi == 0 || *p == '.') continue;` -- a dotted import (import a.b.c) hit `*p == '.'` after reading the first segment and was skipped whole, so the walk never resolved the dotted module to a file and never recursed into it. A binary import nested inside a dotted-path module was therefore never discovered: import foo.validate (validate holds import phonenumber_ae) -> "unresolved import 'phonenumber_ae'". This is dsa's real shape -- import harness.components.phone.validate -- and dotted package imports are exactly what the modules="." root export exists to support. Read the whole module token including dots; if it is dotted, convert a.b.c -> a/b/c and resolve it with ae_source_module_path (same path form the source resolver uses: a/b/c.ae or a/b/c/module.ae across . / src / --lib dirs), then recurse into that file. The dotted NAME is still never a binary-import candidate itself (a binary import is always a bare name); only the recursion changes. A dotted std./contrib. import that does not resolve to a local source file simply no-ops, so nothing regresses. Verified against the peer's A/B: A (import foo.validate, dotted) failed before, resolves now; B (import validate, flat) unchanged; a 4-deep dotted chain (harness.components.phone.validate -> engine) resolves; import std.string still builds. Reverting only tools/ae.c makes the new dotted test step fail. Leak-clean. Test: binary_import gains a dotted-transitive step -- pkg/dotwrap.ae reached as import pkg.dotwrap, holding import gizmo; asserts ae run resolves it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
paul-hammant
added a commit
that referenced
this pull request
Sep 19, 2026
#2106's graph-walk followed only FLAT imports. In ae_scan_binary_imports the per-import line did `if (mi == 0 || *p == '.') continue;` -- a dotted import (import a.b.c) hit `*p == '.'` after reading the first segment and was skipped whole, so the walk never resolved the dotted module to a file and never recursed into it. A binary import nested inside a dotted-path module was therefore never discovered: import foo.validate (validate holds import phonenumber_ae) -> "unresolved import 'phonenumber_ae'". This is dsa's real shape -- import harness.components.phone.validate -- and dotted package imports are exactly what the modules="." root export exists to support. Read the whole module token including dots; if it is dotted, convert a.b.c -> a/b/c and resolve it with ae_source_module_path (same path form the source resolver uses: a/b/c.ae or a/b/c/module.ae across . / src / --lib dirs), then recurse into that file. The dotted NAME is still never a binary-import candidate itself (a binary import is always a bare name); only the recursion changes. A dotted std./contrib. import that does not resolve to a local source file simply no-ops, so nothing regresses. Verified against the peer's A/B: A (import foo.validate, dotted) failed before, resolves now; B (import validate, flat) unchanged; a 4-deep dotted chain (harness.components.phone.validate -> engine) resolves; import std.string still builds. Reverting only tools/ae.c makes the new dotted test step fail. Leak-clean. Test: binary_import gains a dotted-transitive step -- pkg/dotwrap.ae reached as import pkg.dotwrap, holding import gizmo; asserts ae run resolves it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
paul-hammant
force-pushed
the
fix/binary-import-transitive-prepass
branch
from
September 19, 2026 07:24
fa1c298 to
782836a
Compare
…the entry prepare_binary_imports (tools/ae.c) discovered binary-package imports by scanning only the ENTRY file's import lines. An import of a binary package that sat in a NON-ENTRY module -- a wrapper/adapter the entry pulls in transitively -- was never seen, its interface stub never synthesized, and the build failed with "unresolved import <binpkg>". Only a binary import written directly in the entry file resolved. This blocked the whole adapter pattern: a project that wraps an engine in validate/module.ae (import phonenumber_ae) and imports validate everywhere never triggered the prepass (datastar-aether's phone component: main -> phone component -> validate -> phonenumber_ae). Fix (the general one, chosen over a manifest declaration): the prepass now walks the source-import graph. For each import line it recurses into the imported SOURCE module's file (resolved via the new ae_source_module_path, factored out of ae_source_module_exists) and synthesizes a stub for any BINARY-package import it finds at any depth. A heap-allocated visited-set of file paths dedupes and breaks cycles; the stub-emit logic is factored into ae_emit_binimport_stub and is otherwise unchanged. POSIX-only, same as the rest of the binary-import path. Verified: the committed pre-fix prepass fails the transitive case (import in a wrapper -> "unresolved import 'gizmo'"); the fix resolves + runs it (cc=42), through a 3-deep chain too, and the compiler still reports genuine source import cycles rather than the prepass hanging. Leak-clean (visited-set freed). Test: tests/integration/binary_import gains a transitive step -- a wrapper module (wrap.ae) imports the binary package gizmo, and the entry (app_transitive.ae) imports only wrap; asserts ae run + ae build resolve it. Reverting only tools/ae.c to the entry-only prepass makes that step fail, so it is a real regression guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#2106's graph-walk followed only FLAT imports. In ae_scan_binary_imports the per-import line did `if (mi == 0 || *p == '.') continue;` -- a dotted import (import a.b.c) hit `*p == '.'` after reading the first segment and was skipped whole, so the walk never resolved the dotted module to a file and never recursed into it. A binary import nested inside a dotted-path module was therefore never discovered: import foo.validate (validate holds import phonenumber_ae) -> "unresolved import 'phonenumber_ae'". This is dsa's real shape -- import harness.components.phone.validate -- and dotted package imports are exactly what the modules="." root export exists to support. Read the whole module token including dots; if it is dotted, convert a.b.c -> a/b/c and resolve it with ae_source_module_path (same path form the source resolver uses: a/b/c.ae or a/b/c/module.ae across . / src / --lib dirs), then recurse into that file. The dotted NAME is still never a binary-import candidate itself (a binary import is always a bare name); only the recursion changes. A dotted std./contrib. import that does not resolve to a local source file simply no-ops, so nothing regresses. Verified against the peer's A/B: A (import foo.validate, dotted) failed before, resolves now; B (import validate, flat) unchanged; a 4-deep dotted chain (harness.components.phone.validate -> engine) resolves; import std.string still builds. Reverting only tools/ae.c makes the new dotted test step fail. Leak-clean. Test: binary_import gains a dotted-transitive step -- pkg/dotwrap.ae reached as import pkg.dotwrap, holding import gizmo; asserts ae run resolves it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
paul-hammant
force-pushed
the
fix/binary-import-transitive-prepass
branch
from
September 19, 2026 07:29
782836a to
b59a9bc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
prepare_binary_imports(tools/ae.c) discovered binary-package imports by scanning only the entry file'simportlines. Animport <binpkg>sitting in a non-entry module — a wrapper/adapter the entry pulls in transitively — was never discovered, its interface stub never synthesized, and the build failed withunresolved import '<binpkg>'. Only a binary import written directly in the entry file resolved.This blocked the whole adapter pattern: a project that wraps an engine in
validate/module.ae(import phonenumber_ae) and importsvalidateeverywhere never triggered the prepass — datastar-aether's phone component (main → phone component → validate → phonenumber_ae).Reported and verified from the code by the datastar-aether / libphonenumber-ae line, against
fix/ae-host-triple-freebsd(0.693.0 +50cc9871).The fix
The prepass now walks the full source-import graph (chosen over a manifest declaration — the general fix that makes a binary package usable anywhere a source module is). For each import line it recurses into the imported source module's file (via a new
ae_source_module_path, factored out ofae_source_module_exists) and synthesizes a stub for any binary-package import found at any depth. A heap-allocated visited-set of file paths dedupes and breaks cycles; the stub-emit logic is factored intoae_emit_binimport_stub, otherwise unchanged. POSIX-only, same as the rest of the binary-import path.Verification
importin a wrapper →unresolved import 'gizmo'); the fix resolves + runs it (cc=42), through a 3-deep chain too, and the compiler still reports genuine source-import cycles rather than the prepass hanging.tests/integration/binary_importgains a transitive step — a wrapper (wrap.ae) imports the binary packagegizmo, the entry (app_transitive.ae) imports onlywrap; assertsae run+ae buildresolve it. Reverting onlytools/ae.cto the entry-only prepass makes that step fail → a real regression guard.Relation to #2099
Separate from #2099 (the binary-package feature + install-name + FreeBSD triples). This is a distinct resolution bug in the prepass, on its own branch. Both are needed end-to-end for the adapter consume path; this one is independently correct against any binary-import producer (
--emit=lib), not justae add.🤖 Generated with Claude Code
https://claude.ai/code/session_01YAS2Xtsem7MRFggdiEaakq