Skip to content

Cross module callbacks - #70

Open
saidctb wants to merge 94 commits into
mainfrom
cross-module-callbacks
Open

saidctb wants to merge 94 commits into
mainfrom
cross-module-callbacks

Conversation

@saidctb

@saidctb saidctb commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

No description provided.

saidctb and others added 4 commits September 14, 2026 05:37
Wrapping PRIMA surfaced four defects in the callback path.

A dummy procedure's interface name kept the casefolded key used to match
it, so a generated .pyi annotated `procedure(OBJ)` as `obj` while importing
`OBJ` and could not be rebuilt from its own contract. The parser now keeps
the declared spelling and normalizes case at each comparison.

An assumed-shape array in a callback prototype emitted the plan's runtime
extent marker as Fortran text, `dimension(::Strided)`. The bridge now
lowers a runtime extent to an assumed-shape dummy and measures the
contiguous call-local copy from it. An array result has no caller
descriptor to measure and reports that directly instead.

An abstract interface imported from another file did not resolve during
single-file conversion, so `generate --pyi a.f90 b.f90` degraded the dummy
to an opaque placeholder. Resolution now matches multi-file builds, and an
interface that no supplied source declares is reported by name.

An `intent(out)` primitive scalar was projected as an independent value.
Python has no writable scalar, so the write was silently discarded and the
native caller read uninitialized memory. Such a dummy now reaches Python as
rank-zero storage, and the value spelling is a policy error naming the
replacement. A prototype describes a native callback interface, so it still
mirrors the native argument list; `@native_call` projection remains
available in the contract for a return-oriented callable. Plan validation
covers the storage projection, which the scalar rule previously skipped.

Callback parameters now document the exact callable they expect, generated
from the same completed prototype the trampoline is built from, so the
documented signature cannot drift from the real ABI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Fortran permits a dummy with no declared INTENT to be both read and
modified, but a primitive scalar callback dummy without one was projected
as an independent value and copied in only, so a write by the Python
callable was discarded.

Semantic normalization now records rank-zero storage for such a dummy, and
the callback transfer direction follows that completed storage rather than
re-deriving copy-in from the absent intent. The declaration itself is
unchanged: no intent is synthesized into the semantic origin or the
generated Fortran interface, so the contract records the absence by
carrying no direction wrapper.

    real(8), intent(in) :: f       ->  f: In(Addr(Float64))   copy-in
    real(8), intent(out) :: f      ->  f: Out(Float64[()])    copy-out
    real(8), intent(inout) :: f    ->  f: InOut(Float64[()])  copy-in/out
    real(8) :: f                   ->  f: Float64[()]         copy-in/out

--assume-intent-in-scalars continues to elect which default an undeclared
intent receives, narrowing that last row to the input-only projection
without giving the dummy a direction it never declared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
The callback key rules kept a sentence stating that every primitive scalar
callback argument arrives as an independent NumPy scalar value, which
contradicted the writable-storage rule documented directly below it. One
rule now covers both projections.

The undeclared-intent regression asserted the generated contract text and
then rebuilt from the Fortran source, so nothing proved the bare
Float64[()] spelling survived being read back. It now builds through that
generated contract and runs the callback, covering source, contract,
policy, codegen and runtime in one pass; the shared helper takes an
optional fixture package so a round trip needs no checked-in contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
An abstract interface imported from another module was converted in the
scope of the module that imported it. An interface body is written where it
is declared, so a derived type it names belongs to the declaring module; a
consumer that imported only the interface attributed that type to itself
and failed with no completed wrapper type definition for a type it never
declares. Interface lookup now carries the declaring module, the prototype's
dummies convert in that module's context, and a type local to the declaring
module records it as the origin.

Resolution also stopped at module-level imports. A `use` inside a single
procedure, a standalone procedure's own imports, and an interface
re-exported through another module now all resolve, following a chain of any
length to the module that declares it. File, project and per-file CLI
conversion share one resolver instead of each carrying its own lookup, and
contract reconciliation follows a re-export so a prototype imported from a
module that only republishes it still binds to its declaration.

A contract now also imports a prototype it references but never declares,
which a procedure-local `use` previously left as a free name.

Callback docstrings state each array argument's rank and extents, taken from
the completed transfer plan, and every generated docstring spells a runtime
extent the way the contract spells it rather than exposing the internal
marker.

Regression coverage: imported interfaces owning derived types, the three
resolution routes, multi-file `generate --pyi` through parse and build
including a renamed import, rank-two assumed-shape callbacks, writable
scalar storage on the bridge-free direct bind(C) route, and the
`--assume-intent-in-scalars` override end to end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
saidctb and others added 25 commits September 15, 2026 02:42
Three gaps remained in how an imported callback interface carries its
source facts.

Declaring-module ownership was recorded only while iterating dummies, so a
function interface returning a type its own module declares attributed that
type to the consuming module and failed to build. The helper now takes a
semantic type and its declaration rather than an argument, and both the
dummies and the result use it. Binding a prototype reference from a
contract records the same origin, so the generated `.pyi` builds too.

A renamed import kept only the local spelling, so the reference named an
interface the declaring module never defines and the contract imported a
name that does not exist there. The resolver now carries the local spelling
beside the declaring signature, through any number of re-export hops, and
the reference records both. A reference differing from the declaration only
in case is the same interface, so it is spelled canonically rather than
binding a second name.

Following a re-export ignored Fortran accessibility, so a module that
imported an interface privately still appeared to publish it. Reaching
names from another module now applies that module's own visibility rules,
at every hop; a module still sees its own private interfaces.

Not addressed here: resolving a cross-module derived type through the
runtime namespace. A wrapper looks the type up on the module owning the
function rather than the one declaring the type, which also affects an
ordinary function returning an imported type and predates this branch. The
callback-result regression therefore asserts the build, not a call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Two bridge diagnostics interpolated extent expressions straight from the
plan, so rejecting a strided callback array result reported extents
['::Strided'] -- the explicit step the IR stores -- rather than the
shorthand the author wrote.

`Strided` is a public contract name, so `T[::Strided]` and `T[::]` are two
spellings of one contract while `T[:]` is the distinct contiguous one. The
shorthand now has a single owner beside the marker set it belongs to, and
the docstring builder reads it from there instead of keeping a private copy
under a name that implied the explicit form was internal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Carrying the declaring module's classes into prototype binding pushed
reconcile_external_type_refs to complexity 21, over the staged limit of 20.
The prototype branch moves to its own function, which also lets the module
name candidates reuse the helper the re-export index already uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
`T[::]` already spells a strided axis and `T[:]` a contiguous one, so the
explicit `T[::Strided]` and `T[0:n:Strided]` forms were a second way to
write contracts that already had one. The docs described `Strided` as a
compatibility spelling for an older form and told authors to use the short
one; it is now gone rather than carried.

The step position spelled nothing else, so a value there is refused with a
message naming the spelling to use instead. Without that check the removed
form would still have parsed: its text happens to match the marker the IR
carries for a strided axis, so dropping the contract name alone left it
working for anyone who did not import the name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
The IR named a strided axis after a contract name that no longer exists,
so every layer that showed one to a reader translated it back: the `.pyi`
printer, the docstring builder and two bridge diagnostics each converted
the token to `::`. Producing `::` directly removes the translation and the
mismatch behind it.

The axis mode had been read from the word itself, so that rule moves beside
the marker set it belongs to and states what actually marks a strided axis:
a trailing empty step, with bounds (`lower:upper:`) or without (`::`).

Six sites re-declared the runtime marker sets as literals; they now read
the shared ones. The absence assertions in the callback planning tests went
with the token -- `::` is Fortran's declaration separator, so its absence
from generated source says nothing, and the positive spellings beside them
already prove the lowering.

`prik semantics` output changes with the IR, so its two expected payloads
are regenerated. Contracts, docstrings and generated sources are byte for
byte unchanged, having already printed the contract spelling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A reference reached through renaming re-exports followed the module
provenance back to the declaration but kept the alias it was last bound to,
so the metadata claimed the declaring module defines a name it never does:
name MID against origin_module A, where A declares OBJ.

The declaration names the symbol, so _bind_prototype_reference takes it from
the resolved prototype instead of from a caller that may only hold an
intermediate alias. The one caller that already passed the declaring name is
unaffected, and the caller that could not know it no longer has to.

A rename and a same-name re-export were each covered; their combination was
not, which is where this sat. Both routes are now covered: the contract
chain through reconciliation, and a Fortran chain generated to contracts,
built and called.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Fortran lets a scope build a generic interface from several blocks, each
contributing specifics. PRIMA does this under preprocessor guards, adding
kind-specific procedures only for the precisions a build supports, so
`huge_value` arrives as two blocks that gfortran accepts and the parser
rejected as a duplicate declaration.

Blocks naming one generic in one scope now merge into a single interface
carrying every entry in declaration order, keyed by module so two modules
in a file keep their own. Abstract and unnamed blocks are never generics
and are untouched, and the duplicate check still holds for every other unit
kind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A local interface block repeating a use-associated generic name extends
that generic; it does not replace it. PRIK resolved only the specifics a
module declared, so an extending module published a generic missing
everything it inherited and rejected calls gfortran accepts.

The importing scope now resolves the specifics that reached it through the
import as well as its own, following the import chain. Accumulation stays
one-directional, as Fortran requires: the declaring module gains nothing
from a module that extends it later. An inherited specific joins the
importing module privately, since the import bound the generic name and not
the specific's own, so it is reachable only through the generic.

Two identities had been inferred from an overload's first specific, which
only holds while one module owns them all. A generic now records the scope
that declares it, so an extended generic is published by the extending
module rather than the one it inherited from, and a module generic
addresses each candidate by the scope owning that procedure so an inherited
one stays findable. Class-bound overloads are addressed by their class as
before, which owns every candidate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A module that names an imported procedure in a `public` statement means it
to be part of its own interface, but PRIK dropped the module entirely: a
facade that only re-exports reached Python as nothing at all, so callers
had to reach past it into the modules it was hiding.

The name is published without repeating the declaration. A re-export names
an existing wrapper rather than adding one, so the plan carries an alias
binding the name to the callable its declaring namespace already exposes.
One wrapper is generated, the contract keeps spelling the re-export as the
import it already was, and `facade.proc is home.proc` holds.

Naming the entity is what states the intent. A name public only because the
module default is public carries no such statement, and mirroring that would
republish everything a module happens to import under every namespace that
imports it, so those are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A declaration expression naming an imported specification function recorded
the import spelling as the function's native scope, so a relative sibling
import left the scope as `.extent_helpers` where Fortran names the module
`extent_helpers`. Imported type identities were already normalised; this
applies the same rule to declaration callables, and fixes the namespace
branch beside it, which split on the leading dot and produced an empty name.

Assertions across the Fortran, C and round-trip suites pinned the previous
absolute spelling and now expect the relative one. The C frontend emits
sibling header imports through the same printer, so those move with it.

Found by running the full suite, which the relative-import change had not
been through: five Fortran and three C failures, one of them this defect and
the rest pinned spellings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A `use` statement replaced any earlier import of the same module instead of
adding to it, so a scope naming one module across several statements kept
only the last. PRIMA splits iso_fortran_env across three lines, so
`DP => REAL64` was dropped and `real(RP)` could not be resolved from source:
the kind reached the compiler probe as a project name the probe cannot see.
With every import kept, the existing project symbol table resolves `RP` to
`REAL64` and `IK` to `kind(0)`, which the probe evaluates as the intrinsic
expressions they are.

A bare `use` imports everything, so it absorbs any list beside it rather
than being narrowed by one.

Ordering a procedure's outputs also compared an unplaced position against
placed ones and raised a comparison error. An output with no position is
what the check exists to catch, so it is reported as an unsupported wrapper
policy instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A derived type may build one type-bound generic from several `generic ::`
statements, each contributing specifics. The parser recorded one binding
per statement, so a type declaring

    generic :: area => area_int
    generic :: area => area_real

carried two bindings both named `area`. Only the first reached dispatch,
and calling the generic with the argument types of any later statement
raised `no matching overload` at runtime. The single-statement spelling
worked, so whether a call resolved depended on how the source was written.

The generated contract hid this: its printer renders same-named overload
sets as consecutive `@overload` defs, which is what Python wants, so both
spellings produced byte-identical `.pyi` text and the loss surfaced only
in the built extension.

Merge the statements where the module-level generic interface blocks are
already merged. The key ignores case and internal spacing so a defined
operator merges across `operator(+)` and `operator (+)`. Attributes come
from the first statement: the standard requires every statement for one
binding to declare the same accessibility.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
An interface body types its own dummies, and the kind it names may come
from a `use` written inside that body -- no module variable or module
procedure declares it. The variable-context walk visited a module's
variables, procedures and derived types but never its interfaces, so
those dummies contributed no target-probe requirement and the conversion
later raised on a storage fact nothing had measured.

The two input routes disagreed as a result: `generate --pyi` failed with
`Unsupported Fortran semantic type for variable 'nf':
integer(kind=kind(0))` on sources that `build_fortran_extension`
accepted, because a wrapper build's larger parsed set happened to raise
the same requirement elsewhere.

Walk the interfaces a file or module declares, and report the variables
of the bodies they hold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
An overload declaration names a specific and restates its public
signature. The projection that shapes that signature lives on the
specific -- a declaration carrying `native_call` is rejected outright --
so the declaration can only spell what the projection leaves visible.
The comparison read the native form instead, and rejected two shapes a
generated contract routinely holds.

An output argument projected into a result kept the write-through its
argument passing states. Whether the call writes through a dummy is not
part of a result type, and the comparison already read ownership from
the declaration for that reason; its storage mutability now follows.

A native scalar descriptor result kept its descriptor topology, which
only a `native_call` result wrapper can name. The contract printer
already strips it when emitting such a result as a nullable value, and
the comparison now expects what the printer writes. Reading that
annotation back needed the `| None` unwrapped as well, which until now
happened only for a slot some projection marked nullable.

The effect was a contract the same tool refused to read back: a generic
over `intent(out)` allocatable arguments, such as an allocation helper,
failed on `safealloc` against its first specific.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A source-derived contract declares its entities under Python names, so a
Fortran entity kept in capitals is declared lower case with its source
spelling recorded beside it. Imports were written straight from the
parser's `use` mapping instead, leaving a contract that defines `ik`
imported as `IK` -- a name nothing defines, which failed when the package
was loaded back.

A prototype is the exception. It keeps the spelling its own contract
declares, because an annotation naming it is written the same way, so an
import binding one keeps that spelling too.

Which names those are is a fact about the contracts that declare them,
not the one reading them: a module re-exporting a prototype references it
nowhere in its own body. The stub emitter already holds every module it
renders, so it collects the prototypes they declare and tells each module
before any of them writes an import. Either spelling in a renamed import
identifies a prototype -- the source names what the dependency declares,
the target what the importer calls it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A contract is written to be edited, and the name a declaration states is
what Python should call the entity. `SourceName` was read the other way
round: the source spelling replaced the declared name, so renaming a
variable exported the native spelling and dropped the edit entirely. It
records the native entity now, exactly as `bind` does for a callable,
and the declared name stands. A source name inside `Final[...]` reaches
its declaration as well, where the reader looked only through a bare
`Annotated` and dropped it.

Generated contracts were caught by this too. A Fortran entity Python
cannot spell is declared under a name that it can -- `lambda` becomes
`lambda_` -- and reading that back installed the unusable spelling, so
the declaration the contract stated was unreachable.

A class may state a native type through `bind`, which was refused
outright, leaving a derived type locked to a name its Fortran type also
answers to. Policy already read `native_name or name`, so only the
refusal and the reference lookup had to change: an imported reference
names a type the way its declaring contract writes it, and resolving it
searches that module alone, never a type of the same name elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Fortran names entities without regard to case, so writing a capitalized
`IK` as `ik` renames nothing -- the generated Fortran reaches it either
way. Every such declaration nevertheless carried a `SourceName` or
`@bind` stating the capitals back, which said nothing the declaration
did not already say. Across one real library's contracts that was 60
annotations, none of them load-bearing.

The naming policy already drew this line: `normalize_public_name`
reports `needs_fix` against the casefolded source, so a pure case change
is deliberately not a fix. The printer compared the spellings exactly
instead and never consulted it.

A name Python cannot hold as written keeps its original: a keyword, a
character an identifier cannot carry, a name a collision moved aside.
So does every name from a source language that is case-sensitive, where
the spellings are still compared as written.

A renamed class now states its native type, so the rename survives
regeneration. A C struct keeps its own representation rules, which spell
`struct node` without a decorator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Publishing an imported name says this module means it to be part of its
own interface. What that reaches at runtime depends on what the name
declares, and every explicitly-public import was treated the same way.
A module publishing an imported callback prototype states where a
signature comes from, and a signature is not an object Python holds, so
the alias reached for an attribute of a module exporting nothing and the
build stopped on a namespace that does not exist.

Each re-export now records what it publishes, read from the module
declaring it. A procedure and a derived type reach Python as one exported
object and become aliases; a prototype, a module variable whose state
stays live, and a generic keep to the semantic and contract-import paths
already carrying them.

An alias also binds a Python attribute, which a Fortran spelling is not.
The declaration supplies the name its namespace actually published, so a
procedure written in capitals is reached under the name it was exported
as, and one rule serves the source and contract routes alike.

A plain `use` carries every public name of the module it reads, so a name
published without being declared here is one of them. The `public`
statement says which, and an origin two such modules could supply stays
unresolved rather than guessed. A `use` that publishes nothing still
re-exports nothing.

Two further names were read as though a spelling identified an entity on
its own. A generic built from several blocks merged on the module rather
than the scope declaring it, so two procedures' local interfaces of one
name became a single generic answering both. A contract wrote an
overload's target and a prototype import in source spelling, naming a
declaration the contract does not hold and forcing one module's prototype
spelling onto every module using that name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
…ontract states

Four corrections to the re-export and naming work.

A name may be published by a module that was published a name in turn,
and reading only the module a `use` names stopped at the first hop. The
entity then looked like nothing at all and its re-export was dropped, so
a twice-published procedure reached Python through no namespace. Each hop
is followed now, through renames and plain `use` alike, until the
declaration itself is reached; a cycle ends the search rather than
circling it.

A contract's imports were all read as exports, so an import written to
express a declaration became a Python name after a source-to-contract
round trip and the two builds disagreed about what a module publishes. A
re-export is stated by aliasing the name to itself, the way a stub marks
anything it re-exports, and a plain import states only what the contract
needed to name. Both still reach the export tree, because a contract
reading from this one has to resolve what it names; only the published
ones become attributes. A package entry contract is the exception it has
always been: it declares little and exists to choose a surface, so the
names it imports are the ones it means to publish.

A generic declared inside a procedure belongs to that procedure. The
parser stopped merging two such blocks but reported them as the module's
own, so later stages read them as module generics of one name. The
declaring scope is recorded and module generics are read from module
scope alone.

Two names were still derived rather than read. A prototype identity was
matched exactly, so a `use` differing only in case asked for a spelling
no contract declares; it is matched without regard to case and answers
with the spelling its contract uses. An import and an overload target
were normalized independently of the module that named the declaration,
which a collision can move aside: a module holding `lambda` and `lambda_`
publishes `lambda_` and `lambda__2`, and publishing the second silently
bound the first. Each module is named once before any import is written,
and a source spelling identifies a declaration ahead of a published one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
An import cannot say whether a name is needed to express a declaration
or meant to be published. `use shapes_mod, only : crate => box` reads the
same whether the rename avoids a collision or publishes the type under a
new name, so no import spelling can carry the difference. A contract now
closes with `__all__`, naming its whole public surface.

PRIK writes what the source publishes: a module's own public
declarations, and any imported name a `public` statement names. The list
exists to be edited. Removing a name leaves the entity declared and
reachable from other contracts while it stops reaching Python; adding an
imported one publishes it here, including a name imported only to express
a declaration; an empty list publishes nothing. A contract stating no
list has named no surface, so everything it reaches is published, which
is what an entry contract selecting from its package has always meant.

Every name reaches the export tree either way, because a contract reading
from this one still has to resolve what it names. Only a published name
becomes an attribute. Names that reach Python by other means keep to
them: a sub-namespace attaches the package tree rather than binding an
entity, and an intrinsic module has no contract to read a name from.

A re-exported procedure also binds the callable its declaring module
exported rather than being wrapped again, so a contract build gives the
same object a source build does, renamed re-exports included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A contract's `__all__` is its public surface, and a sub-namespace is part
of that surface: `pkg.solver_mod` is an attribute like any other. It was
exempt, so an entry naming one module still exposed every module it
imported, and a list that read as a selection pruned nothing.

The exemption is gone rather than made conditional, and a generated entry
states the modules it imports so there is something to take off. Leaving
one off keeps the package from exposing it; stating no list still
publishes everything the contract reaches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Three corrections to the export surface, and one wording change.

A declaration published from several namespaces was owned by whichever
namespace the entry contract reached first. An entry composes a package
by importing from it, and the order it does so says nothing about where
anything is declared, so a facade listed before the module it reads from
became the owner of a procedure it only republishes -- and the wrapper
went to the wrong namespace while the identity assertion still held.
Ownership now comes from the contract declaring the entity: each
contract's own namespace is read from the export tree, and every other
publication aliases what that one owns.

A wildcard import took every name its dependency held, including names
that dependency's `__all__` withheld. It reads the published surface now.
A withheld name stays in the tree and reachable by asking for it, because
a contract may need one to express a declaration or mean to publish it
itself; only what a dependency publishes answers to `*`.

The contract route could also publish what a source build cannot. A
module variable holds state that stays live where it is declared and a
generic is a dispatch surface rather than one object, yet attaching
either to a second namespace gave a contract build an attribute the
source build never exposes. A generated contract states neither, and a
contract asking for one is refused by name rather than quietly differing.

`__all__` states a contract's public symbol surface, which is not the
same as the set of runtime objects: a prototype belongs on that list and
names a callback signature rather than anything the extension exposes.
The reference documents what each kind of published name becomes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
The C route already selects a public surface while reading source, and a
semantic contract already states one in __all__. Nothing connected them, so
--export-symbols read as a private include-exposure knob and a generated
contract's __all__ read as something separate PRIK happened to write.

Name them as the same statement in two places: the allowlist selects the
source-side public surface, and generating a contract records the Python
names it publishes in __all__. After that the contract is authoritative --
edit the list, not the allowlist, which a contract build rejects.

The two lists live in different naming domains, the file naming native C
identifiers and __all__ naming what reaches Python, so the docs say that
rather than implying the spellings are interchangeable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
saidctb and others added 30 commits September 18, 2026 07:08
`_parse_use_statement()` recognized `only :` and then threw that fact away, so
a scope's import table could not tell

    use m, only : p => q          carries p, and nothing else
    use m,        p => q          carries p, and everything else m publishes

apart: both left one mapping, and every reader took a non-empty mapping list to
mean the `use` was narrowed. A module renaming one name therefore lost access
to every other name its module offered, and route collection saw no wildcard
where the language provides one.

Record it on the mapping, where the reading that produced it belongs, and read
it in the three places that ask what a `use` carries. A rename also makes the
entity reachable only by its local name, so the carried set excludes the name
it renamed away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Merging the generics a module reaches made specific procedures collide.
`_inherited_generic_specifics()` keyed them by bare name, and
`_resolve_overload_targets()` looked them up the same way, so two contributors
each declaring

    interface convert
      module procedure to_value
    end interface

produced one candidate: the second was taken for the first and dropped, and
the merged generic silently lost a signature it must dispatch over.

A specific is identified by the module declaring it together with the name that
module gives it. `_SpecificProcedure` carries that pair, the inherited, inline,
own and type-bound lookups are all keyed by it, and a target names which
declaration it means rather than which spelling.

A contract then has to write two declarations spelled alike, so the emission
records what it named each one -- by scope, not by spelling -- and an overload
target reads that record. Without it both dispatchers named the first
declaration. A class member is named inside its class, so the record covers
module-level declarations only, exactly as the published-name record does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Settling a procedure-local prototype's contract spelling reserved the module's
declared names first, so a suggestion could not take one. A use-associated name
binds in the module too, and the contract writes an import for it, but it was
not held -- so a module writing

    use helper_mod, only : first_cb

whose contained procedure `first` declares `cb` produced a contract where

    from .helper_mod import first_cb
    @prototype
    def first_cb(...)

bind the same name, the prototype shadowing the import.

Hold what the module imports beside what it declares. The prototype takes the
next spelling instead, and the annotation naming it follows, because both read
the one settled name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Two readings in export completion settled a decision instead of reading it.

`__all__` was compared case-insensitively. A contract is Python, where `Foo`
and `foo` are different names, so a list naming `Foo` beside a declaration
written `foo` names something the module does not define -- and publishing
`foo` for it invents a surface the contract never stated.

And a declaration's export list was tested for truth, so an absent key and an
explicit empty list were treated alike. They mean opposite things: no key means
no stage has projected the declaration yet, and the default publication
applies; an empty list means a stage decided it publishes nowhere, which
substituting that default reverses. `_apply_source_python_exports()` writes
exactly that empty list for a private declaration, so the two were only ever
kept apart by a second guard reaching the same conclusion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Identifying a specific by its declaring module left `_module_overload_sets()`
one branch over the staged complexity limit. The rebinding it does for a
constructor and for an ordinary generic is the same loop written twice, so it
becomes one helper: each private specific is bound through the public name that
reaches it -- the generic, or the type name for a constructor.

No behavior changes; the two call sites already did exactly this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
`dict[str, list[FortranUseMapping]]` had run out of room. An empty list meant
"imports everything", so `use m, only :` -- valid syntax that imports nothing
-- could not be told from `use m`. `_record_use_mappings()` resolved the
overlap by letting a bare `use` replace whatever an earlier statement listed,
so `use m, only : p => q` followed by `use m` lost `p` altogether, though the
language reads the two together. And putting `only` on each mapping, as the
previous commit did, could not record a statement that has no mappings at all.

Keep what the source says: `FortranUseStatement(module, only, mappings)`, one
per statement, appended rather than merged. `FortranUseAssociation.of()` is the
single reading of them -- what a scope sees under which name -- and every
consumer asks it instead of deciding for itself what an empty list or a rename
means.

That deletes the interpretations rather than adding to them:
`_carries_every_public_name()` and `_renamed_away_names()` are gone, and the
`if not mappings` branches in route collection, public names, callback
interfaces, declaration-call resolution, derived-type origins, contract imports
and compile-time symbols are replaced by `imports_all` and `carried()`. Those
last five had never been taught about non-`only` renames at all, so
`use kinds, wp => rk` no longer loses the rest of what `kinds` offers.

`fortran2ir.py` and `parser.py` both end up shorter than before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Making a specific's identity structural left `_bound_overload_sets()` passing
its now tuple-keyed lookup to two helpers that still asked for a bare name.
`_apply_assignment_projection_to_originals()` therefore found nothing, so a
type-bound

    generic :: assignment(=) => assign_value

projected its bound object as the result on the generic's candidate while
`assign_value` itself kept the unprojected signature -- the same call behaving
differently through the two names. The module-level path happened to work only
because it was handed the old string-keyed map.

Finish the migration rather than teach the helpers two key shapes. One reading,
`_declared_specific()`, derives a specific's identity from the declaration it
came from, and both helpers and both call sites use it. Nothing is left that
has to guess how its lookup is keyed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Passing tests say a bug is gone; they do not say the code got simpler. This
branch has reached the size where local patches start working against the
architecture, so record the criterion we are now reviewing against: a fix
either deletes a way of deciding or it does not land.

The rule names the shapes that keep producing these bugs -- a record that has
to mean several things, a lookup reached by two key shapes, a completed
decision ambiguous with an absent one, a consumer special-casing what a plan
should have decided -- and says to replace the structure rather than widen it.
Introducing a record or a small class is the preferred move where it lets a
reader see the rule in one place; the condition is that it deletes what it
replaces, since moving or wrapping leaves every existing reader standing.

It also says to keep the accumulated regressions while doing that. They are the
specification of what PRIK supports, and simplifying by dropping cases is not
simplifying.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
`uses` was still a dictionary keyed by module name, which left three problems
that the statement record alone could not fix.

The key repeated `statement.module`, and its values were mutable lists a
procedure inherited by sharing: `proc_state.uses.update(module.uses)` copies the
dictionary and not the lists, so a `use dep, only : y` written inside a
contained procedure appended to the list the module itself held. A
procedure-local import therefore reached module accessibility and re-export
analysis. Module names are also case-insensitive, so `use DEP` and `use dep`
were held under separate keys although the language reads them as one use.

And `FortranUseAssociation.carried()` returned one entity per local name.
Fortran does not promise that: `use dep, x => y` where `dep` also publishes `x`
makes two entities reach `x`, and the second silently won.

So `uses` becomes `list[FortranUseStatement]`, frozen with tuple mappings,
appended rather than merged; a scope inherits by concatenation. `ScopeUses`
groups them case-insensitively and answers the two questions consumers have --
which routes reach a local name, and which names a scope reaches. It reports
routes rather than choosing between them, because whether two routes are an
ambiguity or a set of contributors is a question about the entities, which the
stage holding them already decides.

Deleted rather than moved: `_record_use_mappings()`, `FortranUseAssociation`
and its `carried()`, the parent-diffing branch of `_procedure_local_uses()`,
`_fortran_owner_used_modules()` in the build, and the remaining per-consumer
`imports_all` / `mappings` walks in route collection, public names, callback
interfaces, contract imports, declaration calls and derived-type origins.
`used_module_names()` is now the one reading of what a scope depends on, shared
by project indexing, file ordering and compile batching -- which had been three
readings, and which the aliasing bug had been quietly propping up.

Existing files lose 105 lines; the resolver adds 130 in one place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Naming a contract's declarations was decided in four places: export policy
chose published names, the printer allocated the rest while rendering and kept
`settled_names`, `published_names`, `published_specifics`, `reexport_names`
and `class_python_names` to remember what it had chosen, class-surface policy
ran its own `NamingPolicy` for members, and `published_names()` rendered a
whole module just to learn what it would be called. Those four could disagree,
and every per-stage test still passed.

`_complete_contract_names()` now records a `CONTRACT_NAME_METADATA` on every
declaration -- published or withheld, class members and nested classes
included -- plus the spelling each overload target names. The printer and
class-surface construction read it through `completed_contract_name()`, and
the printer keeps no naming state beyond the purely textual aliases for
`prik.contracts` imports. That reader sits beside its key in
`semantics/models.py`, because a printer may not import policy.

Finishing it surfaced four problems the old arrangement had hidden:

- A build folds every source module into one, so a single root ledger made
  four facades each re-exporting `scale_value` collide. Each name is now held
  where its own authority places it: a published declaration in its completed
  export namespace, a re-export in its publisher's, a withheld declaration in
  the file being completed. A declaration's native module is not that file --
  a facade carries its generic's inherited specifics -- so it is not used.
- A user class named `Vector` was printed as `prik.contracts.Vector`. The old
  code avoided it only because the semantic type still carried the lowercase
  Fortran name; a completed spelling is now a declaration, never a symbol.
- One procedure can be declared at module level and as the method binding it,
  and the `.pyi` reader resolves an overload target against module procedures
  first. Targets are written in that same order, so the contract names the
  declaration its reader finds -- the other one routes a private procedure the
  bridge cannot reach.
- Completion walked only top-level classes, leaving nested classes' members
  unnamed.

Two class-naming assertions change from `PointType`/`point_t` to
`Pointtype`/`Point_T`. Those tests emitted without completing policy, so the
printer had printed the raw semantic name -- a name no build published; the
runtime export was already `Pointtype`/`Point_T`, and the contract now agrees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Export completion now records every declaration's decision, publishing
nowhere included: a private declaration, one a contract leaves out of
__all__, and a nested class all record []. completed_python_exports()
fails on a declaration it never completed instead of publishing it under
its own name, which is what bound a leaf contract's imported sibling type
in the leaf. build_function_wrapper_policy() takes module_export
explicitly; the type-bound-target guess is gone.

The plan kept a type only where it was published, so existence and
publication were one decision. One placement rule now defines each type:
where it is published, under those names; beside its parent class when it
is nested and unpublished; at the root, unbound, otherwise. It replaces
the two copies of the export grouping in the derived-type and class
planners. DerivedTypePlan carries contract_name and nested_in, and one
type_definition_name() rule names the class generated code reaches, so
the binding, Python surface, docstrings, and polymorphic variants read
the plan instead of filtering surface.python_names. A nested class is
bound on its parent (module.outer.inner).

A cleanup action on a returned derived object read its family from the
type's spelling, so a result written through an import alias failed
planning. LifecyclePolicy now carries the transfer's derived handoff and
the name-guess fallback is deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A generated contract copied its module's `use` statements and then the
printer patched the list with five synthesizers, each deduplicating its own
way, and spelled every item through three name maps and a normalization
fallback. A facade extending a generic it `use`s from two modules therefore
imported `convert` from each of them beside declaring the merged generic,
and the package it generated could not be read back.

complete_contract_imports() now completes each module's imports once,
after names, over the modules written together. A contract binds the names
its declarations use and the names it publishes, and every binding passes
one table keyed by local name: a module never imports from itself or a
declaration it carries, one entity reached twice binds once, and a name
meaning two entities is refused. Each item records both spellings, and the
printer renders them without choosing or spelling anything; its
synthesizers, _public_import_name, _verbatim_import_names, the namespace
validators, and the declared_prototype_names/contract_names_by_module
plumbing are deleted.

fortran2ir no longer mirrors `use` statements. It records the use
associations the module's declarations depend on, each read from the
module declaring the entity, sharing one resolution pass with re-export
records. A re-export from a compiler-supplied module records the kind
`intrinsic`, which publication policy treats as unpublishable, so neither
the printer nor the planner keeps its own intrinsic-module check.

A renamed import written with capitals now binds the name its annotation
writes; it bound the normalized published spelling before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A type a module used in a signature and also published was spelled by two
decisions: export naming gave the published name a class spelling for
__all__, while the annotation, which no completion reached, wrote the use
statement's spelling. One import can bind only one of them, so either the
annotation or __all__ named something the contract never bound, and the
generated package could not be read back (from .shapes import Point
beside p: point).

Naming completion now records one spelling for every name a module
imports, under CONTRACT_IMPORT_NAMES_METADATA: a re-export's completed
name, or, for a type the module does not re-export, a class spelling held
in the same ledger. Type annotations, base classes, the import binding,
and __all__ all read it. A re-export is spelled by its entity whether or
not it is published -- a type as a class, a prototype as declared -- so the
dependency-only branch and the printer's and planner's prototype special
cases are gone. A name the module's own declarations use as a type is
spelled as one even where its declaring module was not read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
…llee

SemanticExpressionCallable.name is documented as the contract spelling,
but it kept the source spelling while contract-import completion bound the
callee under its completed name. A specification function named `lambda`
was imported as `lambda_` and one colliding with it as `lambda__2`, while
the shape still called lambda(n) -- not Python at all -- so such a function
could size an array neither from source nor from its contract. The parser
could not even read the call: a native name Python reserves failed the
expression parse.

The declaration-expression parser now sets a reserved native name aside
through Python's tokenizer and restores it in the tree, so the call is
read. Contract-name completion sets each callable reference's name to the
name the contract binds the callee by, and respells that call in the public
shape through the parsed expression, changing call targets only; native_name
and native_scope keep reaching the Fortran function. Imported calls are
spelled once, when the imported names are recorded, and calls to a
declared callable by that callable's identity, so completing again reads
the decision back. The planner binds each reference under the spelling it
carries.

A build's merged wrapper module now owns copies of its source modules.
Completing it for the build mutated the very modules the build's contract
package was written from, so the contract read spellings completed for a
different namespace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
The parser CLI converted each module alone and printed it without
completion or import planning, a second .pyi route beside the contract
pipeline. It now converts every inspected module together and emits them
through emit_module_stubs(), so --pyi shows the contract `prik generate
--pyi` writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A declaration expression may call only a pure function, and policy checks
the declaration it calls. A Fortran source records that purity, but a
contract could state it only on a prototype -- @pure elsewhere was
rejected and the printer never wrote it -- so any generated contract whose
array extents call an imported function read that function back impure
and failed to build with "must be pure", while the source it came from
built.

The loader now accepts @pure on a module-level function and records it the
way Fortran source does, and the printer writes it for a module function
whose Fortran attributes are pure. A method and an @overload dispatcher
still reject it: neither names the native procedure being described.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
The binding checks every declared extent it can evaluate, but a
specification function can only be evaluated in Fortran, so an axis the
bridge sizes was skipped. A shorter intent(out) actual such as
y(extent_for(n)) was then written past its end, corrupting the heap, and a
shorter intent(in) one was read past it.

Each argument with such an axis now has an argument_extent entrypoint
group: the bridge evaluates the declared extent into it, runs the native
procedure only when every actual matches, and the binding raises the same
TypeError a binding-checked extent raises, with the cleanup a native
status error uses. An omitted optional actual has no extent to check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
a2e4f7d guarded the native call alone. On a mismatch the bridge still
converted its actuals, allocated the result, malloc'd result storage, and
copied the never-written result into it; the binding then ran the
derived-transaction checks and the array copy-back before rejecting.

The bridge now evaluates each declared argument extent from its
parameters before anything else and runs the rest of the procedure only
when every actual matches, so a mismatch prepares, calls, and produces
nothing and returns a null pointer result. The binding rejects as the
first step after the call, releasing only what it took before the call.

That release -- string buffers, array temporaries, and native result
storage -- was spelled out in seven post-call failure paths; it is now
one helper they all read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
The entrypoint plan recorded only that an argument or result had an
extent group; the C binding and the Fortran bridge each re-derived which
axes the bridge evaluates and what the output carrying each is called.
NativeEntrypointParameterPlan now carries those extents -- axis and ABI
name -- and both backends read them: the paired _bridge_extent_axes,
_argument_extent_name, and _declaration_extent_result_* helpers are
deleted, and one group kind set covers result and argument extents alike.

The rejection boundary 1b26c74 drew gets the runtime guards the review
asked for: an array crossing through a COPY_F temporary is left untouched
by a rejected call, and a derived result a rejected call never produced
surfaces the shape error rather than a missing-result error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
fortran2ir still ran the one NamingPolicy allocation left in semantic
conversion: _settle_prototype_contract_names() gave each prototype a
Python spelling and rewrote SemanticPrototype.name, the callback
annotation's name, and its reference's local_name before export policy
ran. It held every use-associated name while doing so -- the model contract
imports have since replaced -- so a procedure-local interface was
suffixed for a name the contract never binds.

Conversion now keeps only identity: a prototype's module, declaring
procedure, and native name, and a callback reference to it. Contract-name
completion spells prototypes in the module's ledger -- a module's own
before any withheld declaration, a procedure-local one after, qualified by
its scope, both keeping their declared case -- and gives each callback
annotation its prototype's spelling; the printer reads both.

A callback's prototype policy was identified by module and name, so two
procedures each declaring a `cb` stayed apart only through the spelling
the converter had settled. Its identity now includes the declaring scope.

The settlement, fortran2ir's NamingPolicy import, and its helper walk
over a module's types are deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
ProcedureOverloadSet carries its own metadata, yet its Python export
decision was stored on its first specific, and four places encoded that:
_owner_metadata() in export policy, _declaration_metadata() in the build
pipeline, _entry_exports() in completion, and build_module_overload_policy
reading completed_python_exports(first). The generic was the conceptual
owner and an arbitrary specific the physical one, and a generic without
specifics had nowhere to keep a decision -- the adapter returned a
temporary {} that was discarded.

The decision now lives on the generic, every reader reads
declaration.metadata as it does for any other declaration, and the three
adapters and the empty-generic special case are deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Policy stopped selecting a native-kind copy for logical arrays once a
NumPy integer of each element's own width became their buffer:
_array_logical_argument_abi() returns only NOT_APPLICABLE or C_BOOL_VIEW,
with both copy flags false, and _array_writeback_abi() only NOT_APPLICABLE
or NATIVE_ARRAY. Everything that lowered or validated the other answers
stayed, reachable from no completed plan:

- ArrayLogicalABI.NATIVE_KIND_COPY and its reason string;
- the array_copy_in / array_copy_out policy and plan fields, their
  projection, and their consistency diagnostics;
- the array writeback ABI -- enum, policy and plan field, producer, and
  validator -- whose only distinguishing value was never produced;
- the bridge's logical-array copy initializers and finalizers, the
  exact-kind declarations, the low-bit writeback, and their helpers, plus
  _array_native_argument_expression(), which collapses into the boundary
  expression it wrapped.

A test that rejected an edited plan restoring the low-bit writeback pinned
behavior that no longer exists and is removed; the Boolean writeback test
keeps its invariant that the bridge passes the array straight through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A type's class, wrapper helper, and operation maps are defined in the one
namespace that defines the type, but generated code looked them up in the
namespace of the calling procedure. A procedure of another module returning,
accepting, or calling back with the type raised AttributeError, and a
generic or polymorphic argument of it never matched. The binding now
retains the module object of each namespace defining a type and fetches
the type's artifacts there; derived module variables, which already
worked this way through per-variable owners, use the same retained
namespace object. The callback context no longer carries a module.

Those artifacts, and the class constructor entrypoint, were also named
after the type's native spelling, so two modules declaring a type spelled
alike could not be built together. They are keyed on the backend symbol,
which is already qualified where native names collide. Docstrings find a
published class name by type identity for the same reason.

A plan defining one type in two namespaces is rejected before emission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
A generated class looked its base up among the classes of its own
namespace, so a type extending another module's type failed generation
with a KeyError. It now names the base, and the base's operation map,
through the namespace defining it, which module initialization binds into
the setup script's dictionary before it runs.

That namespace has to be set up first. Planning orders namespaces so each
comes after the ones defining the bases its classes extend, keeping path
order otherwise, and module initialization creates every namespace before
setting any of them up. Validation already required this order; it held
only when path order happened to agree.

The derived-types guide also states that an allocatable derived function
result must be allocated when the function returns. gfortran reads the
result before any code the bridge can generate sees it, so an unallocated
one cannot be turned into None.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN6oB7jC7wZXgFmuec4B7Q
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant