Re-exported extension members cannot be imported through the re-exporting module #182

Closed
opened 2026-10-08 07:22:18 +00:00 by orion · 3 comments
Collaborator

Problem

A module that re-exports an extension member (public use List.doubled from "./ext") cannot be imported by name from the outside: use List.doubled from "../src/lib" (or from the package path) fails with "List is not exported by ..., It is declared there, but not public".

Cause (reading of compiler/src/semantics/scope.trb)

bindMemberImport hands on the entries of the re-exporting module with entry.copy(span: ...), so entry.path stays the original "./ext". memberImportOf only accepts an entry whose path equals the path written in the importing use, so the importer's item is not recognized as a member import and falls through to the export check.

Reproduction

Package me/scr: src/ext.trb holds extend Bytes { fn doubled(): Bytes { ... } }, src/lib.trb holds public use List.doubled from "./ext", a test does use List.doubled from "../src/lib". Importing from ./ext directly works.

Acceptance

  • A member re-exported with public use List.x from "./file" can be imported from the re-exporting module and from the package path.
  • A test in compiler/tests covers the chain.

Found while implementing #175 (methods live in std/compression/src/lib.trb meanwhile). Touches compiler/src/semantics, no syntax change; seed impact to be checked on refine.

## Problem A module that re-exports an extension member (`public use List.doubled from "./ext"`) cannot be imported by name from the outside: `use List.doubled from "../src/lib"` (or from the package path) fails with "`List` is not exported by ..., It is declared there, but not `public`". ## Cause (reading of `compiler/src/semantics/scope.trb`) `bindMemberImport` hands on the entries of the re-exporting module with `entry.copy(span: ...)`, so `entry.path` stays the original `"./ext"`. `memberImportOf` only accepts an entry whose `path` equals the path written in the importing `use`, so the importer's item is not recognized as a member import and falls through to the export check. ## Reproduction Package `me/scr`: `src/ext.trb` holds `extend Bytes { fn doubled(): Bytes { ... } }`, `src/lib.trb` holds `public use List.doubled from "./ext"`, a test does `use List.doubled from "../src/lib"`. Importing from `./ext` directly works. ## Acceptance - A member re-exported with `public use List.x from "./file"` can be imported from the re-exporting module and from the package path. - A test in `compiler/tests` covers the chain. Found while implementing #175 (methods live in `std/compression/src/lib.trb` meanwhile). Touches `compiler/src/semantics`, no syntax change; seed impact to be checked on refine.
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261008 until 2026-10-08T12:38:42Z

[loop:torbscript] claim refine by session:loop-torbscript-20261008 until 2026-10-08T12:38:42Z
Author
Collaborator

[loop:torbscript] refine: ready. Cause verified against current main: bindMemberImport (compiler/src/semantics/scope.trb) hands on the re-exporting module's entries with entry.copy(span: item.name().span), so entry.path stays "./ext"; memberImportOf matches entry.span == at && entry.path == path && entry.name == name against the path written in the importing use, finds nothing, isMemberImport is false and the item falls into the export check ("List is not exported by ..."). entry.path is also what checkOneMemberImport prints in "<path> adds no member ...".

Goal: a member re-exported with public use List.x from "./file" is importable by name from the re-exporting module and from the package path.

Acceptance criteria:

  • A fixture in compiler/tests/check.test.trb (same messages(files) style as the existing member-import tests): ext.trb holds extend Bytes { fn doubled(): Bytes {...} }, lib.trb holds public use Bytes.doubled from "./ext", main.trb holds use Bytes.doubled from "./lib" and calls value.doubled(); messages(files).isEmpty(). Same chain imported by the package path if the fixture helper can express a second package; otherwise note why in the PR.
  • A two-hop chain (main -> lib -> mid -> ext) and as renaming through the re-export resolve too.
  • Regression kept: importing a member the package does not add through the re-exporting module still reports "adds no member", at the importer's use line, naming the path written there (not the original ./ext); the existing member-import tests and the prelude re-exports (public use Int64.seconds from "std/time") are unchanged.
  • Implementer note: the fix is in the entry's path handling (record the path written at each hop, or stop requiring it in memberImportOf and let span + name decide); watch bindMember, which de-duplicates on package + name, so an importer that already holds the original entry must still get an entry for its own item span.

Pinned records: CONCEPT.md "An extension member is named where it is used" (members imported by path use Type.member from ..., the prelude re-exports them by name with public use) and the re-export form in section Modules (public use ... from). It implements existing documented behavior, so no ROADMAP milestone is touched and no design choice is open (no language change, no new syntax, not breaking, no CONCEPT Open Question).

Seed: no. No syntax change, no new native, no driver flag, no std name looked up by string; the compiler source stays within the forms the current seed already accepts.

Size: one PR, one Conventional Commit (fix(semantics): ...), touching compiler/src/semantics/scope.trb and compiler/tests/check.test.trb. Tier A gates in CI; tier B is not required (no compiler/src/ir, backend or runtime change). Once it lands, the follow-up of moving the #175 methods back is a separate issue if wanted.

Implementer model: opus/high (compiler/src). Worker usage: n/a (not reported by the runtime). next: implement

[loop:torbscript] refine: ready. Cause verified against current main: `bindMemberImport` (compiler/src/semantics/scope.trb) hands on the re-exporting module's entries with `entry.copy(span: item.name().span)`, so `entry.path` stays `"./ext"`; `memberImportOf` matches `entry.span == at && entry.path == path && entry.name == name` against the path written in the importing `use`, finds nothing, `isMemberImport` is false and the item falls into the export check ("`List` is not exported by ..."). `entry.path` is also what `checkOneMemberImport` prints in "`<path>` adds no member ...". Goal: a member re-exported with `public use List.x from "./file"` is importable by name from the re-exporting module and from the package path. Acceptance criteria: - A fixture in `compiler/tests/check.test.trb` (same `messages(files)` style as the existing member-import tests): `ext.trb` holds `extend Bytes { fn doubled(): Bytes {...} }`, `lib.trb` holds `public use Bytes.doubled from "./ext"`, `main.trb` holds `use Bytes.doubled from "./lib"` and calls `value.doubled()`; `messages(files).isEmpty()`. Same chain imported by the package path if the fixture helper can express a second package; otherwise note why in the PR. - A two-hop chain (`main` -> `lib` -> `mid` -> `ext`) and `as` renaming through the re-export resolve too. - Regression kept: importing a member the package does not add through the re-exporting module still reports "adds no member", at the importer's `use` line, naming the path written there (not the original `./ext`); the existing member-import tests and the prelude re-exports (`public use Int64.seconds from "std/time"`) are unchanged. - Implementer note: the fix is in the entry's path handling (record the path written at each hop, or stop requiring it in `memberImportOf` and let span + name decide); watch `bindMember`, which de-duplicates on package + name, so an importer that already holds the original entry must still get an entry for its own item span. Pinned records: CONCEPT.md "An extension member is named where it is used" (members imported by path `use Type.member from ...`, the prelude re-exports them by name with `public use`) and the re-export form in section Modules (`public use ... from`). It implements existing documented behavior, so no ROADMAP milestone is touched and no design choice is open (no language change, no new syntax, not breaking, no CONCEPT Open Question). Seed: no. No syntax change, no new native, no driver flag, no std name looked up by string; the compiler source stays within the forms the current seed already accepts. Size: one PR, one Conventional Commit (`fix(semantics): ...`), touching `compiler/src/semantics/scope.trb` and `compiler/tests/check.test.trb`. Tier A gates in CI; tier B is not required (no `compiler/src/ir`, `backend` or `runtime` change). Once it lands, the follow-up of moving the #175 methods back is a separate issue if wanted. Implementer model: opus/high (compiler/src). Worker usage: n/a (not reported by the runtime). next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T16:42:23Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T16:42:23Z
argus closed this issue 2026-10-08 12:35:56 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#182
No description provided.