diff --git a/AGENTS.md b/AGENTS.md index 2744026da..f990188d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,63 @@ the selected plan requires a genuinely new emitted-code mechanism; those generators should otherwise keep reusing and dispatching existing planned paths. +A decision is read, not recomputed. Completed policy moving forward also means a +later stage must not derive the same answer a second time, which is harder to +notice than an override because the second site often calls the same helper and +so reads as reuse rather than as a second authority. When two places need one +answer, ask: **if these two call sites disagreed, which one would be wrong?** If +that has no answer, the decision has two authorities and no owner; if it has +one, the other site must read the answer rather than compute it. This applies to +derivation carrying state or a condition — a collision counter, a reservation +ledger, a language gate, a default — because that is what drifts; calling a +pure, total helper from several stages is fine. Read the owner's recorded +output: the completed policy, the shared plan, or the metadata the owner wrote. +Where a stage cannot run the owner's full completion, run the narrower +completion step for that one decision rather than deriving it again — contract +extraction must describe C that the direct-only wrapper would reject, so +`emit_module_stubs` completes public-name policy for every module and the rest +only where a build request allows it. Sharing the owner's helper is not enough +when the derivation keeps a ledger: two allocators fed the same declarations in +a different order produce the same set of names attached to different +declarations, which every per-stage test still passes. + +A fix removes an interpretation path or it does not land. "The regression is +fixed and the tests pass" is half an answer; the other half is **did this delete +a way of deciding, or add one?** A representation that has to mean several +things is the usual source of these bugs, and widening it with another flag or +another fallback leaves every existing reader intact and adds a reader. So when +a record cannot express a case, replace the record; when a lookup is reached by +two key shapes, finish the migration to one; when a completed decision is +ambiguous with an absent one, make completion record it; when a consumer +special-cases what a plan should have decided, move the decision into the plan. +Introducing a record, a small class, or a named reading is the preferred move +when it lets a reader see the rule in one place, and it does not need a separate +mandate: reach for it whenever it fixes the bug in fewer lines than another +branch would, and change an existing structure freely when replacing it is what +makes the code read more simply. Prefer that to a new condition threaded through +existing paths, which each reader then has to hold in mind. The one condition is +that the new thing is accepted only if it deletes the branches and helpers it +replaces — moving them to another module, or wrapping them behind a new name, +does not count. The practical test before committing: the file you changed +should be no harder to read than before, and the count of places that answer +your question should have gone down. + +Keep the regressions while doing it. The tests that pin bare, `only`, renamed +and repeated `use` forms, route accessibility, transitive re-exports, merged +generics, prototype collisions, exact `__all__`, and source-build versus +generated-`.pyi` replay are the specification of what PRIK supports; simplify +what sits under them, never by dropping the cases they cover. + +Where one decision reaches users through two artifacts, a test must compare +those artifacts rather than only check each one. A built extension and the +`.pyi` contract describing it are one such pair: each had passing tests while +the names they published disagreed, because nothing asserted that they agreed. +Treat the same comparison as a recommendation, not a requirement, for internal +pairs such as a wrapper plan and the sources generated from it. Watch for a +second policy or allocator instance, for a language, route, or flag gate at the +consumer that the owner lacks, and for a `prik/printers/` helper that returns a +name, kind, or decision rather than text. + To answer an ABI question, or to decide whether something belongs in the binding or in the Fortran bridge, first ask: **how would this work for a `bind(C)` procedure, where there is no bridge at all?** A direct entrypoint has diff --git a/CHANGELOG.md b/CHANGELOG.md index 746c4224f..f94345441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,643 @@ release tags add a leading `v` to the package version. ## Unreleased +- Optional Fortran callbacks and optional reference dummies inside callback + interfaces preserve `PRESENT()` through source and generated-contract builds. + +- A build named after one of its source modules -- the CLI's default, taken + from the first source -- no longer renames a type another of its modules + uses privately. Completing the merged build counted that use as an import, + although the build declares the type, so the class took `Box_2` in every + docstring while Python and the contracts published `Box`. A type the merged + module declares is now never one of its imports. + +- A rejected polymorphic argument names each accepted class the way its + contract declares it, rather than by a private name when the class is bound + under none; a rejected component value names its type the way the + component's declaration refers to it, as an argument's rejection already + did. + +- A type may extend a type another module declares. The generated class + looked its base up among its own namespace's classes, so such a build failed + with a `KeyError`; it now names the base through the namespace defining it. + Planning orders namespaces so the one defining a base is set up first, and + module initialization creates every namespace before setting any up. + +- The derived-types guide states that an `allocatable` derived function + result must be allocated when the function returns, as Fortran requires of + every non-pointer result. The compiler reads the result before the wrapper + can, so an unallocated one is a native error rather than `None`. + +- A type is usable from every module whose procedures take or return it. + Generated code looked a type's class and wrapper helper up in the namespace + of the calling procedure, so `box_ops.boxed()` returning a `shared_types` + type raised `AttributeError`, and a callback result, a polymorphic or + generic argument, or a component of another module's type failed the same + way. The binding now retains the module object of each namespace defining a + type and fetches its class, wrapper helper, and operation maps there; a + derived module variable, which already worked this way, uses the same + mechanism. A plan that defines one type in two namespaces is rejected + (`duplicate-derived-type-identity`). + +- Two modules may each declare a type spelled alike. A type's constructor, + wrapper helper, and operation map were named after its native spelling, so + such a build failed with `Generated support procedure entrypoint symbols + are not unique`; they are now named after its backend symbol, which is + already qualified where native names collide. Docstrings find a type's + published name by its identity for the same reason. + +- The retired Boolean-array copy is removed. Policy stopped selecting a + native-kind copy or a post-call low-bit normalization for logical arrays + once a NumPy integer of the element's own width became their buffer, but + the `NATIVE_KIND_COPY` array ABI, the `array_copy_in`/`array_copy_out` + plan fields, the array writeback ABI, and the bridge code lowering them + remained, reachable from no completed plan. + +- A generic owns its Python export decision the way every other declaration + does, in its own metadata. It was stored on the generic's first specific, + so three readers each reached through that specific and a generic without + specifics could not record a decision at all. + +- A prototype's contract spelling is completed in post-IR policy, in the same + ledger as every other name, rather than allocated while Fortran source is + converted. Conversion keeps a prototype's identity -- its module, declaring + procedure, and native name -- and a callback reference to it; completion + spells both, and a callback's generated interface is identified by that + structure rather than by its spelling. A procedure-local interface is no + longer suffixed for a name the module reaches through `use` but whose + contract never binds it (`first_cb` rather than `first_cb_2`). + +- An array argument sized by a specification function is checked against the + extent its dummy declares. The binding checks every other declared extent, + but only the Fortran bridge can evaluate a specification function, so a + shorter `intent(out)` actual such as `y(extent_for(n))` was written past its + end and corrupted the heap, and a shorter `intent(in)` one was read past it. + The bridge now evaluates the declared extent from its parameters before + anything else and runs the rest of the procedure only when the actual + matches, so a mismatch prepares, calls, and produces nothing; the binding + raises the same `TypeError` a mismatched extent always raises before any + other post-call step. An omitted optional actual is not checked. + +- A generated contract states `@pure` on a pure module function, and a + contract may write it there. A declaration expression may call only a pure + function, and `@pure` was accepted only on prototypes, so any generated + contract whose array extents call an imported function failed to build with + "must be pure" while the source it came from built. + +- A call in a declaration expression is spelled the way the contract binds its + callee. A Fortran specification function named `lambda`, or one whose name + collides once escaped, was imported under its completed name (`lambda_`, + `lambda__2`) while the shape still called `lambda(n)`, which is not Python; + such a function could not size an array at all, from source or from its + contract. Contract-name completion now sets the callable reference's name to + that spelling and respells the call in the shape expression through the + parsed expression, changing call targets only, while the native identity + still reaches the Fortran function. A build's merged wrapper module now owns + copies of its source modules, so completing it no longer changes the + contracts written beside the build. + +- The Fortran parser CLI's `--pyi` report emits every inspected module together + through the contract pipeline, so it shows the contract `prik generate --pyi` + writes rather than an unplanned rendering of each module alone. + +- A generated contract imports what it needs to bind, not the `use` + statements its source wrote. A facade extending a generic it `use`s from two + modules imported `convert` from each of them beside declaring the merged + generic itself, so the package it generated could not be read back and only + the source build worked. Imports are now completed once, before emission, by + `complete_contract_imports()`: a contract binds the names its declarations use + and the names it publishes, each from the module declaring it, and a name + reached twice through re-exporting modules binds once. The `.pyi` printer + renders those statements and no longer chooses or spells an import. A `use` + whose names no declaration mentions and the module does not publish, such as + a bare `use types_mod`, is no longer written into the contract. + +- An imported name is spelled one way throughout a contract: the way the module + publishes it. A type the module used in a signature and also published was + imported under one spelling while `__all__` or the annotation wrote another + (`from .shapes import Point` beside `p: point`), so the generated package + could not be read back. The import, the annotation, and `__all__` now read + one completed name, and a type is spelled as a class whether or not the + module publishes it: `from .shapes import Point` and `p: Point`, or + `from .shapes import Point as Mypoint` for `use shapes, only : MyPoint => + point`. A prototype keeps the spelling it is declared with everywhere. + +- A contract publishes exactly its `__all__`. A type it left out -- such as + the sibling type a leaf contract imports for its signatures -- was still + bound in the built module under its own name, because export completion left + it undecided and wrapper policy defaulted an undecided declaration to + publishing itself. Completion now records every declaration's decision, + publishing nowhere included, and wrapper policy fails on one it never + completed. A type published nowhere still exists natively and as a Python + class, so the procedures taking and returning it work. A class written inside + another is bound on its parent (`module.outer.inner`) rather than in the + module namespace. A cleanup action on a returned derived object reads its + family from the transfer's derived handoff, so a result type written through + an import alias (`-> box` for `from .shared_types import Box as box`) no + longer fails planning. + +- Contract spelling is now completed once in post-IR policy for every + declaration, including withheld helpers and class members. Generated + contracts, cross-module import spelling, and class-surface policy read that + decision directly; contract emission no longer allocates names or renders a + module to discover them. + +- A scope's `use` statements are kept as a flat, immutable list and read by one + resolver. A procedure inheriting its module's imports could previously append + to the very list the module held, so `use dep, only : y` written inside a + contained procedure reached module accessibility and re-export analysis. Two + statements spelling one module differently (`use DEP` beside `use dep`) were + also held apart, and a local name reached by two entities -- `use dep, x => y` + where `dep` also publishes `x` -- silently resolved to one of them instead of + being reported ambiguous. Callback interfaces, declaration-expression + procedures, and derived types now reconcile the same candidate routes rather + than interpreting renames independently. + +- A type-bound defined assignment updates the method it names. Making a + specific's identity structural left two helpers looking the original up by + bare name, so `generic :: assignment(=) => assign_value` projected its bound + object on the generic's candidate while the method itself kept the + unprojected signature. + +- Every legal `use` form is now represented, and several statements naming one + module are read together. `use m, only :` is valid and imports nothing, yet + was indistinguishable from a bare `use m`; `use m, only : p => q` followed by + `use m` discarded the first statement entirely; and a rename without `only` + still dropped every other name its module offered -- losing imported + compile-time symbols, callback interfaces and derived types along with them. + The parser keeps each statement as written and one reading interprets them. + +- A contract's `__all__` selects declarations by exact spelling, and a + declaration already projected to no Python namespace keeps that projection. + Names were compared case-insensitively, so `__all__ = ["Foo"]` published a + declaration written `foo` although Python names are case-sensitive; and an + empty export list read as "nothing decided yet", so a default publication + replaced a decision an earlier stage had taken. + +- A procedure-local prototype cannot take a name its module imports. Allocating + its contract spelling held only the module's declared names, so a module + importing `first_cb` and declaring `cb` inside `first` wrote a prototype that + shadowed the import the contract also writes. + +- A merged generic keeps specifics that two contributing modules spell alike. + Specific procedures were looked up by name alone, so a second contributor's + `to_value` looked like the first and was dropped, losing a signature the + generic must dispatch over. A specific is now identified by the module + declaring it, and a contract writing two of them gives each its own Python + name and names it in the matching `@overload(...)`. + +- A `use` that only renames still carries the rest of its module. The parser + recorded `use m, p => q` exactly as `use m, only : p => q`, so everything + else `m` publishes was dropped, and the renamed entity was also still + reachable under its own spelling. A mapping now records whether its statement + narrowed to an `only` list, which is what separates the two forms. + +- A contract's `__all__` decides what it publishes when it is read back. A + prototype and a generic are written into the body so annotations and dispatch + resolve, and both read back public by default, so a contract that withheld + them from `__all__` still had a public export completed for them. Export + policy now names only the surface the contract states; the declarations stay + written and reachable for naming and import resolution. + +- A prototype is identified by the scope declaring it, and its contract + spelling is allocated against the names the module already holds. Joining the + scope to the name produced a spelling that could collide with a real + declaration -- a module-level `first_cb` beside `first`'s own `cb` -- so the + two became one prototype and a callback was typed by the other's signature. + Two scopes whose joined spellings coincided (`a_b` declaring `c`, `a` + declaring `b_c`) collided the same way. Each now keeps a distinct contract + name, and a module's own block still publishes the spelling another module + imports it by. + +- A generic assembles the specifics of every accessible interface that + contributes to it. A module importing `convert` from two modules that each + declare a generic of that name kept only the first, silently losing the + other's specific procedures, and importing both without declaring one locally + dropped the name entirely as an ambiguity. Contributors are now gathered in + source order through every route, transitively, with one declaration counted + once; accessibility still applies at each hop, and a generic is still not + published into a second Python namespace. + +- A use-associated name is resolved from every route carrying it, whichever + way each route entered. A module writing `use a_mod, only : x` beside a plain + `use c_mod` that also offers `x` reaches two different entities, and the + named route was examined first and published `a_mod::x` as the canonical + owner -- which a re-exported module variable then generates native access to + directly, so the Fortran compiler never diagnoses the ambiguity. Routes that + name one entity still resolve, and a plain `use` of a module PRIK never read + carries no assumed name. + +- An enum's enumerators are carried by `use` like the constants they are. A + plain `use` of a module declaring `enumerator :: red = 1` carried nothing for + `red`, and naming it in an `only` list produced a re-export of unknown kind, + which the module-variable publication machinery does not attach. Enumerators + are now read as variables wherever this layer reads a module's declarations, + including as declaration dependencies when an enumerator's value names an + imported constant. + +- Following a name through an intermediate module applies that module's own + accessibility. A module importing `x` and declaring `private :: x` no longer + passes a route to the declaration behind it, and a module reaching two + different `x` no longer resolves to whichever route was read first. Both + cases are now reported unresolved, as a direct import of two disagreeing + routes already was. + +- A generated contract publishes a prototype or a generic only where the + module makes it reachable. A `private` abstract interface and a `private` + generic were written into `__all__` although the module keeps both to + itself, and an interface block written inside a contained procedure was + promoted to a module publication -- so two procedures each declaring + `abstract interface ... cb` shared one prototype, and the second was given + the first's signature. Such a block now takes its own scope-qualified + contract name, stays out of `__all__`, and each procedure's callback is + typed by the interface its own scope declares. + +- Generated Fortran continuation never breaks a line inside a character + literal. A long call was split at every comma, so an argument such as + `'alpha, beta'` continued mid-literal and compiled to `alpha, beta` -- a + different string, with no diagnostic. Lines now break at the call's own + arguments, which also keeps a nested call whole. + +- A character constant may state its kind before the opening quote, so + `character(kind=c_char, len=3), parameter :: tagged = c_char_'abc'` is + published and returned as `abc`. The kind-prefixed spelling was not + recognized as a literal at all, leaving the parameter with no value and the + build refusing it as an unsupported module variable. One reader now decides + what a whole character literal is, so `'a' // 'b'` is recorded as the + expression it is rather than as a literal. + +- A character parser model records its selector through + `FortranVariable.record_character_selector`, which reads the length, the + kind, and whether the stored text is a length in one place. A model built by + hand -- the type-mapping report's rows, a test -- now states the same facts a + parsed declaration does instead of leaving them to a second reader of the + joined `kind` text. Semantic conversion reads that recorded selector as the + authority for a character kind, so a model carrying only the selector is not + reported as the default character kind. + +- Compile-time specialization reaches only the fields that hold declaration + expressions. Every semantic metadata string was resolved as an expression, + so a recorded decision spelling a parameter's name -- a pointer's + `runtime` association in a module that also declares `integer, parameter :: + runtime` -- was replaced by that parameter's value. Shapes, bounds, + character lengths, initializers, and default values specialize as before. + +- A character declaration's kind is read whole, so + `character(len=8, kind=max(c_char, 1))` states the kind it writes. The kind + was found again inside the selector's joined spelling with a pattern that + stops at the first comma, cutting a kind expression holding a comma of its + own down to `max(c_char`. + +- A Fortran character constant reaches Python holding the characters it + declares. Fortran doubles a quote to hold one, which Python reads instead as + two literals written side by side and joins, so + `character(len=5), parameter :: word = 'don''t'` was published and returned + as `dont` -- four characters under a declared length of five. Both the + generated contract and the built extension now state `don't`. + +- A generated contract states a character constant's own contents. Respelling + the Fortran spellings Python writes differently reached inside the literal + too, so `character(len=6), parameter :: text = ".true."` was published as + `Final[String[6]] = 'True'` -- a different value, and one contradicting its + own declared length. A logical or a real written the same way outside quotes + is respelled as before. + +- A raw address contract accepts an extent built from the declaration calls + PRIK supports, so `Addr(Float64[max(n, m)])` and `Addr(Float64[abs(n)])` are + no longer refused as unresolved. Deciding that by scanning the extent's text + counted the call's own name among the values it reads, which no argument + carries. An extent naming something no argument supplies, a runtime extent, + and an unsupported call are refused as before. + +- TA-Lib's pinned reference harness now preserves binary64 array inputs across + its preliminary JSON self-checks, preventing architecture-dependent BETA + mismatches without weakening the 322-indicator PRIK comparison. + +- A module variable's canonical wrapper plan is now owned by its declaring + native module and name. Adding, removing, or renaming Python facades changes + only namespace publications, so support-operation and holder identities no + longer move between facades. + +- Generated Fortran contracts distinguish a module's dependencies from its + Python publications without changing Fortran accessibility. An implicitly + public imported name used by that module's own declarations remains + semantically reachable through the module and available to express the + contract, but reaches Python there only when named in a `public` statement. + +- Fortran use-association accessibility now honors `public` and `private` + statements that name an imported module, including entities reached through + multiple routes. Plain `use` discovery also carries named generic and + abstract interfaces through the semantic accessibility graph. + +- A contract may publish a module variable only through a facade, leaving the + namespace declaring it out of Python entirely. Owning the one native variable + plan used to put that namespace there anyway, so a package hiding its + declaring module exposed it regardless. + +- A published Fortran `parameter` is documented as what it is. Each namespace + receives the declared value as an ordinary Python attribute: assignment is + not refused, rebinding one name leaves the Fortran parameter unchanged, and + it does not rebind any other namespace publishing the same parameter. The + reference previously called this a read-only constant. + +- A module publishing a re-exported name links again when that alias is the only + thing it needs a bundled helper for. Binding an alias calls one, but a module + with no arguments, results, module variables or derived-type fields was + treated as needing none, so the generated extension referenced + `prik_bind_namespace_alias` without carrying it and failed to import. + +- A module-variable re-export now publishes another live route to the declaring + variable instead of being omitted or rejected. Every namespace reuses one + completed variable plan and its native accessors, so scalar assignment, array + mutation, allocation, pointer association, derived state, and read-only + parameters retain one native identity. Generic interfaces remain publishable + only by their declaring namespace. + +- A wrapper's Python names are decided once, by post-IR export policy, and + every stage that writes a name reads that decision. The `.pyi` printer + allocated its own instead, so a build and the contract describing it could + disagree: a C function named for a Python keyword built as `lambda_` while + its contract said `def lambda(`, which is not Python at all. Worse, the two + allocators walked declarations in different orders -- policy takes classes, + functions, overloads, then variables, the printer took variables before + functions -- so a module variable and a procedure whose names both normalize + to `lambda_` were settled one way by the build and the other way by the + contract. The contract then held the right set of names attached to the wrong + declarations. A contract now names exactly what the build beside it + publishes, derived types included, and records the source spelling with + `@bind` wherever the two differ. + +- A wrapped type is spelled like the Python class it becomes. Fortran writes + one declaration under many spellings, so PRIK picks one, and picking + `point_t` for something used as `Point_T(...)` read as a function. A derived + type now publishes with each underscore-separated word capitalized -- + `type :: point_t` reaches Python as `Point_T` -- while every other Fortran + declaration stays lowercased. Renaming one in the contract still works, so + this is a default rather than a constraint. This changes the published class + names of existing Fortran wrappers. + +- A C declaration keeps the case it is written in. Folding it is a Fortran rule, + correct there because Fortran writes one declaration many ways and none of + the spellings is its own. C names each declaration exactly, so folding both + lost that name -- `BarBaz` reached Python as `barbaz` -- and invented + collisions the source does not have: `Foo` and `foo` are two functions, and + they arrived as `foo` and `foo_2` with nothing to say which was which. This + changes the published names of existing C wrappers. Folded import lookup now + succeeds only when it identifies one declaration, rather than selecting an + ambiguous spelling by insertion order. + +- The contract a source build writes beside its artifacts states published + Python names. It stated raw source spellings, so a Fortran build wrote + `def SCALE_VALUE(` next to a module exposing `scale_value`. + +- A re-exported declaration is owned by the contract declaring it, whichever + order an entry contract imports from. The namespace encountered first owned + it, so a facade listed before the module it reads from took ownership of a + procedure it only republishes. + +- A wildcard import reads the surface its dependency publishes, where it used to + take every name that dependency held. A withheld name stays reachable by + asking for it, which a contract needing it to express a declaration -- or + meaning to publish it itself -- still does. + +- A contract's `__all__` names its sub-namespaces as well, so leaving one off + keeps the package from exposing it. A generated entry contract states the + modules it imports for that reason, and a contract stating no list still + publishes everything it reaches. + +- A contract states everything it publishes in a closing `__all__`. An import + cannot say whether a name is needed to express a declaration or meant to be + published, because a rename reads the same either way, so the list settles it. + PRIK writes what the source publishes -- the module's own public declarations, + explicitly public imports, and implicitly accessible imports that are not + declaration dependencies -- and the list is there to be edited: remove a name + to stop publishing it, add an imported one to publish it, or remove the list + to publish everything the contract reaches. Reading C + source states the same thing through `--export-symbols` / + `build_c_extension(export_symbols=...)`, which selects the source-side public + surface and writes the corresponding Python names into the generated + contract's `__all__`. + +- A re-exported procedure binds the callable its declaring module exported + rather than being wrapped again, so a contract build gives the same object a + source build does, under a renamed re-export as well. + +- A published name is followed to the module declaring it, however many + modules published it along the way. Reading only the module a `use` names + left a name published twice over looking like nothing at all, and the + re-export was dropped. + +- A generated contract states a re-export by aliasing the name to itself, the + way a stub marks anything it publishes, so an import written to express a + declaration is no longer republished. Source and contract builds agree on + what a module exports; a package entry contract still selects its surface by + importing, which is what such a contract is for. + +- A generic declared inside a procedure is no longer read as one of its + module's own, and an import binds the name a collision made its declaring + contract use rather than one derived from the source spelling. + +- Publishing an imported name re-exports it at runtime only where the name is + one Python object to bind. A module publishing an imported callback prototype + states where a signature comes from, and a signature is not an object, so + binding one reached for an attribute of a module that exports nothing and the + build failed outright. Each re-export now records what it publishes, and only + a procedure or a derived type becomes a runtime alias; every other kind keeps + to the semantic and contract-import paths that already carry it. + +- A re-export binds the Python name its declaring module actually published + rather than the Fortran spelling it was written with, so publishing an entity + spelled in capitals no longer looks up an attribute that does not exist. + +- A plain `use` now re-exports the accessible names it carries unless a name is + only a dependency of the importing module's declarations. An explicit + `public` statement still publishes that dependency; an origin that two used + modules could supply stays unresolved rather than guessed. + +- A generic interface built from several blocks merges within the scope + declaring it. Two procedures of one module may each declare an interface of + the same name, and merging them on the module they share let one procedure's + specifics answer the other's calls. + +- A generated contract writes an overload's target and a prototype import the + way the contract declaring them spells each one. The overload named a source + spelling that matched no declaration it holds, and a prototype's spelling was + kept for every module using that name rather than the one declaring it. + +- A contract can now rename what it declares. `SourceName` states the native + entity a variable or constant reaches, the way `bind` already did for a + callable, instead of replacing the name the declaration states -- editing a + contract to give an entity a Python name exported the source spelling and + dropped the edit. A source name inside `Final[...]` reaches its declaration + as well, where it was previously ignored. A generated contract is affected + too: a Fortran entity Python cannot spell, such as one named `lambda`, is + declared as `lambda_` and now stays reachable under that name. + +- A class can state the native type it reaches through `bind`, so a derived + type can be exported under a different Python name. An imported class + reference resolves through the name its declaring contract states, and a + renamed class keeps its `bind` when the contract is regenerated. + +- A generated Fortran contract no longer records a source spelling that differs + from its Python name only by case. Fortran names entities without regard to + case, so a capitalized `IK` written as `ik` renames nothing and the generated + Fortran reaches it either way; every such declaration nevertheless carried a + `SourceName` or `@bind` stating the capitals back. A name Python cannot hold + as written -- a keyword, an illegal character, one a collision moved aside -- + is a real rename and still keeps its original, as does every name from a + source language that is case-sensitive. + +- A generated contract now imports each name under the spelling the contract + that defines it uses. A source-derived contract declares a Fortran entity + under a Python name, so one spelled in capitals is declared lower case, while + the import kept asking for the source spelling and named nothing the + dependency defines -- loading the package back failed on it. A prototype is + unchanged: it keeps its declared spelling wherever it is written, so an import + binding one keeps it too. + +- An overload declaration whose specific projects an output argument into its + result is now accepted. The check compared the declared result against the + projected one including the write-through the native argument passing states, + and a native scalar descriptor result including the descriptor topology that + only a `native_call` result wrapper can name -- neither of which a declared + result type spells. A generated contract carrying such a generic, for example + one over `intent(out)` allocatable arguments, was rejected on read-back by the + same tool that wrote it. + +- A contract generated from a source whose abstract interface types a dummy + through a kind of its own now resolves that kind. An interface body's + variables reached no target probe, so a kind named only there -- through a + `use` written inside the body -- had no storage fact and `generate --pyi` + failed on a declaration the wrapper build accepted. + +- A derived type building one generic binding from several `generic ::` + statements now collects every specific into that binding. Each statement was + recorded as its own binding of the same name, so only the first reached + dispatch and calling the generic with the argument types of any later + statement raised `no matching overload`. + +- A scope naming the same module in several `use` statements now keeps every + import. Each statement was replacing the previous one, so only the last + survived; a module splitting a long import list across lines silently lost + the names the earlier lines carried, and any kind parameter among them stopped + resolving. + +- A procedure whose outputs have no completed ordering is now reported as an + unsupported wrapper policy instead of raising a comparison error. + +- Generated Fortran module leaves now import sibling contracts relatively, so + building a leaf directly loads the contracts its declarations depend on. + A native derived type exported through several modules shares one set of + generated support procedures. + +- A module that names an imported procedure in a `public` statement now + publishes it, so a facade module reaches Python instead of disappearing. The + declaration is not repeated: the published name binds to the one wrapper its + declaring module exposes, so `facade.proc is home.proc`, and the contract + keeps spelling the re-export as the import it already was. A name public only + because the module default is public states no such intent and is unchanged. + +- A generic interface that repeats a `use`-associated name now extends that + generic instead of replacing it, so the importing module dispatches to the + specifics it inherited as well as its own. Accumulation stays one-directional, + as Fortran requires: the declaring module does not gain what a later module + adds. An inherited specific is reachable only through the generic, because the + import never bound its own name. + +- A generic interface may now be declared across several blocks in one scope, + which Fortran allows and real sources use to add specifics under + preprocessor guards. The blocks become one generic carrying every entry in + declaration order, instead of being rejected as a duplicate declaration. + +- A callback interface reached through renaming re-exports now records the name + its declaring module gives it. The reference followed the module back to the + declaration but kept an alias from partway along the chain, so it named a + symbol that module does not define. + +- The semantic IR now carries a strided axis as `::`, the spelling a contract + uses, instead of a longer internal token. `prik semantics` output changes + accordingly; contracts, docstrings and generated sources are unaffected + because they already printed the contract spelling. + +- Removed the `Strided` contract name and the dimension step that carried it. + `T[::]` already spells a strided axis and `T[:]` a contiguous one, so the + longer `T[::Strided]` and `T[0:n:Strided]` forms are gone rather than kept as + a second way to write the same contract. A value in a dimension's step + position is now rejected with a message naming the spelling to use. + +- A callback interface's result now keeps the declaring module's type identity, + matching its dummies. An imported function interface returning a type its own + module declares previously attributed that type to the consuming module and + failed to build, both from Fortran source and from a generated contract. + +- A renamed callback import keeps the declared interface name beside the local + one, so a contract imports `OBJ as LOCAL_OBJ` rather than a name the declaring + module never defines. A reference that differs from the declaration only in + case is now spelled canonically instead of binding a second name. + +- Following a re-exported callback interface respects Fortran accessibility. A + module that imports an interface privately no longer exposes it to a later + `use`, and the rule applies at every hop of a chain. + +- An abstract interface imported from another module now converts in the scope + of the module that declares it. A derived type the interface names belongs to + that module, so wrapping a consumer that imports only the interface — and not + the types it mentions — no longer fails against a type identity attributed to + the consuming module. + +- Callback interface resolution now covers a `use` inside a single procedure, a + standalone procedure's own imports, and an interface re-exported through any + number of modules. File, project, and `generate --pyi` conversion share one + resolver rather than each carrying its own lookup, and a contract that + re-exports a prototype resolves back to the module that declares it. + +- A contract now imports a prototype it references but never declares, so an + interface named by a procedure-local `use` is bound in the generated `.pyi` + instead of appearing as a free name. + +- Callback docstrings now state each array argument's rank and extents. Every + generated docstring and diagnostic spells a runtime extent with the shorthand + a contract uses (`Float64[::]`) rather than the explicit step the IR stores + (`Float64[::Strided]`); the two are the same contract, while `Float64[:]` + remains the distinct contiguous one. + +- A primitive scalar callback dummy the callee may write now reaches Python as + rank-zero storage (`Out(Float64[()])`) instead of an independent value, so + the value the callback computes reaches the native caller. This covers + `intent(out)` and `intent(inout)`, and also a dummy with no declared + `intent`, which Fortran permits the callee to modify — that case keeps its + missing direction in the contract as a bare `Float64[()]` rather than gaining + a synthesized one. `--assume-intent-in-scalars` elects the input-only default + for it instead. Python has no writable scalar, so the previous `Out(Addr(T))` spelling + silently discarded the write; it is now a policy error naming the replacement. + A prototype still mirrors the native argument list — edit it with + `@native_call` to project an output into the callable's return value instead. + +- Generated docstrings now state a callback's exact callable signature — + arity, per-argument direction and element type, how an output is delivered, + and the lifetime and fatal-error rules — taken from the same completed + prototype the trampoline is generated from. + +- Assumed-shape array arguments are now supported inside a callback prototype. + A `procedure(iface)` dummy whose interface declares `values(:)` lowers to an + assumed-shape bridge dummy and a contiguous call-local copy measured from it, + instead of emitting an invalid array declaration. Array callback *results* + still require an exact shape and now report that directly. + +- A dummy procedure's interface name keeps the spelling it was declared with. + Generated `.pyi` contracts previously annotated `procedure(OBJ)` as `obj` + while importing `OBJ`, so PRIK could not rebuild from the contract it had + just written. + +- `prik generate --pyi` now resolves an abstract interface imported from + another supplied source file, matching multi-file wrapper builds. + +- A `procedure(iface)` dummy whose interface no supplied source declares now + reports the interface by name and asks for the module that declares it, + instead of failing against an opaque placeholder type. Contract extraction + spells that interface name so the generated `.pyi` stays consistent with the + import it already emits. + ## 0.5.0 — 2026-09-13 - Added CMake integration through the packaged `UsePRIK.cmake` helper and a diff --git a/docs/developer/codebase-map.md b/docs/developer/codebase-map.md index e5f5fd4d2..c9662faff 100644 --- a/docs/developer/codebase-map.md +++ b/docs/developer/codebase-map.md @@ -39,7 +39,7 @@ boundary; the modules are where the change lands. | Prepared source, provenance, and target facts | [`prik.preprocessing`](packages/preprocessing.md) | `source.py`, `fortran.py`, `c.py`, `probes/fortran_types.py`, `probes/c_types.py` | | Parsed language facts | [`prik.parsers`](packages/parsers.md) | `fortran/parser.py`, `pyi/parser.py`, `c/` | | Shared language-neutral meaning | [`prik.semantics`](packages/semantics.md) | `models.py`, `fortran2ir.py`, `pyi2ir.py`, `c2ir.py`, `scalar_types.py` | -| Completed interoperability policy | [`prik.policy`](packages/policy.md) | `completion.py`, `construction.py`, `ownership.py`, `exports.py`, `native_array_handles.py` | +| Completed interoperability policy | [`prik.policy`](packages/policy.md) | `completion.py`, `construction.py`, `ownership.py`, `exports.py`, `contract_imports.py`, `native_array_handles.py` | | Deterministic wrapper planning | [`prik.planning`](packages/planning.md) | `models.py`, `planner.py`, `entrypoints.py` | | Binding, bridge, and Python-facade lowering | [`prik.codegen`](packages/codegen.md) | `c/binding.py`, `c/python_surface.py`, `fortran/bridge.py`, `primitive_scalar_types.py` | | Generated-text serialization | [`prik.printers`](packages/printers.md) | `c.py`, `fortran.py`, `pyi.py` | diff --git a/docs/developer/packages/codegen.md b/docs/developer/packages/codegen.md index 5d9885219..5725d0552 100644 --- a/docs/developer/packages/codegen.md +++ b/docs/developer/packages/codegen.md @@ -212,20 +212,32 @@ python3 prik/codegen/c/python_surface.py Rendered Python facade: _prik_unset = object() -_prik_ops_state = {} +_prik_ops_state_t = {} class State: 'Opaque native state.' __slots__ = ('_prik_capsule', '_prik_owner', '_prik_ops', '_prik_origin') def __new__(cls, *args, **kwargs): 'Construction is disabled.' raise TypeError('State objects come from native code.') -def _prik_wrap_State(capsule, owner=None, ops=None, origin='direct'): +def _prik_wrap_state_t(capsule, owner=None, ops=None, origin='direct'): ... ``` The slots, rejected constructor, and wrapper helper are generated from that class plan. They show the planned Python surface without selecting its native -lifecycle policy. +lifecycle policy. The operation map and wrapper helper are keyed on the type's +backend symbol, which stays unique when two modules declare a type spelled +alike. + +A type is defined in one namespace, and generated code taking or returning it +may live in any other. The binding therefore retains the module object of each +namespace that defines a type, and fetches the class, its wrapper helper, and +its operation maps from there rather than from the calling namespace. A +derived module variable's helpers live beside its type, so its getter reaches +them the same way. A class extending a type another namespace defines names its +base through that namespace too: module initialization creates every namespace +first, then sets them up in plan order, and binds each namespace a setup script +reaches into its dictionary before the script runs. ## Tests And Evidence diff --git a/docs/developer/packages/codegen/c-binding.md b/docs/developer/packages/codegen/c-binding.md index 6365e081c..86a614fea 100644 --- a/docs/developer/packages/codegen/c-binding.md +++ b/docs/developer/packages/codegen/c-binding.md @@ -125,6 +125,7 @@ plan = ModulePlan( binding=BindingModulePlan(...), entrypoint=NativeEntrypointModulePlan(...), bridge=BridgeModulePlan(...), + variables=(), namespaces=(namespace,), ) @@ -196,6 +197,7 @@ plan = ModulePlan( binding=BindingModulePlan(owner_path="demo"), entrypoint=NativeEntrypointModulePlan(owner_path="demo"), bridge=BridgeModulePlan(owner_path="demo"), + variables=(), namespaces=(namespace,), native_generated_code_groups=( NativeGeneratedCodeGroupPlan( diff --git a/docs/developer/packages/codegen/fortran-bridge.md b/docs/developer/packages/codegen/fortran-bridge.md index edf3a0f30..3984433dc 100644 --- a/docs/developer/packages/codegen/fortran-bridge.md +++ b/docs/developer/packages/codegen/fortran-bridge.md @@ -105,6 +105,7 @@ plan = ModulePlan( binding=BindingModulePlan(...), entrypoint=NativeEntrypointModulePlan(...), bridge=BridgeModulePlan(...), + variables=(), namespaces=(namespace,), ) @@ -176,6 +177,7 @@ plan = ModulePlan( binding=BindingModulePlan(owner_path="demo"), entrypoint=NativeEntrypointModulePlan(owner_path="demo"), bridge=BridgeModulePlan(owner_path="demo"), + variables=(), namespaces=(namespace,), native_generated_code_groups=( NativeGeneratedCodeGroupPlan( diff --git a/docs/developer/packages/naming.md b/docs/developer/packages/naming.md index 448ec98e7..16d4a1075 100644 --- a/docs/developer/packages/naming.md +++ b/docs/developer/packages/naming.md @@ -19,10 +19,13 @@ choose exports, ownership, wrapper support, or emitted syntax. ## The Two Naming Routes ```text -source spelling + public namespace +source spelling + contract namespace -> normalize Python identifier -> reserve it or add a collision suffix - -> public export name + -> completed contract spelling + +completed contract spelling + publication policy + -> zero or more public export placements owner identity + preferred generated name + target rules -> escape reserved or special names @@ -30,7 +33,7 @@ owner identity + preferred generated name + target rules -> deterministic native symbol ``` -Public names and generated symbols are deliberately separate. Escaping a +Contract names and generated symbols are deliberately separate. Escaping a Python keyword must not rename the underlying Fortran symbol, and a C or Fortran restriction must not change the public Python API. @@ -54,7 +57,13 @@ prik/naming/ `NativeSymbolNames.compact()`. It combines a readable prefix with a hash of the full owner identity under a requested length limit. -`NamingPolicy` retains public reservations for one construction operation. +`NamingPolicy` retains contract-namespace reservations for one policy +completion operation. Post-IR policy records the selected spelling on semantic +owners; contract emission and class-surface construction read that result and +do not create their own reservation ledgers, and neither does semantic +conversion: a prototype, too, is spelled in that ledger, keeping the case it is +declared in. Publication is separate: a withheld declaration still has a +contract spelling so annotations can name it. `NativeSymbolNames` is stateless: the same owner, preferred spelling, and limit always produce the same result. diff --git a/docs/developer/packages/parsers.md b/docs/developer/packages/parsers.md index 831a576eb..a55f7afe1 100644 --- a/docs/developer/packages/parsers.md +++ b/docs/developer/packages/parsers.md @@ -62,6 +62,7 @@ prik/parsers/ │ ├── lexer.py │ ├── models.py │ ├── parser.py +│ ├── scope.py │ ├── type_resolver.py │ └── utils.py ├── pyi/ @@ -85,9 +86,10 @@ prik/parsers/ | [`prik/parsers/fortran/utils.py`](../../../prik/parsers/fortran/utils.py) | `detect_source_form()` chooses fixed or free form; `split_csv()` separates only top-level Fortran comma lists. | Source-form detection or grammar-neutral list splitting changes. | | [`prik/parsers/fortran/lexer.py`](../../../prik/parsers/fortran/lexer.py) | `preprocess_lines()` produces logical lines with original coordinates; `strip_comment()` preserves string literals and OpenMP directives. | Comment handling, continuation folding, or location preservation changes. | | [`prik/parsers/fortran/models.py`](../../../prik/parsers/fortran/models.py) | Passive source-fact records: `FortranFile`, `FortranProject`, units, declarations, shapes, and `FortranParseError`. | A parser result, source fact, or diagnostic representation changes. | +| [`prik/parsers/fortran/scope.py`](../../../prik/parsers/fortran/scope.py) | `ScopeUses` aggregates a scope's `use` statements and is the authority for rename semantics, accessible local names, and candidate routes. Semantic consumers decide only what those routes mean for their entity category. | `use` association or scope dependency interpretation changes. | | [`prik/parsers/fortran/type_resolver.py`](../../../prik/parsers/fortran/type_resolver.py) | `extract_kind_from_type_spec()` preserves intrinsic kind and character syntax after declaration parsing. | Parser-level type-spec spelling extraction changes. | | [`prik/parsers/fortran/parser.py`](../../../prik/parsers/fortran/parser.py) | `FortranParser`, `parse_fortran_file()`, and `parse_fortran_project()` build file and project models. | Grammar, source-unit structure, declarations, parser diagnostics, or project assembly changes. | -| [`prik/parsers/fortran/cli.py`](../../../prik/parsers/fortran/cli.py) | `main()` formats parser reports and diagnostics. Its `--semantics` and `--pyi` options explicitly invoke later stages. | Parser CLI arguments, report layout, or diagnostic presentation changes. | +| [`prik/parsers/fortran/cli.py`](../../../prik/parsers/fortran/cli.py) | `main()` formats parser reports and diagnostics. Its `--semantics` and `--pyi` options explicitly invoke later stages; `--pyi` emits every inspected module through `emit_module_stubs()`, so it shows the contract `prik generate --pyi` writes. | Parser CLI arguments, report layout, or diagnostic presentation changes. | | [`prik/parsers/c/`](../../../prik/parsers/c/README.md) | `parse_c_file()` and `parse_c_project()` build `CFile`/`CProject` records; the local lexer, models, resolver, and CLI preserve C declarations, project facts, diagnostics, and report output. | C tokenization, declarations, type resolution, project assembly, or parser reports change. | | [`prik/parsers/pyi/__init__.py`](../../../prik/parsers/pyi/__init__.py) | Re-exports `parse_pyi_text()` and `parse_pyi_file()`. | The supported raw-`.pyi` parser import surface changes. | | [`prik/parsers/pyi/parser.py`](../../../prik/parsers/pyi/parser.py) | Parses text or a file into `ast.Module` with no contract interpretation. | Raw Python syntax input, file reading, or parse diagnostics change. | diff --git a/docs/developer/packages/planning.md b/docs/developer/packages/planning.md index 3126bdc83..e9f3763ba 100644 --- a/docs/developer/packages/planning.md +++ b/docs/developer/packages/planning.md @@ -64,6 +64,7 @@ ModulePlan │ └── NativeEntrypointSignaturePlan ├── NativeGeneratedCodeGroupPlan (zero or more) ├── BridgeModulePlan (optional; Fortran-local holder inventories) +├── ModuleVariablePlan (canonical native-variable registry) └── NamespacePlan (root and child namespaces) ├── FunctionPlan │ ├── ArgumentTransferPlan @@ -72,7 +73,7 @@ ModulePlan │ ├── NativeEntrypointProjectedSlotPlan │ │ └── BridgeCallSlotPlan (optional adapter facet) │ └── LifecycleActionPlan - └── ModuleVariablePlan + └── ModuleVariablePublicationPlan (namespace bindings to canonical variables) ``` Each callable, argument, and result always owns binding and entrypoint views; @@ -83,6 +84,17 @@ projection, presence and length fields, descriptors, and hidden outputs. Bridge records own adapter-local representation conversion and the invocation of the original Fortran procedure. +One module-level `ModuleVariablePlan` owns each declaring native variable and +its completed getter, setter, ownership, descriptor, array, and derived-object +mechanisms. Its owner path is the declaring native module and name, independent +of Python publication. A namespace-level `ModuleVariablePublicationPlan` holds +a direct reference to that canonical plan plus the Python names published in +the namespace. Re-exporting module state therefore adds publication records +without resolving ownership from a second key, changing variable identity, or +adding accessors, support procedures, initialization, allocation state, or +pointer state. Parameters use the same structure while retaining constant-value +lowering. + `NativeEntrypointModulePlan.support_procedures` is the authoritative registry for externally linked generated helper callables that are not ordinary wrapped functions. Each operation stores one collision-safe key and symbol plus a @@ -143,22 +155,31 @@ For each module, the planner first collects top-level and nested semantic classes into one depth-first, source-ordered tuple. That same collection feeds derived-type name indexing, backend-symbol allocation, and `_ClassPolicyCatalog`, so a nested class cannot reach projection without its -symbol being registered. It projects direct functions and variables, then uses +symbol being registered. It projects direct functions and canonical variables, then uses the catalogue to join each public class to its completed derived-type, surface, method, and overload policies. The catalogue is read-only: it maps existing owner paths to their semantic declarations without deciding policy again. +Each type is defined in one namespace: the one publishing it, beside its +parent class when it is nested and unpublished, and the root otherwise. +Generated code taking or returning the type reaches its class and helpers +there, so `WrapperGenerator` rejects a plan defining one type twice +(`duplicate-derived-type-identity`). A derived module variable's private +helpers are placed in that same namespace. + The planner attaches class and overload callables to the function collections that need their native entrypoints. It completes generated symbols, adds every required parent namespace, and creates namespace plans in root-first path -order. Finally it collects headers selected by completed descriptor-handle +order, except that a namespace whose classes extend a type another namespace +defines comes after that namespace. A namespace's classes are created when it +is set up, in plan order, so the base has to exist first. Finally it collects headers selected by completed descriptor-handle plans and returns one editable `ModulePlan`. ### `models.py`: shared plans and three lowering views -`models.py` defines editable `StageRecord` plans. `ModulePlan` is the root; -each `NamespacePlan` groups the public functions, variables, derived types, -classes, and overloads for one Python path. A `FunctionPlan` owns call-wide +`models.py` defines editable `StageRecord` plans. `ModulePlan` is the root and +owns canonical module variables; each `NamespacePlan` groups public functions, +variable publications, derived types, classes, and overloads for one Python path. A `FunctionPlan` owns call-wide ordering, while its transfers, results, entrypoint parameters, projected call slots, optional adapter facets, and lifecycle actions carry the datatype-specific details. diff --git a/docs/developer/packages/policy.md b/docs/developer/packages/policy.md index 517117ef0..8415ed353 100644 --- a/docs/developer/packages/policy.md +++ b/docs/developer/packages/policy.md @@ -30,6 +30,7 @@ prik/policy/ ├── models.py ├── ownership.py ├── exports.py +├── contract_imports.py ├── construction.py ├── completion.py └── native_array_handles.py @@ -57,7 +58,8 @@ downstream fallback. | [`prik/policy/__init__.py`](../../../prik/policy/__init__.py) | Re-exports `complete_semantic_policies()` as the normal policy-stage entrypoint. | The supported policy import surface changes. | | [`prik/policy/models.py`](../../../prik/policy/models.py) | Immutable records and enums for function, argument, result, slot, lifecycle, class, overload, callback, array, descriptor, status, and transformation policy. | A completed decision needs a durable backend-neutral representation. | | [`prik/policy/ownership.py`](../../../prik/policy/ownership.py) | Ownership vocabulary, `OwnershipContext`, `OwnershipDecision`, `OwnershipPolicyResolver`, and action dispatchers resolve lifetime triples and fail-closed lowering actions. | Object kind, owner, transfer, destruction, storage, barrier, assignment, or setter selection changes. | -| [`prik/policy/exports.py`](../../../prik/policy/exports.py) | `PythonExportPolicy`, `complete_python_export_policy()`, and `completed_python_exports()` create collision-checked Python placement. | Export namespace, visibility, or collision behavior changes. | +| [`prik/policy/exports.py`](../../../prik/policy/exports.py) | `complete_python_export_policy()` completes collision-checked contract spellings and Python placement; focused readers expose those recorded decisions. | Contract naming, export namespace, visibility, or collision behavior changes. | +| [`prik/policy/contract_imports.py`](../../../prik/policy/contract_imports.py) | `complete_contract_imports()` replaces each module's imports with the names its contract binds from other modules, spelled as the sources and the completed contracts write them. | What a generated contract imports, or how an imported name is spelled, changes. | | [`prik/policy/construction.py`](../../../prik/policy/construction.py) | Feature constructors build coherent function, result, native-slot, callback, class, overload, and module-variable policies from completed ownership decisions. | A supported feature needs different completed policy composition. | | [`prik/policy/completion.py`](../../../prik/policy/completion.py) | `complete_semantic_policies()` runs the dependency-ordered completion pass, attaches outcomes, and validates blockers. | Completion order, cross-declaration completion, or the stage boundary changes. | | [`prik/policy/native_array_handles.py`](../../../prik/policy/native_array_handles.py) | `NativeArrayHandlePolicy`, ABI selectors and dispatchers, and `native_array_handle_build_requirements()` describe already-completed descriptor handles and their build requirements. | Descriptor-backed array ABI selection, allowed operations, dispatch, or build headers change. | @@ -158,10 +160,30 @@ that boundary. ### `exports.py` and `native_array_handles.py`: focused completion products -`complete_python_export_policy()` writes one collision-checked Python name for -each public declaration in its namespace. `completed_python_exports()` reads -those names as immutable `PythonExportPolicy` records while assembling a -wrapper policy. +`complete_python_export_policy()` writes one collision-checked contract +spelling for every declaration, including withheld helpers and class members, +then records zero or more public placements independently. +`completed_python_exports()` reads the placements as immutable +`PythonExportPolicy` records while assembling wrapper policy. The contract +spelling is read with `completed_contract_name()`, which lives beside +`CONTRACT_NAME_METADATA` in `prik/semantics/models.py` so contract emission can +read the decision without importing policy; class-surface construction reads it +the same way. A name the module imports is completed in the same ledger, as the +module publishes it or, for a type it does not publish, as a class, and +recorded under `CONTRACT_IMPORT_NAMES_METADATA`; its annotations, its import, +and `__all__` all read that one spelling. A callable a declaration expression +calls is spelled the same way: completion sets `SemanticExpressionCallable.name` +to the contract spelling and respells that call in the shape expression through +the parsed expression, so only call targets change; `native_name` and +`native_scope` keep the native identity. + +`complete_contract_imports()` runs once names are complete, over the modules +written together. A contract binds what its declarations name and what it +publishes, never a `use` statement as written: a `use` that only extends a +generic the module declares binds nothing. Every binding passes one table keyed +by local name, so an entity reached twice binds once and a name meaning two +entities is refused. It replaces `SemanticModule.imports` with the result, and +the printer renders those statements without deciding any of them. `completion.py` creates native-array handle policies for descriptor-backed arrays. `native_array_handles.py` carries those records through the rest of diff --git a/docs/developer/packages/printers.md b/docs/developer/packages/printers.md index f6f7c850d..3b79c1c45 100644 --- a/docs/developer/packages/printers.md +++ b/docs/developer/packages/printers.md @@ -41,7 +41,7 @@ SemanticModule graph -> PyiPrinter -> editable .pyi | [`prik/printers/__init__.py`](../../../prik/printers/__init__.py) | Re-exports `CSourcePrinter`, `FortranSourcePrinter`, `PyiPrinter`, and `emit_module()`. | The supported printer import surface changes. | | [`prik/printers/c.py`](../../../prik/printers/c.py) | `CSourcePrinter` serializes C translation units, headers, declarations, functions, tables, and statements. | C syntax layout, escaping, or formatting changes. | | [`prik/printers/fortran.py`](../../../prik/printers/fortran.py) | `FortranSourcePrinter` serializes bridge modules, interfaces, declarations, procedures, and free-form wrapped statements. | Fortran source layout or line-wrapping changes. | -| [`prik/printers/pyi.py`](../../../prik/printers/pyi.py) | `PyiPrinter`, `emit_module()`, and `_PyiEmissionContext` serialize semantic modules and scope imports, aliases, namespaces, and defaults for one emission. | Editable contract spelling or emission-context behavior changes. | +| [`prik/printers/pyi.py`](../../../prik/printers/pyi.py) | `PyiPrinter`, `emit_module()`, and `_PyiEmissionContext` serialize semantic modules and scope imports, aliases, namespaces, and defaults for one emission. | Editable contract rendering or emission-context behavior changes. | The fact that code generation calls a printer at the end of wrapper rendering does not make printing part of codegen ownership. `pipeline/wrapper.py` @@ -77,9 +77,14 @@ unsplittable line that remains above the 132-column compiler-safe limit. ### `pyi.py`: semantic IR to an editable contract `PyiPrinter.emit()` creates a fresh `_PyiEmissionContext` for every call. The -context records contract imports, aliases, public-name reservations, source -array defaults, and nested namespaces without mutating a reusable printer or -the semantic IR. +context records contract imports, aliases, source array defaults, and nested +namespaces without mutating a reusable printer or the semantic IR. Contract +spellings and overload-target spellings must already be completed on semantic +owners by post-IR policy; the printer reads them and keeps no naming allocator. +Imports from other modules are the statements `complete_contract_imports()` +recorded in `SemanticModule.imports`, each item spelled both ways; the printer +writes the source spellings or the completed ones and never chooses which names +to bind. For a module, the printer first renders public classes, prototypes, variables, functions, and overload sets into body sections. As visitors use contract @@ -152,12 +157,15 @@ from prik.contracts import Float64, bind def double_value( value: Float64 ) -> Float64: ... + +__all__ = ["double_value"] ``` The native examples prove that punctuation and layout are added to already formed nodes. The `.pyi` import and `@bind` line show that required contract imports and native identity are derived from semantic IR without attaching -wrapper policy. +wrapper policy, and the closing `__all__` states the surface the module +publishes. ## Tests And Evidence diff --git a/docs/index.md b/docs/index.md index d98b62e38..48f893c22 100644 --- a/docs/index.md +++ b/docs/index.md @@ -173,7 +173,7 @@ python3 -m prik points.f90 --out geometry import numpy as np import geometry.points as points -item = points.point(x=np.float64(3.0), y=np.float64(4.0)) +item = points.Point(x=np.float64(3.0), y=np.float64(4.0)) points.move(item, np.float64(1.0), np.float64(-2.0)) print(item.x, item.y) # 4.0 2.0 @@ -191,16 +191,16 @@ The generated `points.pyi` is: ```python from prik.contracts import Addr, Arg, Float64, native_call -class point: +class Point: x: Float64 = 0.0 y: Float64 = 0.0 def __init__(self, *, x: Float64 = 0.0, y: Float64 = 0.0) -> None: ... @native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) -def move(item: point, dx: Float64, dy: Float64) -> None: ... +def move(item: Point, dx: Float64, dy: Float64) -> None: ... -def norm_squared(item: point) -> Float64: ... +def norm_squared(item: Point) -> Float64: ... ``` Generate it: @@ -220,7 +220,7 @@ The edited `points.pyi` is: ```python from prik.contracts import Addr, Arg, Float64, Pass, bind, native_call -class point: +class Point: x: Float64 = 0.0 y: Float64 = 0.0 @@ -260,7 +260,7 @@ The native Fortran is unchanged, but the Python surface is now: import numpy as np import geometry.points as points -item = points.point(x=np.float64(3.0), y=np.float64(4.0)) +item = points.Point(x=np.float64(3.0), y=np.float64(4.0)) item.translate(np.float64(1.0), np.float64(-2.0)) print(item.x, item.y) # 4.0 2.0 diff --git a/docs/user/examples/c/libm-wrapper.md b/docs/user/examples/c/libm-wrapper.md index 9c3aeb797..8b186d84e 100644 --- a/docs/user/examples/c/libm-wrapper.md +++ b/docs/user/examples/c/libm-wrapper.md @@ -87,6 +87,12 @@ under [`examples/c/libm/`](../../../../examples/c/libm/). reviewed 60-function public surface. The export allowlist excludes the rest of the platform header and fails if a requested ISO C99 function is missing. +The allowlist names native C functions; the generated contract records the +corresponding Python public names in `__all__`. Review that list together with +the signatures below it. Once you build from the contract, `__all__` is the +statement of what the module publishes, and `--export-symbols` no longer takes +part. + Generate the contract for the active target with: ```bash diff --git a/docs/user/examples/c/ta-lib-wrapper.md b/docs/user/examples/c/ta-lib-wrapper.md index a7ab0b04b..a1eafb86d 100644 --- a/docs/user/examples/c/ta-lib-wrapper.md +++ b/docs/user/examples/c/ta-lib-wrapper.md @@ -369,7 +369,10 @@ The suite has four complementary layers: TA-Lib's runner performs abstraction-protocol self-checks before the indicator comparisons. That API is outside this example, so those setup requests are forwarded directly to the native reference server. They are not counted as -PRIK calls and cannot satisfy the required 322-name coverage set. +PRIK calls and cannot satisfy the required 322-name coverage set. The native +build helper configures this preliminary protocol to write array values with +17 significant digits, preserving its binary64 inputs across JSON without +changing the TA-Lib library used by either numerical comparison path. ## Tested platforms diff --git a/docs/user/examples/fortran/bspline-wrapper.md b/docs/user/examples/fortran/bspline-wrapper.md index 0bebab1e3..d9338eb04 100644 --- a/docs/user/examples/fortran/bspline-wrapper.md +++ b/docs/user/examples/fortran/bspline-wrapper.md @@ -135,7 +135,7 @@ import numpy as np import prik_bspline.bspline_oo_module as bspline x = np.linspace(0.0, 2.0 * np.pi, 25) -spline = bspline.bspline_1d(x, np.sin(x), np.int32(4)) +spline = bspline.Bspline_1d(x, np.sin(x), np.int32(4)) value, iflag = spline.evaluate(np.float64(1.234), np.int32(0)) area, iflag = spline.integral(np.float64(0.0), np.float64(np.pi)) @@ -145,11 +145,11 @@ The abstract base is exported but cannot be constructed. Its concrete extensions inherit the base bindings and answer its deferred operations: ```python -bspline.bspline_class() +bspline.Bspline_Class() # TypeError: bspline_class is an abstract native type and cannot be # instantiated; create one of its concrete extensions instead -issubclass(bspline.bspline_1d, bspline.bspline_class) # True +issubclass(bspline.Bspline_1d, bspline.Bspline_Class) # True ``` The procedural module exposes the matching `db1ink` through `db6ink` setup diff --git a/docs/user/faq/index.md b/docs/user/faq/index.md index 921d4dffd..80bd4ba8f 100644 --- a/docs/user/faq/index.md +++ b/docs/user/faq/index.md @@ -125,6 +125,13 @@ python3 -m prik generate --pyi --language c include/vendor.h \ --out vendor.pyi ``` +`symbols.txt` defines the source-side public function surface. PRIK records the +corresponding Python public names in `vendor.pyi`'s `__all__`. Review and edit +that list alongside the signatures: once you build from the contract, `__all__` +controls what the contract publishes and `--export-symbols` is no longer used. +Adding a name to `__all__` publishes a declaration the contract already +reaches; it cannot conjure one the C sources never declared. + Pass the header's normal `-I`, `-D`, and `--std` options when it needs them. Review `vendor.pyi` before building. Primitive scalar signatures are ready to use; edit pointer parameters when they represent arrays, outputs, or strings. diff --git a/docs/user/guide/c/symbols-headers-and-dependencies.md b/docs/user/guide/c/symbols-headers-and-dependencies.md index 03668ac3e..27fffb0ea 100644 --- a/docs/user/guide/c/symbols-headers-and-dependencies.md +++ b/docs/user/guide/c/symbols-headers-and-dependencies.md @@ -171,9 +171,18 @@ python3 -m prik generate --pyi --language c api_probe.h \ --out contracts/api.pyi ``` -The export file selects the semantic API, not linker exports. Selected -functions still need native link inputs and a signature supported by the C -wrapper. See [C include +The name file defines the source-side public function surface, not linker +exports. `generate --pyi` writes that surface into the generated contract's +`__all__`. The two lists live in different naming domains: the file names +native C identifiers, and `__all__` names the Python names the contract +publishes. + +The allowlist chooses the initial contract surface from C source. Once the +`.pyi` exists, `__all__` is the editable authority for what that contract +publishes, and a contract build rejects `--export-symbols`. + +Selected functions still need native link inputs and a signature supported by +the C wrapper. See [C include exposure](../../reference/cli-commands.md#c-include-exposure) for the file format and validation rules. @@ -187,6 +196,12 @@ build = build_c_extension( ) ``` +`export_symbols=` is the Python API equivalent of `--export-symbols`: it selects +the same source-side public function surface for a direct C source build. The +build writes type stubs for the extension into a `contracts/` directory beside +it, and those stubs state the selected surface as `__all__` the same way +`generate --pyi` does. + ### Inspect a broader C API The parser and contract generator accept more syntax than the supported diff --git a/docs/user/guide/callbacks.md b/docs/user/guide/callbacks.md index 475311d1b..ae5a08dfd 100644 --- a/docs/user/guide/callbacks.md +++ b/docs/user/guide/callbacks.md @@ -79,6 +79,22 @@ api.apply(lambda value: np.float64(3.0 * value), np.float64(2.5)) The lambda receives converted Python objects, not `Addr(...)` markers. +Optional procedure dummies use the same Python spelling as other optional +arguments. Omit the callable or pass `None` to make Fortran observe +`present(callback) == .false.`: + +```python +api.run(np.int32(4)) +api.run(np.int32(4), None) +api.run(np.int32(4), report) +``` + +Optional dummies inside a callback prototype arrive at the Python callable as +`None` when the native callback invocation omits them. The callable keeps the +prototype's full positional argument list, so one callable such as +`report(value, status=None)` handles both `call report(value)` and +`call report(value, status)`. + --- ## Small Example @@ -216,6 +232,14 @@ copying an undefined incoming value, and `InOut(...)` copies the incoming value and writes changes back after the callback. Omitting the wrapper preserves an omitted Fortran `intent` rather than inventing one. +An assumed-shape callback dummy is spelled `Float64[::]`, and Python receives +the extent the native caller passed: + +| Fortran callback dummy | Matching prototype | +| --- | --- | +| `real(8), intent(in) :: values(count)` | `values: In(Float64[count])` | +| `real(8), intent(in) :: values(:)` | `values: In(Float64[::])` | + For scalar arguments, choose the spelling from the Fortran callback dummy: | Fortran callback dummy | Matching prototype | @@ -226,6 +250,51 @@ For scalar arguments, choose the spelling from the Fortran callback dummy: Both forms call Python with an independent `np.float64` scalar. The difference is the native calling convention PRIK must match. +A dummy the callee may write is different: PRIK generates rank-zero storage for +it, because Python has no writable scalar. A dummy with no declared `intent` +counts here — Fortran lets the callee both read and modify it, so PRIK is +conservative and the contract records the missing direction by carrying no +wrapper: + +| Fortran callback dummy | Generated prototype | Callback may | +| --- | --- | --- | +| `real(8), intent(in) :: f` | `f: In(Addr(Float64))` | read | +| `real(8), intent(out) :: f` | `f: Out(Float64[()])` | write | +| `real(8), intent(inout) :: f` | `f: InOut(Float64[()])` | read and write | +| `real(8) :: f` | `f: Float64[()]` | read and write | + +Pass `--assume-intent-in-scalars` to treat an undeclared scalar as input-only +instead; the dummy still records no direction, it simply stops being writable. + +Python receives a rank-zero NumPy view of the native storage. Assign through it; +rebinding the name changes nothing the native caller will read: + +```python +def objective(x, f): + f[...] = float(np.sum(x * x)) # delivers the value + f = float(np.sum(x * x)) # rebinds a local name; the caller sees nothing +``` + +To keep an ordinary Python function, write a small adapter and pass that: + +```python +def objective(x): + return float(np.sum(x * x)) + +def objective_prik(x, f): + f[...] = objective(x) +``` + +A prototype keeps the native callback's argument list, so the Python callable +mirrors the Fortran interface. To call a return-style function instead, edit the +prototype to project the output: + +```python +@prototype +@native_call([Arg(0), Return("f", 0)]) +def OBJ(x: In(Float64[::])) -> Float64: ... +``` + `Value(T)` is only for supported non-primitive scalar value dummies, such as a derived-type callback dummy declared with the Fortran `value` attribute. @@ -236,10 +305,10 @@ derived-type callback dummy declared with the Fortran `value` attribute. - The callback is only valid **during** the wrapped native call. - Native code must not store the callback for later use. - Return the exact NumPy scalar type when PRIK expects a scalar callback result. -- Primitive scalar callback arguments arrive as independent NumPy scalar values, - whether the native dummy is `value` or reference. -- Primitive scalar reference writeback is unsupported; return a scalar result - instead. +- Primitive scalar callback arguments projected as values arrive as independent + NumPy scalar values, whether the native dummy is `value` or reference. + Writable reference scalars — `out`, `inout`, or no declared `intent` — arrive + as rank-zero storage you assign through. - Arrays and derived-type arguments can expose live native state; copy data you need after the wrapped call returns. @@ -256,10 +325,6 @@ The current callback contract does not support: - Stored callbacks, persistent callbacks, procedure pointers, or callbacks invoked after the wrapped call returns. Pass the callable into each wrapped call that needs it. -- Optional callback procedure arguments. Expose a separate native entry point - for the no-callback path, or require the callback argument. -- Optional arguments inside a `@prototype`. Pass an explicit value, sentinel, or - presence flag instead. - Pure callback prototypes. A Python callback adapter calls the Python runtime, so it cannot satisfy a pure Fortran procedure contract. In particular, one pure prototype cannot be used both as a callback annotation and as a called @@ -269,7 +334,9 @@ The current callback contract does not support: or supported scalar derived types. - Arrays passed by Fortran `value`, arrays of derived values, and array callback results without a complete fixed shape. Pass arrays by reference and give array - results an exact primitive shape. + results an exact primitive shape; an array *argument* may be assumed-shape. +- Optional callback dummies passed by `value`. Use a reference dummy so absence + has a C-interoperable null-pointer representation. - Variable-length callback strings. Use a fixed positive `String[n]` length. - Callback execution on a different Python thread. The callback must run on the same thread that entered the wrapper. diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index 16c1ffc87..c0a0532f0 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -93,7 +93,7 @@ The generated `points.pyi` is: ```python from prik.contracts import Addr, Arg, Float64, native_call -class point: +class Point: def __init__( self, *, @@ -104,14 +104,14 @@ class point: x: Float64 = 0.0 y: Float64 = 0.0 -class holder: +class Holder: def __init__(self) -> None: ... - origin: point + origin: Point @native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) def move( - item: point, + item: Point, dx: Float64, dy: Float64 ) -> None: ... @@ -120,11 +120,11 @@ def move( def make_point( x: Float64, y: Float64 -) -> point: ... +) -> Point: ... def set_origin( - container: holder, - item: point + container: Holder, + item: Point ) -> None: ... ``` @@ -149,7 +149,7 @@ sys.path.insert(0, "build/geometry") import geometry.points as points # Create new object -item = points.point(x=np.float64(1.0), y=np.float64(2.0)) +item = points.Point(x=np.float64(1.0), y=np.float64(2.0)) # Call method (inout mutation) points.move(item, np.float64(3.0), np.float64(4.0)) @@ -180,17 +180,17 @@ Result: The class docstring gives a short index: ```python -print(points.point.__doc__) +print(points.Point.__doc__) ``` ```text -point +Point Opaque wrapper for native type point. Constructor ----------- -point(*, x=0.0, y=0.0) -> point +Point(*, x=0, y=0) -> Point Fields ------ @@ -201,7 +201,7 @@ y : float64 The constructor has its own detailed docstring: ```python -print(points.point.__init__.__doc__) +print(points.Point.__init__.__doc__) ``` --- @@ -217,6 +217,11 @@ print(points.point.__init__.__doc__) - **Fields**: Public scalar numeric/logical/complex fields become Python attributes. - **Nested types**: Appear as generated objects tied to their parent. - **Results**: Derived-type function results create new independent objects. + An `allocatable` result must be allocated when the function returns, as + Fortran requires of every non-pointer function result; the compiler reads it + before the wrapper can, so returning it unallocated is a native error PRIK + cannot turn into `None`. A `pointer` result may be disassociated: the + returned object then raises `ReferenceError` when its value is read. - **Default constructor**: Automatically generated from public, writable primitive scalar fields. - **Constructor fields**: Passed by keyword (`logical`, `integer`, `real`, and @@ -281,12 +286,12 @@ as the constructor. In this mapping, `@bind` selects the native initializer, `@native_call(...)` gives its argument order, `Pass()` inserts the new -`point`, and `Addr(Arg(i))` passes Python argument `i` by address: +`Point`, and `Addr(Arg(i))` passes Python argument `i` by address: ```python from prik.contracts import Addr, Arg, Float64, Pass, bind, native_call -class point: +class Point: x: Float64 y: Float64 @@ -299,8 +304,8 @@ Replace the generated field-keyword `__init__` declaration with this one. The edit changes construction only; it does not create `initialize_point` in the native module. -After rebuilding, `points.point.__init__.__doc__` starts with -`point(x, y) -> point` and lists both parameters. +After rebuilding, `points.Point.__init__.__doc__` starts with +`Point(x, y) -> Point` and lists both parameters. For the complete replacement rules, see [Replace the Constructor](../reference/pyi-contracts/functions-and-classes.md#replace-the-constructor). @@ -349,12 +354,12 @@ the module declaration: ```python from prik.contracts import Addr, Arg, Float64, Pass, native_call -class point: +class Point: @native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))]) def move(self, dx: Float64, dy: Float64) -> None: ... @native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) -def move(item: point, dx: Float64, dy: Float64) -> None: ... +def move(item: Point, dx: Float64, dy: Float64) -> None: ... ``` Both declarations call the existing native `move` procedure: @@ -402,7 +407,7 @@ Fortran one: | `procedure, public ::` on a binding | Published regardless of the type default | The class docstring now lists `move(dx, dy) -> None` under `Methods`. -`points.point.move.__doc__` contains its complete parameter and return details. +`points.Point.move.__doc__` contains its complete parameter and return details. For the complete mapping rules, see [Expose a Module Procedure as a Method](../reference/pyi-contracts/functions-and-classes.md#expose-a-module-procedure-as-a-method). @@ -556,7 +561,7 @@ generic: ```python from prik.contracts import Float64, Int32, bind, overload, private -class counter: +class Counter: @private def add_integer(self, amount: Int32) -> Int32: ... @@ -610,19 +615,19 @@ The generated contract exposes `operator(+)` as `__add__`: ```python from prik.contracts import overload, private -class point: +class Point: @overload("add_points") - def __add__(self, right: point) -> point: ... + def __add__(self, right: Point) -> Point: ... @private -def add_points(left: point, right: point) -> point: ... +def add_points(left: Point, right: Point) -> Point: ... ``` Python uses the normal operator: ```python -left = points.point(x=np.float64(1.0), y=np.float64(2.0)) -right = points.point(x=np.float64(3.0), y=np.float64(4.0)) +left = points.Point(x=np.float64(1.0), y=np.float64(2.0)) +right = points.Point(x=np.float64(3.0), y=np.float64(4.0)) total = left + right print(total.x, total.y) # 4.0 6.0 ``` @@ -630,7 +635,7 @@ print(total.x, total.y) # 4.0 6.0 The magic method docstring shows the accepted operator signatures: ```python -print(points.point.__add__.__doc__) +print(points.Point.__add__.__doc__) ``` The relevant part is: @@ -640,7 +645,7 @@ __add__(*args, **kwargs) Supported Signatures -------------------- -__add__(right: point) -> point +__add__(right: Point) -> Point ``` | Fortran generic | Python method | Python syntax | diff --git a/docs/user/guide/wrapping-modules.md b/docs/user/guide/wrapping-modules.md index bd861b851..7c7201b6d 100644 --- a/docs/user/guide/wrapping-modules.md +++ b/docs/user/guide/wrapping-modules.md @@ -103,12 +103,14 @@ mod.counter = np.int32(9) print(mod.counter) # 9 print(mod.summarize()) # 21 -print(mod.nmax) # 12 (read-only parameter) +print(mod.nmax) # 12 (the declared parameter value) ``` -- `parameter` declarations become read-only constants in the generated - contract. -- Assigning to a constant in Python only creates a local shadow — it does **not** mutate the native value. +- `parameter` declarations become `Final[...]` constants in the generated + contract, carrying the value the Fortran `parameter` declares. +- Assignment is not refused. Assigning to one rebinds that Python name only: it + does **not** mutate the native value, and it does not change any other + namespace publishing the same parameter. --- @@ -220,6 +222,10 @@ Public functions, variables, constants, and generated classes are exported at the extension root. If the original module imports were replaced, `library.module1` and `library.module2` are no longer exported. The native Fortran modules and their storage do not move; only the Python API changes. +Publishing a module variable in more than one namespace gives every name the +same live storage, so a write, allocation, pointer association, or derived +object mutation through one name is visible through all of them. Parameters +remain read-only constants in every namespace. Wildcard imports never use import order to resolve a collision. If both modules export the same name, the wrapper build fails and asks for an explicit diff --git a/docs/user/reference/cli-commands.md b/docs/user/reference/cli-commands.md index d569c8d92..942dc3ae8 100644 --- a/docs/user/reference/cli-commands.md +++ b/docs/user/reference/cli-commands.md @@ -389,11 +389,22 @@ C contracts—not whether the native compiler can find an include file. | `--include-exposure {reachable-project,roots-only}` | Exposes reachable project headers by default, or only the root inputs. | | `--public-include PATH_OR_PATTERN` | Exposes declarations from matching included files. Repeat as needed. | | `--private-include PATH_OR_PATTERN` | Hides declarations from matching included files. Repeat as needed. | -| `--export-symbols FILE` | Selects the exact reachable C functions named by FILE and makes those declarations public, including declarations from otherwise-private system headers. | - -`--export-symbols` is a function-only allowlist for commands that produce -semantic IR: source builds, `semantics`, and `generate --pyi`. The UTF-8 file -contains one ASCII C identifier per line; blank lines and text after `#` are ignored. +| `--export-symbols FILE` | Selects the exact reachable C functions named by FILE as the source-side public surface, including declarations from otherwise-private system headers. `generate --pyi` records the corresponding Python public names in the contract's `__all__`. | + +`--export-symbols` is a function-only allowlist for commands that read C +source: source builds, `semantics`, and `generate --pyi`. It defines the +source-side public function surface. When `generate --pyi` writes that surface +as an editable semantic contract, the corresponding Python public names are +written to the contract's `__all__`. + +The two lists live in different naming domains: the file names native C +identifiers, and `__all__` names what the contract publishes to Python. After +generation the contract is authoritative — edit `__all__` to change what it +publishes rather than passing `--export-symbols` again, which a contract build +rejects. + +The UTF-8 file contains one ASCII C identifier per line; blank lines and text +after `#` are ignored. Every listed name must resolve to exactly one reachable function. Empty files, invalid or repeated names, unknown names, names of non-function declarations, and ambiguous declarations fail the command. All declarations not selected by diff --git a/docs/user/reference/pyi-contracts/exports-and-modules.md b/docs/user/reference/pyi-contracts/exports-and-modules.md index 5a98b78e6..ad9acdf82 100644 --- a/docs/user/reference/pyi-contracts/exports-and-modules.md +++ b/docs/user/reference/pyi-contracts/exports-and-modules.md @@ -41,8 +41,9 @@ native module or select a native object file. Only declarations reachable from `__init__.pyi` are public. Missing files, import cycles, and two different exports using the same Python name are -errors. Explicit aliases share the same native target, but Python object -identity is not guaranteed for every read. +errors. Explicit aliases share the same native target. A module-variable alias +reads and writes the declaring variable's live native storage; it does not +create another variable. ## Remove or Hide a Declaration @@ -158,6 +159,45 @@ nmax: Final[Int32] = 12 See [Wrapping Modules](../../guide/wrapping-modules.md#shape-the-module-api-with-the-contract) for the resulting Python usage. +## Re-export Module State + +Import a variable into another leaf and include it in that leaf's `__all__`: + +```python +# state.pyi +from prik.contracts import Int32 + +counter: Int32 +__all__ = ["counter"] +``` + +```python +# facade.pyi +from .state import counter + +__all__ = ["counter"] +``` + +Both `package.state.counter` and `package.facade.counter` access the same +native variable. Assignment, array mutation, allocation, pointer association, +and derived-object changes made through either namespace are immediately +visible through the other. PRIK completes the variable's access and ownership +policy once; the second namespace changes publication only. + +A re-exported `Final[...]` parameter behaves differently, because a Fortran +`parameter` has no native storage to share and no setter. Each namespace +receives the same native constant value as an ordinary Python attribute: + +- every publication starts at the value the Fortran `parameter` declares; +- assigning to one, such as `facade.limit`, rebinds that Python name and does + not modify the Fortran parameter; +- assigning to one does not rebind the others, so the namespaces can disagree + afterwards. + +Assignment is not refused. Nothing enforces the constant at runtime, so treat +a published parameter as a value each namespace holds rather than a shared +read-only view of native state. + ## Next Use [Functions and Classes](functions-and-classes.md) to add methods, diff --git a/docs/user/reference/pyi-contracts/functions-and-classes.md b/docs/user/reference/pyi-contracts/functions-and-classes.md index adfbe8d40..21fa9bbd9 100644 --- a/docs/user/reference/pyi-contracts/functions-and-classes.md +++ b/docs/user/reference/pyi-contracts/functions-and-classes.md @@ -128,6 +128,19 @@ deallocation. Use an ordinary method instead when cleanup is optional, repeatable, or must report a recoverable status to Python. +## Nest a Class + +A class written inside another is reached through it, as in Python: + +```python +class grid: + class cell: + value: Int32 +``` + +The module publishes `grid`; the nested type is `grid.cell`, not a module +attribute of its own. + ## Type-Bound and Magic Methods Type-bound and magic methods follow the same rules: diff --git a/docs/user/reference/pyi-format.md b/docs/user/reference/pyi-format.md index 071f0e403..24d2679e3 100644 --- a/docs/user/reference/pyi-format.md +++ b/docs/user/reference/pyi-format.md @@ -114,8 +114,8 @@ The generated forms therefore have these responsibilities: | C `.pyi` | Selected C declarations in one directly buildable contract file. | A contract build receives one entry `.pyi`: the package `__init__.pyi` for the -Fortran layout above, or the C file itself. Relative imports from a package -entry discover its leaf files. +full Fortran package, a Fortran module leaf for that module and its imported +siblings, or the C file itself. Relative imports discover dependent contracts. ### Entry Contract And Extension Identity @@ -141,13 +141,21 @@ contracts/ Building `api.pyi` directly exposes its declarations at the extension root and uses `api` as the default extension name. -Use the entry, not every imported leaf, on the command line: +Use one entry on the command line to build the full package: ```bash python3 -m prik contracts/solver/__init__.pyi \ --native-objects build/solver.o ``` +To build a module leaf directly, pass that leaf as the entry. Its relative +imports load sibling contracts needed by its declarations: + +```bash +python3 -m prik contracts/solver/solver_mod.pyi \ + --native-objects build/solver.o +``` + A source-free C contract also needs its native language selected explicitly: ```bash @@ -172,6 +180,84 @@ Aliases change the Python API only. They do not rename native modules, types, or symbols. Conflicting wildcard exports are rejected; resolve them with explicit imports and aliases. +### Stating What A Contract Publishes + +A contract may end with `__all__`, naming every entity it publishes: + +```python +from prik.contracts import Int32 +from .shapes_mod import box + +def area(item: box) -> Int32: ... + +__all__ = ["area"] +``` + +The list states the contract's complete public symbol surface, not only the names +it re-exports. It settles a question import syntax cannot answer, because one +import serves two purposes: naming a type a declaration needs, and publishing an +entity this contract means to expose. `from .shapes_mod import box as crate` +reads the same whether `crate` avoids a collision or is published under a new +name. + +A published symbol is not always a Python object the extension exposes. What the +name declares decides how publishing it appears: + +| Published symbol | How it appears | +| --- | --- | +| Procedure | A runtime callable. | +| Derived type | A runtime type. | +| Package sub-namespace | A runtime namespace attribute. | +| Prototype | A callback signature contracts name, with no runtime object. | +| Module variable | Live state or a constant; every publication reaches the declaring variable. | +| Generic interface | A dispatch surface, publishable only by the namespace declaring it. | + +A procedure and a derived type each reach Python as one object, so another +namespace can bind that object under whatever name the importing contract +states. A module-variable re-export instead installs another route to the same +declaring variable: reads, writes, allocation, pointer association, and derived +object state remain shared. A `Final[...]` parameter is published with the same +constant semantics in every namespace. A generic is a dispatch surface rather +than one object and remains publishable only by its declaring namespace. + +PRIK writes the list into every generated contract. It includes the module's +own public declarations, explicitly public imports, and accessible imported +names that are not dependencies of its own declarations. For example, a type +imported only to declare an argument stays available as an import in the +contract but is not published to Python unless the module names it in a +`public` statement. This Python publication choice does not change the name's +Fortran accessibility through the importing module. Fortran accessibility may +also name an imported module itself: making every route to an entity private +withholds it, while any explicitly public route keeps it accessible. Edit the +list freely. + +| Edit | Effect | +| --- | --- | +| Remove a name | The entity stays declared and callable from other contracts, but no longer reaches Python here. | +| Add an imported name | Publishes it here as well, including one imported only to express a declaration. | +| `__all__ = []` | Publishes nothing from this contract. | +| Remove `__all__` | Publishes everything the contract reaches, its declarations and its imports alike. | + +A name in `__all__` must be one the contract declares or imports; naming +anything else is rejected before wrapper planning, so renaming a declaration +means renaming what the contract publishes. + +A wildcard import reads the surface its dependency publishes: + +```python +from .shapes_mod import * +``` + +brings in what `shapes_mod` states in its own `__all__` and nothing it withheld. +A withheld name stays reachable by asking for it, which a contract needing it to +express a declaration -- or meaning to publish it itself -- still can: + +```python +from .shapes_mod import crate + +__all__ = ["crate"] +``` + ### Contract Import Graph PRIK parses contract files without executing them. Relative imports recursively @@ -472,7 +558,7 @@ Python declaration and native callable names differ. | `@native_call([...], result=...)` | Function, method, or constructor | Shared: state the complete native argument order and optional native result mapping. | | `@overload("specific", generic=...)` | Function or method | Shared: add one exact candidate to a generated Python overload set. | | `@prototype` | Module-level function declaration | Fortran exact procedure interface used by callbacks or declaration expressions. | -| `@pure` | `@prototype` declaration | Fortran: preserve the native pure characteristic. | +| `@pure` | Module-level function or `@prototype` declaration | Fortran: preserve the native pure characteristic. | | `@raises(status=..., message=..., success=...)` | Function or method | Shared: consume named status outputs and raise on non-success. | | `@nogil` | Function or method | Shared: request GIL release around the completed native call. | | `@abstractmethod` | Method | Fortran deferred binding. | @@ -480,8 +566,8 @@ Python declaration and native callable names differ. | `@staticmethod` | Method | Python stub marker for a method without `self`. | Decorators are validated in context. `@prototype` cannot combine with wrapper -decorators, `@overload` cannot combine with `@native_call`, and `@pure` requires -`@prototype`. +decorators, `@overload` cannot combine with `@native_call`, and `@pure` applies +to a module-level native procedure, not a method or an `@overload` dispatcher. A status projection can hide its consumed output from the Python return: @@ -542,8 +628,10 @@ def update_values( def apply_update(callback: update_values) -> None: ... ``` -`@pure` is valid only with `@prototype`. Calling a pure prototype name inside a -declaration expression identifies a standalone specification function. Current +`@pure` states that a native procedure is pure, which a function called in a +declaration expression must be: a module function the expression imports +carries it, and calling a pure prototype name identifies a standalone +specification function. Current callback wrapper support is Fortran-specific; C function pointers can be inspected but are not buildable C callbacks. @@ -748,9 +836,6 @@ and supported pure specification functions. `size(values, 2)`, for example, becomes the second public extent. PRIK rejects expressions it cannot resolve before lowering. -`Strided` is a compatibility spelling for older explicit forms such as -`T[::Strided]`; author the shorter `T[::]` form. - ### Character Length And Shape `String` uses the first subscription for character length and a second @@ -938,12 +1023,27 @@ def consume(class_: Annotated[Int32, SourceName("class")]) -> None: ... Python export name. `SourceName(...)` preserves a native data or argument name. These are separate operations. -When PRIK generates a Fortran contract, it lowercases Fortran identifiers, -adds a trailing underscore to Python keywords, normalizes other invalid Python -identifiers, and gives remaining collisions deterministic numeric suffixes. -The same policy covers module members, classes, methods, fields, and argument -names. `--strict-wrapper-names` rejects a generated name that would need any of -these fixes. +When PRIK generates a contract it adds a trailing underscore to Python +keywords, normalizes other invalid Python identifiers, and gives remaining +collisions deterministic numeric suffixes. + +PRIK chooses a spelling only where the source has none. Fortran writes one +declaration under many spellings, so PRIK picks: a wrapped type becomes a +Python class and is spelled like one, capitalizing each underscore-separated +word, and every other declaration is lowercased. `type :: point_t` publishes as +`Point_T`, and `subroutine SCALE_VALUE` as `scale_value`. C names each +declaration exactly, so its spelling is kept as written: `BarBaz` stays +`BarBaz`, `struct point` stays `point`, and `Foo` and `foo` remain two +functions. + +These are defaults, not constraints. Rename a declaration in the contract and +the build follows it, because a Fortran name resolves without regard to case +and `@bind(...)` states a native name that differs from the Python one. The +same policy covers module members, classes, methods, fields, and argument +names, and it decides both the names a build publishes and the names the +contract describing that build states. `--strict-wrapper-names` rejects a +generated name Python could not otherwise spell; it does not object to the +chosen casing. Fortran `bind(C, name=...)` changes the native symbol, not the Python name. In an edited contract, `@bind("native_name")` records that native-name distinction; @@ -994,7 +1094,7 @@ valid and whether it is buildable. | Storage and result types | `Addr`, `Allocatable`, `Pointer`, `Returns`, `private` | | Compatibility/category types | `Matrix`, `Vector`, `OpaqueHandle`, `WrappedType` | | Class and C inspection markers | `CAnonymous`, `CAnonymousMember`, `CStruct`, `CUnion`, `Opaque` | -| Shape and layout markers | `Contiguous`, `COPY_F`, `Flat`, `ORDER_ANY`, `ORDER_C`, `ORDER_F`, `Strided` | +| Shape and layout markers | `Contiguous`, `COPY_F`, `Flat`, `ORDER_ANY`, `ORDER_C`, `ORDER_F` | | General metadata | `Aliased`, `ArrayCategory`, `AssumedType`, `FortranAllocatable`, `Immutable`, `MaybeUnallocated`, `Polymorphic`, `SourceName` | | Constraints and ownership | `Bounded`, `Finite`, `Range`, `Ownership`, `Transfer`, `Destruction`, `PointerAssociation`, `PointerPolicy` | | Prototype direction | `In`, `Out`, `InOut` | @@ -1032,7 +1132,7 @@ The loader rejects malformed language forms before wrapper planning: - Python enum classes instead of `Final[...]` integer constants; - `typing.overload` instead of PRIK `@overload("specific")`; - `@overload` combined with `@native_call`; -- `@pure` without `@prototype`; +- `@pure` on a method or an `@overload` dispatcher; - `@native_abi(...)` outside Fortran or with a value other than `"c"`; - incomplete, duplicated, or out-of-range `@native_call` entries; - untyped hidden literals inside `@native_call`; diff --git a/docs/user/reference/python-api.md b/docs/user/reference/python-api.md index 25b0149eb..bb2c509e9 100644 --- a/docs/user/reference/python-api.md +++ b/docs/user/reference/python-api.md @@ -80,6 +80,25 @@ native_math = build.import_module() print(native_math.add(np.float64(3.0), np.float64(2.5))) ``` +Pass `export_symbols` to restrict the build to an exact reviewed set of +reachable C functions. That set is the source-side public surface: it selects +which C declarations are converted, and the matching stub emission records the +corresponding Python public names in the module's `__all__`. + +```python +from prik import build_c_extension + +build = build_c_extension( + "vendor.h", + output_dir="build", + export_symbols=["vendor_open", "vendor_close"], +) +``` + +Unknown names fail the build rather than silently producing a smaller module. +Once you author or generate a semantic `.pyi` contract, that contract's own +`__all__` states the public surface and `export_symbols` no longer applies. + For an authored C semantic contract, use `build_pyi_extension` with `native_language="c"` and `native_c_sources=[...]`. [C Pointers, Arrays, and Strings](../guide/c/pointers-arrays-and-strings.md#author-a-contract-for-pointers-and-arrays) diff --git a/examples/c/libm/README.md b/examples/c/libm/README.md index 3874f3345..f875e437d 100644 --- a/examples/c/libm/README.md +++ b/examples/c/libm/README.md @@ -15,7 +15,9 @@ caller-owned output arrays, see [TA-Lib](../ta_lib/README.md). Its layout mirrors the other real-library examples: - `libm_probe.h` includes the target toolchain's own ``. -- `iso_c99_routines.txt` is the reviewed 60-function allowlist. +- `iso_c99_routines.txt` is the reviewed 60-function allowlist: it selects the + source-side public function surface, and the generated contract records the + corresponding Python public names in `__all__`. - `build_prik.sh` generates the target contract and builds the extension. - `build_all.sh` exposes the built module on `PYTHONPATH`. - `routine_inventory.py` groups every public function and names its test. @@ -98,6 +100,11 @@ if ! python3 -m prik --language c "$LIBM_BUILD_ROOT/prik/contract/libm_api.pyi" fi ``` +The first command generates `libm_api.pyi`, which lists the allowlisted +functions in `__all__`; the second builds from that contract. From then on +`__all__` is what publishes the API, and the allowlist is read only when the +contract is regenerated from C source. + The public signature uses target-sized NumPy contract types. Exact native C identities appear only at the native boundary. For example, an LP64 target may generate: diff --git a/examples/c/ta_lib/README.md b/examples/c/ta_lib/README.md index 820061c2e..12e9d8f1e 100644 --- a/examples/c/ta_lib/README.md +++ b/examples/c/ta_lib/README.md @@ -147,7 +147,9 @@ wrapper builds, and makes the generated module importable. `build_prik.sh` performs three checked operations: 1. [`native_build.py`](native_build.py) fetches, verifies, builds, and caches - the pinned native release and its reference-test tools. + the pinned native release and its reference-test tools. The helper configures + the abstract-test JSON request writer for exact binary64 round trips; it does + not modify the TA-Lib library linked by either comparison path. 2. PRIK generates a complete public-header inventory for the pinned compiler target. The inventory is for the surface audit; it is not used as the wrapper contract. @@ -212,7 +214,9 @@ integer output arrays, while the session fixture checks `TA_Initialize` and The runner starts with abstraction-protocol self-checks. The adapter forwards those setup requests to the direct reference server because the abstraction API is explicitly excluded. They do not cross the generated wrapper and do -not count toward the required 322-indicator coverage set. +not count toward the required 322-indicator coverage set. Those self-checks use +17 significant digits for array values, so their JSON transport preserves the +runner's binary64 inputs exactly on every target. The detailed user guide includes the complete [test flow and CI target explanation](../../../docs/user/examples/c/ta-lib-wrapper.md#where-the-expected-results-come-from). diff --git a/examples/c/ta_lib/native_build.py b/examples/c/ta_lib/native_build.py index 2159db89f..091dc6127 100644 --- a/examples/c/ta_lib/native_build.py +++ b/examples/c/ta_lib/native_build.py @@ -18,6 +18,9 @@ TA_LIB_COMMIT = "2247d599bddf37ed37e3a709371517e46efc66f6" TA_LIB_REPOSITORY = "https://github.com/TA-Lib/ta-lib.git" DEFAULT_JOB_LIMIT = 8 +REFERENCE_HARNESS_REVISION = "abstract-json-binary64-roundtrip-v1" +_ABSTRACT_ARRAY_FORMAT = 'pos += snprintf(buf + pos, buf_size - pos, "%.15g", data[i]);' +_ROUNDTRIP_ABSTRACT_ARRAY_FORMAT = 'pos += snprintf(buf + pos, buf_size - pos, "%.17g", data[i]);' def _require_tool(name: str) -> str: @@ -76,6 +79,26 @@ def _verified_source(cache_root: Path, git: str) -> Path: return source +def _reference_runner_source(text: str) -> str: + """Preserve binary64 inputs across the pinned runner's JSON protocol.""" + original_count = text.count(_ABSTRACT_ARRAY_FORMAT) + roundtrip_count = text.count(_ROUNDTRIP_ABSTRACT_ARRAY_FORMAT) + if original_count == 0 and roundtrip_count == 1: + return text + if original_count != 1 or roundtrip_count != 0: + raise RuntimeError("pinned TA-Lib abstract-array serializer no longer matches the reviewed source") + return text.replace(_ABSTRACT_ARRAY_FORMAT, _ROUNDTRIP_ABSTRACT_ARRAY_FORMAT) + + +def _prepare_reference_runner(source: Path) -> None: + """Apply the reviewed protocol-only adjustment to TA-Lib's test runner.""" + path = source / "src" / "tools" / "ta_regtest" / "test_abstract.c" + current = path.read_text(encoding="utf-8") + prepared = _reference_runner_source(current) + if prepared != current: + path.write_text(prepared, encoding="utf-8") + + def _installed_library(prefix: Path) -> bool: include = prefix / "include" / "ta-lib" / "ta_libc.h" libraries = tuple((prefix / "lib").glob("libta-lib.*")) @@ -179,12 +202,19 @@ def build_ta_lib(compiler: str) -> tuple[Path, Path, Path, Path]: compiler = str(Path(compiler).resolve()) cache_root = _cache_root() source = _verified_source(cache_root, _require_tool("git")) + _prepare_reference_runner(source) key = _compiler_key(compiler) build = cache_root / f"build-{TA_LIB_TAG}-{key}" prefix = cache_root / f"install-{TA_LIB_TAG}-{key}" complete = prefix / ".prik-ta-lib-complete" + completion = f"{TA_LIB_TAG}\n{TA_LIB_COMMIT}\n{REFERENCE_HARNESS_REVISION}\n" runner, oracle = _reference_paths(build) - if complete.is_file() and _installed_library(prefix) and _reference_tools_built(build): + if ( + complete.is_file() + and complete.read_text(encoding="utf-8") == completion + and _installed_library(prefix) + and _reference_tools_built(build) + ): return prefix, runner, oracle, _shared_library(prefix) cmake = _require_tool("cmake") @@ -206,7 +236,7 @@ def build_ta_lib(compiler: str) -> tuple[Path, Path, Path, Path]: oracle = _build_reference_server(compiler, source, build) if not runner.is_file(): raise RuntimeError(f"TA-Lib build did not produce its regression runner at {runner}") - complete.write_text(f"{TA_LIB_TAG}\n{TA_LIB_COMMIT}\n", encoding="utf-8") + complete.write_text(completion, encoding="utf-8") return prefix, runner, oracle, _shared_library(prefix) diff --git a/examples/c/ta_lib/tests/test_native_build.py b/examples/c/ta_lib/tests/test_native_build.py new file mode 100644 index 000000000..d1f8e596c --- /dev/null +++ b/examples/c/ta_lib/tests/test_native_build.py @@ -0,0 +1,22 @@ +"""Pinned native-build preparation for the TA-Lib validation harness.""" + +import pytest + +from ..native_build import _reference_runner_source + + +def test_abstract_requests_preserve_binary64_inputs_across_json(): + source = 'pos += snprintf(buf + pos, buf_size - pos, "%.15g", data[i]);' + value = 0.12345678901234566 + + prepared = _reference_runner_source(source) + + assert prepared == 'pos += snprintf(buf + pos, buf_size - pos, "%.17g", data[i]);' + assert _reference_runner_source(prepared) == prepared + assert float(format(value, ".15g")) != value + assert float(format(value, ".17g")) == value + + +def test_abstract_protocol_adjustment_rejects_unreviewed_upstream_source(): + with pytest.raises(RuntimeError, match="no longer matches"): + _reference_runner_source("unrecognized serializer") diff --git a/examples/fortran/bspline/README.md b/examples/fortran/bspline/README.md index fe906d709..5ae3eecc7 100644 --- a/examples/fortran/bspline/README.md +++ b/examples/fortran/bspline/README.md @@ -84,7 +84,7 @@ import numpy as np import prik_bspline.bspline_oo_module as bspline x = np.linspace(0.0, 2.0 * np.pi, 25) -spline = bspline.bspline_1d(x, np.sin(x), np.int32(4)) # generic constructor +spline = bspline.Bspline_1d(x, np.sin(x), np.int32(4)) # generic constructor value, iflag = spline.evaluate(np.float64(1.234), np.int32(0)) print(value) # about 0.943811 @@ -96,11 +96,11 @@ print(area) # about 2.0 The abstract base is present but cannot be constructed: ```python -bspline.bspline_class() +bspline.Bspline_Class() # TypeError: bspline_class is an abstract native type and cannot be # instantiated; create one of its concrete extensions instead -issubclass(bspline.bspline_1d, bspline.bspline_class) # True +issubclass(bspline.Bspline_1d, bspline.Bspline_Class) # True ``` ## Run focused tests diff --git a/examples/fortran/bspline/routine_inventory.py b/examples/fortran/bspline/routine_inventory.py index 7bbe972c8..8ec6cb9df 100644 --- a/examples/fortran/bspline/routine_inventory.py +++ b/examples/fortran/bspline/routine_inventory.py @@ -4,15 +4,15 @@ #: Object-oriented classes, most-derived first, over one abstract base. CLASSES: tuple[str, ...] = ( - "bspline_1d", - "bspline_2d", - "bspline_3d", - "bspline_4d", - "bspline_5d", - "bspline_6d", + "Bspline_1d", + "Bspline_2d", + "Bspline_3d", + "Bspline_4d", + "Bspline_5d", + "Bspline_6d", ) -ABSTRACT_BASE = "bspline_class" +ABSTRACT_BASE = "Bspline_Class" #: Bindings the abstract base declares and every class answers. DEFERRED_BINDINGS: tuple[str, ...] = ("destroy", "size_of") diff --git a/examples/fortran/bspline/tests/test_object_oriented_api.py b/examples/fortran/bspline/tests/test_object_oriented_api.py index 7ed4451c8..1f23c0fe9 100644 --- a/examples/fortran/bspline/tests/test_object_oriented_api.py +++ b/examples/fortran/bspline/tests/test_object_oriented_api.py @@ -19,7 +19,7 @@ def _sine_spline(bspline_oo, points=25): x = np.linspace(0.0, 2.0 * np.pi, points) - spline = bspline_oo.bspline_1d(x, np.sin(x), CUBIC) + spline = bspline_oo.Bspline_1d(x, np.sin(x), CUBIC) assert spline.status_ok() return spline @@ -43,11 +43,11 @@ def test_every_reviewed_class_is_exported(bspline_oo): def test_abstract_base_cannot_be_instantiated(bspline_oo): """`bspline_class` is declared abstract, so only its extensions have instances.""" with pytest.raises(TypeError, match="abstract native type and cannot be instantiated"): - bspline_oo.bspline_class() + bspline_oo.Bspline_Class() def test_every_class_extends_the_abstract_base(bspline_oo): - base = bspline_oo.bspline_class + base = bspline_oo.Bspline_Class for name in CLASSES: assert issubclass(getattr(bspline_oo, name), base), name @@ -56,7 +56,7 @@ def test_every_class_extends_the_abstract_base(bspline_oo): def test_every_concrete_class_interpolates_an_affine_grid(bspline_oo, dimension): """Every dimension-specific constructor and evaluator works end to end.""" axes, values = _affine_grid(dimension) - spline = getattr(bspline_oo, f"bspline_{dimension}d")(*axes, values, *(CUBIC,) * dimension) + spline = getattr(bspline_oo, f"Bspline_{dimension}d")(*axes, values, *(CUBIC,) * dimension) value, iflag = spline.evaluate(*(np.float64(0.3),) * dimension, *(np.int32(0),) * dimension) @@ -74,7 +74,7 @@ def test_every_class_answers_the_deferred_and_inherited_bindings(bspline_oo): def test_generic_constructor_accepts_each_declared_signature(bspline_oo): """`interface bspline_1d` publishes an empty and a data-driven constructor.""" - empty = bspline_oo.bspline_1d() + empty = bspline_oo.Bspline_1d() assert empty.status_ok() is False spline = _sine_spline(bspline_oo) @@ -112,7 +112,7 @@ def test_two_dimensional_interpolation_matches_the_sampled_surface(bspline_oo): y = np.linspace(0.0, 1.0, 20) samples = np.asfortranarray(np.exp(-(x[:, None] ** 2 + y[None, :] ** 2))) - spline = bspline_oo.bspline_2d(x, y, samples, CUBIC, CUBIC) + spline = bspline_oo.Bspline_2d(x, y, samples, CUBIC, CUBIC) assert spline.status_ok() value, iflag = spline.evaluate(np.float64(0.33), np.float64(0.47), np.int32(0), np.int32(0)) @@ -124,7 +124,7 @@ def test_deferred_bindings_dispatch_through_the_abstract_base(bspline_oo): """The base declares `size_of` and `destroy`; the object's own type answers.""" spline = _sine_spline(bspline_oo) - assert bspline_oo.bspline_class.size_of(spline) == spline.size_of() + assert bspline_oo.Bspline_Class.size_of(spline) == spline.size_of() assert spline.size_of() > np.int32(0) spline.destroy() diff --git a/prik/cli.py b/prik/cli.py index 0c2f0cd7f..90067f463 100644 --- a/prik/cli.py +++ b/prik/cli.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import ast import json import os import shlex @@ -617,6 +618,9 @@ def _convert_fortran_semantic_sources( refresh=context.refresh_fortran_type_probe, ) converted_files = [] + # A module that imports an abstract interface from another supplied file + # must resolve it here, exactly as a multi-file wrapper build does. + modules_by_file = {id(fobj): list(fobj.modules) for _p, fobj in parsed_files} for p, fobj in parsed_files: compile_time_values = _fortran_compile_time_values(fobj, context.preprocessing, **probe_options) type_facts = _fortran_type_facts( @@ -631,6 +635,9 @@ def _convert_fortran_semantic_sources( compile_time_values=compile_time_values, wrapped_derived_types=wrapped_derived_types, assume_intent_in_scalars=context.assume_intent_in_scalars, + sibling_modules=[ + module for key, modules in modules_by_file.items() if key != id(fobj) for module in modules + ], **({"type_facts": type_facts} if type_facts is not None else {}), ) converted_files.append((p, modules)) @@ -651,7 +658,6 @@ def _convert_fortran_semantic_sources( def _semantic_payload_for_converted_files(converted_files) -> dict[str, dict]: from prik.pipeline.pyi import emit_module_stubs - from prik.printers import emit_module out: dict[str, dict] = {} available_modules = [module for _p, modules in converted_files for module in modules] @@ -662,9 +668,12 @@ def _semantic_payload_for_converted_files(converted_files) -> dict[str, dict]: continue if _is_c_semantic_file(modules): # A generated C starter contract preserves raw source facts, even - # for a form that the direct-only wrapper policy will later block. - # ``--pyi`` is contract extraction, not wrapper planning. - module_stubs = {module.name: emit_module(module).strip() for module in modules} + # for a form that the direct-only wrapper policy will later block: + # ``--pyi`` is contract extraction, not wrapper planning. Emission + # still goes through the shared stub pipeline, which completes the + # public names policy owns without completing wrapper policy. + stubs = emit_module_stubs(modules, normalize_public_names=True) + module_stubs = {module.name: stubs[module.name] for module in modules} out[str(p)] = { "semantic_modules": [asdict(module) for module in modules], "pyi": "\n\n".join(module_stubs.values()).strip(), @@ -703,7 +712,7 @@ def _fortran_contract_payload(path: Path, modules, available_modules) -> dict[st emit_module_stubs( native_modules, available_modules=available_modules, - normalize_fortran_public_names=True, + normalize_public_names=True, ) if native_modules else {} @@ -715,7 +724,7 @@ def _fortran_contract_payload(path: Path, modules, available_modules) -> dict[st external_stubs = emit_module_stubs( [module], available_modules=available_modules, - normalize_fortran_public_names=True, + normalize_public_names=True, ) external_text.append(external_stubs.pop(module.name)) for name, text in external_stubs.items(): @@ -738,29 +747,61 @@ def _fortran_contract_payload(path: Path, modules, available_modules) -> dict[st def _source_root_stub(module_names: list[str], external_text: list[str]) -> str: + from prik.printers.pyi import PyiPrinter + contract_imports: set[str] = set() + exported_names: list[str] = [] external_sections = [] for text in external_text: - imports, body = _split_contract_imports(text) + imports, exported, body = _split_contract_imports(text) contract_imports.update(imports) + exported_names.extend(name for name in exported if name not in exported_names) if body: external_sections.append(body) contract_section = f"from prik.contracts import {', '.join(sorted(contract_imports))}" if contract_imports else "" lines = [f"from . import {name}" for name in module_names] import_section = "\n".join(line for line in [contract_section, *lines] if line) sections = [import_section, *external_sections] + # The entry publishes its package tree as well as any standalone name, and + # both are stated so either can be taken off the list. + exported_names = [*module_names, *exported_names] + if exported_names: + # Each source file states what it publishes, and this entry holds them + # all, so one list closes the file the way one does in any contract, + # wrapped the same way a long list is wrapped anywhere else. + sections.append(PyiPrinter.emit_exported_names(exported_names)) return "\n\n".join(section for section in sections if section).strip() -def _split_contract_imports(text: str) -> tuple[set[str], str]: +def _split_contract_imports(text: str) -> tuple[set[str], list[str], str]: + """Separate a contract's required imports and stated exports from its body.""" imports: set[str] = set() + exported: list[str] = [] body_lines = [] + pending: list[str] = [] for line in text.splitlines(): + if pending: + # A long list is written over several lines, so it is read back the + # same way: gather until the brackets close. + pending.append(line) + joined = "\n".join(pending) + if joined.count("[") == joined.count("]"): + exported.extend(ast.literal_eval(joined.split("=", 1)[1].strip())) + pending = [] + continue if line.startswith("from prik.contracts import "): imports.update(item.strip() for item in line.removeprefix("from prik.contracts import ").split(",")) continue + if line.startswith("__all__"): + if line.count("[") == line.count("]"): + exported.extend(ast.literal_eval(line.split("=", 1)[1].strip())) + else: + pending = [line] + continue body_lines.append(line) - return imports, "\n".join(body_lines).strip() + if pending: + raise ValueError(f"Unterminated __all__ in generated contract: {pending[0]!r}") + return imports, exported, "\n".join(body_lines).strip() def _format_pyi_report(semantic_report: dict[str, dict]) -> str: @@ -998,8 +1039,8 @@ def _validate_pyi_wrapper_options(args: argparse.Namespace, parser: argparse.Arg ) if getattr(args, "export_symbols", None): parser.error( - "--export-symbols selects declarations while reading C source; a semantic .pyi contract " - "already states its public functions" + "--export-symbols selects the public surface while reading C source; a semantic .pyi " + "contract already states its public surface in __all__" ) if not getattr(args, "external_native_implementation", False) and not ( getattr(args, "native_fortran_sources", None) @@ -2290,7 +2331,10 @@ def _add_semantic_interpretation_options( group.add_argument( "--export-symbols", metavar="FILE", - help="Select exact reachable C functions from a UTF-8 name file; C semantic commands only", + help=( + "Select exact reachable C functions from a UTF-8 name file as the source-side " + "public surface; generate --pyi records the corresponding Python names in __all__" + ), ) diff --git a/prik/codegen/c/binding.py b/prik/codegen/c/binding.py index e52704e4a..11ec4a24b 100644 --- a/prik/codegen/c/binding.py +++ b/prik/codegen/c/binding.py @@ -14,7 +14,11 @@ import re from typing import ClassVar -from prik.utilities.declaration_expressions import declaration_extent_uses_power, render_declaration_extent +from prik.utilities.declaration_expressions import ( + RUNTIME_EXTENT_MARKERS, + declaration_extent_uses_power, + render_declaration_extent, +) from prik.policy.ownership import ( CodegenAction, ObjectKind, @@ -26,6 +30,7 @@ ArrayEntrypointABI, ArrayPythonLayout, CallbackABIKind, + CallbackOptionalityAction, CallbackResultAction, CallbackTransferAction, ClassConstructorKind, @@ -109,12 +114,14 @@ LifecycleActionPlan, ModulePlan, ModuleVariablePlan, + ModuleVariablePublicationPlan, NamespacePlan, NativeArrayHandlePlan, NativeEntrypointABIValueKind, NativeEntrypointABIValuePlan, GeneratedSupportProcedureImplementationOwner, GeneratedSupportProcedureEntrypointPlan, + NativeEntrypointExtentPlan, NativeEntrypointParameterPlan, NativeEntrypointProjectedSlotPlan, NativeEntrypointResultPlan, @@ -156,6 +163,10 @@ class _CArgumentNames: polymorphic_name: str +#: Entrypoint groups carrying extents the bridge evaluates and hands back. +_EXTENT_GROUPS = frozenset({"declaration_extent", "argument_extent"}) + + @dataclass class _CFunctionContext: """Per-function names and role substitutions shared across C lowering. @@ -230,7 +241,6 @@ class CBindingGenerator(ClassVisitor): class; unsupported plan actions fail instead of being reinterpreted here. """ - _RUNTIME_EXTENT_MARKERS = frozenset({":", "::Strided", "Flat"}) _SHARED_OUTPUT_CLEANUP_MIN_RESULTS = 4 def require_supported(self, plan: ModulePlan) -> None: @@ -326,15 +336,19 @@ def binding_module(self, plan: ModulePlan) -> CModule: self._binding_owned_derived_owner_paths = frozenset(plan.binding.owned_derived_type_owner_paths) self._binding_allocatable_holder_owner_paths = frozenset(plan.binding.allocatable_holder_type_owner_paths) self._binding_pointer_holder_owner_paths = frozenset(plan.binding.pointer_holder_type_owner_paths) - # Stage 2: complete the immutable name index consumed by Python-surface emission. - class_python_names = { - surface.type_identity: surface.python_names[0] + # Stage 2: index the one namespace defining each type. Code taking or + # returning a type can be in any namespace, so it reaches the type's + # class and helpers there, never in its own. + self._type_homes = { + derived.type_identity: (namespace.python_path, derived) for namespace in plan.namespaces - for surface in namespace.classes - if surface.python_names + for derived in namespace.derived_types } # Stage 3: select support and assemble generated functions in dependency order. - functions = tuple(function for namespace in plan.namespaces for function in self.visit(namespace)) + functions = ( + *(function for namespace in plan.namespaces for function in self.visit(namespace)), + *(function for variable in plan.variables for function in self.visit(variable)), + ) needs_native_support = self.requires_native_support(plan) needs_free = self._module_needs_allocator(plan) return CModule( @@ -359,7 +373,7 @@ def binding_module(self, plan: ModulePlan) -> CModule: *self._derived_handle_operation_functions(plan), *self._native_array_operation_functions(plan), *functions, - *self._overload_dispatch_functions(plan, class_python_names), + *self._overload_dispatch_functions(plan), self._module_init(plan, needs_native_support), ), ) @@ -647,10 +661,7 @@ def _scalar_result_expression(self, scalar, value_pointer: str, *, module: bool def _visit_NamespacePlan(self, plan: NamespacePlan) -> tuple[CFunction, ...]: """Return binding functions directly owned by one Python namespace.""" - return ( - *(self.visit(function) for function in plan.functions), - *(function for variable in plan.variables for function in self.visit(variable)), - ) + return tuple(self.visit(function) for function in plan.functions) def requires_native_support(self, plan: ModulePlan) -> bool: """Return whether module lowering consumes bundled native helpers.""" @@ -660,6 +671,9 @@ def requires_native_support(self, plan: ModulePlan) -> bool: # Every published component converts through the bundled helpers, so a # type whose module exposes only `bind(C)` procedures still needs them. or any(derived.fields for derived in self._derived_types(plan)) + # A namespace alias binds its target through a bundled helper, which a + # module publishing nothing else would otherwise never include. + or any(namespace.aliases for namespace in plan.namespaces) ) def _module_needs_allocator(self, plan: ModulePlan) -> bool: @@ -772,8 +786,7 @@ def _module_uses_memory_copy(self, plan: ModulePlan) -> bool: or self._module_uses_array_result_copy(plan) or any( variable.binding.getter_action is ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE - for namespace in plan.namespaces - for variable in namespace.variables + for variable in plan.variables ) or self._module_uses_derived_string_copy(plan) or self._module_uses_non_direct_derived_calls(plan) @@ -847,7 +860,6 @@ def _callback_runtime_declarations(self, plan: ModulePlan) -> tuple: callback.binding.context_type_symbol, ( CParameter("callable", "PyObject *"), - CParameter("module", "PyObject *"), CParameter("thread_id", "unsigned long"), CParameter( "previous", @@ -958,6 +970,35 @@ def _callback_trampoline_function(self, callback: CallbackHandoffPlan) -> CFunct body=tuple(nodes), ) + def _type_namespace(self, type_identity: tuple[str, str]) -> str: + """Return the retained module object of the namespace a type lives in.""" + return self._namespace_owner_name(self._type_homes[type_identity][0]) + + def _type_class_name(self, type_identity: tuple[str, str]) -> str: + """Return the name a type's class is defined under in its home.""" + return self._type_homes[type_identity][1].definition_name + + def _type_display_name(self, type_identity: tuple[str, str]) -> str: + """Return the name a message calls a type by: the one its contract declares. + + A type bound under no public name is defined under a private one, which + a message should not show. + """ + return self._type_homes[type_identity][1].contract_name + + def _type_attribute(self, type_identity: tuple[str, str], attribute: str) -> str: + """Return a new reference to one attribute of a type's home namespace.""" + return f'PyObject_GetAttrString({self._type_namespace(type_identity)}, "{attribute}")' + + def _type_class(self, type_identity: tuple[str, str]) -> str: + """Return a new reference to a type's class.""" + return self._type_attribute(type_identity, self._type_class_name(type_identity)) + + def _type_wrap_helper(self, type_identity: tuple[str, str]) -> str: + """Return a new reference to the helper wrapping a type's native storage.""" + backend_symbol = self._type_homes[type_identity][1].backend_symbol + return self._type_attribute(type_identity, CBindingNames.class_wrap_helper(backend_symbol)) + @staticmethod def _callback_abort_if_null( callback: CallbackHandoffPlan, @@ -978,9 +1019,45 @@ def _callback_python_argument_nodes( target: str, ) -> tuple: """Dispatch one completed Python projection into a small conversion leaf.""" + if transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER: + present_target = f"{target}_present" + present_nodes = self._callback_required_python_argument_nodes( + callback, + transfer, + position, + present_target, + ) + base = self._callback_parameter_base_name(transfer) + return ( + CDeclaration(target, "PyObject *", CodeExpression("NULL")), + CIf( + CodeExpression(f"{base}_data == NULL"), + body=( + CExpressionStatement(CodeExpression("Py_INCREF(Py_None)")), + CExpressionStatement(CodeExpression(f"{target} = Py_None")), + ), + else_body=( + *present_nodes, + CExpressionStatement(CodeExpression(f"{target} = {present_target}")), + ), + ), + self._callback_abort_if_null(callback, target, "failed to convert callback argument"), + ) + return self._callback_required_python_argument_nodes(callback, transfer, position, target) + + def _callback_required_python_argument_nodes( + self, + callback: CallbackHandoffPlan, + transfer: CallbackTransferPlan, + position: int, + target: str, + ) -> tuple: + """Project one callback dummy whose data pointer is known to be present.""" match transfer.python_action: case PythonBarrierAction.SCALAR_VALUE: nodes = self._callback_scalar_value_nodes(transfer, target) + case PythonBarrierAction.SCALAR_STORAGE: + nodes = self._callback_scalar_storage_nodes(transfer, target) case PythonBarrierAction.ARRAY_STORAGE: nodes = self._callback_array_nodes(transfer, position, target) case PythonBarrierAction.STRING_STORAGE: @@ -1019,6 +1096,41 @@ def _callback_scalar_value_nodes( ), ) + def _callback_scalar_storage_nodes( + self, + transfer: CallbackTransferPlan, + target: str, + ) -> tuple[CDeclaration, ...]: + """Materialize one completed rank-zero storage projection over native memory. + + The Python callable receives a rank-zero view of the same storage the + adapter hands the native caller, so an ``out`` or ``inout`` dummy is + written through instead of arriving as an independent value. + """ + if transfer.abi is not CallbackABIKind.REFERENCE: + raise ValueError( + f"Unsupported rank-zero storage callback ABI for {transfer.owner_path!r}: {transfer.abi.value}" + ) + scalar = PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name) + parameter = self._callback_parameter_base_name(transfer) + flags = "NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_ALIGNED" + if transfer.adapter_action in { + CallbackTransferAction.COPY_OUT, + CallbackTransferAction.COPY_IN_OUT, + CallbackTransferAction.BORROW_WRITABLE, + }: + flags += " | NPY_ARRAY_WRITEABLE" + return ( + CDeclaration( + target, + "PyObject *", + CodeExpression( + f"PyArray_New(&PyArray_Type, 0, NULL, {scalar.numpy_type_macro}, " + f"NULL, {parameter}_data, 0, {flags}, NULL)" + ), + ), + ) + def _callback_array_nodes( self, transfer: CallbackTransferPlan, @@ -1103,9 +1215,7 @@ def _callback_derived_nodes( CDeclaration( helper, "PyObject *", - CodeExpression( - f'PyObject_GetAttrString(callback_context->module, "_prik_wrap_{transfer.semantic_type_name}")' - ), + CodeExpression(self._type_wrap_helper(transfer.derived_type_identity)), ), CDeclaration( target, @@ -1261,7 +1371,7 @@ def _callback_derived_result_nodes( CDeclaration( "callback_expected_type", "PyObject *", - CodeExpression(f'PyObject_GetAttrString({context}->module, "{transfer.semantic_type_name}")'), + CodeExpression(self._type_class(transfer.derived_type_identity)), ), self._callback_abort_if_null( callback, @@ -2106,7 +2216,7 @@ def _module_declarations( *self._derived_private_method_prototypes(plan), *self._overload_dispatch_prototypes(plan), *self._derived_handle_operation_declarations(plan), - *self._derived_module_owner_declarations(plan), + *self._namespace_owner_declarations(plan), *self._module_variable_declarations(plan), *self._native_array_operation_declarations(plan), *self._namespace_declarations(plan), @@ -2216,7 +2326,7 @@ def _class_constructor_prototypes(self, plan: ModulePlan) -> tuple[CFunctionProt self._generated_support_procedure_entrypoint(surface.owner_path, "class:create") ), CFunctionPrototype( - CBindingNames.class_create_method(surface), + CBindingNames.class_create_method(surface.backend_symbol), "PyObject *", (CParameter("self", "PyObject *"), CParameter("args", "PyObject *")), "static", @@ -2247,7 +2357,7 @@ def _class_constructor_function( destroy = self._generated_support_procedure_entrypoint(derived.owner_path, "derived:destroy").symbol_name create = self._generated_support_procedure_entrypoint(surface.owner_path, "class:create").symbol_name return CFunction( - CBindingNames.class_create_method(surface), + CBindingNames.class_create_method(surface.backend_symbol), "PyObject *", parameters=(CParameter("self", "PyObject *"), CParameter("args", "PyObject *")), storage="static", @@ -2282,7 +2392,7 @@ def _class_constructor_function( CDeclaration( helper, "PyObject *", - CodeExpression(f'PyObject_GetAttrString(self, "{CBindingNames.class_wrap_helper(surface)}")'), + CodeExpression(self._type_wrap_helper(surface.type_identity)), ), CIf( CodeExpression(f"{helper} == NULL"), @@ -3427,7 +3537,6 @@ def _direct_nested_field_getter(self, derived: DerivedTypePlan, field: DerivedFi """Return direct nested field getter from the supplied completed binding records; this helper preserves the selected binding behavior.""" if field.derived is None: raise ValueError(f"Nested field {field.owner_path!r} has no derived handoff") - child_type = field.derived.type_name child_symbol = field.derived.backend_symbol body = ( *self._derived_owner_address_nodes(derived), @@ -3451,7 +3560,7 @@ def _direct_nested_field_getter(self, derived: DerivedTypePlan, field: DerivedFi CodeExpression(f'PyCapsule_New(child_address, "{self._derived_capsule_name(child_symbol)}", NULL)'), ), CIf(CodeExpression("child_capsule == NULL"), body=(CReturn(CodeExpression("NULL")),)), - *self._borrowed_derived_wrapper_nodes(child_type, "child_capsule", "owner_obj", None), + *self._borrowed_derived_wrapper_nodes(field.derived.type_identity, "child_capsule", "owner_obj", None), ) return self._derived_private_method(self._derived_field_method_name(derived, field, "get"), body) @@ -3465,7 +3574,7 @@ def _direct_nested_field_setter( return None body = ( *self._derived_owner_and_value_nodes(derived), - *self._exact_derived_type_check_nodes(field.derived.type_name, "value_obj", field.name), + *self._exact_derived_type_check_nodes(field.derived, "value_obj", field.name), *self._derived_address_from_object_nodes(field.derived.backend_symbol, "value_obj", "value"), CExpressionStatement( CodeExpression( @@ -3489,7 +3598,7 @@ def _module_nested_member_getter( CDeclaration("owner_obj", "PyObject *"), CExpressionStatement(CodeExpression('if (!PyArg_ParseTuple(args, "O", &owner_obj)) return NULL')), *self._borrowed_derived_wrapper_nodes( - field.derived.type_name, + field.derived.type_identity, "Py_None", "owner_obj", self._module_member_ops_name(variable, member.path), @@ -3513,7 +3622,7 @@ def _module_nested_member_setter( CExpressionStatement( CodeExpression('if (!PyArg_ParseTuple(args, "OO", &owner_obj, &value_obj)) return NULL') ), - *self._exact_derived_type_check_nodes(field.derived.type_name, "value_obj", field.name), + *self._exact_derived_type_check_nodes(field.derived, "value_obj", field.name), *self._derived_address_from_object_nodes(field.derived.backend_symbol, "value_obj", "value"), CExpressionStatement( CodeExpression(f"{self._module_member_bridge_name(variable, member, 'set')}(value_address)") @@ -3524,7 +3633,7 @@ def _module_nested_member_setter( def _borrowed_derived_wrapper_nodes( self, - type_name: str, + type_identity: tuple[str, str], capsule_name: str, owner_name: str, ops_name: str | None, @@ -3536,7 +3645,7 @@ def _borrowed_derived_wrapper_nodes( CDeclaration( "child_helper", "PyObject *", - CodeExpression(f'PyObject_GetAttrString(self, "_prik_wrap_{type_name}")'), + CodeExpression(self._type_wrap_helper(type_identity)), ), CIf( CodeExpression("child_helper == NULL"), @@ -3643,12 +3752,16 @@ def _derived_address_from_object_nodes(self, type_symbol: str, object_name: str, CIf(CodeExpression(f"{address} == NULL"), body=(CReturn(CodeExpression("NULL")),)), ) - @staticmethod - def _exact_derived_type_check_nodes(type_name: str, object_name: str, label: str) -> tuple: - """Require the exact exported opaque class before a concrete field copy.""" + def _exact_derived_type_check_nodes(self, handoff: DerivedHandoffPlan, object_name: str, label: str) -> tuple: + """Require the exact exported opaque class before a concrete field copy. + + The message names the type the way the declaration being set refers to + it, which tells apart two types spelled alike where they are declared. + """ expected = f"{label}_expected_type" + type_name = handoff.type_name return ( - CDeclaration(expected, "PyObject *", CodeExpression(f'PyObject_GetAttrString(self, "{type_name}")')), + CDeclaration(expected, "PyObject *", CodeExpression(self._type_class(handoff.type_identity))), CIf(CodeExpression(f"{expected} == NULL"), body=(CReturn(CodeExpression("NULL")),)), CIf( CodeExpression(f"Py_TYPE({object_name}) != (PyTypeObject *){expected}"), @@ -5955,10 +6068,9 @@ def _documented(functions: tuple[CFunction, ...], *doc: str) -> tuple[CFunction, def _visit_ModuleVariablePlan(self, plan: ModuleVariablePlan) -> tuple[CFunction, ...]: """Lower binding-owned getter and setter actions into C functions.""" - # The binding facet names the Python attribute and the C symbols it - # calls; the native Fortran variable belongs to the bridge facet and is - # deliberately not read here. - name = plan.binding.python_names[0] + # One helper serves every Python publication, so its documentation uses + # the stable declaring native name rather than an arbitrary alias. + name = plan.owner_path.rsplit(".", 1)[-1] return ( *self._documented( self._lower_module_getter(plan), @@ -6059,7 +6171,7 @@ def _lower_module_setter_character_value(self, plan: ModuleVariablePlan) -> tupl attribute assignment is an ``int`` slot, not a returned object. """ length = self._module_character_length(plan) - name = plan.binding.python_names[0] + name = plan.owner_path.rsplit(".", 1)[-1] return ( CFunction( self._module_setter_name(plan), @@ -6383,7 +6495,7 @@ def _lower_module_getter_derived_object(self, plan: ModuleVariablePlan) -> tuple raise ValueError(f"Derived module object {plan.owner_path!r} has no access plan") if derived.access is ModuleObjectAccessMechanism.VALUE_COPY: return self._lower_module_getter_derived_value_copy(plan) - owner = self._derived_module_owner_name(plan) + owner = self._namespace_owner_name(plan.binding.support_namespace) capsule_expression = ( CodeExpression( f"PyCapsule_New({self._module_bridge_getter_name(plan)}(), " @@ -6411,9 +6523,7 @@ def _lower_module_getter_derived_value_copy(self, plan: ModuleVariablePlan) -> t derived = plan.derived if derived is None: raise ValueError(f"Derived module constant {plan.owner_path!r} has no handoff") - type_name = derived.handoff.type_name type_symbol = derived.handoff.backend_symbol - owner = self._derived_module_owner_name(plan) address = "address" capsule = "capsule" helper = "helper" @@ -6451,7 +6561,7 @@ def _lower_module_getter_derived_value_copy(self, plan: ModuleVariablePlan) -> t CDeclaration( helper, "PyObject *", - CodeExpression(f'PyObject_GetAttrString({owner}, "_prik_wrap_{type_name}")'), + CodeExpression(self._type_wrap_helper(derived.handoff.type_identity)), ), CIf( CodeExpression(f"{helper} == NULL"), @@ -6481,12 +6591,11 @@ def _module_derived_wrapper_nodes( """Call the namespace's internal wrapper helper with explicit owner/ops.""" if plan.derived is None: return () - type_name = plan.derived.handoff.type_name nodes = [ CDeclaration( "helper", "PyObject *", - CodeExpression(f'PyObject_GetAttrString({owner}, "_prik_wrap_{type_name}")'), + CodeExpression(self._type_wrap_helper(plan.derived.handoff.type_identity)), ), CIf( CodeExpression("helper == NULL"), @@ -6597,7 +6706,7 @@ def _module_setter_unpack_statement(self, plan, scalar_type) -> CExpressionState "value", ( f'PyErr_Format(PyExc_TypeError, "Expected an argument of type ' - f"{scalar_type.python_type_name} for module variable {plan.binding.python_names[0]}. " + f"{scalar_type.python_type_name} for module variable {plan.owner_path.rsplit('.', 1)[-1]}. " "Received \", Py_TYPE(value_obj)->tp_name)" ), "-1", @@ -6626,7 +6735,7 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> CFunction: *argument_declarations, *alias_declarations, *self._callback_context_declarations(plan), - *self._declaration_extent_result_declarations(plan), + *self._entrypoint_extent_declarations(plan), *self._direct_result_declaration(plan, context), *self._native_output_declarations(plan, context), self._parse_statement(plan, context), @@ -6799,10 +6908,19 @@ def _lower_argument_callback( ) -> tuple[CDeclaration | CIf, ...]: """Validate an immediate Python callable before any context is retained.""" names = context.arguments[plan.owner_path] + optional = plan.binding.optional_mode is OptionalMode.NULLABLE_VALUE return ( - CDeclaration(names.object_name, "PyObject *"), + CDeclaration( + names.object_name, + "PyObject *", + CodeExpression("Py_None") if optional else None, + ), CIf( - CodeExpression(f"!PyCallable_Check({names.object_name})"), + CodeExpression( + f"{names.object_name} != Py_None && !PyCallable_Check({names.object_name})" + if optional + else f"!PyCallable_Check({names.object_name})" + ), body=( CExpressionStatement( CodeExpression( @@ -6823,6 +6941,7 @@ def _callback_context_declarations( CDeclaration( self._callback_context_name(argument), argument.callback.binding.context_type_symbol, + CodeExpression("{0}"), ) for argument in plan.arguments if argument.callback is not None @@ -6832,20 +6951,19 @@ def _callback_context_push_nodes( self, plan: FunctionPlan, context: _CFunctionContext, - ) -> tuple[CExpressionStatement, ...]: + ) -> tuple[CExpressionStatement | CIf, ...]: """Retain callables and publish each stack context immediately before entry.""" - return tuple( - node - for argument in plan.arguments - if argument.callback is not None - for node in ( + nodes = [] + for argument in plan.arguments: + if argument.callback is None: + continue + body = ( CExpressionStatement( CodeExpression( f"{self._callback_context_name(argument)}.callable = " f"{context.arguments[argument.owner_path].object_name}" ) ), - CExpressionStatement(CodeExpression(f"{self._callback_context_name(argument)}.module = self")), CExpressionStatement( CodeExpression(f"{self._callback_context_name(argument)}.thread_id = PyThread_get_thread_ident()") ), @@ -6859,25 +6977,28 @@ def _callback_context_push_nodes( CExpressionStatement( CodeExpression(f"Py_INCREF({context.arguments[argument.owner_path].object_name})") ), - CExpressionStatement(CodeExpression("Py_INCREF(self)")), CExpressionStatement( CodeExpression( f"{argument.callback.binding.context_current_symbol} = &{self._callback_context_name(argument)}" ) ), ) - ) + if argument.binding.optional_mode is OptionalMode.NULLABLE_VALUE: + name = context.arguments[argument.owner_path].object_name + nodes.append(CIf(CodeExpression(f"{name} != Py_None"), body=body)) + else: + nodes.extend(body) + return tuple(nodes) def _callback_context_pop_nodes( self, plan: FunctionPlan, - ) -> tuple[CExpressionStatement, ...]: + ) -> tuple[CExpressionStatement | CIf, ...]: """Restore nested stacks and release retained objects in reverse order.""" arguments = tuple(argument for argument in plan.arguments if argument.callback is not None) - return tuple( - node - for argument in reversed(arguments) - for node in ( + nodes = [] + for argument in reversed(arguments): + body = ( CExpressionStatement( CodeExpression( f"{argument.callback.binding.context_current_symbol} = " @@ -6887,10 +7008,18 @@ def _callback_context_pop_nodes( CExpressionStatement( CodeExpression(f"Py_XDECREF({self._callback_context_name(argument)}.last_result)") ), - CExpressionStatement(CodeExpression(f"Py_DECREF({self._callback_context_name(argument)}.module)")), CExpressionStatement(CodeExpression(f"Py_DECREF({self._callback_context_name(argument)}.callable)")), ) - ) + if argument.binding.optional_mode is OptionalMode.NULLABLE_VALUE: + nodes.append( + CIf( + CodeExpression(f"{self._callback_context_name(argument)}.callable != NULL"), + body=body, + ) + ) + else: + nodes.extend(body) + return tuple(nodes) @staticmethod def _callback_context_name(argument: ArgumentTransferPlan) -> str: @@ -7076,16 +7205,17 @@ def _polymorphic_argument_nodes( for variant in dispatch.variants: nodes.extend( ( - CExpressionStatement( - CodeExpression(f'{expected} = PyObject_GetAttrString(self, "{variant.python_name}")') - ), + CExpressionStatement(CodeExpression(f"{expected} = {self._type_class(variant.type_identity)}")), CIf(CodeExpression(f"{expected} == NULL"), body=(CReturn(CodeExpression("NULL")),)), CIf( CodeExpression(f"Py_TYPE({names.object_name}) == (PyTypeObject *){expected}"), body=( CExpressionStatement(CodeExpression(f"{code} = {variant.abi_code}")), CExpressionStatement( - CodeExpression(f"{type_name} = {self._c_string_literal(variant.python_name)}") + CodeExpression( + f"{type_name} = " + f"{self._c_string_literal(self._type_display_name(variant.type_identity))}" + ) ), CExpressionStatement( CodeExpression(f"{type_symbol} = {self._c_string_literal(variant.backend_symbol)}") @@ -7101,7 +7231,7 @@ def _polymorphic_argument_nodes( CExpressionStatement(CodeExpression(f"Py_DECREF({expected})")), ) ) - accepted = ", ".join(variant.python_name for variant in dispatch.variants) + accepted = ", ".join(self._type_display_name(variant.type_identity) for variant in dispatch.variants) nodes.append( CIf( CodeExpression(f"{code} == 0"), @@ -8028,7 +8158,7 @@ def _outlined_array_bind_axis_value( flattened: bool, ) -> str | None: """Lower one axis extent, or None when the axis carries no declared extent.""" - if flattened or expression in self._RUNTIME_EXTENT_MARKERS: + if flattened or expression in RUNTIME_EXTENT_MARKERS: return None if array.extent_evaluation[axis] == "bridge": return None @@ -8199,7 +8329,7 @@ def _native_array_actual_shape_checks( nodes = [] for axis, expression in enumerate(actual.shape): if ( - expression in {":", "::Strided", "Flat"} + expression in RUNTIME_EXTENT_MARKERS or (actual.flatten_storage and axis == actual.flat_axis) or array.extent_evaluation[axis] == "bridge" ): @@ -8334,7 +8464,7 @@ def _array_shape_checks( if handoff is None or handoff.rank is None: return () checks = [] - runtime_markers = {":", "::Strided", "Flat"} + runtime_markers = RUNTIME_EXTENT_MARKERS for axis, expression in enumerate(handoff.shape): if expression in runtime_markers: continue @@ -8365,7 +8495,7 @@ def _descriptor_array_shape_checks( return () checks = [] for axis, expression in enumerate(handoff.shape): - if expression in {":", "::Strided", "Flat"} or handoff.extent_evaluation[axis] == "bridge": + if expression in RUNTIME_EXTENT_MARKERS or handoff.extent_evaluation[axis] == "bridge": continue expected = self._array_extent_expression(handoff, axis, expression, context) checks.append( @@ -9845,9 +9975,10 @@ def _result_extent_expression( expression: str, context: _CFunctionContext, ) -> str: - """Use the entrypoint result for native axes and local roles for all others.""" - if handoff.extent_evaluation[axis] == "bridge": - return self._declaration_extent_result_name(result, axis) + """Use the extent the bridge returned for its axes and local roles for all others.""" + evaluated = context.function.entrypoint.extent_names(result.owner_path).get(axis) + if evaluated is not None: + return evaluated return self._array_extent_expression(handoff, axis, expression, context) def _array_result_creation_expression( @@ -10033,7 +10164,7 @@ def _lower_result_derived( CDeclaration( helper, "PyObject *", - CodeExpression(f'PyObject_GetAttrString(self, "_prik_wrap_{plan.derived.type_name}")'), + CodeExpression(self._type_wrap_helper(plan.derived.type_identity)), ), CIf( CodeExpression(f"{helper} == NULL"), @@ -10067,8 +10198,6 @@ def _lower_holder_result( """Wrap one nullable typed holder without exposing its component address.""" if plan.derived is None: raise ValueError(f"Derived result {plan.owner_path!r} has no handoff plan") - type_name = plan.derived.type_name - type_symbol = plan.derived.backend_symbol storage = plan.derived.storage native_name = self._result_native_name(plan, context) python_name = context.python_results[plan.owner_path] @@ -10084,8 +10213,7 @@ def _lower_holder_result( CReturn(CodeExpression("NULL")), ), else_body=self._holder_wrapper_nodes( - type_name, - type_symbol, + plan.derived, storage, self._derived_target_owner(plan.derived), native_name, @@ -10097,8 +10225,7 @@ def _lower_holder_result( def _holder_wrapper_nodes( self, - type_name: str, - type_symbol: str, + derived: DerivedHandoffPlan, storage: DerivedObjectStorage, owner: str, address: str, @@ -10107,7 +10234,7 @@ def _holder_wrapper_nodes( ) -> tuple[CDeclaration | CExpressionStatement | CIf, ...]: """Construct one holder-backed wrapper with a single cleanup path.""" capsule_name, destructor_name, destroy_name, ops_name, origin = self._holder_wrapper_symbols( - type_symbol, + derived.backend_symbol, storage, ) capsule = f"{target}_capsule" @@ -10130,7 +10257,7 @@ def _holder_wrapper_nodes( CDeclaration( helper, "PyObject *", - CodeExpression(f'PyObject_GetAttrString(self, "_prik_wrap_{type_name}")'), + CodeExpression(self._type_wrap_helper(derived.type_identity)), ), CIf( CodeExpression(f"{helper} == NULL"), @@ -10143,7 +10270,7 @@ def _holder_wrapper_nodes( CDeclaration( ops, "PyObject *", - CodeExpression(f'PyObject_GetAttrString(self, "{ops_name}")'), + CodeExpression(self._type_attribute(derived.type_identity, ops_name)), ), CIf( CodeExpression(f"{ops} == NULL"), @@ -12201,6 +12328,7 @@ def _output_nodes( *self._callback_context_push_nodes(plan, context), *self._lower_entrypoint_call(plan, context), *self._callback_context_pop_nodes(plan), + *self._argument_extent_rejection_nodes(plan, context), *self._derived_call_failure_nodes(plan, context), *self._derived_after_native_failure_nodes(plan, context), *self._derived_result_allocation_failure_nodes(plan, context), @@ -12415,9 +12543,7 @@ def _derived_after_native_failure_nodes( CIf( CodeExpression(f"{fault} != NULL && {fault}[0] != '\\0' && {fault}[0] != '0'"), body=( - *self._string_replacement_cleanup_nodes(plan, context), - *self._binding_transformation_cleanup_nodes(plan, context), - *self._native_result_failure_cleanup_nodes(plan.results, context), + *self._post_call_failure_cleanup_nodes(plan, context), CExpressionStatement( CodeExpression( 'PyErr_SetString(PyExc_RuntimeError, "injected derived failure after native return")' @@ -12439,9 +12565,7 @@ def _derived_call_failure_nodes( CIf( CodeExpression(f"{self._derived_status_name(context.arguments[argument.owner_path])} != 0"), body=( - *self._string_replacement_cleanup_nodes(plan, context), - *self._binding_transformation_cleanup_nodes(plan, context), - *self._native_result_failure_cleanup_nodes(plan.results, context), + *self._post_call_failure_cleanup_nodes(plan, context), *self._one_derived_call_error_nodes(argument, context), CReturn(CodeExpression("NULL")), ), @@ -12493,11 +12617,7 @@ def _derived_result_allocation_failure_nodes( if not derived: return () native_names = tuple(self._result_native_name(result, context) for result in derived) - cleanup = [ - *self._string_replacement_cleanup_nodes(plan, context), - *self._binding_transformation_cleanup_nodes(plan, context), - *self._native_result_failure_cleanup_nodes(plan.results, context), - ] + cleanup = self._post_call_failure_cleanup_nodes(plan, context) return ( CIf( CodeExpression(" || ".join(f"{name} == NULL" for name in native_names)), @@ -12695,9 +12815,7 @@ def _lower_status_error_runtime_error( policy = plan.binding.status_error status_name = context.native_outputs[policy.status_role] condition = CodeExpression(f"{status_name} != {policy.success}") - transformation_cleanup = self._binding_transformation_cleanup_nodes(plan, context) - string_cleanup = self._string_replacement_cleanup_nodes(plan, context) - native_result_cleanup = self._native_result_failure_cleanup_nodes(plan.results, context) + cleanup = self._post_call_failure_cleanup_nodes(plan, context) if policy.message_role is None and policy.message_argument is None: return ( CIf( @@ -12709,9 +12827,7 @@ def _lower_status_error_runtime_error( f"(int){status_name})" ) ), - *string_cleanup, - *transformation_cleanup, - *native_result_cleanup, + *cleanup, CReturn(CodeExpression("NULL")), ), ), @@ -12771,17 +12887,13 @@ def _lower_status_error_runtime_error( CIf( CodeExpression(f"{message_object} == NULL"), body=( - *string_cleanup, - *transformation_cleanup, - *native_result_cleanup, + *cleanup, CReturn(CodeExpression("NULL")), ), ), CExpressionStatement(CodeExpression(f"PyErr_SetObject(PyExc_RuntimeError, {message_object})")), CExpressionStatement(CodeExpression(f"Py_DECREF({message_object})")), - *string_cleanup, - *transformation_cleanup, - *native_result_cleanup, + *cleanup, CReturn(CodeExpression("NULL")), ), ), @@ -12795,9 +12907,7 @@ def _lower_status_error_runtime_error( CodeExpression(f"{message_name} == NULL"), body=( CExpressionStatement(CodeExpression("PyErr_NoMemory()")), - *string_cleanup, - *transformation_cleanup, - *native_result_cleanup, + *cleanup, CReturn(CodeExpression("NULL")), ), ), @@ -12817,9 +12927,7 @@ def _lower_status_error_runtime_error( CIf( CodeExpression(f"{message_object} == NULL"), body=( - *string_cleanup, - *transformation_cleanup, - *native_result_cleanup, + *cleanup, CReturn(CodeExpression("NULL")), ), ), @@ -12828,9 +12936,7 @@ def _lower_status_error_runtime_error( body=( CExpressionStatement(CodeExpression(f"PyErr_SetObject(PyExc_RuntimeError, {message_object})")), CExpressionStatement(CodeExpression(f"Py_DECREF({message_object})")), - *string_cleanup, - *transformation_cleanup, - *native_result_cleanup, + *cleanup, CReturn(CodeExpression("NULL")), ), ), @@ -13017,8 +13123,7 @@ def _holder_writeback_value_nodes( CExpressionStatement(CodeExpression(f"{result} = Py_None")), ), else_body=self._holder_wrapper_nodes( - source.derived.type_name, - source.derived.backend_symbol, + source.derived, storage, self._derived_target_owner(source.derived), names.value_name, @@ -13230,20 +13335,80 @@ def _direct_result_declaration( scalar_type = PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name) return (CDeclaration(context.result_name, scalar_type.c_spelling),) - def _declaration_extent_result_declarations(self, plan: FunctionPlan) -> tuple[CDeclaration, ...]: - """Declare storage populated by native-dependent main-bridge extent outputs.""" + @staticmethod + def _entrypoint_extent_declarations(plan: FunctionPlan) -> tuple[CDeclaration, ...]: + """Declare storage for every extent the bridge evaluates and hands back.""" return tuple( - CDeclaration( - self._declaration_extent_result_name(result, axis), - "int64_t", - CodeExpression("0"), + CDeclaration(extent.parameter_name, "int64_t", CodeExpression("0")) + for parameter in plan.entrypoint.parameters + for extent in parameter.extents + ) + + def _argument_extent_rejection_nodes( + self, + plan: FunctionPlan, + context: _CFunctionContext, + ) -> tuple[CIf, ...]: + """Reject an actual shorter or longer than the extent its dummy declares. + + Only the bridge can evaluate a specification function. It returned the + extent the dummy declares and ran the native procedure only when every + actual matched, so a mismatch arrives here with nothing called, and is + reported the way a binding-checked extent is. + """ + cleanup = self._post_call_failure_cleanup_nodes(plan, context) + return tuple( + CIf( + CodeExpression( + self._argument_extent_mismatch(argument, extent, context.arguments[argument.owner_path]) + ), + body=( + CExpressionStatement( + CodeExpression( + f'PyErr_SetString(PyExc_TypeError, "Argument {argument.binding.python_name} has ' + f'incompatible shape at axis {extent.axis}")' + ) + ), + *cleanup, + CReturn(CodeExpression("NULL")), + ), ) - for result in plan.results - if result.array is not None - for axis, evaluation in enumerate(result.array.extent_evaluation) - if evaluation == "bridge" + for parameter in plan.entrypoint.parameters + if parameter.source_kind == "argument_extent" + for argument in (self._argument_by_owner(plan, parameter.owner_path),) + for extent in parameter.extents + ) + + def _post_call_failure_cleanup_nodes( + self, + plan: FunctionPlan, + context: _CFunctionContext, + ) -> tuple[CExpressionStatement, ...]: + """Release what the call path holds when it fails after the entrypoint returns. + + That is the string buffers and array temporaries taken before the call + and any native result storage; every release is safe for storage the + call did not produce. + """ + return ( + *self._string_replacement_cleanup_nodes(plan, context), + *self._binding_transformation_cleanup_nodes(plan, context), + *self._native_result_failure_cleanup_nodes(plan.results, context), ) + @staticmethod + def _argument_extent_mismatch( + argument: ArgumentTransferPlan, + extent: NativeEntrypointExtentPlan, + names: _CArgumentNames, + ) -> str: + """Return when one actual disagrees with the extent its dummy declares.""" + mismatch = f"{extent.parameter_name} != {names.extent_names[extent.axis]}" + if argument.entrypoint.optional_mode is OptionalMode.REQUIRED: + return mismatch + # An omitted actual has no extent to disagree with. + return f"{names.object_name} != Py_None && {mismatch}" + def _native_output_declarations( self, plan: FunctionPlan, @@ -13538,9 +13703,7 @@ def _binding_transformation_post_call_nodes( ) -> tuple[CExpressionStatement | CIf, ...]: """Copy back ordinary temporaries and retain published replacements.""" nodes = [] - cleanup = self._binding_transformation_cleanup_nodes(plan, context) - string_cleanup = self._string_replacement_cleanup_nodes(plan, context) - native_result_cleanup = self._native_result_failure_cleanup_nodes(plan.results, context) + cleanup = self._post_call_failure_cleanup_nodes(plan, context) for argument in plan.arguments: action = self._transformation_action(argument, WritebackPhase.COPY_OUT) if action is not TransformationAction.COPY_ARRAY_REPRESENTATION: @@ -13552,12 +13715,7 @@ def _binding_transformation_post_call_nodes( CodeExpression( f"PyArray_CopyInto((PyArrayObject *){names.object_name}, (PyArrayObject *){temporary}) < 0" ), - body=( - *string_cleanup, - *cleanup, - *native_result_cleanup, - CReturn(CodeExpression("NULL")), - ), + body=(*cleanup, CReturn(CodeExpression("NULL"))), ) ) nodes.extend(self._binding_transformation_success_cleanup_nodes(plan, context)) @@ -13726,6 +13884,8 @@ def _entrypoint_parameter_values( if slot.native_scalar_c_type is not None and slot.passing is EntrypointPassingConvention.C_VALUE: values[0] = f"({slot.native_scalar_c_type}){values[0]}" return tuple(values) + if parameter.source_kind in _EXTENT_GROUPS: + return tuple(f"&{extent.parameter_name}" for extent in parameter.extents) if parameter.source_kind == "projected_slot": return self._projected_slot_values( plan, @@ -13738,8 +13898,6 @@ def _entrypoint_parameter_values( return self._entrypoint_hidden_result_values(result, name) if parameter.source_kind == "direct_result": return self._entrypoint_direct_result_values(result, context) - if parameter.source_kind == "declaration_extent": - return self._declaration_extent_result_values_for_result(result) raise ValueError(f"Unsupported entrypoint parameter group {parameter.source_kind!r}") @staticmethod @@ -13859,19 +14017,6 @@ def _entrypoint_hidden_results(self, plan: FunctionPlan) -> tuple[NativeEntrypoi if parameter.source_kind == "hidden_result" ) - def _declaration_extent_result_values_for_result( - self, - result: NativeEntrypointResultPlan, - ) -> tuple[str, ...]: - """Return extent output actuals for one planned result group.""" - if result.array is None: - return () - return tuple( - f"&{self._declaration_extent_result_name(result, axis)}" - for axis, evaluation in enumerate(result.array.extent_evaluation) - if evaluation == "bridge" - ) - def _entrypoint_hidden_result_values( self, result: NativeEntrypointResultPlan, @@ -13915,7 +14060,10 @@ def _entrypoint_argument_values( if plan.callback is not None: if not plan.entrypoint.pass_callback_parameter: return () - return (plan.callback.entrypoint.support_procedure.symbol_name,) + symbol = plan.callback.entrypoint.support_procedure.symbol_name + if plan.entrypoint.optional_mode is OptionalMode.NULLABLE_VALUE: + return (f"{names.object_name} != Py_None ? {symbol} : NULL",) + return (symbol,) if plan.entrypoint.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER: return self._string_entrypoint_argument_values(plan, names, passing=passing) if plan.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: @@ -14064,6 +14212,8 @@ def _entrypoint_parameter_declarations( self._argument_by_owner(plan, parameter.owner_path), passing=slot.passing, ) + if parameter.source_kind in _EXTENT_GROUPS: + return tuple(CParameter(extent.parameter_name, "int64_t *") for extent in parameter.extents) if parameter.source_kind == "projected_slot": return self._projected_slot_parameters(self._projected_slot_for_parameter(plan, parameter)) result = self._entrypoint_result_by_owner(plan, parameter.owner_path) @@ -14071,28 +14221,8 @@ def _entrypoint_parameter_declarations( return self._entrypoint_result_parameters(result) if parameter.source_kind == "direct_result": return self._direct_entrypoint_result_parameters(result) - if parameter.source_kind == "declaration_extent": - return self._declaration_extent_result_parameters_for_result(result) raise ValueError(f"Unsupported entrypoint parameter group {parameter.source_kind!r}") - def _declaration_extent_result_parameters_for_result( - self, - result: NativeEntrypointResultPlan, - ) -> tuple[CParameter, ...]: - """Declare native-dependent extent outputs for one result group.""" - if result.array is None: - return () - return tuple( - CParameter(self._declaration_extent_result_name(result, axis), "int64_t *") - for axis, evaluation in enumerate(result.array.extent_evaluation) - if evaluation == "bridge" - ) - - @staticmethod - def _declaration_extent_result_name(result: ResultPlan | NativeEntrypointResultPlan, axis: int) -> str: - """Return the shared entrypoint ABI name for one evaluated result axis.""" - return f"prik_decl_extent_{result.result_position}_{axis}" - def _owned_native_array_bridge_prototypes(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: """Declare typed Fortran operations over binding-owned result descriptors.""" return tuple( @@ -14412,14 +14542,14 @@ def _module_property_support( ), reject_replacement=(variable.binding.setter_action is SetterAction.REJECT_REPLACEMENT), ) - for variable in namespace.variables + for variable, publication in self._variable_publications(namespace) if variable.binding.getter_action not in { ModuleGetterAction.CONSTANT_VALUE, ModuleGetterAction.NATIVE_CONSTANT_VALUE, ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE, } - for python_name in variable.binding.python_names + for python_name in publication.python_names ) if not entries: return None @@ -14674,31 +14804,40 @@ def _derived_module_variables(self, plan: ModulePlan) -> tuple[ModuleVariablePla """Return every live native-owned derived module object.""" return tuple(variable for variable in self._variables(plan) if variable.derived is not None) - def _derived_module_owner_declarations(self, plan: ModulePlan) -> tuple[CDeclaration, ...]: - """Retain the Python module owner for borrowed derived objects.""" + def _owner_namespaces(self, plan: ModulePlan) -> tuple[tuple[str, ...], ...]: + """Return each namespace generated code reaches outside a call's own. + + That is every type's home, and the namespace each derived module + object's helpers live in, which also owns the objects it lends out. + """ + paths = {path for path, _ in self._type_homes.values()} + paths.update(variable.binding.support_namespace for variable in self._derived_module_variables(plan)) + return tuple(sorted(paths)) + + def _namespace_owner_declarations(self, plan: ModulePlan) -> tuple[CDeclaration, ...]: + """Retain the module object of each namespace reached from another.""" return tuple( - CDeclaration( - self._derived_module_owner_name(variable), - "static PyObject *", - CodeExpression("NULL"), - ) - for variable in self._derived_module_variables(plan) + CDeclaration(self._namespace_owner_name(path), "static PyObject *", CodeExpression("NULL")) + for path in self._owner_namespaces(plan) ) - @staticmethod - def _derived_module_owner_name(variable: ModuleVariablePlan) -> str: - """Return the binding-local derived module owner name derived from the supplied completed binding records; this helper preserves completed policy.""" - owner = re.sub(r"\W", "_", variable.owner_path).casefold() - return f"prik_module_{owner}_derived_owner" + @classmethod + def _namespace_owner_name(cls, python_path: tuple[str, ...]) -> str: + """Return the retained module object of one namespace.""" + return f"prik_namespace_{cls._path_symbol(python_path)}_owner" def _method_table(self, module: ModulePlan, namespace: NamespacePlan) -> CMethodDefTable: """Build method table from the supplied completed binding records; emitted nodes only project completed binding actions.""" return CMethodDefTable( f"{module.binding.owner_path}_{self._namespace_symbol(namespace)}_methods", - self._method_entries(namespace), + self._method_entries(module, namespace), ) - def _method_entries(self, namespace: NamespacePlan) -> tuple[CMethodDefEntry, ...]: + def _method_entries( + self, + module: ModulePlan, + namespace: NamespacePlan, + ) -> tuple[CMethodDefEntry, ...]: """Return the exact callable definitions installed in one namespace.""" return ( *( @@ -14713,15 +14852,15 @@ def _method_entries(self, namespace: NamespacePlan) -> tuple[CMethodDefEntry, .. *self._overload_method_entries(namespace), *( CMethodDefEntry( - CBindingNames.class_create_method(surface), - CBindingNames.class_create_method(surface), + CBindingNames.class_create_method(surface.backend_symbol), + CBindingNames.class_create_method(surface.backend_symbol), "METH_VARARGS", "", ) for surface in namespace.classes if surface.constructor.kind is not ClassConstructorKind.ABSENT ), - *self._derived_private_method_entries(namespace), + *self._derived_private_method_entries(module, namespace), ) @staticmethod @@ -14769,23 +14908,15 @@ def _namespace_overload_dispatches(namespace: NamespacePlan) -> tuple[_COverload seen.add(id(overload)) return tuple(dispatches) - def _overload_dispatch_functions( - self, - plan: ModulePlan, - class_python_names: dict[tuple[str, str], str], - ) -> tuple[CFunction, ...]: + def _overload_dispatch_functions(self, plan: ModulePlan) -> tuple[CFunction, ...]: """Lower every completed overload surface into one C dispatcher.""" return tuple( - self._overload_dispatch_function(dispatch, class_python_names) + self._overload_dispatch_function(dispatch) for namespace in plan.namespaces for dispatch in self._namespace_overload_dispatches(namespace) ) - def _overload_dispatch_function( - self, - dispatch: _COverloadDispatch, - class_python_names: dict[tuple[str, str], str], - ) -> CFunction: + def _overload_dispatch_function(self, dispatch: _COverloadDispatch) -> CFunction: """Classify one call, assign a candidate ID, and switch to its wrapper.""" overload = dispatch.overload positional_offset = 1 if dispatch.receiver else 0 @@ -14805,7 +14936,6 @@ def _overload_dispatch_function( + self._overload_candidate_condition( matches, positional_offset=positional_offset, - class_python_names=class_python_names, ) + ")" ), @@ -14899,7 +15029,6 @@ def _overload_candidate_condition( matches: tuple[OverloadArgumentMatchPlan, ...], *, positional_offset: int, - class_python_names: dict[tuple[str, str], str], ) -> str: """Return one ordered candidate predicate over borrowed call arguments.""" shape = self._overload_call_shape_condition(matches) @@ -14907,7 +15036,6 @@ def _overload_candidate_condition( self._overload_argument_condition( match, self._overload_argument_value_expression(match, index, positional_offset), - class_python_names, ) for index, match in enumerate(matches) ) @@ -14950,10 +15078,9 @@ def _overload_argument_condition( self, match: OverloadArgumentMatchPlan, value: str, - class_python_names: dict[tuple[str, str], str], ) -> str: """Wrap one exact C predicate with its required or optional presence rule.""" - predicate = self._overload_required_argument_condition(match, value, class_python_names) + predicate = self._overload_required_argument_condition(match, value) if match.optional: return f"({value} == NULL || ({predicate}))" return f"({value} != NULL && ({predicate}))" @@ -14962,14 +15089,14 @@ def _overload_required_argument_condition( self, match: OverloadArgumentMatchPlan, value: str, - class_python_names: dict[tuple[str, str], str], ) -> str: """Return the C-API predicate for one completed overload match kind.""" if match.kind is OverloadMatchKind.DERIVED: if match.derived_type_identity is None: raise ValueError(f"Derived overload argument {match.python_name!r} has no type identity") - class_name = self._c_string_literal(class_python_names[match.derived_type_identity]) - expected = f"PyDict_GetItemString(PyModule_GetDict(self), {class_name})" + class_name = self._c_string_literal(self._type_class_name(match.derived_type_identity)) + namespace = self._type_namespace(match.derived_type_identity) + expected = f"PyDict_GetItemString(PyModule_GetDict({namespace}), {class_name})" return f"{expected} != NULL && (PyObject *)Py_TYPE({value}) == {expected}" if match.kind is OverloadMatchKind.NUMPY_ARRAY: numpy_type = PrimitiveScalarTypeRegistry.type_for(match.semantic_type_name).numpy_type_macro @@ -15169,14 +15296,18 @@ def _overload_default_case(self, overload: OverloadPlan) -> CCase: ), ) - def _derived_private_method_entries(self, namespace: NamespacePlan) -> tuple[CMethodDefEntry, ...]: + def _derived_private_method_entries( + self, + module: ModulePlan, + namespace: NamespacePlan, + ) -> tuple[CMethodDefEntry, ...]: """Expose private field callables used by generated Python properties.""" names = ( *self._direct_field_method_names(namespace), - *self._module_member_method_names(namespace), + *self._module_member_method_names(module, namespace), *self._allocatable_holder_method_names(namespace), *self._pointer_holder_method_names(namespace), - *self._module_proxy_guard_method_names(namespace), + *self._module_proxy_guard_method_names(module, namespace), ) return tuple(CMethodDefEntry(name, name, "METH_VARARGS", "") for name in names) @@ -15190,11 +15321,15 @@ def _direct_field_method_names(self, namespace: NamespacePlan) -> tuple[str, ... for action in self._field_method_actions(field) ) - def _module_member_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: + def _module_member_method_names( + self, + module: ModulePlan, + namespace: NamespacePlan, + ) -> tuple[str, ...]: """Return the binding-local module member method names derived from the supplied completed binding records; this helper preserves completed policy.""" return tuple( self._module_member_method_name(variable, member, action) - for variable in namespace.variables + for variable in self._support_variables(module, namespace) if variable.derived is not None and variable.derived.access is ModuleObjectAccessMechanism.MEMBER_PROXY for member in variable.derived.member_paths for action in self._field_method_actions(member.field) @@ -15238,16 +15373,20 @@ def _pointer_holder_method_names(self, namespace: NamespacePlan) -> tuple[str, . guards = tuple(self._pointer_holder_presence_method_name(derived.backend_symbol) for derived in holders) return (*fields, *guards) - def _module_proxy_guard_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: + def _module_proxy_guard_method_names( + self, + module: ModulePlan, + namespace: NamespacePlan, + ) -> tuple[str, ...]: """Return the binding-local module proxy guard method names derived from the supplied completed binding records; this helper preserves completed policy.""" presence = tuple( self._module_derived_presence_method_name(variable) - for variable in namespace.variables + for variable in self._support_variables(module, namespace) if self._nullable_derived_module_proxy(variable) ) native_ops = tuple( self._derived_origin_capsule_method_name(variable) - for variable in namespace.variables + for variable in self._support_variables(module, namespace) if variable.derived is not None ) return (*presence, *native_ops) @@ -15289,21 +15428,61 @@ def _module_init( CodeExpression(f"PyModule_Create(&{module_name}_{self._namespace_symbol(root_namespace)}_module)"), ), CExpressionStatement(CodeExpression("if (mod == NULL) return NULL")), - *self._namespace_configuration_nodes( - plan, - root_namespace, - "mod", - ), + *self._module_initializer_nodes(plan), + *self._module_native_array_owner_nodes(plan, "mod"), *(node for namespace in child_namespaces for node in self._child_namespace_nodes(plan, namespace)), + # Every namespace exists before any is set up, and they are set + # up in plan order, which puts a namespace defining a base class + # before one whose classes extend it. + *( + node + for namespace in plan.namespaces + for node in self._namespace_configuration_nodes( + plan, + namespace, + self._namespace_object_name(namespace), + ) + ), *( node for namespace in child_namespaces for node in self._child_namespace_import_registration_nodes(plan, namespace) ), + # Aliases bind after every namespace is populated, so the + # callable a re-export names already exists. + *( + node + for namespace in (root_namespace, *child_namespaces) + for node in self._namespace_alias_nodes(plan, namespace) + ), CReturn(CodeExpression("mod")), ), ) + def _namespace_alias_nodes( + self, + plan: ModulePlan, + namespace: NamespacePlan, + ) -> tuple[CExpressionStatement, ...]: + """Bind each re-exported name to the callable its owner already exposes. + + A re-export publishes an existing declaration, so the name is bound to + that one object rather than to a second wrapper for the same procedure. + """ + target = self._namespace_object_name(namespace) + nodes: list[CExpressionStatement] = [] + for alias in namespace.aliases: + source = self._namespace_object_name(self._namespace(plan, alias.source_namespace)) + nodes.append( + CExpressionStatement( + CodeExpression( + f'if (prik_bind_namespace_alias({target}, "{alias.python_name}", ' + f'{source}, "{alias.source_name}") < 0) {{ Py_DECREF(mod); return NULL; }}' + ) + ) + ) + return tuple(nodes) + def _ordered_child_namespaces(self, plan: ModulePlan) -> tuple[NamespacePlan, ...]: """Return parents before descendants regardless of editable tuple order.""" return tuple( @@ -15318,7 +15497,7 @@ def _child_namespace_nodes( module: ModulePlan, namespace: NamespacePlan, ) -> tuple[CDeclaration | CExpressionStatement, ...]: - """Create, attach, and configure one child Python module.""" + """Create one child Python module and attach it to its parent.""" object_name = self._namespace_object_name(namespace) parent = self._namespace_object_name(self._namespace(module, namespace.python_path[:-1])) definition = f"{module.binding.owner_path}_{self._namespace_symbol(namespace)}_module" @@ -15332,11 +15511,6 @@ def _child_namespace_nodes( f"{{ Py_DECREF({object_name}); Py_DECREF(mod); return NULL; }}" ) ), - *self._namespace_configuration_nodes( - module, - namespace, - object_name, - ), ) def _child_namespace_import_registration_nodes( @@ -15376,24 +15550,25 @@ def _namespace_configuration_nodes( else () ) return ( + *self._namespace_owner_nodes(module, namespace, object_name), *property_nodes, *self._namespace_python_initializer_nodes( + module, namespace, object_name, ), - *self._module_native_array_owner_nodes(namespace, object_name), - *self._derived_module_owner_nodes(namespace, object_name), - *self._module_initializer_nodes(namespace), - *self._module_constant_nodes(namespace, object_name), + *self._module_constant_nodes(module, namespace, object_name), ) def _namespace_python_initializer_nodes( self, + module: ModulePlan, namespace: NamespacePlan, module_object: str, ) -> tuple[CDeclaration | CExpressionStatement | CIf, ...]: """Install exact overload dispatch plus generated opaque wrapper types.""" - has_proxy = any(variable.derived is not None for variable in namespace.variables) + variables = self._support_variables(module, namespace) + has_proxy = any(variable.derived is not None for variable in variables) if not namespace.derived_types and not has_proxy: return () allocatable_holders = self._namespace_binding_holder_types( @@ -15408,16 +15583,30 @@ def _namespace_python_initializer_nodes( allocatable_holder_identities=frozenset(derived.type_identity for derived in allocatable_holders), pointer_holder_identities=frozenset(derived.type_identity for derived in pointer_holders), nullable_module_proxy_owner_paths=frozenset( - variable.owner_path for variable in namespace.variables if self._nullable_derived_module_proxy(variable) + variable.owner_path for variable in variables if self._nullable_derived_module_proxy(variable) ), + type_homes=self._type_homes, ) - source = PythonSurfaceEmitter(context).emit(namespace) + emitter = PythonSurfaceEmitter(context) + source = emitter.emit(namespace, variables) literal = self._c_string_literal(source) result_name = f"{self._namespace_symbol(namespace)}_python_setup" dictionary = f"{self._namespace_symbol(namespace)}_python_dict" return ( CDeclaration(dictionary, "PyObject *", CodeExpression(f"PyModule_GetDict({module_object})")), CIf(CodeExpression(f"{dictionary} == NULL"), body=(CReturn(CodeExpression("NULL")),)), + # A class extending a type another namespace defines reaches its base + # through that namespace, which planning set up before this one. + *( + CIf( + CodeExpression( + f'PyDict_SetItemString({dictionary}, "{CBindingNames.namespace_reference(path)}", ' + f"{self._namespace_owner_name(path)}) < 0" + ), + body=(CReturn(CodeExpression("NULL")),), + ) + for path in emitter.referenced_namespaces(namespace) + ), CDeclaration( result_name, "PyObject *", @@ -15435,12 +15624,12 @@ def _c_string_literal(value: str) -> str: def _module_native_array_owner_nodes( self, - namespace: NamespacePlan, - _module_object: str, + plan: ModulePlan, + module_object: str, ) -> tuple[CExpressionStatement, ...]: """Retain the root extension package for every borrowed native array.""" nodes = [] - for variable in namespace.variables: + for variable in plan.variables: if variable.binding.getter_action not in { ModuleGetterAction.BORROWED_ARRAY_VIEW, ModuleGetterAction.NATIVE_ARRAY_HANDLE, @@ -15449,32 +15638,33 @@ def _module_native_array_owner_nodes( owner = self._module_native_array_owner_name(variable) nodes.extend( ( - CExpressionStatement(CodeExpression("Py_INCREF(mod)")), - CExpressionStatement(CodeExpression(f"{owner} = mod")), + CExpressionStatement(CodeExpression(f"Py_INCREF({module_object})")), + CExpressionStatement(CodeExpression(f"{owner} = {module_object}")), ) ) return tuple(nodes) - def _derived_module_owner_nodes( + def _namespace_owner_nodes( self, + module: ModulePlan, namespace: NamespacePlan, module_object: str, - ) -> tuple[CExpressionStatement, ...]: - """Retain one module reference for each live borrowed derived object.""" - nodes = [] - for variable in namespace.variables: - if variable.derived is None: - continue - owner = self._derived_module_owner_name(variable) - nodes.extend( - ( + ) -> tuple[CIf, ...]: + """Retain this namespace's module object when another reaches into it.""" + if namespace.python_path not in self._owner_namespaces(module): + return () + owner = self._namespace_owner_name(namespace.python_path) + return ( + CIf( + CodeExpression(f"{owner} == NULL"), + body=( CExpressionStatement(CodeExpression(f"Py_INCREF({module_object})")), CExpressionStatement(CodeExpression(f"{owner} = {module_object}")), - ) - ) - return tuple(nodes) + ), + ), + ) - def _module_initializer_nodes(self, namespace: NamespacePlan) -> tuple[CExpressionStatement, ...]: + def _module_initializer_nodes(self, plan: ModulePlan) -> tuple[CExpressionStatement, ...]: """Return import-time native assignments selected by completed policy.""" return tuple( CExpressionStatement( @@ -15483,28 +15673,31 @@ def _module_initializer_nodes(self, namespace: NamespacePlan) -> tuple[CExpressi f"{self._module_literal(variable, variable.binding.initializer)})" ) ) - for variable in namespace.variables + for variable in plan.variables if variable.binding.initializer is not None ) def _module_constant_nodes( self, + plan: ModulePlan, namespace: NamespacePlan, module_object: str, ) -> tuple[CDeclaration | CExpressionStatement, ...]: """Materialize scalar constants in the ordinary module dictionary.""" nodes = [] index = 0 - for variable in namespace.variables: + namespace_symbol = self._namespace_symbol(namespace) + for variable, publication in self._variable_publications(namespace): if variable.binding.getter_action not in { ModuleGetterAction.CONSTANT_VALUE, ModuleGetterAction.NATIVE_CONSTANT_VALUE, ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE, }: continue - for python_name in variable.binding.python_names: - value_name = f"constant_{variable.symbol_name}_value_{index}" - object_name = f"constant_{variable.symbol_name}_object_{index}" + local_stem = f"{namespace_symbol}_{variable.symbol_name}" + for python_name in publication.python_names: + value_name = f"constant_{local_stem}_value_{index}" + object_name = f"constant_{local_stem}_object_{index}" nodes.extend( ( *self._module_constant_declarations(variable, value_name, object_name), @@ -15712,8 +15905,25 @@ def _functions(self, plan: ModulePlan) -> tuple[FunctionPlan, ...]: return tuple(function for namespace in plan.namespaces for function in namespace.functions) def _variables(self, plan: ModulePlan) -> tuple[ModuleVariablePlan, ...]: - """Return variables from the supplied completed binding records; this helper preserves the selected binding behavior.""" - return tuple(variable for namespace in plan.namespaces for variable in namespace.variables) + """Return the canonical module-variable registry in planner order.""" + return plan.variables + + def _variable_publications( + self, + namespace: NamespacePlan, + ) -> tuple[tuple[ModuleVariablePlan, ModuleVariablePublicationPlan], ...]: + """Pair namespace publications with their canonical variable plans.""" + return tuple((publication.variable, publication) for publication in namespace.variable_publications) + + def _support_variables( + self, + plan: ModulePlan, + namespace: NamespacePlan, + ) -> tuple[ModuleVariablePlan, ...]: + """Read the planned namespace for private module-variable helpers.""" + return tuple( + variable for variable in plan.variables if variable.binding.support_namespace == namespace.python_path + ) def _namespace(self, plan: ModulePlan, python_path: tuple[str, ...]) -> NamespacePlan: """Return the binding-local namespace derived from the supplied completed binding records; this helper preserves completed policy.""" @@ -15724,7 +15934,12 @@ def _namespace(self, plan: ModulePlan, python_path: tuple[str, ...]) -> Namespac def _namespace_symbol(self, plan: NamespacePlan) -> str: """Return the binding-local namespace symbol derived from the supplied completed binding records; this helper preserves completed policy.""" - return "_".join(plan.python_path).casefold() if plan.python_path else "root" + return self._path_symbol(plan.python_path) + + @staticmethod + def _path_symbol(python_path: tuple[str, ...]) -> str: + """Return the C symbol fragment naming one namespace path.""" + return CBindingNames.namespace_symbol(python_path) def _namespace_object_name(self, plan: NamespacePlan) -> str: """Return the binding-local namespace object name derived from the supplied completed binding records; this helper preserves completed policy.""" diff --git a/prik/codegen/c/naming.py b/prik/codegen/c/naming.py index da249ae76..0c1171aa2 100644 --- a/prik/codegen/c/naming.py +++ b/prik/codegen/c/naming.py @@ -4,7 +4,6 @@ from prik.naming.native_symbols import NativeSymbolNames from prik.planning.models import ( - ClassSurfacePlan, DerivedFieldPlan, DerivedMemberPathPlan, DerivedTypePlan, @@ -103,21 +102,37 @@ def module_derived_presence_method(variable: ModuleVariablePlan) -> str: return f"_prik_module_{variable.symbol_name.casefold()}_require_present" @staticmethod - def class_create_method(surface: ClassSurfacePlan) -> str: + def namespace_symbol(python_path: tuple[str, ...]) -> str: + """Return the symbol fragment naming one namespace path.""" + return "_".join(python_path).casefold() if python_path else "root" + + @classmethod + def namespace_reference(cls, python_path: tuple[str, ...]) -> str: + """Return the name one namespace's Python source reaches another by.""" + return f"_prik_namespace_{cls.namespace_symbol(python_path)}" + + @staticmethod + def type_ops(backend_symbol: str) -> str: + """Return the Python operation-map name for a type's direct storage.""" + return f"_prik_ops_{backend_symbol.casefold()}" + + @staticmethod + def class_create_method(backend_symbol: str) -> str: """Return the private C constructor callable installed in the namespace.""" - return f"_prik_create_{surface.type_identity[1].casefold()}" + return f"_prik_create_{backend_symbol.casefold()}" @staticmethod - def class_wrap_helper( - surface: ClassSurfacePlan | None, - *, - fallback: str | None = None, - ) -> str: - """Return the Python helper attaching existing native storage.""" - name = surface.python_names[0] if surface is not None else fallback - if name is None: - raise ValueError("Class wrapper helper requires a Python type name") - return f"_prik_wrap_{name}" + def class_wrap_helper(backend_symbol: str) -> str: + """Return the Python helper attaching existing native storage. + + The helper is internal, and the generated code reaching for it knows + the native type it is wrapping rather than the name Python publishes + that type under, so it is keyed on the type's backend symbol the way + every other per-type helper is. That symbol is unique across the + extension, where the native name alone is not: two modules may each + declare a type spelled alike. + """ + return f"_prik_wrap_{backend_symbol.casefold()}" @staticmethod def overload_dispatch_symbol(overload: OverloadPlan) -> str: diff --git a/prik/codegen/c/python_surface.py b/prik/codegen/c/python_surface.py index 1eb281dcf..b09a4a480 100644 --- a/prik/codegen/c/python_surface.py +++ b/prik/codegen/c/python_surface.py @@ -13,6 +13,7 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from prik.codegen.c.naming import CBindingNames @@ -40,11 +41,17 @@ @dataclass(frozen=True) class PythonSurfaceContext: - """Store namespace facts already selected by planning and C orchestration.""" + """Store namespace facts already selected by planning and C orchestration. + + ``type_homes`` maps each type identity to the namespace defining it and + its plan there, so a class extending a type another namespace defines + reaches that base where it lives. + """ allocatable_holder_identities: frozenset[tuple[str, str]] pointer_holder_identities: frozenset[tuple[str, str]] nullable_module_proxy_owner_paths: frozenset[str] + type_homes: Mapping[tuple[str, str], tuple[tuple[str, ...], DerivedTypePlan]] class PythonSurfaceEmitter(ClassVisitor): @@ -53,29 +60,30 @@ class PythonSurfaceEmitter(ClassVisitor): def __init__(self, context: PythonSurfaceContext) -> None: self._context = context - def emit(self, namespace: NamespacePlan) -> str: + def emit( + self, + namespace: NamespacePlan, + variables: tuple[ModuleVariablePlan, ...], + ) -> str: """Return overloads, opaque classes, and typed member operation maps.""" - return self.visit(namespace) + return self.visit(namespace, variables) - def _visit_NamespacePlan(self, namespace: NamespacePlan) -> str: + def _visit_NamespacePlan( + self, + namespace: NamespacePlan, + variables: tuple[ModuleVariablePlan, ...], + ) -> str: """Render one planned namespace as executable Python source.""" surfaces = self._class_surfaces(namespace) - class_names = self._class_names(namespace) - ops_names = self._direct_ops_names(namespace) sections = [ "_prik_unset = object()", *( - self._derived_type_python_source( - derived, - surfaces.get(derived.type_identity), - class_names, - ops_names, - ) + self._derived_type_python_source(namespace, derived, surfaces.get(derived.type_identity)) for derived in namespace.derived_types ), ] sections.extend(self._holder_ops_python_sources(namespace)) - sections.extend(self._module_proxy_ops_python_sources(namespace)) + sections.extend(self._module_proxy_ops_python_sources(variables)) return "\n\n".join(section for section in sections if section) @staticmethod @@ -83,14 +91,37 @@ def _class_surfaces(namespace: NamespacePlan) -> dict[tuple[str, str], ClassSurf """Index planned class surfaces by completed type identity.""" return {surface.type_identity: surface for surface in namespace.classes} - @staticmethod - def _class_names(namespace: NamespacePlan) -> dict[tuple[str, str], str]: - """Index visible class names needed for inheritance rendering.""" - return {surface.type_identity: surface.python_names[0] for surface in namespace.classes if surface.python_names} + def referenced_namespaces(self, namespace: NamespacePlan) -> tuple[tuple[str, ...], ...]: + """Return each other namespace this one's source reaches a type in. + + The source names each such namespace as ``CBindingNames.namespace_reference``, + which has to be bound before it runs. + """ + paths = { + self._context.type_homes[base][0] for surface in namespace.classes for base in surface.base_identities[:1] + } + return tuple(sorted(paths - {namespace.python_path})) + + def _type_reference(self, namespace: NamespacePlan, type_identity: tuple[str, str], attribute: str) -> str: + """Return how this namespace's source names one attribute a type defines. + + A type this namespace defines is named directly; one another namespace + defines is reached through that namespace. + """ + path = self._context.type_homes[type_identity][0] + if path == namespace.python_path: + return attribute + return f"{CBindingNames.namespace_reference(path)}.{attribute}" + + def _type_class_reference(self, namespace: NamespacePlan, type_identity: tuple[str, str]) -> str: + """Return how this namespace's source names a type's class.""" + derived = self._context.type_homes[type_identity][1] + return self._type_reference(namespace, type_identity, derived.definition_name) - def _direct_ops_names(self, namespace: NamespacePlan) -> dict[tuple[str, str], str]: - """Index operation dictionaries inherited by generated subclasses.""" - return {derived.type_identity: self._direct_type_ops_name(derived) for derived in namespace.derived_types} + def _type_ops_reference(self, namespace: NamespacePlan, type_identity: tuple[str, str]) -> str: + """Return how this namespace's source names a type's operation map.""" + derived = self._context.type_homes[type_identity][1] + return self._type_reference(namespace, type_identity, self._direct_type_ops_name(derived)) def _holder_ops_python_sources(self, namespace: NamespacePlan) -> tuple[str, ...]: """Render allocatable and pointer holder operation maps by completed identity.""" @@ -107,26 +138,27 @@ def _holder_ops_python_sources(self, namespace: NamespacePlan) -> tuple[str, ... ), ) - def _module_proxy_ops_python_sources(self, namespace: NamespacePlan) -> tuple[str, ...]: + def _module_proxy_ops_python_sources( + self, + variables: tuple[ModuleVariablePlan, ...], + ) -> tuple[str, ...]: """Render persistent module-derived operation maps in declaration order.""" return tuple( - self._module_proxy_ops_python_source(variable) - for variable in namespace.variables - if variable.derived is not None + self._module_proxy_ops_python_source(variable) for variable in variables if variable.derived is not None ) def _derived_type_python_source( self, + namespace: NamespacePlan, derived: DerivedTypePlan, surface: ClassSurfacePlan | None, - class_names: dict[tuple[str, str], str], - ops_names: dict[tuple[str, str], str], ) -> str: """Return one opaque wrapper assembled from its completed class surface.""" - name = derived.python_names[0] + name = derived.definition_name ops_name = self._direct_type_ops_name(derived) - base = self._class_base_name(surface, class_names) - base_ops = self._class_base_ops_name(surface, ops_names) + base_identity = surface.base_identities[0] if surface is not None and surface.base_identities else None + base = None if base_identity is None else self._type_class_reference(namespace, base_identity) + base_ops = None if base_identity is None else self._type_ops_reference(namespace, base_identity) slots = self._class_slots(base) own_ops = self._direct_type_ops_literal(derived) combined_ops = self._combined_ops_literal(base_ops, own_ops) @@ -138,18 +170,29 @@ def _derived_type_python_source( ] lines.extend(self._class_constructor_python_lines(surface)) lines.extend(self._derived_class_member_python_lines(derived, surface)) - lines.extend(self._class_wrap_helper_python_lines(surface, name, ops_name)) + lines.extend(self._class_wrap_helper_python_lines(derived, ops_name)) + lines.extend(self._unbound_class_python_lines(namespace, derived)) return "\n".join(lines) - @staticmethod - def _class_base_ops_name( - surface: ClassSurfacePlan | None, - ops_names: dict[tuple[str, str], str], - ) -> str | None: - """Return the inherited operation-map name, when one is planned.""" - if surface is None or not surface.base_identities: - return None - return ops_names[surface.base_identities[0]] + def _unbound_class_python_lines(self, namespace: NamespacePlan, derived: DerivedTypePlan) -> tuple[str, ...]: + """Name a class bound under no public name, and bind it on its parent. + + Such a class is defined under a private name, so it takes the name its + contract calls it by. A nested one is then reached through its parent + and qualified by it, as a class written inside another is in Python. + """ + if derived.python_names: + return () + name = derived.definition_name + contract = derived.contract_name + if derived.nested_in is None: + return (f"{name}.__name__ = {name}.__qualname__ = {contract!r}",) + parent = self._type_class_reference(namespace, derived.nested_in) + return ( + f"{name}.__name__ = {contract!r}", + f"{name}.__qualname__ = {parent}.__qualname__ + {'.' + contract!r}", + f"{parent}.{contract} = {name}", + ) @staticmethod def _class_slots(base: str | None) -> str: @@ -192,16 +235,12 @@ def _class_overload_python_source_lines(self, overloads: tuple[OverloadPlan, ... """Flatten overload descriptors while preserving plan order.""" return tuple(line for overload in overloads for line in self._class_overload_python_lines(overload)) - def _class_wrap_helper_python_lines( - self, - surface: ClassSurfacePlan | None, - name: str, - ops_name: str, - ) -> tuple[str, ...]: + @staticmethod + def _class_wrap_helper_python_lines(derived: DerivedTypePlan, ops_name: str) -> tuple[str, ...]: """Render the sole helper that attaches existing opaque native storage.""" return ( - f"def {CBindingNames.class_wrap_helper(surface, fallback=name)}(capsule, owner=None, ops=None, origin='direct'):", - f" value = object.__new__({name})", + f"def {CBindingNames.class_wrap_helper(derived.backend_symbol)}(capsule, owner=None, ops=None, origin='direct'):", + f" value = object.__new__({derived.definition_name})", " value._prik_capsule = capsule", " value._prik_owner = owner", f" value._prik_ops = {ops_name} if ops is None else ops", @@ -244,7 +283,7 @@ def _default_constructor_python_lines(self, surface: ClassSurfacePlan) -> tuple[ signature = f", *, {parameters}" if parameters else "" lines = [ " def __new__(cls, *args, **kwargs):", - f" return {CBindingNames.class_create_method(surface)}()", + f" return {CBindingNames.class_create_method(surface.backend_symbol)}()", f" def __init__(self{signature}):", f" {surface.constructor.docstring!r}", ] @@ -267,7 +306,7 @@ def _bound_constructor_python_lines(self, surface: ClassSurfacePlan) -> tuple[st parameters = self._callable_public_arguments(target) lines = [ " def __new__(cls, *args, **kwargs):", - f" return {CBindingNames.class_create_method(surface)}()", + f" return {CBindingNames.class_create_method(surface.backend_symbol)}()", f" def __init__(self{self._python_parameter_suffix(parameters)}):", f" {surface.constructor.docstring!r}", " _prik_arguments = {'self': self}", @@ -290,7 +329,7 @@ def _overloaded_constructor_python_lines(self, surface: ClassSurfacePlan) -> tup if overload.candidate_passed_objects and overload.candidate_passed_objects[0]: return ( " def __new__(cls, *args, **kwargs):", - f" return {CBindingNames.class_create_method(surface)}()", + f" return {CBindingNames.class_create_method(surface.backend_symbol)}()", *self._class_overload_python_lines( overload, constructor=True, @@ -447,16 +486,6 @@ def _optional_keyword_collection_lines( ) return tuple(lines) - @staticmethod - def _class_base_name( - surface: ClassSurfacePlan | None, - class_names: dict[tuple[str, str], str], - ) -> str | None: - """Return the planned Python base-class name.""" - if surface is None or not surface.base_identities: - return None - return class_names[surface.base_identities[0]] - @staticmethod def _derived_property_python_lines(field: DerivedFieldPlan) -> tuple[str, ...]: """Build a property from completed getter and setter actions.""" @@ -531,14 +560,14 @@ def _pointer_holder_ops_python_source(self, derived: DerivedTypePlan) -> str: @staticmethod def _direct_type_ops_name(derived: DerivedTypePlan) -> str: """Return the Python operation-map name for direct storage.""" - return f"_prik_ops_{derived.type_name.casefold()}" + return CBindingNames.type_ops(derived.backend_symbol) def _module_proxy_ops_python_source(self, variable: ModuleVariablePlan) -> str: """Return one operation dictionary per reachable plain-module object path.""" if variable.derived is None: return "" if variable.derived.access is ModuleObjectAccessMechanism.DIRECT_ADDRESS: - direct = f"_prik_ops_{variable.derived.handoff.type_name.casefold()}" + direct = CBindingNames.type_ops(variable.derived.handoff.backend_symbol) native_ops = CBindingNames.derived_origin_capsule_method(variable) return f"{CBindingNames.module_member_ops(variable, ())} = dict({direct}, _native_ops={native_ops}())" grouped: dict[tuple[str, ...], list[DerivedMemberPathPlan]] = {} @@ -576,18 +605,19 @@ def _module_proxy_ops_literal( example_identity = ("state", "state_t") example_derived = DerivedTypePlan( owner_path="state.State", - type_name="State", type_identity=example_identity, backend_symbol="state_t", native_type_name="state_t", native_scope="state", python_names=("State",), + contract_name="State", fields=(), bind_c=False, ) example_surface = ClassSurfacePlan( owner_path="state.State", type_identity=example_identity, + backend_symbol="state_t", python_names=("State",), base_identities=(), constructor=ConstructorPlan( @@ -610,7 +640,9 @@ def _module_proxy_ops_literal( derived_types=(example_derived,), classes=(example_surface,), ) - example_context = PythonSurfaceContext(frozenset(), frozenset(), frozenset()) + example_context = PythonSurfaceContext( + frozenset(), frozenset(), frozenset(), {example_identity: ((), example_derived)} + ) print("Rendered Python facade:") - print(PythonSurfaceEmitter(example_context).emit(example_namespace)) + print(PythonSurfaceEmitter(example_context).emit(example_namespace, ())) diff --git a/prik/codegen/docstrings.py b/prik/codegen/docstrings.py index 50cd5e351..029af2b48 100644 --- a/prik/codegen/docstrings.py +++ b/prik/codegen/docstrings.py @@ -24,15 +24,18 @@ ArrayHandoffPlan, BindingStatusErrorPlan, CallbackHandoffPlan, + CallbackResultPlan, CallbackTransferPlan, ClassMethodPlan, ClassSurfacePlan, ConstructorPlan, DatatypeFamily, DerivedFieldPlan, + DerivedTypePlan, FunctionPlan, ModulePlan, ModuleVariablePlan, + ModuleVariablePublicationPlan, NamespacePlan, OverloadPlan, ResultPlan, @@ -67,7 +70,7 @@ _LOGICAL_ARRAY_NOTE = "Fortran logical elements; compare with .astype(bool) rather than to 1." -_UNKNOWN_EXTENTS = frozenset({"", ":", "::", "*", ".."}) +_UNKNOWN_EXTENTS = frozenset({"", ":", "*", ".."}) class WrapperDocstringBuilder: @@ -88,6 +91,18 @@ def render(self, plan: ModulePlan) -> ModulePlan: are explicit plan overrides and remain unchanged. The same plan is returned for generation-stage chaining. """ + # A docstring documents the Python API, so a wrapped type is named the + # way its namespace publishes it. Planning settled that name; indexing + # it here keeps every rendered signature reading the same one. + self._published_class_names = { + derived.type_identity: derived.contract_name + for namespace in plan.namespaces + for derived in namespace.derived_types + } + # Render every canonical variable first so namespace summaries only + # read completed documentation from that owner. + for variable in plan.variables: + self._render_module_variable(variable) for namespace in plan.namespaces: self._render_namespace(plan.owner_path, namespace) return plan @@ -99,22 +114,22 @@ def _render_namespace(self, module_name: str, namespace: NamespacePlan) -> None: for derived_type in namespace.derived_types: for field in derived_type.fields: self._render_field(field) - for variable in namespace.variables: - self._render_module_variable(variable) for overload in namespace.overloads: self._render_overload(overload) derived_types = {item.type_identity: item for item in namespace.derived_types} for surface in namespace.classes: - derived_type = derived_types.get(surface.type_identity) - self._render_class_surface(surface, () if derived_type is None else derived_type.fields) + self._render_class_surface(surface, derived_types[surface.type_identity]) if namespace.docstring is None: + variable_publications = tuple( + (publication.variable, publication) for publication in namespace.variable_publications + ) namespace.docstring = self.namespace( module_name, namespace.python_path, namespace.functions, - namespace.variables, + variable_publications, namespace.classes, namespace.overloads, ) @@ -149,9 +164,10 @@ def _render_overload(self, overload: OverloadPlan) -> None: def _render_class_surface( self, surface: ClassSurfacePlan, - fields: tuple[DerivedFieldPlan, ...], + derived_type: DerivedTypePlan, ) -> None: """Render one class's dependent records before its aggregate summary.""" + fields = derived_type.fields for field in fields: self._render_field(field) for method in surface.methods: @@ -167,10 +183,10 @@ def _render_class_surface( if constructor.overload is not None: self._render_overload(constructor.overload) if constructor.docstring is None: - constructor.docstring = self.constructor(surface.python_names[0], constructor, fields) + constructor.docstring = self.constructor(derived_type.contract_name, constructor, fields) if surface.docstring is None: surface.docstring = self.class_surface( - surface.python_names[0], + derived_type.contract_name, surface.type_identity[1], constructor, fields, @@ -184,7 +200,7 @@ def namespace( module_name: str, path: tuple[str, ...], functions: tuple[FunctionPlan, ...], - variables: tuple[ModuleVariablePlan, ...], + variables: tuple[tuple[ModuleVariablePlan, ModuleVariablePublicationPlan], ...], classes, overloads: tuple[OverloadPlan, ...], ) -> str: @@ -205,7 +221,11 @@ def namespace( self._append_section( lines, "Module Attributes", - tuple(line for variable in variables for line in self._module_variable_summary_lines(variable)), + tuple( + line + for variable, publication in variables + for line in self._module_variable_summary_lines(variable, publication.python_names) + ), ) self._append_section(lines, "Functions", callable_lines) self._append_section(lines, "Classes", tuple(name for surface in classes for name in surface.python_names)) @@ -467,7 +487,7 @@ def module_variable(self, variable: ModuleVariablePlan) -> str: documentation. Getter, setter, array-handle, and derived-object text comes directly from the completed variable plan. """ - name = variable.binding.python_names[0] + name = variable.owner_path.rsplit(".", 1)[-1] nullable = variable.binding.getter_action is ModuleGetterAction.NULLABLE_SNAPSHOT lines = [f"{name} : {self._type(variable, nullable=nullable, signature=False)}"] lines.extend(self._array_lines(variable.array)) @@ -748,6 +768,7 @@ def _argument_lines(self, argument: ArgumentTransferPlan) -> tuple[str, ...]: optional = argument.binding.optional_mode not in {OptionalMode.REQUIRED, OptionalMode.REQUIRED_DESCRIPTOR} nullable = optional or argument.binding.nullable lines = [f"{argument.binding.python_name} : {self._type(argument, nullable=nullable, signature=False)}"] + lines.extend(self._callback_signature_lines(argument)) lines.extend(self._array_lines(argument.array)) lines.extend(self._native_c_array_storage_lines(argument)) lines.extend(self._optional_lines(argument)) @@ -913,6 +934,19 @@ def _type(self, transfer, *, nullable: bool, signature: bool) -> str: return type_name return f"{type_name} | None" if signature else f"{type_name} or None" + def _published_class_name(self, transfer) -> str: + """Return the name a namespace publishes one wrapped type under. + + The type is found by its identity: two modules may each declare a type + spelled alike, and each is published under its own name. Rendering runs + before the plan is validated, so a reference to a type the plan does not + define keeps its semantic name here and is rejected by validation. + """ + derived = getattr(transfer, "derived", None) + handoff = getattr(derived, "handoff", derived) + identity = handoff.type_identity if handoff is not None else transfer.derived_type_identity + return self._published_class_names.get(identity, str(transfer.semantic_type_name)) + def _base_type(self, transfer) -> str: """Map one completed transfer family and storage facet to public type text. @@ -923,7 +957,7 @@ def _base_type(self, transfer) -> str: if getattr(transfer, "datatype_family", None) is DatatypeFamily.CALLBACK: return self._callback_type(transfer.callback) if getattr(transfer, "datatype_family", None) is DatatypeFamily.DERIVED: - return transfer.semantic_type_name + return self._published_class_name(transfer) scalar = _SCALAR_TYPES.get(transfer.semantic_type_name, transfer.semantic_type_name) array_element = _ARRAY_ELEMENT_TYPES.get(transfer.semantic_type_name, scalar) handle = getattr(transfer, "native_array_handle", None) @@ -967,19 +1001,74 @@ def _callback_type(self, callback: CallbackHandoffPlan | None) -> str: @staticmethod def _callback_transfer_type(transfer: CallbackTransferPlan) -> str: - """Render a callback prototype argument or result from completed ABI facts. + """Render one callback prototype dummy as the Python object it receives. - Derived transfers preserve their type identity. Arrays and reference - ABI transfers render as NumPy arrays; other transfers use the scalar - map. The helper is pure and does not inspect outer wrapper policy. + The spelling follows the completed Python projection rather than the + native ABI: a dummy projected as storage arrives as an array the + callable can write through, and one projected as a value does not. """ if transfer.derived_type_identity is not None: return transfer.semantic_type_name scalar = _SCALAR_TYPES.get(transfer.semantic_type_name, transfer.semantic_type_name) - if transfer.array is not None or transfer.abi.value == "reference": + if transfer.python_action in {PythonBarrierAction.ARRAY_STORAGE, PythonBarrierAction.SCALAR_STORAGE}: return f"ndarray[{scalar}]" return scalar + def _callback_signature_lines(self, argument: ArgumentTransferPlan) -> tuple[str, ...]: + """Document the exact callable one callback parameter expects. + + Every fact comes from the completed prototype the trampoline is + generated from, so the documented arity, direction and access cannot + drift from the callable the native caller actually invokes. + """ + callback = argument.callback + if callback is None: + return () + parameters = ", ".join(transfer.name for transfer in callback.arguments) + result = self._callback_result_type(callback.result) + return ( + f" Called as: {argument.binding.python_name}({parameters}) -> {result}", + *(f" {self._callback_parameter_text(transfer)}" for transfer in callback.arguments), + " Valid only during this call; do not retain the callable or its arguments.", + " An exception or an invalid return value terminates the process.", + ) + + @staticmethod + def _callback_parameter_text(transfer: CallbackTransferPlan) -> str: + """Render one prototype dummy with the shape and access it presents.""" + parts = [f"{transfer.name} : {WrapperDocstringBuilder._callback_transfer_type(transfer)}"] + parts.extend(WrapperDocstringBuilder._callback_array_facts(transfer.array)) + if transfer.intent is not None: + parts.append(f"intent({transfer.intent})") + text = ", ".join(parts) + if transfer.python_action is PythonBarrierAction.SCALAR_STORAGE: + text += f"; assign through it ({transfer.name}[...] = value)" + return text + + @staticmethod + def _callback_array_facts(array: ArrayHandoffPlan | None) -> tuple[str, ...]: + """Describe one callback array's rank and extents from its completed plan. + + The callable's ABI depends on both, and extents are spelled the way the + `.pyi` contract spells them so the two descriptions agree. + """ + if array is None or not array.rank: + return () + display = array.display_shape or array.shape + extents = ", ".join(str(extent) for extent in display) + return (f"rank {array.rank}",) + ((f"shape ({extents})",) if extents else ()) + + @staticmethod + def _callback_result_type(result: CallbackResultPlan) -> str: + """Render what the callable must return, or ``None`` for a subroutine.""" + transfer = result.transfer + if transfer is None: + return "None" + if transfer.derived_type_identity is not None: + return transfer.semantic_type_name + scalar = _SCALAR_TYPES.get(transfer.semantic_type_name, transfer.semantic_type_name) + return f"ndarray[{scalar}]" if transfer.array is not None else scalar + @staticmethod def _array_lines(array: ArrayHandoffPlan | None) -> tuple[str, ...]: """Render rank, resolved display shape, and layout notes for one array facet. @@ -993,7 +1082,8 @@ def _array_lines(array: ArrayHandoffPlan | None) -> tuple[str, ...]: lines = [WrapperDocstringBuilder._array_rank_line(array)] display_shape = array.display_shape or array.shape if display_shape and all(str(extent) not in _UNKNOWN_EXTENTS for extent in display_shape): - lines.append(f" Shape: ({', '.join(map(str, display_shape))})") + extents = (str(extent) for extent in display_shape) + lines.append(f" Shape: ({', '.join(extents)})") layout = WrapperDocstringBuilder._array_layout_label(array) if layout is not None: lines.append(f" Layout: {layout}") @@ -1068,7 +1158,11 @@ def _result_name(result: ResultPlan, arguments: tuple[ArgumentTransferPlan, ...] return result.projected_call_slot.python_name return "result" if result.result_position == 0 else f"result_{result.result_position}" - def _module_variable_summary_lines(self, variable: ModuleVariablePlan) -> tuple[str, ...]: + def _module_variable_summary_lines( + self, + variable: ModuleVariablePlan, + python_names: tuple[str, ...] | None = None, + ) -> tuple[str, ...]: """Expand one module-variable docstring for every exported Python alias. The first line supplies the rendered type while the remaining details @@ -1082,7 +1176,8 @@ def _module_variable_summary_lines(self, variable: ModuleVariablePlan) -> tuple[ _name, separator, type_name = first.partition(" : ") if not separator: return (first,) - return tuple(line for name in variable.binding.python_names for line in (f"{name} : {type_name}", *details)) + names = (variable.owner_path.rsplit(".", 1)[-1],) if python_names is None else python_names + return tuple(line for name in names for line in (f"{name} : {type_name}", *details)) def _keyword_field_signature( self, diff --git a/prik/codegen/fortran/bridge.py b/prik/codegen/fortran/bridge.py index b0ecc0514..e1035b9ba 100644 --- a/prik/codegen/fortran/bridge.py +++ b/prik/codegen/fortran/bridge.py @@ -14,7 +14,7 @@ import re from prik.naming.native_symbols import NativeSymbolNames -from prik.utilities.declaration_expressions import render_declaration_extent +from prik.utilities.declaration_expressions import RUNTIME_EXTENT_MARKERS, render_declaration_extent from prik.policy.ownership import ( AssignmentMode, CodegenAction, @@ -26,10 +26,9 @@ from prik.policy.models import ( ArgumentHandoffMode, ArrayEntrypointABI, - ArrayLogicalABI, - ArrayWritebackABI, BridgeDataAction, CallbackABIKind, + CallbackOptionalityAction, CallbackResultAction, CallbackTransferAction, ClassInvocationKind, @@ -54,6 +53,7 @@ NativeArrayOwnerStorage, NativeArrayResultAllocation, NativeDescriptorHandoffABI, + NativeEntrypointAction, NativeInvocationKind, EntrypointPassingConvention, EntrypointProjectionAction, @@ -189,6 +189,10 @@ def _plan_semantic_type_names(node: object, _seen: set[int] | None = None) -> fr return frozenset(names) +#: Entrypoint groups carrying extents the bridge evaluates and hands back. +_EXTENT_GROUPS = frozenset({"declaration_extent", "argument_extent"}) + + class FortranBridgeGenerator(ClassVisitor): """Build the Fortran half of a wrapper from validated bridge-plan views. @@ -271,6 +275,36 @@ def _adapter_slots(function: FunctionPlan) -> tuple[NativeEntrypointProjectedSlo def _visit_ModulePlan(self, plan: ModulePlan) -> FortranModule: """Build one complete bridge module from one validated module plan.""" + self._prepare_module_context(plan) + procedures = self._module_procedures(plan) + # Assemble imports, declarations, and procedures from plan projections. + return FortranModule( + name=f"bind_c_{plan.entrypoint.owner_path}_wrapper", + uses=( + FortranUse("iso_c_binding", self._iso_c_symbols(plan)), + *self._native_module_uses(plan), + ), + type_definitions=( + *self._derived_holder_definitions(plan), + *self._native_array_owner_definitions(plan), + ), + interfaces=( + *self._derived_call_interfaces(plan), + *self._prototype_interfaces(plan), + *self._external_interfaces(plan), + *self._module_descriptor_callback_interfaces(plan), + *self._derived_array_callback_interfaces(plan), + *self._native_array_owner_callback_interfaces(plan), + *self._module_array_capture_interfaces(plan), + *self._allocator_interfaces(plan), + ), + declarations=self._prototype_entity_declarations(plan), + procedures=self._apply_generated_support_procedure_entrypoints(procedures), + standalone_procedures=self._callback_standalone_adapter_procedures(plan), + ) + + def _prepare_module_context(self, plan: ModulePlan) -> None: + """Cache validated module-wide facts consumed by bridge emitters.""" self._generated_support_procedure_entrypoints = { (procedure.owner_path, procedure.role): procedure for procedure in plan.entrypoint.support_procedures } @@ -290,73 +324,59 @@ def _visit_ModulePlan(self, plan: ModulePlan) -> FortranModule: plan.bridge.allocatable_holder_field_type_owner_paths ) self._bridge_pointer_holder_field_owner_paths = frozenset(plan.bridge.pointer_holder_field_type_owner_paths) + + def _module_procedures(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: + """Return bridge procedures in their established emission order.""" # Scoped origins are module-wide facts needed by derived-call lowering. scoped_origin_type_identities = self._scoped_origin_type_identities(plan) - procedures = ( + return ( *( procedure for namespace in plan.namespaces for procedure in self.visit(namespace, scoped_origin_type_identities) ), + *(procedure for variable in plan.variables for procedure in self.visit(variable)), # Typed derived-field access remains separate from class orchestration. *self._derived_field_procedures(plan), # Native-aware opaque-owner destruction is Phase 8 substrate, not class orchestration. *self._class_constructor_procedures(plan), + *self._derived_lifecycle_procedures(plan), + *( + procedure + for variable in self._derived_origin_variables(plan) + for procedure in self._derived_origin_procedures(variable) + ), + ) + + def _derived_lifecycle_procedures(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: + """Return planned destruction and presence helpers for derived storage.""" + derived_types = self._derived_types(plan) + return ( *( self._derived_destroy_procedure(derived) - for derived in self._derived_types(plan) + for derived in derived_types if self._has_generated_support_procedure_entrypoint(derived.owner_path, "derived:destroy") ), *( self._allocatable_holder_destroy_procedure(derived) - for derived in self._derived_types(plan) + for derived in derived_types if self._has_generated_support_procedure_entrypoint(derived.owner_path, "holder:allocatable:destroy") ), *( self._allocatable_holder_presence_procedure(derived) - for derived in self._derived_types(plan) + for derived in derived_types if self._has_generated_support_procedure_entrypoint(derived.owner_path, "holder:allocatable:present") ), *( self._pointer_holder_destroy_procedure(derived) - for derived in self._derived_types(plan) + for derived in derived_types if self._has_generated_support_procedure_entrypoint(derived.owner_path, "holder:pointer:destroy") ), *( self._pointer_holder_presence_procedure(derived) - for derived in self._derived_types(plan) + for derived in derived_types if self._has_generated_support_procedure_entrypoint(derived.owner_path, "holder:pointer:present") ), - *( - procedure - for variable in self._derived_origin_variables(plan) - for procedure in self._derived_origin_procedures(variable) - ), - ) - # Assemble imports, declarations, and procedures from plan projections. - return FortranModule( - name=f"bind_c_{plan.entrypoint.owner_path}_wrapper", - uses=( - FortranUse("iso_c_binding", self._iso_c_symbols(plan)), - *self._native_module_uses(plan), - ), - type_definitions=( - *self._derived_holder_definitions(plan), - *self._native_array_owner_definitions(plan), - ), - interfaces=( - *self._derived_call_interfaces(plan), - *self._prototype_interfaces(plan), - *self._external_interfaces(plan), - *self._module_descriptor_callback_interfaces(plan), - *self._derived_array_callback_interfaces(plan), - *self._native_array_owner_callback_interfaces(plan), - *self._module_array_capture_interfaces(plan), - *self._allocator_interfaces(plan), - ), - declarations=self._prototype_entity_declarations(plan), - procedures=self._apply_generated_support_procedure_entrypoints(procedures), - standalone_procedures=self._callback_standalone_adapter_procedures(plan), ) def _generated_support_procedure_entrypoint( @@ -499,7 +519,8 @@ def _support_procedure_fortran_type(value: NativeEntrypointABIValuePlan) -> str def _callback_standalone_adapter_procedures(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: """Return separately linked callback adapters in stable site order.""" return tuple( - self._callback_standalone_adapter_procedure(callback, plan) for callback in self._callback_sites(plan) + self._callback_standalone_adapter_procedure(callback, plan) + for callback in self._callback_adapter_sites(plan) ) def _derived_holder_definitions(self, plan: ModulePlan) -> tuple[FortranTypeDefinition, ...]: @@ -597,7 +618,6 @@ def _visit_NamespacePlan( for function in plan.functions for procedure in self._default_native_array_argument_operations(function) ), - *(procedure for variable in plan.variables for procedure in self.visit(variable)), ) def _visit_FunctionPlan( @@ -626,8 +646,6 @@ def _visit_FunctionPlan( *self._derived_pointer_call_initializers(plan), *function_body, *self._logical_scalar_argument_finalizers(plan), - *self._logical_array_argument_finalizers(plan), - *self._array_writeback_finalizers(plan), *self._derived_pointer_call_finalizers(plan), *self._required_descriptor_finalizers(plan), *self._string_value_finalizers(plan), @@ -665,21 +683,25 @@ def _visit_FunctionPlan( *self._native_output_declarations(plan), *self._derived_result_allocation_declarations(plan), ), - body=( - *self._character_local_initializers(plan), - *self._native_array_owner_initializers(plan), - *self._descriptor_initializers(plan), - *self._required_descriptor_initializers(plan), - *self._logical_scalar_argument_initializers(plan), - *self._opaque_address_initializers(plan), - *self._array_initializers(plan), - *self._logical_array_argument_initializers(plan), - *self._raw_array_address_initializers(plan), - *self._string_value_initializers(plan), - *self._string_address_initializers(plan), - *self._declaration_extent_result_assignments(plan), - *self._direct_array_result_initializers(plan), - *derived_body, + body=self._extent_checked_body( + plan, + result_name, + result_type, + ( + *self._character_local_initializers(plan), + *self._native_array_owner_initializers(plan), + *self._descriptor_initializers(plan), + *self._required_descriptor_initializers(plan), + *self._logical_scalar_argument_initializers(plan), + *self._opaque_address_initializers(plan), + *self._array_initializers(plan), + *self._raw_array_address_initializers(plan), + *self._string_value_initializers(plan), + *self._string_address_initializers(plan), + *self._extent_assignments(plan, "declaration_extent"), + *self._direct_array_result_initializers(plan), + *derived_body, + ), ), is_subroutine=is_subroutine, internal_procedures=( @@ -697,6 +719,11 @@ def _entrypoint_parameter_declarations( """Lower one shared C-ABI parameter group into a bind(C) declaration.""" if parameter.source_kind == "argument": return self.visit(self._argument_by_owner(plan, parameter.owner_path)) + if parameter.source_kind in _EXTENT_GROUPS: + return tuple( + FortranParameter(extent.parameter_name, "integer(c_int64_t)", ("intent(out)",)) + for extent in parameter.extents + ) if parameter.source_kind == "projected_slot": return self._projected_slot_parameters(self._projected_slot_for_parameter(plan, parameter)) result = self._result_by_owner(plan, parameter.owner_path) @@ -707,8 +734,6 @@ def _entrypoint_parameter_declarations( *self._owned_direct_result_parameters(result), *self._scalar_descriptor_direct_result_parameters_for_result(result), ) - if parameter.source_kind == "declaration_extent": - return self._declaration_extent_result_parameters_for_result(result) raise ValueError(f"Unsupported entrypoint parameter group {parameter.source_kind!r}") @staticmethod @@ -753,45 +778,66 @@ def _projected_slot_parameters( raise ValueError(f"Unsupported projected Fortran parameter passing {slot.passing.value!r}") return (FortranParameter(slot.native_name.casefold(), type_name, attributes),) - def _declaration_extent_result_parameters_for_result( + def _extent_checked_body( self, - result: NativeEntrypointResultPlan, - ) -> tuple[FortranParameter, ...]: - """Expose bridge-evaluated extents for one entrypoint result group.""" - if result.array is None: - return () - return tuple( - FortranParameter( - self._declaration_extent_result_name(result, axis), - "integer(c_int64_t)", - ("intent(out)",), - ) - for axis, evaluation in enumerate(result.array.extent_evaluation) - if evaluation == "bridge" + plan: FunctionPlan, + result_name: str | None, + result_type: str | None, + body: tuple, + ) -> tuple: + """Run the whole procedure only when every checked actual has its declared extent. + + A dummy a specification function sizes is as long as that function + says, and only Fortran can evaluate it. The extents are read from the + parameters alone, before anything is prepared, so an actual of another + length leaves nothing converted, called, allocated, or produced; a + pointer result is null, and the binding reports the declared extent. + """ + extents_match = self._argument_extents_match(plan) + if extents_match is None: + return body + unproduced = ( + (FortranAssignment(result_name, CodeExpression("c_null_ptr")),) if result_type == "type(c_ptr)" else () + ) + return ( + *self._extent_assignments(plan, "argument_extent"), + FortranIf(CodeExpression(extents_match), body=body, else_body=unproduced), ) - def _declaration_extent_result_assignments(self, plan: FunctionPlan) -> tuple[FortranAssignment, ...]: - """Evaluate native-dependent result axes inside the Fortran bridge.""" + def _argument_extents_match(self, plan: FunctionPlan) -> str | None: + """Return when every declared argument extent equals the actual one, or ``None``.""" + role_names = self._array_shape_role_names(plan) + conditions = [] + for parameter in plan.entrypoint.parameters: + if parameter.source_kind != "argument_extent": + continue + argument = self._argument_by_owner(plan, parameter.owner_path) + for extent in parameter.extents: + matches = f"{extent.parameter_name} == {role_names[argument.array.extent_roles[extent.axis]]}" + if argument.entrypoint.optional_mode is not OptionalMode.REQUIRED: + # An omitted actual has no extent to disagree with. + matches = f"(.not. {self._presence_condition(argument)} .or. {matches})" + conditions.append(matches) + return " .and. ".join(conditions) or None + + def _extent_assignments(self, plan: FunctionPlan, source_kind: str) -> tuple[FortranAssignment, ...]: + """Evaluate each extent one kind of group hands back, from its owner's declared shape.""" assignments = [] - for result in plan.results: - if result.array is None or "bridge" not in result.array.extent_evaluation: + for parameter in plan.entrypoint.parameters: + if parameter.source_kind != source_kind: continue - shape = self._array_shape_from_roles(result.array, plan) + owner = ( + self._argument_by_owner(plan, parameter.owner_path) + if source_kind == "argument_extent" + else self._result_by_owner(plan, parameter.owner_path) + ) + shape = self._array_shape_from_roles(owner.array, plan) assignments.extend( - FortranAssignment( - self._declaration_extent_result_name(result, axis), - CodeExpression(f"int({shape[axis]}, c_int64_t)"), - ) - for axis, evaluation in enumerate(result.array.extent_evaluation) - if evaluation == "bridge" + FortranAssignment(extent.parameter_name, CodeExpression(f"int({shape[extent.axis]}, c_int64_t)")) + for extent in parameter.extents ) return tuple(assignments) - @staticmethod - def _declaration_extent_result_name(result: ResultPlan | NativeEntrypointResultPlan, axis: int) -> str: - """Return the shared entrypoint ABI name for one evaluated result axis.""" - return f"prik_decl_extent_{result.result_position}_{axis}" - # Immediate callback adapters. def _callback_standalone_adapter_procedure( self, @@ -852,13 +898,15 @@ def _callback_native_parameter(self, transfer: CallbackTransferPlan) -> FortranP attributes.append("value") if transfer.intent is not None: attributes.append(f"intent({transfer.intent})") + if transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER: + attributes.append("optional") if transfer.abi is not CallbackABIKind.VALUE and transfer.adapter_action in { CallbackTransferAction.BORROW_READ_ONLY, CallbackTransferAction.BORROW_WRITABLE, }: attributes.append("target") if transfer.rank: - attributes.append(f"dimension({self._callback_shape(transfer)})") + attributes.append(f"dimension({self._callback_dummy_shape(transfer)})") return FortranParameter( self._callback_parameter_base_name(transfer), self._callback_native_type(transfer), @@ -874,6 +922,14 @@ def _callback_standalone_adapter_uses( native_imports = self._callback_native_imports(callback) adapter_imports = ( *(("c_loc",) if any(transfer.abi is not CallbackABIKind.VALUE for transfer in callback.arguments) else ()), + *( + ("c_null_ptr",) + if any( + transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER + for transfer in callback.arguments + ) + else () + ), *( ("c_f_pointer",) if callback.result.action @@ -932,50 +988,90 @@ def _callback_transfer_declarations( }: attributes = ["target"] if transfer.rank: - attributes.append(f"dimension({self._callback_shape(transfer)})") + if transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER: + attributes.extend(("allocatable", self._array_dimension_attribute(transfer.rank))) + else: + attributes.append(f"dimension({self._callback_storage_shape(transfer)})") declarations.append( FortranDeclaration( self._callback_storage_name(transfer), - self._callback_native_type(transfer), + self._callback_abi_storage_type(transfer), tuple(attributes), ) ) + if transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER: + if transfer.abi is CallbackABIKind.DATA_AND_SHAPE: + declarations.extend( + FortranDeclaration(f"{base}_extent_{axis}", "integer(c_int64_t)") for axis in range(transfer.rank) + ) + elif transfer.abi is CallbackABIKind.DATA_AND_LENGTH: + declarations.append(FortranDeclaration(f"{base}_length", "integer(c_int64_t)")) return tuple(declarations) def _callback_transfer_preparation( self, transfer: CallbackTransferPlan, - ) -> tuple[FortranAssignment, ...]: + ) -> tuple[FortranAssignment | FortranAllocate | FortranIf, ...]: """Copy into call-local storage when selected, then expose its address.""" if transfer.abi is CallbackABIKind.VALUE: return () base = self._callback_parameter_base_name(transfer) storage = self._callback_address_source(transfer) - statements = [] + statements: list[FortranAssignment | FortranAllocate] = [] + if ( + transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER + and transfer.rank + and transfer.adapter_action + in { + CallbackTransferAction.COPY_IN, + CallbackTransferAction.COPY_OUT, + CallbackTransferAction.COPY_IN_OUT, + } + ): + statements.append( + FortranAllocate( + storage, + tuple(CodeExpression(f"size({base}, dim={axis + 1})") for axis in range(transfer.rank)), + ) + ) if transfer.adapter_action in { CallbackTransferAction.COPY_IN, CallbackTransferAction.COPY_IN_OUT, }: statements.append(FortranAssignment(storage, CodeExpression(base))) statements.append(FortranAssignment(f"{base}_data", CodeExpression(f"c_loc({storage})"))) - return tuple(statements) + statements.extend(self._callback_optional_metadata_assignments(transfer, storage)) + if transfer.optionality is CallbackOptionalityAction.REQUIRED: + return tuple(statements) + initializers = [FortranAssignment(f"{base}_data", CodeExpression("c_null_ptr"))] + initializers.extend(self._callback_optional_metadata_initializers(transfer)) + return ( + *initializers, + FortranIf(CodeExpression(f"present({base})"), body=tuple(statements)), + ) def _callback_transfer_writeback( self, transfer: CallbackTransferPlan, - ) -> tuple[FortranAssignment, ...]: + ) -> tuple[FortranAssignment | FortranIf, ...]: """Copy writable callback storage back to the native dummy exactly once.""" if transfer.adapter_action not in { CallbackTransferAction.COPY_OUT, CallbackTransferAction.COPY_IN_OUT, }: return () - return ( - FortranAssignment( - self._callback_parameter_base_name(transfer), - CodeExpression(self._callback_storage_name(transfer)), - ), + assignment = FortranAssignment( + self._callback_parameter_base_name(transfer), + CodeExpression(self._callback_storage_name(transfer)), ) + if transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER: + return ( + FortranIf( + CodeExpression(f"present({self._callback_parameter_base_name(transfer)})"), + body=(assignment,), + ), + ) + return (assignment,) def _callback_invocation( self, @@ -1009,12 +1105,19 @@ def _callback_c_argument_expressions( if transfer.abi is CallbackABIKind.VALUE: return (CodeExpression(base),) if transfer.abi is CallbackABIKind.DATA_AND_SHAPE: + if transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER: + return ( + CodeExpression(f"{base}_data"), + *(CodeExpression(f"{base}_extent_{axis}") for axis in range(transfer.rank)), + ) storage = self._callback_address_source(transfer) return ( CodeExpression(f"{base}_data"), *(CodeExpression(f"size({storage}, dim={axis + 1}, kind=c_int64_t)") for axis in range(transfer.rank)), ) if transfer.abi is CallbackABIKind.DATA_AND_LENGTH: + if transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER: + return (CodeExpression(f"{base}_data"), CodeExpression(f"{base}_length")) storage = self._callback_address_source(transfer) return ( CodeExpression(f"{base}_data"), @@ -1022,6 +1125,47 @@ def _callback_c_argument_expressions( ) return (CodeExpression(f"{base}_data"),) + def _callback_optional_metadata_initializers( + self, + transfer: CallbackTransferPlan, + ) -> tuple[FortranAssignment, ...]: + """Initialize metadata paired with an absent callback dummy.""" + base = self._callback_parameter_base_name(transfer) + if transfer.abi is CallbackABIKind.DATA_AND_SHAPE: + return tuple( + FortranAssignment(f"{base}_extent_{axis}", CodeExpression("0_c_int64_t")) + for axis in range(transfer.rank) + ) + if transfer.abi is CallbackABIKind.DATA_AND_LENGTH: + return (FortranAssignment(f"{base}_length", CodeExpression("0_c_int64_t")),) + return () + + def _callback_optional_metadata_assignments( + self, + transfer: CallbackTransferPlan, + storage: str, + ) -> tuple[FortranAssignment, ...]: + """Measure metadata only after an optional callback dummy is present.""" + if transfer.optionality is CallbackOptionalityAction.REQUIRED: + return () + base = self._callback_parameter_base_name(transfer) + if transfer.abi is CallbackABIKind.DATA_AND_SHAPE: + return tuple( + FortranAssignment( + f"{base}_extent_{axis}", + CodeExpression(f"size({storage}, dim={axis + 1}, kind=c_int64_t)"), + ) + for axis in range(transfer.rank) + ) + if transfer.abi is CallbackABIKind.DATA_AND_LENGTH: + return ( + FortranAssignment( + f"{base}_length", + CodeExpression(f"int(len({storage}), kind=c_int64_t)"), + ), + ) + return () + def _callback_result_declarations( self, callback: CallbackHandoffPlan, @@ -1062,7 +1206,7 @@ def _callback_result_reconstruction( ( CodeExpression("callback_result_data"), CodeExpression("callback_result_view"), - CodeExpression(f"[{self._callback_shape(transfer)}]"), + CodeExpression(f"[{self._callback_result_shape(transfer)}]"), ), ), FortranAssignment("callback_result", CodeExpression("callback_result_view")), @@ -1086,11 +1230,13 @@ def _callback_native_result_type(self, transfer: CallbackTransferPlan | None) -> raise ValueError("Callback function result is missing its transfer plan") result_type = self._callback_native_type(transfer) if transfer.rank: - result_type += f", dimension({self._callback_shape(transfer)})" + result_type += f", dimension({self._callback_result_shape(transfer)})" return result_type def _callback_native_type(self, transfer: CallbackTransferPlan) -> str: """Return one typed native callback value without selecting behavior.""" + if transfer.native_fortran_type is not None: + return transfer.native_fortran_type if transfer.abi is CallbackABIKind.DERIVED_ADDRESS: if transfer.derived_backend_symbol is None: raise ValueError(f"Callback derived transfer {transfer.owner_path!r} has no backend symbol") @@ -1099,19 +1245,65 @@ def _callback_native_type(self, transfer: CallbackTransferPlan) -> str: return f"character(kind=c_char, len={transfer.character_length})" return PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name).fortran_spelling + def _callback_abi_storage_type(self, transfer: CallbackTransferPlan) -> str: + """Return the interoperable storage type selected for the C trampoline.""" + if transfer.native_fortran_type is not None: + return PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name).fortran_spelling + return self._callback_native_type(transfer) + @staticmethod def _callback_parameter_base_name(transfer: CallbackTransferPlan) -> str: """Return the base Fortran dummy name reserved for one callback transfer.""" return re.sub(r"\W", "_", transfer.name).casefold() - def _callback_shape(self, transfer: CallbackTransferPlan) -> str: - """Render completed callback extents in native Fortran syntax.""" + def _callback_array_shape(self, transfer: CallbackTransferPlan) -> tuple[str, ...]: + """Return one callback transfer's completed public extent expressions.""" if transfer.array is None or transfer.array.rank is None: raise ValueError(f"Callback array transfer {transfer.owner_path!r} has no shape plan") + return tuple(transfer.array.shape) + + def _callback_dummy_shape(self, transfer: CallbackTransferPlan) -> str: + """Render one callback dummy's extents in native Fortran syntax. + + A runtime extent lowers to an assumed-shape axis, so the dummy takes + the native caller's descriptor. The contiguous call-local copy + declared beside it carries the concrete bounds instead. + """ return ", ".join( - render_declaration_extent(expression, {}, target="fortran") for expression in transfer.array.shape + ":" if expression in RUNTIME_EXTENT_MARKERS else render_declaration_extent(expression, {}, target="fortran") + for expression in self._callback_array_shape(transfer) ) + def _callback_storage_shape(self, transfer: CallbackTransferPlan) -> str: + """Render the contiguous call-local copy's extents for one callback dummy. + + An assumed-shape dummy cannot back ``c_loc``, so the copy is an + automatic array measured from the dummy it was declared beside. + """ + base = self._callback_parameter_base_name(transfer) + return ", ".join( + f"size({base}, {axis + 1})" + if expression in RUNTIME_EXTENT_MARKERS + else render_declaration_extent(expression, {}, target="fortran") + for axis, expression in enumerate(self._callback_array_shape(transfer)) + ) + + def _callback_result_shape(self, transfer: CallbackTransferPlan) -> str: + """Render a callback array result's extents, which must be explicit. + + A function result has no caller descriptor to measure, so a runtime + extent here means policy admitted a form the native result cannot + spell. + """ + shape = self._callback_array_shape(transfer) + runtime = [expression for expression in shape if expression in RUNTIME_EXTENT_MARKERS] + if runtime: + raise ValueError( + f"Callback array result {transfer.owner_path!r} has runtime extents {runtime} " + "and cannot be spelled as a native function result" + ) + return ", ".join(render_declaration_extent(expression, {}, target="fortran") for expression in shape) + def _callback_address_source(self, transfer: CallbackTransferPlan) -> str: """Return the C-address expression that backs one callback transfer.""" if transfer.adapter_action in { @@ -3643,7 +3835,15 @@ def _visit_ArgumentTransferPlan(self, plan: ArgumentTransferPlan) -> tuple[Fortr def _lower_argument(self, plan: ArgumentTransferPlan) -> tuple[FortranParameter, ...]: """Dispatch one completed bridge optional mode explicitly.""" if plan.callback is not None: - return () + if not plan.entrypoint.pass_callback_parameter: + return () + return ( + FortranParameter( + plan.entrypoint.parameter_name, + "type(c_funptr)", + ("value",), + ), + ) mode = plan.entrypoint.optional_mode if plan.object_kind is ObjectKind.DERIVED_TYPE: return self._lower_derived_argument(plan, mode) @@ -4574,7 +4774,7 @@ def _native_argument_expression(self, plan: ArgumentTransferPlan) -> str: return f"{name}_call_pointer" return name if plan.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: - return self._array_native_argument_expression(plan) + return self._array_boundary_argument_expression(plan) if plan.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: handle = plan.native_array_handle if handle is not None and handle.handoff.abi is NativeDescriptorHandoffABI.FORTRAN_OWNER: @@ -4589,6 +4789,8 @@ def _native_argument_expression(self, plan: ArgumentTransferPlan) -> str: def _presence_condition(self, plan: ArgumentTransferPlan) -> str: """Return the local C-pointer association condition for one nullable entrypoint argument.""" name = plan.entrypoint.parameter_name + if plan.callback is not None: + return f"c_associated({name})" if plan.derived_call is not None: return f"bound_{name}_access /= 0_c_int" handle = plan.native_array_handle @@ -4612,6 +4814,8 @@ def _present_preparation( return () action = plan.bridge.data_action match action: + case BridgeDataAction.DIRECT_TRANSFER: + return () case BridgeDataAction.ASSOCIATE_VIEW: return self._prepare_present_associated_view(plan) case BridgeDataAction.COPY_REPRESENTATION: @@ -4682,16 +4886,6 @@ def _prepare_present_representation_copy( plan: ArgumentTransferPlan, ) -> tuple[FortranCall | FortranAssignment | FortranIf, ...]: """Copy only when completed policy requires a different native representation.""" - if plan.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: - nodes: list[FortranCall | FortranAssignment | FortranIf] = list(self._array_pointer_initializer_nodes(plan)) - if plan.array_copy_in: - nodes.append( - FortranAssignment( - self._logical_array_native_name(plan), - CodeExpression(self._array_boundary_argument_expression(plan)), - ) - ) - return tuple(nodes) if plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: name = plan.entrypoint.parameter_name return (FortranAssignment(f"{name}_native", CodeExpression(name)),) @@ -4737,6 +4931,8 @@ def _optional_argument_declarations( argument: ArgumentTransferPlan, ) -> tuple[FortranDeclaration, ...]: """Return optional helper declarations for one completed handoff.""" + if argument.callback is not None: + return () handle = argument.native_array_handle if handle is not None and handle.handoff.abi is NativeDescriptorHandoffABI.FORTRAN_OWNER: if handle.owner_type_name is None: @@ -4955,194 +5151,8 @@ def _array_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, . ("pointer", self._array_dimension_attribute(array.rank)), ) ) - if argument.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: - if not argument.array_native_type: - raise ValueError(f"Logical array {argument.owner_path!r} has no native type spelling") - declarations.append( - FortranDeclaration( - self._logical_array_native_name(argument), - argument.array_native_type, - (self._logical_array_dimension_attribute(argument),), - ) - ) - if argument.array_writeback_abi is ArrayWritebackABI.LOGICAL_LOW_BIT_INT8: - declarations.append( - FortranDeclaration( - self._logical_array_byte_pointer_name(argument), - self._logical_array_integer_type(argument.semantic_type_name), - ("pointer", "dimension(:)"), - ) - ) return tuple(declarations) - def _logical_array_argument_initializers( - self, - plan: FunctionPlan, - ) -> tuple[FortranAssignment, ...]: - """Copy required one-byte Boolean inputs into exact-kind native arrays.""" - return tuple( - FortranAssignment( - self._logical_array_native_name(argument), - CodeExpression(self._array_boundary_argument_expression(argument)), - ) - for argument in plan.arguments - if argument.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY - and argument.array_copy_in - and argument.entrypoint.optional_mode is OptionalMode.REQUIRED - ) - - def _logical_array_argument_finalizers( - self, - plan: FunctionPlan, - ) -> tuple[FortranAssignment | FortranIf, ...]: - """Copy exact-kind logical outputs into canonical one-byte storage. - - ``merge`` converts truth values while assigning them to the original - ``logical(c_bool)`` view, so copy-out and canonicalization share one - array traversal. Optional buffers are written only when present. - """ - finalizers = [] - for argument in plan.arguments: - if argument.array_logical_abi is not ArrayLogicalABI.NATIVE_KIND_COPY or not argument.array_copy_out: - continue - target = self._array_boundary_argument_expression(argument) - native = self._logical_array_native_name(argument) - assignment = FortranAssignment( - target, - CodeExpression(f"merge(.true._c_bool, .false._c_bool, {native})"), - ) - if argument.entrypoint.optional_mode is OptionalMode.REQUIRED: - finalizers.append(assignment) - else: - finalizers.append(FortranIf(CodeExpression(self._presence_condition(argument)), body=(assignment,))) - return tuple(finalizers) - - @staticmethod - def _logical_array_native_name(argument: ArgumentTransferPlan) -> str: - """Return the bridge-local exact-kind array name for ``argument``.""" - return f"{argument.entrypoint.parameter_name}_native" - - def _logical_array_dimension_attribute(self, argument: ArgumentTransferPlan) -> str: - """Render automatic-array extents in the completed native orientation.""" - array = argument.array - if array is None or array.rank is None: - raise ValueError(f"Logical array {argument.owner_path!r} requires a concrete rank") - name = argument.entrypoint.parameter_name - extents = [f"{name}_extent_{axis}" for axis in range(array.rank)] - if array.native_order == "ORDER_C": - extents.reverse() - return f"dimension({', '.join(extents)})" - - def _array_writeback_finalizers( - self, - plan: FunctionPlan, - ) -> tuple[FortranAssignment | FortranCall | FortranIf | FortranSelectCase, ...]: - """Normalize mutable array bytes through their completed writeback ABI.""" - finalizers = [] - for argument in plan.arguments: - match argument.array_writeback_abi: - case ArrayWritebackABI.NOT_APPLICABLE | ArrayWritebackABI.NATIVE_ARRAY: - continue - case ArrayWritebackABI.LOGICAL_LOW_BIT_INT8: - nodes = self._logical_array_writeback_nodes(argument) - case _: - raise ValueError( - f"Unsupported array writeback ABI for {argument.owner_path!r}: {argument.array_writeback_abi!r}" - ) - if argument.entrypoint.optional_mode is OptionalMode.REQUIRED: - finalizers.extend(nodes) - else: - finalizers.append(FortranIf(CodeExpression(self._presence_condition(argument)), body=nodes)) - return tuple(finalizers) - - def _logical_array_writeback_nodes( - self, - argument: ArgumentTransferPlan, - ) -> tuple[FortranAssignment | FortranCall | FortranSelectCase, ...]: - """Associate raw Boolean storage and retain only each element's truth bit.""" - array = argument.array - if array is None: - raise ValueError(f"Logical array {argument.owner_path!r} has no handoff") - if array.rank is not None: - return self._logical_array_writeback_for_rank(argument, array.rank) - name = argument.entrypoint.parameter_name - cases = tuple( - FortranCase( - rank, - self._logical_array_writeback_for_rank(argument, rank), - ) - for rank in range(1, 16) - ) - return (FortranSelectCase(CodeExpression(f"{name}_rank"), (*cases, FortranCase(None, ()))),) - - def _logical_array_writeback_for_rank( - self, - argument: ArgumentTransferPlan, - rank: int, - ) -> tuple[FortranCall | FortranAssignment, ...]: - """Return logical-array writeback nodes for one rank using the completed ABI conversion action.""" - name = argument.entrypoint.parameter_name - byte_pointer = self._logical_array_byte_pointer_name(argument) - byte_count = " * ".join(f"{name}_extent_{axis}" for axis in range(rank)) - return ( - FortranCall( - "c_f_pointer", - ( - CodeExpression(f"bound_{name}"), - CodeExpression(byte_pointer), - CodeExpression(f"[{byte_count}]"), - ), - ), - FortranAssignment( - byte_pointer, - CodeExpression(self._logical_array_canonical_expression(argument.semantic_type_name, byte_pointer)), - ), - ) - - @staticmethod - def _logical_array_integer_type(semantic_type_name: str) -> str: - """Return the integer type covering one Boolean element's own width. - - The mask reinterprets the caller's buffer, so it has to step by the - element width rather than by bytes: a `logical(4)` array is four-byte - integers, not four times as many one-byte ones. - """ - return { - "Bool": "integer(c_int8_t)", - "Bool8": "integer(c_int8_t)", - "Bool16": "integer(c_int16_t)", - "Bool32": "integer(c_int32_t)", - "Bool64": "integer(c_int64_t)", - }[semantic_type_name] - - @staticmethod - def _logical_array_kind_suffix(semantic_type_name: str) -> str: - """Return the integer kind suffix matching one Boolean element's width.""" - return { - "Bool": "c_int8_t", - "Bool8": "c_int8_t", - "Bool16": "c_int16_t", - "Bool32": "c_int32_t", - "Bool64": "c_int64_t", - }[semantic_type_name] - - def _logical_array_canonical_expression(self, semantic_type_name: str, target: str) -> str: - """Return the expression reducing Boolean storage to zero and one. - - The rule is C's: any non-zero value is true, which is what converting to - ``_Bool`` produces and what NumPy, Python and C all read back. It is not - a low-bit test -- that would call ``2`` false, disagreeing with every one - of them -- and it maps both representations compilers emit, ``1`` and - ``-1``, onto the single value the interoperable type is defined to hold. - """ - kind = self._logical_array_kind_suffix(semantic_type_name) - return f"merge(1_{kind}, 0_{kind}, {target} /= 0_{kind})" - - @staticmethod - def _logical_array_byte_pointer_name(argument: ArgumentTransferPlan) -> str: - """Return the bridge-local byte-pointer name for one logical-array rank conversion.""" - return f"{argument.entrypoint.parameter_name}_logical_bytes" - def _array_initializers(self, plan: FunctionPlan) -> tuple[FortranCall | FortranIf, ...]: """Associate each completed ordinary array data/extent handoff.""" initializers = [] @@ -5295,12 +5305,6 @@ def _array_pointer_name(self, argument: ArgumentTransferPlan) -> str: name = argument.entrypoint.parameter_name return f"{name}_base" if argument.array is not None and argument.array.contiguous is False else name - def _array_native_argument_expression(self, argument: ArgumentTransferPlan) -> str: - """Pass exact-kind logical storage or the planned boundary array view.""" - if argument.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: - return self._logical_array_native_name(argument) - return self._array_boundary_argument_expression(argument) - def _array_boundary_argument_expression(self, argument: ArgumentTransferPlan) -> str: """Return the array the native call receives: a buffer or a section of one.""" array = argument.array @@ -6177,9 +6181,8 @@ def _direct_array_result_initializers( ): return () shape = list(self._array_result_shape(plan, result)) - for axis, evaluation in enumerate(result.array.extent_evaluation): - if evaluation == "bridge": - shape[axis] = self._declaration_extent_result_name(result, axis) + for axis, evaluated in plan.entrypoint.extent_names(result.owner_path).items(): + shape[axis] = evaluated return (FortranAllocate(f"result_value({', '.join(shape)})"),) def _array_result_depends_on_descriptor( @@ -8471,7 +8474,7 @@ def _prototype_interfaces( def _prototype_plans(self, plan: ModulePlan) -> tuple[ProcedurePrototypePlan, ...]: """Deduplicate callback and direct-call uses by generated interface symbol.""" candidates = ( - *(callback.prototype for callback in self._callback_sites(plan)), + *(callback.prototype for callback in self._callback_adapter_sites(plan)), *( declaration.prototype for function in self._functions(plan) @@ -8521,6 +8524,8 @@ def _procedure_prototype_parameter( attributes.append("value") if argument.intent is not None: attributes.append(f"intent({argument.intent})") + if argument.optional: + attributes.append("optional") if argument.rank: attributes.append(f"dimension({self._procedure_prototype_shape(argument.array, argument.owner_path)})") return FortranParameter( @@ -8536,7 +8541,7 @@ def _procedure_prototype_result_type( """Declare one exact function result from the shared prototype plan.""" result_type = self._procedure_prototype_type(result) if result.rank: - result_type += f", dimension({self._procedure_prototype_shape(result.array, result.owner_path)})" + result_type += f", dimension({self._procedure_prototype_result_shape(result.array, result.owner_path)})" return result_type def _procedure_prototype_type( @@ -8544,6 +8549,8 @@ def _procedure_prototype_type( value: ProcedurePrototypeArgumentPlan | ProcedurePrototypeResultPlan, ) -> str: """Return the native type shared by callback and direct prototype uses.""" + if isinstance(value, ProcedurePrototypeArgumentPlan) and value.native_fortran_type is not None: + return value.native_fortran_type if value.derived_backend_symbol is not None: return f"type({self._derived_native_alias(value.derived_backend_symbol)})" if value.semantic_type_name == "String": @@ -8554,9 +8561,29 @@ def _procedure_prototype_type( @staticmethod def _procedure_prototype_shape(array: ArrayHandoffPlan | None, owner_path: str) -> str: - """Render an exact prototype array shape without backend role substitution.""" + """Render a prototype dummy's shape without backend role substitution. + + A runtime extent lowers to an assumed-shape axis so the interface body + matches the native declaration it describes. + """ if array is None or array.rank is None: raise ValueError(f"Prototype value {owner_path!r} has no concrete shape") + return ", ".join( + ":" if expression in RUNTIME_EXTENT_MARKERS else render_declaration_extent(expression, {}, target="fortran") + for expression in array.shape + ) + + @staticmethod + def _procedure_prototype_result_shape(array: ArrayHandoffPlan | None, owner_path: str) -> str: + """Render a prototype function result's shape, which must be explicit.""" + if array is None or array.rank is None: + raise ValueError(f"Prototype value {owner_path!r} has no concrete shape") + runtime = [expression for expression in array.shape if expression in RUNTIME_EXTENT_MARKERS] + if runtime: + raise ValueError( + f"Prototype result {owner_path!r} has runtime extents {runtime} " + "and cannot be spelled as a native function result" + ) return ", ".join(render_declaration_extent(expression, {}, target="fortran") for expression in array.shape) def _procedure_prototype_imports( @@ -8637,11 +8664,12 @@ def _native_external_declarations(self, plan: FunctionPlan) -> tuple[FortranDecl ), ) - def _callback_sites(self, plan: ModulePlan) -> tuple[CallbackHandoffPlan, ...]: - """Return callback sites in stable native-call order.""" + def _callback_adapter_sites(self, plan: ModulePlan) -> tuple[CallbackHandoffPlan, ...]: + """Return callback sites whose completed route needs a Fortran adapter.""" return tuple( argument.callback for function in self._functions(plan) + if function.entrypoint.action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER for argument in sorted(function.arguments, key=lambda item: item.native_position) if argument.callback is not None ) @@ -9417,8 +9445,8 @@ def _functions(self, plan: ModulePlan) -> tuple[FunctionPlan, ...]: return tuple(function for namespace in plan.namespaces for function in namespace.functions) def _variables(self, plan: ModulePlan) -> tuple[ModuleVariablePlan, ...]: - """Flatten namespaces into module-variable plans while preserving module and namespace order.""" - return tuple(variable for namespace in plan.namespaces for variable in namespace.variables) + """Return the canonical module-variable registry in planner order.""" + return plan.variables def _iso_symbol(self, semantic_type_name: str) -> str: """Return the iso_c_binding symbol required by one semantic primitive type.""" @@ -9495,6 +9523,12 @@ def _uses_c_function_pointer_symbols(self, plan: ModulePlan) -> bool: for operation in plan.entrypoint.support_procedures for parameter in operation.signature.parameters ) + callback_parameters = any( + argument.entrypoint.pass_callback_parameter + for function in self._functions(plan) + if function.entrypoint.action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + for argument in function.arguments + ) module_descriptors = any(self._uses_module_descriptor_backend(variable) for variable in self._variables(plan)) field_descriptors = any( field.access @@ -9505,7 +9539,7 @@ def _uses_c_function_pointer_symbols(self, plan: ModulePlan) -> bool: for derived in self._derived_types(plan) for field in derived.fields ) - return support_callbacks or module_descriptors or field_descriptors + return support_callbacks or callback_parameters or module_descriptors or field_descriptors def _uses_derived_interop_symbols(self, plan: ModulePlan) -> bool: """Return whether completed derived call or module-variable actions require derived interop support.""" diff --git a/prik/contracts/__init__.py b/prik/contracts/__init__.py index c640da998..3741cac97 100644 --- a/prik/contracts/__init__.py +++ b/prik/contracts/__init__.py @@ -239,7 +239,6 @@ def apply(target): ORDER_F = _ContractExpression() Pointer = _DescriptorContract("pointer") Polymorphic = _ContractExpression() -Strided = _ContractExpression() Arg = _expression ArrayCategory = _expression @@ -411,7 +410,6 @@ def destroy(target): "Returns", "SizeT", "SourceName", - "Strided", "String", "Transfer", "UInt", diff --git a/prik/naming/__init__.py b/prik/naming/__init__.py index 6f31439ef..ccf9176c0 100644 --- a/prik/naming/__init__.py +++ b/prik/naming/__init__.py @@ -17,6 +17,7 @@ PublicNameRecord, generated_symbol_rules, normalize_public_name, + preserves_source_case, ) __all__ = ( @@ -32,6 +33,7 @@ "bridge_source_name", "generated_symbol_rules", "normalize_public_name", + "preserves_source_case", "stub_identifier", "wrapper_header_name", ) diff --git a/prik/naming/policy.py b/prik/naming/policy.py index 5657214b5..d1d7f1452 100644 --- a/prik/naming/policy.py +++ b/prik/naming/policy.py @@ -10,6 +10,8 @@ from prik.utilities.strings import create_incremented_string _NON_IDENTIFIER = re.compile(r"[^0-9A-Za-z_]") +# Only a case-insensitive source language has no spelling of its own to keep. +_CASE_INSENSITIVE_SOURCE_LANGUAGES = frozenset({"fortran"}) _SYMBOL_CONTEXTS = frozenset({"module", "function", "class", "variable", "wrapper"}) _PARENT_CONTEXTS = frozenset({"module", "function", "class", "loop", "program"}) @@ -53,23 +55,61 @@ def has_clash(self, name: object, symbols: set[object]) -> bool: return folded in self.keywords or any(folded == str(symbol).casefold() for symbol in symbols) -def normalize_public_name(raw_name: object) -> NormalizedPublicName: - """Convert a source spelling into a valid, lower-case Python identifier.""" +def preserves_source_case(source_language: object) -> bool: + """Return whether a source language's own casing is part of a name. + + A case-insensitive language writes the same declaration many ways, so no + spelling is the declaration's own and one canonical lower-case form is the + Python name. Every other language distinguishes two spellings as two + declarations, so the source casing is the name and folding it would both + lose the identity and invent collisions the source does not have. + """ + return str(source_language or "").casefold() not in _CASE_INSENSITIVE_SOURCE_LANGUAGES + + +def _capitalized_words(name: str) -> str: + """Return one identifier with each underscore-separated word capitalized.""" + return "_".join(word[:1].upper() + word[1:] for word in name.split("_")) + + +def normalize_public_name( + raw_name: object, + *, + preserve_case: bool = False, + category: str = "function", +) -> NormalizedPublicName: + """Convert a source spelling into a valid Python identifier. + + PRIK chooses a spelling only where the source has none. ``preserve_case`` + says the source casing is part of the name (see ``preserves_source_case``), + and then the spelling is adjusted only where Python cannot accept it. + Otherwise the choice is PRIK's: a wrapped type reaches Python as a class, + so a ``class`` capitalizes each word -- ``point_t`` becomes ``Point_T`` -- + and every other declaration is lower-cased. + + ``needs_fix`` reports only the adjustments Python forced, never the chosen + style, so ``--strict-wrapper-names`` rejects a name Python cannot spell + rather than one PRIK merely cased. + """ source = str(raw_name).strip() - folded = source.casefold() - normalized = _NON_IDENTIFIER.sub("_", folded) or "_" + candidate = source if preserve_case else source.casefold() + normalized = _NON_IDENTIFIER.sub("_", candidate) or "_" if not (normalized[0].isalpha() or normalized[0] == "_"): normalized = f"_{normalized}" if keyword.iskeyword(normalized): normalized = f"{normalized}_" - return NormalizedPublicName(normalized, needs_fix=normalized != folded) + needs_fix = normalized != candidate + if not preserve_case and category == "class": + normalized = _capitalized_words(normalized) + return NormalizedPublicName(normalized, needs_fix=needs_fix) class NamingPolicy: """Allocate Python exports and language-safe generated symbols.""" - def __init__(self, *, strict_public_names: bool = False): + def __init__(self, *, strict_public_names: bool = False, preserve_case: bool = False): self.strict_public_names = strict_public_names + self.preserve_case = preserve_case self._public_names: dict[tuple[str, ...], dict[str, PublicNameRecord]] = {} def reserve_public_name( @@ -79,9 +119,18 @@ def reserve_public_name( *, category: str, owner: object | None = None, + preserve_case: bool | None = None, ) -> str: - """Reserve one public Python name within its namespace.""" - normalized = normalize_public_name(raw_name) + """Reserve one public Python name within its namespace. + + ``preserve_case`` overrides the policy's rule for a name written as it + is declared wherever it appears, such as a prototype's. + """ + normalized = normalize_public_name( + raw_name, + preserve_case=self.preserve_case if preserve_case is None else preserve_case, + category=category, + ) raw_text = str(raw_name) namespace_key = tuple(str(part) for part in namespace) namespace_text = ".".join(namespace_key) or "" @@ -111,6 +160,28 @@ def reserve_public_name( reserved[name] = PublicNameRecord(raw_text, category, str(owner or raw_name)) return name + def hold_completed_public_name( + self, + namespace: tuple[str, ...], + name: object, + *, + category: str, + owner: object | None = None, + ) -> str: + """Hold an already-completed spelling without interpreting it again.""" + completed = str(name) + namespace_key = tuple(str(part) for part in namespace) + reserved = self._public_names.setdefault(namespace_key, {}) + existing = reserved.get(completed) + if existing is not None: + namespace_text = ".".join(namespace_key) or "" + raise ValueError( + f"Completed public {category} name {completed!r} in {namespace_text} collides with " + f"{existing.category} {existing.raw_name!r} ({existing.owner})" + ) + reserved[completed] = PublicNameRecord(completed, category, str(owner or name)) + return completed + def has_generated_symbol_clash(self, name: object, symbols: set[object], *, language: str) -> bool: """Return whether ``name`` is unusable in the selected language.""" return generated_symbol_rules(language).has_clash(name, symbols) diff --git a/prik/parsers/fortran/cli.py b/prik/parsers/fortran/cli.py index 2b8c261f4..cff43eabc 100644 --- a/prik/parsers/fortran/cli.py +++ b/prik/parsers/fortran/cli.py @@ -89,24 +89,38 @@ def _parse_paths(paths: list[str]) -> dict[str, dict]: def _semantic_report(paths: list[str]) -> dict[str, dict]: - """Generate semantic IR and pyi text per parsed file.""" - from prik.semantics.fortran2ir import fortran_module_to_semantic_module - from prik.printers import emit_module + """Generate semantic IR and the generated .pyi per parsed file. - parsed = _parse_paths(paths) - semantic_out: dict[str, dict] = {} - parser = FortranParser() + Every module read is converted together and its contract emitted the way + ``prik generate --pyi`` emits it, so an import names what the module it + reads from declares and the report shows the contract a build would use. + """ + from prik.parsers.fortran.models import FortranProject + from prik.pipeline.pyi import emit_module_stubs + from prik.semantics.fortran2ir import fortran_project_to_semantic_modules - for fname in parsed: - code = Path(fname).read_text(encoding="utf-8") - fobj = parser.parse_file(code, filename=fname) - modules = [fortran_module_to_semantic_module(m) for m in fobj.modules] - semantic_out[fname] = { - "semantic_modules": [asdict(m) for m in modules], - "pyi": "\n\n".join(emit_module(m) for m in modules).strip(), + parser = FortranParser() + files = { + fname: parser.parse_file(Path(fname).read_text(encoding="utf-8"), filename=fname) + for fname in _parse_paths(paths) + } + converted = { + module.name.casefold(): module + for module in fortran_project_to_semantic_modules(FortranProject(files=list(files.values()))) + } + modules_by_file = { + fname: [converted[module.name.casefold()] for module in parsed.modules if module.name.casefold() in converted] + for fname, parsed in files.items() + } + modules = [module for file_modules in modules_by_file.values() for module in file_modules] + stubs = emit_module_stubs(modules, normalize_public_names=True) if modules else {} + return { + fname: { + "semantic_modules": [asdict(module) for module in file_modules], + "pyi": "\n\n".join(stubs[module.name] for module in file_modules).strip(), } - - return semantic_out + for fname, file_modules in modules_by_file.items() + } def _format_pyi_report(semantic_report: dict[str, dict]) -> str: diff --git a/prik/parsers/fortran/models.py b/prik/parsers/fortran/models.py index 3279e1141..4608846b1 100644 --- a/prik/parsers/fortran/models.py +++ b/prik/parsers/fortran/models.py @@ -17,6 +17,7 @@ from dataclasses import dataclass, field from typing import Any +from prik.parsers.fortran.type_resolver import extract_character_selector from prik.utilities.declaration_expressions import split_dimension_bounds, split_top_level_expression @@ -254,6 +255,35 @@ def character_length_syntax(self) -> bool: """Whether the stored character ``kind`` text is actually a length.""" return bool(getattr(self, "_character_length_syntax", False)) + @property + def character_kind_expression(self) -> str | None: + """The kind a character declaration states, separated from its length.""" + return getattr(self, "_character_kind_expression", None) + + @property + def character_length_expression(self) -> str | None: + """The length a character declaration states, separated from its kind. + + A character selector carries two expressions, either of which may hold + commas of its own, so the parser records them apart rather than leaving + a later stage to split one joined spelling. + """ + return getattr(self, "_character_length_expression", None) + + def record_character_selector(self, type_spec: str) -> None: + """Record what one character declaration's parenthesized selector states. + + This is the only supported way to give a character model its selector + facts, so every producer -- the parser, the type-mapping report, a test + -- reaches them through one reading of the source text. A model built + without it states no length and no kind, which is what a bare + ``character`` declaration means. + """ + selector = extract_character_selector(type_spec) + self._character_length_expression = selector.length + self._character_kind_expression = selector.kind + self._character_length_syntax = selector.length_syntax + @property def polymorphic(self) -> bool: """Whether this variable was declared with Fortran ``class(...)``.""" @@ -338,7 +368,7 @@ class FortranProcedureSignature: result: FortranArgument | None = None attributes: list[str] = field(default_factory=list) bind_name: str | None = None - uses: dict[str, list[FortranUseMapping]] = field(default_factory=dict) + uses: list[FortranUseStatement] = field(default_factory=list) in_interface: bool = False variables: dict[str, FortranVariable] = field(default_factory=dict) common_variables: list[str] = field(default_factory=list) @@ -367,6 +397,17 @@ class FortranInterface: specific_procedures: list[str] = field(default_factory=list) abstract: bool = False + declaring_scope_kind: str = "module" + """Kind of scope declaring this block: file, module, submodule or procedure.""" + + declaring_scope_path: list[str] = field(default_factory=list) + """Names of the scopes enclosing this block, outermost first. + + A generic belongs to the scope declaring it, so a block written inside a + procedure names a generic of that procedure and not of its module. Keeping + the owner lets later stages read only the generics a module itself declares. + """ + @dataclass class FortranEnumerator: @@ -385,11 +426,28 @@ class FortranEnum: visibility: str = "public" +@dataclass(frozen=True) +class FortranUseStatement: + """One ``use`` statement exactly as the source writes it. + + ``only`` records whether the statement narrowed to an ``only`` list, which + is independent of what it listed: ``use m`` lists nothing and narrows + nothing, ``use m, only :`` lists nothing and narrows to nothing. Statements + are kept apart and immutable because the language reads several for one + module together, and a scope that inherits another's imports must not be + able to add to them. + """ + + module: str + only: bool = False + mappings: tuple[FortranUseMapping, ...] = () + + @dataclass class FortranModule: name: str filename: str | None = None - uses: dict[str, list[FortranUseMapping]] = field(default_factory=dict) + uses: list[FortranUseStatement] = field(default_factory=list) variables: list[FortranVariable] = field(default_factory=list) procedures: list[FortranProcedureSignature] = field(default_factory=list) derived_types: list[FortranDerivedType] = field(default_factory=list) @@ -407,7 +465,7 @@ class FortranSubmodule: parent: str ancestor: str | None = None filename: str | None = None - uses: dict[str, list[FortranUseMapping]] = field(default_factory=dict) + uses: list[FortranUseStatement] = field(default_factory=list) variables: list[FortranVariable] = field(default_factory=list) procedures: list[FortranProcedureSignature] = field(default_factory=list) derived_types: list[FortranDerivedType] = field(default_factory=list) @@ -420,7 +478,7 @@ class FortranSubmodule: class FortranProgram: name: str | None = None filename: str | None = None - uses: dict[str, list[FortranUseMapping]] = field(default_factory=dict) + uses: list[FortranUseStatement] = field(default_factory=list) variables: list[FortranVariable] = field(default_factory=list) procedures: list[FortranProcedureSignature] = field(default_factory=list) enums: list[FortranEnum] = field(default_factory=list) diff --git a/prik/parsers/fortran/parser.py b/prik/parsers/fortran/parser.py index 9362a07be..26e0c51e1 100644 --- a/prik/parsers/fortran/parser.py +++ b/prik/parsers/fortran/parser.py @@ -10,7 +10,7 @@ from __future__ import annotations import re -from collections.abc import Mapping, Sequence +from collections.abc import Iterable, Mapping, Sequence from copy import deepcopy from dataclasses import dataclass, field as dataclass_field, replace from pathlib import Path @@ -18,7 +18,9 @@ from typing import ClassVar, Literal from prik.utilities.declaration_expressions import ( + declaration_expression_identifiers, evaluate_integer_expression, + fortran_character_value, split_declaration_assignment, split_dimension_bounds, split_top_level_expression, @@ -26,6 +28,7 @@ from prik.utilities.visitor import ClassVisitor from prik.parsers.fortran.lexer import preprocess_lines +from prik.parsers.fortran.scope import ScopeUses, used_module_names from prik.parsers.fortran.models import ( FortranArgument, FortranBlockData, @@ -40,10 +43,11 @@ FortranProgram, FortranProject, FortranSubmodule, + FortranUseStatement, FortranUseMapping, FortranVariable, ) -from prik.parsers.fortran.type_resolver import extract_kind_from_type_spec +from prik.parsers.fortran.type_resolver import extract_character_selector, extract_kind_from_type_spec from prik.parsers.fortran.utils import split_csv _PARSER_ARCHITECTURE_GUIDE = """ @@ -366,8 +370,17 @@ class _Declaration: explicit_visibility: str | None = None target_kind_expression: str | None = None character_length_syntax: bool = False + character_length_expression: str | None = None + character_kind_expression: str | None = None declared_storage_bits: int | None = None + def record_character_selector(self, type_spec: str) -> None: + """Record what one character declaration's parenthesized selector states.""" + selector = extract_character_selector(type_spec) + self.character_length_expression = selector.length + self.character_kind_expression = selector.kind + self.character_length_syntax = selector.length_syntax + @dataclass class _ProcedureState: @@ -382,8 +395,8 @@ class _ProcedureState: signature: FortranProcedureSignature symbols: dict[str, FortranArgument] typed_symbols: set[str] = dataclass_field(default_factory=set) - uses: dict[str, list[FortranUseMapping]] = dataclass_field(default_factory=dict) - local_uses: dict[str, list[FortranUseMapping]] = dataclass_field(default_factory=dict) + uses: list[FortranUseStatement] = dataclass_field(default_factory=list) + local_uses: list[FortranUseStatement] = dataclass_field(default_factory=list) local_params: dict[str, str] = dataclass_field(default_factory=dict) legacy_local_params: set[str] = dataclass_field(default_factory=set) implicit_typed_symbols: dict[str, str] = dataclass_field(default_factory=dict) @@ -1860,7 +1873,7 @@ def _visit_ProcedureUnit( proc_state.filename = filename proc_state.header_lineno = header[1] proc_state.header_source_line = header[2] - proc_state.uses.update(getattr(parent_scope.model, "uses", {})) + proc_state.uses.extend(getattr(parent_scope.model, "uses", ())) scope = self._helper_scope_for_model("procedure", proc_state.signature, parent=parent_scope, state=proc_state) self._parse_specification_part(scope, unit.specification, filename=filename) child_units = unit.children @@ -1938,10 +1951,12 @@ def _helper_attach_file_interfaces( units: _ParsedFileUnits, ) -> list[FortranInterface]: """Collect interfaces and attach module-owned blocks to their owners.""" - interfaces = [ - self._visit(unit, parent_scope=scope, filename=filename) - for unit, scope in self._collect_interface_source_units(lines, filename) - ] + interfaces = self._merged_generic_interfaces( + [ + self._interface_with_scope(unit, scope, filename) + for unit, scope in self._collect_interface_source_units(lines, filename) + ] + ) for module in units.modules: module.interfaces = [ iface for iface in interfaces if iface.module and iface.module.lower() == module.name.lower() @@ -1952,6 +1967,63 @@ def _helper_attach_file_interfaces( ] return [iface for iface in interfaces if iface.module is None] + def _interface_with_scope( + self, + unit: SourceUnit, + scope: _ParserScope, + filename: str | None, + ) -> tuple[FortranInterface, tuple[tuple[str, str], ...]]: + """Parse one interface block and record the scope that declares it.""" + interface = self._visit(unit, parent_scope=scope, filename=filename) + identity = self._interface_scope_identity(scope) + interface.declaring_scope_kind = identity[-1][0] if identity else "file" + interface.declaring_scope_path = [name for _kind, name in identity if name] + return interface, identity + + @staticmethod + def _interface_scope_identity(scope: _ParserScope | None) -> tuple[tuple[str, str], ...]: + """Return the lexical scope chain that owns one interface block. + + A generic belongs to the scope declaring it, and a module, a submodule + and each procedure inside them are all separate scopes. The chain names + every enclosing one, so two procedures of the same module never look + like a single owner. + """ + chain: list[tuple[str, str]] = [] + while scope is not None: + chain.append((str(scope.kind), str(scope.name or "").casefold())) + scope = scope.parent + return tuple(reversed(chain)) + + @staticmethod + def _merged_generic_interfaces( + interfaces: list[tuple[FortranInterface, tuple[tuple[str, str], ...]]], + ) -> list[FortranInterface]: + """Combine blocks that extend one generic interface into a single record. + + Fortran lets a generic interface be built from several blocks in the + same scope, each contributing specifics. They name one generic, so the + parser reports one interface carrying every entry in declaration order. + Two scopes that happen to use one name declare two generics, so the + lexical owner is part of the identity rather than the module alone. + Abstract and unnamed blocks are never generics and stay as they are. + """ + merged: dict[tuple[tuple[tuple[str, str], ...], str], FortranInterface] = {} + result: list[FortranInterface] = [] + for interface, scope_identity in interfaces: + if not interface.name or interface.abstract: + result.append(interface) + continue + key = (scope_identity, interface.name.lower()) + existing = merged.get(key) + if existing is None: + merged[key] = interface + result.append(interface) + continue + existing.procedures.extend(interface.procedures) + existing.specific_procedures.extend(interface.specific_procedures) + return result + def _resolve_file_compile_time_facts(self, units: _ParsedFileUnits) -> None: """Apply source-visible compile-time symbols within one parsed file. @@ -2082,9 +2154,9 @@ def _project_file_requirements(parsed_file: FortranFile) -> set[str]: """ requirements: set[str] = set() for module in parsed_file.modules: - requirements.update(name.lower() for name in module.uses) + requirements.update(used_module_names(module)) for submodule in parsed_file.submodules: - requirements.update(name.lower() for name in submodule.uses) + requirements.update(used_module_names(submodule)) requirements.add(submodule.parent.lower()) if submodule.ancestor: requirements.add(submodule.ancestor.lower()) @@ -2233,7 +2305,7 @@ def _helper_index_project_module(self, project: FortranProject, module: FortranM """Index one module and its owned public models.""" module_key = module.name.lower() self._insert_unique_scope_symbol(project.modules, module_key, module, label="project module scope") - project.dependencies[module_key] = {name.lower() for name in module.uses} + project.dependencies[module_key] = used_module_names(module) self._helper_index_project_owner_members(project, module, module_key) def _helper_index_project_submodule(self, project: FortranProject, submodule: FortranSubmodule) -> None: @@ -2245,7 +2317,7 @@ def _helper_index_project_submodule(self, project: FortranProject, submodule: Fo submodule, label="project submodule scope", ) - dependencies = {submodule.parent.lower(), *(name.lower() for name in submodule.uses)} + dependencies = {submodule.parent.lower(), *used_module_names(submodule)} if submodule.ancestor: dependencies.add(submodule.ancestor.lower()) project.dependencies[submodule_key] = dependencies @@ -2303,7 +2375,7 @@ def _helper_index_project_program(self, project: FortranProject, program: Fortra return program_key = program.name.lower() self._insert_unique_scope_symbol(project.programs, program_key, program, label="project program scope") - project.dependencies[program_key] = {name.lower() for name in program.uses} + project.dependencies[program_key] = used_module_names(program) def _helper_index_project_interface( self, @@ -2811,7 +2883,12 @@ def _helper_validate_sibling_units( continue if unit.kind == "procedure": key = ("procedure", unit.name.lower()) - elif unit.kind in {"module", "submodule", "program", "block_data", "derived_type", "interface"}: + elif unit.kind == "interface": + # A generic interface may be declared in several blocks, each + # adding specifics to the same name, so a repeat is not a + # duplicate declaration. + continue + elif unit.kind in {"module", "submodule", "program", "block_data", "derived_type"}: key = (unit.kind, unit.name.lower()) else: continue @@ -3418,8 +3495,7 @@ def _parse_module_like_spec_line( parsed_use = self._parse_use_statement(stripped) if parsed_use and hasattr(target, "uses"): - module_name, mappings = parsed_use - target.uses[module_name] = mappings + target.uses.append(parsed_use) return if _REGEX["derived_type"].match(stripped): @@ -3586,9 +3662,8 @@ def _parse_procedure_spec_line( return parsed_use = self._parse_use_statement(stripped) if parsed_use: - module_name, mappings = parsed_use - proc_state.uses[module_name] = mappings - proc_state.local_uses[module_name] = mappings + proc_state.uses.append(parsed_use) + proc_state.local_uses.append(parsed_use) return # This parser is a subset parser focused on wrapper-relevant metadata. # These statements do not affect extracted signature typing/shapes. @@ -3712,6 +3787,23 @@ def _parse_type_spec_line( code="PARSE_UNSUPPORTED_DECLARATION", ) + @staticmethod + def _record_generic_binding(dtype: FortranDerivedType, binding: dict) -> None: + """Record one ``generic ::`` statement on a derived type. + + Fortran lets a type-bound generic be built from several statements in + one type, each contributing specifics. They name one binding, so the + parser reports one record carrying every target in declaration order. + The standard requires every statement for a binding to declare the same + accessibility, so the first statement's attributes stand for the rest. + """ + key = "".join(str(binding["name"]).split()).lower() + for existing in dtype.generic_bindings: + if "".join(str(existing["name"]).split()).lower() == key: + existing["targets"].extend(binding["targets"]) + return + dtype.generic_bindings.append(binding) + @staticmethod def _apply_default_component_visibility( dtype: FortranDerivedType, @@ -3769,13 +3861,14 @@ def _parse_derived_type_contains_line( attrs = [a.strip().lower() for a in split_csv(attr_txt)] if attr_txt else [] lhs, rhs_txt = [x.strip() for x in right.split("=>", 1)] rhs = [r.strip() for r in split_csv(rhs_txt)] - dtype.generic_bindings.append( + self._record_generic_binding( + dtype, { "name": lhs, "targets": rhs, "attrs": attrs, "visibility": _binding_visibility(attrs, dtype.binding_visibility), - } + }, ) return @@ -3941,7 +4034,10 @@ def _parse_declaration_left( return declaration, split_csv((decl.group("attrs") or "").strip().lstrip(", ")) if re.match(r"^procedure\s*\(", left, re.IGNORECASE): procm = _REGEX["procedure_dummy"].match(left) - iface = procm.group("iface").lower() if procm else None + # The interface name is a user-visible symbol that reaches the + # generated .pyi contract, so it keeps its declared spelling; + # every comparison against it normalizes case at the comparison. + iface = procm.group("iface") if procm else None return self._new_declaration("procedure", iface), split_csv( (procm.group("attrs") if procm else "").strip().lstrip(", ") ) @@ -4043,7 +4139,7 @@ def _store_procedure_declaration( filename=filename, code="PARSE_INTERNAL_STATE", ) - if declaration.base_type == "procedure" and declaration.kind in proc_state.imports: + if declaration.base_type == "procedure" and self._scope_key(declaration.kind or "") in proc_state.imports: declaration.kind = "" for normalized_name, shape, _initializer, entity_declaration in self._declaration_entities( right, @@ -4199,8 +4295,10 @@ def _intrinsic_declaration(base_type: str, type_spec: str) -> _Declaration: base_type, extract_kind_from_type_spec(base_type, type_spec), ) - if base_type == "character" and type_spec and re.search(r"\bkind\s*=", type_spec, re.IGNORECASE) is None: - declaration.character_length_syntax = True + if base_type == "character" and type_spec: + # The selector is read once here, while its top-level items are + # known, so no later stage has to split a joined spelling again. + declaration.record_character_selector(type_spec) return declaration @staticmethod @@ -4224,8 +4322,8 @@ def _apply_type_spelling_metadata(var: FortranVariable, spelling: str) -> None: base_type, type_spec, _tail = intrinsic if base_type in {"double precision", "double complex"}: var._target_kind_expression = "kind(1.0d0)" - elif base_type == "character" and type_spec and re.search(r"\bkind\s*=", type_spec, re.IGNORECASE) is None: - var._character_length_syntax = True + elif base_type == "character" and type_spec: + var.record_character_selector(type_spec) @staticmethod def _apply_declaration_attributes( @@ -4322,6 +4420,10 @@ def _apply_internal_type_metadata(arg: FortranVariable, declaration: _Declaratio arg._target_kind_expression = declaration.target_kind_expression if declaration.character_length_syntax: arg._character_length_syntax = True + if declaration.character_length_expression is not None: + arg._character_length_expression = declaration.character_length_expression + if declaration.character_kind_expression is not None: + arg._character_kind_expression = declaration.character_kind_expression if declaration.declared_storage_bits is not None: arg._declared_storage_bits = declaration.declared_storage_bits if declaration.polymorphic: @@ -4692,12 +4794,15 @@ def _resolve_procedure_signature_types( for arg in sig.arguments: if arg.kind: arg.kind = self._resolve_kind_expression(arg.kind, local_params, resolver=local_resolver) + self._resolve_character_length(arg, local_params, resolver=local_resolver) if arg.shape: arg.shape = [local_resolver.resolve(dim) for dim in arg.shape] if arg.base_type == "unknown" and not state.implicit_none: arg.base_type = self._infer_implicit_base_type(arg.name) if sig.result and sig.result.kind: sig.result.kind = self._resolve_kind_expression(sig.result.kind, local_params, resolver=local_resolver) + if sig.result is not None: + self._resolve_character_length(sig.result, local_params, resolver=local_resolver) return self._collect_relevant_local_params(sig, local_params) def _reconcile_procedure_local_declarations( @@ -4826,7 +4931,7 @@ def _attach_procedure_scope_metadata( attr = f"import({symbol})" if attr not in sig.attributes: sig.attributes.append(attr) - sig.uses = dict(state.uses) + sig.uses = list(state.uses) sig.common_variables = list(state.common_variables) @staticmethod @@ -4843,7 +4948,7 @@ def _copy_finalized_procedure_signature( not deep-copy arguments or other signature members. """ finalized = replace(sig) - finalized._local_uses = dict(state.local_uses) + finalized._local_uses = list(state.local_uses) return finalized @staticmethod @@ -5126,7 +5231,7 @@ def _build_compile_time_symbols( @staticmethod def _imported_compile_time_symbols( - uses: Mapping[str, list[FortranUseMapping]], + uses: Iterable[FortranUseStatement], symbols: _CompileTimeSymbols, *, include_intrinsic_aliases: bool, @@ -5141,30 +5246,34 @@ def _imported_compile_time_symbols( when the intrinsic module has no parsed model; ordinary procedure scope lookup leaves that target-dependent spelling untouched. """ + scope = ScopeUses(uses) + offered = {module: symbols.in_module(module.casefold()) for module in scope.modules()} imported: dict[str, str] = {} - for dependency, mappings in uses.items(): - dependency_name = dependency.casefold() - dependency_symbols = symbols.in_module(dependency_name) - if not mappings: - imported.update(dependency_symbols) + for name in scope.accessible_names(lambda module: offered[module]): + expressions = { + offered[route.module][route.source_name.casefold()] + for route in scope.routes_for(name, lambda module: offered[module]) + if route.source_name.casefold() in offered[route.module] + } + # Routes that disagree leave the name meaning more than one value, + # which is not something to choose between. + if len(expressions) == 1: + imported[name.casefold()] = next(iter(expressions)) + if not include_intrinsic_aliases: + return imported + # An intrinsic module has no parsed symbols, so a name imported from + # one stands for its own target-dependent spelling. + for module in scope.modules(): + if module.casefold() not in _INTRINSIC_COMPILE_TIME_MODULES: continue - for mapping in mappings: - source_name = mapping.source.casefold() - expression = dependency_symbols.get(source_name) - if ( - expression is None - and include_intrinsic_aliases - and dependency_name in _INTRINSIC_COMPILE_TIME_MODULES - ): - expression = mapping.source - if expression is not None: - imported[mapping.local_name.casefold()] = expression + for mapping in scope.mappings(module): + imported.setdefault(mapping.local_name.casefold(), mapping.source) return imported @staticmethod def _compile_time_symbols_for_scope( owner_name: str | None, - uses: Mapping[str, list[FortranUseMapping]], + uses: Iterable[FortranUseStatement], symbols: _CompileTimeSymbols, ) -> dict[str, str]: """Return a mutable flat symbol map visible to one parsed scope. @@ -5254,6 +5363,7 @@ def _resolve_procedure_signature_facts( visible_symbols, resolver=resolver, ) + FortranParser._resolve_character_length(argument, visible_symbols, resolver=resolver) if resolve_shapes and argument.shape: argument.shape = [resolver.resolve(dimension) for dimension in argument.shape] if signature.result and signature.result.kind: @@ -5324,6 +5434,7 @@ def _resolve_module_like_compile_time_facts( visible, resolver=resolver, ) + FortranParser._resolve_character_length(variable, visible, resolver=resolver) if variable.shape: variable.shape = [resolver.resolve(dimension) for dimension in variable.shape] variable.lbound, variable.ubound = FortranParser._extract_bounds(variable.shape) @@ -5356,10 +5467,35 @@ def _resolve_derived_type_compile_time_facts( visible, resolver=resolver, ) + FortranParser._resolve_character_length(field, visible, resolver=resolver) if field.shape: field.shape = [resolver.resolve(dimension) for dimension in field.shape] field.lbound, field.ubound = FortranParser._extract_bounds(field.shape) + @staticmethod + def _resolve_character_length( + variable: FortranVariable, + symbols: Mapping[str, str], + *, + resolver: _CompileTimeResolver | None = None, + ) -> None: + """Resolve a separated character length against the kind's own symbols. + + The length is recorded apart from the kind, so it is resolved wherever + the kind is: a declaration written ``character(len=fixed)`` states the + value ``fixed`` names, the same as one written ``character(fixed)``. + """ + active_resolver = resolver or _CompileTimeResolver(symbols) + for attribute in ("_character_length_expression", "_character_kind_expression"): + declared = getattr(variable, attribute, None) + if not declared: + continue + setattr( + variable, + attribute, + active_resolver.resolve(FortranParser._resolve_symbol_reference(str(declared), symbols)), + ) + @staticmethod def _resolve_kind_expression( expr: str, @@ -5412,11 +5548,16 @@ def _collect_relevant_local_params(sig: FortranProcedureSignature, local_params: @staticmethod def _extract_symbol_names(expr: str) -> set[str]: - """Extract lowercase identifier tokens from one expression.""" + """Return the lower-case names one expression reads. + + The names come from parsing, so a character literal's contents stay + part of its value: a parameter whose value is ``"widen"`` does not read + a parameter named ``widen``. + """ keywords = {"and", "or", "not"} return { token.lower() - for token in re.findall(r"[A-Za-z_][A-Za-z0-9_]*", expr or "") + for token in declaration_expression_identifiers(expr or "") if not token.isdigit() and token.lower() not in keywords } @@ -5449,7 +5590,10 @@ def _is_literal_parameter_value(value: str) -> bool: return True if re.fullmatch(r"\.(?:true|false)\.", text, re.IGNORECASE): return True - if re.fullmatch(r"(['\"]).*\1", text): + # One reader decides what a whole character literal is, so the value a + # parameter records is the one later stages decode. Matching any text + # between two quotes also accepted `'a' // 'b'`, which is an expression. + if fortran_character_value(text) is not None: return True if text.startswith("[") and text.endswith("]"): return all(FortranParser._is_literal_parameter_value(part) for part in split_csv(text[1:-1])) @@ -5682,14 +5826,19 @@ def _bind_c_name(tail: str) -> str | None: return name if name else None @staticmethod - def _parse_use_statement(line: str) -> tuple[str, list[FortranUseMapping]] | None: - """Parse a ``use`` statement into its module and explicit mappings.""" + def _parse_use_statement(line: str) -> FortranUseStatement | None: + """Parse one ``use`` statement into the facts the source states. + + Whether the statement narrowed to an ``only`` list is separate from + what it listed: ``use m, only :`` lists nothing and brings in nothing, + while ``use m`` also lists nothing and brings in everything. + """ match = _REGEX["use"].match(line) if not match: return None rest = (match.group("rest") or "").strip() if not rest: - return match.group("module"), [] + return FortranUseStatement(match.group("module")) payload = rest.lstrip(",").strip() only_match = re.match(r"^only\s*:\s*(?P.*)$", payload, re.IGNORECASE) if only_match: @@ -5705,7 +5854,7 @@ def _parse_use_statement(line: str) -> tuple[str, list[FortranUseMapping]] | Non source = token target = None mappings.append(FortranUseMapping(source=source, target=target)) - return match.group("module"), mappings + return FortranUseStatement(match.group("module"), only_match is not None, tuple(mappings)) # ----------------------------------------------------------------------------- diff --git a/prik/parsers/fortran/scope.py b/prik/parsers/fortran/scope.py new file mode 100644 index 000000000..b64d23027 --- /dev/null +++ b/prik/parsers/fortran/scope.py @@ -0,0 +1,145 @@ +"""How one Fortran scope reads its ``use`` statements. + +A scope may name one module in several statements, spelled any way, and the +language reads them together: an ``only`` list narrows what its own statement +brings in, a rename binds an entity under a new name and leaves the old one +naming nothing, and any statement without ``only`` carries whatever else the +module publishes. + +This is the single reading of that. It answers what a scope sees under a local +name and by which routes, and it answers nothing else: several routes to +different entities are reported as several routes, because whether that is an +ambiguity or a set of contributors is a question about the entities, which the +stage holding them decides. +""" + +from __future__ import annotations + +from collections.abc import Callable, Collection, Iterable +from dataclasses import dataclass + +from prik.parsers.fortran.models import FortranUseMapping, FortranUseStatement + +#: Answers the public names one used module offers, or ``None`` when unread. +OfferedNames = Callable[[str], Collection[str] | None] + + +@dataclass(frozen=True) +class UseRoute: + """One way a scope reaches a name: the module used, and the name there.""" + + module: str + source_name: str + + @property + def key(self) -> tuple[str, str]: + """Return the case-folded identity two spellings of one route share.""" + return self.module.casefold(), self.source_name.casefold() + + +class ScopeUses: + """One scope's ``use`` statements, grouped by the module each names. + + Fortran module names are case-insensitive, so ``use DEP`` and ``use dep`` + are statements about one module and are read together. + """ + + def __init__(self, statements: Iterable[FortranUseStatement]) -> None: + self._by_module: dict[str, list[FortranUseStatement]] = {} + for statement in statements: + self._by_module.setdefault(statement.module.casefold(), []).append(statement) + + def modules(self) -> tuple[str, ...]: + """Return each used module once, spelled as its first statement wrote it.""" + return tuple(statements[0].module for statements in self._by_module.values()) + + def imports_all(self, module: str) -> bool: + """Return whether any statement for ``module`` omitted ``only``.""" + return any(not statement.only for statement in self._by_module.get(module.casefold(), ())) + + def mappings(self, module: str) -> tuple[FortranUseMapping, ...]: + """Return every name the statements for ``module`` listed, in order.""" + seen: dict[tuple[str, str], FortranUseMapping] = {} + for statement in self._by_module.get(module.casefold(), ()): + for mapping in statement.mappings: + seen.setdefault((mapping.source.casefold(), mapping.local_name.casefold()), mapping) + return tuple(seen.values()) + + def routes_for(self, local_name: str, offered: OfferedNames) -> tuple[UseRoute, ...]: + """Return every route by which this scope reaches one local name. + + A listed name states its own route. A module imported whole is a route + for a name it publishes, unless a rename took that name away. A module + this project never read cannot be enumerated, so it offers no route + rather than an assumed one. + """ + folded = local_name.casefold() + routes: dict[tuple[str, str], UseRoute] = {} + for module in self.modules(): + for mapping in self.mappings(module): + if mapping.local_name.casefold() == folded: + route = UseRoute(module, mapping.source) + routes.setdefault(route.key, route) + for module in self.modules(): + if not self.imports_all(module) or folded in self._renamed_away(module): + continue + names = offered(module) + if names is not None and folded in names: + route = UseRoute(module, local_name) + routes.setdefault(route.key, route) + return tuple(routes.values()) + + def accessible_names(self, offered: OfferedNames) -> tuple[str, ...]: + """Return every local name this scope reaches, in source order. + + A listed name keeps the spelling its ``use`` statement bound it under; + a name carried whole keeps the spelling its module publishes. + """ + names: dict[str, str] = {} + for module in self.modules(): + for mapping in self.mappings(module): + names.setdefault(mapping.local_name.casefold(), mapping.local_name) + for module in self.modules(): + if not self.imports_all(module): + continue + renamed_away = self._renamed_away(module) + for name in sorted(offered(module) or ()): + if name not in renamed_away: + names.setdefault(name.casefold(), name) + return tuple(names.values()) + + def unresolved_routes_for(self, local_name: str, offered: OfferedNames) -> tuple[UseRoute, ...]: + """Return possible whole-module routes whose names cannot be enumerated. + + A plain ``use`` of a module this project never read carries names none + of which can be listed. The route stays possible unless a rename took + this spelling away; the consuming entity category decides whether that + uncertainty makes the name ambiguous or permits an opaque fallback. + """ + folded = local_name.casefold() + return tuple( + UseRoute(module, local_name) + for module in self.modules() + if self.imports_all(module) and offered(module) is None and folded not in self._renamed_away(module) + ) + + def _renamed_away(self, module: str) -> frozenset[str]: + """Return the names a rename reaches, which are not reachable as written.""" + return frozenset(item.source.casefold() for item in self.mappings(module) if item.target) + + +def used_module_names(owner: object) -> set[str]: + """Return every module one scope names, lowercased. + + A ``use`` written inside a contained procedure or an interface body is a + dependency of the scope holding it just as much as one written at its top, + so the whole tree is read. Compile ordering and project dependencies both + ask this, and they have to get the same answer. + """ + statements: list[FortranUseStatement] = list(getattr(owner, "uses", ())) + for procedure in getattr(owner, "procedures", ()): + statements.extend(getattr(procedure, "uses", ())) + for interface in getattr(owner, "interfaces", ()): + for procedure in getattr(interface, "procedures", ()): + statements.extend(getattr(procedure, "uses", ())) + return {statement.module.lower() for statement in statements} diff --git a/prik/parsers/fortran/type_resolver.py b/prik/parsers/fortran/type_resolver.py index 05a75f118..fb94443cc 100644 --- a/prik/parsers/fortran/type_resolver.py +++ b/prik/parsers/fortran/type_resolver.py @@ -8,6 +8,8 @@ from __future__ import annotations +from typing import NamedTuple + from prik.parsers.fortran.utils import split_csv @@ -49,6 +51,53 @@ def extract_kind_from_type_spec(base_type: str, type_spec: str) -> str | None: return None +class CharacterSelector(NamedTuple): + """What one character declaration's selector states. + + ``length_syntax`` records that the declaration's stored ``kind`` text is a + length rather than a kind, which is what ``character(8)``, ``character(*)`` + and ``character(len=n)`` all mean. It is read from the same split as the + two expressions, so a selector is interpreted once. + """ + + length: str | None = None + kind: str | None = None + + @property + def length_syntax(self) -> bool: + """Whether the selector names no kind, leaving its text a length.""" + return self.kind is None + + +def extract_character_selector(type_spec: str) -> CharacterSelector: + """Return one character declaration's length and kind expressions. + + The selector carries two independent expressions, either of which may + contain commas of its own, so they are separated here where the top-level + items are already known rather than rediscovered from a joined spelling. + A positional specifier states the length, which is what ``character(8)`` + and ``character(*)`` mean. + """ + if not type_spec: + return CharacterSelector() + inside = type_spec[1:-1].strip() + if not inside: + return CharacterSelector() + length: str | None = None + kind: str | None = None + for item in split_csv(inside): + key, separator, value = item.partition("=") + if not separator: + length = length or item.strip() or None + continue + keyword = key.strip().lower() + if keyword == "len": + length = value.strip() or None + elif keyword == "kind": + kind = value.strip() or None + return CharacterSelector(length, kind) + + if __name__ == "__main__": examples = [ ("integer", "(4)"), diff --git a/prik/pipeline/build.py b/prik/pipeline/build.py index edc3ac4e5..699ea0d30 100644 --- a/prik/pipeline/build.py +++ b/prik/pipeline/build.py @@ -39,6 +39,7 @@ from prik.parsers.c import parse_c_file from prik.parsers.c.cli import attach_preprocessing_recipe from prik.parsers.fortran.parser import parse_fortran_project +from prik.parsers.fortran.scope import used_module_names from prik.preprocessing.probes.fortran_types import ( evaluate_fortran_type_facts, evaluate_fortran_type_requirements, @@ -61,12 +62,11 @@ PYTHON_EXPORTS_PREPARED_METADATA, RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA, ProcedureOverloadSet, - SemanticClass, SemanticFunction, SemanticImport, SemanticModule, SemanticPrototype, - SemanticVariable, + SemanticReexport, _module_semantic_types, ) from prik.semantics.native_contract import NATIVE_CONTRACT_PREPARED_METADATA, validate_pyi_native_contract @@ -76,6 +76,7 @@ ) from prik.policy.completion import _DEFERRED_C_DIRECT_DIAGNOSTIC_CODES, complete_semantic_policies from prik.policy.models import FunctionWrapperPolicy, NativeEntrypointAction +from prik.policy.exports import complete_reexport_publication_policy from prik.pipeline.pyi import _PyiSemanticModuleCache from prik.semantics.pyi_metadata import PYI_LOADED_METADATA from prik.planning import NativeGeneratedCodeGroupPlan, WrapperPlanner @@ -844,10 +845,12 @@ def _write_build_contract_package( reshaping the Python surface never needs a separate `generate --pyi` run. The package lives in its own directory inside the build output so its ``__init__.pyi`` cannot make the build directory look like a Python package. + Only a source build writes one, so the declarations are named in Fortran or + C and the contract states the Python names this build just published. """ if not source_modules: return () - stubs = emit_module_stubs(source_modules) + stubs = emit_module_stubs(source_modules, normalize_public_names=True) package_dir = output_dir / BUILD_CONTRACT_DIRECTORY_NAME package_dir.mkdir(parents=True, exist_ok=True) written = [] @@ -1636,22 +1639,6 @@ def _serial_compile_batches(object_files: Iterable[ObjectFile]) -> tuple[tuple[O return tuple((object_file,) for object_file in object_files) -def _fortran_owner_used_modules(owner: object) -> set[str]: - """Return lowercased modules used directly or indirectly by one owner. - - ``owner`` may be a parsed module, program, procedure, or submodule. The - helper reads its ``uses`` mappings and the uses of contained procedures and - interface procedures, returning a new set without changing the parsed AST. - """ - used = {str(name).lower() for name in getattr(owner, "uses", {})} - for procedure in getattr(owner, "procedures", ()): - used.update(str(name).lower() for name in getattr(procedure, "uses", {})) - for interface in getattr(owner, "interfaces", ()): - for procedure in getattr(interface, "procedures", ()): - used.update(str(name).lower() for name in getattr(procedure, "uses", {})) - return used - - def _fortran_file_used_modules(parsed_file: object) -> set[str]: """Return lowercased module dependencies declared by one parsed file. @@ -1667,10 +1654,9 @@ def _fortran_file_used_modules(parsed_file: object) -> set[str]: ) used = set() for owner in owners: - used.update(_fortran_owner_used_modules(owner)) + used.update(used_module_names(owner)) for interface in getattr(parsed_file, "interfaces", ()): - for procedure in getattr(interface, "procedures", ()): - used.update(str(name).lower() for name in getattr(procedure, "uses", {})) + used.update(used_module_names(interface)) for submodule in getattr(parsed_file, "submodules", ()): used.add(str(submodule.parent).lower()) if submodule.ancestor: @@ -2046,6 +2032,16 @@ class _PyiExportNode: declarations: list[object] = field(default_factory=list) children: dict[str, _PyiExportNode] = field(default_factory=dict) origins: set[Path] = field(default_factory=set) + unpublished: set[str] = field(default_factory=set) + """Names this node resolves but its contract left out of ``__all__``. + + A contract stating no list publishes everything it reaches, so a name is + withheld only where the contract named its surface and left this one off. + A sub-namespace is part of that surface like anything else, so leaving one + off keeps the package from exposing it. Such a name still resolves, because + a contract reading from this one has to resolve what it names; it simply + does not become a Python attribute here. + """ def _apply_pyi_python_exports(entry: Path, modules_by_path: dict[Path, SemanticModule]) -> None: @@ -2063,6 +2059,40 @@ def _apply_pyi_python_exports(entry: Path, modules_by_path: dict[Path, SemanticM tree = _pyi_export_tree(entry, modules_by_path, cache={}, pending=set()) _record_pyi_exports(tree) + namespace_by_contract = _namespace_by_contract(tree, entry) + # A declaration published from more than one namespace is one entity, so + # the namespaces beyond its own bind what its own already exports rather + # than each wrapping the native declaration again. Which namespace owns it + # is settled by the contract declaring it, never by the order an entry + # happens to import from. A rename changes the name a namespace binds, + # never the object behind it. + for path, module in modules_by_path.items(): + home = namespace_by_contract.get(path) + for declaration, entity_kind in ( + *((item, "derived_type") for item in module.classes), + *((item, "procedure") for item in module.functions), + ): + exports = _declaration_exports(declaration) + if len(exports) < 2: + continue + primary = next( + (export for export in exports if tuple(export["namespace"]) == home), + exports[0], + ) + aliases = [export for export in exports if export is not primary] + source_namespace = ".".join(primary["namespace"]) + for alias in aliases: + module.reexports.append( + SemanticReexport( + local_name=alias["name"], + origin_module=source_namespace, + source_name=primary["name"], + module=".".join(alias["namespace"]), + entity_kind=entity_kind, + ) + ) + exports[:] = [primary] + _reject_unsupported_republication(path, module, home) def _pyi_export_tree( @@ -2106,6 +2136,14 @@ def _pyi_export_tree( if not isinstance(semantic_import, SemanticImport) or not semantic_import.module.startswith("."): continue _merge_relative_import(tree, path, semantic_import, modules_by_path, cache, pending) + if module.exported_names is not None: + # A stated list is the whole public surface: a name on it is published + # whether this contract declares or imports it, and one left off stays + # available to express declarations without reaching Python. 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. + _apply_stated_exports(tree, path, module.exported_names) pending.remove(path) cache[path] = tree return tree @@ -2132,7 +2170,11 @@ def _merge_relative_import( dependency_tree = _required_export_tree(dependency, modules_by_path, cache, pending) for item in semantic_import.items: if item.source == "*": + # A wildcard takes the surface the dependency publishes. A name + # it withheld is still reachable, but only by asking for it. for name, child in dependency_tree.children.items(): + if name in dependency_tree.unpublished: + continue _merge_export_child(tree, name, child, origin=path) continue if item.source not in dependency_tree.children: @@ -2167,6 +2209,15 @@ def _required_export_tree( return _pyi_export_tree(path, modules_by_path, cache=cache, pending=pending) +def _apply_stated_exports(tree: _PyiExportNode, path: Path, exported_names: list[str]) -> None: + """Publish exactly the names one contract states, and nothing else.""" + stated = list(dict.fromkeys(exported_names)) + missing = [name for name in stated if name not in tree.children] + if missing: + raise ValueError(f"{path}: __all__ names nothing this contract declares or imports: {missing}") + tree.unpublished = {name for name in tree.children if name not in set(stated)} + + def _merge_export_child(tree: _PyiExportNode, name: str, child: _PyiExportNode, *, origin: Path) -> None: """Insert one named export into ``tree`` or reject a conflicting origin. @@ -2186,6 +2237,53 @@ def _merge_export_child(tree: _PyiExportNode, name: str, child: _PyiExportNode, ) +def _reject_unsupported_republication( + path: Path, + module: SemanticModule, + home: tuple[str, ...] | None, +) -> None: + """Refuse a generic published outside the namespace declaring it. + + A module variable has a dedicated publication plan that routes every + namespace to one native variable plan. A generic remains a dispatch + surface rather than one bindable object, so it cannot be republished. + """ + for declaration, kind in ((item, "generic") for item in module.overload_sets): + exports = _declaration_exports(declaration) + relocated = [export for export in exports if home is None or tuple(export["namespace"]) != home] + if not relocated: + continue + declaring = "" if home is None else (".".join(home) or "") + namespaces = ", ".join(".".join(export["namespace"]) or "" for export in relocated) + raise ValueError( + f"{path}: {kind} {declaration.name!r} is declared in {declaring} and published in " + f"{namespaces}; this kind is publishable only by the namespace declaring it" + ) + + +def _namespace_by_contract(tree: _PyiExportNode, entry: Path) -> dict[Path, tuple[str, ...]]: + """Return the Python namespace each contract's own declarations live in. + + A contract publishes its declarations in one namespace of its own, and any + other namespace publishing them is republishing what that one owns. The + entry contract owns the package root; every other namespace node names the + contract it was built from. + """ + namespaces: dict[Path, tuple[str, ...]] = {entry: ()} + + def walk(node: _PyiExportNode, namespace: tuple[str, ...]) -> None: + for name, child in node.children.items(): + if not child.children: + continue + child_namespace = (*namespace, name) + for origin in child.origins: + namespaces.setdefault(origin, child_namespace) + walk(child, child_namespace) + + walk(tree, ()) + return namespaces + + def _record_pyi_exports(tree: _PyiExportNode, namespace: tuple[str, ...] = ()) -> None: """Write resolved namespace paths from an export tree into declarations. @@ -2194,6 +2292,8 @@ def _record_pyi_exports(tree: _PyiExportNode, namespace: tuple[str, ...] = ()) - The declaration metadata is intentionally mutated for later planning. """ for name, child in tree.children.items(): + if name in tree.unpublished: + continue for declaration in child.declarations: if isinstance(declaration, SemanticPrototype): continue @@ -2209,32 +2309,14 @@ def _module_declarations(module: SemanticModule) -> tuple[object, ...]: return (*module.variables, *module.functions, *module.overload_sets, *module.classes) -def _declaration_metadata(declaration: object) -> dict[str, object]: - """Return the mutable metadata dictionary for one supported declaration. - - Overload sets use their first candidate's metadata because that is where - their shared export projection is stored. Unsupported objects raise - ``TypeError`` rather than silently lose metadata. - """ - if isinstance(declaration, ProcedureOverloadSet): - if not declaration.procedures: - return {} - return declaration.procedures[0].metadata - if isinstance(declaration, SemanticVariable | SemanticFunction | SemanticClass): - return declaration.metadata - raise TypeError(f"Unsupported semantic declaration: {type(declaration).__name__}") - - def _declaration_exports(declaration: object) -> list[dict[str, object]]: """Return and initialize the declaration's mutable Python export list.""" - metadata = _declaration_metadata(declaration) - return metadata.setdefault(PYTHON_EXPORTS_METADATA, []) + return declaration.metadata.setdefault(PYTHON_EXPORTS_METADATA, []) def _set_declaration_exports(declaration: object, exports: list[dict[str, object]]) -> None: """Replace one declaration's stored Python export projection in place.""" - metadata = _declaration_metadata(declaration) - metadata[PYTHON_EXPORTS_METADATA] = exports + declaration.metadata[PYTHON_EXPORTS_METADATA] = exports def _apply_source_python_exports(modules: list[SemanticModule]) -> None: @@ -2245,6 +2327,7 @@ def _apply_source_python_exports(modules: list[SemanticModule]) -> None: procedures receive the root namespace; private declarations receive none. """ for module in modules: + complete_reexport_publication_policy(module, contract_named=False) module.metadata[PYTHON_EXPORTS_PREPARED_METADATA] = True namespace = (module.name.casefold(),) if module.origin.source_kind == "module" else () for declaration in _module_declarations(module): @@ -2257,6 +2340,32 @@ def _apply_source_python_exports(modules: list[SemanticModule]) -> None: ), ) + variables_by_identity = { + (module.name.casefold(), str(variable.origin.native_name or variable.name).casefold()): variable + for module in modules + for variable in module.variables + } + for module in modules: + for reexport in module.reexports: + if not reexport.publishes_to_python(): + continue + if reexport.entity_kind != "variable": + continue + variable = variables_by_identity.get( + (str(reexport.origin_module).casefold(), str(reexport.source_name).casefold()) + ) + if variable is None: + raise ValueError( + f"Cannot resolve re-exported module variable {reexport.origin_module}.{reexport.source_name}" + ) + export = { + "namespace": tuple(part.casefold() for part in str(reexport.module).split(".") if part), + "name": str(reexport.local_name), + } + exports = _declaration_exports(variable) + if export not in exports: + exports.append(export) + # Native build inputs and link planning @@ -3048,15 +3157,21 @@ def _merge_wrapper_modules(modules: list[SemanticModule], *, name: str | None = Concatenates every declaration category while preserving list order and derives combined metadata and the origin from the first module. An empty input cannot produce a wrapper and raises ``ValueError``. + + The merged module is completed for this build as one namespace, which is + not how each source module's own contract is completed, so it owns copies: + completing it leaves the source modules describing their own contracts. """ if not modules: raise ValueError("wrapper build found no Fortran modules or standalone procedures") + modules = deepcopy(modules) return SemanticModule( name=name or modules[0].name, functions=[function for module in modules for function in module.functions], prototypes=[prototype for module in modules for prototype in module.prototypes], overload_sets=[overload for module in modules for overload in module.overload_sets], + reexports=[reexport for module in modules for reexport in module.reexports], classes=[semantic_class for module in modules for semantic_class in module.classes], variables=[variable for module in modules for variable in module.variables], metadata=_wrapper_module_metadata(modules), @@ -3819,8 +3934,12 @@ def build_c_extension( unsupported operations raise a documented completed-policy diagnostic before planning, generated files, or compiler commands. A selected genuine identifier collision may use a separate C forwarder translation unit. - ``export_symbols`` restricts semantic conversion to those exact reachable - C functions and can explicitly select declarations from included headers. + ``export_symbols`` names the source-side public surface: semantic conversion + keeps exactly those reachable C functions, and can explicitly select + declarations from included headers. It is the C-source equivalent of the + ``__all__`` a semantic ``.pyi`` contract states for itself; emitted stubs + record the corresponding Python public names there. Unknown names are + rejected rather than silently narrowing the module. ``compile_input_sources`` controls whether the parsed C sources are also compiled. ``native_c_sources`` adds separately compiled C inputs, while explicit Fortran inputs are supported only as ordinary link dependencies. diff --git a/prik/pipeline/pyi.py b/prik/pipeline/pyi.py index ef5f2fb94..030c89320 100644 --- a/prik/pipeline/pyi.py +++ b/prik/pipeline/pyi.py @@ -16,6 +16,8 @@ from prik.parsers.pyi import parse_pyi_text from prik.policy.completion import complete_semantic_policies +from prik.policy.contract_imports import complete_contract_imports +from prik.policy.exports import complete_python_export_policy from prik.printers.pyi import emit_module from prik.semantics.models import EXTERNAL_TYPE_REF_METADATA, SemanticClass, SemanticModule, _module_semantic_types from prik.semantics.pyi_metadata import PYI_LOADED_METADATA @@ -104,7 +106,7 @@ def emit_module_stubs( modules: SemanticModule | Iterable[SemanticModule], *, available_modules: Iterable[SemanticModule] | None = None, - normalize_fortran_public_names: bool = False, + normalize_public_names: bool = False, ) -> dict[str, str]: """Complete and render semantic modules plus opaque dependencies. @@ -116,6 +118,7 @@ def emit_module_stubs( generated contract package by a pipeline stage. """ source_modules = _module_list(modules) + available = _module_list(available_modules) if available_modules is not None else source_modules emitted_modules: dict[str, SemanticModule] = {} for module in source_modules: if module.name in emitted_modules: @@ -124,18 +127,33 @@ def emit_module_stubs( for dependency in opaque_dependency_modules( source_modules, - available_modules=available_modules, + available_modules=available, ): target = emitted_modules.setdefault(dependency.name, SemanticModule(name=dependency.name)) existing = {cls.name for cls in target.classes} target.classes.extend(cls for cls in dependency.classes if cls.name not in existing) + # Public names are owned by post-IR policy for every route, so they are + # completed even where the rest of policy cannot run: a C starter contract + # describes source the direct-only wrapper may go on to reject, and naming + # a declaration does not depend on whether that declaration is buildable. + # Available modules also participate in this naming pass. They are not + # emitted, but an emitted module importing one must ask for the exact name + # its separately emitted contract declares. + naming_modules = dict(emitted_modules) + for module in available: + naming_modules.setdefault(module.name, deepcopy(module)) + for module in naming_modules.values(): + complete_python_export_policy(module) complete_semantic_policies(module for module in emitted_modules.values() if module.origin.source_language != "c") + # Each import asks the module it reads from for the name that module's + # contract declares, emitted here or not. + complete_contract_imports( + emitted_modules.values(), + dependencies=(module for name, module in naming_modules.items() if name not in emitted_modules), + ) return { - module_name: emit_module( - module, - normalize_fortran_public_names=normalize_fortran_public_names, - ).strip() + module_name: emit_module(module, normalize_public_names=normalize_public_names).strip() for module_name, module in emitted_modules.items() } diff --git a/prik/pipeline/type_mapping_report.py b/prik/pipeline/type_mapping_report.py index f5d9de7df..286ee3c08 100644 --- a/prik/pipeline/type_mapping_report.py +++ b/prik/pipeline/type_mapping_report.py @@ -84,19 +84,24 @@ def _fortran_type( kind: str | None = None, *, target_kind_expression: str | None = None, + character_selector: str | None = None, character_length_syntax: bool = False, declared_storage_bits: int | None = None, ) -> tuple[str, FortranVariable]: """Build one report-only Fortran variable and its displayed spelling. The helper records metadata that the existing Fortran converter consumes - when deriving a target type key. It returns the spelling and configured - variable without mutating any caller-owned object; the private attributes - intentionally distinguish legacy storage and character-length forms. + when deriving a target type key. A parenthesized character declaration + records its selector the way the parser does, so a report row states the + same length and kind a parsed declaration would; the legacy ``character*n`` + forms carry no selector and state length syntax directly. It returns the + spelling and configured variable without mutating any caller-owned object. """ variable = FortranVariable(name="value", base_type=base_type, kind=kind or "") if target_kind_expression: variable._target_kind_expression = target_kind_expression + if character_selector is not None: + variable.record_character_selector(character_selector) if character_length_syntax: variable._character_length_syntax = True if declared_storage_bits is not None: @@ -143,9 +148,9 @@ def _fortran_type( *(_fortran_type(f"logical(kind={kind})", "logical", kind) for kind in ("1", "2", "4", "8")), _fortran_type("logical(c_bool)", "logical", "c_bool"), _fortran_type("character", "character"), - _fortran_type("character(len=n)", "character", "n", character_length_syntax=True), - _fortran_type("character(kind=1)", "character", "kind=1"), - _fortran_type("character(kind=c_char)", "character", "kind=c_char"), + _fortran_type("character(len=n)", "character", "n", character_selector="(len=n)"), + _fortran_type("character(kind=1)", "character", "kind=1", character_selector="(kind=1)"), + _fortran_type("character(kind=c_char)", "character", "kind=c_char", character_selector="(kind=c_char)"), ) _FORTRAN_LEGACY_TYPES = ( diff --git a/prik/pipeline/wrapper.py b/prik/pipeline/wrapper.py index f5850d744..90cb50b37 100644 --- a/prik/pipeline/wrapper.py +++ b/prik/pipeline/wrapper.py @@ -20,6 +20,7 @@ from pathlib import Path import time +from prik.utilities.declaration_expressions import RUNTIME_DIMENSION_MARKERS from prik.utilities.stage_values import StageRecord from prik.policy.ownership import ( AssignmentMode, @@ -37,14 +38,13 @@ from prik.policy.models import ( ArgumentHandoffMode, ArrayEntrypointABI, - ArrayLogicalABI, ArrayPythonLayout, - ArrayWritebackABI, BridgeDataAction, CallbackABIKind, CallbackFatalAction, CallbackGILAction, CallbackLifecycleAction, + CallbackOptionalityAction, CallbackResultAction, CallbackThreadAction, CallbackTransferAction, @@ -324,14 +324,15 @@ def _plan_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnostic, .. ) diagnostics.extend(self._generated_support_procedure_entrypoint_diagnostics(plan)) diagnostics.extend(self._namespace_tree_diagnostics(plan)) + diagnostics.extend(self._module_variable_publication_diagnostics(plan)) + for variable in plan.variables: + diagnostics.extend(self._module_variable_diagnostics(variable)) - # Validate every typed member against the shared records in its namespace. + # Validate every namespace-owned member against its shared records. for namespace in plan.namespaces: diagnostics.extend(self._namespace_diagnostics(namespace)) for function in namespace.functions: diagnostics.extend(self._function_diagnostics(function)) - for variable in namespace.variables: - diagnostics.extend(self._module_variable_diagnostics(variable)) for class_surface in namespace.classes: diagnostics.extend(self._class_surface_diagnostics(namespace, class_surface)) functions = {id(function) for function in namespace.functions} @@ -340,6 +341,7 @@ def _plan_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnostic, .. # Validate graph-wide ordering, generated spellings, and header dependencies. diagnostics.extend(self._class_graph_diagnostics(plan)) + diagnostics.extend(self._derived_type_identity_diagnostics(plan)) diagnostics.extend(self._generated_symbol_diagnostics(plan)) diagnostics.extend(self._required_header_diagnostics(plan)) return tuple(diagnostics) @@ -403,7 +405,7 @@ def _generated_support_procedure_entrypoint_diagnostics( for operation in operations: diagnostics.extend(self._generated_support_procedure_diagnostics(operation)) try: - expected_projection = build_generated_support_procedure_projection(plan.namespaces) + expected_projection = build_generated_support_procedure_projection(plan.namespaces, plan.variables) except ValueError as error: diagnostics.append(self._diagnostic(plan.owner_path, "invalid-auxiliary-entrypoint-inventory", str(error))) return tuple(diagnostics) @@ -520,8 +522,14 @@ def _required_header_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDia """Require module headers to equal the completed handle-plan union.""" handles = tuple( handle - for namespace in plan.namespaces - for handle in self._namespace_native_array_handles(namespace) + for handle in ( + *(variable.native_array_handle for variable in plan.variables), + *( + handle + for namespace in plan.namespaces + for handle in self._namespace_native_array_handles(namespace) + ), + ) if handle is not None ) expected_headers = list(self._native_array_required_headers(handles)) @@ -581,7 +589,6 @@ def _namespace_native_array_handles( return ( *(argument.native_array_handle for function in namespace.functions for argument in function.arguments), *(result.native_array_handle for function in namespace.functions for result in function.results), - *(variable.native_array_handle for variable in namespace.variables), *(field.native_array_handle for derived in namespace.derived_types for field in derived.fields), ) @@ -853,6 +860,15 @@ def _class_graph_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnos seen.add(surface.type_identity) return tuple(diagnostics) + def _derived_type_identity_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Require each type to be defined once, where generated code reaches it.""" + counts = Counter(derived.type_identity for namespace in plan.namespaces for derived in namespace.derived_types) + return tuple( + self._diagnostic(plan.owner_path, "duplicate-derived-type-identity", identity) + for identity, count in counts.items() + if count > 1 + ) + # Derived-type definition, field, and module validation. def _derived_type_diagnostics(self, plan: NamespacePlan) -> tuple[WrapperPlanDiagnostic, ...]: """Validate namespace-owned opaque type and field identities.""" @@ -875,8 +891,7 @@ def _one_derived_type_diagnostics(self, derived) -> tuple[WrapperPlanDiagnostic, identity = ( (self._diagnostic(derived.owner_path, "incomplete-derived-type-identity", derived),) if ( - not derived.type_name - or not derived.native_type_name + not derived.native_type_name or not derived.native_scope or derived.type_identity != (derived.native_scope, derived.native_type_name) ) @@ -1022,7 +1037,7 @@ def _valid_nested_derived_field(field) -> bool: def _python_export_name_diagnostics(self, plan: NamespacePlan) -> tuple[WrapperPlanDiagnostic, ...]: """Return duplicate local export-name diagnostics.""" names = [function.binding.python_name for function in plan.functions] - names.extend(name for variable in plan.variables for name in variable.binding.python_names) + names.extend(name for publication in plan.variable_publications for name in publication.python_names) names.extend(name for derived in plan.derived_types for name in derived.python_names) names.extend(overload.python_name for overload in plan.overloads) return tuple( @@ -1040,14 +1055,6 @@ def _export_owner_diagnostics(self, plan: NamespacePlan) -> tuple[WrapperPlanDia diagnostics.append( self._diagnostic(function.owner_path, "inconsistent-function-export-owner", expected_owner) ) - for variable in plan.variables: - if not variable.binding.python_names: - continue - expected_owner = f"{plan.owner_path}.{variable.binding.python_names[0]}" - if variable.owner_path != expected_owner: - diagnostics.append( - self._diagnostic(variable.owner_path, "inconsistent-variable-export-owner", expected_owner) - ) for overload in plan.overloads: expected_owner = f"{plan.owner_path}.{overload.python_name}" if overload.owner_path != expected_owner: @@ -1056,16 +1063,60 @@ def _export_owner_diagnostics(self, plan: NamespacePlan) -> tuple[WrapperPlanDia ) return tuple(diagnostics) + def _module_variable_publication_diagnostics( + self, + plan: ModulePlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate that every publication references one canonical variable plan.""" + variable_ids = {id(variable) for variable in plan.variables} + diagnostics = [] + namespace_paths = {namespace.python_path for namespace in plan.namespaces} + diagnostics.extend( + self._diagnostic( + variable.owner_path, + "missing-module-variable-support-namespace", + variable.binding.support_namespace, + ) + for variable in plan.variables + if variable.binding.support_namespace not in namespace_paths + ) + for namespace in plan.namespaces: + for publication in namespace.variable_publications: + if id(publication.variable) not in variable_ids: + diagnostics.append( + self._diagnostic( + namespace.owner_path, + "missing-module-variable-publication-owner", + publication.variable.owner_path, + ) + ) + if not publication.python_names: + diagnostics.append( + self._diagnostic( + namespace.owner_path, + "empty-module-variable-publication", + publication.variable.owner_path, + ) + ) + return tuple(diagnostics) + def _generated_symbol_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnostic, ...]: """Reject missing or colliding C/Fortran symbol stems before lowering.""" owners_by_symbol: dict[str, list[str]] = {} diagnostics = list(self._namespace_symbol_diagnostics(plan)) for namespace in plan.namespaces: - for item in (*namespace.functions, *namespace.variables): + for item in namespace.functions: if not item.symbol_name or not item.symbol_name.isidentifier(): diagnostics.append(self._diagnostic(item.owner_path, "invalid-generated-symbol", item.symbol_name)) continue owners_by_symbol.setdefault(item.symbol_name.casefold(), []).append(item.owner_path) + for variable in plan.variables: + if not variable.symbol_name or not variable.symbol_name.isidentifier(): + diagnostics.append( + self._diagnostic(variable.owner_path, "invalid-generated-symbol", variable.symbol_name) + ) + continue + owners_by_symbol.setdefault(variable.symbol_name.casefold(), []).append(variable.owner_path) diagnostics.extend( self._diagnostic(plan.owner_path, "duplicate-generated-symbol", f"{symbol}:{','.join(owners)}") for symbol, owners in owners_by_symbol.items() @@ -1091,8 +1142,6 @@ def _module_variable_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Return getter, setter, and initialization consistency diagnostics.""" diagnostics = [] - if not plan.binding.python_names: - diagnostics.append(self._diagnostic(plan.owner_path, "missing-module-python-name", plan.owner_path)) diagnostics.extend(self._module_variable_entrypoint_diagnostics(plan)) diagnostics.extend(self._module_getter_diagnostics(plan)) if plan.binding.getter_action is ModuleGetterAction.DERIVED_OBJECT: @@ -1675,6 +1724,11 @@ def _expected_entrypoint_parameter_groups(plan: FunctionPlan) -> tuple[tuple[str for result in plan.results if result.array is not None and "bridge" in result.array.extent_evaluation ) + groups.extend( + (argument.owner_path, "argument_extent") + for argument in plan.arguments + if argument.array is not None and "bridge" in argument.array.extent_evaluation + ) return tuple(groups) def _entrypoint_parameter_name_diagnostics( @@ -1969,7 +2023,6 @@ def _argument_diagnostics( *self._optional_argument_diagnostics(plan), *self._argument_family_diagnostics(plan, available_roles), *self._argument_transformation_diagnostics(plan), - *self._array_writeback_abi_diagnostics(plan), *self._argument_data_action_diagnostics(plan), *self._bridge_data_diagnostics( plan.owner_path, @@ -1979,28 +2032,6 @@ def _argument_diagnostics( ] return tuple(diagnostics) - def _array_writeback_abi_diagnostics( - self, - plan: ArgumentTransferPlan, - ) -> tuple[WrapperPlanDiagnostic, ...]: - """Validate completed mutable-array normalization without selecting it.""" - expected = ArrayWritebackABI.NOT_APPLICABLE - if plan.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER and ( - plan.mutates_native or self._publishes_array_replacement(plan) - ): - # Every element type is written back the same way: a Boolean one - # already holds the zero or one its interoperable form requires. - expected = ArrayWritebackABI.NATIVE_ARRAY - if plan.array_writeback_abi is expected: - return () - return ( - self._diagnostic( - plan.owner_path, - "invalid-array-writeback-abi", - f"{plan.array_writeback_abi.value}; expected {expected.value}", - ), - ) - # Layer-owned representation transformation validation. def _argument_transformation_diagnostics( self, @@ -2204,9 +2235,11 @@ def _prototype_argument_matches_transfer( return ( argument.name, argument.semantic_type_name, + argument.native_fortran_type, argument.rank, argument.passed_by_value, argument.intent, + argument.optional, argument.character_length, WrapperGenerator._prototype_array_shape(argument.array), argument.derived_type_identity, @@ -2214,9 +2247,11 @@ def _prototype_argument_matches_transfer( ) == ( transfer.name, transfer.semantic_type_name, + transfer.native_fortran_type, transfer.rank, transfer.passed_by_value, transfer.intent, + transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER, transfer.character_length, WrapperGenerator._prototype_array_shape(transfer.array), transfer.derived_type_identity, @@ -2318,30 +2353,66 @@ def _callback_transfer_diagnostics( diagnostics = [] if not transfer.owner_path or not transfer.name: diagnostics.append(self._diagnostic(transfer.owner_path, "incomplete-callback-transfer", position)) + diagnostics.extend(self._callback_optionality_diagnostics(transfer, position)) diagnostics.extend(self._callback_array_role_diagnostics(transfer, position)) diagnostics.extend(self._callback_string_role_diagnostics(transfer, position)) diagnostics.extend(self._callback_derived_role_diagnostics(transfer, position)) diagnostics.extend(self._callback_scalar_projection_diagnostics(transfer, position)) return tuple(diagnostics) - def _callback_scalar_projection_diagnostics( + def _callback_optionality_diagnostics( self, transfer: CallbackTransferPlan, position: int, ) -> tuple[WrapperPlanDiagnostic, ...]: - """Require every primitive scalar callback transfer to use its value projection.""" - if position < 0 or transfer.object_kind is not ObjectKind.SCALAR or transfer.rank != 0: + """Reject a presence action that cannot use the transfer's completed ABI.""" + invalid = transfer.optionality is CallbackOptionalityAction.BLOCKED or ( + transfer.optionality is CallbackOptionalityAction.NULL_DATA_POINTER + and (transfer.abi is CallbackABIKind.VALUE or transfer.passed_by_value) + ) + if not invalid: return () - valid = ( - transfer.python_action is PythonBarrierAction.SCALAR_VALUE - and transfer.abi in {CallbackABIKind.VALUE, CallbackABIKind.REFERENCE} - and transfer.adapter_action - in { - CallbackTransferAction.COPY_IN, - CallbackTransferAction.COPY_OUT, - CallbackTransferAction.COPY_IN_OUT, - } + return ( + self._diagnostic( + transfer.owner_path, + "invalid-callback-optionality", + (position, transfer.optionality.value, transfer.abi.value), + ), ) + + def _callback_scalar_projection_diagnostics( + self, + transfer: CallbackTransferPlan, + position: int, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Require every primitive scalar callback transfer to use a completed projection. + + A rank-zero primitive dummy is projected either as an independent value + or, when the native caller reads it back, as rank-zero storage the + callable writes through. Any other pairing of projection, ABI and copy + direction means completed policy and the plan disagree. + """ + if position < 0 or transfer.rank != 0: + return () + copies = { + CallbackTransferAction.COPY_IN, + CallbackTransferAction.COPY_OUT, + CallbackTransferAction.COPY_IN_OUT, + } + if transfer.object_kind is ObjectKind.SCALAR: + valid = ( + transfer.python_action is PythonBarrierAction.SCALAR_VALUE + and transfer.abi in {CallbackABIKind.VALUE, CallbackABIKind.REFERENCE} + and transfer.adapter_action in copies + ) + elif transfer.object_kind is ObjectKind.NUMPY_ARRAY: + valid = ( + transfer.python_action is PythonBarrierAction.SCALAR_STORAGE + and transfer.abi is CallbackABIKind.REFERENCE + and transfer.adapter_action in copies + ) + else: + return () return ( () if valid @@ -2632,7 +2703,10 @@ def _argument_policy_consistency_diagnostics( adapter = slot.adapter if adapter is None: return (self._diagnostic(plan.owner_path, "missing-argument-adapter-facet", None),) - if plan.entrypoint.pass_callback_parameter: + expected_callback_parameter = bool( + plan.callback is not None and plan.entrypoint.optional_mode is OptionalMode.NULLABLE_VALUE + ) + if plan.entrypoint.pass_callback_parameter is not expected_callback_parameter: diagnostics.append( self._diagnostic( plan.owner_path, @@ -2766,16 +2840,6 @@ def _logical_argument_slot_diagnostics( slot.array_native_type, ) ) - if slot.array_copy_in != plan.array_copy_in: - diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-array-copy-in", slot.array_copy_in)) - if slot.array_copy_out != plan.array_copy_out: - diagnostics.append( - self._diagnostic( - plan.owner_path, - "inconsistent-array-copy-out", - slot.array_copy_out, - ) - ) return tuple(diagnostics) def _argument_slot_consistency_diagnostics( @@ -2818,8 +2882,6 @@ def _expected_argument_data_action(self, plan: ArgumentTransferPlan) -> BridgeDa """Return the data action implied by completed orthogonal selectors.""" if plan.callback is not None: return BridgeDataAction.DIRECT_TRANSFER - if plan.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: - return BridgeDataAction.COPY_REPRESENTATION if plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: return BridgeDataAction.COPY_REPRESENTATION if self._uses_typed_derived_value(plan): @@ -3540,12 +3602,7 @@ def _array_buffer_action_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-array-handoff-mode", plan.entrypoint.handoff_mode.value) ) - expected_data_action = ( - BridgeDataAction.COPY_REPRESENTATION - if plan.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY - else BridgeDataAction.ASSOCIATE_VIEW - ) - if plan.bridge.data_action is not expected_data_action: + if plan.bridge.data_action is not BridgeDataAction.ASSOCIATE_VIEW: diagnostics.append( self._diagnostic(plan.owner_path, "invalid-array-data-action", plan.bridge.data_action.value) ) @@ -5033,7 +5090,7 @@ def _array_extent_evaluation_is_consistent(array: ArrayHandoffPlan) -> bool: def _array_result_extent_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Reject unresolved ordinary array result extent spellings.""" array = plan.array - if array is not None and any(shape in {":", "::Strided", "...", "Flat"} for shape in array.shape): + if array is not None and any(shape in RUNTIME_DIMENSION_MARKERS for shape in array.shape): return (self._diagnostic(plan.owner_path, "unresolved-array-result-shape", array.shape),) return () diff --git a/prik/planning/__init__.py b/prik/planning/__init__.py index 445d25155..b2f5bf4dd 100644 --- a/prik/planning/__init__.py +++ b/prik/planning/__init__.py @@ -38,6 +38,7 @@ LifecycleActionPlan, ModulePlan, ModuleVariablePlan, + ModuleVariablePublicationPlan, NativeGeneratedCodeGroupKind, NativeGeneratedCodeGroupPlan, NativeEntrypointABIValueKind, @@ -93,6 +94,7 @@ "LifecycleActionPlan", "ModulePlan", "ModuleVariablePlan", + "ModuleVariablePublicationPlan", "NamespacePlan", "NativeArrayActualPlan", "NativeArrayDefaultHandlePlan", diff --git a/prik/planning/entrypoints.py b/prik/planning/entrypoints.py index 9a46916a8..4b66b114a 100644 --- a/prik/planning/entrypoints.py +++ b/prik/planning/entrypoints.py @@ -89,9 +89,10 @@ class GeneratedSupportProcedureProjection: def build_generated_support_procedure_projection( namespaces: tuple[NamespacePlan, ...], + variables: tuple[ModuleVariablePlan, ...], ) -> GeneratedSupportProcedureProjection: """Return external and backend-local support membership in stable order.""" - builder = _GeneratedSupportProcedureEntrypointBuilder(namespaces) + builder = _GeneratedSupportProcedureEntrypointBuilder(namespaces, variables) projection = builder.build() procedures = projection.support_procedures keys = [procedure.key for procedure in procedures] @@ -112,7 +113,7 @@ def build_callback_support_procedure_entrypoint( result, ) -> GeneratedSupportProcedureEntrypointPlan: """Project the binding trampoline once while its callback site is planned.""" - builder = _GeneratedSupportProcedureEntrypointBuilder(()) + builder = _GeneratedSupportProcedureEntrypointBuilder((), ()) parameters = tuple( parameter for transfer in arguments for parameter in builder._callback_transfer_parameters(transfer) ) @@ -129,12 +130,25 @@ def build_callback_support_procedure_entrypoint( class _GeneratedSupportProcedureEntrypointBuilder: """Project operation existence, symbols, and ABI signatures from completed plans.""" - def __init__(self, namespaces: tuple[NamespacePlan, ...]) -> None: + def __init__( + self, + namespaces: tuple[NamespacePlan, ...], + variables: tuple[ModuleVariablePlan, ...], + ) -> None: self.namespaces = namespaces self.functions = tuple(function for namespace in namespaces for function in namespace.functions) - self.variables = tuple(variable for namespace in namespaces for variable in namespace.variables) - self.derived_types = tuple(derived for namespace in namespaces for derived in namespace.derived_types) - self.classes = tuple(surface for namespace in namespaces for surface in namespace.classes) + self.variables = variables + # One native type may be exported through several Python namespaces. + # Its support procedures belong to the native type, not each export. + derived_by_identity = {} + classes_by_identity = {} + for namespace in namespaces: + for derived in namespace.derived_types: + derived_by_identity.setdefault(derived.type_identity, derived) + for surface in namespace.classes: + classes_by_identity.setdefault(surface.type_identity, surface) + self.derived_types = tuple(derived_by_identity.values()) + self.classes = tuple(classes_by_identity.values()) def build(self) -> GeneratedSupportProcedureProjection: """Collect external and binding-local support in declaration order.""" @@ -406,7 +420,7 @@ def _class_constructor_operations(self) -> tuple[GeneratedSupportProcedureEntryp self._operation( surface.owner_path, "class:create", - f"bind_c_prik_create_{surface.type_identity[1].casefold()}", + f"bind_c_prik_create_{surface.backend_symbol.casefold()}", result=self._opaque_result(), ) for surface in self.classes @@ -483,6 +497,13 @@ def _allocatable_holder_types(self) -> tuple[DerivedTypePlan, ...]: if case.action is not DerivedCallAction.INCOMPATIBLE ) ) + identities.update( + variable.derived.handoff.type_identity + for variable in self.variables + if variable.derived is not None + and variable.derived.handoff.storage + in {DerivedObjectStorage.MODULE_ALLOCATABLE, DerivedObjectStorage.MODULE_ALLOCATABLE_TARGET} + ) return tuple(derived for derived in self.derived_types if derived.type_identity in identities) def _pointer_holder_types(self) -> tuple[DerivedTypePlan, ...]: @@ -504,6 +525,11 @@ def _pointer_holder_types(self) -> tuple[DerivedTypePlan, ...]: if case.action is not DerivedCallAction.INCOMPATIBLE ) ) + identities.update( + variable.derived.handoff.type_identity + for variable in self.variables + if variable.derived is not None and variable.derived.handoff.storage is DerivedObjectStorage.MODULE_POINTER + ) return tuple(derived for derived in self.derived_types if derived.type_identity in identities) def _allocatable_holder_field_types(self) -> tuple[DerivedTypePlan, ...]: diff --git a/prik/planning/models.py b/prik/planning/models.py index dbce5c7c0..b3fceeda5 100644 --- a/prik/planning/models.py +++ b/prik/planning/models.py @@ -36,12 +36,12 @@ ArrayLogicalABI, ArrayEntrypointABI, ArrayPythonLayout, - ArrayWritebackABI, BridgeDataAction, CallbackABIKind, CallbackFatalAction, CallbackGILAction, CallbackLifecycleAction, + CallbackOptionalityAction, CallbackResultAction, CallbackThreadAction, CallbackTransferAction, @@ -332,18 +332,39 @@ class DerivedTypePlan(StageRecord): The planner supplies identity, native naming, fields, and abstractness; generated class assembly uses this record as the authoritative type shape. + + A type exists whether or not it is published: a published signature may + take or return one. ``python_names`` are the names this namespace binds it + under, possibly none; ``contract_name`` is what the contract calls it; and + ``nested_in`` names the class it is bound on instead of a namespace. + + A type is defined in one namespace only. Code taking or returning it can + live in any namespace, so generated code reaches the class and its helpers + in the namespace defining it rather than in its own. """ owner_path: str - type_name: str type_identity: tuple[str, str] backend_symbol: str native_type_name: str native_scope: str python_names: tuple[str, ...] + contract_name: str fields: tuple[DerivedFieldPlan, ...] bind_c: bool abstract: bool = False + nested_in: tuple[str, str] | None = None + + @property + def definition_name(self) -> str: + """Return the name generated code defines and reaches this type by here. + + A bound type is defined under the first name it is bound as. A type + bound under no public name is still defined -- generated code has to + reach the class to wrap a returned instance, subclass it, or check an + argument -- so it takes a private name no contract publishes. + """ + return self.python_names[0] if self.python_names else f"_prik_type_{self.backend_symbol}" @dataclass @@ -442,6 +463,7 @@ class ClassSurfacePlan(StageRecord): owner_path: str type_identity: tuple[str, str] + backend_symbol: str python_names: tuple[str, ...] base_identities: tuple[tuple[str, str], ...] constructor: ConstructorPlan @@ -741,13 +763,9 @@ class BridgeModulePlan(StageRecord): @dataclass class BindingModuleVariablePlan(StageRecord): - """Describe Python module-attribute access and initialization for one value. + """Describe Python access and initialization for one native module value.""" - ``python_names`` retains every public spelling. The binding consumes the - completed getter and setter actions plus the selected initializer/value. - """ - - python_names: tuple[str, ...] + support_namespace: tuple[str, ...] getter_action: ModuleGetterAction setter_action: SetterAction initializer: Any @@ -783,7 +801,8 @@ class ModuleVariablePlan(StageRecord): """Join binding, entrypoint, and bridge views of one module-state value. Optional array, native-handle, and derived-object facets are attached only - when policy selected them. Namespace plans own these records for emission. + when policy selected them. ``ModulePlan`` owns these records by declaring + native identity; namespace plans contain publications only. """ owner_path: str @@ -804,6 +823,19 @@ class ModuleVariablePlan(StageRecord): array_address: ModuleArrayAddressMechanism | None = None +@dataclass +class ModuleVariablePublicationPlan(StageRecord): + """Publish one existing module-variable plan in a Python namespace. + + ``variable`` is the sole plan that owns native access, storage, + initialization, and support procedures. This record adds only Python names + in one namespace; it never creates another variable plan. + """ + + variable: ModuleVariablePlan + python_names: tuple[str, ...] + + @dataclass class BindingFunctionPlan(StageRecord): """Store Python-visible call behavior for one generated binding function. @@ -824,18 +856,29 @@ class BindingFunctionPlan(StageRecord): accepts_keyword_arguments: bool = True +@dataclass +class NativeEntrypointExtentPlan(StageRecord): + """One extent only the bridge can evaluate, and the C-ABI output carrying it.""" + + axis: int + parameter_name: str + + @dataclass class NativeEntrypointParameterPlan(StageRecord): """Order one argument or result parameter group in the shared C ABI. The referenced argument or result entrypoint facet owns the group's exact transport. ``position`` orders groups after any direct function return. + ``extents`` are the outputs an extent group carries, one per axis a + specification function sizes, so neither backend enumerates them again. """ owner_path: str position: int source_kind: str native_position: int | None = None + extents: tuple[NativeEntrypointExtentPlan, ...] = () @dataclass @@ -853,6 +896,15 @@ class NativeEntrypointFunctionPlan(StageRecord): # declaration of ``symbol_name`` cannot collide with a header declaration. collision_adapter_symbol: str | None = None + def extent_names(self, owner_path: str) -> dict[int, str]: + """Return the output name of each bridge-evaluated axis one owner has.""" + return { + extent.axis: extent.parameter_name + for parameter in self.parameters + if parameter.owner_path == owner_path + for extent in parameter.extents + } + @dataclass class BridgeFunctionPlan(StageRecord): @@ -1039,8 +1091,6 @@ class NativeEntrypointProjectedSlotPlan(StageRecord): scalar_native_type: str | None = None array_logical_abi: ArrayLogicalABI = ArrayLogicalABI.NOT_APPLICABLE array_native_type: str | None = None - array_copy_in: bool = False - array_copy_out: bool = False literal_type: str | None = None literal_value: Any = None result_position: int | None = None @@ -1063,7 +1113,6 @@ class PolymorphicVariantPlan(StageRecord): type_identity: tuple[str, str] backend_symbol: str - python_name: str abi_code: int @@ -1086,9 +1135,11 @@ class ProcedurePrototypeArgumentPlan(StageRecord): owner_path: str name: str semantic_type_name: str + native_fortran_type: str | None rank: int passed_by_value: bool intent: str | None + optional: bool character_length: int | None array: ArrayHandoffPlan | None derived_type_identity: tuple[str, str] | None @@ -1136,10 +1187,12 @@ class CallbackTransferPlan(StageRecord): owner_path: str name: str semantic_type_name: str + native_fortran_type: str | None object_kind: ObjectKind rank: int passed_by_value: bool intent: str | None + optionality: CallbackOptionalityAction abi: CallbackABIKind adapter_action: CallbackTransferAction python_action: PythonBarrierAction @@ -1231,9 +1284,6 @@ class ArgumentTransferPlan(StageRecord): scalar_native_type: str | None array_logical_abi: ArrayLogicalABI array_native_type: str | None - array_copy_in: bool - array_copy_out: bool - array_writeback_abi: ArrayWritebackABI object_kind: ObjectKind ownership_owner: OwnershipOwner transfer_mode: TransferMode @@ -1387,22 +1437,37 @@ class DeclarationCallablePlan(StageRecord): prototype: ProcedurePrototypePlan | None = None +@dataclass +class NamespaceAliasPlan(StageRecord): + """Bind one name in a namespace to a callable another namespace owns. + + A re-export publishes an existing declaration rather than adding one, so + the alias names where the callable lives instead of repeating its plan. + """ + + python_name: str + source_namespace: tuple[str, ...] + source_name: str + + @dataclass class NamespacePlan(StageRecord): """Represent one Python namespace and its directly exported wrapper owners. ``python_path`` identifies the root or child module path; contained tuples - preserve planner order for functions, variables, types, classes, and - overloads. ``ModulePlan`` groups these namespaces into one generation unit. + preserve planner order for functions, variable publications, types, + classes, and overloads. ``ModulePlan`` groups these namespaces into one + generation unit. """ owner_path: str python_path: tuple[str, ...] functions: tuple[FunctionPlan, ...] = () - variables: tuple[ModuleVariablePlan, ...] = () + variable_publications: tuple[ModuleVariablePublicationPlan, ...] = () derived_types: tuple[DerivedTypePlan, ...] = () classes: tuple[ClassSurfacePlan, ...] = () overloads: tuple[OverloadPlan, ...] = () + aliases: tuple[NamespaceAliasPlan, ...] = () docstring: str | None = None @@ -1420,6 +1485,7 @@ class ModulePlan(StageRecord): binding: BindingModulePlan entrypoint: NativeEntrypointModulePlan bridge: BridgeModulePlan | None + variables: tuple[ModuleVariablePlan, ...] namespaces: tuple[NamespacePlan, ...] native_generated_code_groups: tuple[NativeGeneratedCodeGroupPlan, ...] = () required_headers: tuple[str, ...] = () @@ -1478,6 +1544,7 @@ class WrapperPlanDiagnostic(StageRecord): binding=BindingModulePlan(owner_path="demo"), entrypoint=NativeEntrypointModulePlan(owner_path="demo"), bridge=BridgeModulePlan(owner_path="demo"), + variables=(), namespaces=(NamespacePlan(owner_path="demo", python_path=(), functions=(function,)),), ) diff --git a/prik/planning/planner.py b/prik/planning/planner.py index ab41dac7a..dfb6bc8df 100644 --- a/prik/planning/planner.py +++ b/prik/planning/planner.py @@ -117,10 +117,12 @@ LifecycleActionPlan, ModulePlan, ModuleVariablePlan, + ModuleVariablePublicationPlan, NativeGeneratedCodeGroupKind, NativeGeneratedCodeGroupPlan, GeneratedSupportProcedureImplementationOwner, NativeEntrypointArgumentPlan, + NativeEntrypointExtentPlan, NativeEntrypointCallbackPlan, NativeEntrypointFunctionPlan, DirectCABIPlan, @@ -130,6 +132,7 @@ NativeEntrypointParameterPlan, NativeEntrypointProjectedSlotPlan, NativeEntrypointResultPlan, + NamespaceAliasPlan, NamespacePlan, NativeArrayActualPlan, NativeArrayDefaultHandlePlan, @@ -154,6 +157,9 @@ build_generated_support_procedure_projection, ) +# Re-export reaches Python only where the published name is one exported object. +_ALIASABLE_REEXPORT_KINDS = frozenset({"procedure", "derived_type"}) + _DATATYPE_FAMILIES = { **dict.fromkeys(BOOLEAN_SEMANTIC_TYPE_NAMES, DatatypeFamily.BOOL), @@ -231,6 +237,16 @@ def from_semantic_class(cls, semantic_class: models.SemanticClass) -> _ClassPoli ) +@dataclass(frozen=True) +class _TypePlacement: + """One namespace a type is defined in, the names binding it, and its parent.""" + + entry: _ClassPolicyEntry + namespace: tuple[str, ...] + python_names: tuple[str, ...] + nested_in: tuple[str, str] | None + + @dataclass(frozen=True) class _ClassPolicyCatalog: """Organize completed class policies for one wrapper-planning operation. @@ -362,17 +378,25 @@ def _visit_SemanticModule(self, module: models.SemanticModule) -> ModulePlan: # Initialize every class-backed index from one complete ordered collection. semantic_classes = _ClassPolicyCatalog.ordered_semantic_classes(module.classes) class_policies = _ClassPolicyCatalog.from_semantic_classes(semantic_classes) - self._derived_type_names = {semantic_class.name for semantic_class in semantic_classes} self._derived_field_plans: dict[str, DerivedFieldPlan] = {} self._complete_derived_backend_symbols(semantic_classes) # Project every public surface before linking private callable entries. - functions, variables, derived_types, classes, overloads = self._namespace_member_plans( + functions, variables, variable_publications, derived_types, classes, overloads = self._namespace_member_plans( module, class_policies, ) + aliases = self._aliases_by_namespace(module) if not any( - (*functions.values(), *variables.values(), *derived_types.values(), *classes.values(), *overloads.values()) + ( + *functions.values(), + variables, + *variable_publications.values(), + *derived_types.values(), + *classes.values(), + *overloads.values(), + *aliases.values(), + ) ): raise ValueError(f"Semantic module {module.name!r} has no public wrapper exports") @@ -381,8 +405,17 @@ def _visit_SemanticModule(self, module: models.SemanticModule) -> ModulePlan: self._attach_overload_functions(functions, overloads) # Complete stable namespace paths, generated symbols, and required headers. - namespaces = self._namespace_plans(module.name, functions, variables, derived_types, classes, overloads) - support_projection = build_generated_support_procedure_projection(namespaces) + namespaces = self._namespace_plans( + module.name, + functions, + variables, + variable_publications, + derived_types, + classes, + overloads, + aliases, + ) + support_projection = build_generated_support_procedure_projection(namespaces, variables) support_procedures = support_projection.support_procedures generated_code_groups = self._native_generated_code_groups( module.name, @@ -413,9 +446,10 @@ def _visit_SemanticModule(self, module: models.SemanticModule) -> ModulePlan: if generated_code_groups else None ), + variables=variables, namespaces=namespaces, native_generated_code_groups=generated_code_groups, - required_headers=self._required_headers(namespaces), + required_headers=self._required_headers(namespaces, variables), ) @staticmethod @@ -468,23 +502,25 @@ def _namespace_member_plans( self, module: models.SemanticModule, class_policies: _ClassPolicyCatalog, - ) -> tuple[dict, dict, dict, dict, dict]: + ) -> tuple[dict, dict, dict, dict, dict, dict]: """Build namespace-owned plan maps from one shared class-policy catalog. - Direct functions and variables are projected first. The local catalog + Direct functions and canonical variables are projected first. The local catalog then organizes each public class once so derived-type and Python-class projections consume the same semantic declaration, completed policies, and callable owner-path maps. """ # Project ordinary module members independently from class-owned surfaces. functions = self._functions_by_namespace(module) - variables = self._variables_by_namespace(module) + variables, variable_publications = self._module_variables_and_publications(module) + placements = self._type_placements(class_policies) return ( functions, variables, - self._derived_types_by_namespace(class_policies), - self._classes_by_namespace(module.name, class_policies), + variable_publications, + self._derived_types_by_namespace(placements), + self._classes_by_namespace(module.name, placements), self._module_overloads_by_namespace(module), ) @@ -505,46 +541,174 @@ def _namespace_plans( self, module_name: str, functions: dict, - variables: dict, + variables: tuple[ModuleVariablePlan, ...], + variable_publications: dict, derived_types: dict, classes: dict, overloads: dict, + aliases: dict, ) -> tuple[NamespacePlan, ...]: """Freeze linked namespace members in dependency-safe path order.""" self._complete_generated_symbols(functions, variables) - namespace_paths = self._namespace_paths((*functions, *variables, *derived_types, *classes, *overloads)) - return tuple( + namespace_paths = self._namespace_paths( + (*functions, *variable_publications, *derived_types, *classes, *overloads, *aliases) + ) + namespaces = tuple( self._namespace_plan( module_name, path, tuple(functions[path]), - tuple(variables[path]), + tuple(variable_publications[path]), tuple(derived_types[path]), tuple(classes[path]), tuple(overloads[path]), + tuple(aliases[path]), ) for path in namespace_paths ) + self._complete_variable_support_namespaces(variables, namespaces) + return self._bases_first(namespaces) + + @staticmethod + def _bases_first(namespaces: tuple[NamespacePlan, ...]) -> tuple[NamespacePlan, ...]: + """Order namespaces so each comes after those defining the bases it extends. + + A namespace's classes are created when it is set up, and a class + extending one another module declares needs that base to exist. Path + order is kept wherever inheritance does not decide; namespaces whose + classes extend each other's are left in path order for validation to + reject. + """ + defined_in = { + surface.type_identity: namespace.python_path for namespace in namespaces for surface in namespace.classes + } + needs = { + namespace.python_path: { + defined_in[base] + for surface in namespace.classes + for base in surface.base_identities + if base in defined_in + } + - {namespace.python_path} + for namespace in namespaces + } + ordered: list[NamespacePlan] = [] + remaining = list(namespaces) + while remaining: + placed = {namespace.python_path for namespace in ordered} + ready = next((namespace for namespace in remaining if needs[namespace.python_path] <= placed), None) + if ready is None: + return (*ordered, *remaining) + ordered.append(ready) + remaining.remove(ready) + return tuple(ordered) + + @staticmethod + def _complete_variable_support_namespaces( + variables: tuple[ModuleVariablePlan, ...], + namespaces: tuple[NamespacePlan, ...], + ) -> None: + """Place a derived variable's private helpers beside its type's own. + + They wrap the variable with that type's class and extend its operation + map, so they live in the one namespace defining the type. Canonical + ownership does not move. + """ + defined_in = { + derived.type_identity: namespace.python_path + for namespace in namespaces + for derived in namespace.derived_types + } + for variable in variables: + variable.binding.support_namespace = ( + () if variable.derived is None else defined_in.get(variable.derived.handoff.type_identity, ()) + ) + + def _aliases_by_namespace(self, module: models.SemanticModule) -> dict[tuple[str, ...], list[NamespaceAliasPlan]]: + """Group each published re-export under the namespace that publishes it. + + An alias binds one Python object already exported elsewhere, so it is + planned only where the published name reaches Python as exactly that. + The declaration it names supplies the attribute to read, because a + Fortran spelling is not a Python attribute and only the completed export + knows which name the declaring namespace actually bound. The alias + publishes under the name export policy completed for it, inside the same + ledger as this module's declarations, so it cannot take one of theirs. + """ + grouped = defaultdict(list) + for reexport in module.reexports: + if not reexport.publishes_to_python(): + continue + if reexport.entity_kind not in _ALIASABLE_REEXPORT_KINDS: + continue + source_namespace = tuple(part.casefold() for part in reexport.origin_module.split(".") if part) + source_name = self._exported_declaration_name(module, source_namespace, reexport.source_name) + if source_name is None: + continue + grouped[tuple(part.casefold() for part in reexport.module.split(".") if part)].append( + NamespaceAliasPlan( + python_name=reexport.python_name or str(reexport.local_name), + source_namespace=source_namespace, + source_name=source_name, + ) + ) + return grouped + + @staticmethod + def _exported_declaration_name( + module: models.SemanticModule, + namespace: tuple[str, ...], + source_name: str, + ) -> str | None: + """Return the Python name one namespace bound for a re-exported entity. + + A record reaching here states the entity either the way its source + declares it or the way its own contract already published it, so both + spellings identify the declaration. Finding none means the namespace + exports no such object and there is nothing an alias could bind. + """ + wanted = source_name.casefold() + published: str | None = None + for declaration in (*module.functions, *module.classes): + if getattr(declaration, "visibility", "public") != "public": + continue + native = str(getattr(declaration, "native_name", "") or declaration.name).casefold() + exports = declaration.metadata.get(models.PYTHON_EXPORTS_METADATA) or () + for export in exports: + name = export.get("name") + if not name or tuple(export.get("namespace") or ()) != namespace: + continue + # A source spelling identifies the declaration itself, while a + # published one identifies what a namespace called it. Only a + # collision makes the two name different declarations, and then + # the source spelling is the one that came from Fortran. + if native == wanted: + return str(name) + if str(name).casefold() == wanted: + published = published or str(name) + return published def _namespace_plan( self, module_name: str, path: tuple[str, ...], functions: tuple[FunctionPlan, ...], - variables: tuple[ModuleVariablePlan, ...], + variable_publications: tuple[ModuleVariablePublicationPlan, ...], derived_types: tuple[DerivedTypePlan, ...], classes: tuple[ClassSurfacePlan, ...], overloads: tuple[OverloadPlan, ...], + aliases: tuple[NamespaceAliasPlan, ...] = (), ) -> NamespacePlan: """Create one namespace after its generated symbols are complete.""" return NamespacePlan( owner_path=self._namespace_owner_path(module_name, path), python_path=path, functions=functions, - variables=variables, + variable_publications=variable_publications, derived_types=derived_types, classes=classes, overloads=overloads, + aliases=aliases, ) def _complete_derived_backend_symbols( @@ -557,12 +721,6 @@ def _complete_derived_backend_symbols( self._derived_backend_symbols = { policy.type_identity: self._derived_backend_symbol_for_policy(policy, counts) for policy in policies } - self._class_python_names = self._completed_class_python_names(policies) - - @staticmethod - def _completed_class_python_names(policies: tuple[DerivedTypePolicy, ...]) -> dict[tuple[str, str], str]: - """Index the primary completed Python export for each native type.""" - return {policy.type_identity: policy.python_names[0] for policy in policies if policy.python_names} @staticmethod def _derived_backend_symbol_for_policy(policy: DerivedTypePolicy, counts: Counter) -> str: @@ -586,38 +744,72 @@ def _derived_backend_symbol(self, type_identity: tuple[str, str]) -> str: # Derived-type definitions, fields, and class surfaces. def _derived_types_by_namespace( self, - class_policies: _ClassPolicyCatalog, + placements: tuple[_TypePlacement, ...], ) -> dict[tuple[str, ...], list[DerivedTypePlan]]: """Project opaque types from completed class and field policies.""" grouped = defaultdict(list) - for entry in class_policies.entries: - policy = entry.derived_policy - surface = entry.surface_policy - exports_by_namespace = defaultdict(list) - for export in policy.python_exports: - exports_by_namespace[export.namespace].append(export.name) - for namespace, python_names in exports_by_namespace.items(): - grouped[namespace].append( - self._derived_type_plan( - policy, - tuple(python_names), - fields=surface.effective_fields, - ) + for placement in placements: + entry = placement.entry + grouped[placement.namespace].append( + self._derived_type_plan( + entry.derived_policy, + placement.python_names, + fields=entry.surface_policy.effective_fields, + contract_name=models.completed_contract_name(entry.semantic_class), + nested_in=placement.nested_in, ) + ) return grouped + @staticmethod + def _type_placements(class_policies: _ClassPolicyCatalog) -> tuple[_TypePlacement, ...]: + """Return each namespace a type is defined in, and the names bound there. + + A type is defined in the namespace that publishes it, under the names + it is published as. Generated code reaches a type in the one namespace + defining it, so a plan defining it in two is rejected. A type that + publishes nowhere still exists -- a + published signature may take or return one -- so it is defined once + without a public name: beside its parent class, which binds it, when it + is nested, and at the root otherwise. + """ + parents = {id(child): entry for entry in class_policies.entries for child in entry.semantic_class.classes} + homes: dict[int, tuple[tuple[str, ...], ...]] = {} + placements: list[_TypePlacement] = [] + # Entries arrive parents first, so a nested type finds its parent's home. + for entry in class_policies.entries: + published: dict[tuple[str, ...], list[str]] = defaultdict(list) + for export in entry.derived_policy.python_exports: + published[export.namespace].append(export.name) + parent = parents.get(id(entry.semantic_class)) + if published: + found = tuple( + _TypePlacement(entry, namespace, tuple(names), None) for namespace, names in published.items() + ) + elif parent is not None: + found = tuple( + _TypePlacement(entry, namespace, (), parent.derived_policy.type_identity) + for namespace in homes[id(parent.semantic_class)] + ) + else: + found = (_TypePlacement(entry, (), (), None),) + homes[id(entry.semantic_class)] = tuple(item.namespace for item in found) + placements.extend(found) + return tuple(placements) + def _derived_type_plan( self, policy: DerivedTypePolicy, python_names: tuple[str, ...], *, fields: tuple[DerivedFieldPolicy, ...] | None = None, + contract_name: str, + nested_in: tuple[str, str] | None, ) -> DerivedTypePlan: """Mechanically project one completed derived type and its public fields.""" planned_fields = tuple(self._derived_field_plan(field) for field in (fields or policy.fields)) return DerivedTypePlan( owner_path=policy.owner_path, - type_name=policy.type_name, type_identity=policy.type_identity, backend_symbol=self._derived_backend_symbol(policy.type_identity), native_type_name=policy.native_type_name, @@ -626,30 +818,27 @@ def _derived_type_plan( fields=planned_fields, bind_c=policy.bind_c, abstract=policy.abstract, + contract_name=contract_name, + nested_in=nested_in, ) # Generated class surfaces compose Phase 8 types and ordinary function plans. def _classes_by_namespace( self, module_name: str, - class_policies: _ClassPolicyCatalog, + placements: tuple[_TypePlacement, ...], ) -> dict[tuple[str, ...], list[ClassSurfacePlan]]: - """Project completed class surfaces into their public namespaces.""" + """Project each class surface beside the type it is defined with.""" grouped = defaultdict(list) - for entry in class_policies.entries: - policy = entry.surface_policy - exports_by_namespace = defaultdict(list) - for export in policy.python_exports: - exports_by_namespace[export.namespace].append(export.name) - for namespace, python_names in exports_by_namespace.items(): - grouped[namespace].append( - self._class_surface_plan( - module_name, - namespace, - entry, - tuple(python_names), - ) + for placement in placements: + grouped[placement.namespace].append( + self._class_surface_plan( + module_name, + placement.namespace, + placement.entry, + placement.python_names, ) + ) return grouped def _class_surface_plan( @@ -678,6 +867,7 @@ def _class_surface_plan( return ClassSurfacePlan( owner_path=policy.owner_path, type_identity=policy.type_identity, + backend_symbol=self._derived_backend_symbol(policy.type_identity), python_names=python_names, base_identities=policy.base_identities, constructor=constructor, @@ -1041,12 +1231,16 @@ def _module_function_policy(function: models.SemanticFunction) -> FunctionWrappe return None return completed_function_wrapper_policy(function) - def _variables_by_namespace( + def _module_variables_and_publications( self, module: models.SemanticModule, - ) -> dict[tuple[str, ...], list[ModuleVariablePlan]]: - """Group exported module-variable plans by completed Python namespace.""" - variables = defaultdict(list) + ) -> tuple[ + tuple[ModuleVariablePlan, ...], + dict[tuple[str, ...], list[ModuleVariablePublicationPlan]], + ]: + """Build the canonical native-variable registry and namespace publications.""" + variables = [] + publications = defaultdict(list) for variable in module.variables: if variable.visibility != "public": continue @@ -1054,26 +1248,54 @@ def _variables_by_namespace( exports_by_namespace = defaultdict(list) for export in policy.python_exports: exports_by_namespace[export.namespace].append(export.name) + plan = self._module_variable_plan(policy) + variables.append(plan) for namespace, python_names in exports_by_namespace.items(): - variables[namespace].append( - self._module_variable_plan(policy, namespace, tuple(python_names), module.name) + publications[namespace].append( + ModuleVariablePublicationPlan( + variable=plan, + python_names=tuple(python_names), + ) ) - return variables + return tuple(variables), publications def _complete_generated_symbols( self, functions: dict[tuple[str, ...], list[FunctionPlan]], - variables: dict[tuple[str, ...], list[ModuleVariablePlan]], + variables: tuple[ModuleVariablePlan, ...], ) -> None: - """Keep unique symbols short and qualify only colliding local names.""" - entries = (*self._planned_items(functions), *self._planned_items(variables)) + """Keep unique symbols short and qualify from stable native identity.""" + variable_entries = tuple((self._variable_native_namespace(item), item) for item in variables) + entries = (*self._planned_items(functions), *variable_entries) counts = Counter(item.symbol_name.casefold() for _namespace, item in entries) for namespace, item in entries: if counts[item.symbol_name.casefold()] > 1: item.symbol_name = self._symbol_name(namespace, item.symbol_name) + self._separate_folded_generated_symbols(entries) self._qualify_variable_bridge_collisions(functions, variables) self._complete_entrypoint_symbols(functions) + @staticmethod + def _separate_folded_generated_symbols(entries: tuple[tuple[tuple[str, ...], object], ...]) -> None: + """Separate stems that only a case-sensitive source keeps apart. + + A generated symbol is shared with Fortran, which folds case, so two + declarations a case-sensitive language distinguishes by spelling alone + reach one stem that qualifying by namespace cannot separate. They + publish different Python names, so the stems are numbered in plan order. + """ + taken: set[str] = set() + for _namespace, item in entries: + stem = item.symbol_name + if stem.casefold() not in taken: + taken.add(stem.casefold()) + continue + suffix = 2 + while f"{stem}_{suffix}".casefold() in taken: + suffix += 1 + item.symbol_name = f"{stem}_{suffix}" + taken.add(item.symbol_name.casefold()) + @staticmethod def _complete_entrypoint_symbols( functions: dict[tuple[str, ...], list[FunctionPlan]], @@ -1087,24 +1309,22 @@ def _complete_entrypoint_symbols( def _qualify_variable_bridge_collisions( self, functions: dict[tuple[str, ...], list[FunctionPlan]], - variables: dict[tuple[str, ...], list[ModuleVariablePlan]], - ) -> None: - """Qualify a variable helper when its get/set spelling collides with a function.""" - for namespace, namespace_variables in variables.items(): - function_symbols = {function.symbol_name for function in functions[namespace]} - self._qualify_namespace_variable_helpers(namespace, namespace_variables, function_symbols) - - def _qualify_namespace_variable_helpers( - self, - namespace: tuple[str, ...], - variables: list[ModuleVariablePlan], - function_symbols: set[str], + variables: tuple[ModuleVariablePlan, ...], ) -> None: - """Resolve get/set helper collisions inside one Python namespace.""" + """Qualify a native variable helper when it collides with any function.""" + function_symbols = {function.symbol_name for items in functions.values() for function in items} for variable in variables: helper_symbols = {f"get_{variable.symbol_name}", f"set_{variable.symbol_name}"} if function_symbols & helper_symbols: - variable.symbol_name = self._symbol_name(namespace, variable.symbol_name) + variable.symbol_name = self._symbol_name( + self._variable_native_namespace(variable), + variable.symbol_name, + ) + + @staticmethod + def _variable_native_namespace(variable: ModuleVariablePlan) -> tuple[str, ...]: + """Return the declaring native path used only for generated-name qualification.""" + return tuple(part.casefold() for part in variable.owner_path.rsplit(".", 1)[0].split(".") if part) def _planned_items(self, grouped: dict[tuple[str, ...], list]) -> tuple[tuple[tuple[str, ...], object], ...]: """Flatten namespace groups while retaining each item's namespace.""" @@ -1113,23 +1333,18 @@ def _planned_items(self, grouped: dict[tuple[str, ...], list]) -> tuple[tuple[tu def _module_variable_plan( self, policy: ModuleVariablePolicy, - namespace: tuple[str, ...], - python_names: tuple[str, ...], - module_name: str, ) -> ModuleVariablePlan: """Project one completed module-variable policy into its shared plan record. - ``policy`` supplies all accessor, setter, descriptor, and derived - object decisions. ``namespace`` and ``python_names`` select the - exported owner path and binding aliases. The result shares array and - derived-field projections with the rest of the module; no accessor or - ownership policy is selected here. + ``policy`` supplies the declaring native identity plus all accessor, + setter, descriptor, and derived-object decisions. Publications are + projected separately and cannot change this record's owner path. """ # Roles are present only where the completed accessor policy requires them. getter_role = self._module_getter_role(policy) setter_role = f"{policy.owner_path}:setter" if policy.setter_action is SetterAction.WRITE_THROUGH else None return ModuleVariablePlan( - owner_path=self._export_owner_path(module_name, namespace, python_names[0]), + owner_path=policy.owner_path, symbol_name=policy.native_name.casefold(), semantic_type_name=policy.semantic_type_name, datatype_family=self._transfer_datatype_family( @@ -1137,7 +1352,7 @@ def _module_variable_plan( policy.derived.handoff if policy.derived is not None else None, ), binding=BindingModuleVariablePlan( - python_names=python_names, + support_namespace=(), getter_action=policy.getter_action, setter_action=policy.setter_action, initializer=policy.initializer, @@ -1355,21 +1570,52 @@ def _entrypoint_parameter_plans( ) ) ) - groups.extend( - (result.owner_path, "declaration_extent", None) - for result in results - if result.array is not None and "bridge" in result.array.extent_evaluation - ) + extents = WrapperPlanner._entrypoint_extent_groups(arguments, results) + groups.extend((owner, kind, None) for owner, kind in extents) return tuple( NativeEntrypointParameterPlan( owner_path=owner, position=position, source_kind=source_kind, native_position=native_position, + extents=extents.get((owner, source_kind), ()), ) for position, (owner, source_kind, native_position) in enumerate(groups) ) + @staticmethod + def _entrypoint_extent_groups( + arguments: tuple[ArgumentTransferPlan, ...], + results: tuple[NativeEntrypointResultPlan, ...], + ) -> dict[tuple[str, str], tuple[NativeEntrypointExtentPlan, ...]]: + """Return the extents each owner's group hands back, keyed by owner and group kind. + + Only the bridge can evaluate a specification function, so it hands back + each extent one sizes: a result's to allocate by, an argument's for the + binding to check the actual by. + """ + extents: dict[tuple[str, str], tuple[NativeEntrypointExtentPlan, ...]] = {} + for result in results: + if result.array is not None and "bridge" in result.array.extent_evaluation: + extents[(result.owner_path, "declaration_extent")] = WrapperPlanner._bridge_extents( + result.array, f"prik_decl_extent_{result.result_position}" + ) + for argument in arguments: + if argument.array is not None and "bridge" in argument.array.extent_evaluation: + extents[(argument.owner_path, "argument_extent")] = WrapperPlanner._bridge_extents( + argument.array, f"{argument.entrypoint.parameter_name}_declared_extent" + ) + return extents + + @staticmethod + def _bridge_extents(array, prefix: str) -> tuple[NativeEntrypointExtentPlan, ...]: + """Name the output carrying each axis of one array the bridge evaluates.""" + return tuple( + NativeEntrypointExtentPlan(axis=axis, parameter_name=f"{prefix}_{axis}") + for axis, evaluation in enumerate(array.extent_evaluation) + if evaluation == "bridge" + ) + def _entrypoint_result_plans( self, results: tuple[ResultPlan, ...], @@ -1557,8 +1803,6 @@ def _projected_slot_plans( scalar_native_type=slot_policy.scalar_native_type, array_logical_abi=slot_policy.array_logical_abi, array_native_type=slot_policy.array_native_type, - array_copy_in=slot_policy.array_copy_in, - array_copy_out=slot_policy.array_copy_out, literal_type=slot_policy.literal_type, literal_value=slot_policy.literal_value, result_position=slot_policy.result_position, @@ -1650,9 +1894,6 @@ def _visit_ArgumentPolicy( scalar_native_type=policy.scalar_native_type, array_logical_abi=policy.array_logical_abi, array_native_type=policy.array_native_type, - array_copy_in=policy.array_copy_in, - array_copy_out=policy.array_copy_out, - array_writeback_abi=policy.array_writeback_abi, object_kind=policy.ownership.kind, ownership_owner=policy.ownership.owner, transfer_mode=policy.ownership.transfer, @@ -1736,10 +1977,12 @@ def _callback_transfer_plan(self, policy: CallbackTransferPolicy) -> CallbackTra owner_path=policy.owner_path, name=policy.name, semantic_type_name=policy.semantic_type_name, + native_fortran_type=policy.native_fortran_type, object_kind=policy.object_kind, rank=policy.rank, passed_by_value=policy.passed_by_value, intent=policy.intent, + optionality=policy.optionality, abi=policy.abi, adapter_action=policy.adapter_action, python_action=policy.python_action, @@ -1784,9 +2027,11 @@ def _procedure_prototype_argument_plan( owner_path=policy.owner_path, name=policy.name, semantic_type_name=policy.semantic_type_name, + native_fortran_type=policy.native_fortran_type, rank=policy.rank, passed_by_value=policy.passed_by_value, intent=policy.intent, + optional=policy.optional, character_length=policy.character_length, array=self._array_plan(policy.array, policy.owner_path), derived_type_identity=policy.derived_type_identity, @@ -1829,7 +2074,6 @@ def _polymorphic_dispatch_plan( PolymorphicVariantPlan( type_identity=identity, backend_symbol=self._derived_backend_symbol(identity), - python_name=self._class_python_names[identity], abi_code=index, ) for index, identity in enumerate(policy.variants, start=1) @@ -1948,7 +2192,7 @@ def _visit_LifecyclePolicy( policy: LifecyclePolicy, ) -> LifecycleActionPlan: """Return one transfer-owned action for function-wide ordering.""" - family = self._datatype_family(policy.semantic_type_name) + family = self._transfer_datatype_family(policy.semantic_type_name, policy.derived) binding = None bridge = None if policy.phase is WritebackPhase.NATIVE_MUTATION: @@ -2558,12 +2802,18 @@ def _declaration_callable_roles( """Return bridge-resolved declaration-callable symbol roles.""" return tuple(item.symbolic_role for item in declaration_callables) - def _required_headers(self, namespaces: tuple[NamespacePlan, ...]) -> tuple[str, ...]: + def _required_headers( + self, + namespaces: tuple[NamespacePlan, ...], + variables: tuple[ModuleVariablePlan, ...], + ) -> tuple[str, ...]: """Return the union of headers selected by completed handle plans.""" handles = tuple( handle - for namespace in namespaces - for handle in self._namespace_native_array_handles(namespace) + for handle in ( + *(item.native_array_handle for item in variables), + *(handle for namespace in namespaces for handle in self._namespace_native_array_handles(namespace)), + ) if handle is not None ) headers = list(self._native_array_headers(handles)) @@ -2621,10 +2871,9 @@ def _namespace_native_array_handles( self, namespace: NamespacePlan, ) -> tuple[NativeArrayHandlePlan | None, ...]: - """Return argument, result, and module handle plans for one namespace.""" + """Return argument, result, and derived-field handles for one namespace.""" return ( *(handle for function in namespace.functions for handle in self._function_native_array_handles(function)), - *(variable.native_array_handle for variable in namespace.variables), *self._derived_field_native_array_handles(namespace), ) @@ -2667,8 +2916,6 @@ def _datatype_family(self, semantic_type_name: str) -> DatatypeFamily: try: return _DATATYPE_FAMILIES[semantic_type_name] except KeyError: - if semantic_type_name in getattr(self, "_derived_type_names", set()): - return DatatypeFamily.DERIVED raise ValueError(f"Unsupported first-lane scalar type {semantic_type_name!r}") from None def _transfer_datatype_family( diff --git a/prik/policy/completion.py b/prik/policy/completion.py index 49b283fe8..c6e8ec3e5 100644 --- a/prik/policy/completion.py +++ b/prik/policy/completion.py @@ -15,6 +15,7 @@ from collections.abc import Iterable from prik.semantics.scalar_types import SEMANTIC_SCALAR_TYPE_NAMES +from prik.utilities.declaration_expressions import declaration_extent_references from prik.policy.ownership import ( CodegenAction, OwnershipDecision, @@ -288,18 +289,7 @@ def _is_entry_export_reachable(declaration: object) -> bool: """Keep private declarations and public declarations selected by entry exports.""" if getattr(declaration, "visibility", "public") == "private": return True - return bool(_entry_exports(declaration)) - - -def _entry_exports(declaration: object) -> object: - """Return a declaration's entry-export metadata, with overloads using their first procedure.""" - if isinstance(declaration, models.ProcedureOverloadSet): - if not declaration.procedures: - return () - return declaration.procedures[0].metadata.get(models.PYTHON_EXPORTS_METADATA, ()) - if isinstance(declaration, models.SemanticVariable | models.SemanticFunction | models.SemanticClass): - return declaration.metadata.get(models.PYTHON_EXPORTS_METADATA, ()) - raise TypeError(f"Unsupported semantic declaration: {type(declaration).__name__}") + return bool(declaration.metadata.get(models.PYTHON_EXPORTS_METADATA, ())) def _complete_ownership_policies( @@ -368,6 +358,7 @@ def _complete_ownership_policies( procedure, f"{procedure_scope}.{overload_set.name}.{procedure.name}", derived_types=derived_types, + module_export=False, ) # Build resolved module overload tables after every candidate is complete. overload_functions = { @@ -546,7 +537,6 @@ def _complete_class_surface_policies( owner_path=derived.owner_path, derived=derived, class_identities=identities, - strict_wrapper_names=strict_wrapper_names, ) completed_derived = replace(derived, fields=surface.effective_fields) semantic_class.metadata[models.RESOLVED_DERIVED_TYPE_POLICY_METADATA] = completed_derived @@ -718,6 +708,7 @@ def _complete_concrete_class_methods( method, function_owner_path, derived_types=derived_types, + module_export=False, class_call=calls.get(owner_path), polymorphic_variants=polymorphic_variants, ) @@ -805,6 +796,7 @@ def _complete_one_class_overload_method( owner_path, derived_types=derived_types, class_call=call, + module_export=False, polymorphic_variants=polymorphic_variants, native_dispatch_name=native_dispatch_name, ) @@ -1097,7 +1089,7 @@ def _complete_function( *, derived_types: dict[tuple[str, str], DerivedTypePolicy] | None = None, class_call: ClassMethodPolicy | None = None, - module_export: bool | None = None, + module_export: bool, polymorphic_variants: dict[tuple[str, str], tuple[tuple[str, str], ...]] | None = None, native_dispatch_name: str | None = None, ) -> None: @@ -2120,12 +2112,19 @@ def _semantic_shape(semantic_type: models.SemanticType) -> list[str]: def _is_resolved_extent(value: object, visible_scalar_names: set[str]) -> bool: - """Report whether an extent is concrete or references only visible scalar inputs.""" + """Report whether an extent is concrete or references only visible scalar inputs. + + The references come from parsing the extent, which is what distinguishes a + value the extent reads from the name of a call it makes: ``max(n, m)`` + reads ``n`` and ``m``, and requiring ``max`` to be a visible scalar would + refuse an expression declaration support otherwise accepts. Syntax that + stage cannot resolve reports a name no argument carries, so it stays + refused. + """ text = str(value).strip() if not text or text in {":", "*", "...", ".."} or ":" in text: return False - names = set(re.findall(r"\b[A-Za-z_]\w*\b", text)) - return names <= visible_scalar_names + return set(declaration_extent_references(text)) <= visible_scalar_names def _complete_variable( diff --git a/prik/policy/construction.py b/prik/policy/construction.py index 82d73340a..2ff497a01 100644 --- a/prik/policy/construction.py +++ b/prik/policy/construction.py @@ -20,7 +20,7 @@ from immutabledict import immutabledict from prik.contracts import NATIVE_C_SCALAR_IDENTITIES -from prik.naming import NamingPolicy +from prik.utilities.declaration_expressions import fortran_character_value from prik.semantics import models from prik.semantics.metadata import ( ADDRESS_ROLE_METADATA, @@ -66,7 +66,6 @@ RAW_STRING_ADDRESS_COPY_REASON, DERIVED_VALUE_COPY_REASON, LOGICAL_SCALAR_KIND_COPY_REASON, - LOGICAL_ARRAY_KIND_COPY_REASON, NativeEntrypointAction, DirectCABITypePolicy, DirectCABIPolicy, @@ -78,7 +77,6 @@ ArgumentConversionPhase, BridgeDataAction, DirectResultABI, - ArrayWritebackABI, ScalarLogicalABI, ArrayLogicalABI, ArrayPythonLayout, @@ -87,6 +85,7 @@ TransformationLayer, TransformationAction, CallbackABIKind, + CallbackOptionalityAction, CallbackTransferAction, CallbackResultAction, CallbackLifecycleAction, @@ -173,6 +172,7 @@ FunctionWrapperPolicy, ) from prik.utilities.declaration_expressions import ( + RUNTIME_DIMENSION_MARKERS, declaration_expression_call_sites, declaration_extent_references, resolve_declaration_extent, @@ -440,7 +440,7 @@ def build_derived_type_policy( else [] ) ) - exports = completed_python_exports(semantic_class, semantic_class.name) + exports = completed_python_exports(semantic_class) native_type_name = str(semantic_class.native_name or semantic_class.name) native_scope = str(semantic_class.origin.native_scope or owner_path.split(".", 1)[0]) return DerivedTypePolicy( @@ -478,14 +478,15 @@ def build_class_surface_policy( owner_path: str, derived: DerivedTypePolicy, class_identities: dict[str, tuple[str, str]], - strict_wrapper_names: bool = False, ) -> ClassSurfacePolicy: """Complete constructor, method, inheritance, and registration decisions.""" - naming = NamingPolicy(strict_public_names=strict_wrapper_names) - fields = _python_named_class_fields(derived.fields, naming, owner_path) + # Contract-name completion already applied strict naming and one shared + # member ledger. Class policy reads those spellings rather than allocating + # a second surface whose collision order could disagree with the contract. + fields = _python_named_class_fields(semantic_class, derived.fields, owner_path) named_derived = replace(derived, fields=fields) - methods = _python_named_class_methods(semantic_class, naming, owner_path) - overloads = _python_named_class_overloads(semantic_class, naming, owner_path) + methods = _python_named_class_methods(semantic_class, owner_path) + overloads = _python_named_class_overloads(semantic_class, owner_path) constructor, constructor_blockers = _class_constructor_policy( semantic_class, owner_path=owner_path, @@ -522,21 +523,16 @@ def build_class_surface_policy( def _python_named_class_fields( + semantic_class: models.SemanticClass, fields: tuple[DerivedFieldPolicy, ...], - naming: NamingPolicy, owner_path: str, ) -> tuple[DerivedFieldPolicy, ...]: - """Reserve readable Python field names while retaining native spellings.""" - namespace = (owner_path,) + """Read completed field names while retaining native owner identities.""" + completed = {f"{owner_path}.{field.name}": models.completed_contract_name(field) for field in semantic_class.fields} return tuple( replace( field, - name=naming.reserve_public_name( - namespace, - field.name, - category="field", - owner=field.owner_path, - ), + name=completed[field.owner_path], ) for field in fields ) @@ -544,11 +540,9 @@ def _python_named_class_fields( def _python_named_class_methods( semantic_class: models.SemanticClass, - naming: NamingPolicy, owner_path: str, ) -> tuple[ClassMethodPolicy, ...]: - """Reserve method names in the same Python namespace as public fields.""" - namespace = (owner_path,) + """Read method names completed in the same namespace as public fields.""" methods = [] for method in semantic_class.methods: if method.name == "__init__": @@ -557,12 +551,7 @@ def _python_named_class_methods( if policy.public: policy = replace( policy, - python_name=naming.reserve_public_name( - namespace, - policy.python_name, - category="function", - owner=policy.owner_path, - ), + python_name=models.completed_contract_name(method), ) methods.append(policy) return tuple(methods) @@ -570,11 +559,9 @@ def _python_named_class_methods( def _python_named_class_overloads( semantic_class: models.SemanticClass, - naming: NamingPolicy, owner_path: str, ) -> tuple[OverloadPolicy, ...]: - """Split reflected operators, then reserve every public overload name.""" - namespace = (owner_path,) + """Split reflected operators and read every completed overload name.""" policies = [] for overload in semantic_class.overload_sets: names = tuple( @@ -593,12 +580,7 @@ def _python_named_class_overloads( policies.append( replace( policy, - python_name=naming.reserve_public_name( - namespace, - policy.python_name, - category="function", - owner=policy.owner_path, - ), + python_name=models.completed_contract_name(procedures[0]), ) ) return tuple(policies) @@ -744,6 +726,22 @@ def _class_method_blockers(method: ClassMethodPolicy) -> str | None: return None +def _overload_candidate_scope( + procedure: models.SemanticFunction, + owner_path: str, + module_generic: bool, +) -> str: + """Return the scope that addresses one overload candidate. + + A module generic addresses each specific by the module that owns it, so a + specific inherited from an imported generic stays findable. A class-bound + overload is addressed by its class instead, which owns every candidate. + """ + if not module_generic: + return owner_path + return str(procedure.origin.native_scope or owner_path) + + def _overload_policy( owner_path: str, overload: models.ProcedureOverloadSet, @@ -751,13 +749,15 @@ def _overload_policy( python_name: str | None = None, procedures: tuple[models.SemanticFunction, ...] | None = None, python_exports: tuple[PythonExportPolicy, ...] = (), + module_generic: bool = False, ) -> OverloadPolicy: """Complete one overload set from explicit concrete-procedure links.""" selected = tuple(overload.procedures) if procedures is None else procedures public_name = python_name or overload.name candidates = tuple( OverloadCandidatePolicy( - owner_path=f"{owner_path}.{overload.name}.{procedure.name}", + owner_path=f"{_overload_candidate_scope(procedure, owner_path, module_generic)}" + f".{overload.name}.{procedure.name}", arguments=(), passed_object=False, ) @@ -780,14 +780,14 @@ def build_module_overload_policy( overload: models.ProcedureOverloadSet, ) -> OverloadPolicy: """Complete the stable owner and Python exports for one module generic.""" - if not overload.procedures: - return _overload_policy(module.name, overload) - first = overload.procedures[0] - native_scope = str(first.origin.native_scope or module.name) + # A generic extending an imported one holds specifics from another module, + # so the declared scope names the owner rather than the first specific. + first_scope = overload.procedures[0].origin.native_scope if overload.procedures else None return _overload_policy( - native_scope, + str(overload.native_scope or first_scope or module.name), overload, - python_exports=completed_python_exports(first, overload.name), + python_exports=completed_python_exports(overload), + module_generic=True, ) @@ -1064,7 +1064,7 @@ def _module_variable_policy_base( return { "owner_path": owner_path, "name": variable.name, - "python_exports": completed_python_exports(variable, variable.name), + "python_exports": completed_python_exports(variable), "native_name": str(variable.origin.native_name or variable.name), "native_module": str(variable.origin.native_scope or module_name), "semantic_type_name": variable.semantic_type.name, @@ -1394,18 +1394,24 @@ def build_callback_handoff_policy( blockers.extend(_callback_result_blockers(return_type, result)) # Complete the shared exact signature after argument and result ABI facts exist. prototype_ref = semantic_type.metadata.get(models.PROTOTYPE_REF_METADATA) - source_name = prototype_ref.get("name") if isinstance(prototype_ref, dict) else None - local_name = prototype_ref.get("local_name") if isinstance(prototype_ref, dict) else None - origin_module = prototype_ref.get("origin_module") if isinstance(prototype_ref, dict) else None + reference = prototype_ref if isinstance(prototype_ref, dict) else {} + source_name = reference.get("name") if not isinstance(source_name, str) or not source_name: blockers.append("callback argument requires a resolved named prototype") source_name = semantic_type.name - if not isinstance(local_name, str) or not local_name: - local_name = semantic_type.name + # A prototype is its declaring module and scope with the name that scope + # gives it; the contract spelling only names it. + identity = ".".join( + (reference.get("origin_module") or owner_path, *reference.get("declaring_scope", ()), source_name) + ) + written = semantic_type.metadata.get(models.CONTRACT_NAME_METADATA) + if not isinstance(written, str) or not written: + blockers.append("callback prototype has no completed contract spelling") + written = semantic_type.name prototype = _procedure_prototype_policy( owner_path=owner_path, - name=local_name, - identity=f"{origin_module or owner_path}.{source_name}", + name=written, + identity=identity, pure=_prototype_metadata_is_pure(semantic_type.metadata.get("prototype_metadata")), source_language=semantic_type.metadata.get("prototype_source_language"), native_abi=semantic_type.metadata.get("prototype_native_abi"), @@ -1500,10 +1506,12 @@ def _callback_transfer_policy( passed_by_value = bool(argument.origin.metadata.get("value")) derived = _is_scalar_derived_type(semantic_type) array = _array_handoff_policy(semantic_type) if int(semantic_type.rank or 0) > 0 else None + _logical_abi, native_fortran_type = _scalar_logical_argument_abi(argument) return CallbackTransferPolicy( owner_path=owner_path, name=argument.name, semantic_type_name=semantic_type.name, + native_fortran_type=native_fortran_type, object_kind=decision.kind, rank=int(semantic_type.rank or 0), passed_by_value=passed_by_value, @@ -1512,6 +1520,13 @@ def _callback_transfer_policy( if (intent := argument.origin.metadata.get(models.PROTOTYPE_INTENT_METADATA)) is not None else None ), + optionality=( + CallbackOptionalityAction.BLOCKED + if argument.optional and passed_by_value + else CallbackOptionalityAction.NULL_DATA_POINTER + if argument.optional + else CallbackOptionalityAction.REQUIRED + ), abi=_callback_abi_kind(argument, derived=derived), adapter_action=_callback_adapter_action(argument), python_action=decision.python_barrier_action, @@ -1542,19 +1557,27 @@ def _callback_abi_kind( def _callback_adapter_action( argument: models.SemanticArgument, ) -> CallbackTransferAction: - """Select callback copy direction from the prototype's exact dummy intent.""" + """Select callback copy direction from the prototype's completed dummy contract. + + A declared ``intent`` names the direction outright. With none declared the + callee may both read and modify the dummy, so the direction follows the + completed storage: writable rank-zero storage copies in and out, while a + value projection is input-only. + """ semantic_type = argument.semantic_type intent = argument.origin.metadata.get(models.PROTOTYPE_INTENT_METADATA) if intent == "out": return CallbackTransferAction.COPY_OUT if intent == "inout": return CallbackTransferAction.COPY_IN_OUT - if ( - intent == "in" - or bool(argument.origin.metadata.get("value")) - or (semantic_type.name in _PLAN_PRIMITIVE_SCALAR_TYPES and int(semantic_type.rank or 0) == 0) - ): + if intent == "in" or bool(argument.origin.metadata.get("value")): return CallbackTransferAction.COPY_IN + if semantic_type.name in _PLAN_PRIMITIVE_SCALAR_TYPES and int(semantic_type.rank or 0) == 0: + return ( + CallbackTransferAction.COPY_IN_OUT + if _is_scalar_storage_type(semantic_type) + else CallbackTransferAction.COPY_IN + ) return CallbackTransferAction.COPY_IN_OUT @@ -1565,8 +1588,11 @@ def _callback_transfer_blockers( """Reject callback forms whose typed adapter ABI is incomplete.""" semantic_type = argument.semantic_type blockers = list(_runtime_semantic_validation_blockers(semantic_type, f"callback argument {argument.name!r}")) - if argument.optional: - blockers.append(f"callback argument {argument.name!r} cannot be optional") + if transfer.optionality is CallbackOptionalityAction.BLOCKED: + blockers.append( + f"callback argument {argument.name!r} cannot be both optional and passed by value; " + "use a reference dummy so absence has a null-pointer ABI" + ) if _uses_unsupported_callback_descriptor(semantic_type): blockers.append( f"callback argument {argument.name!r} uses unsupported allocatable, pointer, " @@ -1574,6 +1600,13 @@ def _callback_transfer_blockers( ) if transfer.passed_by_value and transfer.rank > 0: blockers.append(f"callback argument {argument.name!r} cannot pass an array by value") + if _discards_callback_scalar_writeback(transfer): + # Python has no writable scalar, so a value projection cannot deliver + # anything back to the native caller that reads this dummy after the call. + blockers.append( + f"callback argument {argument.name!r} is intent({transfer.intent}) and cannot use the " + f"value spelling Addr({semantic_type.name}); use {semantic_type.name}[()] for writable storage" + ) if semantic_type.name == "String": if transfer.character_length is None or transfer.character_length <= 0: blockers.append(f"callback argument {argument.name!r} requires a fixed positive character length") @@ -1587,6 +1620,17 @@ def _callback_transfer_blockers( return tuple(blockers) +def _discards_callback_scalar_writeback(transfer: CallbackTransferPolicy) -> bool: + """Report whether a written-back scalar dummy was projected as an unwritable value.""" + return bool( + transfer.rank == 0 + and not transfer.passed_by_value + and transfer.intent is not None + and str(transfer.intent).casefold() in {"out", "inout"} + and transfer.python_action is PythonBarrierAction.SCALAR_VALUE + ) + + def _callback_result_policy( return_type: object, *, @@ -1603,10 +1647,12 @@ def _callback_result_policy( owner_path=owner_path, name="result", semantic_type_name=return_type.name, + native_fortran_type=None, object_kind=decision.kind, rank=int(return_type.rank or 0), passed_by_value=False, intent=None, + optionality=CallbackOptionalityAction.REQUIRED, abi=( CallbackABIKind.DERIVED_ADDRESS if derived @@ -1673,7 +1719,7 @@ def build_function_wrapper_policy( owner_path: str, derived_types: Mapping[tuple[str, str], DerivedTypePolicy] | None = None, class_call: ClassMethodPolicy | None = None, - module_export: bool | None = None, + module_export: bool, polymorphic_variants: Mapping[tuple[str, str], tuple[tuple[str, str], ...]] | None = None, native_dispatch_name: str | None = None, ) -> FunctionWrapperPolicy: @@ -1772,7 +1818,10 @@ def build_function_wrapper_policy( blockers = (*blockers, *entrypoint_diagnostics) return FunctionWrapperPolicy( owner_path=owner_path, - python_exports=completed_python_exports(function, function.name), + # Only a module-level publication has module exports: a method is + # reached through its class and an overload candidate through its + # generic, whose own policies carry their placement. + python_exports=completed_python_exports(function) if module_export else (), native_name=native_name, native_invocation=native_invocation, native_operator=native_operator, @@ -1789,9 +1838,7 @@ def build_function_wrapper_policy( release_gil=bool(function.metadata.get(models.RUNTIME_RELEASE_GIL_METADATA)), status_error=status_error, class_call=class_call, - module_export=( - not bool(function.metadata.get("fortran_type_bound_target")) if module_export is None else module_export - ), + module_export=module_export, supported=not blockers, arguments=tuple(arguments), results=results, @@ -1993,7 +2040,8 @@ def _complete_entrypoint_argument_route( entrypoint_pass_descriptor_presence=(uses_adapter and argument.optional_mode is OptionalMode.DESCRIPTOR), entrypoint_pass_derived_transaction=(uses_adapter and argument.derived_call is not None), entrypoint_pass_callback_parameter=( - action is NativeEntrypointAction.DIRECT_C_ABI and argument.callback is not None + argument.callback is not None + and (action is NativeEntrypointAction.DIRECT_C_ABI or argument.optional_mode is OptionalMode.NULLABLE_VALUE) ), entrypoint_optionality=( EntrypointOptionalityAction.EXPLICIT_NATIVE_PRESENCE @@ -2443,7 +2491,7 @@ def _argument_declares_nullable_c_pointer(argument: ArgumentPolicy, semantic_typ def _argument_requests_native_write(argument: ArgumentPolicy) -> bool: """Return whether a completed contract expects native writes to be visible.""" - return bool(argument.writable or argument.projects_result or argument.array_copy_out) + return bool(argument.writable or argument.projects_result) def _c_direct_scalar_name(semantic_type: models.SemanticType | None) -> str | None: @@ -3031,10 +3079,7 @@ def _argument_policy( function = context.function argument_path = f"{context.owner_path}.{argument.name}" scalar_logical_abi, scalar_native_type = _scalar_logical_argument_abi(argument) - array_logical_abi, array_native_type, array_copy_in, array_copy_out = _array_logical_argument_abi( - argument, - decision, - ) + array_logical_abi, array_native_type = _array_logical_argument_abi(argument) optional_mode = _optional_mode(argument, decision) callback = _callback_handoff_policy(argument) array_policy = _array_handoff_policy( @@ -3113,15 +3158,6 @@ def _argument_policy( scalar_native_type=scalar_native_type, array_logical_abi=array_logical_abi, array_native_type=array_native_type, - array_copy_in=array_copy_in, - array_copy_out=array_copy_out, - array_writeback_abi=_array_writeback_abi( - argument.semantic_type, - decision, - boundary.handoff_mode, - array_policy, - array_logical_abi, - ), optional=argument.optional, optional_mode=boundary.optional_mode, conversion_phase=boundary.conversion_phase, @@ -3238,7 +3274,7 @@ def _completed_argument_bridge_action( native_slot.value_kind if native_slot is not None else None, ) action, reason = _derived_argument_bridge_data_action(derived, action, reason) - return _logical_argument_bridge_action(argument, decision, action, reason) + return _logical_argument_bridge_action(argument, action, reason) def _argument_boundary_policy( @@ -3250,11 +3286,12 @@ def _argument_boundary_policy( ) -> _ArgumentBoundaryPolicy: """Normalize callback inputs onto the ordinary argument-policy schema.""" if callback is not None: + optional_mode = OptionalMode.NULLABLE_VALUE if argument.optional else OptionalMode.REQUIRED return _ArgumentBoundaryPolicy( - optional_mode=OptionalMode.REQUIRED, + optional_mode=optional_mode, conversion_phase=ArgumentConversionPhase.IMMEDIATE, handoff_mode=ArgumentHandoffMode.VALUE, - nullable=False, + nullable=argument.optional, writable=False, descriptor_boundary=False, codegen_action=CodegenAction.CALL_LOCAL_INPUT, @@ -3314,8 +3351,6 @@ def _completed_argument_blockers( if callback is not None: blockers.extend(callback.blockers) blockers.extend(_callback_derived_type_blockers(callback, derived_types)) - if argument.optional: - blockers.append(f"argument {argument.name!r} is an unsupported optional callback") else: blockers.extend( _argument_blockers( @@ -3629,7 +3664,6 @@ def _hidden_result_candidate( ) bridge_data_action, bridge_copy_reason = _logical_argument_bridge_action( argument, - decision, bridge_data_action, bridge_copy_reason, ) @@ -3938,10 +3972,7 @@ def _projected_argument_slot( value_kind = _native_argument_value_kind(argument, mapping.value_kind or "arg") callback = _callback_handoff_policy(argument) scalar_logical_abi, scalar_native_type = _scalar_logical_argument_abi(argument) - array_logical_abi, array_native_type, array_copy_in, array_copy_out = _array_logical_argument_abi( - argument, - decision, - ) + array_logical_abi, array_native_type = _array_logical_argument_abi(argument) derived = _argument_derived_handoff(argument, decision, callback, argument_path, derived_types) bridge_data_action, bridge_copy_reason = _completed_projected_bridge_action( argument, @@ -3975,8 +4006,6 @@ def _projected_argument_slot( scalar_native_type=scalar_native_type, array_logical_abi=array_logical_abi, array_native_type=array_native_type, - array_copy_in=array_copy_in, - array_copy_out=array_copy_out, result_position=mapping.result_position, semantic_type_name=argument.semantic_type.name, character_length=_character_length(argument.semantic_type), @@ -4012,7 +4041,7 @@ def _completed_projected_bridge_action( value_kind, ) action, reason = _derived_argument_bridge_data_action(derived, action, reason) - return _logical_argument_bridge_action(argument, decision, action, reason) + return _logical_argument_bridge_action(argument, action, reason) def _native_slot_barrier_actions( @@ -4104,15 +4133,11 @@ def _hidden_result_native_call_slot_policy( ) bridge_data_action, bridge_copy_reason = _logical_argument_bridge_action( argument, - decision, bridge_data_action, bridge_copy_reason, ) scalar_logical_abi, scalar_native_type = _scalar_logical_argument_abi(argument) - array_logical_abi, array_native_type, array_copy_in, array_copy_out = _array_logical_argument_abi( - argument, - decision, - ) + array_logical_abi, array_native_type = _array_logical_argument_abi(argument) blockers = ( (f"native-call result slot {native_position} has no completed bridge data action",) if bridge_data_action is BridgeDataAction.BLOCKED @@ -4137,8 +4162,6 @@ def _hidden_result_native_call_slot_policy( scalar_native_type=scalar_native_type, array_logical_abi=array_logical_abi, array_native_type=array_native_type, - array_copy_in=array_copy_in, - array_copy_out=array_copy_out, result_position=mapping.result_position, semantic_type_name=argument.semantic_type.name, character_length=_character_length(argument.semantic_type), @@ -4262,10 +4285,7 @@ def _implicit_native_call_slot_policies( continue value_kind = _native_argument_value_kind(argument, "arg") scalar_logical_abi, scalar_native_type = _scalar_logical_argument_abi(argument) - array_logical_abi, array_native_type, array_copy_in, array_copy_out = _array_logical_argument_abi( - argument, - decision, - ) + array_logical_abi, array_native_type = _array_logical_argument_abi(argument) callback = argument.semantic_type.metadata.get(models.RESOLVED_CALLBACK_POLICY_METADATA) callback = callback if isinstance(callback, CallbackHandoffPolicy) else None derived = ( @@ -4295,7 +4315,6 @@ def _implicit_native_call_slot_policies( ) bridge_data_action, bridge_copy_reason = _logical_argument_bridge_action( argument, - decision, bridge_data_action, bridge_copy_reason, ) @@ -4322,8 +4341,6 @@ def _implicit_native_call_slot_policies( scalar_native_type=scalar_native_type, array_logical_abi=array_logical_abi, array_native_type=array_native_type, - array_copy_in=array_copy_in, - array_copy_out=array_copy_out, semantic_type_name=argument.semantic_type.name, character_length=_character_length(argument.semantic_type), array=_array_handoff_policy( @@ -4373,6 +4390,12 @@ def _derived_argument_handoff_blockers( """Require the exact native type definition for a typed value call.""" if derived is None: return () + interface = argument.semantic_type.metadata.get(models.UNRESOLVED_PROCEDURE_INTERFACE_METADATA) + if interface is not None: + return ( + f"argument {argument.name!r} declares procedure interface {str(interface)!r}, " + "which no supplied source declares; add the module that declares it to the build inputs", + ) return _derived_type_definition_blockers(f"argument {argument.name!r}", derived, derived_types) @@ -4766,7 +4789,15 @@ def _resolve_derived_type_policy( if exact is not None: return exact if semantic_type.metadata.get(models.EXTERNAL_TYPE_REF_METADATA) is not None: - return None + # An imported reference names the type the way the module declaring it + # writes it, which is its own name rather than the native type it binds. + # The search stays inside that module, so a type of the same name + # declared elsewhere is never reached. + scope, name = requested_identity + imported_matches = tuple( + policy for policy in derived_types.values() if policy.native_scope == scope and policy.type_name == name + ) + return imported_matches[0] if len(imported_matches) == 1 else None local_matches = tuple(policy for policy in derived_types.values() if policy.type_name == semantic_type.name) return local_matches[0] if len(local_matches) == 1 else None @@ -5713,7 +5744,7 @@ def _ordinary_array_result_blockers( if decision.nullable or decision.descriptor_boundary: blockers.append(f"{label} is descriptor-backed or nullable") array = _array_handoff_policy(semantic_type) - if array is None or array.rank is None or any(shape in {":", "::Strided", "...", "Flat"} for shape in array.shape): + if array is None or array.rank is None or any(shape in RUNTIME_DIMENSION_MARKERS for shape in array.shape): blockers.append(f"{label} ordinary array shape is not fully expressible") elif array.native_order != array.order: blockers.append(f"{label} COPY_F applies only to Python-visible array arguments") @@ -5867,6 +5898,10 @@ def _result_position_blockers( ) if not positions: return () + if any(position is None for position in positions): + # An unplaced output has no position to order, which this check reports + # rather than comparing against the positions that do exist. + return (f"binding result positions are incomplete; received {positions}",) if sorted(positions) == list(range(len(positions))) and len(set(positions)) == len(positions): return () return (f"binding result positions must cover 0..{len(positions) - 1} exactly once; received {positions}",) @@ -6053,6 +6088,7 @@ def _lifecycle_policies( semantic_type_name=argument.semantic_type_name, result_position=argument.result_position, object_kind=argument.ownership.kind, + derived=argument.derived, ) for phase in phases ) @@ -6078,6 +6114,7 @@ def action(result: ResultPolicy, operation: LifecycleOperation) -> LifecyclePoli semantic_type_name=result.semantic_type_name, result_position=result.result_position, object_kind=result.ownership.kind, + derived=result.derived, operation=operation, ) @@ -7071,7 +7108,10 @@ def _scalar_module_literal_value(value: object, semantic_type_name: str) -> obje if lowered in {".false.", "false"}: return False if semantic_type_name == "String": - return ast.literal_eval(text) + # Fortran doubles a quote to hold one, which Python reads as two + # literals side by side and joins, dropping the quote. + character = fortran_character_value(text) + return character if character is not None else ast.literal_eval(text) normalized = text.replace("D", "e").replace("d", "e") parsed = ast.literal_eval(normalized) if semantic_type_name in {"Complex64", "Complex128"} and isinstance(parsed, tuple): @@ -7271,7 +7311,12 @@ def _scalar_logical_argument_abi( if source_type is None: if semantic_type.name in {"Bool", "Bool8"}: return ScalarLogicalABI.C_BOOL, "logical(c_bool)" - return ScalarLogicalABI.NATIVE_KIND_COPY, None + native_kind = {"Bool16": 2, "Bool32": 4, "Bool64": 8}.get(semantic_type.name) + return ( + (ScalarLogicalABI.NATIVE_KIND_COPY, f"logical(kind={native_kind})") + if native_kind is not None + else (ScalarLogicalABI.NATIVE_KIND_COPY, None) + ) compact = "".join(source_type.casefold().split()) if compact == "logical(kind=c_bool)": return ScalarLogicalABI.C_BOOL, "logical(c_bool)" @@ -7280,29 +7325,22 @@ def _scalar_logical_argument_abi( def _array_logical_argument_abi( argument: models.SemanticArgument, - decision: OwnershipDecision, -) -> tuple[ArrayLogicalABI, str | None, bool, bool]: - """Complete native storage and directional copies for a Boolean array. +) -> tuple[ArrayLogicalABI, str | None]: + """Complete the native storage one Boolean array is viewed as. - The helper consumes semantic type/origin facts and completed ownership. It - returns the ABI selector, exact native spelling, and independent copy-in - and copy-out flags. Exact ``c_bool`` arrays borrow the NumPy buffer; other - Fortran logical kinds require a bridge-local representation. + The buffer is a NumPy integer of the element's own width, so the native + pointer describes the caller's storage exactly for every logical kind, and + nothing is copied either way. A spelling the source did not record is left + unset; backend lowering then resolves the width from the semantic type. """ semantic_type = argument.semantic_type if not is_boolean_semantic_type_name(semantic_type.name) or int(semantic_type.rank or 0) <= 0: - return ArrayLogicalABI.NOT_APPLICABLE, None, False, False - # The buffer is a NumPy integer of the element's own width, so the native - # pointer describes the caller's storage exactly and no directional copy is - # required for any logical kind. - # A spelling the source did not record is left unset; backend lowering then - # resolves the width from the semantic type itself. - return ArrayLogicalABI.C_BOOL_VIEW, _fortran_logical_native_type(argument), False, False + return ArrayLogicalABI.NOT_APPLICABLE, None + return ArrayLogicalABI.C_BOOL_VIEW, _fortran_logical_native_type(argument) def _logical_argument_bridge_action( argument: models.SemanticArgument, - decision: OwnershipDecision, action: BridgeDataAction, reason: str | None, ) -> tuple[BridgeDataAction, str | None]: @@ -7310,9 +7348,6 @@ def _logical_argument_bridge_action( abi, _native_type = _scalar_logical_argument_abi(argument) if abi is ScalarLogicalABI.NATIVE_KIND_COPY: return BridgeDataAction.COPY_REPRESENTATION, LOGICAL_SCALAR_KIND_COPY_REASON - array_abi, _native_type, _copy_in, _copy_out = _array_logical_argument_abi(argument, decision) - if array_abi is ArrayLogicalABI.NATIVE_KIND_COPY: - return BridgeDataAction.COPY_REPRESENTATION, LOGICAL_ARRAY_KIND_COPY_REASON return action, reason @@ -7458,28 +7493,6 @@ def _argument_handoff_mode(decision: OwnershipDecision) -> ArgumentHandoffMode: # Ordinary-array handoff policy. -def _array_writeback_abi( - semantic_type: models.SemanticType, - decision: OwnershipDecision, - handoff_mode: ArgumentHandoffMode, - array: ArrayHandoffPolicy | None, - logical_abi: ArrayLogicalABI, -) -> ArrayWritebackABI: - """Complete mutable ordinary-array byte normalization before planning. - - A Boolean array needs no more than any other kind. Its elements already - hold the zero or one a C ``_Bool`` is defined to hold, because the compiler - profiles request the option that guarantees it, so there is nothing left to - reduce. Reducing anyway could not help a translation unit built without - that option either: such a compiler represents false as the complement of - true, which no test applied here could tell from a true value. - """ - del logical_abi - if array is None or handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER or not decision.mutates_native: - return ArrayWritebackABI.NOT_APPLICABLE - return ArrayWritebackABI.NATIVE_ARRAY - - def _array_handoff_policy( semantic_type: models.SemanticType, *, @@ -7745,7 +7758,7 @@ def _is_phase6_raw_array_address_type(semantic_type: models.SemanticType) -> boo supported_element = _is_plan_primitive_value_type(semantic_type) or ( semantic_type.name == "String" and policy.itemsize is not None ) - return supported_element and all(item not in {":", "::Strided", "...", "Flat"} for item in policy.shape) + return supported_element and all(item not in RUNTIME_DIMENSION_MARKERS for item in policy.shape) def _is_raw_array_address_type(semantic_type: models.SemanticType) -> bool: @@ -7973,6 +7986,7 @@ def _semantic_prototype_argument_policy( owner_path=f"{owner_path}.prototype_argument.{argument.name}", name=argument.name, semantic_type_name=semantic_type.name, + native_fortran_type=_scalar_logical_argument_abi(argument)[1], rank=int(semantic_type.rank or 0), passed_by_value=bool(argument.origin.metadata.get("value")), intent=( @@ -7980,6 +7994,7 @@ def _semantic_prototype_argument_policy( if (intent := argument.origin.metadata.get(models.PROTOTYPE_INTENT_METADATA)) is not None else None ), + optional=argument.optional, character_length=_character_length(semantic_type), array=_array_handoff_policy(semantic_type) if int(semantic_type.rank or 0) > 0 else None, derived_type_identity=( @@ -8191,8 +8206,9 @@ def _argument_native_name( python_barrier_action=PythonBarrierAction.NONE, native_barrier_action=NativeBarrierAction.NONE, ) + semantic_function.metadata[models.PYTHON_EXPORTS_METADATA] = [{"namespace": (), "name": "scale"}] print(f"before: math.scale({semantic_argument.name}): {semantic_argument.semantic_type.name} semantic IR") - policy = build_function_wrapper_policy(semantic_function, owner_path="math.scale") + policy = build_function_wrapper_policy(semantic_function, owner_path="math.scale", module_export=True) print( f"after: {policy.arguments[0].bridge_data_action.value}; " f"result={policy.results[0].direct_result_abi.value}; " diff --git a/prik/policy/contract_imports.py b/prik/policy/contract_imports.py new file mode 100644 index 000000000..bd5581b90 --- /dev/null +++ b/prik/policy/contract_imports.py @@ -0,0 +1,214 @@ +"""Complete the names each generated contract binds from other modules. + +A contract has to bind every name it writes without declaring: a type or a +prototype its signatures name, a callable its declaration expressions call, the +module a procedure-local type is qualified by, and each name it publishes out of +another module. That, and not the ``use`` statements its source happened to +write, is what it imports. A ``use`` that only extends a generic the module +declares, or reaches a name no declaration mentions, binds nothing here. + +``complete_contract_imports`` replaces a module's ``imports`` with those +bindings, spelled both ways a contract is written: as the sources name each +side, and as the completed contracts do. The printer renders them and decides +nothing. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Iterator + +from prik.naming import normalize_public_name, preserves_source_case +from prik.policy.exports import ( + contract_name_for_source, + contract_names_by_source, + declaration_identity, + declared_identities, + imported_type_reference, +) +from prik.semantics import models +from prik.semantics.pyi_metadata import PYI_LOADED_METADATA + + +def complete_contract_imports( + modules: Iterable[models.SemanticModule], + *, + dependencies: Iterable[models.SemanticModule] = (), +) -> None: + """Replace each module's imports with the bindings its contract writes. + + Names must already be completed for ``modules`` and ``dependencies``: an + import asks the module it reads from for the name that module's contract + declares. ``dependencies`` are contracts completed alongside but not + written here. + """ + modules = list(modules) + completed = {module.name.casefold(): contract_names_by_source(module) for module in (*dependencies, *modules)} + for module in modules: + module.imports = _ContractImports(module, completed).bindings() + + +class _ContractImports: + """Bind each name one contract needs, once, from the one entity it names. + + Every binding passes through ``_bind``. A module does not import from + itself or import a declaration it already carries; a name reached twice + from the same entity binds once; and a name bound to two entities, or to + one while the contract declares another, cannot be written. + """ + + def __init__(self, module: models.SemanticModule, completed: dict[str, dict[str, str]]): + self._module = module + self._completed = completed + # A loaded contract already writes Python; only a native one is spelled. + self._native = not module.metadata.get(PYI_LOADED_METADATA) + self._preserve_case = not self._native or preserves_source_case(module.origin.source_language) + self._key = str if self._preserve_case else str.casefold + # The one spelling naming completed for each name this contract imports. + self._imported = module.metadata.get(models.CONTRACT_IMPORT_NAMES_METADATA, {}) if self._native else {} + declarations = ( + *module.functions, + *module.classes, + *module.variables, + *module.prototypes, + *module.overload_sets, + ) + self._declared_names = {self._key(models.completed_contract_name(item)) for item in declarations} + self._declared = declared_identities(module) + self._bound: dict[str, tuple[str, str]] = {} + self._statements: list[str | models.SemanticImport] = [] + self._from: dict[str, models.SemanticImport] = {} + + def bindings(self) -> list[str | models.SemanticImport]: + """Return the import statements the contract writes, in writing order. + + The contract's own imports come first, then the names it publishes in + the order its source reached them, then every name its declarations + refer to. Those are sorted, so reordering declarations never reorders + the imports they need. + """ + for statement in self._module.imports: + self._stated(statement) + for reexport in self._module.reexports: + # A name published out of another module is bound to be published. + if reexport.publishes_to_python() and reexport.origin_module: + self._bind( + str(reexport.origin_module), + str(reexport.source_name or reexport.local_name), + str(reexport.local_name), + verbatim=reexport.entity_kind == "prototype", + ) + for origin, source, local, written, kind in sorted(set(self._references())): + self._bind(origin, source, local, written=written, verbatim=kind in {"prototype", "namespace"}) + return self._statements + + def _stated(self, statement: str | models.SemanticImport) -> None: + """Carry one import the module states itself.""" + if isinstance(statement, str) or not statement.items: + self._statements.append(statement) + return + for item in statement.items: + self._bind(statement.module, item.source, item.target or item.source) + + def _references(self) -> Iterator[tuple[str, str, str, str, str]]: + """Yield ``(module, source, local, written, kind)`` for each name a declaration names. + + ``written`` is the spelling the declaration writes, which completion + recorded on the reference itself. + """ + for semantic_type in models._module_semantic_types(self._module): + yield from _type_reference(semantic_type) + yield from _prototype_reference(semantic_type.metadata.get(models.PROTOTYPE_REF_METADATA)) + yield from _callable_references(semantic_type) + + def _bind( + self, + origin: str, + source: str, + local: str, + *, + written: str | None = None, + verbatim: bool = False, + ) -> None: + """Bind ``local`` to ``source`` read from ``origin``, or refuse a second meaning. + + The contract binds the name as it writes it: ``written`` for a + reference completion already spelled, and otherwise the spelling + completion recorded for ``local``, which ``__all__`` writes too. + """ + origin_key = origin.lstrip(".").casefold() + if origin_key == self._module.name.casefold() or declaration_identity(origin_key, source) in self._declared: + return + contract_target = written or contract_name_for_source(self._imported, local) or local + key = self._key(contract_target) + identity = (origin_key, self._key(source)) + existing = self._bound.get(key) + if existing == identity: + return + if existing is not None or key in self._declared_names: + raise ValueError( + f"Contract for {self._module.name!r} cannot bind {local!r} to {origin}.{source}: " + "the name already means something else there" + ) + self._bound[key] = identity + module_text = f".{origin}" if self._native and not origin.startswith(".") else origin + statement = self._from.get(module_text) + if statement is None: + statement = self._from[module_text] = models.SemanticImport(module=module_text) + self._statements.append(statement) + contract_source = self._contract_source(origin_key, source, verbatim) + statement.items.append( + models.SemanticImportItem( + source=source, + target=None if local == source else local, + contract_source=contract_source, + contract_target=None if contract_target == contract_source else contract_target, + ) + ) + + def _contract_source(self, origin_key: str, source: str, verbatim: bool) -> str: + """Return the name the module read from declares for ``source``. + + Its own completion settled that. A module outside this completion is + spelled the way this one spells a name, except a prototype or a module, + which keep their spelling everywhere; a loaded contract already writes + Python and keeps every spelling. + """ + if not self._native: + return source + completed = contract_name_for_source(self._completed.get(origin_key), source) + if completed is not None: + return completed + return source if verbatim else normalize_public_name(source, preserve_case=self._preserve_case).name + + +def _type_reference(semantic_type: models.SemanticType) -> Iterator[tuple[str, str, str, str, str]]: + """Yield the binding one annotation naming an imported type needs.""" + reference = imported_type_reference(semantic_type) + if reference is None: + return + if reference.procedure_local: + # A procedure-local type is written qualified by its module. + yield ".", reference.module, reference.module, reference.module, "namespace" + else: + written = str(semantic_type.metadata.get(models.CONTRACT_NAME_METADATA) or reference.local) + yield reference.module, reference.name, reference.local, written, "type" + + +def _prototype_reference(ref: object) -> Iterator[tuple[str, str, str, str, str]]: + """Yield the binding one callback annotation naming a prototype needs.""" + if not isinstance(ref, dict): + return + origin = str(ref.get("origin_module") or "") + local = str(ref.get("local_name") or ref.get("name") or "") + if origin and local: + yield origin, str(ref.get("name") or local), local, local, "prototype" + + +def _callable_references(semantic_type: models.SemanticType) -> Iterator[tuple[str, str, str, str, str]]: + """Yield the binding each callable a declaration expression calls needs.""" + array = semantic_type.storage.array if semantic_type.storage is not None else None + for axis in array.expression_callables if array is not None else (): + for reference in axis: + if reference.native_scope is not None: + local = reference.name.rsplit(".", 1)[-1] + yield reference.native_scope, reference.native_name or local, local, local, "procedure" diff --git a/prik/policy/exports.py b/prik/policy/exports.py index 62c3808d9..3c58a6fb6 100644 --- a/prik/policy/exports.py +++ b/prik/policy/exports.py @@ -1,22 +1,27 @@ -"""Resolve Python export names for later wrapper-policy construction. +"""Resolve contract spellings and Python exports before later stages run. -``complete_python_export_policy`` walks public semantic declarations in their -lowering order, normalizes their requested names, and reserves one name in each -Python namespace. It writes the completed names back to semantic metadata so -all policy constructors see the same collision-checked result. +``complete_python_export_policy`` walks semantic declarations in lowering +order, completes public placement, then records the collision-checked spelling +the generated contract declares for every owner. Withheld declarations and +class members still need a contract identity even when they publish nothing. ``completed_python_exports`` retrieves that metadata as immutable ``PythonExportPolicy`` records while wrapper policy is assembled. This module -decides Python placement only: it does not choose a wrapper mechanism or emit -the namespace. +decides Python placement and contract spelling only: it does not choose a +wrapper mechanism or emit the namespace. """ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from typing import NamedTuple -from prik.naming import NamingPolicy, normalize_public_name +from prik.naming import NamingPolicy, normalize_public_name, preserves_source_case from prik.semantics import models +from prik.semantics.pyi_metadata import PYI_LOADED_METADATA +from prik.semantics.models import export_namespace +from prik.utilities.declaration_expressions import rename_declaration_expression_calls @dataclass(frozen=True) @@ -27,33 +32,222 @@ class PythonExportPolicy: name: str +def _stated_export_names(module: models.SemanticModule) -> set[str] | None: + """Return the surface one contract states, or ``None`` when it states none. + + A contract that writes no ``__all__`` publishes what it declares, so there + is nothing stated to read and every declaration is completed as before. An + empty list is a statement, not the absence of one: it says the module + publishes nothing. + + The names are compared exactly. A contract is Python, where ``Foo`` and + ``foo`` are different names, so a list naming ``Foo`` does not publish a + declaration written ``foo`` -- it names something the module does not + define. + """ + if module.exported_names is None: + return None + return {str(name) for name in module.exported_names} + + def complete_python_export_policy( module: models.SemanticModule, *, strict_wrapper_names: bool = False, ) -> None: - """Resolve every public export name within its owning Python namespace.""" - naming = NamingPolicy(strict_public_names=strict_wrapper_names) + """Resolve every public export name within its owning Python namespace. + + A module read from a semantic ``.pyi`` is already named in Python -- the + contract states the names it publishes -- so those spellings are kept + exactly. Only a module converted from native source has names PRIK must + choose, and only where the source language has no spelling of its own. + + Such a contract also states its whole surface in ``__all__``, which is the + authority on what it publishes. A declaration it leaves out stays written + and reachable, because annotations and imports resolve against it, and no + export is completed for it -- a prototype or a generic reads back public by + default, and completing one would publish what the contract declined to. + """ + contract_named = bool(module.metadata.get(PYI_LOADED_METADATA)) + complete_reexport_publication_policy(module, contract_named=contract_named) + stated = _stated_export_names(module) + naming = NamingPolicy( + strict_public_names=strict_wrapper_names, + preserve_case=contract_named or preserves_source_case(module.origin.source_language), + ) for owner in _module_export_owners(module): - if getattr(owner, "visibility", "public") == "private": + metadata = owner.metadata + if getattr(owner, "visibility", "public") == "private" or ( + stated is not None and str(owner.name) not in stated + ): + # Completion states every owner's decision, publishing nowhere + # included, so no later reader is left to answer it differently. + metadata.setdefault(models.PYTHON_EXPORTS_METADATA, []) continue - metadata = _owner_metadata(owner) exports = metadata.get(models.PYTHON_EXPORTS_METADATA) - if not exports: + if exports is None: + # No earlier stage placed this declaration, so it publishes itself + # in its own namespace. An empty list is not that: it is a stage + # having decided the declaration publishes nothing. exports = [{"namespace": (), "name": None}] metadata[models.PYTHON_EXPORTS_METADATA] = exports category = _owner_category(owner) for export in exports: - namespace = _export_namespace(export) + namespace = export_namespace(export) raw_name = owner.name if export.get("name") is None else export["name"] resolved_name = naming.reserve_public_name( namespace, raw_name, - category=category, + category="function" if contract_named else category, owner=f"{category} {owner.name}", ) - if export.get("name") is None: - export["name"] = resolved_name + export["name"] = resolved_name + # A nested class is bound on its parent class, never in a namespace. + for parent in _all_classes(module.classes): + for nested in parent.classes: + nested.metadata.setdefault(models.PYTHON_EXPORTS_METADATA, []) + _complete_reexport_names(module, naming, contract_named=contract_named) + _complete_contract_names( + module, + strict_wrapper_names=strict_wrapper_names, + contract_named=contract_named, + ) + + +#: Entity kinds a second namespace cannot publish, whatever it may reach. +#: +#: A generic dispatcher has no single object another namespace can bind, so it +#: is published where it is declared and nowhere else. An intrinsic module's +#: name has no declaration at all, so nothing is there to publish. +UNPUBLISHABLE_REEXPORT_KINDS = frozenset({"generic", "intrinsic"}) + + +def complete_reexport_publication_policy( + module: models.SemanticModule, + *, + contract_named: bool | None = None, +) -> None: + """Complete which public use associations become Python publications. + + Native Fortran keeps declaration dependencies semantically accessible but + does not expose them in the generated Python namespace unless an explicit + ``public`` statement names them. A loaded contract has already stated its + export surface, so every re-export record constructed from that surface is + published. + + A generic is reachable through the importing module like any other name, + but it dispatches rather than naming one object, so PRIK publishes it in + its declaring namespace alone. That is a publication decision, settled here + once, rather than an accessibility one. + """ + if contract_named is None: + contract_named = bool(module.metadata.get(PYI_LOADED_METADATA)) + for reexport in module.reexports: + if reexport.python_exported is not None: + continue + if reexport.entity_kind in UNPUBLISHABLE_REEXPORT_KINDS: + reexport.python_exported = False + continue + reexport.python_exported = bool( + contract_named or not reexport.declaration_dependency or reexport.explicitly_public + ) + + +def _complete_reexport_names( + module: models.SemanticModule, + naming: NamingPolicy, + *, + contract_named: bool, +) -> None: + """Name each use-associated binding in its importing namespace. + + Published associations add runtime attributes; dependency-only associations + still add contract imports. Both compete with declarations for a Python + spelling, so the same ledger names them after the module's declarations. + The spelling follows the entity, whether or not it is published: a type is + spelled as a class wherever it is written, and a prototype keeps the + spelling it is declared with. A name the module's own declarations use as a + type is one, even where the module declaring it was not read. + """ + types = { + reference.local.casefold() + for reference in map(imported_type_reference, models._module_semantic_types(module)) + if reference is not None and not reference.procedure_local + } + for reexport in module.reexports: + if reexport.python_name: + continue + published = reexport.publishes_to_python() + if published and reexport.entity_kind == "variable": + completed_name = _completed_variable_reexport_name(module, reexport) + if completed_name is not None: + reexport.python_name = completed_name + continue + namespace = _reexport_namespace(module, reexport) + owner = f"re-export {reexport.local_name}" + if reexport.entity_kind == "prototype": + reexport.python_name = naming.hold_completed_public_name( + namespace, reexport.local_name, category="function", owner=owner + ) + continue + kind = "derived_type" if str(reexport.local_name).casefold() in types else reexport.entity_kind + category = {"derived_type": "class", "variable": "variable"}.get(kind, "function") + reexport.python_name = naming.reserve_public_name( + namespace, + reexport.local_name, + category="function" if contract_named else category, + owner=owner, + ) + + +def _completed_variable_reexport_name( + module: models.SemanticModule, + reexport: models.SemanticReexport, +) -> str | None: + """Read a variable re-export name from its declaring variable policy. + + A merged source build contains the declaring variable, whose export list is + the authority for every publication. Contract extraction may emit an + importing module separately, in which case the declaration is unavailable + and the re-export is named locally instead. + """ + wanted_module = str(reexport.origin_module).casefold() + wanted_name = str(reexport.source_name).casefold() + namespace = _reexport_namespace(module, reexport) + for variable in module.variables: + native_module = str(variable.origin.native_scope or "").casefold() + native_name = str(variable.origin.native_name or variable.name).casefold() + if native_module != wanted_module or native_name != wanted_name: + continue + for export in variable.metadata.get(models.PYTHON_EXPORTS_METADATA, ()): + if export_namespace(export) == namespace and export.get("name") is not None: + return str(export["name"]) + return None + + +def _placement_namespace(module: models.SemanticModule, scope: object) -> tuple[str, ...]: + """Return the namespace a name written by module ``scope`` is placed in. + + Completing that same module places it at the module's own root. Completing + a merged package -- a build folds every source module into one -- places it + inside the namespace that module occupies there, beside the declarations it + sits with, so names from two modules never compete for one spelling. + """ + declaring = str(scope or "") + if not declaring or declaring.casefold() == str(module.name).casefold(): + return () + return tuple(part.casefold() for part in declaring.split(".") if part) + + +def _reexport_namespace(module: models.SemanticModule, reexport: models.SemanticReexport) -> tuple[str, ...]: + """Return the Python namespace one re-export publishes into: its publisher's.""" + return _placement_namespace(module, reexport.module) + + +def _declaring_namespace(module: models.SemanticModule, owner) -> tuple[str, ...]: + """Return the namespace of the module one declaration is written in.""" + scope = owner.native_scope if isinstance(owner, models.ProcedureOverloadSet) else owner.origin.native_scope + return _placement_namespace(module, scope) def _module_export_owners(module: models.SemanticModule): @@ -61,11 +255,513 @@ def _module_export_owners(module: models.SemanticModule): return (*module.classes, *module.functions, *module.overload_sets, *module.variables) -def _owner_metadata(owner) -> dict[str, object]: - """Return the metadata mapping that owns one export policy.""" - if isinstance(owner, models.ProcedureOverloadSet): - return owner.procedures[0].metadata if owner.procedures else {} - return owner.metadata +def _complete_contract_names( + module: models.SemanticModule, + *, + strict_wrapper_names: bool, + contract_named: bool, +) -> None: + """Record every declaration spelling consumed by contract emission. + + A name has to be unique among the names written in one contract, so each + is held where it is placed: a published declaration in the namespace its + export completed, a re-export in its publisher's, a withheld declaration + in the file being completed. A build folds every source module into one, + and placing by those authorities keeps two modules' names apart there. + Published spellings are held first, so a withheld helper cannot move a + public API aside; each class then gets one member ledger, shared with + class-surface policy. + """ + preserve_case = contract_named or preserves_source_case(module.origin.source_language) + naming = NamingPolicy(strict_public_names=strict_wrapper_names, preserve_case=preserve_case) + owners = _module_export_owners(module) + + for owner in owners: + placed = _own_export(module, owner) + if placed is None: + continue + namespace, completed = placed + naming.hold_completed_public_name( + namespace, + completed, + category=_owner_category(owner), + owner=f"{_owner_category(owner)} {owner.name}", + ) + owner.metadata[models.CONTRACT_NAME_METADATA] = completed + + # Imports bind names in the same contract namespace as declarations. Their + # export spelling was already settled against public declarations; holding + # it here prevents a withheld declaration from taking the binding. + for reexport in module.reexports: + if reexport.python_name: + naming.hold_completed_public_name( + _reexport_namespace(module, reexport), + reexport.python_name, + category="function", + owner=f"re-export {reexport.local_name}", + ) + + imported = _complete_imported_names(module, naming, contract_named=contract_named) + + # A module's own prototype declares the name other modules import, so it + # is spelled before any withheld declaration takes a name. + _complete_prototype_contract_names(module, naming, (item for item in module.prototypes if not item.declaring_scope)) + + # A withheld declaration is written in the file being completed, whatever + # module declared it natively: a generic's inherited specifics are carried + # into the facade that extends it, beside each other. + for owner in owners: + if owner.metadata.get(models.CONTRACT_NAME_METADATA) is not None: + continue + owner.metadata[models.CONTRACT_NAME_METADATA] = naming.reserve_public_name( + (), + owner.name, + category=_owner_category(owner), + owner=f"{_owner_category(owner)} {owner.name}", + ) + + # A block inside a procedure is that procedure's alone, spelled after the + # module's own declarations and qualified by its scope. + _complete_prototype_contract_names(module, naming, (item for item in module.prototypes if item.declaring_scope)) + _complete_prototype_reference_names(module) + + for semantic_class in module.classes: + _complete_class_member_contract_names( + semantic_class, + (*_declaring_namespace(module, semantic_class), models.completed_contract_name(semantic_class)), + strict_wrapper_names=strict_wrapper_names, + preserve_case=preserve_case, + ) + + _complete_type_reference_names(module, imported) + _complete_declared_callable_names(module, contract_named=contract_named) + _complete_overload_target_contract_names(module, preserve_case=preserve_case) + + +def _complete_prototype_contract_names(module: models.SemanticModule, naming: NamingPolicy, prototypes) -> None: + """Spell each prototype in the module's contract ledger, as it is declared. + + A prototype is identified by its declaring scope and the name that scope + gives it, and two contained procedures may give theirs the same name, so a + procedure-local one suggests its scope with its name. The spelling keeps + the case it is declared in, wherever the prototype is written. + """ + for prototype in prototypes: + suggestion = "_".join((*prototype.declaring_scope, str(prototype.native_name or prototype.name))) + prototype.metadata[models.CONTRACT_NAME_METADATA] = naming.reserve_public_name( + _declaring_namespace(module, prototype), + suggestion, + category="function", + owner=f"prototype {suggestion}", + preserve_case=True, + ) + + +def _complete_prototype_reference_names(module: models.SemanticModule) -> None: + """Spell each callback annotation the way the contract names its prototype. + + A prototype the module declares is named by its completed spelling. One it + imports keeps the name this module binds it under, which a prototype keeps + wherever it is written. + """ + declared = { + _prototype_identity( + prototype.origin.native_scope or module.name, + prototype.declaring_scope, + prototype.native_name or prototype.name, + ): models.completed_contract_name(prototype) + for prototype in module.prototypes + } + for semantic_type in models._module_semantic_types(module): + reference = semantic_type.metadata.get(models.PROTOTYPE_REF_METADATA) + if not isinstance(reference, dict): + continue + identity = _prototype_identity( + reference.get("origin_module", ""), reference.get("declaring_scope", ()), reference.get("name", "") + ) + written = str(reference.get("local_name") or semantic_type.name) + semantic_type.metadata[models.CONTRACT_NAME_METADATA] = declared.get(identity, written) + + +def _prototype_identity(module_name: object, scope, name: object) -> tuple[str, tuple[str, ...], str]: + """Return one prototype's ``(module, declaring scope, name)`` identity.""" + return str(module_name).casefold(), tuple(str(part) for part in scope), str(name).casefold() + + +def _own_export(module: models.SemanticModule, owner) -> tuple[tuple[str, ...], str] | None: + """Return the namespace and spelling one declaration is published under at home. + + Its home is the namespace of the module it is written in, or the root of + the file being completed; an export elsewhere is a second publication of + it, which names nothing in its own contract. + """ + home = {(), _declaring_namespace(module, owner)} + for export in owner.metadata.get(models.PYTHON_EXPORTS_METADATA, ()) or (): + if not isinstance(export, dict) or export.get("name") is None: + continue + namespace = tuple(part.casefold() for part in export_namespace(export)) + if namespace in home: + return namespace, str(export["name"]) + return None + + +def _all_classes(classes: list[models.SemanticClass]): + """Yield every class, each followed by the classes nested inside it.""" + for semantic_class in classes: + yield semantic_class + yield from _all_classes(semantic_class.classes) + + +def _complete_class_member_contract_names( + semantic_class: models.SemanticClass, + namespace: tuple[str, ...], + *, + strict_wrapper_names: bool, + preserve_case: bool, +) -> None: + """Complete one class's field, method, and overload spellings once.""" + naming = NamingPolicy(strict_public_names=strict_wrapper_names, preserve_case=preserve_case) + for field in semantic_class.fields: + field.metadata[models.CONTRACT_NAME_METADATA] = naming.reserve_public_name( + namespace, + field.name, + category="field", + owner=field.name, + ) + for method in semantic_class.methods: + if method.name.startswith("__"): + method.metadata[models.CONTRACT_NAME_METADATA] = method.name + continue + method.metadata[models.CONTRACT_NAME_METADATA] = naming.reserve_public_name( + namespace, + method.name, + category="function", + owner=method.name, + ) + for overload in semantic_class.overload_sets: + source_names = tuple( + dict.fromkeys( + str(procedure.metadata.get(models.PYTHON_METHOD_NAME_METADATA, overload.name)) + for procedure in overload.procedures + ) + ) or (str(overload.name),) + for source_name in source_names: + completed = naming.reserve_public_name( + namespace, + source_name, + category="function", + owner=source_name, + ) + overload.metadata.setdefault(models.CONTRACT_NAME_METADATA, completed) + for procedure in overload.procedures: + procedure_name = str(procedure.metadata.get(models.PYTHON_METHOD_NAME_METADATA, overload.name)) + if procedure_name == source_name: + procedure.metadata[models.CONTRACT_NAME_METADATA] = completed + # A nested class is written inside its parent, so it is named among the + # parent's members and its own members get a ledger beneath that name. + for nested in semantic_class.classes: + nested.metadata[models.CONTRACT_NAME_METADATA] = naming.reserve_public_name( + namespace, + nested.name, + category="class", + owner=nested.name, + ) + _complete_class_member_contract_names( + nested, + (*namespace, models.completed_contract_name(nested)), + strict_wrapper_names=strict_wrapper_names, + preserve_case=preserve_case, + ) + + +def _complete_imported_names( + module: models.SemanticModule, + naming: NamingPolicy, + *, + contract_named: bool, +) -> dict[str, str]: + """Record the one spelling the contract writes for each name it imports. + + A re-export is already named: the name the module publishes it under is the + name the contract binds and writes. A type the module imports without + re-exporting it takes the class spelling a published type would, and a + callable a declaration expression calls the spelling a function would, each + held beside the module's own names. A contract that was read already names + what it imports and keeps every spelling. + + The record is read back when completion runs again: by then the calls it + spelled carry their completed names, which are not names to import. + """ + recorded = module.metadata.get(models.CONTRACT_IMPORT_NAMES_METADATA) + if recorded is not None: + return recorded + completed = {str(reexport.local_name): str(reexport.python_name) for reexport in module.reexports} + if not contract_named: + declared = declared_identities(module) + for local, category in _imported_local_names(module, declared): + if contract_name_for_source(completed, local) is None: + completed[local] = naming.reserve_public_name((), local, category=category, owner=f"import {local}") + + def imported_spelling(reference: models.SemanticExpressionCallable) -> str | None: + identity = _callable_identity(reference) + if identity is None or identity in declared: + return None + return contract_name_for_source(completed, reference.name) + + # A call to an imported callable is spelled now, once: afterwards its + # reference carries the completed name, which is not a name it imports. + _respell_expression_calls(module, imported_spelling) + module.metadata[models.CONTRACT_IMPORT_NAMES_METADATA] = completed + return completed + + +def _imported_local_names(module: models.SemanticModule, declared: set[tuple[str, str]]): + """Yield ``(local name, category)`` for each name a declaration reads from another module.""" + for semantic_type in models._module_semantic_types(module): + reference = imported_type_reference(semantic_type) + if ( + reference is not None + and not reference.procedure_local + and declaration_identity(reference.module, reference.name) not in declared + ): + yield reference.local, "class" + for callable_reference in _expression_callables(semantic_type): + identity = _callable_identity(callable_reference) + if identity is not None and identity not in declared: + yield callable_reference.name, "function" + + +def _complete_declared_callable_names(module: models.SemanticModule, *, contract_named: bool) -> None: + """Spell each call to a callable the module declares under that callable's contract name. + + The reference and the call in the public shape change together, so the + expression and the declaration it calls agree; the native identity stays + beside them. Imported calls were spelled with the names the module imports + them by, and every name a read contract writes is kept. + """ + if contract_named: + return + declared = { + declaration_identity(item.origin.native_scope or module.name, item.native_name or item.name): ( + models.completed_contract_name(item) + ) + for item in (*module.functions, *module.prototypes) + } + _respell_expression_calls(module, lambda reference: declared.get(_callable_identity(reference))) + + +def _respell_expression_calls( + module: models.SemanticModule, + spelling: Callable[[models.SemanticExpressionCallable], str | None], +) -> None: + """Give each call a declaration expression makes the spelling ``spelling`` returns. + + A reference and its call sites change together; ``None`` keeps a call as it + is written. Only call targets change in the expression text. + """ + for semantic_type in models._module_semantic_types(module): + array = semantic_type.storage.array if semantic_type.storage is not None else None + for axis, references in enumerate(array.expression_callables if array is not None else ()): + names: dict[str, str] = {} + for reference in references: + completed = spelling(reference) + if completed is not None and completed != reference.name: + names[reference.name] = completed + reference.name = completed + if names: + for shape in (semantic_type.shape, array.shape): + if axis < len(shape): + shape[axis] = rename_declaration_expression_calls(str(shape[axis]), names) + + +def _expression_callables(semantic_type: models.SemanticType): + """Yield every callable one type's declaration expressions call.""" + array = semantic_type.storage.array if semantic_type.storage is not None else None + for references in array.expression_callables if array is not None else (): + yield from references + + +def _callable_identity(reference: models.SemanticExpressionCallable) -> tuple[str, str] | None: + """Return the declaration one call reaches, or ``None`` for a call with no module.""" + if reference.native_scope is None: + return None + return declaration_identity(reference.native_scope, reference.native_name or reference.name.rsplit(".", 1)[-1]) + + +def declared_identities(module: models.SemanticModule) -> set[tuple[str, str]]: + """Return the ``(module, name)`` identity of every declaration the module carries.""" + return { + *( + declaration_identity( + item.origin.native_scope or module.name, getattr(item, "native_name", None) or item.name + ) + for item in (*module.functions, *module.classes, *module.variables, *module.prototypes) + ), + *(declaration_identity(item.native_scope or module.name, item.name) for item in module.overload_sets), + } + + +def declaration_identity(scope: object, name: object) -> tuple[str, str]: + """Return the case-folded ``(module, name)`` identity of one declaration.""" + return str(scope).casefold(), str(name).casefold() + + +def _complete_type_reference_names(module: models.SemanticModule, imported: dict[str, str]) -> None: + """Spell every type a declaration names the way the contract binds it. + + A class the module declares is written under its contract name, and an + imported one under the name the module imports it by, so an annotation, the + import binding its name, and ``__all__`` write one spelling. + """ + classes = tuple(_all_classes(module.classes)) + declared = {str(cls.name): models.completed_contract_name(cls) for cls in classes} + # A build merges modules, so a type one of them imports can be declared here. + declared_by_identity = { + declaration_identity(cls.origin.native_scope or module.name, cls.native_name or cls.name): ( + models.completed_contract_name(cls) + ) + for cls in classes + } + for semantic_type in models._module_semantic_types(module): + reference = imported_type_reference(semantic_type) + if reference is None: + completed = contract_name_for_source(declared, semantic_type.name) + elif reference.procedure_local: + continue + elif declaration_identity(reference.module, reference.name) in declared_by_identity: + completed = declared_by_identity[declaration_identity(reference.module, reference.name)] + else: + completed = contract_name_for_source(imported, reference.local) + if completed is not None: + semantic_type.metadata[models.CONTRACT_NAME_METADATA] = completed + # A base is named, not annotated: the class it names is declared here or imported. + for semantic_class in _all_classes(module.classes): + semantic_class.metadata[models.CONTRACT_BASE_NAMES_METADATA] = { + base: contract_name_for_source(declared, base) or contract_name_for_source(imported, base) or base + for base in semantic_class.base_classes + } + + +class ImportedTypeReference(NamedTuple): + """One annotation naming a type another module declares.""" + + module: str + name: str + local: str + procedure_local: bool + + +def imported_type_reference(semantic_type: models.SemanticType) -> ImportedTypeReference | None: + """Return the imported type one annotation names, or ``None``. + + A procedure-local type is written qualified by its module, so only the + module is bound for it; a type whose local name is already qualified names + a module the contract imports itself. + """ + ref = semantic_type.metadata.get(models.EXTERNAL_TYPE_REF_METADATA) + if not isinstance(ref, dict): + return None + module, name = ref.get("origin_module"), ref.get("name") + local = ref.get("local_name") or name + if not all(isinstance(value, str) and value for value in (module, name, local)): + return None + procedure_local = ref.get("import_scope") == "procedure" + if not procedure_local and "." in local: + return None + return ImportedTypeReference(module, name, local, procedure_local) + + +def contract_name_for_source(completed: dict[str, str] | None, source: object) -> str | None: + """Return the completed contract spelling for one source name. + + A contract records each name exactly as its source spells it, so two + declarations a case-sensitive language keeps apart keep separate entries. + A case-insensitive source may ask under any spelling, which is answered + only when one entry can mean it: where several fold together the request + names no single declaration, and guessing one would depend on the order + they happened to be recorded in. + """ + if not completed: + return None + wanted = str(source) + exact = completed.get(wanted) + if exact is not None: + return exact + folded = wanted.casefold() + matches = [value for key, value in completed.items() if key.casefold() == folded] + return matches[0] if len(matches) == 1 else None + + +def _complete_overload_target_contract_names( + module: models.SemanticModule, + *, + preserve_case: bool, +) -> None: + """Resolve overload targets to the contract spelling of their specific. + + A target is written the way the contract's reader resolves it: against the + module's own procedures first, then the methods of the type whose generic + it is. One procedure can be declared both ways -- ``counter_add_integer`` + at module level, ``add_integer`` as the method binding it -- and they are + reached differently, so the contract has to name the one its reader finds. + """ + _name_overload_targets(module.overload_sets, (module.functions,), preserve_case=preserve_case) + for semantic_class in _all_classes(module.classes): + _name_overload_targets( + semantic_class.overload_sets, + (module.functions, semantic_class.methods), + preserve_case=preserve_case, + ) + + +def _name_overload_targets( + overloads: list[models.ProcedureOverloadSet], + specific_groups: tuple[list[models.SemanticFunction], ...], + *, + preserve_case: bool, +) -> None: + """Record, on each candidate, the spelling its specific is declared under.""" + by_identity: dict[tuple[str, str], str] = {} + by_source: dict[str, str] = {} + for specifics in specific_groups: + for specific in specifics: + identity = _specific_identity(specific) + if identity is not None: + by_identity.setdefault(identity, models.completed_contract_name(specific)) + by_source.setdefault(str(specific.name), models.completed_contract_name(specific)) + for overload in overloads: + for candidate in overload.procedures: + target = str( + candidate.metadata.get(models.OVERLOAD_TARGET_METADATA) or candidate.native_name or candidate.name + ) + scope = str(candidate.origin.native_scope or "").casefold() + completed = by_identity.get((scope, target.casefold())) or by_source.get(target) + if completed is None: + completed = normalize_public_name(target, preserve_case=preserve_case).name + candidate.metadata[models.CONTRACT_TARGET_NAME_METADATA] = completed + + +def _specific_identity(function: models.SemanticFunction) -> tuple[str, str] | None: + """Return the native declaration identity used by an overload target.""" + scope = str(function.origin.native_scope or "") + native = str(function.native_name or function.name) + if not scope or not native: + return None + return scope.casefold(), native.casefold() + + +def contract_names_by_source(module: models.SemanticModule) -> dict[str, str]: + """Return source spellings mapped to the names this contract declares.""" + names = {str(owner.name): models.completed_contract_name(owner) for owner in _module_export_owners(module)} + names.update( + (str(prototype.name), models.completed_contract_name(prototype)) + for prototype in module.prototypes + if not prototype.declaring_scope + ) + names.update( + (str(reexport.local_name), str(reexport.python_name or reexport.local_name)) for reexport in module.reexports + ) + return names def _owner_category(owner) -> str: @@ -77,21 +773,21 @@ def _owner_category(owner) -> str: return "function" -def _export_namespace(export: dict[str, object]) -> tuple[str, ...]: - """Return one normalized namespace tuple from semantic export metadata.""" - raw_namespace = export.get("namespace", ()) - if not isinstance(raw_namespace, tuple | list): - return () - return tuple(str(part) for part in raw_namespace) +def completed_python_exports(owner) -> tuple[PythonExportPolicy, ...]: + """Return the placements completion recorded for one declaration. - -def completed_python_exports( - owner: models.SemanticFunction | models.SemanticVariable, - default_name: str, -) -> tuple[PythonExportPolicy, ...]: - """Return stable local names grouped by their completed namespace path.""" + This reads the decision and never makes it. Completion records one for + every declaration it reaches, publishing nowhere included, so an empty + result is an answer; a missing one means completion never ran. + """ + recorded = owner.metadata.get(models.PYTHON_EXPORTS_METADATA) + if recorded is None: + raise ValueError( + f"Python export policy for {owner.name!r} is incomplete; " + "run complete_semantic_policies before wrapper planning" + ) exports = [] - for item in owner.metadata.get(models.PYTHON_EXPORTS_METADATA, ()): + for item in recorded: if not isinstance(item, dict): continue name = item.get("name") @@ -100,14 +796,7 @@ def completed_python_exports( f"Python export policy for {owner.name!r} is incomplete; " "run complete_semantic_policies before wrapper planning" ) - exports.append( - PythonExportPolicy( - namespace=_export_namespace(item), - name=str(name), - ) - ) - if not exports and getattr(owner, "visibility", "public") != "private": - exports.append(PythonExportPolicy((), normalize_public_name(default_name).name)) + exports.append(PythonExportPolicy(namespace=export_namespace(item), name=str(name))) return tuple(dict.fromkeys(exports)) @@ -123,7 +812,7 @@ def completed_python_exports( ) example_module = models.SemanticModule("math", functions=[example_function]) complete_python_export_policy(example_module) - example_export = completed_python_exports(example_function, example_function.name)[0] + example_export = completed_python_exports(example_function)[0] print(f"Native semantic owner: {example_module.name}.{example_function.native_name}") print(f"Python export: {'.'.join((*example_export.namespace, example_export.name))}") diff --git a/prik/policy/models.py b/prik/policy/models.py index b1c93bff2..c9ce72bda 100644 --- a/prik/policy/models.py +++ b/prik/policy/models.py @@ -44,7 +44,6 @@ ) DERIVED_VALUE_COPY_REASON = "pass an exact derived pointee through a typed native value dummy" LOGICAL_SCALAR_KIND_COPY_REASON = "adapt a C-interoperable Boolean through storage with the native Fortran logical kind" -LOGICAL_ARRAY_KIND_COPY_REASON = "adapt a one-byte Boolean array through storage with the native Fortran logical kind" class OptionalMode(str, Enum): @@ -207,14 +206,6 @@ class DirectResultABI(str, Enum): LOGICAL_LOW_BIT_INT8 = "logical_low_bit_int8" -class ArrayWritebackABI(str, Enum): - """Completed post-call element ABI for one mutable ordinary array.""" - - NOT_APPLICABLE = "not_applicable" - NATIVE_ARRAY = "native_array" - LOGICAL_LOW_BIT_INT8 = "logical_low_bit_int8" - - class ScalarLogicalABI(str, Enum): """Completed scalar logical adaptation between the C and native dummies.""" @@ -228,7 +219,6 @@ class ArrayLogicalABI(str, Enum): NOT_APPLICABLE = "not_applicable" C_BOOL_VIEW = "c_bool_view" - NATIVE_KIND_COPY = "native_kind_copy" class WritebackPhase(str, Enum): @@ -283,6 +273,14 @@ class CallbackTransferAction(str, Enum): BORROW_WRITABLE = "borrow_writable" +class CallbackOptionalityAction(str, Enum): + """Completed presence ABI for one callback dummy.""" + + REQUIRED = "required" + NULL_DATA_POINTER = "null_data_pointer" + BLOCKED = "blocked" + + class CallbackResultAction(str, Enum): """Typed result conversion performed by one callback trampoline.""" @@ -1007,6 +1005,9 @@ class LifecyclePolicy: semantic_type_name: str result_position: int object_kind: ObjectKind + # The handoff of the transfer this action belongs to, which is what makes + # the value a derived object; its type's spelling does not. + derived: DerivedHandoffPolicy | None operation: LifecycleOperation = LifecycleOperation.WRITEBACK @@ -1052,9 +1053,11 @@ class ProcedurePrototypeArgumentPolicy: owner_path: str name: str semantic_type_name: str + native_fortran_type: str | None rank: int passed_by_value: bool intent: str | None + optional: bool character_length: int | None array: ArrayHandoffPolicy | None derived_type_identity: tuple[str, str] | None @@ -1242,10 +1245,12 @@ class CallbackTransferPolicy: owner_path: str name: str semantic_type_name: str + native_fortran_type: str | None object_kind: ObjectKind rank: int passed_by_value: bool intent: str | None + optionality: CallbackOptionalityAction abi: CallbackABIKind adapter_action: CallbackTransferAction python_action: PythonBarrierAction @@ -1294,9 +1299,6 @@ class ArgumentPolicy: scalar_native_type: str | None array_logical_abi: ArrayLogicalABI array_native_type: str | None - array_copy_in: bool - array_copy_out: bool - array_writeback_abi: ArrayWritebackABI optional: bool optional_mode: OptionalMode conversion_phase: ArgumentConversionPhase @@ -1418,8 +1420,6 @@ class NativeCallSlotPolicy: scalar_native_type: str | None = None array_logical_abi: ArrayLogicalABI = ArrayLogicalABI.NOT_APPLICABLE array_native_type: str | None = None - array_copy_in: bool = False - array_copy_out: bool = False literal_type: str | None = None literal_value: Any = None result_position: int | None = None @@ -1500,6 +1500,7 @@ class FunctionWrapperPolicy: semantic_type_name="Float64", result_position=0, object_kind=ObjectKind.NUMPY_ARRAY, + derived=None, ) print(f"Array policy: rank={example_array.rank}, shape={example_array.shape}, order={example_array.order}") diff --git a/prik/printers/fortran.py b/prik/printers/fortran.py index 592739d82..99779206f 100644 --- a/prik/printers/fortran.py +++ b/prik/printers/fortran.py @@ -11,6 +11,7 @@ import textwrap +from prik.utilities.declaration_expressions import split_top_level_expression from prik.codegen.nodes import ( FortranAllocate, FortranAssignment, @@ -593,17 +594,19 @@ def _continued_item_ending(self, last_item: bool, last_argument: bool, suffix: s return suffix if last_argument else ", &" def _array_constructor_items(self, expression: str) -> tuple[str, ...] | None: - """Parse a simple bracketed constructor into item text, or return None. + """Parse a bracketed constructor into item text, or return None. - This intentionally recognizes only the shallow layout form used by the - continuation renderer; nested semantic expression parsing belongs earlier. + Items are separated at the constructor's own commas, so a nested call + or a character literal holding a comma stays one item. Breaking a line + inside a literal would change the characters it states; this helper + performs layout only, and nested semantic parsing belongs earlier. """ if not (expression.startswith("[") and expression.endswith("]")): return None content = expression[1:-1] if not content: return None - return tuple(item.strip() for item in content.split(",")) + return tuple(item.strip() for item in split_top_level_expression(content, ",")) def _parenthesized_items( self, @@ -611,15 +614,18 @@ def _parenthesized_items( *, minimum_items: int = 2, ) -> tuple[str, tuple[str, ...]] | None: - """Parse one shallow parenthesized value into its name and item texts. + """Parse one parenthesized value into its name and item texts. - The optional minimum keeps callers from expanding short forms. Unmatched, - nameless, or too-short expressions return None and remain opaque source. + Items are separated at this value's own commas, so a nested call or a + character literal holding a comma stays one item and no continuation + lands inside it. The optional minimum keeps callers from expanding short + forms. Unmatched, nameless, or too-short expressions return None and + remain opaque source. """ opening = expression.find("(") if opening < 1 or not expression.endswith(")"): return None - items = tuple(item.strip() for item in expression[opening + 1 : -1].split(",")) + items = tuple(item.strip() for item in split_top_level_expression(expression[opening + 1 : -1], ",")) if len(items) < minimum_items: return None return expression[:opening], items diff --git a/prik/printers/pyi.py b/prik/printers/pyi.py index b7f066ef9..860aadec5 100644 --- a/prik/printers/pyi.py +++ b/prik/printers/pyi.py @@ -18,7 +18,7 @@ from prik.codegen.primitive_scalar_types import NumpyDtypeRegistry from prik.contracts import CONTRACT_SYMBOLS, CONTRACT_TYPE_NAMES -from prik.naming import NamingPolicy +from prik.utilities.declaration_expressions import fortran_character_value, outside_character_literals from prik.semantics.scalar_types import SEMANTIC_SCALAR_TYPE_NAMES from prik.semantics.ownership_metadata import ( OWNERSHIP_POLICY_METADATA, @@ -39,7 +39,9 @@ USER_PRIVATE_METADATA, ) from prik.semantics.models import ( - EXTERNAL_TYPE_REF_METADATA, + CONTRACT_BASE_NAMES_METADATA, + CONTRACT_NAME_METADATA, + CONTRACT_TARGET_NAME_METADATA, FORTRAN_GENERIC_NAME_METADATA, OVERLOAD_KIND_METADATA, OVERLOAD_TARGET_METADATA, @@ -51,6 +53,7 @@ PYTHON_VALUE_MUTABILITY_METADATA, PROTOTYPE_INTENT_METADATA, PROTOTYPE_REF_METADATA, + UNRESOLVED_PROCEDURE_INTERFACE_METADATA, RUNTIME_RELEASE_GIL_METADATA, HIDDEN_NATIVE_OUTPUT_METADATA, RUNTIME_STATUS_ERROR_METADATA, @@ -63,9 +66,11 @@ SemanticDestructor, SemanticFunction, SemanticImport, + completed_contract_name, SemanticImportItem, SemanticMethod, SemanticModule, + SemanticReexport, SemanticPrototype, SemanticStorageContract, SemanticType, @@ -73,6 +78,7 @@ _module_semantic_types, ) from prik.semantics.native_array_handles import native_array_data_type, native_array_descriptor_kind +from prik.semantics.pyi_metadata import PYI_LOADED_METADATA from prik.utilities.visitor import ClassVisitor _WRAPPED_CALLABLE_TYPE_METADATA = "pyi_wrapped_callable_type" @@ -85,13 +91,11 @@ class _PyiEmissionContext: """Own all state accumulated while rendering one semantic node tree.""" - normalize_fortran_public_names: bool + normalize_public_names: bool default_array_order: str | None = None semantic_class_names: frozenset[str] = frozenset() contract_aliases: dict[str, str] = field(default_factory=dict) contract_imports: set[str] = field(default_factory=set) - naming_policy: NamingPolicy = field(default_factory=NamingPolicy) - reserved_public_names: dict[tuple[tuple[str, ...], str, object], str] = field(default_factory=dict) public_namespace: tuple[str, ...] = () def contract(self, name: str) -> str: @@ -111,21 +115,6 @@ def inside_class(self, name: str) -> _PyiEmissionContext: """Return a child namespace view sharing this emission's accumulators.""" return replace(self, public_namespace=(*self.public_namespace, name)) - def public_name(self, raw_name: str, *, category: str, owner: object) -> str: - """Reserve and return one normalized name inside the current namespace.""" - key = (self.public_namespace, category, self._public_owner_key(owner)) - reserved = self.reserved_public_names.get(key) - if reserved is not None: - return reserved - public_name = self.naming_policy.reserve_public_name( - self.public_namespace, - raw_name, - category=category, - owner=raw_name, - ) - self.reserved_public_names[key] = public_name - return public_name - def contract_import(self) -> str: """Return the direct import for contract symbols used by this emission.""" if not self.contract_imports: @@ -136,13 +125,6 @@ def contract_import(self) -> str: items.append(f"{name} as {alias}" if alias else name) return f"from {_CONTRACT_MODULE} import {', '.join(items)}" - @staticmethod - def _public_owner_key(owner: object) -> object: - """Return a stable cache key for one emitted public declaration.""" - if isinstance(owner, str | int | tuple): - return owner - return id(owner) - class PyiPrinter(ClassVisitor): """Emit editable Python stub text from semantic IR models. @@ -159,13 +141,15 @@ class PyiPrinter(ClassVisitor): # Public entrypoints and state # ------------------------------------------------------------------ - def __init__(self, *, normalize_fortran_public_names: bool = False): + def __init__(self, *, normalize_public_names: bool = False): """Configure public-name normalization for independent emissions. - Set normalize_fortran_public_names when emitting source-derived Fortran - contracts whose public names need Python normalization. + Set normalize_public_names when emitting a contract extracted from + native source, whose declarations are named in that language rather + than in Python. A contract read back from .pyi is already named in + Python and keeps every spelling verbatim. """ - self._normalize_fortran_public_names = normalize_fortran_public_names + self._normalize_public_names = normalize_public_names def emit(self, node) -> str: """Render one supported semantic model to semantic .pyi text. @@ -182,10 +166,10 @@ def _emission_context(self, node) -> _PyiEmissionContext: """Build isolated state for one public emission call.""" if not isinstance(node, SemanticModule): return _PyiEmissionContext( - normalize_fortran_public_names=self._normalize_fortran_public_names, + normalize_public_names=self._normalize_public_names, ) return _PyiEmissionContext( - normalize_fortran_public_names=self._normalize_fortran_public_names, + normalize_public_names=self._normalize_public_names, default_array_order=self._native_default_array_order(node.origin.source_language), semantic_class_names=frozenset( str(cls.name) @@ -229,8 +213,14 @@ def _visit_SemanticType( if semantic_type.name == "Unknown" or semantic_type.dtype == "Unknown": raise ValueError("Cannot emit .pyi with unresolved semantic type 'Unknown'") array_descriptor = native_array_descriptor_kind(semantic_type) - if PROTOTYPE_REF_METADATA in semantic_type.metadata: - text = semantic_type.name + unresolved_interface = semantic_type.metadata.get(UNRESOLVED_PROCEDURE_INTERFACE_METADATA) + if unresolved_interface is not None: + # The declaration named an interface no supplied module declares. + # Spelling that name keeps the extracted contract self-consistent + # with the import already emitted for it. + text = str(unresolved_interface) + elif PROTOTYPE_REF_METADATA in semantic_type.metadata: + text = self._prototype_reference_name(semantic_type, context) elif array_descriptor is not None: wrapper = "Allocatable" if array_descriptor == "allocatable" else "Pointer" text = f"{context.contract(wrapper)}[{self._visit(native_array_data_type(semantic_type), context)}]" @@ -303,7 +293,7 @@ def _visit_SemanticPrototype( decorators.append(f"@{context.contract('pure')}") decorators.append(f"@{context.contract('prototype')}") return self._emit_callable( - name=prototype.name, + name=self._prototype_name(prototype, context), arguments=arguments, return_type=self._visit(return_type, context), decorator="\n".join(decorators) + "\n", @@ -362,6 +352,26 @@ def _emit_method( parameter_indent=" ", ).rstrip() + @staticmethod + def _overload_target_name(candidate: SemanticFunction, context: _PyiEmissionContext) -> str: + """Return the specific an overload names, as this contract declares it. + + The target names a declaration in the same contract, and a contract + writing its declarations under Python names writes that one the same + way. Naming the source spelling instead points at no declaration the + contract holds. + """ + target = str(candidate.metadata.get(OVERLOAD_TARGET_METADATA) or candidate.native_name or candidate.name) + if not context.normalize_public_names: + return target + completed = candidate.metadata.get(CONTRACT_TARGET_NAME_METADATA) + if completed is None: + raise ValueError( + f"Contract overload target for {target!r} is incomplete; " + "run complete_python_export_policy before emission" + ) + return str(completed) + def _visit_ProcedureOverloadSet( self, overload_set: ProcedureOverloadSet, @@ -373,7 +383,7 @@ def _visit_ProcedureOverloadSet( definitions = [] for procedure in overload_set.procedures: candidate = deepcopy(procedure) - target = str(candidate.metadata.get(OVERLOAD_TARGET_METADATA) or candidate.native_name or candidate.name) + target = self._overload_target_name(candidate, context) if in_class: candidate = self._overload_method(overload_set, candidate) definition = self._emit_method( @@ -383,7 +393,8 @@ def _visit_ProcedureOverloadSet( ) indent = " " else: - candidate.name = overload_set.name + candidate.name = self._overload_set_name(overload_set, context) + candidate.metadata[CONTRACT_NAME_METADATA] = candidate.name definition = self._emit_function( candidate, context, @@ -421,11 +432,12 @@ def _visit_SemanticClass( ) -> str: """Emit class syntax.""" bases = ( - f"({', '.join(self._class_base_text(base, context) for base in cls.base_classes)})" + f"({', '.join(self._class_base_text(cls, base, context) for base in cls.base_classes)})" if cls.base_classes else "" ) - body = self._class_body(cls, context.inside_class(cls.name)) + emitted_name = self._class_name(cls, context) + body = self._class_body(cls, context.inside_class(emitted_name)) decorators = [] if self._is_private(cls): decorators.append(f"@{context.contract('private')}") @@ -433,18 +445,29 @@ def _visit_SemanticClass( decorators.append(f"@{context.contract('abstract')}") if self._class_uses_c_abi(cls): decorators.append(f'@{context.contract("native_abi")}("c")') + # Only a Fortran type states a separate native name here. A C struct + # keeps its native spelling -- `struct node` for `node` -- through its + # own representation rules, which state it without a decorator. + if ( + cls.origin.source_language == "fortran" + and cls.native_name + and self._renames_native_entity(cls, cls.native_name, emitted_name) + ): + decorators.append(f"@{context.contract('bind')}({json.dumps(str(cls.native_name))})") decorator_text = "\n".join(decorators) if decorator_text: decorator_text += "\n" return f""" -{decorator_text}class {cls.name}{bases}: +{decorator_text}class {emitted_name}{bases}: {body} """.strip() @staticmethod - def _class_base_text(base: str, context: _PyiEmissionContext) -> str: + def _class_base_text(cls: SemanticClass, base: str, context: _PyiEmissionContext) -> str: """Return an imported contract base name or a user base name.""" - return context.contract_type(base) + completed = cls.metadata.get(CONTRACT_BASE_NAMES_METADATA, {}) if context.normalize_public_names else {} + name = completed.get(base, base) if isinstance(completed, dict) else base + return context.contract_type(str(name)) @staticmethod def _is_abstract(cls: SemanticClass) -> bool: @@ -501,8 +524,73 @@ def _visit_SemanticModule( sections: list[str] = [] self._append_imports(sections, module, context) sections.extend(body_sections) + # The list reads as a summary of what came before it, so it closes the + # contract rather than standing between the imports and the + # declarations it names. + exported = self._module_exported_names(module, context, overload_targets) + # A contract with nothing in it states nothing; the list summarises a + # surface, and an empty file has none to summarise. + if exported is not None and sections: + sections.append(self.emit_exported_names(exported)) return "\n".join(sections).rstrip() + @staticmethod + def emit_exported_names(exported: list[str]) -> str: + """Render the list of names a contract states that it publishes.""" + if not exported: + return "__all__ = []" + items = ", ".join(json.dumps(name) for name in exported) + line = f"__all__ = [{items}]" + if len(line) <= 116: + return line + body = "\n".join(f" {json.dumps(name)}," for name in exported) + return f"__all__ = [\n{body}\n]" + + def _module_exported_names( + self, + module: SemanticModule, + context: _PyiEmissionContext, + overload_targets: set[str], + ) -> list[str] | None: + """Return every name this contract publishes, in the order it writes them. + + The list states the module's whole public surface rather than only the + names it re-exports, so removing one stops publishing it and adding one + publishes something the contract names for its declarations alone. A + contract that was read rather than derived keeps the list it stated. + """ + if module.exported_names is not None: + return list(module.exported_names) + if module.metadata.get(PYI_LOADED_METADATA): + return None + names: list[str] = [] + for semantic_class in self._contract_items(module.classes): + if not self._is_private(semantic_class): + names.append(self._class_name(semantic_class, context)) + # A prototype the contract needs for typing is not thereby published: + # a private one names a signature the module keeps to itself, and the + # annotations referring to it still resolve inside this file. + names.extend( + self._prototype_name(prototype, context) + for prototype in module.prototypes + if not self._is_private(prototype) + ) + for variable in self._contract_items(module.variables): + if getattr(variable, "visibility", "public") != "private": + names.append(self._module_variable_name(variable, context)) + for function in self._contract_items(module.functions, keep_names=overload_targets): + if not self._is_private(function): + names.append(self._callable_name(function, context)) + names.extend( + self._overload_set_name(overload_set, context) + for overload_set in module.overload_sets + if not self._is_private(overload_set) + ) + for reexport in module.reexports: + if reexport.publishes_to_python(): + names.append(self._reexport_name(reexport, context)) + return list(dict.fromkeys(names)) + # ------------------------------------------------------------------ # Shared helpers # ------------------------------------------------------------------ @@ -545,7 +633,13 @@ def _semantic_base_type( is always spelled so the two are never confused. """ if semantic_type.name != "String": - return context.contract_type(semantic_type.name) + completed = semantic_type.metadata.get(CONTRACT_NAME_METADATA) if context.normalize_public_names else None + if completed is not None: + # The type names a declaration this contract writes, which is + # that declaration even when it is spelled like a contract + # symbol: a user class `Vector` is not `prik.contracts.Vector`. + return str(completed) + return context.contract_type(str(semantic_type.name)) length = semantic_type.metadata.get("fortran_character_length") string = context.contract("String") if length is None or str(length) in {"", "*"}: @@ -657,12 +751,7 @@ def _canonical_array_dimension(dimension: object) -> str: @staticmethod def _printed_array_dimension(dimension: object) -> str: """Return the public `.pyi` spelling for an array dimension.""" - text = PyiPrinter._canonical_array_dimension(dimension) - if text == "::Strided": - return "::" - if text.endswith(":Strided"): - return text[: -len("Strided")] - return text + return PyiPrinter._canonical_array_dimension(dimension) @staticmethod def _array_annotation_metadata( @@ -941,7 +1030,7 @@ def _emit_data_member( self._annotation_target(name), variable, context, - original_name=variable.name if name != variable.name else None, + original_name=variable.name if self._renames_native_entity(variable, variable.name, name) else None, ) def _emit_module_variable( @@ -955,7 +1044,7 @@ def _emit_module_variable( self._annotation_target(name), arg, context, - original_name=arg.name if name != arg.name else None, + original_name=arg.name if self._renames_native_entity(arg, arg.name, name) else None, ) @staticmethod @@ -1132,20 +1221,35 @@ def _pyi_default_value(arg: SemanticVariable) -> str | None: @staticmethod def _python_literal_text(value: str | None) -> str | None: - """Handle python literal text for the current generation context.""" + """Return the Python spelling of one Fortran initializer. + + Only the text outside character literals is respelled. A literal's + contents are the constant's value, so a character parameter holding + ``".true."`` keeps six characters and one holding ``"1d2"`` keeps the + ``d`` it was written with, while a logical or a real written the same + way outside quotes is respelled as Python writes it. + """ if value is None: return None text = str(value).strip() if not text: return None - text = re.sub(r"\.true\.", "True", text, flags=re.IGNORECASE) - text = re.sub(r"\.false\.", "False", text, flags=re.IGNORECASE) - text = re.sub(r"(?<=\d)[dD](?=[+-]?\d)", "e", text) + character = fortran_character_value(text) + if character is not None: + return repr(character) + text = outside_character_literals(text, PyiPrinter._respelled_fortran_literal) try: return ast.unparse(ast.parse(text, mode="eval").body) except SyntaxError: return None + @staticmethod + def _respelled_fortran_literal(text: str) -> str: + """Rewrite the Fortran literal spellings Python spells differently.""" + text = re.sub(r"\.true\.", "True", text, flags=re.IGNORECASE) + text = re.sub(r"\.false\.", "False", text, flags=re.IGNORECASE) + return re.sub(r"(?<=\d)[dD](?=[+-]?\d)", "e", text) + @staticmethod def _fortran_literal_text(value: str | None) -> str | None: """Return a Python literal spelling for literal Fortran initializer text.""" @@ -1332,7 +1436,7 @@ def _constructor_argument( or self._python_literal_text(field.default_value) or "..." ) - if name != field.name: + if self._renames_native_entity(field, field.name, name): type_text = self._annotated_type_text( type_text, [f"{context.contract('SourceName')}({json.dumps(field.name)})"], @@ -1382,7 +1486,6 @@ def _fresh_contract_alias(name: str, reserved: set[str]) -> str: def _module_reserved_names(cls, module: SemanticModule) -> set[str]: """Return user/import names that cannot be reused by contract imports.""" names: set[str] = set() - names.update(cls._required_procedure_namespace_import_names(module)) for imp in module.imports: names.update(cls._import_local_names(imp)) for item in [*module.classes, *module.prototypes, *module.variables, *module.functions, *module.overload_sets]: @@ -1394,6 +1497,9 @@ def _module_reserved_names(cls, module: SemanticModule) -> set[str]: @classmethod def _collect_reserved_item_names(cls, item: object, names: set[str]) -> None: """Collect emitted declaration names that can shadow imports.""" + metadata = getattr(item, "metadata", None) + if isinstance(metadata, dict) and metadata.get(CONTRACT_NAME_METADATA): + names.add(str(metadata[CONTRACT_NAME_METADATA])) for attr in ("name", "native_name"): value = getattr(item, attr, None) if isinstance(value, str) and value: @@ -1424,7 +1530,12 @@ def _import_local_names(imp: str | SemanticImport) -> set[str]: if isinstance(imp, SemanticImport): if not imp.items: return {imp.module.split(".", 1)[0]} - return {item.target or item.source for item in imp.items} + return { + name + for item in imp.items + for name in (item.target or item.source, item.contract_target or item.contract_source) + if name + } names = set() for item in str(imp).split(","): module_name, _, alias = item.strip().partition(" as ") @@ -1437,251 +1548,14 @@ def _append_imports( module: SemanticModule, context: _PyiEmissionContext, ) -> None: - """Append imports.""" + """Append the contract vocabulary import, then each completed import.""" contract_import = context.contract_import() if contract_import: sections.append(contract_import) - imports = self._effective_imports(module) - for imp in imports: - sections.append(self._emit_import(imp)) - if contract_import or imports: + sections.extend(self._emit_import(imp, context) for imp in module.imports) + if contract_import or module.imports: sections.append("") - @classmethod - def _effective_imports(cls, module: SemanticModule) -> list[str | SemanticImport]: - """Handle effective imports for the current generation context.""" - imports = [ - imp - for imp in module.imports - if not PyiPrinter._is_source_kind_import(imp) and not PyiPrinter._is_contract_import(imp) - ] - procedure_namespaces = cls._required_procedure_namespace_import_names(module) - cls._validate_procedure_namespace_imports(module, procedure_namespaces, imports) - satisfied_namespaces = cls._satisfied_procedure_namespace_import_names(imports, procedure_namespaces) - imports.extend(cls._synthetic_flat_external_type_imports(module, imports, procedure_namespaces)) - imports.extend(cls._missing_expression_callable_imports(module, imports)) - imports.extend(cls._missing_procedure_namespace_imports(procedure_namespaces, satisfied_namespaces)) - return imports - - @classmethod - def _missing_expression_callable_imports( - cls, - module: SemanticModule, - imports: list[str | SemanticImport], - ) -> list[SemanticImport]: - """Return explicit imports needed to preserve declaration-call origins. - - The semantic array provenance is consumed without changing its call - expression. Existing explicit imports win; wildcard-like native module - imports gain only the specific callable names needed by the generated - contract, which makes a later `.pyi` load unambiguous. - """ - existing = { - (item.target or item.source).casefold(): (imported.module, item.source) - for imported in imports - if isinstance(imported, SemanticImport) - for item in imported.items - } - local_names = {function.name.casefold() for function in module.functions} - required: dict[str, list[SemanticImportItem]] = {} - for semantic_type in _module_semantic_types(module): - storage = semantic_type.storage - array = storage.array if storage is not None else None - if array is None: - continue - for axis_references in array.expression_callables: - for reference in axis_references: - if reference.native_scope is None or reference.name.casefold() in local_names: - continue - local_name = reference.name.rsplit(".", 1)[-1] - native_name = reference.native_name or local_name - previous = existing.get(local_name.casefold()) - if previous is not None: - if previous != (reference.native_scope, native_name): - raise ValueError( - f"Declaration-expression callable import collides with existing name: {local_name!r}" - ) - continue - required.setdefault(reference.native_scope, []).append( - SemanticImportItem( - source=native_name, - target=local_name if local_name != native_name else None, - ) - ) - existing[local_name.casefold()] = (reference.native_scope, native_name) - return [SemanticImport(module=module_name, items=items) for module_name, items in required.items()] - - @classmethod - def _synthetic_flat_external_type_imports( - cls, - module: SemanticModule, - imports: list[str | SemanticImport], - procedure_namespaces: set[str], - ) -> list[SemanticImport]: - """Return synthetic flattened imports needed by external type refs.""" - imported_items = { - (imp.module, item.source, item.target or item.source) - for imp in imports - if isinstance(imp, SemanticImport) - for item in imp.items - } - synthetic: dict[str, list[SemanticImportItem]] = {} - for semantic_type in _module_semantic_types(module): - ref = cls._flat_external_type_import_ref(semantic_type) - if ref is None: - continue - origin_module, source_name, local_name = ref - key = (origin_module, source_name, local_name) - if key in imported_items: - continue - if local_name in procedure_namespaces: - raise ValueError( - f"Procedure-local Fortran import namespace collides with generated .pyi name: {local_name!r}" - ) - synthetic.setdefault(origin_module, []).append( - SemanticImportItem( - source=source_name, - target=local_name if local_name != source_name else None, - ) - ) - imported_items.add(key) - return [ - SemanticImport( - module=module_name, - items=sorted(items, key=lambda item: (item.source, item.target or "")), - ) - for module_name, items in sorted(synthetic.items()) - ] - - @classmethod - def _flat_external_type_import_ref(cls, semantic_type: SemanticType) -> tuple[str, str, str] | None: - """Return flattened external type import fields, or None for qualified refs.""" - ref = semantic_type.metadata.get(EXTERNAL_TYPE_REF_METADATA) - if not isinstance(ref, dict) or cls._is_procedure_local_external_ref(ref): - return None - origin_module = ref.get("origin_module") - source_name = ref.get("name") - local_name = ref.get("local_name") or source_name - if not all(isinstance(value, str) and value for value in (origin_module, source_name, local_name)): - return None - if "." in local_name: - return None - return origin_module, source_name, local_name - - @staticmethod - def _missing_procedure_namespace_imports( - procedure_namespaces: set[str], - satisfied_namespaces: set[str], - ) -> list[SemanticImport]: - """Return missing namespace imports for procedure-local external refs.""" - missing_namespaces = sorted(procedure_namespaces - satisfied_namespaces) - if not missing_namespaces: - return [] - return [ - SemanticImport( - module=".", - items=[SemanticImportItem(source=name) for name in missing_namespaces], - ) - ] - - @staticmethod - def _is_procedure_local_external_ref(ref: dict[object, object]) -> bool: - """Return whether an external ref came from a procedure-local Fortran use.""" - return ref.get("import_scope") == "procedure" - - @classmethod - def _required_procedure_namespace_import_names(cls, module: SemanticModule) -> set[str]: - """Return module namespaces required by procedure-local imported types.""" - names: set[str] = set() - for semantic_type in _module_semantic_types(module): - ref = semantic_type.metadata.get(EXTERNAL_TYPE_REF_METADATA) - if not isinstance(ref, dict) or not cls._is_procedure_local_external_ref(ref): - continue - origin_module = ref.get("origin_module") - source_name = ref.get("name") - local_name = ref.get("local_name") - if not all(isinstance(value, str) and value for value in (origin_module, source_name, local_name)): - continue - names.add(origin_module) - return names - - @classmethod - def _validate_procedure_namespace_imports( - cls, - module: SemanticModule, - procedure_namespaces: set[str], - imports: list[str | SemanticImport], - ) -> None: - """Reject namespace imports that would collide with emitted public names.""" - if not procedure_namespaces: - return - declaration_collisions = procedure_namespaces & cls._top_level_declaration_names(module) - import_collisions = { - name - for imp in imports - for name in cls._import_local_names(imp) & procedure_namespaces - if not cls._import_satisfies_procedure_namespace(imp, name) - } - collisions = sorted(declaration_collisions | import_collisions) - if collisions: - joined = ", ".join(repr(name) for name in collisions) - raise ValueError(f"Procedure-local Fortran import namespace collides with generated .pyi name: {joined}") - - @staticmethod - def _top_level_declaration_names(module: SemanticModule) -> set[str]: - """Return names emitted in a module-level stub namespace.""" - return { - str(item.name) - for item in [ - *module.classes, - *module.prototypes, - *module.variables, - *module.functions, - *module.overload_sets, - ] - if getattr(item, "name", None) - } - - @classmethod - def _satisfied_procedure_namespace_import_names( - cls, - imports: list[str | SemanticImport], - procedure_namespaces: set[str], - ) -> set[str]: - """Return procedure namespace imports already provided by module imports.""" - return { - name - for name in procedure_namespaces - if any(cls._import_satisfies_procedure_namespace(imp, name) for imp in imports) - } - - @staticmethod - def _import_satisfies_procedure_namespace(imp: str | SemanticImport, name: str) -> bool: - """Return whether an import binds exactly the required module namespace.""" - if isinstance(imp, SemanticImport): - if not imp.items: - return imp.module == name - return imp.module == "." and any(item.source == name and item.target is None for item in imp.items) - for item in str(imp).split(","): - module_name, _, alias = item.strip().partition(" as ") - if alias: - continue - if module_name == name: - return True - return False - - @staticmethod - def _is_source_kind_import(imp: str | SemanticImport) -> bool: - """Return whether an import only names a source-language kind module.""" - module = imp.module if isinstance(imp, SemanticImport) else str(imp).split()[0] - return module.casefold().lstrip(".") in {"iso_c_binding", "iso_fortran_env"} - - @staticmethod - def _is_contract_import(imp: str | SemanticImport) -> bool: - """Return whether an import names the generated contract namespace.""" - module = imp.module if isinstance(imp, SemanticImport) else str(imp).split()[0] - return module == _CONTRACT_MODULE - @staticmethod def _has_overload_sets(module: SemanticModule) -> bool: """Return whether has overload sets.""" @@ -1693,22 +1567,27 @@ def class_has_overloads(cls: SemanticClass) -> bool: class_has_overloads(cls) for cls in module.classes if isinstance(cls, SemanticClass) ) - @staticmethod - def _emit_import(imp: str | SemanticImport) -> str: - """Emit import syntax.""" + @classmethod + def _emit_import(cls, imp: str | SemanticImport, context: _PyiEmissionContext) -> str: + """Emit one import statement as completion spelled it.""" if isinstance(imp, str): return f"import {imp}" if not imp.items: return f"import {imp.module}" - items = ", ".join(PyiPrinter._emit_import_item(item) for item in imp.items) - return f"from {imp.module} import {items}" + return f"from {imp.module} import {', '.join(cls._emit_import_item(item, context) for item in imp.items)}" @staticmethod - def _emit_import_item(item: SemanticImportItem) -> str: - """Emit import item syntax.""" - if item.target and item.target != item.source: - return f"{item.source} as {item.target}" - return item.source + def _emit_import_item(item: SemanticImportItem, context: _PyiEmissionContext) -> str: + """Emit one imported name, as the sources or the completed contracts spell it.""" + if not context.normalize_public_names: + source, bound = item.source, item.target + elif item.contract_source is None: + raise ValueError( + f"Contract import of {item.source!r} is incomplete; run complete_contract_imports before emission" + ) + else: + source, bound = item.contract_source, item.contract_target + return f"{source} as {bound}" if bound and bound != source else source def _append_items(self, sections: list[str], items: list, emit_item) -> None: """Append items.""" @@ -1980,17 +1859,57 @@ def _callable_name( owner: object | None = None, ) -> str: """Return the Python-visible callable name to write in the contract.""" - if ( - not context.normalize_fortran_public_names - or func.name.startswith("__") - or func.origin.source_language != "fortran" - ): + if not context.normalize_public_names: return func.name - return context.public_name( - func.name, - category="method" if isinstance(func, SemanticMethod) else "function", - owner=owner if owner is not None else func, - ) + return completed_contract_name(func) + + @staticmethod + def _reexport_name(reexport: SemanticReexport, context: _PyiEmissionContext) -> str: + """Return the Python name this contract publishes one re-export under. + + Export policy names a re-export in the same ledger as the module's own + declarations, so the contract states what it completed rather than a + spelling derived here, which could take a name a declaration holds. + """ + local = str(reexport.local_name) + if not context.normalize_public_names: + return local + if not reexport.python_name: + raise ValueError( + f"Contract name for re-export {local!r} is incomplete; " + "run complete_python_export_policy before emission" + ) + return str(reexport.python_name) + + @staticmethod + def _prototype_name(prototype: SemanticPrototype, context: _PyiEmissionContext) -> str: + """Return the spelling a prototype is declared under in the contract.""" + return completed_contract_name(prototype) if context.normalize_public_names else str(prototype.name) + + @staticmethod + def _prototype_reference_name(semantic_type: SemanticType, context: _PyiEmissionContext) -> str: + """Return the spelling a callback annotation names its prototype by.""" + if not context.normalize_public_names: + return str(semantic_type.name) + completed = semantic_type.metadata.get(CONTRACT_NAME_METADATA) + if completed is None: + raise ValueError( + f"Contract name for prototype reference {semantic_type.name!r} is incomplete; " + "run complete_python_export_policy before emission" + ) + return str(completed) + + @staticmethod + def _class_name(cls: SemanticClass, context: _PyiEmissionContext) -> str: + """Return the Python-visible class name to write in the contract.""" + return completed_contract_name(cls) if context.normalize_public_names else str(cls.name) + + @staticmethod + def _overload_set_name(overload_set: ProcedureOverloadSet, context: _PyiEmissionContext) -> str: + """Return the Python-visible name of one module-level overload set.""" + if not context.normalize_public_names: + return str(overload_set.name) + return completed_contract_name(overload_set) @staticmethod def _data_member_name( @@ -1998,9 +1917,9 @@ def _data_member_name( context: _PyiEmissionContext, ) -> str: """Return the Python-visible class data-member name.""" - if not context.normalize_fortran_public_names: + if not context.normalize_public_names: return variable.name - return context.public_name(variable.name, category="field", owner=variable) + return completed_contract_name(variable) @staticmethod def _module_variable_name( @@ -2008,9 +1927,9 @@ def _module_variable_name( context: _PyiEmissionContext, ) -> str: """Return the Python-visible module variable name.""" - if not context.normalize_fortran_public_names: + if not context.normalize_public_names: return variable.name - return context.public_name(variable.name, category="variable", owner=variable) + return completed_contract_name(variable) def _decorators( self, @@ -2030,6 +1949,12 @@ def _decorators( and not func.metadata.get(OVERLOAD_TARGET_METADATA) ): decorators.append(f"{indent}@{context.contract('standalone')}") + if ( + not isinstance(func, SemanticMethod) + and not func.metadata.get(OVERLOAD_TARGET_METADATA) + and any(str(attribute).casefold() == "pure" for attribute in func.metadata.get("fortran_attributes", ())) + ): + decorators.append(f"{indent}@{context.contract('pure')}") if not func.metadata.get(OVERLOAD_TARGET_METADATA) and self._requires_native_call(func): decorators.append( f"{indent}{self._native_call(self._pyi_projection(func), context, self._native_result_projection(func), func)}" @@ -2104,13 +2029,13 @@ def _bind_target( if bind_target is not None: return bind_target - if isinstance(func, SemanticMethod) and func.name != emitted_name: + if isinstance(func, SemanticMethod) and PyiPrinter._renames_native_entity(func, func.name, emitted_name): if not context.public_namespace: return func.native_name class_name = context.public_namespace[-1] return f"{class_name}.{func.name}" - if func.native_name and func.native_name != emitted_name: + if func.native_name and PyiPrinter._renames_native_entity(func, func.native_name, emitted_name): return func.native_name return None @@ -2520,6 +2445,26 @@ def _is_private(node) -> bool: """Return whether is private.""" return getattr(node, "visibility", "public") == "private" + @staticmethod + def _renames_native_entity(declaration: object, native_name: object, emitted_name: str) -> bool: + """Return whether an emitted name has to record the spelling it came from. + + A Fortran entity is named without regard to case, so writing one under a + lower-case Python name renames nothing and states nothing worth + recording. Any other difference is a real rename -- a Python keyword, a + character an identifier cannot hold, a name a collision moved aside -- + and the declaration keeps the original beside it. Every other source + language names its entities exactly, so there the spellings are compared + as written. + """ + native = str(native_name) + if native == emitted_name: + return False + origin = getattr(declaration, "origin", None) + if getattr(origin, "source_language", None) != "fortran": + return True + return native.casefold() != emitted_name.casefold() + @staticmethod def _annotation_target(name: str) -> str: """Handle annotation target for the current generation context.""" @@ -2545,15 +2490,15 @@ def _parameter_target(name: str) -> str: _DEFAULT_PRINTER = PyiPrinter() -def emit_module(module: SemanticModule, *, normalize_fortran_public_names: bool = False) -> str: +def emit_module(module: SemanticModule, *, normalize_public_names: bool = False) -> str: """Render one semantic module through the shared default printer. - Use this convenience entrypoint for ordinary one-module emission. Set - normalize_fortran_public_names to use a printer configured for normalized - public names. Both paths create a fresh module emission context. + Set normalize_public_names when the module is named in its own source + language rather than in Python. Every path creates a fresh module emission + context. """ - if normalize_fortran_public_names: - return PyiPrinter(normalize_fortran_public_names=True).emit(module) + if normalize_public_names: + return PyiPrinter(normalize_public_names=True).emit(module) return _DEFAULT_PRINTER.emit(module) diff --git a/prik/runtime/native_support/prik_binding.h b/prik/runtime/native_support/prik_binding.h index eea14001f..00c820106 100644 --- a/prik/runtime/native_support/prik_binding.h +++ b/prik/runtime/native_support/prik_binding.h @@ -1406,6 +1406,22 @@ static inline PyObject *prik_float32_to_numpy(const float *value) return result; } +/* Bind one name in a namespace to a callable another namespace owns, so a + * re-exported procedure resolves to the single wrapper that defines it. */ +static inline int prik_bind_namespace_alias(PyObject *target, const char *name, PyObject *source, + const char *source_name) +{ + PyObject *value = PyObject_GetAttrString(source, source_name); + int status; + + if (value == NULL) { + return -1; + } + status = PyObject_SetAttrString(target, name, value); + Py_DECREF(value); + return status; +} + static inline PyObject *prik_float64_to_numpy(const double *value) { PyObject *result = PyArrayScalar_New(Double); diff --git a/prik/semantics/c2ir.py b/prik/semantics/c2ir.py index 0af9d2759..f8071d322 100644 --- a/prik/semantics/c2ir.py +++ b/prik/semantics/c2ir.py @@ -2045,11 +2045,14 @@ def select_c_export_functions( ) -> list[SemanticModule]: """Restrict C semantic IR to an exact, fail-closed function allowlist. + ``symbols`` names native C functions and states the source-side public + surface, the way a semantic ``.pyi`` contract states its own ``__all__``. The selection happens after ordinary include exposure has recorded source provenance and before policy completion. Selected functions receive one explicit-export marker so a declaration from an included system header is intentionally treated as part of the wrapped translation unit. Every - other declaration category is removed from the selected semantic surface. + other declaration category is removed from the selected semantic surface, + so the emitted stub publishes exactly the corresponding Python names. """ selected_modules = list(modules) requested = _validated_c_export_symbols(symbols) diff --git a/prik/semantics/fortran2ir.py b/prik/semantics/fortran2ir.py index 8d269ccfc..b27b71c8a 100644 --- a/prik/semantics/fortran2ir.py +++ b/prik/semantics/fortran2ir.py @@ -15,12 +15,14 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Iterable, Iterator +from typing import NamedTuple from copy import deepcopy -from dataclasses import dataclass +from dataclasses import dataclass, replace import re from pathlib import Path +from prik.parsers.fortran.scope import ScopeUses, UseRoute from prik.parsers.fortran.models import ( FortranArgument, FortranBlockData, @@ -28,18 +30,22 @@ FortranEnum, FortranEnumerator, FortranFile, + FortranInterface, FortranModule, FortranProject, FortranProgram, FortranProcedureSignature, FortranSubmodule, - FortranUseMapping, + FortranUseStatement, FortranVariable, ) from prik.utilities.declaration_expressions import ( + is_strided_extent, ArrayExpressionSource, canonicalize_declaration_extent, declaration_expression_calls, + declaration_expression_identifiers, + outside_character_literals, fortran_extent_to_python, is_declaration_expression_helper, split_dimension_bounds, @@ -72,6 +78,7 @@ PYTHON_STATIC_METADATA, PROTOTYPE_INTENT_METADATA, PROTOTYPE_REF_METADATA, + UNRESOLVED_PROCEDURE_INTERFACE_METADATA, SemanticArgument, SemanticArrayContract, SemanticClass, @@ -86,6 +93,7 @@ SemanticModule, SemanticOrigin, SemanticPrototype, + SemanticReexport, SemanticStorageContract, SemanticType, SemanticVariable, @@ -172,6 +180,52 @@ # Internal conversion context +@dataclass(frozen=True) +class _CallbackInterface: + """Pair one resolvable callback interface with the module that declares it. + + The declaring module is what makes an imported interface convertible: its + dummies are written in that module's lexical scope, so a derived type the + interface names belongs to the declaring module even when the consuming + module never imports that type. + """ + + signature: FortranProcedureSignature + module: FortranModule | None = None + local_name: str | None = None + """Spelling the importing scope binds, when a ``use`` renamed the interface.""" + + declaring_scope: tuple[str, ...] = () + """Contained procedure declaring the block, empty for a module's own block.""" + + @property + def native_name(self) -> str: + """Return the name the declaring module gives this interface.""" + return self.signature.name + + @property + def visible_name(self) -> str: + """Return the canonical spelling visible where the interface was resolved.""" + return self.local_name or self.signature.name + + +class _SpecificProcedure(NamedTuple): + """One generic's specific: the module declaring it, and the name it gives it. + + Two modules may each declare a specific of the same name and contribute + both to one merged generic, so a specific is identified by where it is + declared rather than by its spelling alone. + """ + + module: str + name: str + + @property + def key(self) -> tuple[str, str]: + """Return the case-folded identity this specific is looked up by.""" + return self.module.casefold(), self.name.casefold() + + @dataclass(frozen=True) class _DerivedTypeContext: """Keep lexical derived-type lookup facts while one parser node is converted. @@ -182,8 +236,8 @@ class _DerivedTypeContext: """ module: str | None = None - uses: dict[str, list[FortranUseMapping]] | None = None - procedure_uses: dict[str, list[FortranUseMapping]] | None = None + uses: list[FortranUseStatement] | None = None + procedure_uses: list[FortranUseStatement] | None = None local_types: frozenset[str] = frozenset() @@ -207,7 +261,7 @@ class _DeclarationCallableContext: module: str | None local_procedures: dict[str, SemanticFunction] local_interfaces: dict[str, SemanticPrototype] - uses: dict[str, list[FortranUseMapping]] + uses: list[FortranUseStatement] def _normalize_compile_time_values( @@ -257,7 +311,17 @@ def replace_symbol(match: re.Match[str]) -> str: token = match.group(0) return compile_time_values.get(token.lower(), token) - return re.sub(r"\b[A-Za-z_][A-Za-z0-9_]*\b", replace_symbol, raw) + def substitute(fragment: str) -> str: + return re.sub(r"\b[A-Za-z_][A-Za-z0-9_]*\b", replace_symbol, fragment) + + # A character literal's contents are data, so a symbol spelled inside one + # is not a reference to substitute: ``len("runtime")`` measures seven + # characters whatever value ``runtime`` names. + return outside_character_literals(raw, substitute) + + +# Language-owned modules are contract vocabulary, not sibling contract leaves. +_INTRINSIC_FORTRAN_MODULES = frozenset({"iso_c_binding", "iso_fortran_env"}) class FortranToIRConverter(ClassVisitor): @@ -296,6 +360,7 @@ def __init__( self.wrapped_derived_types = { (str(module).lower(), str(name).lower()) for module, name in (wrapped_derived_types or []) } + self._known_modules: set[str] = {module for module, _name in self.wrapped_derived_types} self._known_procedures: set[tuple[str, str]] = set() self.type_facts = { (str(base_type).lower(), None if kind is None else str(kind).lower()): dict(fact) @@ -344,23 +409,30 @@ def _visit_FortranFile( parsed_file: FortranFile, *, standalone_module_name: str | None = None, + sibling_modules: Iterable[FortranModule] = (), ) -> list[SemanticModule]: """Convert every module and standalone procedure group in one file. The method first expands the wrapped-derived-type lookup from the file, then preserves parser module order. Standalone procedures are emitted - last as the requested synthetic module when present. + last as the requested synthetic module when present. ``sibling_modules`` + supplies modules parsed from other files so that an abstract interface + imported across files resolves the same way it does for a project. """ - converter = self._with_additional_wrapped_types(self._wrapped_types_from_file(parsed_file)) + siblings = tuple(sibling_modules) + converter = self._with_additional_known_modules(module.name for module in (*siblings, *parsed_file.modules)) + converter = converter._with_additional_wrapped_types(self._wrapped_types_from_file(parsed_file)) converter = converter._with_additional_known_procedures(self._known_procedures_from_file(parsed_file)) converter = converter._with_additional_abstract_types(self._abstract_types_from_file(parsed_file)) - modules = [converter.visit(module) for module in parsed_file.modules] + index = self._callback_module_index(siblings, parsed_file.modules) + modules = [converter.visit(module, module_index=index) for module in parsed_file.modules] if parsed_file.procedures: modules.append( converter.procedures_to_semantic_module( parsed_file.procedures, name=standalone_module_name or self._standalone_module_name(parsed_file), - callback_interfaces=self._callback_interface_lookup(parsed_file), + callback_interfaces=self._declared_callback_interfaces(parsed_file), + module_index=index, ) ) return modules @@ -372,25 +444,25 @@ def _visit_FortranProject(self, project: FortranProject) -> list[SemanticModule] while imported callback interfaces are resolved against the project. The returned module ordering matches the input file and parser order. """ - converter = self._with_additional_wrapped_types(self._wrapped_types_from_project(project)) + index = self._callback_module_index( + project.modules.values(), + (module for parsed_file in project.files for module in parsed_file.modules), + ) + converter = self._with_additional_known_modules(module.name for module in index.values()) + converter = converter._with_additional_wrapped_types(self._wrapped_types_from_project(project)) converter = converter._with_additional_known_procedures(self._known_procedures_from_project(project)) converter = converter._with_additional_abstract_types(self._abstract_types_from_project(project)) semantic_modules = [] for parsed_file in project.files: file_converter = converter._with_additional_wrapped_types(converter._wrapped_types_from_file(parsed_file)) - semantic_modules.extend( - file_converter.visit( - module, - callback_interfaces=self._project_callback_interface_lookup(project, module), - ) - for module in parsed_file.modules - ) + semantic_modules.extend(file_converter.visit(module, module_index=index) for module in parsed_file.modules) if parsed_file.procedures: semantic_modules.append( file_converter.procedures_to_semantic_module( parsed_file.procedures, name=self._standalone_module_name(parsed_file), - callback_interfaces=self._callback_interface_lookup(parsed_file), + callback_interfaces=self._declared_callback_interfaces(parsed_file), + module_index=index, ) ) return semantic_modules @@ -495,14 +567,16 @@ def _convert_variable_type( def _character_length(self, var: FortranVariable) -> str: """Return the resolved character length recorded by a parsed declaration. - The helper reads the parser's mixed kind/length spelling, preferring an - explicit ``len=`` fragment and otherwise preserving legacy length syntax; - declarations with neither continue to use Fortran's length-one default. + The parser separates a character selector's length from its kind, so + the length is read from that fact rather than found again inside a + joined spelling, where an expression holding a comma of its own -- a + ``len=max(4, n)`` -- would be cut short. A declaration stating no + length continues to use Fortran's length-one default. """ + declared = getattr(var, "character_length_expression", None) + if declared: + return self._resolve_compile_time_text(str(declared)).strip() raw = self._resolve_compile_time_text(str(var.kind or "")).strip() - length_match = re.search(r"(?:^|,)\s*len\s*=\s*([^,]+)", raw, re.IGNORECASE) - if length_match is not None: - return length_match.group(1).strip() if var.character_length_syntax and raw: return raw return "1" @@ -512,7 +586,7 @@ def _visit_FortranArgument( arg: FortranArgument | FortranVariable, *, derived_type_context: _DerivedTypeContext | None = None, - callback_interfaces: dict[str, FortranProcedureSignature] | None = None, + callback_interfaces: dict[str, _CallbackInterface] | None = None, as_data_member: bool = False, as_type: bool = False, binding_cls: type[SemanticVariable] = SemanticVariable, @@ -572,7 +646,7 @@ def _argument_semantic_type( self, arg: FortranArgument | FortranVariable, *, - callback_interfaces: dict[str, FortranProcedureSignature] | None, + callback_interfaces: dict[str, _CallbackInterface] | None, derived_type_context: _DerivedTypeContext | None, declaration_arrays: dict[str, ArrayExpressionSource] | None, ) -> SemanticType: @@ -670,46 +744,215 @@ def _convert_data_member( return binding @staticmethod - def _callback_interface_lookup( - module: FortranModule | FortranFile, - ) -> dict[str, FortranProcedureSignature]: - """Index explicit and abstract interface procedures usable by dummy procedures.""" - lookup: dict[str, FortranProcedureSignature] = {} - for interface in module.interfaces: + def _declared_callback_interfaces( + container: FortranModule | FortranFile, + ) -> dict[str, _CallbackInterface]: + """Index interfaces declared directly in one module or file. + + A block written inside a contained procedure belongs to that procedure, + which may declare its own interface under a name another procedure uses + for a different one. Those blocks are stored beside the module's own, so + only the module's are indexed here and each procedure adds its own. + """ + owner = container if isinstance(container, FortranModule) else None + blocks = ( + FortranToIRConverter._module_interfaces(container) + if isinstance(container, FortranModule) + else container.interfaces + ) + lookup: dict[str, _CallbackInterface] = {} + for interface in blocks: for signature in interface.procedures: - lookup.setdefault(signature.name.casefold(), signature) + lookup.setdefault(signature.name.casefold(), _CallbackInterface(signature, owner)) if interface.name and len(interface.procedures) == 1: - lookup.setdefault(interface.name.casefold(), interface.procedures[0]) + lookup.setdefault(interface.name.casefold(), _CallbackInterface(interface.procedures[0], owner)) return lookup + @staticmethod + def _callback_module_index(*containers: Iterable[FortranModule]) -> dict[str, FortranModule]: + """Index every known module by casefolded name for interface resolution.""" + return {module.name.casefold(): module for group in containers for module in group} + @classmethod - def _project_callback_interface_lookup( + def _module_callback_interfaces( cls, - project: FortranProject, + modules: dict[str, FortranModule], module: FortranModule, - ) -> dict[str, FortranProcedureSignature]: - """Resolve abstract interfaces imported from another parsed module.""" - modules = {name.casefold(): item for name, item in project.modules.items()} - modules.update({item.name.casefold(): item for parsed_file in project.files for item in parsed_file.modules}) - imported: dict[str, FortranProcedureSignature] = {} - for module_name, mappings in module.uses.items(): - source_module = modules.get(module_name.casefold()) - if source_module is None: - continue - source_lookup = cls._callback_interface_lookup(source_module) - if not mappings: - imported.update(source_lookup) + *, + seen: frozenset[str] = frozenset(), + exported_only: bool = False, + ) -> dict[str, _CallbackInterface]: + """Index every interface name visible in one module. + + Declarations of the module itself take precedence over imported names, + and an import is followed through re-exporting modules so a chain of + ``use`` hops resolves to the module that actually declares it. A module + outside the index leaves its names unresolved, which later stages report + against the ``use`` that named them. + + ``exported_only`` applies the module's accessibility to the result, for + a caller reaching the names from outside through ``use``. A module + still sees its own private interfaces, so it is left off in that case. + The accessibility is the one every other stage reads, so a name reached + through an explicitly public or private ``use`` route is judged the same + way here as anywhere else rather than by a separate calculation. + """ + key = module.name.casefold() + if key in seen: + return {} + visible = cls._declared_callback_interfaces(module) + cls._merge_imported_callback_interfaces( + visible, + modules, + module.uses, + seen=seen | {key}, + override=False, + ) + if not exported_only: + return visible + public = cls._module_public_names(module, modules) + return {name: resolved for name, resolved in visible.items() if resolved.visible_name.casefold() in public} + + @classmethod + def _scope_callback_interfaces( + cls, + modules: dict[str, FortranModule], + uses: list[FortranUseStatement], + *, + base: dict[str, _CallbackInterface], + owner: FortranModule | None = None, + scope_name: str | None = None, + ) -> dict[str, _CallbackInterface]: + """Extend a visible interface set with one inner scope's own declarations. + + A procedure-local ``use``, and an interface block written inside the + procedure, both name the interface in that scope alone, so each takes + precedence over anything the enclosing scope made visible under the + same name. Two procedures may name different interfaces the same way, + which is why the enclosing module contributes only its own blocks. + """ + visible = dict(base) + scope = (str(scope_name),) if scope_name else () + for interface in cls._procedure_interfaces(owner, scope_name): + for signature in interface.procedures: + visible[signature.name.casefold()] = _CallbackInterface(signature, owner, declaring_scope=scope) + if interface.name and len(interface.procedures) == 1: + visible[interface.name.casefold()] = _CallbackInterface( + interface.procedures[0], + owner, + declaring_scope=scope, + ) + cls._merge_imported_callback_interfaces(visible, modules, uses, seen=frozenset(), override=True) + return visible + + @staticmethod + def _interface_declaring_scope(interface) -> tuple[str, ...]: + """Return the contained procedure declaring one interface block, if any. + + A module's own block returns the empty scope, which is what makes its + name reachable through a ``use`` of the module. + """ + if str(getattr(interface, "declaring_scope_kind", "module")).casefold() != "procedure": + return () + return tuple(str(part) for part in getattr(interface, "declaring_scope_path", ()))[-1:] + + @staticmethod + def _procedure_interfaces(owner: FortranModule | None, scope_name: str | None): + """Return the interface blocks written inside one contained procedure.""" + if owner is None or scope_name is None: + return () + wanted = scope_name.casefold() + return tuple( + interface + for interface in owner.interfaces + if str(getattr(interface, "declaring_scope_kind", "module")).casefold() == "procedure" + and [part.casefold() for part in getattr(interface, "declaring_scope_path", ())][-1:] == [wanted] + ) + + @classmethod + def _merge_imported_callback_interfaces( + cls, + visible: dict[str, _CallbackInterface], + modules: dict[str, FortranModule], + uses: list[FortranUseStatement], + *, + seen: frozenset[str], + override: bool, + ) -> None: + """Merge every interface one ``use`` list makes visible into ``visible``. + + A name reached by several ``use`` statements has to name one interface. + Routes are compared by the declaration they reach, so repeating a route + to the same interface is harmless while two different ones leave the + name meaning nothing here. A ``use`` of a module this project never + read is a route as well: it offers whatever it names, which nothing + here can compare, so it makes the name unresolved rather than letting a + readable route answer for it. + """ + declared_here = set(visible) + scope = ScopeUses(uses) + exported = { + module_name: cls._module_callback_interfaces(modules, source, seen=seen, exported_only=True) + for module_name in scope.modules() + if (source := modules.get(module_name.casefold())) is not None + } + for name in scope.accessible_names(lambda module: exported.get(module)): + reached = [ + cls._reached_callback_interface(route, exported) + for route in scope.routes_for(name, lambda module: exported.get(module)) + ] + resolved = cls._one_reached_interface(reached, local_name=name) + if resolved is None: + if name.casefold() not in declared_here: + visible.pop(name.casefold(), None) continue - for mapping in mappings: - signature = source_lookup.get(mapping.source.casefold()) - if signature is not None: - imported[mapping.local_name.casefold()] = signature - return imported + if override: + visible[name.casefold()] = resolved + else: + visible.setdefault(name.casefold(), resolved) + + @staticmethod + def _reached_callback_interface( + route: UseRoute, + exported: dict[str, dict[str, _CallbackInterface]], + ) -> _CallbackInterface | None: + """Return the interface one route reaches, or ``None`` for an unread module.""" + lookup = exported.get(route.module) + return None if lookup is None else lookup.get(route.source_name.casefold()) + + @classmethod + def _one_reached_interface( + cls, + reached: list[_CallbackInterface | None], + *, + local_name: str, + ) -> _CallbackInterface | None: + """Return the one interface a local name reaches, or ``None``. + + Routes are compared by the declaration they reach, so repeating a route + to one interface is harmless while two different ones leave the name + meaning nothing here. A route into a module this project never read + offers whatever it names, which nothing here can compare, so it makes + the name unresolved rather than letting a readable route answer for it. + """ + identities = {None if item is None else cls._callback_identity(item) for item in reached} + if len(identities) != 1: + return None + resolved = next((item for item in reached if item is not None), None) + # The importing scope may bind the interface under another spelling, + # which a contract written here has to state. + return None if resolved is None else replace(resolved, local_name=local_name) + + @staticmethod + def _callback_identity(resolved: _CallbackInterface) -> tuple[str | None, str]: + """Return the declaration one resolved interface names.""" + owner = resolved.module.name.casefold() if resolved.module is not None else None + return (owner, resolved.native_name.casefold()) def _callback_semantic_type( self, arg: FortranArgument | FortranVariable, - callback_interfaces: dict[str, FortranProcedureSignature], + callback_interfaces: dict[str, _CallbackInterface], *, derived_type_context: _DerivedTypeContext | None, ) -> SemanticType: @@ -723,33 +966,71 @@ def _callback_semantic_type( if getattr(arg, "pointer", False): return self._convert_variable_type(arg, derived_type_context=derived_type_context) interface_name = str(arg.kind or arg.name) - signature = callback_interfaces.get(interface_name.casefold()) + resolved = callback_interfaces.get(interface_name.casefold()) + signature = resolved.signature if resolved is not None else None if signature is None: - return self._convert_variable_type(arg, derived_type_context=derived_type_context) + semantic_type = self._convert_variable_type(arg, derived_type_context=derived_type_context) + if getattr(arg, "kind", None): + # The declaration named an interface that no supplied module + # declares, which later stages report against that name rather + # than against the opaque procedure type used as a placeholder. + semantic_type.metadata[UNRESOLVED_PROCEDURE_INTERFACE_METADATA] = interface_name + return semantic_type - context = self._procedure_derived_type_context(signature, derived_type_context) + # An interface body is written in the scope of the module that declares + # it, so its dummies resolve there rather than in the module that + # imported the interface -- which need not import the types it names. + declaring_context = ( + self._module_derived_type_context(resolved.module) + if resolved is not None and resolved.module is not None + else derived_type_context + ) + context = self._procedure_derived_type_context(signature, declaring_context) projected_arguments = list(signature.arguments) callback_arguments = [self.visit(item, derived_type_context=context) for item in projected_arguments] for source_argument, callback_argument in zip(projected_arguments, callback_arguments, strict=True): self._normalize_callback_reference_storage(callback_argument, source_argument) self._record_prototype_argument_intent(callback_argument, source_argument) + self._record_imported_prototype_type_origin( + callback_argument.semantic_type, + source_argument, + resolved, + derived_type_context, + ) callback_return = ( self.visit(signature.result, derived_type_context=context, as_type=True) if signature.result else SemanticType("None", dtype="None") ) + # A result carries the declaring module's types exactly as a dummy does. + self._record_imported_prototype_type_origin( + callback_return, + signature.result, + resolved, + derived_type_context, + ) prototype_module = str(signature.module or "") + # The declaring module names the interface; the importing scope may bind + # a different spelling. Both are source facts, and a contract needs each + # of them to import the right name under the right alias. + native_name = resolved.native_name if resolved is not None else interface_name + local_name = resolved.visible_name if resolved is not None else interface_name + declaring_scope = resolved.declaring_scope if resolved is not None else () return SemanticType( - interface_name, + local_name, dtype="Prototype", metadata={ "arguments": [item.semantic_type for item in callback_arguments], "callback_arguments": callback_arguments, "return": callback_return, PROTOTYPE_REF_METADATA: { - "name": interface_name, - "local_name": interface_name, + "name": native_name, + "local_name": local_name, "origin_module": prototype_module, + # The scope declaring the interface completes its identity: + # two procedures may each declare a `cb` meaning different + # signatures, which contract-name completion spells apart. + "declaring_scope": tuple(declaring_scope), }, "native_callback_kind": signature.kind, "callback_lifetime": "call", @@ -774,16 +1055,23 @@ def _callback_semantic_type( ), ) - @staticmethod def _normalize_callback_reference_storage( + self, callback_argument: SemanticArgument, source_argument: FortranArgument | FortranVariable, ) -> None: - """Make every non-value callback dummy a permissive reference contract.""" + """Make every non-value callback dummy a permissive reference contract. + + A dummy the callee may write needs storage the Python callable can + write through. Python has no writable scalar, so such a primitive + scalar records rank-zero storage rather than the value contract used + for a dummy the callee only reads. + """ if getattr(source_argument, "pass_by_value", False): return semantic_type = callback_argument.semantic_type - if semantic_type.name == "String" and semantic_type.rank == 0: + written_back = self._is_written_back_callback_scalar(source_argument, semantic_type) + if written_back or (semantic_type.name == "String" and semantic_type.rank == 0): semantic_type.storage = SemanticStorageContract( kind="array", read_only=False, @@ -806,6 +1094,59 @@ def _normalize_callback_reference_storage( semantic_type.storage.mutable = True semantic_type.ownership.mutable = True + def _is_written_back_callback_scalar( + self, + source_argument: FortranArgument | FortranVariable, + semantic_type: SemanticType, + ) -> bool: + """Report whether the callee may write one primitive scalar callback dummy. + + Fortran permits a dummy with no declared ``intent`` to be both read and + modified, so an undeclared direction is conservatively writable. Only + ``assume_intent_in_scalars`` elects the input-only default for it; the + declaration itself keeps no intent either way. + """ + if int(semantic_type.rank or 0) != 0 or semantic_type.name not in SEMANTIC_SCALAR_TYPE_NAMES: + return False + intent = getattr(source_argument, "intent", None) + if intent is None: + return not self.assume_intent_in_scalars + return str(intent).casefold() in {"out", "inout"} + + def _record_imported_prototype_type_origin( + self, + semantic_type: SemanticType, + declaration: FortranArgument | FortranVariable | None, + resolved: _CallbackInterface | None, + consuming_context: _DerivedTypeContext | None, + ) -> None: + """Name the declaring module for a derived type an imported interface owns. + + A type declared beside the interface is local to that module, so nothing + in the module that imported the interface identifies it. Recording the + origin keeps the identity with the module that declares the type rather + than the one that happened to import the interface. + """ + if resolved is None or resolved.module is None or declaration is None: + return + if str(getattr(declaration, "base_type", "")).casefold() != "derived": + return + declaring = resolved.module.name + consuming = str(consuming_context.module or "") if consuming_context is not None else "" + if declaring.casefold() == consuming.casefold(): + return + if EXTERNAL_TYPE_REF_METADATA in semantic_type.metadata: + return + name = str(getattr(declaration, "kind", "") or semantic_type.name) + wrapped = (declaring.casefold(), name.casefold()) in self.wrapped_derived_types + semantic_type.metadata[EXTERNAL_TYPE_REF_METADATA] = { + "name": name, + "local_name": name, + "origin_module": declaring, + "wrapped": wrapped, + "representation": "wrapped" if wrapped else "opaque", + } + @staticmethod def _record_prototype_argument_intent( argument: SemanticArgument, @@ -816,24 +1157,59 @@ def _record_prototype_argument_intent( if intent is not None: argument.origin.metadata[PROTOTYPE_INTENT_METADATA] = intent + @staticmethod + def _prototype_reference_identity( + semantic_type: SemanticType | None, + ) -> tuple[str, tuple[str, ...], str] | None: + """Return the prototype identity one callback annotation refers to. + + A prototype is identified by the module declaring it, the contained + procedure owning the block if any, and the name that scope gives it. + The module matters because another one may declare its own prototype + under the same spelling, and the two are different signatures. + """ + if semantic_type is None or semantic_type.storage is None or semantic_type.storage.kind != "callback": + return None + reference = semantic_type.metadata.get(PROTOTYPE_REF_METADATA) + if not isinstance(reference, dict): + return None + scope = tuple(str(part) for part in reference.get("declaring_scope", ())) + return ( + str(reference.get("origin_module", "")).casefold(), + scope, + str(reference.get("name", "")).casefold(), + ) + def _module_prototypes( self, module: FortranModule, context: _DerivedTypeContext, - referenced: set[str], + referenced: set[tuple[str, tuple[str, ...], str]], called: set[str], ) -> list[SemanticPrototype]: - """Convert every referenced interface into one exact prototype signature.""" + """Convert every referenced interface into one exact prototype signature. + + A block written inside a contained procedure declares a signature that + procedure alone can name, and two procedures may spell different + signatures the same way. Such a block therefore takes its own + scope-qualified contract identity and stays private: the contract needs + it to annotate that procedure's callback, but a ``use`` of this module + cannot reach it, so the module does not publish it. + """ prototypes: list[SemanticPrototype] = [] - seen: set[str] = set() + seen: set[tuple[str, tuple[str, ...], str]] = set() for interface in module.interfaces: + scope = self._interface_declaring_scope(interface) for signature in interface.procedures: - name = interface.name if interface.name and len(interface.procedures) == 1 else signature.name - if not (interface.abstract or name.casefold() in referenced or name.casefold() in called): + declared = interface.name if interface.name and len(interface.procedures) == 1 else signature.name + # Identity is the declaring scope with the name that scope + # gives; contract-name completion spells it. + identity = (module.name.casefold(), scope, declared.casefold()) + if not (interface.abstract or identity in referenced or declared.casefold() in called): continue - if name in seen: + if identity in seen: continue - seen.add(name) + seen.add(identity) arguments = [self.visit(item, derived_type_context=context) for item in signature.arguments] for source_argument, argument in zip(signature.arguments, arguments, strict=True): self._normalize_callback_reference_storage(argument, source_argument) @@ -845,15 +1221,16 @@ def _module_prototypes( ) prototypes.append( SemanticPrototype( - name=name, - native_name=name, + name=declared, + native_name=declared, + declaring_scope=scope, arguments=arguments, return_type=return_type, metadata=self._procedure_metadata(signature), - visibility=self._symbol_visibility(module, name), + visibility="private" if scope else self._symbol_visibility(module, declared), origin=SemanticOrigin( source_language="fortran", - native_name=name, + native_name=declared, native_abi=self._procedure_native_abi(signature), native_symbol=self._procedure_native_symbol(signature), native_scope=module.name, @@ -929,7 +1306,7 @@ def _visit_FortranProcedureSignature( visibility: str = "public", *, derived_type_context: _DerivedTypeContext | None = None, - callback_interfaces: dict[str, FortranProcedureSignature] | None = None, + callback_interfaces: dict[str, _CallbackInterface] | None = None, ) -> SemanticFunction: """Convert a parsed procedure signature into its callable semantic contract. @@ -1109,7 +1486,7 @@ def _visit_FortranModule( self, module: FortranModule, *, - callback_interfaces: dict[str, FortranProcedureSignature] | None = None, + module_index: dict[str, FortranModule] | None = None, ) -> SemanticModule: """Assemble the semantic contents of one parsed Fortran module. @@ -1120,10 +1497,8 @@ def _visit_FortranModule( """ context = self._module_derived_type_context(module) self._record_abstract_type_names(module) - callback_interfaces = { - **(callback_interfaces or {}), - **self._callback_interface_lookup(module), - } + index = module_index if module_index is not None else self._callback_module_index([module]) + callback_interfaces = self._module_callback_interfaces(index, module) source_procedures = [ *module.procedures, *self._module_explicit_interface_procedures(module), @@ -1133,15 +1508,27 @@ def _visit_FortranModule( proc, visibility=self._symbol_visibility(module, proc.name), derived_type_context=context, - callback_interfaces=callback_interfaces, + # A procedure-local ``use`` names an interface only inside that + # procedure, so each one resolves against its own imports. + callback_interfaces=self._scope_callback_interfaces( + index, + proc.uses, + base=callback_interfaces, + owner=module, + scope_name=proc.name, + ), ) for proc in source_procedures ] + # A callback argument names the prototype identity it resolved to, so + # the prototypes to convert are read from those identities rather than + # rediscovered from the spellings the annotations happen to use. callback_prototypes = { - argument.semantic_type.name.casefold() + identity for function in semantic_functions for argument in function.arguments - if argument.semantic_type.storage is not None and argument.semantic_type.storage.kind == "callback" + for identity in (self._prototype_reference_identity(argument.semantic_type),) + if identity is not None } prototypes = self._module_prototypes( module, @@ -1155,7 +1542,7 @@ def _visit_FortranModule( module, functions=semantic_functions, prototypes=prototypes, - uses={**module.uses, **procedure.uses}, + uses=[*module.uses, *procedure.uses], ) self._record_function_declaration_callables(function, callable_context) @@ -1179,12 +1566,17 @@ def _visit_FortranModule( ), ) - overload_sets = self._module_overload_sets( + overload_sets, inherited_functions = self._module_overload_sets( module, procedure_lookup, context, semantic_classes, + module_index=index, ) + # A specific reached through a use-associated generic is callable here, + # so it joins this module's functions. The import never bound its own + # name, so it stays private and is reachable only through the generic. + semantic_functions.extend(inherited_functions) metadata = {} common_variables = {name.casefold() for name in module.common_variables} enum_constants = [ @@ -1223,7 +1615,8 @@ def _visit_FortranModule( overload_sets=overload_sets, classes=semantic_classes, variables=module_variables + enum_constants, - imports=self._module_imports(module), + imports=self._module_imports(module, index), + reexports=self._module_reexports(module, index), metadata=metadata, origin=SemanticOrigin( source_language="fortran", @@ -1282,15 +1675,24 @@ def procedures_to_semantic_module( procedures: list[FortranProcedureSignature], *, name: str, - callback_interfaces: dict[str, FortranProcedureSignature] | None = None, + callback_interfaces: dict[str, _CallbackInterface] | None = None, + module_index: dict[str, FortranModule] | None = None, ) -> SemanticModule: """Package standalone procedures as the synthetic semantic module ``name``. This is used by file and project conversion after parser module handling. - Procedure order and optional callback lookup are passed unchanged to the - existing procedure visitor. + Procedure order is preserved, and each procedure resolves interfaces from + its own ``use`` list on top of the supplied file-level lookup. """ - semantic_functions = [self.visit(proc, callback_interfaces=callback_interfaces) for proc in procedures] + index = module_index or {} + base = callback_interfaces or {} + semantic_functions = [ + self.visit( + proc, + callback_interfaces=self._scope_callback_interfaces(index, proc.uses, base=base), + ) + for proc in procedures + ] function_lookup = {function.name.casefold(): function for function in semantic_functions} for procedure, function in zip(procedures, semantic_functions, strict=True): self._record_function_declaration_callables( @@ -1299,7 +1701,7 @@ def procedures_to_semantic_module( module=None, local_procedures=function_lookup, local_interfaces={}, - uses=dict(procedure.uses), + uses=list(procedure.uses), ), ) return SemanticModule( @@ -1312,20 +1714,398 @@ def procedures_to_semantic_module( ) @staticmethod - def _module_imports(module: FortranModule) -> list[str | SemanticImport]: - """Translate parser ``use`` mappings while preserving parser declaration order.""" - imports: list[str | SemanticImport] = [] - for module_name, mappings in module.uses.items(): - if not mappings: - imports.append(module_name) - else: - imports.append( - SemanticImport( - module=module_name, - items=[SemanticImportItem(source=item.source, target=item.target) for item in mappings], - ) + def _effective_accessibility(module: FortranModule): + """Return whether one name is public through its use-association routes. + + Accessibility is settled by precedence: an access statement naming the + entity decides it; otherwise any explicitly public module route makes + it public, while routes make it private only when every one is named + private. The module's bare default applies next and is itself public + when no bare statement appears. + """ + default_public = str(getattr(module, "default_visibility", "public")).casefold() != "private" + explicit_public = {str(name).casefold() for name in getattr(module, "public_symbols", ())} + explicit_private = {str(name).casefold() for name in getattr(module, "private_symbols", ())} + + def is_public(name: object, routes: Iterable[object] = ()) -> bool: + folded = str(name).casefold() + if folded in explicit_private: + return False + if folded in explicit_public: + return True + route_names = {str(route).casefold() for route in routes} + if route_names & explicit_public: + return True + if route_names and route_names <= explicit_private: + return False + return default_public + + return is_public + + @staticmethod + def _module_interfaces(module: FortranModule): + """Return the interface blocks declared by the module itself. + + A block written inside a contained procedure belongs to that procedure, + so what it declares is reachable only there. Those blocks are stored + alongside the module's own, and including them would put a local name + into everything a ``use`` of this module can reach. + """ + return tuple( + interface + for interface in module.interfaces + if str(getattr(interface, "declaring_scope_kind", "module")).casefold() == "module" + ) + + @staticmethod + def _module_enumerators(module: FortranModule): + """Return every enumerator one module's enum blocks declare. + + An enumerator is a named constant the module declares, and PRIK models + it as one: a ``use`` carries it exactly as it carries a ``parameter``, + so this layer reads it wherever it reads the module's variables. + """ + return tuple( + enumerator for enum in getattr(module, "enums", ()) for enumerator in getattr(enum, "enumerators", ()) + ) + + @staticmethod + def _module_declared_names(module: FortranModule) -> set[str]: + """Return the names declared by one module for accessibility resolution. + + A named interface block declares its generic. An abstract block names + no generic, and what it declares are the procedure signatures inside + it, which is what another module imports to write a ``procedure(...)`` + declaration. A specific inside an ordinary generic is not separately + declared here, because the generic is the name that block introduces. + An enumerator is a declared constant, so it is named here as a variable + is, which is what it becomes. + """ + return { + *(procedure.name.casefold() for procedure in module.procedures), + *(derived.name.casefold() for derived in module.derived_types), + *(variable.name.casefold() for variable in getattr(module, "variables", ())), + *(enumerator.name.casefold() for enumerator in FortranToIRConverter._module_enumerators(module)), + *( + interface.name.casefold() + for interface in FortranToIRConverter._module_interfaces(module) + if interface.name is not None + ), + *( + signature.name.casefold() + for interface in FortranToIRConverter._module_interfaces(module) + if interface.name is None + for signature in interface.procedures + if signature.name + ), + } + + @classmethod + def _module_declaration_dependencies(cls, module: FortranModule) -> set[str]: + """Return imported names used to express this module's declarations. + + The parser models retain declaration expressions but not executable + statements here, so intersecting their identifiers with names visible + through ``use`` distinguishes a dependency from an otherwise implicit + default-public re-export. Those identifiers come from parsing each + expression rather than scanning its text, so a name spelled inside a + character literal is read as part of that literal's value and not as a + reference to whatever it happens to spell. An explicit ``public`` + statement remains the module's authoritative request to publish the + name. + """ + + declaration_text: list[str] = [] + + def add_variable(variable: FortranVariable | None) -> None: + if variable is None: + return + declaration_text.extend( + str(value) + for value in ( + variable.kind, + variable.target_kind_expression, + variable.symbolic_value, + variable.value, + *variable.shape, + *variable.lbound, + *variable.ubound, ) - return imports + if value is not None + ) + + def add_procedure(procedure: FortranProcedureSignature) -> None: + for argument in procedure.arguments: + add_variable(argument) + add_variable(procedure.result) + for variable in procedure.variables.values(): + add_variable(variable) + + for variable in module.variables: + add_variable(variable) + for procedure in module.procedures: + add_procedure(procedure) + for derived in module.derived_types: + if derived.extends is not None: + declaration_text.append(str(getattr(derived.extends, "name", derived.extends))) + for field in derived.fields: + add_variable(field) + for binding in derived.procedure_bindings: + interface_name = binding.get("interface") + if interface_name: + declaration_text.append(str(interface_name)) + for interface in module.interfaces: + for procedure in interface.procedures: + add_procedure(procedure) + for enumerator in cls._module_enumerators(module): + declaration_text.extend( + str(value) for value in (enumerator.symbolic_value, enumerator.value) if value is not None + ) + + return { + identifier.casefold() + for text in declaration_text + for identifier in declaration_expression_identifiers(text) + } + + @classmethod + def _module_public_names( + cls, + module: FortranModule, + index: dict[str, FortranModule], + seen: frozenset[str] = frozenset(), + ) -> set[str]: + """Return the names one module offers to a plain ``use`` of it.""" + key = module.name.casefold() + if key in seen: + return set() + seen = seen | {key} + is_public = cls._effective_accessibility(module) + offered: dict[str, set[str]] = {name: set() for name in cls._module_declared_names(module)} + scope = ScopeUses(module.uses) + reachable = cls._offered_names(index, seen) + for name in scope.accessible_names(reachable): + folded = name.casefold() + offered.setdefault(folded, set()).update(route.module for route in scope.routes_for(name, reachable)) + return {name for name, routes in offered.items() if is_public(name, routes)} + + @staticmethod + def _reconcile_routes(origins: list[tuple[str, str, str]]) -> tuple[str, str, str] | None: + """Return the one entity a local name's routes reach, or ``None``. + + An ordinary entity has one declaration, so routes that disagree -- or a + readable route standing beside one this project never parsed -- mean the + local name reaches more than one thing, and choosing between them would + be a guess rather than a reading. + + A generic is the exception the language makes. Several accessible + generic interfaces spelled the same contribute their specific procedures + to one generic, so generic routes are contributors rather than rivals. + The merged specifics belong to the overload set assembled from them; + this record names the first contributor in source order, which is the + route the association is reported through. + """ + distinct = list(dict.fromkeys(origins)) + if len(distinct) == 1: + return distinct[0] + if distinct and all(kind == "generic" for kind, _module, _name in distinct): + return distinct[0] + return None + + @classmethod + def _offered_names(cls, index: dict[str, FortranModule], seen: frozenset[str] = frozenset()): + """Return what each used module publishes, or ``None`` when unparsed.""" + + def offered(module_name: str): + used = index.get(module_name.casefold()) + return None if used is None else cls._module_public_names(used, index, seen) + + return offered + + @classmethod + def _name_routes( + cls, + module: FortranModule, + index: dict[str, FortranModule], + local_name: str, + ) -> tuple[UseRoute, ...]: + """Return every route by which one module reaches one local name. + + A named mapping states the name it carries. A plain ``use`` carries + every public name of what it reads, so it is a route for this name only + when that module is parsed and offers it -- an unparsed module cannot be + enumerated, and assuming it carries the name would refuse resolutions + PRIK can make. How a route entered, through ``only`` or through a plain + ``use``, says nothing about what it carries, so both kinds are collected + together and weighed the same way afterwards. + """ + return ScopeUses(module.uses).routes_for(local_name, cls._offered_names(index)) + + @classmethod + def _use_associated_names( + cls, + module: FortranModule, + index: dict[str, FortranModule], + ) -> tuple[str, ...]: + """Return every local name one module reaches through ``use``, in order. + + A named mapping contributes the spelling it binds; a plain ``use`` + contributes the names the module it reads offers, which are known only + for a parsed one. Named spellings come first, so a name reached both + ways keeps the case its ``use`` statement wrote. + """ + return ScopeUses(module.uses).accessible_names(cls._offered_names(index)) + + @classmethod + def _use_associations( + cls, + module: FortranModule, + index: dict[str, FortranModule], + ) -> Iterator[tuple[str, tuple[str, ...], tuple[str, str, str]]]: + """Yield each use-associated name, the modules routing it, and what it names. + + The entity is ``(kind, declaring module, declared name)``, followed back + through every module re-exporting it. A name this module declares as + well, or one naming two entities, has no single association to report. + """ + declared = cls._module_declared_names(module) + for local_name in cls._use_associated_names(module, index): + routes = cls._name_routes(module, index, local_name) + if local_name.casefold() in declared or not routes: + continue + origin = cls._reconcile_routes( + [cls._resolve_reexport_origin(index, route.module, route.source_name) for route in routes] + ) + if origin is not None: + yield local_name, tuple(dict.fromkeys(route.module for route in routes)), origin + + @classmethod + def _module_imports( + cls, + module: FortranModule, + index: dict[str, FortranModule], + ) -> list[SemanticImport]: + """Return the use associations this module's declarations are written with. + + A contract binds the names its declarations use, and only those. A + ``use`` that extends a generic the module declares, or reaches a name no + declaration mentions, binds nothing a contract writes, and a name from a + module the compiler supplies has no contract to be read from. Each name + is read from the module declaring it, however many modules it passed + through, so every reference to one entity binds it the same way. + """ + dependencies = cls._module_declaration_dependencies(module) + imports: dict[str, SemanticImport] = {} + for local_name, _routes, (kind, origin_module, origin_name) in cls._use_associations(module, index): + if local_name.casefold() not in dependencies or kind == "intrinsic": + continue + imports.setdefault(origin_module.casefold(), SemanticImport(module=origin_module)).items.append( + SemanticImportItem(source=origin_name, target=None if origin_name == local_name else local_name) + ) + return list(imports.values()) + + def _module_reexports( + cls, + module: FortranModule, + module_index: dict[str, FortranModule] | None = None, + ) -> list[SemanticReexport]: + """Return the public names this module accesses through ``use``. + + A use-associated entity belongs to this module's interface when the + module's effective accessibility makes it public, which an ordinary + default-public module does without any access statement naming it. + Declaration use is recorded for later Python publication policy but + does not change this Fortran accessibility decision. + """ + is_public = cls._effective_accessibility(module) + dependencies = cls._module_declaration_dependencies(module) + explicit_public = {str(name).casefold() for name in module.public_symbols} + return [ + SemanticReexport( + local_name, + origin_module, + origin_name, + module.name, + entity_kind=kind, + access_modules=list(route_names), + declaration_dependency=local_name.casefold() in dependencies, + explicitly_public=local_name.casefold() in explicit_public, + ) + for local_name, route_names, (kind, origin_module, origin_name) in cls._use_associations( + module, module_index or {} + ) + if is_public(local_name, route_names) + ] + + @classmethod + def _resolve_reexport_origin( + cls, + index: dict[str, FortranModule], + module_name: str, + source_name: str, + seen: frozenset[tuple[str, str]] = frozenset(), + ) -> tuple[str, str, str]: + """Return where a published name is declared, following every hop. + + A module may publish a name it imported from a module that published it + in turn, so the module a ``use`` reads is not always the one declaring + the entity. Following the chain reports the declaration itself: its + kind, the module holding it, and the name it is declared under. A name + reached through no declaration, or through a cycle, stays unknown. + + Each hop applies the rule a direct import does. A module that does not + declare the name has it only through its own ``use`` statements, so the + chain continues only while Fortran accessibility keeps the name public + there -- a `private` statement in an intermediate module ends it -- and + only while every route through that module names one entity. Two routes + naming different declarations leave the origin genuinely ambiguous + there, exactly as they would in the importing module. + """ + if module_name.casefold() in _INTRINSIC_FORTRAN_MODULES: + # The compiler supplies it: there is no declaration to name, and no + # contract a name could be read from. + return "intrinsic", module_name, source_name + key = (module_name.casefold(), source_name.casefold()) + declaring = index.get(module_name.casefold()) + if declaring is None or key in seen: + return "unknown", module_name, source_name + kind = cls._declared_entity_kind(declaring, source_name) + if kind != "unknown": + return kind, declaring.name, source_name + seen = seen | {key} + routes = cls._name_routes(declaring, index, source_name) + route_names = tuple(dict.fromkeys(route.module for route in routes)) + if not routes or not cls._effective_accessibility(declaring)(source_name, route_names): + return "unknown", module_name, source_name + origin = cls._reconcile_routes( + [cls._resolve_reexport_origin(index, route.module, route.source_name, seen) for route in routes] + ) + return origin if origin is not None else ("unknown", module_name, source_name) + + @staticmethod + def _declared_entity_kind(declaring: FortranModule | None, source_name: str) -> str: + """Return what one name declares in the module that holds its declaration.""" + if declaring is None: + return "unknown" + key = source_name.casefold() + if any(procedure.name.casefold() == key for procedure in declaring.procedures): + return "procedure" + for interface in FortranToIRConverter._module_interfaces(declaring): + if interface.abstract and any(signature.name.casefold() == key for signature in interface.procedures): + return "prototype" + if interface.name and interface.name.casefold() == key: + return "prototype" if interface.abstract else "generic" + if not interface.abstract and any(signature.name.casefold() == key for signature in interface.procedures): + return "procedure" + if any(derived.name.casefold() == key for derived in declaring.derived_types): + return "derived_type" + if any(variable.name.casefold() == key for variable in getattr(declaring, "variables", ())): + return "variable" + # An enumerator is a named constant, which is the representation it + # already has downstream, so a route reaching one names a variable. + if any(enumerator.name.casefold() == key for enumerator in FortranToIRConverter._module_enumerators(declaring)): + return "variable" + return "unknown" def _declaration_callable_context( self, @@ -1333,14 +2113,14 @@ def _declaration_callable_context( functions: Iterable[SemanticFunction] = (), prototypes: Iterable[SemanticPrototype] = (), *, - uses: dict[str, list[FortranUseMapping]] | None = None, + uses: list[FortranUseStatement] | None = None, ) -> _DeclarationCallableContext: """Build lexical procedure facts for one module-owned declaration.""" return _DeclarationCallableContext( module=module.name, local_procedures={function.name.casefold(): function for function in functions}, local_interfaces={prototype.name.casefold(): prototype for prototype in prototypes}, - uses=dict(module.uses if uses is None else uses), + uses=list(module.uses if uses is None else uses), ) def _record_declaration_callables( @@ -1416,37 +2196,33 @@ def _resolve_declaration_callable( declaration=local, ) - explicit = [ - (module_name, mapping.source) - for module_name, mappings in context.uses.items() - for mapping in mappings - if mapping.local_name.casefold() == key - ] - if len(explicit) == 1: - return SemanticExpressionCallable( - name=name, - native_name=explicit[0][1], - native_scope=explicit[0][0], - source_language="fortran", - placement="module", - ) - if explicit: - return None - - wildcard_modules = [module_name for module_name, mappings in context.uses.items() if not mappings] - known_origins = [ - module_name for module_name in wildcard_modules if (module_name.casefold(), key) in self._known_procedures - ] - if len(known_origins) != 1: + scope = ScopeUses(context.uses) + offered = self._known_procedure_names() + routes = scope.routes_for(name, offered) + if len({route.key for route in routes}) != 1: return None return SemanticExpressionCallable( name=name, - native_name=name, - native_scope=known_origins[0], + native_name=routes[0].source_name, + native_scope=routes[0].module, source_language="fortran", placement="module", ) + def _known_procedure_names(self): + """Return the procedure names each module declares, or ``None``.""" + by_module: dict[str, set[str]] = {} + for module_name, procedure_name in self._known_procedures: + by_module.setdefault(module_name.casefold(), set()).add(procedure_name.casefold()) + + def offered(module_name: str): + key = module_name.casefold() + if key in self._known_modules: + return by_module.get(key, set()) + return None + + return offered + def _with_additional_wrapped_types( self, wrapped_types: Iterable[tuple[str, str]], @@ -1470,6 +2246,24 @@ def _with_additional_wrapped_types( ) converter._known_procedures = set(self._known_procedures) converter._abstract_derived_types = set(self._abstract_derived_types) + converter._known_modules |= self._known_modules + return converter + + def _with_additional_known_modules(self, modules: Iterable[str]) -> FortranToIRConverter: + """Return this converter or a clone that also knows parsed modules.""" + merged = self._known_modules | {str(module).casefold() for module in modules} + if merged == self._known_modules: + return self + converter = FortranToIRConverter( + type_map=self.type_map, + compile_time_values=self.compile_time_values, + wrapped_derived_types=self.wrapped_derived_types, + type_facts=self.type_facts, + assume_intent_in_scalars=self.assume_intent_in_scalars, + ) + converter._known_modules = merged + converter._known_procedures = set(self._known_procedures) + converter._abstract_derived_types = set(self._abstract_derived_types) return converter def _with_additional_known_procedures( @@ -1491,6 +2285,7 @@ def _with_additional_known_procedures( ) converter._known_procedures = merged converter._abstract_derived_types = set(self._abstract_derived_types) + converter._known_modules = self._known_modules | {module for module, _name in merged} return converter def _with_additional_abstract_types( @@ -1512,6 +2307,7 @@ def _with_additional_abstract_types( ) converter._known_procedures = set(self._known_procedures) converter._abstract_derived_types = merged + converter._known_modules = self._known_modules | {module for module, _name in merged} return converter @staticmethod @@ -1603,31 +2399,25 @@ def _procedure_derived_type_context( The new context keeps enclosing local types while separating procedure- local imports, which later controls imported type qualification. """ - uses = dict(parent.uses or {}) if parent is not None else {} - uses.update(proc.uses) + inherited = (parent.uses or ()) if parent is not None else () + uses = [*inherited, *proc.uses] return _DerivedTypeContext( module=proc.module or (parent.module if parent is not None else None), uses=uses, - procedure_uses=FortranToIRConverter._procedure_local_uses(proc, parent), + procedure_uses=FortranToIRConverter._procedure_local_uses(proc), local_types=parent.local_types if parent is not None else frozenset(), ) @staticmethod - def _procedure_local_uses( - proc: FortranProcedureSignature, - parent: _DerivedTypeContext | None, - ) -> dict[str, list[FortranUseMapping]]: - """Return imports introduced locally by ``proc`` relative to its parent. + def _procedure_local_uses(proc: FortranProcedureSignature) -> list[FortranUseStatement]: + """Return the ``use`` statements ``proc`` writes itself. - A parser-preserved ``_local_uses`` mapping takes precedence; otherwise - only imports differing from the parent context are returned. + The parser records them apart from the ones it inherits, so a + procedure's own imports are read rather than recovered by comparing its + table with its parent's. """ local_uses = getattr(proc, "_local_uses", None) - if isinstance(local_uses, dict): - return dict(local_uses) - if parent is None or parent.uses is None: - return dict(proc.uses) - return {module: mappings for module, mappings in proc.uses.items() if parent.uses.get(module) != mappings} + return list(proc.uses if local_uses is None else local_uses) def _derived_type_ref( self, @@ -1693,7 +2483,7 @@ def _resolve_derived_type_origin( def _resolve_derived_type_origin_from_uses( self, local_name: str, - uses: dict[str, list[FortranUseMapping]] | None, + uses: list[FortranUseStatement] | None, ) -> _ResolvedDerivedTypeOrigin: """Resolve one derived-type spelling from explicit or wildcard ``use`` maps. @@ -1701,33 +2491,33 @@ def _resolve_derived_type_origin_from_uses( imports intentionally remain unresolved so this conversion stage does not invent a native identity. """ - lname = local_name.lower() - explicit: list[tuple[str, str]] = [] - wildcard_modules: list[str] = [] - for module_name, mappings in (uses or {}).items(): - if not mappings: - wildcard_modules.append(module_name) - continue - for mapping in mappings: - if mapping.local_name.lower() == lname: - explicit.append((module_name, mapping.source)) - - if len(explicit) == 1: - return _ResolvedDerivedTypeOrigin(explicit[0][0], explicit[0][1]) - if len(explicit) > 1: + scope = ScopeUses(uses or ()) + offered = self._wrapped_type_names() + routes = scope.routes_for(local_name, offered) + identities = {route.key for route in routes} + if len(identities) == 1: + return _ResolvedDerivedTypeOrigin(routes[0].module, routes[0].source_name) + if identities: return _ResolvedDerivedTypeOrigin(None, local_name) - - wrapped_wildcards = [ - module_name - for module_name in wildcard_modules - if (module_name.lower(), lname) in self.wrapped_derived_types - ] - if len(wrapped_wildcards) == 1: - return _ResolvedDerivedTypeOrigin(wrapped_wildcards[0], local_name) - if len(wildcard_modules) == 1: - return _ResolvedDerivedTypeOrigin(wildcard_modules[0], local_name) + unresolved = scope.unresolved_routes_for(local_name, offered) + if len({route.key for route in unresolved}) == 1: + return _ResolvedDerivedTypeOrigin(unresolved[0].module, unresolved[0].source_name) return _ResolvedDerivedTypeOrigin(None, local_name) + def _wrapped_type_names(self): + """Return the wrapped type names each module declares, or ``None``.""" + by_module: dict[str, set[str]] = {} + for module_name, type_name in self.wrapped_derived_types: + by_module.setdefault(module_name.casefold(), set()).add(type_name.casefold()) + + def offered(module_name: str): + key = module_name.casefold() + if key in self._known_modules: + return by_module.get(key, set()) + return None + + return offered + def _semantic_type_name(self, var: FortranVariable) -> str: """Map a parsed intrinsic, derived, or procedure declaration to its dtype name. @@ -1765,14 +2555,14 @@ def _semantic_type_name(self, var: FortranVariable) -> str: def _semantic_kind_key(self, var: FortranVariable) -> str | None: """Normalize the declaration's kind text for semantic type-map lookup.""" + base_type = var.base_type.lower() + if base_type == "character": + return self._character_kind_expression(var) + raw_kind = var.target_kind_expression or var.kind if not raw_kind: return None - - base_type = var.base_type.lower() kind = self._resolve_compile_time_text(str(raw_kind)).strip().lower() - if base_type == "character": - return FortranToIRConverter._character_kind_key(kind, character_length_syntax=var.character_length_syntax) if base_type == "logical": return "c_bool" if kind == "c_bool" else kind literal_kind = FortranToIRConverter._literal_kind_key(kind) @@ -1787,32 +2577,29 @@ def _target_type_key(self, var: FortranVariable) -> tuple[str, str | None]: storage key, while explicit character kind clauses retain their kind. """ base_type = var.base_type.lower() + if base_type == "character": + return base_type, self._character_kind_expression(var) + raw_kind = var.target_kind_expression or var.kind if not raw_kind: return base_type, None - kind = self._resolve_compile_time_text(str(raw_kind)).strip().lower() - if base_type == "character": - if var.character_length_syntax: - return base_type, None - kind_match = re.search(r"(?:^|,)\s*kind\s*=\s*([^,]+)", kind) - if kind_match is not None: - kind = kind_match.group(1).strip() - elif kind.startswith("len="): - return base_type, None return base_type, kind - @staticmethod - def _character_kind_key(kind: str, *, character_length_syntax: bool = False) -> str | None: - """Extract a character-kind key while ignoring length-only spellings.""" - if character_length_syntax: - return None - kind_match = re.search(r"(?:^|,)\s*kind\s*=\s*([^,]+)", kind) - if kind_match is not None: - kind = kind_match.group(1).strip() - elif re.match(r"^len\s*=", kind): + def _character_kind_expression(self, var: FortranVariable) -> str | None: + """Return the kind a character declaration states, or ``None`` for the default. + + A character model records its selector apart from the legacy ``kind`` + field, which carries a length for some spellings and nothing at all for + a model built through ``record_character_selector``. The recorded fact + is therefore the authority, read before that field is consulted and in + place of searching a joined spelling, where an expression holding a + comma of its own -- a ``kind=max(c_char, 1)`` -- would be cut short. + """ + declared = getattr(var, "character_kind_expression", None) + if not declared: return None - return kind or None + return self._resolve_compile_time_text(str(declared)).strip().lower() or None def _target_type_fact(self, var: FortranVariable) -> dict[str, object] | None: """Return legacy fixed-width or configured compiler facts for ``var``.""" @@ -2060,7 +2847,7 @@ def _array_axes( if category == "assumed_rank": return ["..."] if category == "assumed_shape" and not contiguous: - return ["::Strided" for _dim in shape] + return ["::" for _dim in shape] axes: list[str] = [] for dim in shape: @@ -2117,7 +2904,7 @@ def _array_contiguous(category: str, *, contiguous: bool) -> bool | None: @staticmethod def _is_strided_axis(axis: str) -> bool: """Return whether an encoded public axis carries the strided marker.""" - return "Strided" in axis + return is_strided_extent(axis) @staticmethod def _reference_storage_contract(*, writes_argument: bool) -> SemanticStorageContract: @@ -2317,13 +3104,41 @@ def _bound_methods( ) return methods + @staticmethod + def _declared_specific(procedure: SemanticFunction) -> tuple[str, str]: + """Return the identity of the declaration one specific was taken from.""" + return _SpecificProcedure( + str(procedure.origin.native_scope or ""), + str(procedure.native_name or procedure.name), + ).key + + @staticmethod + def _bind_private_specifics_through_generic( + overload_set: ProcedureOverloadSet, + targets: list[_SpecificProcedure], + lookup: dict[tuple[str, str], SemanticFunction], + generic_name: str, + ) -> None: + """Bind each private specific through the generic name that reaches it. + + A specific its declaring module keeps private is unreachable by its own + name, while the generic -- or, for a constructor, the type name -- is + public and resolves to the same procedure. + """ + for target, candidate in zip(targets, overload_set.procedures, strict=True): + if lookup[target.key].visibility == "private": + candidate.native_name = generic_name + candidate.metadata[BIND_TARGET_METADATA] = generic_name + def _module_overload_sets( self, module: FortranModule, procedure_lookup: dict[str, SemanticFunction], context: _DerivedTypeContext, semantic_classes: list[SemanticClass], - ) -> list[ProcedureOverloadSet]: + *, + module_index: dict[str, FortranModule] | None = None, + ) -> tuple[list[ProcedureOverloadSet], list[SemanticFunction]]: """Convert module generic interfaces into function or class overload sets. Normal procedure generics remain module overloads. Defined operators @@ -2331,27 +3146,48 @@ def _module_overload_sets( constructors preserve the existing descriptive conversion failure. """ overload_sets: list[ProcedureOverloadSet] = [] + inherited_functions: list[SemanticFunction] = [] class_map = {semantic_class.name.casefold(): semantic_class for semantic_class in semantic_classes} for interface in module.interfaces: if not interface.name or interface.abstract: continue + if interface.declaring_scope_kind == "procedure": + # A generic written inside a procedure belongs to that + # procedure, so it is never part of the module's own interface. + continue + # A specific this module declares is identified by this module, so + # its own and its inline candidates are keyed the same way the + # inherited ones are. inline_lookup = { - signature.name.casefold(): self.visit( + _SpecificProcedure(module.name, signature.name).key: self.visit( signature, visibility=self._symbol_visibility(module, signature.name), derived_type_context=context, ) for signature in interface.procedures } - target_names = interface.specific_procedures or [signature.name for signature in interface.procedures] + own_lookup = { + _SpecificProcedure(module.name, name).key: function for name, function in procedure_lookup.items() + } + target_names, inherited_lookup = self._generic_target_names( + module, + interface, + module_index or {}, + inherited_functions, + ) procedures, missing = self._resolve_overload_targets( target_names, - procedure_lookup | inline_lookup, + own_lookup | inline_lookup | inherited_lookup, visibility=self._symbol_visibility(module, interface.name), ) if missing or not procedures: if self._is_procedure_generic_name(interface.name): - overload_sets.append(ProcedureOverloadSet(interface.name)) + overload_sets.append( + ProcedureOverloadSet( + interface.name, + visibility=self._symbol_visibility(module, interface.name), + ) + ) continue if self._is_procedure_generic_name(interface.name): constructor_class = class_map.get(interface.name.casefold()) @@ -2360,22 +3196,29 @@ def _module_overload_sets( # constructor, so its specifics become the class's own # `__init__` overload set rather than a module generic. constructor_set = self._normal_overload_set("__init__", procedures) - target_lookup = procedure_lookup | inline_lookup - for target_name, candidate in zip(target_names, constructor_set.procedures, strict=True): - if target_lookup[target_name.casefold()].visibility == "private": - # A private specific is unreachable by name; the type - # name is public and resolves to the same procedure. - candidate.native_name = interface.name - candidate.metadata[BIND_TARGET_METADATA] = interface.name + self._bind_private_specifics_through_generic( + constructor_set, + target_names, + own_lookup | inline_lookup | inherited_lookup, + interface.name, + ) self._merge_overload_sets(constructor_class.overload_sets, [constructor_set]) - self._mark_constructor_specifics(procedures, procedure_lookup, interface.name) + self._mark_constructor_specifics(procedures, own_lookup, interface.name) continue - overload_set = self._normal_overload_set(interface.name, procedures) - target_lookup = procedure_lookup | inline_lookup - for target_name, candidate in zip(target_names, overload_set.procedures, strict=True): - if target_lookup[target_name.casefold()].visibility == "private": - candidate.native_name = interface.name - candidate.metadata[BIND_TARGET_METADATA] = interface.name + overload_set = self._normal_overload_set( + interface.name, + procedures, + native_scope=str(module.origin.native_name or module.name) + if hasattr(module, "origin") + else module.name, + visibility=self._symbol_visibility(module, interface.name), + ) + self._bind_private_specifics_through_generic( + overload_set, + target_names, + own_lookup | inline_lookup | inherited_lookup, + interface.name, + ) overload_sets.append(overload_set) continue defined_sets = self._defined_overload_sets( @@ -2383,10 +3226,10 @@ def _module_overload_sets( procedures, class_map, ) - self._apply_assignment_projection_to_originals(interface.name, procedures, procedure_lookup, class_map) + self._apply_assignment_projection_to_originals(interface.name, procedures, own_lookup, class_map) for semantic_class, class_sets in defined_sets: self._merge_overload_sets(semantic_class.overload_sets, class_sets) - return overload_sets + return overload_sets, inherited_functions def _bound_overload_sets( self, @@ -2399,14 +3242,18 @@ def _bound_overload_sets( generic targets preserve the previous empty-placeholder behavior for ordinary procedure names and are otherwise omitted. """ - lookup = {method.name.casefold(): method for method in methods} + # A type-bound generic's specifics are this type's own methods, so + # they are identified by the module declaring the type. + owner_module = str(getattr(dtype, "module", "") or "") + lookup = {_SpecificProcedure(owner_module, method.name).key: method for method in methods} overload_sets: list[ProcedureOverloadSet] = [] for binding in dtype.generic_bindings: name = str(binding["name"]) attrs = {str(attr).casefold() for attr in binding.get("attrs", ())} visibility = "private" if "private" in attrs else "public" if "public" in attrs else None + targets = [_SpecificProcedure(owner_module, str(item)) for item in binding.get("targets", ())] procedures, missing = self._resolve_overload_targets( - list(binding.get("targets", ())), + targets, lookup, visibility=visibility, ) @@ -2416,8 +3263,8 @@ def _bound_overload_sets( continue if self._is_procedure_generic_name(name): overload_set = self._normal_overload_set(name, procedures) - for target_name, candidate in zip(binding.get("targets", ()), overload_set.procedures, strict=True): - if lookup[target_name.casefold()].visibility == "private": + for target, candidate in zip(targets, overload_set.procedures, strict=True): + if lookup[target.key].visibility == "private": candidate.native_name = name candidate.metadata[BIND_TARGET_METADATA] = name overload_sets.append(overload_set) @@ -2441,7 +3288,7 @@ def _apply_assignment_projection_to_originals( self, generic_name: str, procedures: list[SemanticFunction], - lookup: dict[str, SemanticFunction], + lookup: dict[tuple[str, str], SemanticFunction], classes: dict[str, SemanticClass], ) -> None: """Replace valid defined-assignment projections on their original procedures. @@ -2456,14 +3303,14 @@ def _apply_assignment_projection_to_originals( for procedure in procedures: if self._defined_procedure_error(kind, token, procedure, classes) is not None: continue - original = lookup.get((procedure.native_name or procedure.name).casefold()) + original = lookup.get(self._declared_specific(procedure)) if original is not None: original.projection = self._assignment_projection(original, 0) @staticmethod def _mark_constructor_specifics( procedures: list[SemanticFunction], - procedure_lookup: dict[str, SemanticFunction], + procedure_lookup: dict[tuple[str, str], SemanticFunction], type_name: str, ) -> None: """Hide the module functions a generic constructor selects between. @@ -2473,7 +3320,7 @@ def _mark_constructor_specifics( the public spelling the source chose for it. """ for procedure in procedures: - original = procedure_lookup.get((procedure.native_name or procedure.name).casefold()) + original = procedure_lookup.get(FortranToIRConverter._declared_specific(procedure)) if original is not None: original.metadata[CONSTRUCTOR_SPECIFIC_METADATA] = type_name @@ -2491,7 +3338,13 @@ def _merge_overload_sets( existing.procedures.extend(overload_set.procedures) @staticmethod - def _normal_overload_set(name: str, procedures: list[SemanticFunction]) -> ProcedureOverloadSet: + def _normal_overload_set( + name: str, + procedures: list[SemanticFunction], + *, + native_scope: str | None = None, + visibility: str = "public", + ) -> ProcedureOverloadSet: """Copy regular generic candidates and attach generic dispatch metadata. Type-bound methods are projected back to ordinary functions while @@ -2523,7 +3376,7 @@ def _normal_overload_set(name: str, procedures: list[SemanticFunction]) -> Proce candidate.metadata[OVERLOAD_KIND_METADATA] = "generic" candidate.metadata[OVERLOAD_TARGET_METADATA] = candidate.native_name or candidate.name candidates.append(candidate) - return ProcedureOverloadSet(name, candidates) + return ProcedureOverloadSet(name, candidates, native_scope=native_scope, visibility=visibility) def _defined_overload_sets( self, @@ -2777,20 +3630,139 @@ def _is_procedure_generic_name(name: str) -> bool: """Return whether a generic spelling is an ordinary callable identifier.""" return re.fullmatch(r"[a-z_]\w*", name, re.IGNORECASE) is not None + def _generic_target_names( + self, + module: FortranModule, + interface: FortranInterface, + modules: dict[str, FortranModule], + inherited_functions: list[SemanticFunction], + ) -> tuple[list[_SpecificProcedure], dict[tuple[str, str], SemanticFunction]]: + """Order one generic's specifics, inherited before locally declared. + + ``inherited_functions`` collects each specific this module gained from + the generics it extends, so the module can carry them for dispatch. A + specific is identified by its declaring module, so two contributors + that spell one the same way both survive. + """ + inherited, inherited_lookup = self._inherited_generic_specifics(module, interface.name, modules) + known = { + (str(item.origin.native_scope or "").casefold(), str(item.native_name or item.name).casefold()) + for item in inherited_functions + } + inherited_functions.extend(inherited_lookup[target.key] for target in inherited if target.key not in known) + own = [ + _SpecificProcedure(module.name, name) + for name in (interface.specific_procedures or [signature.name for signature in interface.procedures]) + ] + return [*inherited, *own], inherited_lookup + + def _inherited_generic_specifics( + self, + module: FortranModule, + generic_name: str, + modules: dict[str, FortranModule], + ) -> tuple[list[_SpecificProcedure], dict[tuple[str, str], SemanticFunction]]: + """Return the specifics one generic inherits from the generics it extends. + + A local interface block repeating a ``use``-associated generic name + extends that generic rather than replacing it, and the language lets + several accessible generics of one name contribute at once, so every + contributor is read rather than the first route that matches. One + declaration reached by two routes contributes once. Accumulation runs + one way: a declaring module never sees what a later module adds. + """ + inherited: list[_SpecificProcedure] = [] + lookup: dict[tuple[str, str], SemanticFunction] = {} + for source_module, source_generic in self._imported_generic_interfaces(module, generic_name, modules): + signatures = {procedure.name.casefold(): procedure for procedure in source_module.procedures} + source_context = self._module_derived_type_context(source_module) + names = source_generic.specific_procedures or [item.name for item in source_generic.procedures] + for name in names: + target = _SpecificProcedure(source_module.name, name) + signature = signatures.get(name.casefold()) + if signature is None or target.key in lookup: + continue + function = self.visit(signature, visibility="private", derived_type_context=source_context) + lookup[target.key] = function + inherited.append(target) + return inherited, lookup + + @staticmethod + def _module_generic_interface(module: FortranModule, name: str) -> FortranInterface | None: + """Return the module-scope generic one module declares under ``name``. + + A block written inside a contained procedure belongs to that procedure, + so it never contributes to what a ``use`` of the module reaches, and an + abstract block declares prototypes rather than a generic. + """ + return next( + ( + item + for item in FortranToIRConverter._module_interfaces(module) + if item.name and not item.abstract and item.name.casefold() == name.casefold() + ), + None, + ) + + @classmethod + def _imported_generic_interfaces( + cls, + module: FortranModule, + generic_name: str, + modules: dict[str, FortranModule], + seen: frozenset[tuple[str, str]] = frozenset(), + ) -> tuple[tuple[FortranModule, FortranInterface], ...]: + """Return every accessible generic one module imports under one name. + + A generic is not a single-origin entity: accessible generic interfaces + sharing an identifier all contribute their specifics to it. Every route + carrying the name is therefore followed, in source order, and a module + that re-exports the name rather than declaring a generic is walked + through to its own contributors. Fortran accessibility applies at each + hop, so a route a module makes private carries nothing onward. + """ + contributors: list[tuple[FortranModule, FortranInterface]] = [] + for route in cls._name_routes(module, modules, generic_name): + source_module = modules.get(route.module.casefold()) + if source_module is None: + continue + key = (source_module.name.casefold(), route.source_name.casefold()) + if key in seen: + continue + onward = cls._name_routes(source_module, modules, route.source_name) + route_names = tuple(dict.fromkeys(item.module for item in onward)) + if not cls._effective_accessibility(source_module)(route.source_name, route_names): + continue + declared = cls._module_generic_interface(source_module, route.source_name) + if declared is not None: + contributors.append((source_module, declared)) + contributors.extend( + cls._imported_generic_interfaces(source_module, route.source_name, modules, seen | {key}) + ) + # The same declaration reached by more than one route contributes once. + unique: dict[int, tuple[FortranModule, FortranInterface]] = {} + for contributor in contributors: + unique.setdefault(id(contributor[1]), contributor) + return tuple(unique.values()) + @staticmethod def _resolve_overload_targets( - target_names: list[str], - procedure_lookup: dict[str, SemanticFunction], + targets: list[_SpecificProcedure], + procedure_lookup: dict[tuple[str, str], SemanticFunction], *, visibility: str | None, ) -> tuple[list[SemanticFunction], list[str]]: - """Copy resolved generic targets and list target names absent from ``procedure_lookup``.""" + """Copy resolved generic targets and name those absent from ``procedure_lookup``. + + A target is identified by the module declaring it, so two contributors + that spell a specific the same way stay two procedures. + """ procedures: list[SemanticFunction] = [] missing: list[str] = [] - for target_name in target_names: - procedure = procedure_lookup.get(target_name.casefold()) + for target in targets: + procedure = procedure_lookup.get(target.key) if procedure is None: - missing.append(target_name) + missing.append(target.name) continue candidate = deepcopy(procedure) if visibility is not None: @@ -3204,6 +4176,7 @@ def _visit_FortranFile(self, node: FortranFile, **_context): node.block_data_units, node.procedures, node.derived_types, + node.interfaces, ) for collection in collections: for child in collection: @@ -3238,6 +4211,25 @@ def _visit_FortranBlockData(node: FortranBlockData, **_context): for variable in node.variables ) + def _visit_FortranInterface( + self, + node: FortranInterface, + *, + module_name: str | None = None, + **_context, + ): + """Return the variable contexts an interface body declares. + + An interface body types its own dummies, and the kind it names may come + from a ``use`` written inside that body. Those variables reach a target + probe only from here, since no module variable or module procedure + declares them. + """ + contexts = [] + for procedure in node.procedures: + contexts.extend(self._visit(procedure, module_name=module_name or node.module)) + return tuple(contexts) + @staticmethod def _visit_FortranProcedureSignature( node: FortranProcedureSignature, @@ -3299,6 +4291,8 @@ def _module_variable_contexts( contexts.extend(self._visit(procedure, module_name=owner)) for derived_type in node.derived_types: contexts.extend(self._visit(derived_type, module_name=owner)) + for interface in node.interfaces: + contexts.extend(self._visit(interface, module_name=owner)) return tuple(contexts) @@ -3413,6 +4407,20 @@ def collect_fortran_type_storage_requirements( return requirements +def _declared_kind_expression(var: FortranVariable) -> str | None: + """Return the kind text one declaration states, as its model records it. + + A character model records its selector apart from the legacy ``kind`` + field, which carries a length for some spellings and nothing at all for a + model built through ``record_character_selector``, so the character kind is + read from that recorded fact. Every other base type states its kind in the + field itself. + """ + if str(var.base_type or "").lower() == "character": + return var.character_kind_expression + return var.kind or None + + def collect_semantic_compile_time_requirements( parsed, *, @@ -3476,11 +4484,12 @@ def add_requirement( add_requirement("parameter_value", ctx, expression=expression) base_type = parameter_base_type - if base_type not in {"integer", "real", "complex", "logical", "character"} or not var.kind: + declared_kind = _declared_kind_expression(var) + if base_type not in {"integer", "real", "complex", "logical", "character"} or not declared_kind: continue kind_key = converter._semantic_kind_key(var) if converter.type_map.get((base_type, kind_key)) is None: - expression = _resolve_compile_time_text(str(var.kind), values) + expression = _resolve_compile_time_text(str(declared_kind), values) add_requirement( "unsupported_kind", ctx, @@ -3492,32 +4501,40 @@ def add_requirement( return requirements -def _resolve_semantic_value(value, compile_time_values: dict[str, str]): - """Recursively resolve compile-time text inside a semantic metadata value.""" - if isinstance(value, str): - return _resolve_compile_time_text(value, compile_time_values) - if isinstance(value, list): - return [_resolve_semantic_value(item, compile_time_values) for item in value] - if isinstance(value, tuple): - return tuple(_resolve_semantic_value(item, compile_time_values) for item in value) - if isinstance(value, dict): - return {key: _resolve_semantic_value(item, compile_time_values) for key, item in value.items()} - return value +#: Metadata keys whose value is declaration expression text, not an opaque tag. +#: +#: Compile-time specialization rewrites identifiers, so it may only reach a +#: field the schema says holds an expression. Every other metadata value -- +#: a policy tag, a native identity, an enumerated choice -- is opaque text +#: that happens to look like an identifier, and resolving it would silently +#: replace the recorded decision with a parameter's value. +_EXPRESSION_METADATA_KEYS = frozenset({"fortran_character_length", "fortran_initializer"}) + + +def _resolve_metadata_expressions( + metadata: dict[str, object], + compile_time_values: dict[str, str], +) -> dict[str, object]: + """Return ``metadata`` with only its declared expression fields resolved.""" + resolved = dict(metadata) + for key in _EXPRESSION_METADATA_KEYS & resolved.keys(): + value = resolved[key] + if isinstance(value, str): + resolved[key] = _resolve_compile_time_text(value, compile_time_values) + return resolved def _resolve_semantic_type_compile_time_values( semantic_type: SemanticType | None, compile_time_values: dict[str, str], ) -> None: - """Resolve shape, constraint, and storage text on one semantic type in place.""" + """Resolve shape, storage, and expression metadata on one semantic type in place.""" if semantic_type is None: return semantic_type.shape = [_resolve_compile_time_text(dim, compile_time_values) for dim in semantic_type.shape] - for constraint in semantic_type.constraints: - constraint.arguments = _resolve_semantic_value(constraint.arguments, compile_time_values) - semantic_type.metadata = _resolve_semantic_value(semantic_type.metadata, compile_time_values) + semantic_type.metadata = _resolve_metadata_expressions(semantic_type.metadata, compile_time_values) if semantic_type.storage is not None: - semantic_type.storage.metadata = _resolve_semantic_value( + semantic_type.storage.metadata = _resolve_metadata_expressions( semantic_type.storage.metadata, compile_time_values, ) @@ -3533,7 +4550,7 @@ def _resolve_semantic_type_compile_time_values( None if dim is None else _resolve_compile_time_text(dim, compile_time_values) for dim in array.upper_bounds ] - array.metadata = _resolve_semantic_value(array.metadata, compile_time_values) + array.metadata = _resolve_metadata_expressions(array.metadata, compile_time_values) def _resolve_semantic_argument_compile_time_values( @@ -3542,23 +4559,22 @@ def _resolve_semantic_argument_compile_time_values( ) -> None: """Resolve type, default, and metadata text on one semantic argument in place.""" _resolve_semantic_type_compile_time_values(arg.semantic_type, compile_time_values) - arg.default_value = _resolve_semantic_value(arg.default_value, compile_time_values) - arg.metadata = _resolve_semantic_value(arg.metadata, compile_time_values) + if isinstance(arg.default_value, str): + arg.default_value = _resolve_compile_time_text(arg.default_value, compile_time_values) + arg.metadata = _resolve_metadata_expressions(arg.metadata, compile_time_values) def _resolve_semantic_function_compile_time_values( func: SemanticFunction, compile_time_values: dict[str, str], ) -> None: - """Resolve all type-bearing fields and projection values on one function in place.""" + """Resolve all type-bearing fields on one function and its locals in place.""" for arg in func.arguments: _resolve_semantic_argument_compile_time_values(arg, compile_time_values) for local in func.locals: _resolve_semantic_argument_compile_time_values(local, compile_time_values) _resolve_semantic_type_compile_time_values(func.return_type, compile_time_values) - for mapping in func.projection: - mapping.value = _resolve_semantic_value(mapping.value, compile_time_values) - func.metadata = _resolve_semantic_value(func.metadata, compile_time_values) + func.metadata = _resolve_metadata_expressions(func.metadata, compile_time_values) def _resolve_semantic_module_compile_time_values( @@ -3575,8 +4591,8 @@ def _resolve_semantic_module_compile_time_values( _resolve_semantic_argument_compile_time_values(field, compile_time_values) for method in declaration.methods: _resolve_semantic_function_compile_time_values(method, compile_time_values) - declaration.metadata = _resolve_semantic_value(declaration.metadata, compile_time_values) - module.metadata = _resolve_semantic_value(module.metadata, compile_time_values) + declaration.metadata = _resolve_metadata_expressions(declaration.metadata, compile_time_values) + module.metadata = _resolve_metadata_expressions(module.metadata, compile_time_values) def resolve_semantic_compile_time_values( @@ -3674,12 +4690,15 @@ def fortran_file_to_semantic_modules( wrapped_derived_types: Iterable[tuple[str, str]] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, assume_intent_in_scalars: bool = False, + sibling_modules: Iterable[FortranModule] = (), ) -> list[SemanticModule]: """Convert every module and standalone procedure group in one parsed file. Use this rather than the single-module helper when file-level procedures matter. Parser module ordering is retained, and ``standalone_module_name`` - controls the synthetic module used for top-level procedures. + controls the synthetic module used for top-level procedures. Pass + ``sibling_modules`` when other files were parsed alongside this one so an + abstract interface imported across files resolves. Example: >>> parsed = FortranFile(procedures=[FortranProcedureSignature(name="tick", kind="subroutine")]) @@ -3694,6 +4713,7 @@ def fortran_file_to_semantic_modules( ).visit( parsed_file, standalone_module_name=standalone_module_name, + sibling_modules=sibling_modules, ) diff --git a/prik/semantics/models.py b/prik/semantics/models.py index 05b81507f..fd07317ba 100644 --- a/prik/semantics/models.py +++ b/prik/semantics/models.py @@ -14,10 +14,13 @@ from dataclasses import dataclass, field from typing import Any +from prik.utilities.declaration_expressions import outside_character_literals + EXTERNAL_TYPE_REF_METADATA = "external_type_ref" PROTOTYPE_REF_METADATA = "prototype_ref" PROTOTYPE_INTENT_METADATA = "prototype_intent" +UNRESOLVED_PROCEDURE_INTERFACE_METADATA = "unresolved_procedure_interface" INTERNAL_MODULE_VARIABLE_ACCESS_METADATA = "internal_module_variable_access" INTERNAL_MODULE_VARIABLE_NAME_METADATA = "internal_module_variable_name" INTERNAL_NATIVE_ARRAY_HANDLE_OPERATION_METADATA = "internal_native_array_handle_operation" @@ -372,6 +375,16 @@ class SemanticPrototype(SemanticFunction): pure: bool = False + declaring_scope: tuple[str, ...] = () + """Contained procedure declaring the interface, empty for a module's own. + + A prototype's identity is structural -- the scope that declares it together + with the name that scope gives it -- because two procedures may each declare + a different signature under one spelling. ``name`` carries the contract + spelling settled for that identity, which is allocated once and read + everywhere rather than rebuilt from the scope. + """ + # ============================================================ # Semantic Methods @@ -390,6 +403,18 @@ class SemanticMethod(SemanticFunction): class ProcedureOverloadSet: name: str procedures: list[SemanticFunction] = field(default_factory=list) + native_scope: str | None = None + """Module declaring the generic, which need not own every specific.""" + + visibility: str = "public" + """Accessibility the declaring module gives the generic name itself. + + A generic follows its module's accessibility like any other declaration, so + a `private` one names a dispatcher the module keeps to itself. Publication + reads this rather than assuming a generic is public. + """ + + metadata: dict[str, Any] = field(default_factory=dict) FORTRAN_GENERIC_NAME_METADATA = "fortran_generic_name" @@ -398,6 +423,35 @@ class ProcedureOverloadSet: PYTHON_BOUND_POSITION_METADATA = "python_bound_position" PYTHON_METHOD_NAME_METADATA = "python_method_name" PYTHON_EXPORTS_METADATA = "python_exports" +CONTRACT_NAME_METADATA = "contract_name" +CONTRACT_TARGET_NAME_METADATA = "contract_target_name" +CONTRACT_BASE_NAMES_METADATA = "contract_base_names" +#: The one spelling a module's contract writes for each name it imports, keyed +#: by the name its source binds; annotations, imports, and ``__all__`` read it. +CONTRACT_IMPORT_NAMES_METADATA = "contract_import_names" + + +def completed_contract_name(owner, default_name: str | None = None) -> str: + """Return the spelling contract-name completion recorded for one declaration. + + This reads the decision and never makes it: an owner completion did not + reach is an error, because naming it here would be a second authority. + """ + completed = owner.metadata.get(CONTRACT_NAME_METADATA) + if completed is None: + name = default_name if default_name is not None else getattr(owner, "name", None) + raise ValueError(f"Contract name for {name!r} is incomplete; run complete_python_export_policy before emission") + return str(completed) + + +def export_namespace(export: dict[str, object]) -> tuple[str, ...]: + """Return one normalized namespace tuple from semantic export metadata.""" + raw_namespace = export.get("namespace", ()) + if not isinstance(raw_namespace, tuple | list): + return () + return tuple(str(part) for part in raw_namespace) + + PYTHON_EXPORTS_PREPARED_METADATA = "python_exports_prepared" POLICY_COMPLETION_PREPARED_METADATA = "policy_completion_prepared" HIDDEN_NATIVE_OUTPUT_METADATA = "hidden_native_output" @@ -599,12 +653,22 @@ def _canonical_expression(value: Any, name_map: dict[str, str]) -> Any: def _canonical_expression_text(text: str, name_map: dict[str, str]) -> str: + """Rename argument references so two procedures compare by shape, not naming. + + A character literal's contents are its value, not a reference to anything, + so renaming stops at the quotes: two procedures whose string defaults spell + their own argument names -- ``f(n, label='n')`` and ``f(m, label='m')`` -- + default to different text and must not compare equal. + """ if not name_map: return text - result = text - for name, placeholder in name_map.items(): - result = re.sub(rf"\b{re.escape(name)}\b", placeholder, result) - return result + + def renamed(chunk: str) -> str: + for name, placeholder in name_map.items(): + chunk = re.sub(rf"\b{re.escape(name)}\b", placeholder, chunk) + return chunk + + return outside_character_literals(text, renamed) # ============================================================ @@ -659,8 +723,18 @@ class SemanticClass: @dataclass class SemanticImportItem: + """One name an import binds, spelled as the sources and the contracts write it. + + ``source`` names the entity the way the module it is read from spells it, + and ``target`` the name bound here when that differs. Contract-import + completion adds the spellings the completed contracts use, which a + source-derived contract writes instead; they stay unset until then. + """ + source: str target: str | None = None + contract_source: str | None = None + contract_target: str | None = None @dataclass @@ -669,12 +743,71 @@ class SemanticImport: items: list[SemanticImportItem] = field(default_factory=list) +@dataclass +class SemanticReexport: + """Record one public use-associated name and its declaring entity. + + Fortran accessibility determines whether the association exists here. + Python export policy separately decides whether the importing namespace + publishes it; declaration use must not erase the Fortran association. + """ + + local_name: str + origin_module: str + source_name: str + module: str = "" + """Module publishing the name, which is not the one declaring it.""" + + python_name: str = "" + """The Python name this module publishes the re-export under. + + Post-IR export policy completes it inside the same namespace ledger as the + module's own declarations, so an alias cannot be given a name a declaration + already holds. Every later stage reads it rather than deriving one. + """ + + entity_kind: str = "unknown" + """What the published name declares where it comes from. + + Re-export reaches Python as a namespace alias only for an entity that is one + Python object, which today means an ordinary procedure or a derived type. + Every other kind -- a callback prototype, a module variable whose state + stays live, a generic -- keeps to the semantic and contract-import paths + that already carry it, and records its kind here rather than an alias that + would misrepresent it. An ``intrinsic`` name comes from a module the + compiler supplies, which declares nothing a contract could read. + """ + + access_modules: list[str] = field(default_factory=list) + """Immediate used-module routes through which the local name is accessible.""" + + declaration_dependency: bool = False + """Whether this module uses the local name to express a declaration.""" + + explicitly_public: bool = False + """Whether an entity-list ``public`` statement names the local name.""" + + python_exported: bool | None = None + """Completed post-IR decision to publish this association to Python.""" + + def publishes_to_python(self) -> bool: + """Return the completed Python publication decision.""" + if self.python_exported is None: + raise ValueError( + f"Python re-export policy for {self.module}.{self.local_name} is incomplete; " + "run complete_python_export_policy before consuming it" + ) + return self.python_exported + + @dataclass class SemanticModule: name: str functions: list[SemanticFunction] = field(default_factory=list) + reexports: list[SemanticReexport] = field(default_factory=list) + prototypes: list[SemanticPrototype] = field(default_factory=list) overload_sets: list[ProcedureOverloadSet] = field(default_factory=list) @@ -684,6 +817,20 @@ class SemanticModule: imports: list[str | SemanticImport] = field(default_factory=list) + exported_names: list[str] | None = None + """This module's public symbol surface, or ``None`` when it states no list. + + A published symbol is not always one Python object: a prototype names a + callback signature that contracts refer to and nothing exposes at runtime, + while a procedure names a callable. What the name declares decides how + publishing it appears. + + A contract states its whole public surface here, so a name it imports is + published when it is listed and stays a dependency when it is not. The list + is written to be edited: a generated contract fills it with what the source + publishes, and removing or adding a name changes what reaches Python. + """ + metadata: dict[str, Any] = field(default_factory=dict) origin: SemanticOrigin = field(default_factory=SemanticOrigin, compare=False) diff --git a/prik/semantics/pyi2ir.py b/prik/semantics/pyi2ir.py index adde496e8..4502cd3af 100644 --- a/prik/semantics/pyi2ir.py +++ b/prik/semantics/pyi2ir.py @@ -254,7 +254,6 @@ def _resolve_local_prototype_references(self) -> None: semantic_type, prototype, origin_module=self.module.name, - source_name=prototype.name, ) def _resolve_declaration_expression_callables(self) -> None: @@ -292,11 +291,14 @@ def _declaration_callable_imports( namespaces: dict[str, str] = {} for imported in self.module.imports: if isinstance(imported, SemanticImport): + # A sibling leaf is imported relatively, but a native scope is + # the module's own name, so the relative marker is dropped. + module_name = imported.module.lstrip(".") if imported.items: for item in imported.items: - explicit[(item.target or item.source).casefold()] = (imported.module, item.source) + explicit[(item.target or item.source).casefold()] = (module_name, item.source) else: - namespaces[imported.module.split(".", 1)[0].casefold()] = imported.module + namespaces[module_name.split(".", 1)[0].casefold()] = module_name continue for item in str(imported).split(","): module_name, _, alias = item.strip().partition(" as ") @@ -449,6 +451,7 @@ def class_def( visibility: str, native_abi: str | None = None, abstract: bool = False, + native_name: str | None = None, ) -> SemanticClass: """Convert one class AST node, its body, and supported native metadata. @@ -479,7 +482,9 @@ def class_def( metadata["fortran_bind_c"] = True semantic_class = SemanticClass( name=node.name, - native_name=node.name, + # A class names its Python type; `bind` states the native type it + # reaches when the two are spelled differently. + native_name=native_name or node.name, fields=body.fields, methods=body.methods, destructors=body.destructors, @@ -537,6 +542,7 @@ def function_def( has_native_call: bool = False, release_gil: bool = False, error_status_policy: dict[str, object] | None = None, + restates_projected_result: bool = False, ) -> SemanticFunction: """Convert a module-level stub into a semantic function declaration. @@ -550,6 +556,7 @@ def function_def( node, projection=actual_projection, native_result=native_result, + restates_projected_result=restates_projected_result, ) metadata = {BIND_TARGET_METADATA: native_name} if native_name is not None else {} if has_native_call: @@ -648,6 +655,7 @@ def method_def( release_gil: bool = False, error_status_policy: dict[str, object] | None = None, deferred: bool = False, + restates_projected_result: bool = False, ) -> SemanticMethod: """Convert a class stub into a semantic method declaration. @@ -662,6 +670,7 @@ def method_def( projection=actual_projection, native_result=native_result, drop_untyped_self=True, + restates_projected_result=restates_projected_result, ) metadata = {BIND_TARGET_METADATA: native_name} if native_name is not None else {} if deferred: @@ -816,8 +825,6 @@ def ann_assign( """ name = self.annotation_target(node.target) visibility, semantic_type, original_name = self.visible_type(node.annotation) - if original_name is not None: - name = original_name self._validate_python_value_policy( semantic_type, writable=self._type_uses_writable_storage(semantic_type), @@ -829,6 +836,11 @@ def ann_assign( visibility=visibility, default_value=self.assignment_default_value(node.value, semantic_type), ) + if original_name is not None: + # A declared name is what Python calls this entity; `SourceName` + # states the entity it reaches, exactly as `bind` does for a + # callable, and leaves the declared name alone. + binding.origin.native_name = original_name if visibility == "private": binding.origin.metadata[USER_PRIVATE_METADATA] = True binding.optional = self.default_marks_optional(node.value) @@ -850,8 +862,8 @@ def decorators(self, nodes: list[ast.expr], *, context: str) -> _Decorators: raise ValueError("destroy can only be combined with bind") if parsed.overload_target is not None and parsed.has_native_call: raise ValueError("overload cannot be combined with native_call; put native_call on the specific procedure") - if parsed.pure and not parsed.prototype: - raise ValueError("pure requires prototype") + if parsed.pure and parsed.overload_target is not None: + raise ValueError("pure describes a native procedure; an overload dispatcher names none") if parsed.prototype: if parsed.standalone: raise ValueError( @@ -954,11 +966,11 @@ def _apply_prototype_decorator(parsed: _Decorators, node: ast.expr, context: str @staticmethod def _apply_pure_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: - """Mark an exact interface with the native pure characteristic.""" + """Mark a module-level native procedure with the Fortran pure characteristic.""" if isinstance(node, ast.Call): raise ValueError("pure does not accept arguments") if context != ".pyi": - raise ValueError("pure is only valid for module-level prototype declarations") + raise ValueError("pure is only valid for module-level declarations") if parsed.pure: raise ValueError("Duplicate pure decorator") parsed.pure = True @@ -1276,15 +1288,47 @@ def _validate_overload_signature( @staticmethod def _matches_projected_return(declared, target_return) -> bool: - """Compare a declared result with a target's, ignoring result ownership.""" + """Compare a declared result with a target's, ignoring result ownership. + + A projected output is written through as a native argument and returned + as an ordinary result. Whether the call writes it is a property of that + argument passing, which a declared result type does not state, so the + comparison reads it from the declaration rather than the target. + """ declared_type = _PyiAstParser._visible_overload_type(declared) target_type = _PyiAstParser._visible_overload_type(target_return) if declared_type is None or target_type is None: return declared_type == target_type - expected = deepcopy(target_type) + declared_type = deepcopy(declared_type) + # An unwrapped `| None` leaves a parse marker behind when no projection + # consumes it, which names nothing about the type itself. + declared_type.metadata.pop(_PYI_OPTIONAL_RETURN_METADATA, None) + expected = _PyiAstParser._visible_projected_result(target_type) expected.ownership = deepcopy(declared_type.ownership) + if expected.storage is not None and declared_type.storage is not None: + expected.storage.read_only = declared_type.storage.read_only + expected.storage.mutable = declared_type.storage.mutable return declared_type == expected + @staticmethod + def _visible_projected_result(target_type: SemanticType) -> SemanticType: + """Return the public result form a declaration can spell for a projection. + + A native scalar descriptor result is written as a nullable value plus a + `native_call` result wrapper naming the descriptor, and an overload + declaration carries no `native_call`. Its descriptor topology therefore + has no place in the declared annotation, exactly as the contract printer + emits it. + """ + expected = deepcopy(target_type) + if _PyiAstParser._semantic_scalar_descriptor_kind(expected) is None: + return expected + for key in ("fortran_allocatable", "fortran_pointer", "fortran_pointer_association"): + expected.metadata.pop(key, None) + if expected.storage is not None and expected.storage.kind in {"reference", "pointer", "address"}: + expected.storage = None + return expected + @staticmethod def _projected_overload_arguments( function: SemanticFunction, @@ -1943,6 +1987,18 @@ def semantic_type_annotation( ) semantic_type.metadata[OPTIONAL_ABSENT_HANDLE_METADATA] = True return semantic_type, None + if self.is_subscript_of(node, "Final"): + # `Final` marks the value immutable and wraps the annotation that + # carries any source name, which the declaration still needs. + items = self.subscript_items(node) + if len(items) == 1: + semantic_type, original_name = self.semantic_type_annotation( + items[0], + allow_optional_absent_handle=allow_optional_absent_handle, + ) + if not any(constraint.name == "Constant" for constraint in semantic_type.constraints): + semantic_type.constraints.append(SemanticConstraint("Constant")) + return semantic_type, original_name if not self.is_subscript_of(node, "Annotated"): return self.semantic_type(node), None @@ -2235,7 +2291,7 @@ def _flat_array_dimensions( ) lower_bounds, upper_bounds = _PyiAstParser._bounds_from_source_shape(source_shape) return ( - [dim.replace(_STRIDED_DIMENSION_SENTINEL, "Strided") for dim in dims], + [dim.replace(_STRIDED_DIMENSION_SENTINEL, "") for dim in dims], None, source_shape, lower_bounds, @@ -2658,14 +2714,20 @@ def dimension_text(self, node: ast.expr) -> str: return expression def slice_text(self, node: ast.Slice) -> str: - """Render one dimension slice, preserving the contract's strided marker.""" + """Render one dimension slice as written. + + A dimension carries bounds only. The step position spells nothing the + contract grammar defines, so a value there is rejected rather than read + as an extent expression. + """ + if node.step is not None: + step = ast.unparse(node.step) + raise ValueError( + f"Array dimension step {step!r} is not part of the contract grammar; " + "write 'T[::]' for a strided axis or 'T[:]' for a contiguous one" + ) lower = "" if node.lower is None else ast.unparse(node.lower) upper = "" if node.upper is None else ast.unparse(node.upper) - step = "" - if node.step is not None: - step = _STRIDED_DIMENSION_SENTINEL if self.matches_name(node.step, "Strided") else ast.unparse(node.step) - if step: - return f"{lower}:{upper}:{step}" return f"{lower}:{upper}" # Callback and result conversion @@ -3013,6 +3075,7 @@ def _callable_parts( projection: list[ProjectionMapping], native_result: ProjectionMapping | None = None, drop_untyped_self: bool = False, + restates_projected_result: bool = False, ) -> tuple[list[SemanticArgument], SemanticType | None]: """Build a callable's arguments, results, and native projection metadata. @@ -3030,6 +3093,13 @@ def _callable_parts( # Construct direct and projected outputs from the Python return shape. optional_return_positions = self._optional_native_return_positions(projection, native_result) + if restates_projected_result: + # An overload declaration restates the result its specific projects, + # and the projection that makes a slot nullable lives on that + # specific -- a declaration carrying one is rejected outright. Read + # every slot of such a declaration as nullable so it can spell the + # result the specific already produces. + optional_return_positions = set(range(len(self.return_items(node.returns)))) return_type, returned_args = self.return_projection( node.returns, optional_return_positions=optional_return_positions, @@ -3591,6 +3661,7 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: release_gil=decorators.release_gil, error_status_policy=decorators.error_status_policy, deferred=decorators.abstract_method, + restates_projected_result=decorators.overload_target is not None, ) self.parser._reject_private_constructor(node.name, decorators.visibility) if node.name == "__init__" and decorators.bind_target is not None and decorators.overload_target is None: @@ -3634,7 +3705,6 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: decorators = self.parser.decorators(node.decorator_list, context="class body") if ( decorators.has_native_call - or decorators.bind_target is not None or decorators.overload_target is not None or decorators.is_static or decorators.release_gil @@ -3658,6 +3728,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: visibility=decorators.visibility, native_abi=decorators.native_abi, abstract=decorators.abstract, + native_name=decorators.bind_target, ) ) @@ -3697,12 +3768,26 @@ def _visit_AnnAssign(self, node: ast.AnnAssign) -> None: """Convert a module variable declaration.""" self.parser.module.variables.append(self.parser.ann_assign(node)) + def _visit_Assign(self, node: ast.Assign) -> None: + """Record the list of names this contract states that it publishes.""" + targets = [target for target in node.targets if isinstance(target, ast.Name)] + if len(targets) != 1 or targets[0].id != "__all__": + raise ValueError(f"Unsupported .pyi node: {_node_text(node)!r}") + if self.parser.module.exported_names is not None: + raise ValueError("A contract states __all__ once") + try: + names = ast.literal_eval(node.value) + except ValueError as exc: + raise ValueError(f"__all__ expects a list of name strings: {_node_text(node)!r}") from exc + if not isinstance(names, list | tuple) or not all(isinstance(name, str) for name in names): + raise ValueError(f"__all__ expects a list of name strings: {_node_text(node)!r}") + self.parser.module.exported_names = [str(name) for name in names] + def _visit_ClassDef(self, node: ast.ClassDef) -> None: """Convert a semantic class declaration.""" decorators = self.parser.decorators(node.decorator_list, context="class") if ( decorators.has_native_call - or decorators.bind_target is not None or decorators.overload_target is not None or decorators.is_static or decorators.release_gil @@ -3724,6 +3809,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: visibility=decorators.visibility, native_abi=decorators.native_abi, abstract=decorators.abstract, + native_name=decorators.bind_target, ) ) @@ -3752,7 +3838,12 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: has_native_call=decorators.has_native_call, release_gil=decorators.release_gil, error_status_policy=decorators.error_status_policy, + restates_projected_result=decorators.overload_target is not None, ) + if decorators.pure: + # The same fact a Fortran source records, which a specification + # function in a declaration expression is required to carry. + function.metadata["fortran_attributes"] = [*function.metadata.get("fortran_attributes", ()), "pure"] if decorators.overload_target is not None: self.parser._pending_overloads.append( _PendingOverload( @@ -3813,7 +3904,7 @@ def _imported_type_refs(module: SemanticModule) -> dict[str, tuple[str, str, str if isinstance(imp, SemanticImport): for item in imp.items: local_name = item.target or item.source - imported[local_name] = (imp.module, item.source, local_name) + imported[local_name] = (imp.module.lstrip("."), item.source, local_name) if imp.module.startswith("."): imported_namespaces[local_name] = _relative_imported_namespace(imp.module, item.source) continue @@ -3842,17 +3933,47 @@ def _relative_imported_namespace(module_name: str, source_name: str) -> str: return f"{module_path}.{source_name}" +def _record_declaring_module_for_prototype_type( + semantic_type: SemanticType, + declaring_module: str, + declared_types: frozenset[str], +) -> None: + """Name the declaring module for a derived type a referenced prototype owns.""" + if not declaring_module or semantic_type.name not in declared_types: + return + if EXTERNAL_TYPE_REF_METADATA in semantic_type.metadata: + return + semantic_type.metadata[EXTERNAL_TYPE_REF_METADATA] = { + "name": semantic_type.name, + "local_name": semantic_type.name, + "origin_module": declaring_module, + } + + def _bind_prototype_reference( semantic_type: SemanticType, prototype: SemanticPrototype, *, origin_module: str, - source_name: str, + declared_types: frozenset[str] = frozenset(), ) -> None: - """Complete one type annotation as a named callback prototype reference.""" + """Complete one type annotation as a named callback prototype reference. + + The declaring prototype names the symbol. A reference reached through + renaming re-exports carries the last alias it passed through, which names + nothing in the module that declares it, so the name is taken from the + declaration rather than from the caller. + """ local_name = semantic_type.name + source_name = prototype.name arguments = deepcopy(prototype.arguments) return_type = deepcopy(prototype.return_type) or SemanticType("None", dtype="None") + # The prototype's own types are written in the declaring module's scope, so + # a type local to that module keeps its origin when the reference is copied + # into a module that only imported the interface. + declaring_module = str(prototype.origin.native_scope or origin_module) + for value in (*(argument.semantic_type for argument in arguments), return_type): + _record_declaring_module_for_prototype_type(value, declaring_module, declared_types) semantic_type.dtype = "Prototype" semantic_type.metadata = { "arguments": [argument.semantic_type for argument in arguments], @@ -3883,6 +4004,79 @@ def _bind_prototype_reference( ) +def _external_module_candidates(module_name: str) -> tuple[str, ...]: + """Return the spellings one import may use to name the same contract module.""" + stripped = module_name.lstrip(".") + return tuple( + dict.fromkeys(candidate for candidate in (module_name, stripped, stripped.rsplit(".", 1)[-1]) if candidate) + ) + + +def _prototypes_with_reexports(modules: list[SemanticModule]) -> dict[tuple[str, str], SemanticPrototype]: + """Index every prototype name a contract module binds, declared or re-exported. + + A module that imports a prototype and publishes it binds that name without + declaring it, so a consumer importing it from there must still resolve to + the declaring module. Repeating to a fixed point follows a chain of any + length. + """ + resolved = {(module.name, prototype.name): prototype for module in modules for prototype in module.prototypes} + changed = True + while changed: + changed = False + for module in modules: + for imported in module.imports: + if not isinstance(imported, SemanticImport): + continue + for item in imported.items: + local_name = item.target or item.source + if (module.name, local_name) in resolved: + continue + prototype = next( + ( + found + for candidate in _external_module_candidates(imported.module) + if (found := resolved.get((candidate, item.source))) is not None + ), + None, + ) + if prototype is not None: + resolved[(module.name, local_name)] = prototype + changed = True + return resolved + + +def _bind_referenced_prototype( + semantic_type: SemanticType, + ref: dict[str, object], + prototypes: dict[tuple[str, str], SemanticPrototype], + declared_class_names: dict[str, frozenset[str]], +) -> bool: + """Complete one external reference as a prototype, reporting whether it matched.""" + origin_module = ref.get("origin_module") + source_name = ref.get("name") + if not isinstance(origin_module, str) or not isinstance(source_name, str): + return False + prototype = next( + ( + found + for candidate in _external_module_candidates(origin_module) + if (found := prototypes.get((candidate, source_name))) is not None + ), + None, + ) + if prototype is None: + return False + declaring_module = str(prototype.origin.native_scope or "") + _bind_prototype_reference( + semantic_type, + prototype, + origin_module=declaring_module or origin_module.lstrip("."), + declared_types=declared_class_names.get(declaring_module, frozenset()), + ) + return True + + def reconcile_external_type_refs(modules: list[SemanticModule]) -> list[SemanticModule]: """Resolve imported class and prototype references across converted modules. @@ -3893,37 +4087,18 @@ def reconcile_external_type_refs(modules: list[SemanticModule]) -> list[Semantic pipeline chaining; absent external definitions remain opaque references. """ definitions = {(module.name, declaration.name): declaration for module in modules for declaration in module.classes} - prototypes = {(module.name, prototype.name): prototype for module in modules for prototype in module.prototypes} + declared_class_names = { + module.name: frozenset(declaration.name for declaration in module.classes) for module in modules + } + prototypes = _prototypes_with_reexports(modules) functions = {(module.name, function.name): function for module in modules for function in module.functions} for module in modules: for semantic_type in _module_semantic_types(module): ref = semantic_type.metadata.get(EXTERNAL_TYPE_REF_METADATA) if not isinstance(ref, dict): continue - origin_module = ref.get("origin_module") - source_name = ref.get("name") - if isinstance(origin_module, str) and isinstance(source_name, str): - module_candidates = ( - origin_module, - origin_module.lstrip("."), - origin_module.lstrip(".").rsplit(".", 1)[-1], - ) - prototype = next( - ( - candidate_prototype - for candidate in module_candidates - if candidate and (candidate_prototype := prototypes.get((candidate, source_name))) is not None - ), - None, - ) - if prototype is not None: - _bind_prototype_reference( - semantic_type, - prototype, - origin_module=str(prototype.origin.native_scope or origin_module.lstrip(".")), - source_name=source_name, - ) - continue + if _bind_referenced_prototype(semantic_type, ref, prototypes, declared_class_names): + continue declaration = definitions.get((ref.get("origin_module"), ref.get("name"))) wrapped = declaration is not None and ( not isinstance(declaration, SemanticClass) or "Opaque" not in declaration.base_classes diff --git a/prik/utilities/declaration_expressions.py b/prik/utilities/declaration_expressions.py index f853262c3..8f6fbd64f 100644 --- a/prik/utilities/declaration_expressions.py +++ b/prik/utilities/declaration_expressions.py @@ -16,11 +16,16 @@ from __future__ import annotations import ast +import io import re -from collections.abc import Mapping +import tokenize +from keyword import iskeyword +from collections.abc import Callable, Mapping from dataclasses import dataclass __all__ = ( + "RUNTIME_DIMENSION_MARKERS", + "RUNTIME_EXTENT_MARKERS", "ArrayExpressionSource", "DeclarationExpressionCall", "ResolvedDeclarationExtent", @@ -33,6 +38,7 @@ "fortran_extent_to_python", "is_declaration_expression_helper", "is_public_declaration_expression", + "is_strided_extent", "render_declaration_extent", "resolve_declaration_extent", "split_declaration_assignment", @@ -41,7 +47,28 @@ ) -_RUNTIME_DIMENSIONS = frozenset({":", "::Strided", "...", "Flat"}) +# A runtime extent has a concrete rank but no compile-time bound, so a backend +# spells it from the descriptor it is handed rather than from the expression. +RUNTIME_EXTENT_MARKERS = frozenset({":", "::", "Flat"}) + + +def is_strided_extent(expression: str) -> bool: + """Return whether one extent expression describes a strided axis. + + A trailing empty step marks it, with or without bounds: ``::`` spans the + whole axis and ``lower:upper:`` narrows it. Without that step the axis is + contiguous, so ``:`` and ``lower:upper`` are dense. + """ + parts = str(expression).split(":") + return len(parts) == 3 and parts[2] == "" + + +_ASSUMED_RANK_MARKER = "..." +_QUOTED_LITERAL = re.compile(r"'(?:[^']|'')*'|\"(?:[^\"]|\"\")*\"") +_IDENTIFIER_PATTERN = r"\b[A-Za-z_]\w*\b" +_SELECTOR_KEYWORD = re.compile(r"\s*[A-Za-z_]\w*\s*=(?!=)") +# Every extent whose value only exists at run time, assumed rank included. +RUNTIME_DIMENSION_MARKERS = RUNTIME_EXTENT_MARKERS | {_ASSUMED_RANK_MARKER} _FORTRAN_RELATIONAL_OPERATORS = { ".eq.": "==", ".ne.": "!=", @@ -360,7 +387,7 @@ def resolve_declaration_extent( stored on completed policy and consumed by backend rendering. """ # Stage 1: preserve caller-owned runtime dimension markers. - if expression in _RUNTIME_DIMENSIONS: + if expression in RUNTIME_DIMENSION_MARKERS: return ResolvedDeclarationExtent(expression) # Stage 2: parse the public expression before binding any producer roles. @@ -395,7 +422,7 @@ def declaration_extent_references(expression: str) -> tuple[str, ...]: known. Array properties and unsupported syntax return ```` so the later policy stage cannot accidentally treat them as scalar values. """ - if expression in _RUNTIME_DIMENSIONS: + if expression in RUNTIME_DIMENSION_MARKERS: return () tree = _parse_expression(expression) if tree is None: @@ -412,6 +439,79 @@ def declaration_extent_references(expression: str) -> tuple[str, ...]: ) +def declaration_expression_identifiers(expression: str) -> tuple[str, ...]: + """Return the names one declaration expression references. + + Parsing decides what is a reference: an identifier spelled inside a + character literal is part of the literal's value and names nothing, so + ``"box"`` references no ``box``. Text this stage cannot parse -- a kind + selector such as ``len=3``, for instance -- falls back to scanning + identifiers with the literals removed, so a quoted spelling stays out + either way. + """ + names: list[str] = [] + for part in split_top_level_expression(expression, ","): + names.extend(_expression_identifiers(_selector_value(part))) + return tuple(dict.fromkeys(names)) + + +#: A character literal's optional kind, written before its opening quote. +_CHARACTER_KIND_PREFIX = re.compile(r"^(?:[A-Za-z]\w*|\d+)_(?=[\"'])") + + +def fortran_character_value(text: str) -> str | None: + """Return the value of one whole Fortran character literal, or ``None``. + + Fortran doubles a quote to hold one, so ``'don''t'`` is five characters. + Python reads that same spelling as two literals written side by side and + joins them, losing the quote, so a Fortran literal is decoded here rather + than handed to a Python reader. A literal may also state its kind before + the opening quote, as ``c_char_'abc'`` does; the kind is a declared type + fact rather than part of the value, so only the characters are returned. + Text that is not one whole literal returns ``None`` for the caller to read + as an expression. + """ + stripped = _CHARACTER_KIND_PREFIX.sub("", text.strip(), count=1) + if len(stripped) < 2 or stripped[0] != stripped[-1] or stripped[0] not in "\"'": + return None + quote = stripped[0] + body = stripped[1:-1] + index = 0 + value: list[str] = [] + while index < len(body): + character = body[index] + if character == quote: + # A lone quote ends the literal, so this is not one whole literal. + if index + 1 >= len(body) or body[index + 1] != quote: + return None + index += 2 + value.append(quote) + continue + value.append(character) + index += 1 + return "".join(value) + + +def _selector_value(part: str) -> str: + """Return the expression one declaration selector supplies. + + A selector writes its keyword before the value it carries, as ``len=n`` + and ``kind=c_char`` do. The keyword is syntax naming the slot rather than + an entity the declaration reads, so only what follows it is an expression. + """ + match = _SELECTOR_KEYWORD.match(part) + return part[match.end() :] if match is not None else part + + +def _expression_identifiers(expression: str) -> tuple[str, ...]: + """Return the names one expression reads, scanning only what will not parse.""" + text = _python_parseable_fortran_expression(expression) + tree = _parse_expression(text) + if tree is not None: + return tuple(dict.fromkeys(node.id for node in ast.walk(tree) if isinstance(node, ast.Name))) + return tuple(dict.fromkeys(re.findall(_IDENTIFIER_PATTERN, _QUOTED_LITERAL.sub(" ", expression)))) + + def declaration_expression_calls(expression: str) -> tuple[str, ...]: """Return named call targets used by one Python-form declaration expression. @@ -581,11 +681,79 @@ def _parse_expression(expression: str) -> ast.Expression | None: returns an ``eval``-mode tree. It returns ``None`` only for syntax that the public caller must preserve, block, or reframe with its own diagnostic; it never modifies the supplied text. + + A native name may be one Python reserves -- a Fortran function can be + called ``lambda`` -- and it is still a name. Such a name is set aside while + Python parses the rest and restored in the tree, so the call is read as a + call rather than the whole expression as invalid. """ try: return ast.parse(expression, mode="eval") except SyntaxError: + pass + escaped = _escape_reserved_names(expression) + if escaped is None: return None + try: + tree = ast.parse(escaped, mode="eval") + except SyntaxError: + return None + for node in ast.walk(tree): + if isinstance(node, ast.Name) and node.id.startswith(_RESERVED_NAME_ESCAPE): + node.id = node.id.removeprefix(_RESERVED_NAME_ESCAPE) + elif isinstance(node, ast.Attribute) and node.attr.startswith(_RESERVED_NAME_ESCAPE): + node.attr = node.attr.removeprefix(_RESERVED_NAME_ESCAPE) + return tree + + +#: Keywords the lexical translation writes itself; any other one is a native name. +_TRANSLATED_KEYWORDS = frozenset({"and", "or", "not", "True", "False"}) +_RESERVED_NAME_ESCAPE = "_prik_reserved_" + + +def _escape_reserved_names(expression: str) -> str | None: + """Return the text with each reserved native name escaped, or ``None``. + + Python's own tokenizer finds the names, so a literal or an operator is + never mistaken for one. ``None`` means no name needed escaping, or the text + does not tokenize. + """ + try: + tokens = list(tokenize.generate_tokens(io.StringIO(expression).readline)) + except (tokenize.TokenError, SyntaxError): + return None + reserved = [ + token.type == tokenize.NAME and iskeyword(token.string) and token.string not in _TRANSLATED_KEYWORDS + for token in tokens + ] + if not any(reserved): + return None + return tokenize.untokenize( + (token.type, _RESERVED_NAME_ESCAPE + token.string if escape else token.string) + for token, escape in zip(tokens, reserved, strict=True) + ) + + +def rename_declaration_expression_calls(expression: str, names: Mapping[str, str]) -> str: + """Return one expression with its call targets respelled, and nothing else. + + ``names`` maps a call target as the expression writes it to the spelling + that replaces it. Only a called name changes: an argument, a variable, an + attribute, or a literal spelled the same way is left alone. An expression + with nothing to respell, or one that does not parse, is returned unchanged. + """ + tree = _parse_expression(expression) + if tree is None: + return expression + changed = False + for node in ast.walk(tree): + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)): + continue + spelled = names.get(node.func.id, node.func.id) + if spelled != node.func.id: + node.func.id = spelled + changed = True + return ast.unparse(tree) if changed else expression def _python_parseable_fortran_expression(expression: str) -> str: @@ -595,8 +763,29 @@ def _python_parseable_fortran_expression(expression: str) -> str: translation. Unknown names and calls are intentionally retained for later provenance or policy diagnostics. """ - text = expression.strip() - text = _replace_fortran_array_constructors(text) + text = _replace_fortran_array_constructors(expression.strip()) + return outside_character_literals(text, _normalized_fortran_lexemes) + + +def outside_character_literals(text: str, transform: Callable[[str], str]) -> str: + """Apply one text transform to everything but the character literals. + + A literal's contents are its value, so lexical translation has to leave + them alone: ``len(".true.")`` measures six characters whatever ``.true.`` + means outside quotes. + """ + pieces: list[str] = [] + position = 0 + for literal in _QUOTED_LITERAL.finditer(text): + pieces.append(transform(text[position : literal.start()])) + pieces.append(literal.group(0)) + position = literal.end() + pieces.append(transform(text[position:])) + return "".join(pieces) + + +def _normalized_fortran_lexemes(text: str) -> str: + """Rewrite Fortran spellings that Python spells differently.""" text = re.sub(r"(?i)(?<=\d)_[A-Za-z]\w*\b", "", text) text = re.sub(r"(?i)(?<=\d)_[0-9]+\b", "", text) text = re.sub(r"(?i)\b(\d+(?:\.\d*)?)[dD]([+-]?\d+)\b", r"\1e\2", text) @@ -606,8 +795,7 @@ def _python_parseable_fortran_expression(expression: str) -> str: text = re.sub(re.escape(source), replacement, text, flags=re.IGNORECASE) for source, replacement in _FORTRAN_LOGICAL_OPERATORS.items(): text = re.sub(re.escape(source), replacement, text, flags=re.IGNORECASE) - text = text.replace("/=", "!=") - return text.replace("%", ".") + return text.replace("/=", "!=").replace("%", ".") def _qualified_call_name(node: ast.AST) -> str | None: @@ -1591,7 +1779,7 @@ def render_declaration_extent( """ if target not in {"c", "fortran"}: raise ValueError(f"unsupported declaration-expression target: {target!r}") - if expression in _RUNTIME_DIMENSIONS: + if expression in RUNTIME_DIMENSION_MARKERS: return expression try: node = ast.parse(expression, mode="eval").body diff --git a/tests/c/fixtures/pyi/general/basic_array_update.pyi b/tests/c/fixtures/pyi/general/basic_array_update.pyi index 2a6af576a..703712604 100644 --- a/tests/c/fixtures/pyi/general/basic_array_update.pyi +++ b/tests/c/fixtures/pyi/general/basic_array_update.pyi @@ -10,3 +10,5 @@ def add1_strided( x: Float64[...], incx: Int ) -> None: ... + +__all__ = ["add1", "add1_strided"] diff --git a/tests/c/fixtures/pyi/general/c_richer_features.pyi b/tests/c/fixtures/pyi/general/c_richer_features.pyi index 981880eaf..1967acf07 100644 --- a/tests/c/fixtures/pyi/general/c_richer_features.pyi +++ b/tests/c/fixtures/pyi/general/c_richer_features.pyi @@ -43,3 +43,17 @@ def prik_fill_matrix( cols: SizeT, matrix: Float64[rows, cols] ) -> None: ... + +__all__ = [ + "prik_flags", + "prik_context", + "prik_scalar", + "PRIK_STATUS_OK", + "PRIK_STATUS_RETRY", + "PRIK_STATUS_ERROR", + "prik_slow_path", + "prik_sort", + "prik_register_callback", + "prik_status_message", + "prik_fill_matrix", +] diff --git a/tests/c/fixtures/pyi/general/constants.pyi b/tests/c/fixtures/pyi/general/constants.pyi index a43e12456..5e3448dd3 100644 --- a/tests/c/fixtures/pyi/general/constants.pyi +++ b/tests/c/fixtures/pyi/general/constants.pyi @@ -19,3 +19,15 @@ def coordinate_axis_name( ) -> Addr(Int8): ... def coordinate_axis_count() -> SizeT: ... + +__all__ = [ + "COORD_X", + "COORD_Y", + "COORD_Z", + "PRIK_GENERAL_NMAX", + "PRIK_GENERAL_ORIGIN_RANK", + "nmax", + "origin", + "coordinate_axis_name", + "coordinate_axis_count", +] diff --git a/tests/c/fixtures/pyi/general/math_api.pyi b/tests/c/fixtures/pyi/general/math_api.pyi index eefc4bd71..2814f82a4 100644 --- a/tests/c/fixtures/pyi/general/math_api.pyi +++ b/tests/c/fixtures/pyi/general/math_api.pyi @@ -20,3 +20,5 @@ def dot( def fill_identity3( a: Float64[3, 3] ) -> None: ... + +__all__ = ["norm2", "scale", "dot", "fill_identity3"] diff --git a/tests/c/fixtures/pyi/general/mesh.pyi b/tests/c/fixtures/pyi/general/mesh.pyi index a4d080182..11cc0679d 100644 --- a/tests/c/fixtures/pyi/general/mesh.pyi +++ b/tests/c/fixtures/pyi/general/mesh.pyi @@ -26,3 +26,5 @@ def mesh_node_at( mesh: mesh, index: SizeT ) -> node: ... + +__all__ = ["node", "mesh", "node_move", "mesh_init", "mesh_clear", "mesh_node_at"] diff --git a/tests/c/fixtures/pyi/general/modern_math_physics.pyi b/tests/c/fixtures/pyi/general/modern_math_physics.pyi index 257a24a9a..e092badc4 100644 --- a/tests/c/fixtures/pyi/general/modern_math_physics.pyi +++ b/tests/c/fixtures/pyi/general/modern_math_physics.pyi @@ -46,3 +46,15 @@ def fill_identity3_modern( def normalize_particle( p: modern_particle ) -> None: ... + +__all__ = [ + "modern_particle", + "vector3", + "modern_counter", + "init_particle", + "kinetic_energy", + "scale_vector", + "dot3", + "fill_identity3_modern", + "normalize_particle", +] diff --git a/tests/c/fixtures/pyi/general/name_reuse.pyi b/tests/c/fixtures/pyi/general/name_reuse.pyi index 0188ececc..630b0760e 100644 --- a/tests/c/fixtures/pyi/general/name_reuse.pyi +++ b/tests/c/fixtures/pyi/general/name_reuse.pyi @@ -38,3 +38,18 @@ def convert_to_string( def convert_to_logical( same_name: Int8[...] ) -> Bool: ... + +__all__ = [ + "same_name", + "same_name_i", + "same_name_r", + "same_name_l", + "same_name_c", + "same_name_s", + "do_work_i", + "do_work_r", + "do_work_l", + "convert_to_complex", + "convert_to_string", + "convert_to_logical", +] diff --git a/tests/c/fixtures/pyi/general/particles.pyi b/tests/c/fixtures/pyi/general/particles.pyi index 04db96547..cd45fd408 100644 --- a/tests/c/fixtures/pyi/general/particles.pyi +++ b/tests/c/fixtures/pyi/general/particles.pyi @@ -20,3 +20,5 @@ def particle_move( ) -> None: ... def particle_current() -> particle: ... + +__all__ = ["particle", "particle_touch", "particle_reset", "particle_move", "particle_current"] diff --git a/tests/c/fixtures/pyi/general/shape_exprs.pyi b/tests/c/fixtures/pyi/general/shape_exprs.pyi index 6b17c607b..3528a042e 100644 --- a/tests/c/fixtures/pyi/general/shape_exprs.pyi +++ b/tests/c/fixtures/pyi/general/shape_exprs.pyi @@ -35,3 +35,15 @@ def all_exprs( x8: Int[(8 + 3) * (2 + 1) - 1], x9: Int[(8 - 3) * (8 - 2)] ) -> None: ... + +__all__ = [ + "PRIK_EXPR_N0", + "PRIK_EXPR_N1", + "PRIK_EXPR_A", + "PRIK_EXPR_B", + "PRIK_EXPR_C", + "fill_grid", + "update_plane", + "use_expr", + "all_exprs", +] diff --git a/tests/c/functions/end_to_end/test_export_symbol_workflow.py b/tests/c/functions/end_to_end/test_export_symbol_workflow.py index 809b70f62..b8fb6931f 100644 --- a/tests/c/functions/end_to_end/test_export_symbol_workflow.py +++ b/tests/c/functions/end_to_end/test_export_symbol_workflow.py @@ -1,5 +1,6 @@ """Compiled and CLI evidence for selecting functions from a private C include.""" +import ast import shutil import subprocess import sys @@ -33,12 +34,17 @@ def _write_private_include_project(tmp_path: Path) -> tuple[Path, Path, Path]: return header, probe, implementation -def test_generate_pyi_selects_one_function_from_a_private_include(tmp_path: Path): - _header, probe, _implementation = _write_private_include_project(tmp_path) - exports = tmp_path / "exports.txt" - exports.write_text("# reviewed public surface\nincrement\n", encoding="utf-8") - contract = tmp_path / "api.pyi" +def _stated_exports(contract: Path) -> list[str]: + """Return the ``__all__`` a generated contract states about itself.""" + module = ast.parse(contract.read_text(encoding="utf-8"), filename=str(contract)) + for statement in module.body: + targets = getattr(statement, "targets", []) + if any(isinstance(target, ast.Name) and target.id == "__all__" for target in targets): + return [ast.literal_eval(element) for element in statement.value.elts] + raise AssertionError(f"{contract} states no __all__") + +def _generate_contract(probe: Path, exports: Path, contract: Path) -> None: subprocess.run( [ sys.executable, @@ -64,10 +70,53 @@ def test_generate_pyi_selects_one_function_from_a_private_include(tmp_path: Path check=True, ) + +def test_generate_pyi_selects_one_function_from_a_private_include(tmp_path: Path): + _header, probe, _implementation = _write_private_include_project(tmp_path) + exports = tmp_path / "exports.txt" + exports.write_text("# reviewed public surface\nincrement\n", encoding="utf-8") + contract = tmp_path / "api.pyi" + + _generate_contract(probe, exports, contract) + text = contract.read_text(encoding="utf-8") assert "def increment(" in text assert "omitted" not in text assert "private_state" not in text + assert _stated_exports(contract) == ["increment"] + + +def test_generated_all_states_the_selected_declarations_not_the_allowlist_lines(tmp_path: Path): + """The allowlist selects declarations; __all__ states the Python names they publish.""" + header = tmp_path / "reviewed_api.h" + header.write_text("int zulu(int __v);\nint alpha(int __v);\nint omitted(int __v);\n", encoding="utf-8") + probe = tmp_path / "probe.c" + probe.write_text('#include "reviewed_api.h"\n', encoding="utf-8") + exports = tmp_path / "exports.txt" + exports.write_text("# reviewed public surface\n\nalpha\n\nzulu\n", encoding="utf-8") + contract = tmp_path / "api.pyi" + + _generate_contract(probe, exports, contract) + + # Declaration order, not allowlist order: the list follows the declarations + # the selection kept, so comments, blank lines, and the file's own ordering + # never reach it. + assert _stated_exports(contract) == ["zulu", "alpha"] + assert "omitted" not in contract.read_text(encoding="utf-8") + + +def test_a_repeated_allowlist_name_is_rejected_rather_than_stated_twice(tmp_path: Path): + """A stated surface names each declaration once, so a repeat is a request error.""" + _header, probe, _implementation = _write_private_include_project(tmp_path) + exports = tmp_path / "exports.txt" + exports.write_text("increment\nincrement\n", encoding="utf-8") + contract = tmp_path / "api.pyi" + + with pytest.raises(subprocess.CalledProcessError) as exc_info: + _generate_contract(probe, exports, contract) + + assert "Repeated C function name in --export-symbols" in exc_info.value.stderr + assert not contract.exists() def test_source_build_reuses_selection_with_positional_and_collision_policies(tmp_path: Path): diff --git a/tests/c/functions/end_to_end/test_public_name_contract.py b/tests/c/functions/end_to_end/test_public_name_contract.py new file mode 100644 index 000000000..3ce4dccd9 --- /dev/null +++ b/tests/c/functions/end_to_end/test_public_name_contract.py @@ -0,0 +1,119 @@ +"""The contract a C build writes names exactly what that build publishes.""" + +import ast +import shutil +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_c_extension +from prik.pipeline.build import BUILD_CONTRACT_DIRECTORY_NAME +from prik.preprocessing import PreprocessingConfig +from tests.c._support.runtime import sole_native_module + + +pytestmark = pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") + + +def _build(tmp_path: Path, name: str, header_text: str, implementation_text: str, symbols: list[str]): + """Build one C extension from a private header and return it with its stub.""" + header = tmp_path / "api.h" + header.write_text(header_text, encoding="utf-8") + probe = tmp_path / "probe.c" + probe.write_text('#include "api.h"\n', encoding="utf-8") + implementation = tmp_path / "implementation.c" + implementation.write_text(f'#include "api.h"\n{implementation_text}', encoding="utf-8") + output_dir = tmp_path / "build" + + result = build_c_extension( + probe, + output_dir=output_dir, + output_name=name, + input_c_compiler=shutil.which("cc") or "cc", + preprocessing=PreprocessingConfig( + mode="compiler", + compiler=shutil.which("cc") or "cc", + include_exposure="roots-only", + ), + export_symbols=symbols, + native_c_sources=[implementation], + ) + contract = output_dir / BUILD_CONTRACT_DIRECTORY_NAME / "probe.pyi" + return sole_native_module(result.import_module()), contract + + +def _stated_exports(contract: Path) -> list[str]: + """Return the ``__all__`` a generated contract states about itself.""" + module = ast.parse(contract.read_text(encoding="utf-8"), filename=str(contract)) + for statement in module.body: + targets = getattr(statement, "targets", []) + if any(isinstance(target, ast.Name) and target.id == "__all__" for target in targets): + return [ast.literal_eval(element) for element in statement.value.elts] + raise AssertionError(f"{contract} states no __all__") + + +def test_a_contract_names_what_its_own_build_published(tmp_path: Path): + """One naming decision reaches both, so the stub is readable and accurate.""" + module, contract = _build( + tmp_path, + "keyword_api", + "int lambda(int value);\nint lambda_(int value);\nint ordinary(int value);\n", + "int lambda(int v) { return v + 1; }\n" + "int lambda_(int v) { return v + 2; }\n" + "int ordinary(int v) { return v + 3; }\n", + ["lambda", "lambda_", "ordinary"], + ) + + published = {name for name in dir(module) if not name.startswith("_")} + assert published == set(_stated_exports(contract)) + # A name Python cannot bind is moved aside once, for the module and the + # contract alike, and the C spelling is recorded rather than lost. + assert published == {"lambda_", "lambda__2", "ordinary"} + text = contract.read_text(encoding="utf-8") + assert '@bind("lambda")' in text + assert '@bind("lambda_")' in text + + +def test_a_generated_contract_is_readable_python(tmp_path: Path): + """A contract exists to be re-read and edited, so it has to parse.""" + _module, contract = _build( + tmp_path, + "readable_api", + "int lambda(int value);\n", + "int lambda(int v) { return v + 1; }\n", + ["lambda"], + ) + + ast.parse(contract.read_text(encoding="utf-8"), filename=str(contract)) + + +def test_c_declarations_that_differ_only_in_case_stay_apart(tmp_path: Path): + """C spells its declarations exactly, so two spellings are two functions.""" + module, contract = _build( + tmp_path, + "case_api", + "int Foo(int value);\nint foo(int value);\n", + "int Foo(int v) { return v + 1; }\nint foo(int v) { return v + 2; }\n", + ["Foo", "foo"], + ) + + assert set(_stated_exports(contract)) == {"Foo", "foo"} + # Each Python name reaches the C function that spells itself that way. + assert module.Foo(np.int32(10)) == np.int32(11) + assert module.foo(np.int32(10)) == np.int32(12) + + +def test_a_mixed_case_c_name_keeps_its_spelling(tmp_path: Path): + """Folding case would rename a declaration C never asked to rename.""" + module, contract = _build( + tmp_path, + "mixed_case_api", + "int BarBaz(int value);\n", + "int BarBaz(int v) { return v + 1; }\n", + ["BarBaz"], + ) + + assert _stated_exports(contract) == ["BarBaz"] + assert module.BarBaz(np.int32(2)) == np.int32(3) + assert "barbaz" not in contract.read_text(encoding="utf-8") diff --git a/tests/c/infrastructure/cli/pipeline/test_c_cli_argument_contract.py b/tests/c/infrastructure/cli/pipeline/test_c_cli_argument_contract.py index 228f7d631..d0c26abec 100644 --- a/tests/c/infrastructure/cli/pipeline/test_c_cli_argument_contract.py +++ b/tests/c/infrastructure/cli/pipeline/test_c_cli_argument_contract.py @@ -115,3 +115,40 @@ def error(self, message): assert str(requested_error.value) == ( f"C input {c_header} is incompatible with --language fortran; pass --language c. Use --help for examples." ) + + +def test_pyi_wrapper_build_rejects_export_symbols(tmp_path: Path, capsys): + """A contract already states its public surface, so the C allowlist has nothing to select.""" + contract = tmp_path / "api.pyi" + contract.write_text("from prik.contracts import Int\n", encoding="utf-8") + implementation = tmp_path / "api.c" + implementation.write_text("int increment(int value) { return value + 1; }\n", encoding="utf-8") + exports = tmp_path / "exports.txt" + exports.write_text("increment\n", encoding="utf-8") + + with pytest.raises(SystemExit) as exc_info: + prik_cli.main( + [ + str(contract), + "--native-c-sources", + str(implementation), + "--export-symbols", + str(exports), + ] + ) + + assert exc_info.value.code == 2 + message = capsys.readouterr().err + assert "--export-symbols selects the public surface" in message + assert "__all__" in message + + +def test_export_symbols_help_names_the_public_surface_it_selects(): + """The option and a contract's __all__ state the same thing, so the help says so.""" + build_help = prik_cli._build_parser(["input.h", "--language", "c", "--help"]).format_help() + generate_help = prik_cli._generate_parser(["--help"]).format_help() + + for help_text in (build_help, generate_help): + assert "--export-symbols" in help_text + assert "public surface" in help_text + assert "__all__" in help_text diff --git a/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py b/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py index 7db7f2f2a..0c00b72d8 100644 --- a/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py +++ b/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py @@ -319,7 +319,7 @@ def test_cli_c_pyi_out_writes_explicit_multi_header_owner_stubs(tmp_path: Path): assert result.stdout == "" assert "class state(CStruct):" in (tmp_path / "types.pyi").read_text(encoding="utf-8") api_stub = (tmp_path / "api.pyi").read_text(encoding="utf-8") - assert "from types import state" in api_stub + assert "from .types import state" in api_stub assert "class state" not in api_stub assert "state: state" in api_stub assert "Addr(state)" not in api_stub diff --git a/tests/c/infrastructure/jupyter/end_to_end/test_c_magic_runtime.py b/tests/c/infrastructure/jupyter/end_to_end/test_c_magic_runtime.py index 29c50a2f8..933f82fa2 100644 --- a/tests/c/infrastructure/jupyter/end_to_end/test_c_magic_runtime.py +++ b/tests/c/infrastructure/jupyter/end_to_end/test_c_magic_runtime.py @@ -76,6 +76,8 @@ def counting_build(*args, **kwargs): "from prik.contracts import Float64, bind", ) contract = contract.replace("def square(", '@bind("square")\ndef squared(') + # Renaming a declaration renames what the contract publishes. + contract = contract.replace('__all__ = ["square"]', '__all__ = ["squared"]') line = magic_line.removeprefix("%%pyi").strip() magic.pyi(line, contract) diff --git a/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py b/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py index 6c95afa23..044f76178 100644 --- a/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py +++ b/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py @@ -62,7 +62,7 @@ def test_c2ir_explicit_project_headers_import_types_from_their_owner_module(): "representation": "wrapped", } assert "external_type_ref" not in local_state.metadata - assert "from types import state" in stubs["api"] + assert "from .types import state" in stubs["api"] assert "class state" not in stubs["api"] diff --git a/tests/c/primitive_scalars/end_to_end/test_direct_c_runtime.py b/tests/c/primitive_scalars/end_to_end/test_direct_c_runtime.py index c95929ba0..28f31a0b3 100644 --- a/tests/c/primitive_scalars/end_to_end/test_direct_c_runtime.py +++ b/tests/c/primitive_scalars/end_to_end/test_direct_c_runtime.py @@ -34,7 +34,7 @@ def test_c_source_build_calls_renamed_user_symbol_without_a_fortran_adapter(tmp_ @pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") def test_c_native_language_is_explicit_for_a_source_free_pyi_contract(tmp_path: Path): - contract = tmp_path / "contract.pyi" + contract = tmp_path / "direct_c_contract.pyi" contract.write_text( """from prik.contracts import Float64, Int, bind diff --git a/tests/c/records/semantics/test_c_record_semantics.py b/tests/c/records/semantics/test_c_record_semantics.py index c10e26a03..cf9bd30a1 100644 --- a/tests/c/records/semantics/test_c_record_semantics.py +++ b/tests/c/records/semantics/test_c_record_semantics.py @@ -113,10 +113,11 @@ def test_c2ir_private_include_types_remain_available_as_opaque_handles(): "wrapped": False, "representation": "opaque", } - assert "from private import private_context" in stubs["api"] + assert "from .private import private_context" in stubs["api"] assert ( stubs["private"] == "from prik.contracts import CStruct, Opaque\n\nclass private_context(CStruct, Opaque):\n pass" + '\n\n__all__ = ["private_context"]' ) diff --git a/tests/fortran/_support/installed_distribution.py b/tests/fortran/_support/installed_distribution.py index 4a803a67e..291d4942b 100644 --- a/tests/fortran/_support/installed_distribution.py +++ b/tests/fortran/_support/installed_distribution.py @@ -6,7 +6,9 @@ wheel once per session keeps that evidence affordable. """ +import importlib import os +import site import subprocess import sys import venv @@ -86,9 +88,37 @@ def installed_prik_python() -> Path: ) if install.returncode != 0: pytest.fail(f"installing the built wheel failed:\n{install.stderr.strip() or install.stdout.strip()}") + _share_runtime_dependencies(environment_dir) return installed_python +def _share_runtime_dependencies(environment_dir: Path) -> None: + """Make the wheel's runtime dependencies importable in the new environment. + + The wheel is installed without its dependencies, so the environment reads + them from the interpreter that built it. ``system_site_packages`` shares + only the interpreter's system directories, and the commands under test run + isolated, which drops the per-user directory a development install commonly + writes to. The directories holding those dependencies are named here so the + environment resolves them wherever this interpreter found them. + """ + required = ("immutabledict", "numpy", "filelock") + roots = { + str(Path(module.__file__).resolve().parent.parent) + for module in (importlib.import_module(name) for name in required) + if module.__file__ + } + site_packages = tuple(Path(environment_dir).glob("lib/python*/site-packages")) + if not site_packages: + return + shared = [root for root in sorted(roots) if root not in _DEFAULT_SITE_DIRECTORIES] + if shared: + (site_packages[0] / "_prik_runtime_dependencies.pth").write_text("\n".join(shared) + "\n", encoding="utf-8") + + +_DEFAULT_SITE_DIRECTORIES = frozenset(site.getsitepackages()) + + def installed_run(*command: str) -> str: """Return what one command prints from inside the installed environment.""" result = subprocess.run(command, env=clean_environment(), capture_output=True, text=True) diff --git a/tests/fortran/_support/printer_models.py b/tests/fortran/_support/printer_models.py index 01becd058..b0e699d0b 100644 --- a/tests/fortran/_support/printer_models.py +++ b/tests/fortran/_support/printer_models.py @@ -20,6 +20,8 @@ ) from prik.policy.completion import complete_semantic_policies +from prik.policy.contract_imports import complete_contract_imports +from prik.policy.exports import complete_python_export_policy from tests.fortran._support.paths import FORTRAN_ROOT OPERATOR_F90_SOURCE = FORTRAN_ROOT / "generic_interfaces" / "end_to_end" / "fixtures" / "native" / "foperators_f90.f90" @@ -37,6 +39,8 @@ def generate_pyi(source: str) -> str: fmod = parse_fortran_source(source) smod = fortran_module_to_semantic_module(fmod) + complete_python_export_policy(smod) + complete_contract_imports([smod]) return emit_module(smod) diff --git a/tests/fortran/_support/wrapper_build.py b/tests/fortran/_support/wrapper_build.py index 3fe6c0834..560a0359c 100644 --- a/tests/fortran/_support/wrapper_build.py +++ b/tests/fortran/_support/wrapper_build.py @@ -210,7 +210,8 @@ def _compile_native_object(source: Path, native_dir: Path) -> Path: return native_object -def _generate_checked_pyi_contract(source: Path, package_dir: Path, expected_package: Path) -> Path: +def _generate_checked_pyi_contract(source: Path, package_dir: Path, expected_package: Path | None) -> Path: + """Generate one contract package, comparing it to a fixture when given.""" _run_captured_command( [ sys.executable, @@ -225,7 +226,8 @@ def _generate_checked_pyi_contract(source: Path, package_dir: Path, expected_pac _compiler(), ], ) - assert_generated_pyi_package_matches_fixture(package_dir, expected_package) + if expected_package is not None: + assert_generated_pyi_package_matches_fixture(package_dir, expected_package) return package_dir / "__init__.pyi" @@ -262,7 +264,12 @@ def _build_inline_pyi_contract_module( return module, result -def _build_generated_pyi_and_import(source_template: Path, workdir: Path, expected_contract_package: Path): +def _build_generated_pyi_and_import( + source_template: Path, + workdir: Path, + expected_contract_package: Path | None = None, +): + """Generate a contract from source, then build and import through that contract.""" source_dir = workdir / "source" source_dir.mkdir(parents=True) source = source_dir / source_template.name @@ -568,8 +575,8 @@ def _assert_modern_string_examples(module): def _assert_modern_class_examples(module): - assert hasattr(module, "vector") - value = module.vector() + assert hasattr(module, "Vector") + value = module.Vector() value.x = np.float64(3.0) value.y = np.float64(4.0) @@ -585,8 +592,8 @@ def _assert_modern_class_examples(module): assert value.x == np.float64(3.75) assert value.y == np.float64(3.0) - assert hasattr(module, "vector_store") - store = module.vector_store() + assert hasattr(module, "Vector_Store") + store = module.Vector_Store() values = store.values matrix_values = store.matrix assert isinstance(values, AllocatableArray) @@ -642,7 +649,7 @@ def _assert_modern_class_examples(module): with pytest.raises(TypeError, match=r"expected ordering \(F\)"): store.set_matrix(np.array(replacement, order="C")) - made = module.vector_store.make(np.int64(4), np.float64(1.5)) + made = module.Vector_Store.make(np.int64(4), np.float64(1.5)) made_values = made.values assert isinstance(made_values, AllocatableArray) assert made_values.owner is made diff --git a/tests/fortran/allocatables/end_to_end/fixtures/contracts/fallocatable_views_f90/__init__.pyi b/tests/fortran/allocatables/end_to_end/fixtures/contracts/fallocatable_views_f90/__init__.pyi index c816be03c..14c86c14d 100644 --- a/tests/fortran/allocatables/end_to_end/fixtures/contracts/fallocatable_views_f90/__init__.pyi +++ b/tests/fortran/allocatables/end_to_end/fixtures/contracts/fallocatable_views_f90/__init__.pyi @@ -1 +1,3 @@ from . import fallocatable_views_f90 + +__all__ = ["fallocatable_views_f90"] diff --git a/tests/fortran/allocatables/end_to_end/fixtures/contracts/fallocatable_views_f90/fallocatable_views_f90.pyi b/tests/fortran/allocatables/end_to_end/fixtures/contracts/fallocatable_views_f90/fallocatable_views_f90.pyi index b9dcdbcb8..8609d97d9 100644 --- a/tests/fortran/allocatables/end_to_end/fixtures/contracts/fallocatable_views_f90/fallocatable_views_f90.pyi +++ b/tests/fortran/allocatables/end_to_end/fixtures/contracts/fallocatable_views_f90/fallocatable_views_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Aliased, Allocatable, Annotated, Arg, Float64, Int32, Pass, Return, Returns, native_call -class buffer: +class Buffer: def __init__(self) -> None: ... values: Allocatable[Float64[:]] @@ -82,3 +82,21 @@ def make_matrix( n: Int32, m: Int32 ) -> Allocatable[Float64[:, :]]: ... + +__all__ = [ + "Buffer", + "module_values", + "allocate_module_values", + "deallocate_module_values", + "scale_module_values", + "module_values_sum", + "build_values", + "build_matrix", + "make_values", + "replace_values", + "zero_alloc_vector", + "maybe_alloc_vector", + "zero_alloc_matrix", + "maybe_alloc_matrix", + "make_matrix", +] diff --git a/tests/fortran/allocatables/end_to_end/fixtures/contracts/fscalar_allocatables_f90/__init__.pyi b/tests/fortran/allocatables/end_to_end/fixtures/contracts/fscalar_allocatables_f90/__init__.pyi index 309b09a4c..89fc09f39 100644 --- a/tests/fortran/allocatables/end_to_end/fixtures/contracts/fscalar_allocatables_f90/__init__.pyi +++ b/tests/fortran/allocatables/end_to_end/fixtures/contracts/fscalar_allocatables_f90/__init__.pyi @@ -1 +1,3 @@ from . import fscalar_allocatables_f90 + +__all__ = ["fscalar_allocatables_f90"] diff --git a/tests/fortran/allocatables/end_to_end/fixtures/contracts/fscalar_allocatables_f90/fscalar_allocatables_f90.pyi b/tests/fortran/allocatables/end_to_end/fixtures/contracts/fscalar_allocatables_f90/fscalar_allocatables_f90.pyi index 71f50924e..e51453ff4 100644 --- a/tests/fortran/allocatables/end_to_end/fixtures/contracts/fscalar_allocatables_f90/fscalar_allocatables_f90.pyi +++ b/tests/fortran/allocatables/end_to_end/fixtures/contracts/fscalar_allocatables_f90/fscalar_allocatables_f90.pyi @@ -33,3 +33,15 @@ def create_allocatable() -> Float64 | None: ... def maybe_allocatable( flag: Int32 ) -> Float64 | None: ... + +__all__ = [ + "optional_scale", + "clear_module_value", + "set_module_value", + "bump_module_value", + "echo_allocatable", + "update_allocatable", + "clear_allocatable_value", + "create_allocatable", + "maybe_allocatable", +] diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/__init__.pyi b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/__init__.pyi index 39218ff83..acb046855 100644 --- a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import allocatables_direct_bind_c_f90 + +__all__ = ["allocatables_direct_bind_c_f90"] diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/allocatables_direct_bind_c_f90.pyi b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/allocatables_direct_bind_c_f90.pyi index aab1df615..022fda2e4 100644 --- a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/allocatables_direct_bind_c_f90.pyi +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/allocatables_direct_bind_c_f90.pyi @@ -14,3 +14,5 @@ def direct_allocate( def direct_pointer_sum( values: Annotated[Pointer[Float64[:]], PointerAssociation("runtime"), Ownership("caller"), Transfer("call_local"), Destruction("none")] ) -> Float64: ... + +__all__ = ["direct_optional_state", "direct_allocate", "direct_pointer_sum"] diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/__init__.pyi b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/__init__.pyi index c8952818f..a33f07340 100644 --- a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import allocatables_mixed_bind_c_f90 + +__all__ = ["allocatables_mixed_bind_c_f90"] diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/allocatables_mixed_bind_c_f90.pyi b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/allocatables_mixed_bind_c_f90.pyi index 115e5edcb..3b2104f7f 100644 --- a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/allocatables_mixed_bind_c_f90.pyi +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/allocatables_mixed_bind_c_f90.pyi @@ -8,3 +8,5 @@ def direct_allocate( def adapted_sum( values: Allocatable[Float64[:]] ) -> Float64: ... + +__all__ = ["direct_allocate", "adapted_sum"] diff --git a/tests/fortran/allocatables/end_to_end/test_allocatable_handles.py b/tests/fortran/allocatables/end_to_end/test_allocatable_handles.py index 025e9eb87..db8873ca6 100644 --- a/tests/fortran/allocatables/end_to_end/test_allocatable_handles.py +++ b/tests/fortran/allocatables/end_to_end/test_allocatable_handles.py @@ -165,15 +165,15 @@ def test_allocatable_module_fields_and_results_expose_lifetime_safe_handles( assert "Persistent allocatable descriptor handle." in module.__doc__ assert "Replacement assignment is not supported." in module.__doc__ assert "build_values" in module.__doc__ - assert "buffer" in module.__doc__ + assert "Buffer" in module.__doc__ assert "build_values(n) -> AllocatableArray[float64]" in module.build_values.__doc__ assert "values : AllocatableArray[float64]" in module.build_values.__doc__ assert "Descriptor ownership: owned" in module.build_values.__doc__ assert "Unallocated state remains inside the returned handle." in module.build_values.__doc__ assert not hasattr(module, "get_module_values") - assert "Fields" in module.buffer.__doc__ - assert "values : AllocatableArray[float64]" in module.buffer.__doc__ - assert "allocatable array descriptor handle" in module.buffer.values.__doc__ + assert "Fields" in module.Buffer.__doc__ + assert "values : AllocatableArray[float64]" in module.Buffer.__doc__ + assert "allocatable array descriptor handle" in module.Buffer.values.__doc__ module_values = module.module_values assert isinstance(module_values, AllocatableArray) @@ -268,7 +268,7 @@ def test_allocatable_module_fields_and_results_expose_lifetime_safe_handles( gc.collect() np.testing.assert_allclose(retained_result_view, np.array([3.0, 6.0, 9.0], dtype=np.float64)) - values = module.buffer() + values = module.Buffer() field_handle = values.values assert isinstance(field_handle, AllocatableArray) assert field_handle.owner is values diff --git a/tests/fortran/arrays/codegen/test_array_output_identity.py b/tests/fortran/arrays/codegen/test_array_output_identity.py index 46229a113..5d149e181 100644 --- a/tests/fortran/arrays/codegen/test_array_output_identity.py +++ b/tests/fortran/arrays/codegen/test_array_output_identity.py @@ -2,12 +2,10 @@ from __future__ import annotations -import pytest from tests.fortran._support.ownership_policy import parse_pyi_text from prik.policy.ownership import CodegenAction, ObjectKind, OwnershipOwner, TransferMode from prik.policy.completion import complete_semantic_policies -from prik.policy.models import ArrayWritebackABI from prik.pipeline.wrapper import WrapperGenerator from prik.planning import WrapperPlanner from prik.planning.models import WritebackPhase @@ -93,13 +91,7 @@ def test_mutable_bool_array_writeback_needs_no_normalization(): because the compiler profiles request the option that guarantees it, so the callee leaves nothing behind that has to be reduced afterwards. """ - plan = _logical_output_plan() - values, out = plan.namespaces[0].functions[0].arguments[1:] - - assert values.array_writeback_abi is ArrayWritebackABI.NATIVE_ARRAY - assert out.array_writeback_abi is ArrayWritebackABI.NATIVE_ARRAY - - artifacts = WrapperGenerator().generate(plan) + artifacts = WrapperGenerator().generate(_logical_output_plan()) bridge_source = next(source.text for source in artifacts.sources if source.path.suffix == ".f90") assert "call native_invert_flags(n, values, out)" in bridge_source @@ -118,12 +110,3 @@ def test_high_rank_bool_array_bridge_stays_inside_the_fortran_line_limit(): assert "dimension(:, :, :, :, :, :, :, :, :, :, :, :, :, :, :), contiguous :: values" in bridge_source assert max(map(len, bridge_source.splitlines())) <= 132 - - -def test_generator_rejects_a_normalized_mutable_bool_array_writeback_abi(): - """An edited plan cannot reintroduce a normalization pass that is not needed.""" - plan = _logical_output_plan() - plan.namespaces[0].functions[0].arguments[-1].array_writeback_abi = ArrayWritebackABI.LOGICAL_LOW_BIT_INT8 - - with pytest.raises(ValueError, match="invalid-array-writeback-abi"): - WrapperGenerator().generate(plan) diff --git a/tests/fortran/arrays/codegen/test_specification_extent_check_lowering.py b/tests/fortran/arrays/codegen/test_specification_extent_check_lowering.py new file mode 100644 index 000000000..ea68b654d --- /dev/null +++ b/tests/fortran/arrays/codegen/test_specification_extent_check_lowering.py @@ -0,0 +1,59 @@ +"""An argument sized by a specification function is checked before anything else runs.""" + +from __future__ import annotations + +from tests.fortran._support.ownership_policy import parse_pyi_text +from tests.fortran._support.printer_models import generate_wrapper, rendered_source + +CONTRACT = """ +from prik.contracts import Addr, Annotated, Arg, COPY_F, Float64, Int32, ORDER_C, native_call, pure + +@pure +@native_call([Addr(Arg(0))]) +def extent_for(n: Int32) -> Int32: ... + +@native_call([Addr(Arg(0)), Arg(1), Arg(2)]) +def scaled( + n: Int32, + values: Float64[extent_for(n)], + grid: Annotated[Float64[n, n], ORDER_C, COPY_F], +) -> Float64[extent_for(n)]: ... +""" + + +def _sources() -> tuple[str, str]: + artifacts = generate_wrapper(parse_pyi_text(CONTRACT, module_name="extents")) + return rendered_source(artifacts, ".f90"), rendered_source(artifacts, ".c") + + +def test_a_mismatched_actual_leaves_the_bridge_nothing_prepared_called_or_produced(): + """Only the declared extent is evaluated before the check; a mismatch yields a null result. + + Guarding the native call alone still converted the actuals, allocated the + result, and copied storage the call never wrote into it. + """ + bridge, _binding = _sources() + body = bridge[bridge.index("function bind_c_scaled(") : bridge.index("end function bind_c_scaled")] + statements = [ + line.strip() + for line in body.splitlines() + if line.strip() and "::" not in line and "&" not in line and "bind(c" not in line + ] + + assert statements[0].startswith("values_declared_extent_0 = int(") + assert statements[1] == "if (values_declared_extent_0 == values_extent_0) then" + # Everything that prepares, calls, or produces sits inside that branch. + assert statements[-3:] == ["else", "result = c_null_ptr", "end if"] + + +def test_the_binding_rejects_before_any_other_post_call_step(): + """The rejection is the first thing after the call, holding nothing the call produced. + + `grid` is copied through a Fortran-order temporary, whose copy-back is a + post-call step; a rejected call must not reach it. + """ + _bridge, binding = _sources() + after_call = binding[binding.index("= bind_c_scaled(") :].splitlines()[1:] + checks = [line.strip() for line in after_call if line.strip().startswith("if (")] + + assert checks[0] == "if (values_declared_extent_0 != bound_values_extent_0) {" diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/array_ops/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/array_ops/__init__.pyi index 667c6f4eb..a49e5486b 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/array_ops/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/array_ops/__init__.pyi @@ -1 +1,3 @@ from . import array_ops + +__all__ = ["array_ops"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/array_ops/array_ops.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/array_ops/array_ops.pyi index 65c988477..9c248722c 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/array_ops/array_ops.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/array_ops/array_ops.pyi @@ -58,3 +58,16 @@ def fill_optional( def automatic_vector( count: Int32 ) -> Float64[count]: ... + +__all__ = [ + "scale_matrix", + "shift", + "sum_columns", + "sum_flat", + "sum_flat_columns", + "scale_visible_rows", + "scale_without_intent", + "mutate_optional", + "fill_optional", + "automatic_vector", +] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_contracts_f90/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_contracts_f90/__init__.pyi index 31640ad5d..bbf6e7954 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_contracts_f90/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_contracts_f90/__init__.pyi @@ -1 +1,3 @@ from . import farray_contracts_f90 + +__all__ = ["farray_contracts_f90"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_contracts_f90/farray_contracts_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_contracts_f90/farray_contracts_f90.pyi index dcc866593..281a7d421 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_contracts_f90/farray_contracts_f90.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_contracts_f90/farray_contracts_f90.pyi @@ -98,3 +98,26 @@ def shift15( values: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::] ) -> None: ... + +__all__ = [ + "sum_assumed_size", + "scale_lower", + "sum_in", + "bump_inout", + "fill_out", + "shift1", + "shift2", + "shift3", + "shift4", + "shift5", + "shift6", + "shift7", + "shift8", + "shift9", + "shift10", + "shift11", + "shift12", + "shift13", + "shift14", + "shift15", +] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/__init__.pyi index 88b6b5650..19841eadc 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/__init__.pyi @@ -1 +1,3 @@ from . import farray_results_f90 + +__all__ = ["farray_results_f90"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/farray_results_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/farray_results_f90.pyi index dac619931..9763d894d 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/farray_results_f90.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/farray_results_f90.pyi @@ -73,3 +73,31 @@ def maybe_alloc_matrix( rows: Int32, cols: Int32 ) -> Allocatable[Float64[:, :]]: ... + +__all__ = [ + "fixed_vector", + "automatic_vector", + "size_intrinsic_vector", + "automatic_matrix", + "rank3_cube", + "rank1_result", + "rank2_result", + "rank3_result", + "rank4_result", + "rank5_result", + "rank6_result", + "rank7_result", + "rank8_result", + "rank9_result", + "rank10_result", + "rank11_result", + "rank12_result", + "rank13_result", + "rank14_result", + "rank15_result", + "zero_vector", + "zero_alloc_vector", + "maybe_alloc_vector", + "zero_alloc_matrix", + "maybe_alloc_matrix", +] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/fassumed_rank_f90/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/fassumed_rank_f90/__init__.pyi index 662a9634c..10c910198 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/fassumed_rank_f90/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/fassumed_rank_f90/__init__.pyi @@ -1 +1,3 @@ from . import fassumed_rank_f90 + +__all__ = ["fassumed_rank_f90"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/fassumed_rank_f90/fassumed_rank_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/fassumed_rank_f90/fassumed_rank_f90.pyi index cb309f10c..07b1c4045 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/fassumed_rank_f90/fassumed_rank_f90.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/fassumed_rank_f90/fassumed_rank_f90.pyi @@ -12,3 +12,5 @@ def rank_pair_score( left: Float64[...], right: Float64[...] ) -> Int32: ... + +__all__ = ["rank_weighted_sum", "bump_assumed_rank", "rank_pair_score"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays/__init__.pyi index 1746077b5..d476a731c 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays/__init__.pyi @@ -1,6 +1,5 @@ -from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call, standalone +from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, native_call, standalone -@bind("SQUARE_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r4( @@ -9,7 +8,6 @@ def square_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SQUARE_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r8( @@ -18,7 +16,6 @@ def square_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("SQUARE_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_i4( @@ -27,7 +24,6 @@ def square_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c4( @@ -36,7 +32,6 @@ def square_c4( R: Complex64[N] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c8( @@ -45,7 +40,6 @@ def square_c8( R: Complex128[N] ) -> Returns["N", Int32]: ... -@bind("CUBE_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r4( @@ -54,7 +48,6 @@ def cube_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("CUBE_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r8( @@ -63,7 +56,6 @@ def cube_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("CUBE_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_i4( @@ -72,7 +64,6 @@ def cube_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("ADD_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r4( @@ -82,7 +73,6 @@ def add_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ADD_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r8( @@ -92,7 +82,6 @@ def add_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ADD_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_i4( @@ -102,7 +91,6 @@ def add_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("ADD_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c4( @@ -112,7 +100,6 @@ def add_c4( R: Complex64[N] ) -> Returns["N", Int32]: ... -@bind("ADD_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c8( @@ -122,7 +109,6 @@ def add_c8( R: Complex128[N] ) -> Returns["N", Int32]: ... -@bind("SUB_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r4( @@ -132,7 +118,6 @@ def sub_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SUB_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r8( @@ -142,7 +127,6 @@ def sub_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("SUB_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_i4( @@ -152,7 +136,6 @@ def sub_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("MUL_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r4( @@ -162,7 +145,6 @@ def mul_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("MUL_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r8( @@ -172,7 +154,6 @@ def mul_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MUL_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_i4( @@ -182,7 +163,6 @@ def mul_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("DIV_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r4( @@ -192,7 +172,6 @@ def div_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DIV_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r8( @@ -202,7 +181,6 @@ def div_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("POW_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r4( @@ -212,7 +190,6 @@ def pow_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("POW_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r8( @@ -222,7 +199,6 @@ def pow_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ABS_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r4( @@ -231,7 +207,6 @@ def abs_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ABS_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r8( @@ -240,7 +215,6 @@ def abs_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ABS_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_i4( @@ -249,7 +223,6 @@ def abs_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("NEG_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r4( @@ -258,7 +231,6 @@ def neg_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("NEG_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r8( @@ -267,7 +239,6 @@ def neg_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("NEG_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_i4( @@ -276,7 +247,6 @@ def neg_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("SIN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r4( @@ -285,7 +255,6 @@ def sin_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SIN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r8( @@ -294,7 +263,6 @@ def sin_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("COS_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r4( @@ -303,7 +271,6 @@ def cos_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("COS_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r8( @@ -312,7 +279,6 @@ def cos_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("TAN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r4( @@ -321,7 +287,6 @@ def tan_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("TAN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r8( @@ -330,7 +295,6 @@ def tan_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ASIN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r4( @@ -339,7 +303,6 @@ def asin_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ASIN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r8( @@ -348,7 +311,6 @@ def asin_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ACOS_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r4( @@ -357,7 +319,6 @@ def acos_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ACOS_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r8( @@ -366,7 +327,6 @@ def acos_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ATAN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r4( @@ -375,7 +335,6 @@ def atan_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ATAN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r8( @@ -384,7 +343,6 @@ def atan_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r4( @@ -394,7 +352,6 @@ def atan2_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r8( @@ -404,7 +361,6 @@ def atan2_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("EXP_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r4( @@ -413,7 +369,6 @@ def exp_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("EXP_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r8( @@ -422,7 +377,6 @@ def exp_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("LOG_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r4( @@ -431,7 +385,6 @@ def log_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("LOG_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r8( @@ -440,7 +393,6 @@ def log_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("LOG10_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r4( @@ -449,7 +401,6 @@ def log10_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("LOG10_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r8( @@ -458,7 +409,6 @@ def log10_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("SQRT_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r4( @@ -467,7 +417,6 @@ def sqrt_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SQRT_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r8( @@ -476,7 +425,6 @@ def sqrt_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r4( @@ -486,7 +434,6 @@ def hypot_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r8( @@ -496,7 +443,6 @@ def hypot_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MIN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r4( @@ -506,7 +452,6 @@ def min_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("MIN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r8( @@ -516,7 +461,6 @@ def min_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MIN_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_i4( @@ -526,7 +470,6 @@ def min_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("MAX_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r4( @@ -536,7 +479,6 @@ def max_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("MAX_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r8( @@ -546,7 +488,6 @@ def max_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MAX_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_i4( @@ -556,7 +497,6 @@ def max_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("SIGN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r4( @@ -566,7 +506,6 @@ def sign_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SIGN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r8( @@ -576,7 +515,6 @@ def sign_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MOD_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_i4( @@ -586,7 +524,6 @@ def mod_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("MOD_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r4( @@ -596,7 +533,6 @@ def mod_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("MOD_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r8( @@ -606,7 +542,6 @@ def mod_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r4( @@ -615,7 +550,6 @@ def deg2rad_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r8( @@ -624,7 +558,6 @@ def deg2rad_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r4( @@ -633,7 +566,6 @@ def rad2deg_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r8( @@ -642,7 +574,6 @@ def rad2deg_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("DIST2_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r4( @@ -652,7 +583,6 @@ def dist2_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DIST2_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r8( @@ -662,7 +592,6 @@ def dist2_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("DOT2_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r4( @@ -674,7 +603,6 @@ def dot2_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DOT2_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r8( @@ -686,7 +614,6 @@ def dot2_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("DOT3_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r4( @@ -700,7 +627,6 @@ def dot3_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DOT3_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r8( @@ -714,7 +640,6 @@ def dot3_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("CONJ_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c4( @@ -723,7 +648,6 @@ def conj_c4( R: Complex64[N] ) -> Returns["N", Int32]: ... -@bind("CONJ_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c8( @@ -732,7 +656,6 @@ def conj_c8( R: Complex128[N] ) -> Returns["N", Int32]: ... -@bind("REAL_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c4( @@ -741,7 +664,6 @@ def real_c4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("REAL_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c8( @@ -750,7 +672,6 @@ def real_c8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c4( @@ -759,7 +680,6 @@ def aimag_c4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c8( @@ -768,7 +688,6 @@ def aimag_c8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ABS_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c4( @@ -777,7 +696,6 @@ def abs_c4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ABS_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c8( @@ -786,7 +704,6 @@ def abs_c8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r4( @@ -795,7 +712,6 @@ def is_positive_r4( R: Bool8[N] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r8( @@ -804,7 +720,6 @@ def is_positive_r8( R: Bool8[N] ) -> Returns["N", Int32]: ... -@bind("IS_EVEN_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_even_i4( @@ -812,3 +727,91 @@ def is_even_i4( X: Int32[N], R: Bool8[N] ) -> Returns["N", Int32]: ... + +__all__ = [ + "square_r4", + "square_r8", + "square_i4", + "square_c4", + "square_c8", + "cube_r4", + "cube_r8", + "cube_i4", + "add_r4", + "add_r8", + "add_i4", + "add_c4", + "add_c8", + "sub_r4", + "sub_r8", + "sub_i4", + "mul_r4", + "mul_r8", + "mul_i4", + "div_r4", + "div_r8", + "pow_r4", + "pow_r8", + "abs_r4", + "abs_r8", + "abs_i4", + "neg_r4", + "neg_r8", + "neg_i4", + "sin_r4", + "sin_r8", + "cos_r4", + "cos_r8", + "tan_r4", + "tan_r8", + "asin_r4", + "asin_r8", + "acos_r4", + "acos_r8", + "atan_r4", + "atan_r8", + "atan2_r4", + "atan2_r8", + "exp_r4", + "exp_r8", + "log_r4", + "log_r8", + "log10_r4", + "log10_r8", + "sqrt_r4", + "sqrt_r8", + "hypot_r4", + "hypot_r8", + "min_r4", + "min_r8", + "min_i4", + "max_r4", + "max_r8", + "max_i4", + "sign_r4", + "sign_r8", + "mod_i4", + "mod_r4", + "mod_r8", + "deg2rad_r4", + "deg2rad_r8", + "rad2deg_r4", + "rad2deg_r8", + "dist2_r4", + "dist2_r8", + "dot2_r4", + "dot2_r8", + "dot3_r4", + "dot3_r8", + "conj_c4", + "conj_c8", + "real_c4", + "real_c8", + "aimag_c4", + "aimag_c8", + "abs_c4", + "abs_c8", + "is_positive_r4", + "is_positive_r8", + "is_even_i4", +] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/__init__.pyi index 6e2cdf0c1..623777647 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/__init__.pyi @@ -1 +1,3 @@ from . import fmath_arrays_f90 + +__all__ = ["fmath_arrays_f90"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi index 2c5a7a922..34b185943 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi @@ -1,6 +1,5 @@ -from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call +from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, native_call -@bind("SQUARE_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r4_contiguous( N: Int32, @@ -8,7 +7,6 @@ def square_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r8_contiguous( N: Int32, @@ -16,7 +14,6 @@ def square_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_i4_contiguous( N: Int32, @@ -24,7 +21,6 @@ def square_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c4_contiguous( N: Int32, @@ -32,7 +28,6 @@ def square_c4_contiguous( R: Complex64[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c8_contiguous( N: Int32, @@ -40,7 +35,6 @@ def square_c8_contiguous( R: Complex128[:] ) -> Returns["N", Int32]: ... -@bind("CUBE_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r4_contiguous( N: Int32, @@ -48,7 +42,6 @@ def cube_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("CUBE_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r8_contiguous( N: Int32, @@ -56,7 +49,6 @@ def cube_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("CUBE_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_i4_contiguous( N: Int32, @@ -64,7 +56,6 @@ def cube_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("ADD_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r4_contiguous( N: Int32, @@ -73,7 +64,6 @@ def add_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ADD_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r8_contiguous( N: Int32, @@ -82,7 +72,6 @@ def add_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ADD_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_i4_contiguous( N: Int32, @@ -91,7 +80,6 @@ def add_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("ADD_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c4_contiguous( N: Int32, @@ -100,7 +88,6 @@ def add_c4_contiguous( R: Complex64[:] ) -> Returns["N", Int32]: ... -@bind("ADD_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c8_contiguous( N: Int32, @@ -109,7 +96,6 @@ def add_c8_contiguous( R: Complex128[:] ) -> Returns["N", Int32]: ... -@bind("SUB_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r4_contiguous( N: Int32, @@ -118,7 +104,6 @@ def sub_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SUB_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r8_contiguous( N: Int32, @@ -127,7 +112,6 @@ def sub_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("SUB_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_i4_contiguous( N: Int32, @@ -136,7 +120,6 @@ def sub_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("MUL_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r4_contiguous( N: Int32, @@ -145,7 +128,6 @@ def mul_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("MUL_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r8_contiguous( N: Int32, @@ -154,7 +136,6 @@ def mul_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MUL_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_i4_contiguous( N: Int32, @@ -163,7 +144,6 @@ def mul_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("DIV_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r4_contiguous( N: Int32, @@ -172,7 +152,6 @@ def div_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DIV_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r8_contiguous( N: Int32, @@ -181,7 +160,6 @@ def div_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("POW_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r4_contiguous( N: Int32, @@ -190,7 +168,6 @@ def pow_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("POW_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r8_contiguous( N: Int32, @@ -199,7 +176,6 @@ def pow_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ABS_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r4_contiguous( N: Int32, @@ -207,7 +183,6 @@ def abs_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ABS_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r8_contiguous( N: Int32, @@ -215,7 +190,6 @@ def abs_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ABS_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_i4_contiguous( N: Int32, @@ -223,7 +197,6 @@ def abs_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("NEG_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r4_contiguous( N: Int32, @@ -231,7 +204,6 @@ def neg_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("NEG_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r8_contiguous( N: Int32, @@ -239,7 +211,6 @@ def neg_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("NEG_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_i4_contiguous( N: Int32, @@ -247,7 +218,6 @@ def neg_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("SIN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r4_contiguous( N: Int32, @@ -255,7 +225,6 @@ def sin_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SIN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r8_contiguous( N: Int32, @@ -263,7 +232,6 @@ def sin_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("COS_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r4_contiguous( N: Int32, @@ -271,7 +239,6 @@ def cos_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("COS_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r8_contiguous( N: Int32, @@ -279,7 +246,6 @@ def cos_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("TAN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r4_contiguous( N: Int32, @@ -287,7 +253,6 @@ def tan_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("TAN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r8_contiguous( N: Int32, @@ -295,7 +260,6 @@ def tan_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ASIN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r4_contiguous( N: Int32, @@ -303,7 +267,6 @@ def asin_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ASIN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r8_contiguous( N: Int32, @@ -311,7 +274,6 @@ def asin_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ACOS_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r4_contiguous( N: Int32, @@ -319,7 +281,6 @@ def acos_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ACOS_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r8_contiguous( N: Int32, @@ -327,7 +288,6 @@ def acos_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ATAN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r4_contiguous( N: Int32, @@ -335,7 +295,6 @@ def atan_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ATAN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r8_contiguous( N: Int32, @@ -343,7 +302,6 @@ def atan_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r4_contiguous( N: Int32, @@ -352,7 +310,6 @@ def atan2_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r8_contiguous( N: Int32, @@ -361,7 +318,6 @@ def atan2_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("EXP_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r4_contiguous( N: Int32, @@ -369,7 +325,6 @@ def exp_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("EXP_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r8_contiguous( N: Int32, @@ -377,7 +332,6 @@ def exp_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("LOG_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r4_contiguous( N: Int32, @@ -385,7 +339,6 @@ def log_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("LOG_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r8_contiguous( N: Int32, @@ -393,7 +346,6 @@ def log_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("LOG10_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r4_contiguous( N: Int32, @@ -401,7 +353,6 @@ def log10_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("LOG10_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r8_contiguous( N: Int32, @@ -409,7 +360,6 @@ def log10_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("SQRT_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r4_contiguous( N: Int32, @@ -417,7 +367,6 @@ def sqrt_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SQRT_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r8_contiguous( N: Int32, @@ -425,7 +374,6 @@ def sqrt_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r4_contiguous( N: Int32, @@ -434,7 +382,6 @@ def hypot_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r8_contiguous( N: Int32, @@ -443,7 +390,6 @@ def hypot_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MIN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r4_contiguous( N: Int32, @@ -452,7 +398,6 @@ def min_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("MIN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r8_contiguous( N: Int32, @@ -461,7 +406,6 @@ def min_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MIN_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_i4_contiguous( N: Int32, @@ -470,7 +414,6 @@ def min_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("MAX_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r4_contiguous( N: Int32, @@ -479,7 +422,6 @@ def max_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("MAX_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r8_contiguous( N: Int32, @@ -488,7 +430,6 @@ def max_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MAX_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_i4_contiguous( N: Int32, @@ -497,7 +438,6 @@ def max_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("SIGN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r4_contiguous( N: Int32, @@ -506,7 +446,6 @@ def sign_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SIGN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r8_contiguous( N: Int32, @@ -515,7 +454,6 @@ def sign_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MOD_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_i4_contiguous( N: Int32, @@ -524,7 +462,6 @@ def mod_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("MOD_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r4_contiguous( N: Int32, @@ -533,7 +470,6 @@ def mod_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("MOD_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r8_contiguous( N: Int32, @@ -542,7 +478,6 @@ def mod_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r4_contiguous( N: Int32, @@ -550,7 +485,6 @@ def deg2rad_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r8_contiguous( N: Int32, @@ -558,7 +492,6 @@ def deg2rad_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r4_contiguous( N: Int32, @@ -566,7 +499,6 @@ def rad2deg_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r8_contiguous( N: Int32, @@ -574,7 +506,6 @@ def rad2deg_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("DIST2_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r4_contiguous( N: Int32, @@ -583,7 +514,6 @@ def dist2_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DIST2_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r8_contiguous( N: Int32, @@ -592,7 +522,6 @@ def dist2_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("DOT2_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r4_contiguous( N: Int32, @@ -603,7 +532,6 @@ def dot2_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DOT2_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r8_contiguous( N: Int32, @@ -614,7 +542,6 @@ def dot2_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("DOT3_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r4_contiguous( N: Int32, @@ -627,7 +554,6 @@ def dot3_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DOT3_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r8_contiguous( N: Int32, @@ -640,7 +566,6 @@ def dot3_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("CONJ_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c4_contiguous( N: Int32, @@ -648,7 +573,6 @@ def conj_c4_contiguous( R: Complex64[:] ) -> Returns["N", Int32]: ... -@bind("CONJ_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c8_contiguous( N: Int32, @@ -656,7 +580,6 @@ def conj_c8_contiguous( R: Complex128[:] ) -> Returns["N", Int32]: ... -@bind("REAL_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c4_contiguous( N: Int32, @@ -664,7 +587,6 @@ def real_c4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("REAL_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c8_contiguous( N: Int32, @@ -672,7 +594,6 @@ def real_c8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c4_contiguous( N: Int32, @@ -680,7 +601,6 @@ def aimag_c4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c8_contiguous( N: Int32, @@ -688,7 +608,6 @@ def aimag_c8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ABS_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c4_contiguous( N: Int32, @@ -696,7 +615,6 @@ def abs_c4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ABS_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c8_contiguous( N: Int32, @@ -704,7 +622,6 @@ def abs_c8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r4_contiguous( N: Int32, @@ -712,7 +629,6 @@ def is_positive_r4_contiguous( R: Bool8[:] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r8_contiguous( N: Int32, @@ -720,7 +636,6 @@ def is_positive_r8_contiguous( R: Bool8[:] ) -> Returns["N", Int32]: ... -@bind("IS_EVEN_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_even_i4_contiguous( N: Int32, @@ -728,7 +643,6 @@ def is_even_i4_contiguous( R: Bool8[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r4_strided( N: Int32, @@ -736,7 +650,6 @@ def square_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SQUARE_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r8_strided( N: Int32, @@ -744,7 +657,6 @@ def square_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("SQUARE_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_i4_strided( N: Int32, @@ -752,7 +664,6 @@ def square_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c4_strided( N: Int32, @@ -760,7 +671,6 @@ def square_c4_strided( R: Complex64[::] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c8_strided( N: Int32, @@ -768,7 +678,6 @@ def square_c8_strided( R: Complex128[::] ) -> Returns["N", Int32]: ... -@bind("CUBE_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r4_strided( N: Int32, @@ -776,7 +685,6 @@ def cube_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("CUBE_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r8_strided( N: Int32, @@ -784,7 +692,6 @@ def cube_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("CUBE_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_i4_strided( N: Int32, @@ -792,7 +699,6 @@ def cube_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("ADD_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r4_strided( N: Int32, @@ -801,7 +707,6 @@ def add_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ADD_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r8_strided( N: Int32, @@ -810,7 +715,6 @@ def add_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ADD_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_i4_strided( N: Int32, @@ -819,7 +723,6 @@ def add_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("ADD_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c4_strided( N: Int32, @@ -828,7 +731,6 @@ def add_c4_strided( R: Complex64[::] ) -> Returns["N", Int32]: ... -@bind("ADD_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c8_strided( N: Int32, @@ -837,7 +739,6 @@ def add_c8_strided( R: Complex128[::] ) -> Returns["N", Int32]: ... -@bind("SUB_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r4_strided( N: Int32, @@ -846,7 +747,6 @@ def sub_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SUB_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r8_strided( N: Int32, @@ -855,7 +755,6 @@ def sub_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("SUB_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_i4_strided( N: Int32, @@ -864,7 +763,6 @@ def sub_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("MUL_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r4_strided( N: Int32, @@ -873,7 +771,6 @@ def mul_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("MUL_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r8_strided( N: Int32, @@ -882,7 +779,6 @@ def mul_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MUL_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_i4_strided( N: Int32, @@ -891,7 +787,6 @@ def mul_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("DIV_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r4_strided( N: Int32, @@ -900,7 +795,6 @@ def div_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DIV_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r8_strided( N: Int32, @@ -909,7 +803,6 @@ def div_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("POW_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r4_strided( N: Int32, @@ -918,7 +811,6 @@ def pow_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("POW_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r8_strided( N: Int32, @@ -927,7 +819,6 @@ def pow_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ABS_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r4_strided( N: Int32, @@ -935,7 +826,6 @@ def abs_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ABS_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r8_strided( N: Int32, @@ -943,7 +833,6 @@ def abs_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ABS_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_i4_strided( N: Int32, @@ -951,7 +840,6 @@ def abs_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("NEG_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r4_strided( N: Int32, @@ -959,7 +847,6 @@ def neg_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("NEG_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r8_strided( N: Int32, @@ -967,7 +854,6 @@ def neg_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("NEG_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_i4_strided( N: Int32, @@ -975,7 +861,6 @@ def neg_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("SIN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r4_strided( N: Int32, @@ -983,7 +868,6 @@ def sin_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SIN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r8_strided( N: Int32, @@ -991,7 +875,6 @@ def sin_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("COS_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r4_strided( N: Int32, @@ -999,7 +882,6 @@ def cos_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("COS_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r8_strided( N: Int32, @@ -1007,7 +889,6 @@ def cos_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("TAN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r4_strided( N: Int32, @@ -1015,7 +896,6 @@ def tan_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("TAN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r8_strided( N: Int32, @@ -1023,7 +903,6 @@ def tan_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ASIN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r4_strided( N: Int32, @@ -1031,7 +910,6 @@ def asin_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ASIN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r8_strided( N: Int32, @@ -1039,7 +917,6 @@ def asin_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ACOS_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r4_strided( N: Int32, @@ -1047,7 +924,6 @@ def acos_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ACOS_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r8_strided( N: Int32, @@ -1055,7 +931,6 @@ def acos_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ATAN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r4_strided( N: Int32, @@ -1063,7 +938,6 @@ def atan_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ATAN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r8_strided( N: Int32, @@ -1071,7 +945,6 @@ def atan_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r4_strided( N: Int32, @@ -1080,7 +953,6 @@ def atan2_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r8_strided( N: Int32, @@ -1089,7 +961,6 @@ def atan2_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("EXP_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r4_strided( N: Int32, @@ -1097,7 +968,6 @@ def exp_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("EXP_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r8_strided( N: Int32, @@ -1105,7 +975,6 @@ def exp_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("LOG_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r4_strided( N: Int32, @@ -1113,7 +982,6 @@ def log_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("LOG_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r8_strided( N: Int32, @@ -1121,7 +989,6 @@ def log_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("LOG10_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r4_strided( N: Int32, @@ -1129,7 +996,6 @@ def log10_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("LOG10_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r8_strided( N: Int32, @@ -1137,7 +1003,6 @@ def log10_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("SQRT_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r4_strided( N: Int32, @@ -1145,7 +1010,6 @@ def sqrt_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SQRT_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r8_strided( N: Int32, @@ -1153,7 +1017,6 @@ def sqrt_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r4_strided( N: Int32, @@ -1162,7 +1025,6 @@ def hypot_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r8_strided( N: Int32, @@ -1171,7 +1033,6 @@ def hypot_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MIN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r4_strided( N: Int32, @@ -1180,7 +1041,6 @@ def min_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("MIN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r8_strided( N: Int32, @@ -1189,7 +1049,6 @@ def min_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MIN_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_i4_strided( N: Int32, @@ -1198,7 +1057,6 @@ def min_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("MAX_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r4_strided( N: Int32, @@ -1207,7 +1065,6 @@ def max_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("MAX_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r8_strided( N: Int32, @@ -1216,7 +1073,6 @@ def max_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MAX_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_i4_strided( N: Int32, @@ -1225,7 +1081,6 @@ def max_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("SIGN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r4_strided( N: Int32, @@ -1234,7 +1089,6 @@ def sign_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SIGN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r8_strided( N: Int32, @@ -1243,7 +1097,6 @@ def sign_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MOD_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_i4_strided( N: Int32, @@ -1252,7 +1105,6 @@ def mod_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("MOD_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r4_strided( N: Int32, @@ -1261,7 +1113,6 @@ def mod_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("MOD_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r8_strided( N: Int32, @@ -1270,7 +1121,6 @@ def mod_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r4_strided( N: Int32, @@ -1278,7 +1128,6 @@ def deg2rad_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r8_strided( N: Int32, @@ -1286,7 +1135,6 @@ def deg2rad_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r4_strided( N: Int32, @@ -1294,7 +1142,6 @@ def rad2deg_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r8_strided( N: Int32, @@ -1302,7 +1149,6 @@ def rad2deg_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("DIST2_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r4_strided( N: Int32, @@ -1311,7 +1157,6 @@ def dist2_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DIST2_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r8_strided( N: Int32, @@ -1320,7 +1165,6 @@ def dist2_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("DOT2_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r4_strided( N: Int32, @@ -1331,7 +1175,6 @@ def dot2_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DOT2_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r8_strided( N: Int32, @@ -1342,7 +1185,6 @@ def dot2_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("DOT3_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r4_strided( N: Int32, @@ -1355,7 +1197,6 @@ def dot3_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DOT3_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r8_strided( N: Int32, @@ -1368,7 +1209,6 @@ def dot3_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("CONJ_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c4_strided( N: Int32, @@ -1376,7 +1216,6 @@ def conj_c4_strided( R: Complex64[::] ) -> Returns["N", Int32]: ... -@bind("CONJ_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c8_strided( N: Int32, @@ -1384,7 +1223,6 @@ def conj_c8_strided( R: Complex128[::] ) -> Returns["N", Int32]: ... -@bind("REAL_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c4_strided( N: Int32, @@ -1392,7 +1230,6 @@ def real_c4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("REAL_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c8_strided( N: Int32, @@ -1400,7 +1237,6 @@ def real_c8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c4_strided( N: Int32, @@ -1408,7 +1244,6 @@ def aimag_c4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c8_strided( N: Int32, @@ -1416,7 +1251,6 @@ def aimag_c8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ABS_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c4_strided( N: Int32, @@ -1424,7 +1258,6 @@ def abs_c4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ABS_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c8_strided( N: Int32, @@ -1432,7 +1265,6 @@ def abs_c8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r4_strided( N: Int32, @@ -1440,7 +1272,6 @@ def is_positive_r4_strided( R: Bool8[::] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r8_strided( N: Int32, @@ -1448,10 +1279,182 @@ def is_positive_r8_strided( R: Bool8[::] ) -> Returns["N", Int32]: ... -@bind("IS_EVEN_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_even_i4_strided( N: Int32, X: Int32[::], R: Bool8[::] ) -> Returns["N", Int32]: ... + +__all__ = [ + "square_r4_contiguous", + "square_r8_contiguous", + "square_i4_contiguous", + "square_c4_contiguous", + "square_c8_contiguous", + "cube_r4_contiguous", + "cube_r8_contiguous", + "cube_i4_contiguous", + "add_r4_contiguous", + "add_r8_contiguous", + "add_i4_contiguous", + "add_c4_contiguous", + "add_c8_contiguous", + "sub_r4_contiguous", + "sub_r8_contiguous", + "sub_i4_contiguous", + "mul_r4_contiguous", + "mul_r8_contiguous", + "mul_i4_contiguous", + "div_r4_contiguous", + "div_r8_contiguous", + "pow_r4_contiguous", + "pow_r8_contiguous", + "abs_r4_contiguous", + "abs_r8_contiguous", + "abs_i4_contiguous", + "neg_r4_contiguous", + "neg_r8_contiguous", + "neg_i4_contiguous", + "sin_r4_contiguous", + "sin_r8_contiguous", + "cos_r4_contiguous", + "cos_r8_contiguous", + "tan_r4_contiguous", + "tan_r8_contiguous", + "asin_r4_contiguous", + "asin_r8_contiguous", + "acos_r4_contiguous", + "acos_r8_contiguous", + "atan_r4_contiguous", + "atan_r8_contiguous", + "atan2_r4_contiguous", + "atan2_r8_contiguous", + "exp_r4_contiguous", + "exp_r8_contiguous", + "log_r4_contiguous", + "log_r8_contiguous", + "log10_r4_contiguous", + "log10_r8_contiguous", + "sqrt_r4_contiguous", + "sqrt_r8_contiguous", + "hypot_r4_contiguous", + "hypot_r8_contiguous", + "min_r4_contiguous", + "min_r8_contiguous", + "min_i4_contiguous", + "max_r4_contiguous", + "max_r8_contiguous", + "max_i4_contiguous", + "sign_r4_contiguous", + "sign_r8_contiguous", + "mod_i4_contiguous", + "mod_r4_contiguous", + "mod_r8_contiguous", + "deg2rad_r4_contiguous", + "deg2rad_r8_contiguous", + "rad2deg_r4_contiguous", + "rad2deg_r8_contiguous", + "dist2_r4_contiguous", + "dist2_r8_contiguous", + "dot2_r4_contiguous", + "dot2_r8_contiguous", + "dot3_r4_contiguous", + "dot3_r8_contiguous", + "conj_c4_contiguous", + "conj_c8_contiguous", + "real_c4_contiguous", + "real_c8_contiguous", + "aimag_c4_contiguous", + "aimag_c8_contiguous", + "abs_c4_contiguous", + "abs_c8_contiguous", + "is_positive_r4_contiguous", + "is_positive_r8_contiguous", + "is_even_i4_contiguous", + "square_r4_strided", + "square_r8_strided", + "square_i4_strided", + "square_c4_strided", + "square_c8_strided", + "cube_r4_strided", + "cube_r8_strided", + "cube_i4_strided", + "add_r4_strided", + "add_r8_strided", + "add_i4_strided", + "add_c4_strided", + "add_c8_strided", + "sub_r4_strided", + "sub_r8_strided", + "sub_i4_strided", + "mul_r4_strided", + "mul_r8_strided", + "mul_i4_strided", + "div_r4_strided", + "div_r8_strided", + "pow_r4_strided", + "pow_r8_strided", + "abs_r4_strided", + "abs_r8_strided", + "abs_i4_strided", + "neg_r4_strided", + "neg_r8_strided", + "neg_i4_strided", + "sin_r4_strided", + "sin_r8_strided", + "cos_r4_strided", + "cos_r8_strided", + "tan_r4_strided", + "tan_r8_strided", + "asin_r4_strided", + "asin_r8_strided", + "acos_r4_strided", + "acos_r8_strided", + "atan_r4_strided", + "atan_r8_strided", + "atan2_r4_strided", + "atan2_r8_strided", + "exp_r4_strided", + "exp_r8_strided", + "log_r4_strided", + "log_r8_strided", + "log10_r4_strided", + "log10_r8_strided", + "sqrt_r4_strided", + "sqrt_r8_strided", + "hypot_r4_strided", + "hypot_r8_strided", + "min_r4_strided", + "min_r8_strided", + "min_i4_strided", + "max_r4_strided", + "max_r8_strided", + "max_i4_strided", + "sign_r4_strided", + "sign_r8_strided", + "mod_i4_strided", + "mod_r4_strided", + "mod_r8_strided", + "deg2rad_r4_strided", + "deg2rad_r8_strided", + "rad2deg_r4_strided", + "rad2deg_r8_strided", + "dist2_r4_strided", + "dist2_r8_strided", + "dot2_r4_strided", + "dot2_r8_strided", + "dot3_r4_strided", + "dot3_r8_strided", + "conj_c4_strided", + "conj_c8_strided", + "real_c4_strided", + "real_c8_strided", + "aimag_c4_strided", + "aimag_c8_strided", + "abs_c4_strided", + "abs_c8_strided", + "is_positive_r4_strided", + "is_positive_r8_strided", + "is_even_i4_strided", +] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/multid_arrays/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/multid_arrays/__init__.pyi index c5242e931..6a6aa9bf1 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/multid_arrays/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/multid_arrays/__init__.pyi @@ -1 +1,3 @@ from . import multid_arrays + +__all__ = ["multid_arrays"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/multid_arrays/multid_arrays.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/multid_arrays/multid_arrays.pyi index aba84cc11..4d2a04b5e 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/multid_arrays/multid_arrays.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/multid_arrays/multid_arrays.pyi @@ -37,3 +37,13 @@ def checksum3_strided( a: Float64[::, ::, ::], checksum: Float64[1] ) -> None: ... + +__all__ = [ + "scale2_contiguous", + "scale2_strided", + "checksum2_strided", + "scale2_explicit", + "shift3_contiguous", + "shift3_strided", + "checksum3_strided", +] diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/__init__.pyi index d6b33aa84..7dad02c8e 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import arrays_direct_bind_c_f90 + +__all__ = ["arrays_direct_bind_c_f90"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/arrays_direct_bind_c_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/arrays_direct_bind_c_f90.pyi index bb5fefe55..e7509d2f7 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/arrays_direct_bind_c_f90.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/arrays_direct_bind_c_f90.pyi @@ -30,3 +30,5 @@ def scale_matrix( columns: Int32, values: Float64[rows, columns] ) -> None: ... + +__all__ = ["sum_values", "scale_values", "all_flags", "invert_flags", "scale_matrix"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/__init__.pyi index b00ea2ba4..6c5c43855 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import arrays_mixed_bind_c_f90 + +__all__ = ["arrays_mixed_bind_c_f90"] diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/arrays_mixed_bind_c_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/arrays_mixed_bind_c_f90.pyi index 08121f3e2..23bd86096 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/arrays_mixed_bind_c_f90.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/arrays_mixed_bind_c_f90.pyi @@ -11,3 +11,5 @@ def adapted_sum( n: Int32, values: Float64[n] ) -> Float64: ... + +__all__ = ["direct_sum", "adapted_sum"] diff --git a/tests/fortran/arrays/end_to_end/test_declaration_extent_expressions.py b/tests/fortran/arrays/end_to_end/test_declaration_extent_expressions.py index 3a8236c63..e3e8a8030 100644 --- a/tests/fortran/arrays/end_to_end/test_declaration_extent_expressions.py +++ b/tests/fortran/arrays/end_to_end/test_declaration_extent_expressions.py @@ -6,6 +6,7 @@ import pytest from tests.fortran._support.wrapper_build import ( + _build_generated_pyi_and_import, _build_inline_pyi_contract_module, _build_sources_and_import, _build_text_and_import, @@ -369,3 +370,203 @@ def test_prototype_entity_is_visible_inside_a_standalone_target_interface(tmp_pa assert f"procedure({interface_symbol}) :: external_extent" in bridge assert "import :: c_int32_t, external_extent, c_double" in bridge assert "real(c_double), dimension(external_extent(n)) :: native_result" in bridge + + +RESERVED_EXTENT_PROVIDER = """ +module reserved_extent_provider + implicit none +contains + pure integer function lambda(n) + integer, intent(in) :: n + lambda = n + end function lambda + pure integer function lambda_(n) + integer, intent(in) :: n + lambda_ = n + 1 + end function lambda_ +end module reserved_extent_provider +""" + + +RESERVED_EXTENT_OWNER = """ +module reserved_extent_owner + use, intrinsic :: iso_c_binding, only: c_double + use reserved_extent_provider, only: lambda, lambda_ + implicit none +contains + function keyword_values(n) result(output) + integer, intent(in) :: n + real(c_double) :: output(lambda(n)) + output = 1.0_c_double + end function keyword_values + function collided_values(n) result(output) + integer, intent(in) :: n + real(c_double) :: output(2*lambda_(n) + n) + output = 2.0_c_double + end function collided_values +end module reserved_extent_owner +""" + + +def test_specification_functions_python_must_rename_still_size_their_results(tmp_path: Path): + """`lambda` is a Python keyword and `lambda_` then collides with its escape. + + The contract calls them `lambda_` and `lambda__2`, while the native calls + still reach the Fortran functions spelled `lambda` and `lambda_`. + """ + module, _payload = _build_sources_and_import( + [ + ("reserved_extent_provider.f90", RESERVED_EXTENT_PROVIDER), + ("reserved_extent_owner.f90", RESERVED_EXTENT_OWNER), + ], + tmp_path, + ) + + np.testing.assert_array_equal(module.reserved_extent_owner.keyword_values(np.int32(3)), np.full(3, 1.0)) + np.testing.assert_array_equal(module.reserved_extent_owner.collided_values(np.int32(3)), np.full(11, 2.0)) + contract = (tmp_path / "contracts" / "reserved_extent_owner.pyi").read_text(encoding="utf-8") + assert "-> Float64[lambda_(n)]" in contract + assert "-> Float64[2 * lambda__2(n) + n]" in contract + + +CHECKED_EXTENT_SOURCE = """ +module checked_extent_provider + implicit none +contains + pure integer function extent_for(n) + integer, intent(in) :: n + extent_for = n + 1 + end function extent_for +end module checked_extent_provider + +module checked_extent_owner + use, intrinsic :: iso_c_binding, only: c_double + use checked_extent_provider, only: extent_for + implicit none +contains + subroutine fill(n, values) + integer, intent(in) :: n + real(c_double), intent(out) :: values(extent_for(n)) + values = 2.0_c_double + end subroutine fill + real(c_double) function total(n, values) + integer, intent(in) :: n + real(c_double), intent(in) :: values(extent_for(n)) + total = sum(values) + end function total + subroutine maybe_fill(n, values) + integer, intent(in) :: n + real(c_double), intent(inout), optional :: values(extent_for(n)) + if (present(values)) values = 5.0_c_double + end subroutine maybe_fill +end module checked_extent_owner +""" + + +@pytest.mark.parametrize("lane", ["source", "generated_pyi"]) +def test_an_actual_is_checked_against_the_extent_a_specification_function_declares(tmp_path: Path, lane: str): + """An explicit-shape dummy is as long as its declaration says, whoever sizes it. + + Only the Fortran bridge can evaluate a specification function, so the + binding skipped the check it makes for every other extent. A shorter + `intent(out)` actual was then written past its end and a shorter + `intent(in)` one read past it. The contract states the function is pure, + so the generated contract builds and checks the same way the source does. + """ + if lane == "source": + package, _payload = _build_sources_and_import([("checked_extent.f90", CHECKED_EXTENT_SOURCE)], tmp_path) + else: + source = tmp_path / "checked_extent.f90" + source.write_text(CHECKED_EXTENT_SOURCE, encoding="utf-8") + package = _build_generated_pyi_and_import(source, tmp_path / "replay") + owner = package.checked_extent_owner + + values = np.zeros(4) + owner.fill(np.int32(3), values) + np.testing.assert_array_equal(values, np.full(4, 2.0)) + assert owner.total(np.int32(3), np.ones(4)) == 4.0 + owner.maybe_fill(np.int32(3)) + for call in ( + lambda: owner.fill(np.int32(3), np.zeros(3)), + lambda: owner.fill(np.int32(3), np.zeros(5)), + lambda: owner.total(np.int32(3), np.ones(3)), + lambda: owner.maybe_fill(np.int32(3), np.zeros(3)), + ): + with pytest.raises(TypeError, match="has incompatible shape at axis 0"): + call() + + +EXTENT_BOUNDARY_SOURCE = """ +module extent_boundary + use, intrinsic :: iso_c_binding, only: c_double + implicit none + type :: box + integer :: value = 0 + end type box +contains + pure integer function extent_for(n) + integer, intent(in) :: n + extent_for = n + 1 + end function extent_for + subroutine pair(n, grid, values) + integer, intent(in) :: n + real(c_double), intent(inout) :: grid(n, n) + real(c_double), intent(in) :: values(extent_for(n)) + grid = sum(values) + end subroutine pair + function boxed(n, values) result(out) + integer, intent(in) :: n + real(c_double), intent(in) :: values(extent_for(n)) + type(box) :: out + out%value = size(values) + end function boxed +end module extent_boundary +""" + + +EXTENT_BOUNDARY_CONTRACT = """ +from prik.contracts import Addr, Annotated, Arg, COPY_F, Float64, Int32, ORDER_C, native_call, pure + +class Box: + def __init__(self, *, value: Int32 = ...) -> None: ... + + value: Int32 + +@pure +@native_call([Addr(Arg(0))]) +def extent_for(n: Int32) -> Int32: ... + +@native_call([Addr(Arg(0)), Arg(1), Arg(2)]) +def pair(n: Int32, grid: Annotated[Float64[n, n], ORDER_C, COPY_F], values: Float64[extent_for(n)]) -> None: ... + +@native_call([Addr(Arg(0)), Arg(1)]) +def boxed(n: Int32, values: Float64[extent_for(n)]) -> Box: ... +""" + + +def test_a_rejected_extent_runs_nothing_that_follows_the_call(tmp_path: Path): + """A mismatch ends the call where the extent is found: no copy-back, no result. + + `grid` crosses through a Fortran-order temporary whose copy-back follows a + successful call, and `boxed` returns an object only a call produces. A + rejected call must reach neither, so the caller's array is untouched and + the shape error is what surfaces, not a missing result. + """ + module, _ = _build_inline_pyi_contract_module( + tmp_path, + module_name="extent_boundary", + source_text=EXTENT_BOUNDARY_SOURCE, + contract_text=EXTENT_BOUNDARY_CONTRACT, + ) + + grid = np.full((3, 3), 7.0, order="C") + module.pair(np.int32(3), grid, np.ones(4)) + np.testing.assert_array_equal(grid, np.full((3, 3), 4.0)) + grid = np.full((3, 3), 7.0, order="C") + with pytest.raises(TypeError, match="Argument values has incompatible shape at axis 0"): + module.pair(np.int32(3), grid, np.ones(3)) + np.testing.assert_array_equal(grid, np.full((3, 3), 7.0)) + + assert module.boxed(np.int32(3), np.ones(4)).value == 4 + with pytest.raises(TypeError, match="Argument values has incompatible shape at axis 0"): + module.boxed(np.int32(3), np.ones(3)) diff --git a/tests/fortran/arrays/semantics/test_array_semantics.py b/tests/fortran/arrays/semantics/test_array_semantics.py index 75e96b557..5e8a3db5a 100644 --- a/tests/fortran/arrays/semantics/test_array_semantics.py +++ b/tests/fortran/arrays/semantics/test_array_semantics.py @@ -9,6 +9,8 @@ get_function, ) from prik.semantics.models import SemanticExpressionCallable +from prik.policy.contract_imports import complete_contract_imports +from prik.policy.exports import complete_python_export_policy from prik.printers import PyiPrinter from prik.parsers.fortran import parse_fortran_file as parse_fortran_source from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text @@ -43,7 +45,7 @@ def test_array_constraints(): contract = array_contract(x.semantic_type) assert contract.category == "assumed_shape" - assert contract.shape == ["::Strided"] + assert contract.shape == ["::"] assert contract.source_shape == [":"] assert contract.order is None @@ -76,7 +78,7 @@ def test_matrix_semantics(): assert A.semantic_type.rank == 2 contract = array_contract(A.semantic_type) - assert A.semantic_type.shape == ["::Strided", "::Strided"] + assert A.semantic_type.shape == ["::", "::"] assert contract.source_shape == [":", ":"] assert contract.category == "assumed_shape" assert contract.order == "ORDER_F" @@ -171,6 +173,8 @@ def test_fortran_inquiries_become_python_array_expressions_and_keep_source_bound "2 if source.shape[1] > 0 else 1", "2 + source.shape[1] - 1 if source.shape[1] > 0 else 0", ] + complete_python_export_policy(module) + complete_contract_imports([module]) generated = PyiPrinter().emit(module) assert "source.shape[0], max(1, source.shape[1]), source.size, 2 ** source.ndim" in generated assert "2 if source.shape[1] > 0 else 1" in generated @@ -226,11 +230,13 @@ def test_specification_function_calls_keep_local_and_imported_native_identity(): ], ] + complete_python_export_policy(module) + complete_contract_imports([module]) generated = PyiPrinter().emit(module) reloaded = parse_pyi_text(generated, module_name="expression_owner") reloaded_array = get_function(reloaded, "values").return_type.storage.array - assert "from extent_helpers import extent_for as imported_extent" in generated + assert "from .extent_helpers import extent_for as imported_extent" in generated assert "Float64[imported_extent(n), local_extent(n)]" in generated assert reloaded_array.expression_callables == array.expression_callables @@ -246,9 +252,7 @@ def test_wildcard_specification_function_origin_round_trips_unambiguously(): end module extent_helpers module unrelated_helpers -contains -subroutine unrelated() -end subroutine unrelated + integer, parameter :: unrelated = 1 end module unrelated_helpers module expression_owner @@ -267,14 +271,50 @@ def test_wildcard_specification_function_origin_round_trips_unambiguously(): assert array.expression_callables[0][0].native_scope == "extent_helpers" + complete_python_export_policy(module) + complete_contract_imports([module]) generated = PyiPrinter().emit(module) reloaded = parse_pyi_text(generated, module_name="expression_owner") reloaded_array = get_function(reloaded, "values").return_type.storage.array - assert "from extent_helpers import extent_for" in generated + assert "from .extent_helpers import extent_for" in generated assert reloaded_array.expression_callables == array.expression_callables +def test_non_only_rename_does_not_choose_between_specification_function_routes(): + """Ambiguous and renamed-away procedure names keep no invented origin.""" + source = """ +module extent_helpers +contains +integer function x(n) result(extent) + integer, intent(in) :: n + extent = n +end function x +integer function y(n) result(extent) + integer, intent(in) :: n + extent = n +end function y +end module extent_helpers + +module expression_owner + use extent_helpers, x => y +contains +function values(n) result(output) + integer, intent(in) :: n + real(8) :: output(x(n), y(n)) +end function values +end module expression_owner +""" + modules = fortran_file_to_semantic_modules(parse_fortran_source(source)) + module = next(item for item in modules if item.name == "expression_owner") + callables = get_function(module, "values").return_type.storage.array.expression_callables + + assert callables == [ + [SemanticExpressionCallable(name="x", native_name="x", source_language="fortran")], + [SemanticExpressionCallable(name="y", native_name="y", source_language="fortran")], + ] + + def test_unindexed_wildcard_specification_function_origin_is_not_guessed(): source = """ module expression_owner @@ -322,3 +362,35 @@ def test_standalone_specification_interface_round_trips_as_one_pure_prototype_si assert ( get_function(reloaded, "values").return_type.storage.array.expression_callables[0][0].placement == "standalone" ) + + +def test_a_pure_function_contract_states_its_purity_and_reads_it_back(): + """A specification function must be pure, so its contract has to say it is. + + The contract wrote `@pure` only on prototypes, so a pure module function + read back impure and a contract calling it in a declaration expression + could not be built. + """ + module = fortran_module_to_semantic_module( + parse_fortran_source(""" +module extent_provider +contains +pure integer function extent_for(n) + integer, intent(in) :: n + extent_for = n + 1 +end function extent_for +integer function plain(n) + integer, intent(in) :: n + plain = n +end function plain +end module extent_provider +""") + ) + complete_python_export_policy(module) + complete_contract_imports([module]) + contract = PyiPrinter(normalize_public_names=True).emit(module) + + assert "@pure\n@native_call([Addr(Arg(0))])\ndef extent_for(" in contract + assert "@pure\n@native_call([Addr(Arg(0))])\ndef plain(" not in contract + reloaded = parse_pyi_text(contract, module_name="extent_provider") + assert [function.metadata.get("fortran_attributes") for function in reloaded.functions] == [["pure"], None] diff --git a/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py b/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py index 416a96a53..c04dc7efe 100644 --- a/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py +++ b/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py @@ -6,6 +6,7 @@ import pytest from prik.utilities.declaration_expressions import ( + declaration_expression_identifiers, ArrayExpressionSource, DeclarationExpressionCall, ResolvedDeclarationExtent, @@ -18,6 +19,7 @@ fortran_extent_to_python, is_declaration_expression_helper, is_public_declaration_expression, + rename_declaration_expression_calls, render_declaration_extent, resolve_declaration_extent, split_declaration_assignment, @@ -34,7 +36,7 @@ def test_source_helpers_keep_nested_syntax_intact() -> None: "[third, fourth]", ] assert split_top_level_expression("'first''part', second", ",") == ["'first''part'", "second"] - assert split_top_level_expression("first::Strided:upper", ":") == ["first", "", "Strided", "upper"] + assert split_top_level_expression("first::middle:upper", ":") == ["first", "", "middle", "upper"] with pytest.raises(ValueError, match="one character"): split_top_level_expression("value", "::") @@ -111,7 +113,7 @@ def test_normalization_and_inspection_preserve_expression_provenance() -> None: assert declaration_extent_references("n + max(m, 1)") == ("n", "m") assert declaration_extent_references("values.shape[0]") == ("",) assert declaration_extent_references("not valid (") == ("",) - assert declaration_extent_references("::Strided") == () + assert declaration_extent_references("::") == () assert declaration_extent_uses_power("n ** 2") assert not declaration_extent_uses_power("not valid (") assert is_declaration_expression_helper("SUM") @@ -192,7 +194,7 @@ def test_role_resolution_reuses_completed_roles_and_names_blockers() -> None: array_roles = {"values": ("values", ("value_role_0", "value_role_1"))} callable_roles = {"extent_for": ("prik_extent_for", "extent_role")} - assert resolve_declaration_extent("::Strided", scalar_roles, array_roles) == ResolvedDeclarationExtent("::Strided") + assert resolve_declaration_extent("::", scalar_roles, array_roles) == ResolvedDeclarationExtent("::") assert resolve_declaration_extent("n + values.shape[1]", scalar_roles, array_roles) == ResolvedDeclarationExtent( "n + __prik_extent_values_1", ("n", "__prik_extent_values_1"), @@ -320,3 +322,64 @@ def test_backend_renderer_rejects_invalid_target_and_unrenderable_syntax() -> No render_declaration_extent("not valid (", {}, target="c") with pytest.raises(ValueError, match="unsupported completed declaration-expression node"): render_declaration_extent("[n]", {}, target="c") + + +def test_a_character_literal_references_no_name_it_happens_to_spell(): + """Parsing decides what is a reference, so a literal's contents are its value.""" + assert declaration_expression_identifiers('"box"') == () + assert declaration_expression_identifiers("'box'") == () + + +def test_an_expression_reports_the_names_it_reads(): + """A name used in a declaration is a reference wherever it appears.""" + assert declaration_expression_identifiers("crate") == ("crate",) + assert set(declaration_expression_identifiers("n * 2 + other")) == {"n", "other"} + assert set(declaration_expression_identifiers("size(values)")) == {"size", "values"} + + +def test_a_selector_keyword_names_a_slot_rather_than_an_entity(): + """`len` and `kind` are syntax, so only the value they carry is read.""" + assert declaration_expression_identifiers("len=3") == () + assert declaration_expression_identifiers("len=n") == ("n",) + assert declaration_expression_identifiers("kind=c_char") == ("c_char",) + assert declaration_expression_identifiers('kind="box"') == () + + +def test_each_selector_in_one_declaration_is_read_separately(): + """A character declaration carries both selectors in one stored string.""" + assert declaration_expression_identifiers("len=n, kind=c_char") == ("n", "c_char") + assert declaration_expression_identifiers("len=1, kind=c_char") == ("c_char",) + + +def test_a_comparison_is_not_read_as_a_selector(): + """`==` is an operator, so both sides are part of the expression.""" + assert set(declaration_expression_identifiers("a == b")) == {"a", "b"} + + +def test_lexical_translation_leaves_character_literals_alone(): + """A literal's contents are its value, whatever they spell outside quotes.""" + from prik.utilities.declaration_expressions import _python_parseable_fortran_expression + + assert _python_parseable_fortran_expression('len(".true.")') == 'len(".true.")' + assert _python_parseable_fortran_expression('len("a%b")') == 'len("a%b")' + assert _python_parseable_fortran_expression('len("1d2")') == 'len("1d2")' + # Everything outside the literal is still translated. + assert _python_parseable_fortran_expression('obj%field + len("a%b")') == 'obj.field + len("a%b")' + assert _python_parseable_fortran_expression(".true.") == "True" + + +def test_a_native_name_python_reserves_is_still_read_as_a_call(): + """A Fortran function may be called `lambda`; the call is not invalid syntax.""" + assert declaration_expression_calls("lambda(n) + class(2)") == ("lambda", "class") + assert declaration_expression_identifiers("lambda(n) + 1") == ("lambda", "n") + + +def test_respelling_changes_call_targets_and_nothing_else(): + """A variable or a literal spelled like the callee keeps its spelling.""" + assert rename_declaration_expression_calls("lambda(n)", {"lambda": "lambda_"}) == "lambda_(n)" + assert ( + rename_declaration_expression_calls("helper(n) + helper + len('helper(')", {"helper": "helper_2"}) + == "helper_2(n) + helper + len('helper(')" + ) + # Nothing to respell leaves the text exactly as written. + assert rename_declaration_expression_calls("2*n", {"helper": "helper_2"}) == "2*n" diff --git a/tests/fortran/callbacks/codegen/test_callback_planning.py b/tests/fortran/callbacks/codegen/test_callback_planning.py index 527aa537d..58d59255d 100644 --- a/tests/fortran/callbacks/codegen/test_callback_planning.py +++ b/tests/fortran/callbacks/codegen/test_callback_planning.py @@ -4,7 +4,9 @@ import pytest +from prik.parsers.fortran import parse_fortran_file as parse_fortran_source from prik.pipeline.pyi import pyi_file_to_semantic_module, pyi_text_to_semantic_module +from prik.semantics.fortran2ir import FortranToIRConverter from prik.semantics import models from prik.policy.ownership import PythonBarrierAction from prik.policy.completion import complete_semantic_policies @@ -12,9 +14,11 @@ CallbackABIKind, CallbackGILAction, CallbackLifecycleAction, + CallbackOptionalityAction, CallbackResultAction, CallbackThreadAction, CallbackTransferAction, + OptionalMode, ) from prik.pipeline.wrapper import WrapperGenerator from prik.planning import GeneratedSupportProcedureImplementationOwner, WrapperPlanner @@ -64,12 +68,15 @@ def test_callback_policy_completes_value_default_and_explicit_reference_before_p assert scalar.thread_action is CallbackThreadAction.REQUIRE_ENTERING_THREAD assert scalar.gil_actions == (CallbackGILAction.ACQUIRE_GIL, CallbackGILAction.RELEASE_GIL) assert tuple(transfer.abi for transfer in scalar.arguments) == (CallbackABIKind.REFERENCE,) * 3 + # An undeclared intent permits the callee to read and modify the dummy, so + # it copies both ways rather than defaulting to copy-in. assert tuple(transfer.adapter_action for transfer in scalar.arguments) == ( CallbackTransferAction.COPY_IN_OUT, CallbackTransferAction.COPY_OUT, - CallbackTransferAction.COPY_IN, + CallbackTransferAction.COPY_IN_OUT, ) - assert tuple(transfer.python_action for transfer in scalar.arguments) == (PythonBarrierAction.SCALAR_VALUE,) * 3 + # Every dummy the callee may write needs storage Python can write through. + assert tuple(transfer.python_action for transfer in scalar.arguments) == (PythonBarrierAction.SCALAR_STORAGE,) * 3 array = policies["apply_array_storage_callback"].arguments[0].callback assert array.arguments[0].abi is CallbackABIKind.REFERENCE @@ -83,7 +90,7 @@ def test_callback_policy_completes_value_default_and_explicit_reference_before_p assert tuple(transfer.character_length for transfer in string.arguments) == (8, 8, 8) derived = policies["apply_point_callback"].arguments[0].callback - assert derived.arguments[0].derived_type_identity == ("fcallback_all_f90", "point_t") + assert derived.arguments[0].derived_type_identity == ("fcallback_all_f90", "Point_T") assert derived.result.action is CallbackResultAction.RETURN_DERIVED_ADDRESS @@ -144,6 +151,10 @@ def test_callback_plan_projects_one_explicit_site_and_stable_roles_per_argument( ("scalar_projection", "inconsistent-callback-scalar-value-projection"), ("result", "callback-void-has-transfer"), ("entrypoint_parameter", "inconsistent-callback-entrypoint-parameter"), + ("prototype_optional", "inconsistent-callback-prototype-arguments"), + ("native_fortran_type", "inconsistent-callback-prototype-arguments"), + ("optional_value_abi", "invalid-callback-optionality"), + ("blocked_optionality", "invalid-callback-optionality"), ("symbols", "invalid-callback-symbols"), ), ) @@ -157,13 +168,27 @@ def test_callback_plan_edits_fail_central_validation_before_backend_emission(edi callback.arguments[1].extent_roles = () elif edit == "scalar_projection": callback = _callback_argument(plan, "apply_scalar_storage_callback").callback - callback.arguments[0].python_action = PythonBarrierAction.SCALAR_STORAGE + # A rank-zero storage transfer cannot claim the value projection: an + # immutable value cannot deliver a write back to the native caller. + callback.arguments[0].python_action = PythonBarrierAction.SCALAR_VALUE elif edit == "result": callback = _callback_argument(plan, "apply_value_callback").callback callback.result.action = CallbackResultAction.RETURN_VOID elif edit == "entrypoint_parameter": argument = _callback_argument(plan, "apply_value_callback") argument.entrypoint.pass_callback_parameter = True + elif edit == "prototype_optional": + callback = _callback_argument(plan, "apply_value_callback").callback + callback.prototype.arguments[0].optional = True + elif edit == "native_fortran_type": + callback = _callback_argument(plan, "apply_value_callback").callback + callback.prototype.arguments[0].native_fortran_type = "logical(kind=8)" + elif edit == "optional_value_abi": + callback = _callback_argument(plan, "apply_value_callback").callback + callback.arguments[0].optionality = CallbackOptionalityAction.NULL_DATA_POINTER + elif edit == "blocked_optionality": + callback = _callback_argument(plan, "apply_value_callback").callback + callback.arguments[0].optionality = CallbackOptionalityAction.BLOCKED else: callback = _callback_argument(plan, "apply_value_callback").callback callback.entrypoint.support_procedure.symbol_name = callback.bridge.adapter_symbol @@ -237,11 +262,186 @@ def test_every_callback_uses_the_shared_generated_abstract_prototype(): assert "=> transform_callback" not in bridge -def test_optional_callback_retains_one_exact_policy_blocker(): +def test_optional_callback_uses_the_ordinary_presence_plan(): module = pyi_file_to_semantic_module(CONTRACT, module_name="fcallback_all_f90") function = next(item for item in module.functions if item.name == "apply_value_callback") function.arguments[0].optional = True complete_semantic_policies(module) - with pytest.raises(ValueError, match="unsupported optional callback"): - WrapperPlanner().build(module) + plan = WrapperPlanner().build(module) + argument = _callback_argument(plan, "apply_value_callback") + + assert argument.binding.optional_mode is OptionalMode.NULLABLE_VALUE + assert argument.entrypoint.optional_mode is OptionalMode.NULLABLE_VALUE + assert argument.entrypoint.pass_callback_parameter is True + + c_source, bridge = _sources(plan) + assert "bound_callback_obj != Py_None ? prik_callback_trampoline_" in c_source + assert "if (c_associated(callback)) then" in bridge + assert "native_apply_value_callback(callback=prik_callback_adapter_" in bridge + assert "native_apply_value_callback(value=value)" in bridge + + +def test_direct_bind_c_callback_generates_no_fortran_callback_adapter(): + source = """ +module direct_callback + use iso_c_binding + implicit none + + abstract interface + subroutine report(value) bind(C) + import c_int + integer(c_int), value, intent(in) :: value + end subroutine report + end interface + +contains + + subroutine run(callback) bind(C) + procedure(report) :: callback + call callback(4_c_int) + end subroutine run +end module direct_callback +""" + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + callback = _callback_argument(plan, "run").callback + + artifacts = WrapperGenerator().generate(plan) + c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") + assert callback.entrypoint.support_procedure.symbol_name in c_source + assert all(source.path.suffix != ".f90" for source in artifacts.sources) + + +def test_runtime_callback_extents_lower_to_assumed_shape_dummies_and_measured_copies(): + """Codegen spells a runtime extent instead of leaking the plan's marker. + + A runtime extent reaches the bridge as a public marker rather than an + expression, so the dummy takes the caller's descriptor and the contiguous + copy that backs ``c_loc`` is measured from that dummy. + """ + module = pyi_file_to_semantic_module(ARRAY_CONTRACT, module_name="fcallback_array_f90") + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + + callback = _callback_argument(plan, "apply_assumed_shape").callback + assert [transfer.array.shape for transfer in callback.arguments] == [("::",), ("::",)] + + _, bridge = _sources(plan) + assert "real(c_double), intent(in), dimension(:) :: values" in bridge + assert "real(c_double), target, dimension(size(values, 1)) :: values_callback_storage" in bridge + assert "real(c_double), intent(out), dimension(:) :: doubled" in bridge + assert "real(c_double), target, dimension(size(doubled, 1)) :: doubled_callback_storage" in bridge + + +def test_rank_zero_callback_storage_lowers_to_a_direction_correct_native_view(): + """Rank-zero storage aliases native memory instead of copying a value. + + Writeability follows the completed transfer direction, so only an ``out`` + or ``inout`` dummy can be written through. + """ + module = pyi_text_to_semantic_module( + """ +from prik.contracts import Float64, In, InOut, Out, prototype + +@prototype +def directions_callback( + read_value: In(Float64[()]), + update_value: InOut(Float64[()]), + write_value: Out(Float64[()]) +) -> None: ... + +def apply_directions(callback: directions_callback) -> None: ... +""", + module_name="callback_scalar_storage", + ) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + + callback = _callback_argument(plan, "apply_directions").callback + assert [transfer.python_action for transfer in callback.arguments] == [PythonBarrierAction.SCALAR_STORAGE] * 3 + assert [transfer.abi for transfer in callback.arguments] == [CallbackABIKind.REFERENCE] * 3 + + c_source, _bridge = _sources(plan) + read_only = "PyArray_New(&PyArray_Type, 0, NULL, NPY_FLOAT64, NULL, read_value_data, 0, " + assert f"{read_only}NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_ALIGNED, NULL)" in c_source + for parameter in ("update_value", "write_value"): + writable = f"PyArray_New(&PyArray_Type, 0, NULL, NPY_FLOAT64, NULL, {parameter}_data, 0, " + assert f"{writable}NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_ALIGNED | NPY_ARRAY_WRITEABLE, NULL)" in c_source + + +MATRIX_CONTRACT = """ +from prik.contracts import Float64, In, Out, prototype + +@prototype +def matrix_callback( + input: In(Float64[::, ::]), + output: Out(Float64[::, ::]) +) -> None: ... + +def apply_matrix(callback: matrix_callback) -> None: ... +""" + + +def _matrix_plan(): + module = pyi_text_to_semantic_module(MATRIX_CONTRACT, module_name="callback_matrix") + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + +def test_multidimensional_runtime_extents_measure_every_axis_from_the_dummy(): + """Each axis of an assumed-shape callback array is lowered independently. + + A rank-one fix can silently ignore later axes, so the copy that backs + ``c_loc`` must be measured on every axis of the dummy it sits beside. + """ + plan = _matrix_plan() + callback = _callback_argument(plan, "apply_matrix").callback + assert [transfer.array.rank for transfer in callback.arguments] == [2, 2] + + _, bridge = _sources(plan) + assert "real(c_double), intent(in), dimension(:, :) :: input" in bridge + assert "real(c_double), target, dimension(size(input, 1), size(input, 2)) :: input_callback_storage" in bridge + assert "real(c_double), intent(out), dimension(:, :) :: output" in bridge + assert "real(c_double), target, dimension(size(output, 1), size(output, 2)) :: output_callback_storage" in bridge + + +def test_callback_docstrings_carry_array_rank_and_public_extents(): + """A callable's ABI depends on rank and shape, so both are documented. + + Extents use the spelling the `.pyi` contract uses, so the two descriptions + of the same array agree and no internal marker reaches the reader. + """ + plan = _matrix_plan() + c_source, _bridge = _sources(plan) + documentation = c_source.encode().decode("unicode_escape") + + assert "Called as: callback(input, output) -> None" in documentation + assert "input : ndarray[float64], rank 2, shape (::, ::), intent(in)" in documentation + assert "output : ndarray[float64], rank 2, shape (::, ::), intent(out)" in documentation + + +def test_callback_array_result_diagnostic_uses_the_contract_spelling(): + """A rejected shape is reported the way a contract would spell it. + + A function result has no caller descriptor to measure, so a runtime extent + there is refused; the message names the extent the author wrote rather than + the explicit step the IR stores. + """ + module = pyi_text_to_semantic_module( + """ +from prik.contracts import Float64, In, prototype + +@prototype +def strided_result(x: In(Float64)) -> Float64[::]: ... + +def apply(callback: strided_result) -> None: ... +""", + module_name="callback_strided_result", + ) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + + with pytest.raises(ValueError, match=r"runtime extents \['::'\]"): + _sources(plan) diff --git a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/__init__.pyi b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/__init__.pyi index b25110243..c94255d16 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/__init__.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/__init__.pyi @@ -1 +1,3 @@ from . import fcallback_all_f90 + +__all__ = ["fcallback_all_f90"] diff --git a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/fcallback_all_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/fcallback_all_f90.pyi index b6895f49a..dfd94bd7b 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/fcallback_all_f90.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/fcallback_all_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Arg, Float64, In, InOut, Int32, Out, Return, Returns, String, native_call, prototype -class point_t: +class Point_T: def __init__( self, *, @@ -18,9 +18,9 @@ def value_callback( @prototype def scalar_storage_callback( - value: InOut(Addr(Float64)), - output: Out(Addr(Float64)), - missing: Addr(Float64) + value: InOut(Float64[()]), + output: Out(Float64[()]), + missing: Float64[()] ) -> None: ... @prototype @@ -39,8 +39,8 @@ def string_storage_callback( @prototype def point_callback( - value: In(point_t) -) -> point_t: ... + value: In(Point_T) +) -> Point_T: ... @native_call([Arg(0), Addr(Arg(1))]) def apply_value_callback( @@ -71,6 +71,20 @@ def apply_string_storage_callback( def apply_point_callback( callback: point_callback, - value: point_t, - output: point_t + value: Point_T, + output: Point_T ) -> None: ... + +__all__ = [ + "Point_T", + "value_callback", + "scalar_storage_callback", + "array_storage_callback", + "string_storage_callback", + "point_callback", + "apply_value_callback", + "apply_scalar_storage_callback", + "apply_array_storage_callback", + "apply_string_storage_callback", + "apply_point_callback", +] diff --git a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/__init__.pyi b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/__init__.pyi index f291a7423..c8b1722be 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/__init__.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/__init__.pyi @@ -1 +1,3 @@ from . import fcallback_array_f90 + +__all__ = ["fcallback_array_f90"] diff --git a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/fcallback_array_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/fcallback_array_f90.pyi index 5623f4aad..24f2590cb 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/fcallback_array_f90.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/fcallback_array_f90.pyi @@ -1,4 +1,4 @@ -from prik.contracts import Addr, Arg, Float64, In, Int32, native_call, prototype +from prik.contracts import Addr, Arg, Float64, In, Int32, Out, native_call, prototype @prototype def reduce_callback( @@ -12,6 +12,12 @@ def transform_callback( values: In(Float64[count]) ) -> Float64[count]: ... +@prototype +def assumed_shape_callback( + values: In(Float64[::]), + doubled: Out(Float64[::]) +) -> None: ... + @native_call([Arg(0), Addr(Arg(1)), Arg(2)]) def apply_reduce( callback: reduce_callback, @@ -26,3 +32,18 @@ def apply_transform( values: Float64[count], output: Float64[count] ) -> None: ... + +def apply_assumed_shape( + callback: assumed_shape_callback, + values: Float64[::], + doubled: Float64[::] +) -> None: ... + +__all__ = [ + "reduce_callback", + "transform_callback", + "assumed_shape_callback", + "apply_reduce", + "apply_transform", + "apply_assumed_shape", +] diff --git a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/__init__.pyi b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/__init__.pyi index 5e2288edc..feb141ce1 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/__init__.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/__init__.pyi @@ -1 +1,3 @@ from . import fcallback_scalar_f90 + +__all__ = ["fcallback_scalar_f90"] diff --git a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi index 1e9b2d239..87be5f006 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi @@ -11,7 +11,7 @@ def notify_callback( ) -> None: ... @prototype -def callback( +def apply_explicit_callback( value: In(Addr(Float64)) ) -> Float64: ... @@ -23,7 +23,7 @@ def apply_scalar( @native_call([Arg(0), Addr(Arg(1))]) def apply_explicit( - callback: callback, + callback: apply_explicit_callback, value: Float64 ) -> Float64: ... @@ -32,3 +32,5 @@ def call_notify( callback: notify_callback, value: Float64 ) -> None: ... + +__all__ = ["scalar_callback", "notify_callback", "apply_scalar", "apply_explicit", "call_notify"] diff --git a/tests/fortran/callbacks/end_to_end/fixtures/native/fcallback_array_f90.f90 b/tests/fortran/callbacks/end_to_end/fixtures/native/fcallback_array_f90.f90 index 28e673c31..a3b003125 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/native/fcallback_array_f90.f90 +++ b/tests/fortran/callbacks/end_to_end/fixtures/native/fcallback_array_f90.f90 @@ -14,6 +14,11 @@ function transform_callback(count, values) result(output) real(8), intent(in) :: values(count) real(8) :: output(count) end function transform_callback + + subroutine assumed_shape_callback(values, doubled) + real(8), intent(in) :: values(:) + real(8), intent(out) :: doubled(:) + end subroutine assumed_shape_callback end interface contains @@ -33,4 +38,12 @@ subroutine apply_transform(callback, count, values, output) output = callback(count, values) end subroutine apply_transform + + subroutine apply_assumed_shape(callback, values, doubled) + procedure(assumed_shape_callback) :: callback + real(8), intent(in) :: values(:) + real(8), intent(out) :: doubled(:) + + call callback(values, doubled) + end subroutine apply_assumed_shape end module fcallback_array_f90 diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/__init__.pyi b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/__init__.pyi index e0575265b..04fd93957 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import callbacks_direct_bind_c_f90 + +__all__ = ["callbacks_direct_bind_c_f90"] diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/callbacks_direct_bind_c_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/callbacks_direct_bind_c_f90.pyi index 7de4f6d52..4f7a923c6 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/callbacks_direct_bind_c_f90.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/callbacks_direct_bind_c_f90.pyi @@ -23,3 +23,5 @@ def direct_call_notify( callback: direct_notify, value: Int32 ) -> None: ... + +__all__ = ["direct_callback", "direct_notify", "direct_apply", "direct_call_notify"] diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/__init__.pyi b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/__init__.pyi index b715fb96e..4a355398d 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import callbacks_mixed_bind_c_f90 + +__all__ = ["callbacks_mixed_bind_c_f90"] diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/callbacks_mixed_bind_c_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/callbacks_mixed_bind_c_f90.pyi index cf191fe05..1ec0d480a 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/callbacks_mixed_bind_c_f90.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/callbacks_mixed_bind_c_f90.pyi @@ -22,3 +22,5 @@ def adapted_apply( callback: adapted_callback, value: Float64 ) -> Float64: ... + +__all__ = ["direct_callback", "adapted_callback", "direct_apply", "adapted_apply"] diff --git a/tests/fortran/callbacks/end_to_end/test_array_callbacks.py b/tests/fortran/callbacks/end_to_end/test_array_callbacks.py index ac7ee8677..fd1147177 100644 --- a/tests/fortran/callbacks/end_to_end/test_array_callbacks.py +++ b/tests/fortran/callbacks/end_to_end/test_array_callbacks.py @@ -5,7 +5,10 @@ import numpy as np import pytest -from tests.fortran._support.wrapper_build import _build_source_or_generated_pyi_and_import +from tests.fortran._support.wrapper_build import ( + _build_source_and_import, + _build_source_or_generated_pyi_and_import, +) FIXTURES = Path(__file__).parent / "fixtures" CALLBACK_ARRAY_F90_SOURCE = FIXTURES / "native" / "fcallback_array_f90.f90" @@ -40,3 +43,87 @@ def test_immediate_dummy_procedure_converts_array_arguments_and_results( ) assert result is None np.testing.assert_array_equal(transformed, np.array([2.0, 4.0, 6.0], dtype=np.float64)) + + +def test_assumed_shape_callback_arrays_cross_the_boundary_as_contiguous_copies( + pyi_parity_build_mode: str, + tmp_path: Path, +): + """An assumed-shape callback dummy carries its extent from the native descriptor.""" + module = _build_source_or_generated_pyi_and_import( + CALLBACK_ARRAY_F90_SOURCE, + tmp_path, + { + "bind_c_fcallback_array_f90_wrapper.f90", + "fcallback_array_f90_wrapper.c", + "fcallback_array_f90_wrapper.h", + }, + CONTRACT_FIXTURES / "fcallback_array_f90", + pyi_parity_build_mode, + ) + values = np.asfortranarray(np.array([1.5, 2.5, 3.5, 4.5], dtype=np.float64)) + doubled = np.zeros(4, dtype=np.float64) + seen = [] + + def double(data, output): + seen.append(np.array(data)) + output[...] = data * 2.0 + + assert module.apply_assumed_shape(double, values, doubled) is None + np.testing.assert_array_equal(seen[0], values) + np.testing.assert_array_equal(doubled, values * 2.0) + + +MATRIX_SOURCE = """ +module fcallback_matrix_f90 + implicit none + + abstract interface + subroutine matrix_callback(input, output) + real(8), intent(in) :: input(:,:) + real(8), intent(out) :: output(:,:) + end subroutine matrix_callback + end interface + +contains + subroutine apply_matrix(callback, input, output) + procedure(matrix_callback) :: callback + real(8), intent(in) :: input(:,:) + real(8), intent(out) :: output(:,:) + + call callback(input, output) + end subroutine apply_matrix +end module fcallback_matrix_f90 +""" + + +def test_rank_two_assumed_shape_callback_arrays_cross_both_directions(tmp_path: Path): + """Every axis of a multidimensional assumed-shape dummy must survive. + + A rank-one lowering can look correct while dropping later axes, so this + checks the extents the callable observes and the data written back. + """ + source = tmp_path / "fcallback_matrix_f90.f90" + source.write_text(MATRIX_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_matrix_f90_wrapper.f90", + "fcallback_matrix_f90_wrapper.c", + "fcallback_matrix_f90_wrapper.h", + }, + ) + incoming = np.asfortranarray(np.arange(6, dtype=np.float64).reshape(2, 3)) + written = np.asfortranarray(np.zeros((2, 3), dtype=np.float64)) + observed = {} + + def double(input_values, output_values): + observed["shape"] = input_values.shape + observed["values"] = np.array(input_values) + output_values[...] = input_values * 2.0 + + assert module.apply_matrix(double, incoming, written) is None + assert observed["shape"] == (2, 3) + np.testing.assert_array_equal(observed["values"], incoming) + np.testing.assert_array_equal(written, incoming * 2.0) diff --git a/tests/fortran/callbacks/end_to_end/test_callback_scalar_storage.py b/tests/fortran/callbacks/end_to_end/test_callback_scalar_storage.py new file mode 100644 index 000000000..42719594f --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/test_callback_scalar_storage.py @@ -0,0 +1,288 @@ +"""Rank-zero callback storage: writable scalar dummies reach native memory.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_generated_pyi_and_import, + _build_inline_pyi_contract_module, + _build_source_and_import, +) + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = """ +module fcallback_scalar_storage_f90 + implicit none + + abstract interface + subroutine directions_callback(read_value, update_value, write_value) + real(8), intent(in) :: read_value + real(8), intent(inout) :: update_value + real(8), intent(out) :: write_value + end subroutine directions_callback + end interface + +contains + subroutine apply_directions(callback, read_value, update_value, write_value) + procedure(directions_callback) :: callback + real(8), intent(in) :: read_value + real(8), intent(inout) :: update_value + real(8), intent(out) :: write_value + + call callback(read_value, update_value, write_value) + end subroutine apply_directions +end module fcallback_scalar_storage_f90 +""" + +CONTRACT = """ +from prik.contracts import Addr, Arg, Float64, In, InOut, Out, Return, Returns, native_call, prototype + +@prototype +def directions_callback( + read_value: In(Float64[()]), + update_value: InOut(Float64[()]), + write_value: Out(Float64[()]) +) -> None: ... + +@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2)), Return('write_value', 1)]) +def apply_directions( + callback: directions_callback, + read_value: Float64, + update_value: Float64 +) -> tuple[Returns["update_value", Float64], Float64]: ... +""" + + +def test_rank_zero_callback_storage_writes_through_to_the_native_caller(tmp_path: Path): + """A rank-zero storage dummy exposes native memory with direction-correct access. + + The default `Addr(T)` spelling hands Python an independent value, so a + contract that needs an `out` or `inout` callback dummy to reach the native + caller asks for storage instead. + """ + module, _result = _build_inline_pyi_contract_module( + tmp_path, + module_name="fcallback_scalar_storage_f90", + source_text=SOURCE, + contract_text=CONTRACT, + ) + observed = {} + + def callback(read_value, update_value, write_value): + observed["read_writeable"] = read_value.flags.writeable + observed["update_writeable"] = update_value.flags.writeable + observed["write_writeable"] = write_value.flags.writeable + observed["read"] = float(read_value) + observed["update_in"] = float(update_value) + update_value[...] = float(update_value) * 10.0 + write_value[...] = float(read_value) + float(update_value) + + updated, written = module.apply_directions(callback, np.float64(3.0), np.float64(4.0)) + + assert observed == { + "read_writeable": False, + "update_writeable": True, + "write_writeable": True, + "read": 3.0, + "update_in": 4.0, + } + assert updated == np.float64(40.0) + assert written == np.float64(43.0) + + +SOURCE_DEFAULT = """ +module fcallback_default_storage_f90 + implicit none + + abstract interface + subroutine objective_callback(x, f) + real(8), intent(in) :: x(:) + real(8), intent(out) :: f + end subroutine objective_callback + end interface + +contains + subroutine evaluate(calfun, x, total) + procedure(objective_callback) :: calfun + real(8), intent(in) :: x(:) + real(8), intent(out) :: total + + call calfun(x, total) + end subroutine evaluate +end module fcallback_default_storage_f90 +""" + + +def test_out_scalar_callback_writes_back_without_editing_the_contract(tmp_path: Path): + """Wrapping Fortran source directly produces a callback that can answer. + + The generated default must be the spelling that works: an `intent(out)` + scalar reaches Python as writable storage, so the value the callable + computes reaches the native caller with no contract edit. + """ + source = tmp_path / "fcallback_default_storage_f90.f90" + source.write_text(SOURCE_DEFAULT, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_default_storage_f90_wrapper.f90", + "fcallback_default_storage_f90_wrapper.c", + "fcallback_default_storage_f90_wrapper.h", + }, + ) + + def objective(x): + return float(np.sum(x * x)) + + def objective_prik(x, f): + f[...] = objective(x) + + assert module.evaluate(objective_prik, np.array([1.0, 2.0, 3.0])) == np.float64(14.0) + + +def test_callback_docstring_states_the_callable_signature_and_write_through(tmp_path: Path): + """The docstring is the only callback description in the source-only workflow. + + Guessing a callback signature wrong is fatal at the callback boundary, so + `help()` must state the arity, direction, and how an output is delivered. + """ + source = tmp_path / "fcallback_default_storage_f90.f90" + source.write_text(SOURCE_DEFAULT, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_default_storage_f90_wrapper.f90", + "fcallback_default_storage_f90_wrapper.c", + "fcallback_default_storage_f90_wrapper.h", + }, + ) + documentation = module.evaluate.__doc__ + + assert "Called as: calfun(x, f) -> None" in documentation + assert "x : ndarray[float64], rank 1, shape (::), intent(in)" in documentation + assert "f : ndarray[float64], intent(out); assign through it (f[...] = value)" in documentation + assert "An exception or an invalid return value terminates the process." in documentation + + +SOURCE_UNDECLARED = """ +module fcallback_undeclared_intent_f90 + implicit none + + abstract interface + subroutine tweak_callback(value) + real(8) :: value + end subroutine tweak_callback + end interface + +contains + subroutine drive(callback, seed, result) + procedure(tweak_callback) :: callback + real(8), intent(in) :: seed + real(8), intent(out) :: result + + result = seed + call callback(result) + end subroutine drive +end module fcallback_undeclared_intent_f90 +""" + + +def _undeclared_intent_module(tmp_path: Path): + source = tmp_path / "fcallback_undeclared_intent_f90.f90" + source.write_text(SOURCE_UNDECLARED, encoding="utf-8") + return _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_undeclared_intent_f90_wrapper.f90", + "fcallback_undeclared_intent_f90_wrapper.c", + "fcallback_undeclared_intent_f90_wrapper.h", + }, + ) + + +def test_callback_scalar_without_declared_intent_is_read_and_written(tmp_path: Path): + """An undeclared ``intent`` is conservatively both read and written. + + Fortran permits the callee to modify such a dummy, so the callable must + observe the incoming value and see its own write reach the native caller. + """ + module = _undeclared_intent_module(tmp_path) + observed = [] + + def tweak(value): + observed.append(float(value)) + assert value.flags.writeable + value[...] = float(value) * 3.0 + + assert module.drive(tweak, np.float64(7.0)) == np.float64(21.0) + assert observed == [7.0] + + +def test_undeclared_intent_survives_the_generated_contract_round_trip(tmp_path: Path): + """The absent ``intent`` must survive source, contract, codegen and runtime. + + Building through PRIK's own generated contract proves the bare + ``Float64[()]`` spelling carries the conservative read/write transfer all + the way to the trampoline, rather than only appearing in the contract text. + """ + source = tmp_path / "fcallback_undeclared_intent_f90.f90" + source.write_text(SOURCE_UNDECLARED, encoding="utf-8") + workdir = tmp_path / "round_trip" + module = _build_generated_pyi_and_import(source, workdir) + + contract = (workdir / "contracts" / source.stem / f"{source.stem}.pyi").read_text(encoding="utf-8") + assert "value: Float64[()]" in contract + assert "In(" not in contract and "Out(" not in contract and "InOut(" not in contract + + bridge = next((workdir / "pyi_build").glob("bind_c_*_wrapper.f90")).read_text(encoding="utf-8") + assert "real(c_double) :: value" in bridge + assert not any(f"intent({direction}) :: value" in bridge for direction in ("in", "out", "inout")) + assert "value = value_callback_storage" in bridge + + def tweak(value): + value[...] = float(value) * 3.0 + + assert module.drive(tweak, np.float64(7.0)) == np.float64(21.0) + + +def test_assume_intent_in_scalars_makes_an_undeclared_callback_scalar_input_only(tmp_path: Path): + """The flag narrows the default without declaring a direction. + + The contract still carries no direction wrapper, because the source still + declares none; only the projection and the copy direction change. + """ + source = tmp_path / "fcallback_undeclared_intent_f90.f90" + source.write_text(SOURCE_UNDECLARED, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_undeclared_intent_f90_wrapper.f90", + "fcallback_undeclared_intent_f90_wrapper.c", + "fcallback_undeclared_intent_f90_wrapper.h", + }, + assume_intent_in_scalars=True, + ) + contract = (tmp_path / "build" / "contracts" / "fcallback_undeclared_intent_f90.pyi").read_text(encoding="utf-8") + assert "value: Addr(Float64)" in contract + assert "In(" not in contract and "Out(" not in contract and "InOut(" not in contract + + bridge = (tmp_path / "build" / "bind_c_fcallback_undeclared_intent_f90_wrapper.f90").read_text(encoding="utf-8") + assert "real(c_double) :: value" in bridge + assert not any(f"intent({direction}) :: value" in bridge for direction in ("in", "out", "inout")) + # Input-only: nothing is copied back out of the call-local storage. + assert "value = value_callback_storage" not in bridge + + observed = [] + + def tweak(value): + observed.append(float(value)) + + assert module.drive(tweak, np.float64(7.0)) == np.float64(7.0) + assert observed == [7.0] diff --git a/tests/fortran/callbacks/end_to_end/test_direct_bind_c_callback_storage.py b/tests/fortran/callbacks/end_to_end/test_direct_bind_c_callback_storage.py new file mode 100644 index 000000000..7b706b257 --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/test_direct_bind_c_callback_storage.py @@ -0,0 +1,90 @@ +"""Writable scalar callback storage on the bridge-free direct `bind(C)` route.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = """ +module fcallback_direct_storage_f90 + use iso_c_binding + implicit none + + abstract interface + subroutine update_callback(value) bind(C) + import :: c_double + real(c_double), intent(inout) :: value + end subroutine update_callback + + subroutine emit_callback(value) bind(C) + import :: c_double + real(c_double), intent(out) :: value + end subroutine emit_callback + end interface + +contains + real(c_double) function drive_update(callback, seed) bind(C) result(output) + procedure(update_callback) :: callback + real(c_double), value, intent(in) :: seed + + output = seed + call callback(output) + end function drive_update + + real(c_double) function drive_emit(callback) bind(C) result(output) + procedure(emit_callback) :: callback + + call callback(output) + end function drive_emit +end module fcallback_direct_storage_f90 +""" + + +def _direct_module(tmp_path: Path): + source = tmp_path / "fcallback_direct_storage_f90.f90" + source.write_text(SOURCE, encoding="utf-8") + # A bind(C) entry point needs no generated Fortran adapter, so the expected + # source set is exactly the binding pair. + return _build_source_and_import( + source, + tmp_path / "build", + { + "fcallback_direct_storage_f90_wrapper.c", + "fcallback_direct_storage_f90_wrapper.h", + }, + ) + + +def test_direct_bind_c_callbacks_receive_writable_rank_zero_storage(tmp_path: Path): + """The projection must work where no Fortran bridge exists at all. + + A direct entry point calls the trampoline as a plain C function pointer, so + writable storage has to be the binding's doing rather than an adapter's. + """ + module = _direct_module(tmp_path) + observed = {} + + def update(value): + observed["writeable"] = value.flags.writeable + observed["incoming"] = float(value) + value[...] *= 2 + + def emit(value): + observed["emit_writeable"] = value.flags.writeable + value[...] = 42.0 + + assert module.drive_update(update, np.float64(5.0)) == np.float64(10.0) + assert module.drive_emit(emit) == np.float64(42.0) + assert observed == {"writeable": True, "incoming": 5.0, "emit_writeable": True} + + +def test_direct_bind_c_callback_storage_adds_no_fortran_bridge(tmp_path: Path): + """Scalar callback storage must not drag a bridge onto the direct route.""" + _direct_module(tmp_path) + generated = {path.name for path in (tmp_path / "build").glob("*_wrapper.f90")} + + assert generated == set() diff --git a/tests/fortran/callbacks/end_to_end/test_multi_file_contract_generation.py b/tests/fortran/callbacks/end_to_end/test_multi_file_contract_generation.py new file mode 100644 index 000000000..4316c819b --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/test_multi_file_contract_generation.py @@ -0,0 +1,365 @@ +"""Multi-file `generate --pyi`: imported interfaces reach a buildable contract.""" + +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _compiler, _import_from_build_dir +from prik import build_pyi_extension +from prik.pipeline.pyi import pyi_text_to_semantic_module +from prik.semantics.native_contract import native_contract_issues + +pytestmark = pytest.mark.fortran_end_to_end + +PINTRF_SOURCE = """ +module pintrf_mod + implicit none + private + public :: OBJ + + abstract interface + subroutine OBJ(x, f) + implicit none + real(8), intent(in) :: x + real(8), intent(out) :: f + end subroutine OBJ + end interface +end module pintrf_mod +""" + +SOLVER_SOURCE = """ +module solver_mod + use, non_intrinsic :: pintrf_mod, only : OBJ + implicit none +contains + subroutine minimize(calfun, x, f) + procedure(OBJ) :: calfun + real(8), intent(in) :: x + real(8), intent(out) :: f + + call calfun(x, f) + end subroutine minimize +end module solver_mod +""" + +RENAMED_SOURCE = """ +module renamed_mod + use, non_intrinsic :: pintrf_mod, only : LOCAL_OBJ => OBJ + implicit none +contains + subroutine minimize_renamed(calfun, x, f) + procedure(LOCAL_OBJ) :: calfun + real(8), intent(in) :: x + real(8), intent(out) :: f + + call calfun(x, f) + end subroutine minimize_renamed +end module renamed_mod + +module scoped_rename_mod + implicit none +contains + subroutine minimize_scoped(calfun, x, f) + use, non_intrinsic :: pintrf_mod, only : SCOPED_OBJ => OBJ + implicit none + procedure(SCOPED_OBJ) :: calfun + real(8), intent(in) :: x + real(8), intent(out) :: f + + call calfun(x, f) + end subroutine minimize_scoped +end module scoped_rename_mod +""" + + +def _generate_contracts(tmp_path: Path) -> tuple[Path, list[Path]]: + sources = [] + for name, text in ( + ("pintrf.f90", PINTRF_SOURCE), + ("solver.f90", SOLVER_SOURCE), + ("renamed.f90", RENAMED_SOURCE), + ): + path = tmp_path / name + path.write_text(text, encoding="utf-8") + sources.append(path) + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + *[str(path) for path in sources], + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + return contracts, sources + + +def test_multi_file_generation_places_the_prototype_with_its_declaring_module(tmp_path: Path): + """Each module's contract records what that module declares or imports. + + The per-file CLI conversion path is where an imported interface previously + degraded to an opaque placeholder, so this exercises that workflow rather + than whole-project conversion. + """ + contracts, _sources = _generate_contracts(tmp_path) + + declaring = (contracts / "pintrf_mod.pyi").read_text(encoding="utf-8") + assert "@prototype\ndef OBJ(" in declaring + + consuming = (contracts / "solver_mod.pyi").read_text(encoding="utf-8") + assert "from .pintrf_mod import OBJ" in consuming + assert "calfun: OBJ" in consuming + + renamed = (contracts / "renamed_mod.pyi").read_text(encoding="utf-8") + assert "from .pintrf_mod import OBJ as LOCAL_OBJ" in renamed + assert "calfun: LOCAL_OBJ" in renamed + + # A procedure-local rename reaches the contract through the synthetic + # prototype import rather than the module's own import list. + scoped = (contracts / "scoped_rename_mod.pyi").read_text(encoding="utf-8") + assert "from .pintrf_mod import OBJ as SCOPED_OBJ" in scoped + assert "calfun: SCOPED_OBJ" in scoped + assert "import SCOPED_OBJ" not in scoped.replace("OBJ as SCOPED_OBJ", "") + + +def test_generated_multi_file_contracts_parse_without_native_contract_issues(tmp_path: Path): + """PRIK must be able to read back every contract it just wrote.""" + contracts, _sources = _generate_contracts(tmp_path) + + for contract in sorted(contracts.glob("*.pyi")): + if contract.name == "__init__.pyi": + continue + module = pyi_text_to_semantic_module(contract.read_text(encoding="utf-8"), module_name=contract.stem) + assert native_contract_issues(module) == [], contract.name + + +def test_building_from_generated_multi_file_contracts_runs_the_callback(tmp_path: Path): + """The whole route must survive: source, contract, parse, build, call.""" + contracts, sources = _generate_contracts(tmp_path) + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(path) for path in sources], + output_dir=tmp_path / "build", + output_name="multi_file_callbacks", + ) + module = _import_from_build_dir(result.module_name, result.output_dir) + + def objective(x, f): + f[...] = float(x) ** 2 + + assert module.solver_mod.minimize(objective, np.float64(3.0)) == np.float64(9.0) + assert module.renamed_mod.minimize_renamed(objective, np.float64(4.0)) == np.float64(16.0) + assert module.scoped_rename_mod.minimize_scoped(objective, np.float64(5.0)) == np.float64(25.0) + + +CALLBACK_RESULT_TYPES_SOURCE = """ +module cbresult_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + function make_point(x) result(p) + import :: point_t + implicit none + real(8), intent(in) :: x + type(point_t) :: p + end function make_point + end interface +end module cbresult_types +""" + +CALLBACK_RESULT_CONSUMER_SOURCE = """ +module cbresult_consumer + use, non_intrinsic :: cbresult_types, only : make_point, point_t + implicit none +contains + subroutine run(f, seed, out_x) + procedure(make_point) :: f + real(8), intent(in) :: seed + real(8), intent(out) :: out_x + type(point_t) :: made + + made = f(seed) + out_x = made%x + end subroutine run +end module cbresult_consumer +""" + + +def test_imported_callback_returning_a_module_owned_type_builds(tmp_path: Path): + """A callback result type belongs to the module that declares the interface. + + Attributing it to the consuming module produced an identity no wrapper + definition could satisfy, so the build failed outright. The generated + contract must name the declaring module and the extension must build. + + The built extension is not called here: resolving a cross-module derived + type through the runtime namespace is a separate, pre-existing gap that + also affects ordinary functions returning an imported type. + """ + sources = [] + for name, text in ( + ("cbresult_types.f90", CALLBACK_RESULT_TYPES_SOURCE), + ("cbresult_consumer.f90", CALLBACK_RESULT_CONSUMER_SOURCE), + ): + path = tmp_path / name + path.write_text(text, encoding="utf-8") + sources.append(path) + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + *[str(path) for path in sources], + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + + declaring = (contracts / "cbresult_types.pyi").read_text(encoding="utf-8") + assert "def make_point(" in declaring + assert "-> Point_T: ..." in declaring + + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(path) for path in sources], + output_dir=tmp_path / "build", + output_name="callback_result_types", + ) + assert result.shared_library.exists() + + +RENAMED_CHAIN_SOURCE = """ +module chain_declares_mod + implicit none + abstract interface + subroutine OBJ(x, f) + implicit none + real(8), intent(in) :: x + real(8), intent(out) :: f + end subroutine OBJ + end interface +end module chain_declares_mod + +module chain_middle_mod + use, non_intrinsic :: chain_declares_mod, only : MID => OBJ + implicit none + public :: MID +end module chain_middle_mod + +module chain_consumer_mod + use, non_intrinsic :: chain_middle_mod, only : LOCAL => MID + implicit none +contains + subroutine run_chain(calfun, x, f) + procedure(LOCAL) :: calfun + real(8), intent(in) :: x + real(8), intent(out) :: f + + call calfun(x, f) + end subroutine run_chain +end module chain_consumer_mod +""" + + +def test_renamed_reexport_chain_builds_through_its_generated_contracts(tmp_path: Path): + """Each hop renames the interface, so only the declaring module names it. + + A rename and a re-export are covered separately elsewhere; combining them + is what exposes a reference that followed the module back to the declaration + while keeping an alias from somewhere along the way. + """ + source = tmp_path / "chain.f90" + source.write_text(RENAMED_CHAIN_SOURCE, encoding="utf-8") + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + str(source), + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + + # Each contract binds the interface where it is declared, under the name + # its own module calls it, however many modules it passed through. + assert "from .chain_declares_mod import OBJ as MID" in (contracts / "chain_middle_mod.pyi").read_text( + encoding="utf-8" + ) + consuming = (contracts / "chain_consumer_mod.pyi").read_text(encoding="utf-8") + assert "from .chain_declares_mod import OBJ as LOCAL" in consuming + assert "calfun: LOCAL" in consuming + + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(source)], + output_dir=tmp_path / "build", + output_name="renamed_chain_callbacks", + ) + module = _import_from_build_dir(result.module_name, result.output_dir) + + def objective(x, f): + f[...] = float(x) * 7.0 + + assert module.chain_consumer_mod.run_chain(objective, np.float64(6.0)) == np.float64(42.0) + + +def test_renamed_reexport_chain_builds_directly_from_its_fortran_source(tmp_path: Path): + """Publishing an imported interface adds no runtime name to alias. + + A module publishing an imported prototype states where a callback signature + comes from, and a signature is not an object Python holds. Binding one at + runtime reaches for an attribute of a module that exports nothing at all, + so the chain has to reach the build through prototype resolution alone. + """ + from tests.fortran._support.wrapper_build import _build_source_and_import + + source = tmp_path / "chain.f90" + source.write_text(RENAMED_CHAIN_SOURCE, encoding="utf-8") + + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_chain_wrapper.f90", "chain_wrapper.c", "chain_wrapper.h"}, + ) + + def calfun(x, f): + f[()] = x * 3.0 + + assert module.run_chain(calfun, np.float64(4.0)) == pytest.approx(12.0) + # The consuming module is the only namespace with a runtime name, so the + # declaring and publishing modules contributed nothing to alias. + extension = sys.modules[module.__name__.split(".", 1)[0]] + assert not hasattr(extension, "chain_declares_mod") + assert not hasattr(extension, "chain_middle_mod") diff --git a/tests/fortran/callbacks/end_to_end/test_optional_callbacks.py b/tests/fortran/callbacks/end_to_end/test_optional_callbacks.py new file mode 100644 index 000000000..df272c82c --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/test_optional_callbacks.py @@ -0,0 +1,176 @@ +"""Optional callback presence across source and generated-contract builds.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_or_generated_pyi_and_import + + +pytestmark = pytest.mark.fortran_end_to_end + + +SOURCE = """ +module fcallback_optional_f90 + use iso_c_binding + implicit none + + abstract interface + subroutine report(value, status, values, terminate) + integer, intent(in) :: value + integer, intent(in), optional :: status + real(8), intent(in), optional :: values(:) + logical, intent(out), optional :: terminate + end subroutine report + + subroutine c_report(value, status) bind(C) + import c_int + integer(c_int), value, intent(in) :: value + integer(c_int), intent(in), optional :: status + end subroutine c_report + end interface + +contains + + integer function run(mode, callback) result(output) + integer, intent(in) :: mode + procedure(report), optional :: callback + integer :: status + real(8) :: values(2) + logical :: terminate + + output = -1 + if (.not. present(callback)) return + output = mode + select case (mode) + case (0) + call callback(4) + case (1) + status = 9 + call callback(4, status) + case (2) + status = 9 + values = [1.5d0, 2.5d0] + call callback(4, status, values) + case (3) + terminate = .false. + call callback(4, terminate=terminate) + if (terminate) output = 99 + end select + end function run + + integer(c_int) function direct_run(mode, callback) bind(C) result(output) + integer(c_int), value, intent(in) :: mode + procedure(c_report), optional :: callback + integer(c_int) :: status + + output = -1_c_int + if (.not. present(callback)) return + output = mode + if (mode == 0_c_int) then + call callback(4_c_int) + else + status = 9_c_int + call callback(4_c_int, status) + end if + end function direct_run +end module fcallback_optional_f90 +""" + + +@pytest.fixture +def optional_callback_module(pyi_parity_build_mode: str, tmp_path: Path): + source = tmp_path / "fcallback_optional_f90.f90" + source.write_text(SOURCE, encoding="utf-8") + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "bind_c_fcallback_optional_f90_wrapper.f90", + "fcallback_optional_f90_wrapper.c", + "fcallback_optional_f90_wrapper.h", + }, + None, + pyi_parity_build_mode, + ) + build_dir = ( + tmp_path / "source_build" + if pyi_parity_build_mode == "source" + else tmp_path / "generated_pyi_build" / "pyi_build" + ) + return module, build_dir, pyi_parity_build_mode + + +def test_optional_callback_and_optional_dummies_preserve_each_presence_state(optional_callback_module): + module, _build_dir, _build_mode = optional_callback_module + + assert module.run(np.int32(0)) == np.int32(-1) + assert module.run(np.int32(0), None) == np.int32(-1) + + observed = [] + + def report(value, status, values, terminate): + observed.append((value, status, None if values is None else np.array(values), terminate)) + if terminate is not None: + terminate[...] = True + + assert module.run(np.int32(0), report) == np.int32(0) + assert observed[-1] == (np.int32(4), None, None, None) + + assert module.run(np.int32(1), report) == np.int32(1) + assert observed[-1] == (np.int32(4), np.int32(9), None, None) + + assert module.run(np.int32(2), report) == np.int32(2) + value, status, values, terminate = observed[-1] + assert (value, status, terminate) == (np.int32(4), np.int32(9), None) + np.testing.assert_array_equal(values, np.array([1.5, 2.5], dtype=np.float64)) + + assert module.run(np.int32(3), report) == np.int32(99) + assert observed[-1][1:3] == (None, None) + assert isinstance(observed[-1][3], np.ndarray) + assert observed[-1][3].shape == () + + +def test_optional_bind_c_callback_remains_direct_and_preserves_inner_presence(optional_callback_module): + module, build_dir, build_mode = optional_callback_module + + assert module.direct_run(np.int32(0)) == np.int32(-1) + assert module.direct_run(np.int32(0), None) == np.int32(-1) + seen = [] + assert module.direct_run(np.int32(0), lambda value, status: seen.append((value, status))) == np.int32(0) + assert module.direct_run(np.int32(1), lambda value, status: seen.append((value, status))) == np.int32(1) + assert seen == [(np.int32(4), None), (np.int32(4), np.int32(9))] + + if build_mode == "source": + binding = (build_dir / "fcallback_optional_f90_wrapper.c").read_text(encoding="utf-8") + bridge = (build_dir / "bind_c_fcallback_optional_f90_wrapper.f90").read_text(encoding="utf-8") + assert "direct_run(int32_t mode, void (*callback)(int32_t, void *));" in binding + assert "direct_run(bound_mode, bound_callback_obj != Py_None ? prik_callback_trampoline_" in binding + assert "function bind_c_direct_run" not in bridge.casefold() + + +def test_exception_propagation_is_unchanged_for_a_supplied_optional_callback(optional_callback_module): + _module, build_dir, _build_mode = optional_callback_module + result = subprocess.run( + [ + sys.executable, + "-c", + ( + "import numpy as np; import fcallback_optional_f90 as root; " + "module = root.fcallback_optional_f90; " + "module.run(np.int32(0), lambda *args: (_ for _ in ()).throw(ValueError('optional exploded')))" + ), + ], + cwd=build_dir, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert "ValueError: optional exploded" in result.stderr diff --git a/tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py b/tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py index 361754856..b1173f4eb 100644 --- a/tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py +++ b/tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py @@ -63,11 +63,11 @@ def string_callback(read_label, write_label, update_label): assert module.apply_string_storage_callback(string_callback, "OLD ") == ("UPDATED!", "WRITTEN!") - point = module.point_t(x=np.float64(2.0), y=np.float64(5.0)) - shifted = module.point_t() + point = module.Point_T(x=np.float64(2.0), y=np.float64(5.0)) + shifted = module.Point_T() assert ( module.apply_point_callback( - lambda value: module.point_t(x=value.x + 1.0, y=value.y * 2.0), + lambda value: module.Point_T(x=value.x + 1.0, y=value.y * 2.0), point, shifted, ) diff --git a/tests/fortran/callbacks/policy/test_callback_policy.py b/tests/fortran/callbacks/policy/test_callback_policy.py index b069f2936..d0b874c65 100644 --- a/tests/fortran/callbacks/policy/test_callback_policy.py +++ b/tests/fortran/callbacks/policy/test_callback_policy.py @@ -11,8 +11,10 @@ RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA, ) from prik.policy.completion import complete_semantic_policies +from prik.policy.ownership import PythonBarrierAction from prik.policy.models import ( CallbackABIKind, + CallbackOptionalityAction, CallbackTransferAction, FunctionWrapperPolicy, ) @@ -21,10 +23,10 @@ FIXTURES = Path(__file__).parents[1] / "end_to_end" / "fixtures" -def _source_semantic_module(filename: str, *, module_name: str): +def _source_semantic_module(filename: str, *, module_name: str, assume_intent_in_scalars: bool = False): source = FIXTURES / "native" / filename parsed = parse_fortran_project({str(source): _fortran_source_for_pipeline(source, PreprocessingConfig())}) - modules = fortran_project_to_semantic_modules(parsed) + modules = fortran_project_to_semantic_modules(parsed, assume_intent_in_scalars=assume_intent_in_scalars) _apply_source_python_exports(modules) module = _merge_wrapper_modules(modules, name=module_name) complete_semantic_policies(module) @@ -58,7 +60,8 @@ def test_source_callback_value_default_and_explicit_reference_are_completed(): ), ( "def callback_shape(value: Float64 = ...) -> None: ...", - "callback argument 'value' cannot be optional", + "callback argument 'value' cannot be both optional and passed by value; " + "use a reference dummy so absence has a null-pointer ABI", ), ( "def callback_shape() -> Pointer[Float64]: ...", @@ -66,7 +69,7 @@ def test_source_callback_value_default_and_explicit_reference_are_completed(): ), ], ) -def test_callback_descriptor_and_optional_forms_are_blocked_before_codegen(prototype: str, blocker: str): +def test_unsupported_callback_forms_are_blocked_before_codegen(prototype: str, blocker: str): module = parse_pyi_text( f""" @prototype @@ -83,3 +86,242 @@ def apply(callback: callback_shape) -> None: ... assert isinstance(policy, FunctionWrapperPolicy) assert policy.supported is False assert blocker in policy.blockers + + +def test_procedure_interface_from_an_unsupplied_module_is_blocked_by_name(): + """A named interface no input declares is reported against that name. + + Without the module that declares it the dummy has no signature, so the + diagnostic must name the interface the declaration asked for rather than + the opaque placeholder type it fell back to. + """ + source = """ +module solver_mod + use, non_intrinsic :: pintrf_mod, only : OBJ + implicit none +contains + subroutine minimize(calfun, x) + procedure(OBJ) :: calfun + real(8), intent(in) :: x + end subroutine minimize +end module solver_mod +""" + parsed = parse_fortran_project({"solver.f90": source}) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name="solver_mod") + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert policy.supported is False + assert ( + "argument 'calfun' declares procedure interface 'OBJ', which no supplied source declares; " + "add the module that declares it to the build inputs" in policy.blockers + ) + + +def test_writable_callback_scalars_use_rank_zero_storage_without_synthesizing_intent(): + """Every dummy the callee may write is projected as writable storage. + + Python has no writable scalar, so a dummy the native caller reads back must + reach the callable as rank-zero storage. An undeclared ``intent`` is + conservatively writable because Fortran permits the callee to modify it, + and the declaration keeps no intent of its own either way. + """ + module = _source_semantic_module("fcallback_all_f90.f90", module_name="fcallback_all_f90") + function = next(item for item in module.functions if item.name == "apply_scalar_storage_callback") + policy = completed_function_wrapper_policy(function) + transfers = policy.arguments[0].callback.arguments + + assert [transfer.intent for transfer in transfers] == ["inout", "out", None] + assert [transfer.python_action for transfer in transfers] == [PythonBarrierAction.SCALAR_STORAGE] * 3 + assert [transfer.adapter_action for transfer in transfers] == [ + CallbackTransferAction.COPY_IN_OUT, + CallbackTransferAction.COPY_OUT, + CallbackTransferAction.COPY_IN_OUT, + ] + assert policy.supported is True + + +def test_assume_intent_in_scalars_elects_the_input_only_default_for_an_undeclared_intent(): + """The flag chooses which default an undeclared ``intent`` receives. + + It narrows the conservative read/write default to input-only; it does not + give the dummy a declared direction, so the contract still carries none. + """ + module = _source_semantic_module( + "fcallback_all_f90.f90", + module_name="fcallback_all_f90", + assume_intent_in_scalars=True, + ) + function = next(item for item in module.functions if item.name == "apply_scalar_storage_callback") + transfers = completed_function_wrapper_policy(function).arguments[0].callback.arguments + + undeclared = transfers[2] + assert undeclared.intent is None + assert undeclared.python_action is PythonBarrierAction.SCALAR_VALUE + assert undeclared.adapter_action is CallbackTransferAction.COPY_IN + + +@pytest.mark.parametrize( + ("prototype", "blocker"), + [ + ( + "def callback_shape(value: Out(Addr(Float64))) -> None: ...", + "callback argument 'value' is intent(out) and cannot use the value spelling " + "Addr(Float64); use Float64[()] for writable storage", + ), + ( + "def callback_shape(value: InOut(Addr(Int32))) -> None: ...", + "callback argument 'value' is intent(inout) and cannot use the value spelling " + "Addr(Int32); use Int32[()] for writable storage", + ), + ], +) +def test_value_spelling_is_blocked_for_written_back_callback_scalars(prototype: str, blocker: str): + """An out or inout dummy spelled as a value would silently discard the write.""" + module = parse_pyi_text( + f""" +@prototype +{prototype} + +def apply(callback: callback_shape) -> None: ... +""", + module_name="discarded_callback_writeback", + ) + + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert policy.supported is False + assert blocker in policy.blockers + + +def test_read_only_callback_scalars_keep_the_value_spelling(): + """An in dummy is never read back, so the value projection stays valid.""" + module = parse_pyi_text( + """ +@prototype +def callback_shape(value: In(Addr(Float64))) -> None: ... + +def apply(callback: callback_shape) -> None: ... +""", + module_name="read_only_callback_scalar", + ) + + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert policy.supported is True + assert policy.arguments[0].callback.arguments[0].python_action is PythonBarrierAction.SCALAR_VALUE + + +def test_optional_reference_callback_dummy_has_one_null_pointer_presence_decision(): + module = parse_pyi_text( + """ +@prototype +def callback_shape(value: In(Addr(Int32)) = ...) -> None: ... + +def apply(callback: callback_shape = ...) -> None: ... +""", + module_name="optional_callback", + ) + + complete_semantic_policies(module) + policy = completed_function_wrapper_policy(module.functions[0]) + + argument = policy.arguments[0] + assert argument.optional is True + assert argument.callback.arguments[0].optionality is CallbackOptionalityAction.NULL_DATA_POINTER + assert argument.callback.prototype.arguments[0].optional is True + + +def test_imported_interface_keeps_its_declaring_module_in_the_completed_identity(): + """A type an imported interface owns must not be attributed to the consumer. + + The consuming module never imports ``point_t``, so an identity taken from + the consuming scope names a type that module does not define and no wrapper + definition can satisfy it. + """ + sources = { + "callback_types.f90": """ +module callback_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + subroutine move_point(p) + import :: point_t + implicit none + type(point_t), intent(inout) :: p + end subroutine move_point + end interface +end module callback_types +""", + "consumer.f90": """ +module consumer + use callback_types, only : move_point + implicit none +contains + subroutine run(f) + procedure(move_point) :: f + end subroutine run +end module consumer +""", + } + parsed = parse_fortran_project(sources) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name="merged") + complete_semantic_policies(module) + + function = next(item for item in module.functions if item.name == "run") + policy = completed_function_wrapper_policy(function) + + assert policy.supported is True + assert policy.arguments[0].callback.arguments[0].derived_type_identity == ("callback_types", "point_t") + + +def test_imported_interface_result_keeps_its_declaring_module_in_the_completed_identity(): + """A callback result's type identity must name the module that declares it.""" + sources = { + "callback_types.f90": """ +module callback_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + function make_point(x) result(p) + import :: point_t + implicit none + real(8), intent(in) :: x + type(point_t) :: p + end function make_point + end interface +end module callback_types +""", + "consumer.f90": """ +module consumer + use callback_types, only : make_point + implicit none +contains + subroutine run(f) + procedure(make_point) :: f + end subroutine run +end module consumer +""", + } + parsed = parse_fortran_project(sources) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name="merged") + complete_semantic_policies(module) + + policy = completed_function_wrapper_policy(next(item for item in module.functions if item.name == "run")) + + assert policy.supported is True + assert policy.arguments[0].callback.result.transfer.derived_type_identity == ("callback_types", "point_t") diff --git a/tests/fortran/callbacks/semantics/test_callback_route_resolution.py b/tests/fortran/callbacks/semantics/test_callback_route_resolution.py new file mode 100644 index 000000000..ce3572ef3 --- /dev/null +++ b/tests/fortran/callbacks/semantics/test_callback_route_resolution.py @@ -0,0 +1,182 @@ +"""A callback name reached by several `use` routes has to name one interface. + +Callback resolution answers which native declaration a `procedure(...)` names. +Routes are compared by the declaration they reach, so repeating a route is +harmless while two different ones, or one this project never read, leave the +name meaning nothing here. +""" + +from pathlib import Path + +from prik.parsers.fortran import parse_fortran_project +from prik.semantics.fortran2ir import FortranToIRConverter + +HOME = """\ +module known_callbacks + implicit none + abstract interface + subroutine cb(x) + integer, intent(in) :: x + end subroutine cb + end interface +end module known_callbacks + +module relay_mod + use known_callbacks, only : cb + implicit none +end module relay_mod +""" + + +def _visible_callbacks(tmp_path: Path, importer: str, *, module_name: str) -> dict[str, str | None]: + """Return the callback interfaces one module resolves, by declaring module.""" + source = tmp_path / "callbacks.f90" + source.write_text(f"{HOME}\n{importer}", encoding="utf-8") + project = parse_fortran_project([source]) + modules = [module for parsed in (getattr(project, "files", None) or [project]) for module in parsed.modules] + index = FortranToIRConverter._callback_module_index(modules) + importing = next(module for module in modules if module.name == module_name) + resolved = FortranToIRConverter._module_callback_interfaces(index, importing) + return {name: (item.module.name if item.module is not None else None) for name, item in resolved.items()} + + +def test_one_route_resolves_the_callback_it_names(tmp_path: Path): + """A single `use` names one interface, which is what the dummy declares.""" + assert _visible_callbacks( + tmp_path, + """\ +module single_mod + use known_callbacks, only : cb + implicit none +contains + subroutine go(f) + procedure(cb) :: f + end subroutine go +end module single_mod +""", + module_name="single_mod", + ) == {"cb": "known_callbacks"} + + +def test_routes_reaching_one_declaration_resolve_it(tmp_path: Path): + """Importing the same interface twice, directly and through a relay, is one entity.""" + assert _visible_callbacks( + tmp_path, + """\ +module agreeing_mod + use known_callbacks, only : cb + use relay_mod, only : cb + implicit none +contains + subroutine go(f) + procedure(cb) :: f + end subroutine go +end module agreeing_mod +""", + module_name="agreeing_mod", + ) == {"cb": "known_callbacks"} + + +def test_a_readable_route_beside_an_unreadable_one_resolves_nothing(tmp_path: Path): + """An unread module offers whatever it names, which nothing here can compare. + + The re-export graph already refuses to name an entity here, so resolving + the dummy against the readable route would answer a question the rest of + the conversion declined. + """ + assert ( + _visible_callbacks( + tmp_path, + """\ +module competing_mod + use known_callbacks, only : cb + use external_callbacks, only : cb + implicit none +contains + subroutine go(f) + procedure(cb) :: f + end subroutine go +end module competing_mod +""", + module_name="competing_mod", + ) + == {} + ) + + +def test_a_module_keeps_its_own_declaration_over_a_competing_import(tmp_path: Path): + """A module's own interface is what its declarations name, imports aside.""" + assert _visible_callbacks( + tmp_path, + """\ +module owning_mod + use known_callbacks, only : cb + use external_callbacks, only : cb + implicit none + abstract interface + subroutine cb(x) + integer, intent(in) :: x + end subroutine cb + end interface +end module owning_mod +""", + module_name="owning_mod", + ) == {"cb": "owning_mod"} + + +LOCAL_CALLBACKS = """\ +module local_mod + implicit none +contains + subroutine first(f) + abstract interface + subroutine cb(x) + integer, intent(in) :: x + end subroutine cb + end interface + procedure(cb) :: f + call f(1) + end subroutine first + + subroutine second(f) + abstract interface + subroutine cb(x) + real(8), intent(in) :: x + end subroutine cb + end interface + procedure(cb) :: f + call f(1.0d0) + end subroutine second +end module local_mod +""" + + +def test_each_procedure_resolves_the_callback_it_declares(tmp_path: Path): + """Two procedures may name different interfaces the same way. + + A block written inside a procedure belongs to it, and the parser stores + those beside the module's own, so indexing the module's blocks without + regard to scope lets whichever came first answer for both. + """ + source = tmp_path / "local_callbacks.f90" + source.write_text(LOCAL_CALLBACKS, encoding="utf-8") + project = parse_fortran_project([source]) + modules = [module for parsed in (getattr(project, "files", None) or [project]) for module in parsed.modules] + index = FortranToIRConverter._callback_module_index(modules) + owner = next(module for module in modules if module.name == "local_mod") + + # The module declares no interface of its own; both belong to a procedure. + assert FortranToIRConverter._declared_callback_interfaces(owner) == {} + + seen = {} + for procedure in owner.procedures: + scope = FortranToIRConverter._scope_callback_interfaces( + index, + procedure.uses, + base={}, + owner=owner, + scope_name=procedure.name, + ) + seen[procedure.name] = [argument.base_type for argument in scope["cb"].signature.arguments] + + assert seen == {"first": ["integer"], "second": ["real"]} diff --git a/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py b/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py index 30de6fe85..8fee216c0 100644 --- a/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py +++ b/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py @@ -1,8 +1,15 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" from prik.parsers.fortran import parse_fortran_project +from prik.policy.exports import complete_python_export_policy from prik.printers import emit_module from prik.semantics.fortran2ir import FortranToIRConverter +from prik.semantics.models import ( + CONTRACT_NAME_METADATA, + EXTERNAL_TYPE_REF_METADATA, + PROTOTYPE_REF_METADATA, + UNRESOLVED_PROCEDURE_INTERFACE_METADATA, +) from prik.semantics.native_contract import native_contract_issues from tests.fortran._support.semantic_conversion import get_function from prik.parsers.fortran import parse_fortran_file as parse_fortran_source @@ -90,7 +97,13 @@ def test_dummy_procedure_interfaces_become_complete_callable_contracts(): ) explicit_callback = get_function(module, "explicit_case").arguments[0].semantic_type - assert explicit_callback.name == "callback" + # A block written inside a procedure names a signature only that procedure + # can reach, so its identity is qualified by the owning scope, and so is the + # contract spelling completion gives it. + assert explicit_callback.metadata["prototype_ref"]["name"] == "callback" + assert explicit_callback.metadata["prototype_ref"]["declaring_scope"] == ("explicit_case",) + complete_python_export_policy(module) + assert explicit_callback.metadata[CONTRACT_NAME_METADATA] == "explicit_case_callback" assert [argument.name for argument in explicit_callback.metadata["arguments"]] == ["Int32"] assert explicit_callback.metadata["return"].name == "Int32" @@ -118,7 +131,9 @@ def test_dummy_procedure_interfaces_become_complete_callable_contracts(): assert "callback: transform_iface" in emitted assert "@prototype\ndef value_iface(" in emitted assert "value: In(Int32)" in emitted - assert "ref: Addr(Float64)" in emitted + # A dummy with no declared intent keeps that absence in the contract while + # carrying storage the callee may write through. + assert "ref: Float64[()]" in emitted assert "@prototype\ndef string_iface(" in emitted assert "read_label: In(String[8])" in emitted assert native_contract_issues(parse_pyi_text(emitted, module_name=module.name)) == [] @@ -171,6 +186,38 @@ def test_dummy_procedure_interfaces_become_complete_callable_contracts(): assert standalone_callback.metadata["return"].name == "Int32" +def test_optional_callback_presence_round_trips_through_one_contract_spelling(): + source = """ +module optional_callbacks + abstract interface + subroutine report(value, status) + integer, intent(in) :: value + integer, intent(in), optional :: status + end subroutine report + end interface +contains + subroutine run(callback) + procedure(report), optional :: callback + end subroutine run +end module optional_callbacks +""" + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + complete_python_export_policy(module) + + callback = get_function(module, "run").arguments[0] + assert callback.optional is True + assert callback.semantic_type.metadata["callback_arguments"][1].optional is True + + contract = emit_module(module) + assert "status: In(Addr(Int32)) = ..." in contract + assert "callback: report = ..." in contract + + reloaded = parse_pyi_text(contract, module_name="optional_callbacks") + reloaded_callback = get_function(reloaded, "run").arguments[0] + assert reloaded_callback.optional is True + assert reloaded_callback.semantic_type.metadata["callback_arguments"][1].optional is True + + def test_duplicate_interface_signatures_emit_one_named_callback_prototype(): source = """ module duplicate_prototypes @@ -194,3 +241,407 @@ def test_duplicate_interface_signatures_emit_one_named_callback_prototype(): module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) assert [prototype.name for prototype in module.prototypes] == ["callback"] + + +def test_imported_abstract_interface_resolves_across_files_and_keeps_its_declared_name(): + """A `procedure(OBJ)` dummy resolves against the module that declares OBJ. + + The interface name reaches the generated contract as a public symbol, so + the declaration keeps the spelling the interface was declared with rather + than the casefolded key used to match it. + """ + interface_source = """ +module pintrf_mod + implicit none + private + public :: OBJ + + abstract interface + subroutine OBJ(x, f) + implicit none + real(8), intent(in) :: x(:) + real(8), intent(out) :: f + end subroutine OBJ + end interface +end module pintrf_mod +""" + solver_source = """ +module solver_mod + use, non_intrinsic :: pintrf_mod, only : OBJ + implicit none +contains + subroutine minimize(calfun, x, f) + procedure(OBJ) :: calfun + real(8), intent(in) :: x(:) + real(8), intent(out) :: f + call calfun(x, f) + end subroutine minimize +end module solver_mod +""" + project = parse_fortran_project({"pintrf.f90": interface_source, "solver.f90": solver_source}) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["solver_mod"], "minimize").arguments[0].semantic_type + assert callback.name == "OBJ" + assert callback.storage is not None and callback.storage.kind == "callback" + assert [argument.name for argument in callback.metadata["callback_arguments"]] == ["x", "f"] + assert callback.metadata["arguments"][0].shape == ["::"] + assert callback.metadata["return"].name == "None" + + +def test_named_but_undeclared_procedure_interface_is_recorded_for_diagnosis(): + """An unresolved `procedure(OBJ)` keeps the name so later stages can report it.""" + source = """ +module solver_mod + use, non_intrinsic :: pintrf_mod, only : OBJ + implicit none +contains + subroutine minimize(calfun, x) + procedure(OBJ) :: calfun + real(8), intent(in) :: x + end subroutine minimize +end module solver_mod +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + + callback = get_function(module, "minimize").arguments[0].semantic_type + assert callback.metadata[UNRESOLVED_PROCEDURE_INTERFACE_METADATA] == "OBJ" + + +CALLBACK_TYPES_SOURCE = """ +module callback_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + subroutine move_point(p) + import :: point_t + implicit none + type(point_t), intent(inout) :: p + end subroutine move_point + end interface +end module callback_types +""" + + +def test_imported_interface_resolves_its_types_in_the_declaring_module(): + """An interface body is written in the scope of the module that declares it. + + The consuming module need not import the types the interface names, so + those types must keep the declaring module's identity rather than being + attributed to whichever module imported the interface. + """ + consumer_source = """ +module consumer + use callback_types, only : move_point + implicit none +contains + subroutine run(f) + procedure(move_point) :: f + end subroutine run +end module consumer +""" + project = parse_fortran_project({"callback_types.f90": CALLBACK_TYPES_SOURCE, "consumer.f90": consumer_source}) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["consumer"], "run").arguments[0].semantic_type + point = callback.metadata["callback_arguments"][0].semantic_type + assert point.name == "point_t" + assert point.metadata[EXTERNAL_TYPE_REF_METADATA]["origin_module"] == "callback_types" + + +def test_procedure_local_use_resolves_an_imported_interface(): + """A ``use`` inside one procedure names the interface only in that scope.""" + source = """ +module proclocal_mod + implicit none +contains + subroutine run_local(callback) + use callback_types, only : move_point + implicit none + procedure(move_point) :: callback + end subroutine run_local +end module proclocal_mod +""" + project = parse_fortran_project({"callback_types.f90": CALLBACK_TYPES_SOURCE, "users.f90": source}) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["proclocal_mod"], "run_local").arguments[0].semantic_type + assert callback.name == "move_point" + assert callback.storage is not None and callback.storage.kind == "callback" + + +def test_reexported_interface_resolves_through_every_import_hop(): + """An interface published by a re-exporting module resolves to its declarer.""" + reexport_source = """ +module reexport_mod + use callback_types, only : move_point + implicit none + public :: move_point +end module reexport_mod +""" + chain_source = """ +module chain_mod + use reexport_mod, only : move_point + implicit none +contains + subroutine run_chain(callback) + procedure(move_point) :: callback + end subroutine run_chain +end module chain_mod +""" + project = parse_fortran_project( + { + "callback_types.f90": CALLBACK_TYPES_SOURCE, + "reexport.f90": reexport_source, + "chain.f90": chain_source, + } + ) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["chain_mod"], "run_chain").arguments[0].semantic_type + assert callback.name == "move_point" + assert callback.storage is not None and callback.storage.kind == "callback" + point = callback.metadata["callback_arguments"][0].semantic_type + assert point.metadata[EXTERNAL_TYPE_REF_METADATA]["origin_module"] == "callback_types" + + +MAKE_POINT_SOURCE = """ +module callback_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + function make_point(x) result(p) + import :: point_t + implicit none + real(8), intent(in) :: x + type(point_t) :: p + end function make_point + end interface +end module callback_types +""" + + +def test_imported_interface_result_keeps_the_declaring_module(): + """A callback result carries the declaring module's types like a dummy does. + + Ownership was recorded only while iterating dummies, so a function + interface returning a module-owned type attributed it to the consumer. + """ + consumer_source = """ +module consumer + use callback_types, only : make_point + implicit none +contains + subroutine run(f) + procedure(make_point) :: f + end subroutine run +end module consumer +""" + project = parse_fortran_project({"callback_types.f90": MAKE_POINT_SOURCE, "consumer.f90": consumer_source}) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["consumer"], "run").arguments[0].semantic_type + result = callback.metadata["return"] + assert result.name == "point_t" + assert result.metadata[EXTERNAL_TYPE_REF_METADATA]["origin_module"] == "callback_types" + + +def test_procedure_local_rename_keeps_both_the_declared_and_local_names(): + """A renamed import binds a new name without changing the declared one. + + The contract must import the declaring name under the local alias, which + requires keeping the two spellings apart as separate source facts. + """ + source = """ +module ren_types + implicit none + abstract interface + subroutine OBJ(x) + implicit none + real(8), intent(in) :: x + end subroutine OBJ + end interface +end module ren_types + +module ren_consumer + implicit none +contains + subroutine run_ren(callback) + use ren_types, only : LOCAL_OBJ => OBJ + implicit none + procedure(LOCAL_OBJ) :: callback + end subroutine run_ren +end module ren_consumer +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source))[1] + + callback = get_function(module, "run_ren").arguments[0].semantic_type + assert callback.name == "LOCAL_OBJ" + assert callback.metadata[PROTOTYPE_REF_METADATA] == { + "name": "OBJ", + "local_name": "LOCAL_OBJ", + "origin_module": "ren_types", + # The block is the declaring module's own, so no contained procedure + # owns it and the identity carries an empty scope. + "declaring_scope": (), + } + + +def test_non_only_rename_does_not_choose_between_callback_routes(): + """A renamed interface and the same local spelling remain ambiguous.""" + source = """ +module callback_types + abstract interface + subroutine x(value) + real, intent(in) :: value + end subroutine x + subroutine y(value) + integer, intent(in) :: value + end subroutine y + end interface +end module callback_types + +module callback_user + use callback_types, x => y +contains + subroutine apply_x(callback) + procedure(x) :: callback + end subroutine apply_x + subroutine apply_y(callback) + procedure(y) :: callback + end subroutine apply_y +end module callback_user +""" + modules = {module.name: module for module in FortranToIRConverter().visit(parse_fortran_source(source))} + + callback_x = get_function(modules["callback_user"], "apply_x").arguments[0].semantic_type + callback_y = get_function(modules["callback_user"], "apply_y").arguments[0].semantic_type + + assert callback_x.name == "Procedure" + assert callback_x.metadata[UNRESOLVED_PROCEDURE_INTERFACE_METADATA] == "x" + assert callback_y.name == "Procedure" + assert callback_y.metadata[UNRESOLVED_PROCEDURE_INTERFACE_METADATA] == "y" + + +def test_interface_reference_uses_the_declared_spelling(): + """Fortran matches names case-insensitively; Python contracts do not. + + A reference spelled in another case is the same interface, so the contract + keeps the declared spelling instead of binding a second name. + """ + source = """ +module cas_mod + implicit none + abstract interface + subroutine OBJ(x) + implicit none + real(8), intent(in) :: x + end subroutine OBJ + end interface +contains + subroutine run_cas(callback) + procedure(obj) :: callback + end subroutine run_cas +end module cas_mod +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + + assert get_function(module, "run_cas").arguments[0].semantic_type.name == "OBJ" + + +ACCESSIBILITY_SOURCE = """ +module acc_a + implicit none + abstract interface + subroutine OBJ(x) + implicit none + real(8), intent(in) :: x + end subroutine OBJ + end interface +end module acc_a + +module acc_b_public + use acc_a, only : OBJ + implicit none + private + public :: OBJ +end module acc_b_public + +module acc_b_private + use acc_a, only : OBJ + implicit none + private +end module acc_b_private + +module acc_ok + use acc_b_public, only : OBJ + implicit none +contains + subroutine run_ok(callback) + procedure(OBJ) :: callback + end subroutine run_ok +end module acc_ok + +module acc_bad + use acc_b_private, only : OBJ + implicit none +contains + subroutine run_bad(callback) + procedure(OBJ) :: callback + end subroutine run_bad +end module acc_bad +""" + + +def _is_resolved_callback(module, function_name: str) -> bool: + semantic_type = get_function(module, function_name).arguments[0].semantic_type + return semantic_type.storage is not None and semantic_type.storage.kind == "callback" + + +def test_reexported_interface_resolves_only_when_the_module_publishes_it(): + """Following a re-export must respect the module's own accessibility. + + A name a module imports privately is not part of its interface, so reaching + it through ``use`` must not resolve even though the chain exists. + """ + modules = { + module.name: module for module in FortranToIRConverter().visit(parse_fortran_source(ACCESSIBILITY_SOURCE)) + } + + assert _is_resolved_callback(modules["acc_ok"], "run_ok") + assert not _is_resolved_callback(modules["acc_bad"], "run_bad") + + +def test_accessibility_is_enforced_at_every_re_export_hop(): + """A private hop anywhere in the chain stops the name from travelling.""" + source = ( + ACCESSIBILITY_SOURCE + + """ +module acc_mid + use acc_b_public, only : OBJ + implicit none + private +end module acc_mid + +module acc_far + use acc_mid, only : OBJ + implicit none +contains + subroutine run_far(callback) + procedure(OBJ) :: callback + end subroutine run_far +end module acc_far +""" + ) + modules = {module.name: module for module in FortranToIRConverter().visit(parse_fortran_source(source))} + + assert not _is_resolved_callback(modules["acc_far"], "run_far") diff --git a/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py b/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py index a32446cc8..017fb3e05 100644 --- a/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py +++ b/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py @@ -1,7 +1,9 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" import pytest +from prik.pipeline.pyi import pyi_paths_to_semantic_modules from prik.policy.completion import complete_semantic_policies +from prik.semantics.models import PROTOTYPE_REF_METADATA from tests.fortran._support.pyi_conversion import parse_pyi_text @@ -125,7 +127,7 @@ def values(n: Int32) -> Float64[extent_for(n)]: ... @pytest.mark.parametrize( ("decorators", "message"), [ - ("@pure", "pure requires prototype"), + ('@pure\n@overload("declared_impl")', "an overload dispatcher names none"), ("@standalone\n@prototype", "prototype cannot be combined with standalone"), ], ) @@ -140,6 +142,20 @@ def declared(value: Int32) -> Int32: ... ) +def test_a_pure_module_function_states_the_purity_its_native_procedure_has(): + """A specification function has to be pure, and its contract says that it is.""" + module = parse_pyi_text( + """ +@pure +@native_call([Addr(Arg(0))]) +def extent_for(n: Int32) -> Int32: ... +""", + module_name="pure_function", + ) + + assert module.functions[0].metadata["fortran_attributes"] == ["pure"] + + def test_imported_prototype_resolves_as_module_interface_definition(tmp_path): from prik.pipeline.pyi import pyi_paths_to_semantic_modules from prik.pipeline.wrapper import WrapperGenerator @@ -215,3 +231,31 @@ def test_convert_pyi_to_ir_rejects_redundant_or_invalid_prototype_value_wrappers f"@prototype\ndef callback(value: {annotation}) -> None: ...", module_name="callbacks", ) + + +def test_renamed_reexport_chain_resolves_to_the_declaring_name(tmp_path): + """A reference follows both module and symbol provenance to the declaration. + + Each hop of a renaming chain binds a new alias, and only the module that + declares the prototype knows the name it declared. Recording an alias from + somewhere along the chain would name a symbol the declaring module does not + define. + """ + for name, text in ( + ( + "mod_a.pyi", + "from prik.contracts import Float64, In, prototype\n\n@prototype\ndef OBJ(x: In(Float64)) -> None: ...\n", + ), + ("mod_b.pyi", "from mod_a import OBJ as MID\n"), + ("mod_c.pyi", "from mod_b import MID as LOCAL\n\ndef run(callback: LOCAL) -> None: ...\n"), + ): + (tmp_path / name).write_text(text, encoding="utf-8") + + modules = {module.name: module for module in pyi_paths_to_semantic_modules(sorted(tmp_path.glob("*.pyi")))} + + callback = next(item for item in modules["mod_c"].functions if item.name == "run").arguments[0].semantic_type + assert callback.metadata[PROTOTYPE_REF_METADATA] == { + "name": "OBJ", + "local_name": "LOCAL", + "origin_module": "mod_a", + } diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fbind_value_f90/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fbind_value_f90/__init__.pyi index 11d620ebb..081855f99 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fbind_value_f90/__init__.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fbind_value_f90/__init__.pyi @@ -1 +1,3 @@ from . import fbind_value_f90 + +__all__ = ["fbind_value_f90"] diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fbind_value_f90/fbind_value_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fbind_value_f90/fbind_value_f90.pyi index 8cd319629..5b02667ca 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fbind_value_f90/fbind_value_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fbind_value_f90/fbind_value_f90.pyi @@ -40,3 +40,13 @@ def invert_flag( def char_code( ch: String[1] ) -> Int32: ... + +__all__ = [ + "plus_value", + "double_value", + "plus_reference", + "scale_real", + "conjugate_value", + "invert_flag", + "char_code", +] diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath/__init__.pyi index d7a7d8642..677f2e6ae 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath/__init__.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath/__init__.pyi @@ -1,62 +1,53 @@ -from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call, standalone +from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, native_call, standalone -@bind("SQUARE_R4") @standalone @native_call([Addr(Arg(0))]) def square_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SQUARE_R8") @standalone @native_call([Addr(Arg(0))]) def square_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("SQUARE_I4") @standalone @native_call([Addr(Arg(0))]) def square_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("SQUARE_C4") @standalone @native_call([Addr(Arg(0))]) def square_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... -@bind("SQUARE_C8") @standalone @native_call([Addr(Arg(0))]) def square_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... -@bind("CUBE_R4") @standalone @native_call([Addr(Arg(0))]) def cube_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("CUBE_R8") @standalone @native_call([Addr(Arg(0))]) def cube_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("CUBE_I4") @standalone @native_call([Addr(Arg(0))]) def cube_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("ADD_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r4( @@ -64,7 +55,6 @@ def add_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("ADD_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r8( @@ -72,7 +62,6 @@ def add_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("ADD_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_i4( @@ -80,7 +69,6 @@ def add_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("ADD_C4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c4( @@ -88,7 +76,6 @@ def add_c4( Y: Complex64 ) -> tuple[Complex64, Returns["X", Complex64], Returns["Y", Complex64]]: ... -@bind("ADD_C8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c8( @@ -96,7 +83,6 @@ def add_c8( Y: Complex128 ) -> tuple[Complex128, Returns["X", Complex128], Returns["Y", Complex128]]: ... -@bind("SUB_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r4( @@ -104,7 +90,6 @@ def sub_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("SUB_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r8( @@ -112,7 +97,6 @@ def sub_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("SUB_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_i4( @@ -120,7 +104,6 @@ def sub_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MUL_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r4( @@ -128,7 +111,6 @@ def mul_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MUL_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r8( @@ -136,7 +118,6 @@ def mul_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MUL_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_i4( @@ -144,7 +125,6 @@ def mul_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("DIV_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r4( @@ -152,7 +132,6 @@ def div_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("DIV_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r8( @@ -160,7 +139,6 @@ def div_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("POW_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r4( @@ -168,7 +146,6 @@ def pow_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("POW_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r8( @@ -176,133 +153,114 @@ def pow_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("ABS_R4") @standalone @native_call([Addr(Arg(0))]) def abs_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ABS_R8") @standalone @native_call([Addr(Arg(0))]) def abs_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ABS_I4") @standalone @native_call([Addr(Arg(0))]) def abs_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("NEG_R4") @standalone @native_call([Addr(Arg(0))]) def neg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("NEG_R8") @standalone @native_call([Addr(Arg(0))]) def neg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("NEG_I4") @standalone @native_call([Addr(Arg(0))]) def neg_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("SIN_R4") @standalone @native_call([Addr(Arg(0))]) def sin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SIN_R8") @standalone @native_call([Addr(Arg(0))]) def sin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("COS_R4") @standalone @native_call([Addr(Arg(0))]) def cos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("COS_R8") @standalone @native_call([Addr(Arg(0))]) def cos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("TAN_R4") @standalone @native_call([Addr(Arg(0))]) def tan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("TAN_R8") @standalone @native_call([Addr(Arg(0))]) def tan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ASIN_R4") @standalone @native_call([Addr(Arg(0))]) def asin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ASIN_R8") @standalone @native_call([Addr(Arg(0))]) def asin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ACOS_R4") @standalone @native_call([Addr(Arg(0))]) def acos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ACOS_R8") @standalone @native_call([Addr(Arg(0))]) def acos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ATAN_R4") @standalone @native_call([Addr(Arg(0))]) def atan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ATAN_R8") @standalone @native_call([Addr(Arg(0))]) def atan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ATAN2_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r4( @@ -310,7 +268,6 @@ def atan2_r4( X: Float32 ) -> tuple[Float32, Returns["Y", Float32], Returns["X", Float32]]: ... -@bind("ATAN2_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r8( @@ -318,63 +275,54 @@ def atan2_r8( X: Float64 ) -> tuple[Float64, Returns["Y", Float64], Returns["X", Float64]]: ... -@bind("EXP_R4") @standalone @native_call([Addr(Arg(0))]) def exp_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("EXP_R8") @standalone @native_call([Addr(Arg(0))]) def exp_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("LOG_R4") @standalone @native_call([Addr(Arg(0))]) def log_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("LOG_R8") @standalone @native_call([Addr(Arg(0))]) def log_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("LOG10_R4") @standalone @native_call([Addr(Arg(0))]) def log10_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("LOG10_R8") @standalone @native_call([Addr(Arg(0))]) def log10_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("SQRT_R4") @standalone @native_call([Addr(Arg(0))]) def sqrt_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SQRT_R8") @standalone @native_call([Addr(Arg(0))]) def sqrt_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("HYPOT_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r4( @@ -382,7 +330,6 @@ def hypot_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("HYPOT_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r8( @@ -390,7 +337,6 @@ def hypot_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MIN_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r4( @@ -398,7 +344,6 @@ def min_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MIN_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r8( @@ -406,7 +351,6 @@ def min_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MIN_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_i4( @@ -414,7 +358,6 @@ def min_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MAX_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r4( @@ -422,7 +365,6 @@ def max_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MAX_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r8( @@ -430,7 +372,6 @@ def max_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MAX_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_i4( @@ -438,7 +379,6 @@ def max_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("SIGN_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r4( @@ -446,7 +386,6 @@ def sign_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("SIGN_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r8( @@ -454,7 +393,6 @@ def sign_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MOD_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_i4( @@ -462,7 +400,6 @@ def mod_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MOD_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r4( @@ -470,7 +407,6 @@ def mod_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MOD_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r8( @@ -478,35 +414,30 @@ def mod_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("DEG2RAD_R4") @standalone @native_call([Addr(Arg(0))]) def deg2rad_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("DEG2RAD_R8") @standalone @native_call([Addr(Arg(0))]) def deg2rad_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("RAD2DEG_R4") @standalone @native_call([Addr(Arg(0))]) def rad2deg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("RAD2DEG_R8") @standalone @native_call([Addr(Arg(0))]) def rad2deg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("DIST2_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r4( @@ -514,7 +445,6 @@ def dist2_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("DIST2_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r8( @@ -522,7 +452,6 @@ def dist2_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("DOT2_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r4( @@ -532,7 +461,6 @@ def dot2_r4( Y2: Float32 ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["Y1", Float32], Returns["Y2", Float32]]: ... -@bind("DOT2_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r8( @@ -542,7 +470,6 @@ def dot2_r8( Y2: Float64 ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["Y1", Float64], Returns["Y2", Float64]]: ... -@bind("DOT3_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5))]) def dot3_r4( @@ -554,7 +481,6 @@ def dot3_r4( Y3: Float32 ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["X3", Float32], Returns["Y1", Float32], Returns["Y2", Float32], Returns["Y3", Float32]]: ... -@bind("DOT3_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5))]) def dot3_r8( @@ -566,79 +492,156 @@ def dot3_r8( Y3: Float64 ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["X3", Float64], Returns["Y1", Float64], Returns["Y2", Float64], Returns["Y3", Float64]]: ... -@bind("CONJ_C4") @standalone @native_call([Addr(Arg(0))]) def conj_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... -@bind("CONJ_C8") @standalone @native_call([Addr(Arg(0))]) def conj_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... -@bind("REAL_C4") @standalone @native_call([Addr(Arg(0))]) def real_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("REAL_C8") @standalone @native_call([Addr(Arg(0))]) def real_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("AIMAG_C4") @standalone @native_call([Addr(Arg(0))]) def aimag_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("AIMAG_C8") @standalone @native_call([Addr(Arg(0))]) def aimag_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("ABS_C4") @standalone @native_call([Addr(Arg(0))]) def abs_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("ABS_C8") @standalone @native_call([Addr(Arg(0))]) def abs_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("IS_POSITIVE_R4") @standalone @native_call([Addr(Arg(0))]) def is_positive_r4( X: Float32 ) -> tuple[Bool32, Returns["X", Float32]]: ... -@bind("IS_POSITIVE_R8") @standalone @native_call([Addr(Arg(0))]) def is_positive_r8( X: Float64 ) -> tuple[Bool32, Returns["X", Float64]]: ... -@bind("IS_EVEN_I4") @standalone @native_call([Addr(Arg(0))]) def is_even_i4( X: Int32 ) -> tuple[Bool32, Returns["X", Int32]]: ... + +__all__ = [ + "square_r4", + "square_r8", + "square_i4", + "square_c4", + "square_c8", + "cube_r4", + "cube_r8", + "cube_i4", + "add_r4", + "add_r8", + "add_i4", + "add_c4", + "add_c8", + "sub_r4", + "sub_r8", + "sub_i4", + "mul_r4", + "mul_r8", + "mul_i4", + "div_r4", + "div_r8", + "pow_r4", + "pow_r8", + "abs_r4", + "abs_r8", + "abs_i4", + "neg_r4", + "neg_r8", + "neg_i4", + "sin_r4", + "sin_r8", + "cos_r4", + "cos_r8", + "tan_r4", + "tan_r8", + "asin_r4", + "asin_r8", + "acos_r4", + "acos_r8", + "atan_r4", + "atan_r8", + "atan2_r4", + "atan2_r8", + "exp_r4", + "exp_r8", + "log_r4", + "log_r8", + "log10_r4", + "log10_r8", + "sqrt_r4", + "sqrt_r8", + "hypot_r4", + "hypot_r8", + "min_r4", + "min_r8", + "min_i4", + "max_r4", + "max_r8", + "max_i4", + "sign_r4", + "sign_r8", + "mod_i4", + "mod_r4", + "mod_r8", + "deg2rad_r4", + "deg2rad_r8", + "rad2deg_r4", + "rad2deg_r8", + "dist2_r4", + "dist2_r8", + "dot2_r4", + "dot2_r8", + "dot3_r4", + "dot3_r8", + "conj_c4", + "conj_c8", + "real_c4", + "real_c8", + "aimag_c4", + "aimag_c8", + "abs_c4", + "abs_c8", + "is_positive_r4", + "is_positive_r8", + "is_even_i4", +] diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/__init__.pyi index 63123a9e4..4c6f41050 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/__init__.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/__init__.pyi @@ -1 +1,3 @@ from . import fmath_f90 + +__all__ = ["fmath_f90"] diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/fmath_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/fmath_f90.pyi index 6b8daf07d..38cabcd36 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/fmath_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/fmath_f90.pyi @@ -1,458 +1,387 @@ -from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call +from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, native_call -@bind("SQUARE_R4") @native_call([Addr(Arg(0))]) def square_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SQUARE_R8") @native_call([Addr(Arg(0))]) def square_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("SQUARE_I4") @native_call([Addr(Arg(0))]) def square_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("SQUARE_C4") @native_call([Addr(Arg(0))]) def square_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... -@bind("SQUARE_C8") @native_call([Addr(Arg(0))]) def square_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... -@bind("CUBE_R4") @native_call([Addr(Arg(0))]) def cube_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("CUBE_R8") @native_call([Addr(Arg(0))]) def cube_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("CUBE_I4") @native_call([Addr(Arg(0))]) def cube_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("ADD_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("ADD_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("ADD_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("ADD_C4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c4( X: Complex64, Y: Complex64 ) -> tuple[Complex64, Returns["X", Complex64], Returns["Y", Complex64]]: ... -@bind("ADD_C8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c8( X: Complex128, Y: Complex128 ) -> tuple[Complex128, Returns["X", Complex128], Returns["Y", Complex128]]: ... -@bind("SUB_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("SUB_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("SUB_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MUL_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MUL_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MUL_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("DIV_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("DIV_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("POW_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("POW_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("ABS_R4") @native_call([Addr(Arg(0))]) def abs_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ABS_R8") @native_call([Addr(Arg(0))]) def abs_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ABS_I4") @native_call([Addr(Arg(0))]) def abs_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("NEG_R4") @native_call([Addr(Arg(0))]) def neg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("NEG_R8") @native_call([Addr(Arg(0))]) def neg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("NEG_I4") @native_call([Addr(Arg(0))]) def neg_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("SIN_R4") @native_call([Addr(Arg(0))]) def sin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SIN_R8") @native_call([Addr(Arg(0))]) def sin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("COS_R4") @native_call([Addr(Arg(0))]) def cos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("COS_R8") @native_call([Addr(Arg(0))]) def cos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("TAN_R4") @native_call([Addr(Arg(0))]) def tan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("TAN_R8") @native_call([Addr(Arg(0))]) def tan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ASIN_R4") @native_call([Addr(Arg(0))]) def asin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ASIN_R8") @native_call([Addr(Arg(0))]) def asin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ACOS_R4") @native_call([Addr(Arg(0))]) def acos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ACOS_R8") @native_call([Addr(Arg(0))]) def acos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ATAN_R4") @native_call([Addr(Arg(0))]) def atan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ATAN_R8") @native_call([Addr(Arg(0))]) def atan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ATAN2_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r4( Y: Float32, X: Float32 ) -> tuple[Float32, Returns["Y", Float32], Returns["X", Float32]]: ... -@bind("ATAN2_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r8( Y: Float64, X: Float64 ) -> tuple[Float64, Returns["Y", Float64], Returns["X", Float64]]: ... -@bind("EXP_R4") @native_call([Addr(Arg(0))]) def exp_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("EXP_R8") @native_call([Addr(Arg(0))]) def exp_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("LOG_R4") @native_call([Addr(Arg(0))]) def log_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("LOG_R8") @native_call([Addr(Arg(0))]) def log_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("LOG10_R4") @native_call([Addr(Arg(0))]) def log10_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("LOG10_R8") @native_call([Addr(Arg(0))]) def log10_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("SQRT_R4") @native_call([Addr(Arg(0))]) def sqrt_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SQRT_R8") @native_call([Addr(Arg(0))]) def sqrt_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("HYPOT_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("HYPOT_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MIN_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MIN_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MIN_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MAX_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MAX_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MAX_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("SIGN_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("SIGN_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MOD_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MOD_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MOD_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("DEG2RAD_R4") @native_call([Addr(Arg(0))]) def deg2rad_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("DEG2RAD_R8") @native_call([Addr(Arg(0))]) def deg2rad_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("RAD2DEG_R4") @native_call([Addr(Arg(0))]) def rad2deg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("RAD2DEG_R8") @native_call([Addr(Arg(0))]) def rad2deg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("DIST2_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("DIST2_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("DOT2_R4") @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r4( X1: Float32, @@ -461,7 +390,6 @@ def dot2_r4( Y2: Float32 ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["Y1", Float32], Returns["Y2", Float32]]: ... -@bind("DOT2_R8") @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r8( X1: Float64, @@ -470,7 +398,6 @@ def dot2_r8( Y2: Float64 ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["Y1", Float64], Returns["Y2", Float64]]: ... -@bind("DOT3_R4") @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5))]) def dot3_r4( X1: Float32, @@ -481,7 +408,6 @@ def dot3_r4( Y3: Float32 ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["X3", Float32], Returns["Y1", Float32], Returns["Y2", Float32], Returns["Y3", Float32]]: ... -@bind("DOT3_R8") @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5))]) def dot3_r8( X1: Float64, @@ -492,68 +418,145 @@ def dot3_r8( Y3: Float64 ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["X3", Float64], Returns["Y1", Float64], Returns["Y2", Float64], Returns["Y3", Float64]]: ... -@bind("CONJ_C4") @native_call([Addr(Arg(0))]) def conj_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... -@bind("CONJ_C8") @native_call([Addr(Arg(0))]) def conj_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... -@bind("REAL_C4") @native_call([Addr(Arg(0))]) def real_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("REAL_C8") @native_call([Addr(Arg(0))]) def real_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("AIMAG_C4") @native_call([Addr(Arg(0))]) def aimag_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("AIMAG_C8") @native_call([Addr(Arg(0))]) def aimag_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("ABS_C4") @native_call([Addr(Arg(0))]) def abs_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("ABS_C8") @native_call([Addr(Arg(0))]) def abs_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("IS_POSITIVE_R4") @native_call([Addr(Arg(0))]) def is_positive_r4( X: Float32 ) -> tuple[Bool32, Returns["X", Float32]]: ... -@bind("IS_POSITIVE_R8") @native_call([Addr(Arg(0))]) def is_positive_r8( X: Float64 ) -> tuple[Bool32, Returns["X", Float64]]: ... -@bind("IS_EVEN_I4") @native_call([Addr(Arg(0))]) def is_even_i4( X: Int32 ) -> tuple[Bool32, Returns["X", Int32]]: ... + +__all__ = [ + "square_r4", + "square_r8", + "square_i4", + "square_c4", + "square_c8", + "cube_r4", + "cube_r8", + "cube_i4", + "add_r4", + "add_r8", + "add_i4", + "add_c4", + "add_c8", + "sub_r4", + "sub_r8", + "sub_i4", + "mul_r4", + "mul_r8", + "mul_i4", + "div_r4", + "div_r8", + "pow_r4", + "pow_r8", + "abs_r4", + "abs_r8", + "abs_i4", + "neg_r4", + "neg_r8", + "neg_i4", + "sin_r4", + "sin_r8", + "cos_r4", + "cos_r8", + "tan_r4", + "tan_r8", + "asin_r4", + "asin_r8", + "acos_r4", + "acos_r8", + "atan_r4", + "atan_r8", + "atan2_r4", + "atan2_r8", + "exp_r4", + "exp_r8", + "log_r4", + "log_r8", + "log10_r4", + "log10_r8", + "sqrt_r4", + "sqrt_r8", + "hypot_r4", + "hypot_r8", + "min_r4", + "min_r8", + "min_i4", + "max_r4", + "max_r8", + "max_i4", + "sign_r4", + "sign_r8", + "mod_i4", + "mod_r4", + "mod_r8", + "deg2rad_r4", + "deg2rad_r8", + "rad2deg_r4", + "rad2deg_r8", + "dist2_r4", + "dist2_r8", + "dot2_r4", + "dot2_r8", + "dot3_r4", + "dot3_r8", + "conj_c4", + "conj_c8", + "real_c4", + "real_c8", + "aimag_c4", + "aimag_c8", + "abs_c4", + "abs_c8", + "is_positive_r4", + "is_positive_r8", + "is_even_i4", +] diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/__init__.pyi index 60f8fe03a..cc08cafff 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/__init__.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/__init__.pyi @@ -1 +1,3 @@ from . import fscalar_kinds_f90 + +__all__ = ["fscalar_kinds_f90"] diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi index 957e79c19..d982479ca 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi @@ -101,3 +101,25 @@ def conj_c_float_complex( def conj_c_double_complex( value: Complex128 ) -> Complex128: ... + +__all__ = [ + "id_i8", + "id_i16", + "id_i32", + "id_i32_value", + "id_i64", + "copy_i16", + "not_flag", + "invert_flags", + "id_r32", + "id_r64", + "copy_r64", + "conj_c64", + "shift_c128", + "copy_c128", + "id_c_i32", + "id_c_float", + "id_c_double", + "conj_c_float_complex", + "conj_c_double_complex", +] diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/__init__.pyi index 8059f6da2..c2911ac63 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import scalar_direct_bind_c_f90 + +__all__ = ["scalar_direct_bind_c_f90"] diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/scalar_direct_bind_c_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/scalar_direct_bind_c_f90.pyi index 919b914d1..03cd82a17 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/scalar_direct_bind_c_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/scalar_direct_bind_c_f90.pyi @@ -28,3 +28,5 @@ def invert_flag( def optional_state( value: Float64 = ... ) -> Int32: ... + +__all__ = ["renamed_add", "reference_add", "scale_output", "invert_flag", "optional_state"] diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/__init__.pyi index 3f14d14ca..62536acb4 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import scalar_mixed_bind_c_f90 + +__all__ = ["scalar_mixed_bind_c_f90"] diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/scalar_mixed_bind_c_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/scalar_mixed_bind_c_f90.pyi index 6e07ecec9..fb112fcb9 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/scalar_mixed_bind_c_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/scalar_mixed_bind_c_f90.pyi @@ -10,3 +10,5 @@ def direct_add( def adapted_add( value: Int32 ) -> Int32: ... + +__all__ = ["direct_add", "adapted_add"] diff --git a/tests/fortran/data_types/end_to_end/test_project_kind_alias_chain.py b/tests/fortran/data_types/end_to_end/test_project_kind_alias_chain.py new file mode 100644 index 000000000..86189e2f9 --- /dev/null +++ b/tests/fortran/data_types/end_to_end/test_project_kind_alias_chain.py @@ -0,0 +1,106 @@ +"""Project kind aliases resolve to intrinsics before any compiler probe runs.""" + +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import, _compiler, _import_from_build_dir +from prik import build_pyi_extension +from prik.parsers.fortran import parse_fortran_file +from prik.semantics.fortran2ir import collect_fortran_type_storage_requirements + +pytestmark = pytest.mark.fortran_end_to_end + +KIND_ALIAS_SOURCE = """ +module consts_mod + use iso_fortran_env, only : REAL64 + use iso_fortran_env, only : INT32 + implicit none + + integer, parameter :: DP = REAL64 + integer, parameter :: IK_DFT = INT32 + integer, parameter :: RP = DP + integer, parameter :: IK = IK_DFT +end module consts_mod + +module consumer_mod + use consts_mod, only : RP, IK + implicit none +contains + subroutine work(x, n) + real(RP), intent(inout) :: x + integer(IK), intent(in) :: n + x = x * real(n, RP) + end subroutine work +end module consumer_mod +""" + + +def test_kind_alias_chain_reaches_the_probe_as_intrinsic_expressions(tmp_path: Path): + """A project names its kinds through its own parameters, and they resolve. + + Each `use` of one module adds to what the scope imported, and a parameter + may name another, so `RP` reaches `REAL64` through `DP`. The probe measures + target storage and is given expressions a compiler understands, never a + project name it has no way to evaluate. + """ + source = tmp_path / "kinds.f90" + source.write_text(KIND_ALIAS_SOURCE, encoding="utf-8") + + parsed = parse_fortran_file(source) + consumer = next(module for module in parsed.modules if module.name == "consumer_mod") + assert [(argument.name, argument.kind) for argument in consumer.procedures[0].arguments] == [ + ("x", "REAL64"), + ("n", "INT32"), + ] + assert all( + "RP" not in str(requirement["expression"]) and "IK" not in str(requirement["expression"]) + for requirement in collect_fortran_type_storage_requirements(parsed) + ) + + module = _build_source_and_import( + source, + tmp_path / "source_build", + {"bind_c_kinds_wrapper.f90", "kinds_wrapper.c", "kinds_wrapper.h"}, + ) + assert module.consumer_mod.work(np.float64(2.5), np.int32(4)) == pytest.approx(10.0) + + +def test_kind_alias_chain_survives_its_generated_contract(tmp_path: Path): + """The contract states resolved types, and rebuilding keeps the behavior.""" + source = tmp_path / "kinds.f90" + source.write_text(KIND_ALIAS_SOURCE, encoding="utf-8") + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + str(source), + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + + contract = (contracts / "consumer_mod.pyi").read_text(encoding="utf-8") + assert "x: Float64" in contract + assert "n: Int32" in contract + + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(source)], + output_dir=tmp_path / "contract_build", + output_name="kinds_contract", + ) + rebuilt = _import_from_build_dir(result.module_name, result.output_dir) + assert rebuilt.consumer_mod.work(np.float64(2.5), np.int32(4)) == pytest.approx(10.0) diff --git a/tests/fortran/data_types/parsing/test_declarations_and_shapes.py b/tests/fortran/data_types/parsing/test_declarations_and_shapes.py index 6efa2da40..bb76dae3d 100644 --- a/tests/fortran/data_types/parsing/test_declarations_and_shapes.py +++ b/tests/fortran/data_types/parsing/test_declarations_and_shapes.py @@ -2,6 +2,7 @@ import pytest from prik.parsers.fortran import parse_fortran_file, parse_fortran_project +from prik.parsers.fortran.scope import ScopeUses from tests.fortran._support.parser_procedures import ( COMPILE_TIME_EXPRESSION_SOURCE, collect_project_procedure_signatures, @@ -136,7 +137,7 @@ def test_module_variables_and_use_statements(): assert len(modules) == 1 mod = modules[0] assert mod.name == "cfg" - assert mod.uses["iso_c_binding"] == ["c_int"] + assert list(ScopeUses(mod.uses).mappings("iso_c_binding")) == ["c_int"] assert [v.name for v in mod.variables] == ["nmax", "origin"] assert mod.variables[0].is_parameter is True assert mod.variables[1].is_parameter is False diff --git a/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py b/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py index 1add7b3ef..920eb3cc2 100644 --- a/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py +++ b/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py @@ -14,6 +14,13 @@ from prik.parsers.fortran import parse_fortran_file as parse_fortran_source +def _character_variable(name: str, selector: str) -> FortranVariable: + """Build one character model stating the selector a declaration writes.""" + variable = FortranVariable(name=name, base_type="character", kind=selector[1:-1]) + variable.record_character_selector(selector) + return variable + + def test_intrinsic_builtin_kinds_map_to_semantic_types(): converter = FortranToIRConverter() cases = [ @@ -164,7 +171,7 @@ def test_fortran_storage_requirements_follow_resolved_kinds_and_actual_source_ty FortranVariable(name="default_real", base_type="real"), FortranVariable(name="selected", base_type="real", kind="rk"), FortranVariable(name="flag", base_type="logical", kind="8"), - FortranVariable(name="text", base_type="character", kind="len=12, kind=c_char"), + _character_variable("text", "(len=12, kind=c_char)"), ] ) @@ -203,3 +210,40 @@ def test_legacy_fortran_storage_uses_fixed_star_widths_and_probes_double_types() ("real", "kind(1.0d0)", "storage_size(real(0.0,kind=kind(1.0d0)))"), ("complex", "kind(1.0d0)", "storage_size(cmplx(0.0,kind=kind(1.0d0)))"), } + + +def test_interface_body_dummies_require_target_storage_facts(): + """An interface body's dummies reach the target probe like any other variable. + + An abstract interface names its own kinds, often through a ``use`` written + inside the body, and no module variable or module procedure declares them. + Collecting nothing for such a body leaves the conversion without the storage + fact it later demands. + """ + source = """ +module callback_mod + implicit none + private + public :: reporter + abstract interface + subroutine reporter(x, nf) + use kind_mod, only : rp, ik + implicit none + real(rp), intent(in) :: x + integer(ik), intent(in) :: nf + end subroutine reporter + end interface +end module callback_mod +""" + + parsed = parse_fortran_source(source) + + requirements = collect_fortran_type_storage_requirements( + parsed, + compile_time_values={"rp": "kind(0.0d0)", "ik": "kind(0)"}, + ) + + assert [requirement["expression"] for requirement in requirements] == [ + "storage_size(real(0.0,kind=kind(0.0d0)))", + "storage_size(int(0,kind=kind(0)))", + ] diff --git a/tests/fortran/data_types/semantics/test_types_and_storage.py b/tests/fortran/data_types/semantics/test_types_and_storage.py index 893b27c7b..900bc445b 100644 --- a/tests/fortran/data_types/semantics/test_types_and_storage.py +++ b/tests/fortran/data_types/semantics/test_types_and_storage.py @@ -10,6 +10,7 @@ FortranProcedureSignature, FortranProject, FortranUseMapping, + FortranUseStatement, FortranVariable, ) from prik.semantics.fortran2ir import ( @@ -43,10 +44,10 @@ def test_converter_visitor_and_compatibility_methods_cover_public_paths(): ) module = FortranModule( name="m", - uses={ - "iso_c_binding": [FortranUseMapping(source="c_int", target="i32")], - "plain_import": [], - }, + uses=[ + FortranUseStatement("iso_c_binding", True, (FortranUseMapping(source="c_int", target="i32"),)), + FortranUseStatement("plain_import"), + ], variables=[scale], procedures=[proc], derived_types=[dtype], @@ -74,7 +75,9 @@ def test_converter_visitor_and_compatibility_methods_cover_public_paths(): assert converter.visit(proc).name == "work" assert converter.visit(proc).visibility == "public" assert converter.visit(dtype, procedure_lookup={}).base_classes == ["base_t"] - assert converter.visit(module).imports[0].items[0].target == "i32" + # No declaration is written with `i32`, and the compiler supplies + # `iso_c_binding`, so the module states no import for its contract. + assert converter.visit(module).imports == [] modules = converter.visit(parsed) assert [module.name for module in modules] == ["m", "standalone_source"] @@ -157,7 +160,7 @@ def test_fortran_native_storage_contracts_cover_array_categories_and_scalars(): assumed = array_contract(args["assumed"].semantic_type) assert assumed.category == "assumed_shape" - assert assumed.shape == ["::Strided", "::Strided"] + assert assumed.shape == ["::", "::"] assert assumed.order == "ORDER_F" contig = array_contract(args["contig"].semantic_type) @@ -268,7 +271,7 @@ def test_fortran_native_storage_contracts_preserve_exact_bounds_and_member_flags assert semantic_member.semantic_type.storage.array.pointer is True assert plain_member.optional is False assert plain_member.visibility == "public" - assert plain_member.semantic_type.storage.array.shape == ["::Strided"] + assert plain_member.semantic_type.storage.array.shape == ["::"] assert plain_member.semantic_type.storage.array.allocatable is False assert plain_member.semantic_type.storage.array.pointer is False assert plain_member.origin.source_language == "fortran" diff --git a/tests/fortran/derived_types/codegen/test_class_surfaces.py b/tests/fortran/derived_types/codegen/test_class_surfaces.py index 43b0ef678..62857e54b 100644 --- a/tests/fortran/derived_types/codegen/test_class_surfaces.py +++ b/tests/fortran/derived_types/codegen/test_class_surfaces.py @@ -4,7 +4,10 @@ import pytest +from prik.parsers.fortran import parse_fortran_project +from prik.pipeline.build import _apply_source_python_exports, _merge_wrapper_modules from prik.pipeline.pyi import pyi_file_to_semantic_module +from prik.semantics.fortran2ir import fortran_project_to_semantic_modules from prik.policy.completion import complete_semantic_policies from prik.pipeline.wrapper import WrapperGenerator from prik.planning import WrapperPlanner @@ -27,8 +30,8 @@ def _surface(plan, name: str): def test_inheritance_and_polymorphism_are_completed_before_planning(): plan = _plan(INHERITANCE) - base = _surface(plan, "base_shape") - circle = _surface(plan, "circle") + base = _surface(plan, "Base_Shape") + circle = _surface(plan, "Circle") derived = next( item for namespace in plan.namespaces @@ -44,16 +47,61 @@ def test_inheritance_and_polymorphism_are_completed_before_planning(): assert circle.base_identities == (base.type_identity,) assert [field.name for field in derived.fields] == ["size", "radius"] - assert tuple(variant.python_name for variant in describe.arguments[0].polymorphic.variants) == ( - "box", - "circle", - "base_shape", + assert tuple(variant.type_identity for variant in describe.arguments[0].polymorphic.variants) == ( + _surface(plan, "Box").type_identity, + circle.type_identity, + base.type_identity, ) def test_invalid_class_graph_fails_before_emission(): plan = _plan(INHERITANCE) - _surface(plan, "circle").base_identities = (("missing", "base"),) + _surface(plan, "Circle").base_identities = (("missing", "base"),) with pytest.raises(ValueError, match="missing-or-late-class-base"): WrapperGenerator().generate(plan) + + +def test_a_type_defined_in_two_namespaces_fails_before_emission(): + """Generated code reaches a type in the one namespace defining it.""" + plan = _plan(INHERITANCE) + namespace = next(item for item in plan.namespaces if item.derived_types) + namespace.derived_types = (*namespace.derived_types, namespace.derived_types[0]) + + with pytest.raises(ValueError, match="duplicate-derived-type-identity"): + WrapperGenerator().generate(plan) + + +EXTENDING_ANOTHER_MODULE = """\ +module zeta_base + implicit none + type :: shape + integer :: sides = 0 + end type shape +end module zeta_base + +module alpha_child + use zeta_base, only: shape + implicit none + type, extends(shape) :: square + integer :: edge = 1 + end type square +end module alpha_child +""" + + +def test_a_namespace_is_planned_after_the_one_defining_its_base(tmp_path: Path): + """A class extending another namespace's type is created once its base exists. + + Path order would put `alpha_child` first; inheritance overrides it only + where it has to. + """ + (tmp_path / "project.f90").write_text(EXTENDING_ANOTHER_MODULE, encoding="utf-8") + modules = fortran_project_to_semantic_modules(parse_fortran_project(str(tmp_path))) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name="package") + complete_semantic_policies(module) + + plan = WrapperPlanner().build(module) + + assert [namespace.python_path for namespace in plan.namespaces] == [(), ("zeta_base",), ("alpha_child",)] diff --git a/tests/fortran/derived_types/codegen/test_derived_lowering.py b/tests/fortran/derived_types/codegen/test_derived_lowering.py index 9f499d2d0..465bc7f9b 100644 --- a/tests/fortran/derived_types/codegen/test_derived_lowering.py +++ b/tests/fortran/derived_types/codegen/test_derived_lowering.py @@ -204,7 +204,7 @@ class point: ) complete_semantic_policies(module) plan = WrapperPlanner().build(module) - variable = plan.namespaces[0].variables[0] + variable = plan.variables[0] assert variable.derived.handoff.origin is DerivedObjectOrigin.NATIVE_MODULE assert variable.derived.handoff.release is DerivedRelease.NATIVE_OWNER diff --git a/tests/fortran/derived_types/codegen/test_scalar_actual_dummy_plan.py b/tests/fortran/derived_types/codegen/test_scalar_actual_dummy_plan.py index c20b5d9ec..b64abad56 100644 --- a/tests/fortran/derived_types/codegen/test_scalar_actual_dummy_plan.py +++ b/tests/fortran/derived_types/codegen/test_scalar_actual_dummy_plan.py @@ -180,10 +180,10 @@ def test_exact_typed_value_is_not_restricted_to_bind_c_layout(): def test_module_actual_declarations_keep_distinct_runtime_storage(): - namespace = WrapperPlanner().build(_module()).namespaces[0] + plan = WrapperPlanner().build(_module()) storages = { variable.symbol_name: variable.derived.handoff.storage - for variable in namespace.variables + for variable in plan.variables if variable.derived is not None } assert storages == { diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/__init__.pyi index fa1637c7f..55959d684 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/__init__.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/__init__.pyi @@ -1 +1,3 @@ from . import fbind_c_derived_layout_f90 + +__all__ = ["fbind_c_derived_layout_f90"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/fbind_c_derived_layout_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/fbind_c_derived_layout_f90.pyi index 5aef293bf..e6d0d4e18 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/fbind_c_derived_layout_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/fbind_c_derived_layout_f90.pyi @@ -1,7 +1,7 @@ from prik.contracts import Arg, Complex128, Float64, Int32, Value, native_abi, native_call @native_abi("c") -class point: +class Point: def __init__( self, *, @@ -13,19 +13,19 @@ class point: axis: Int32 @native_abi("c") -class tagged_point: +class Tagged_Point: def __init__( self, *, weight: Complex128 = ... ) -> None: ... - position: point + position: Point weight: Complex128 @native_abi("c") def populate( - value: tagged_point, + value: Tagged_Point, x: Float64, axis: Int32, weight: Complex128 @@ -34,5 +34,7 @@ def populate( @native_abi("c") @native_call([Value(Arg(0))]) def score_by_value( - value: tagged_point + value: Tagged_Point ) -> Float64: ... + +__all__ = ["Point", "Tagged_Point", "populate", "score_by_value"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fborrowed_finalizer_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fborrowed_finalizer_f90/__init__.pyi index b1607c137..9c3cef4b9 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fborrowed_finalizer_f90/__init__.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fborrowed_finalizer_f90/__init__.pyi @@ -1 +1,3 @@ from . import fborrowed_finalizer_f90 + +__all__ = ["fborrowed_finalizer_f90"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fborrowed_finalizer_f90/fborrowed_finalizer_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fborrowed_finalizer_f90/fborrowed_finalizer_f90.pyi index 121fc86a7..34536eafb 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fborrowed_finalizer_f90/fborrowed_finalizer_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fborrowed_finalizer_f90/fborrowed_finalizer_f90.pyi @@ -1,14 +1,16 @@ from prik.contracts import Int32, destroy -class child: +class Child: @destroy def cleanup_child(self) -> None: ... -class parent: +class Parent: def __init__(self) -> None: ... - value: child + value: Child def get_final_count() -> Int32: ... def reset_final_count() -> None: ... + +__all__ = ["Child", "Parent", "get_final_count", "reset_final_count"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fclasses_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fclasses_f90/__init__.pyi index ace582376..137a9dce3 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fclasses_f90/__init__.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fclasses_f90/__init__.pyi @@ -1 +1,3 @@ from . import fclasses_f90 + +__all__ = ["fclasses_f90"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fclasses_f90/fclasses_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fclasses_f90/fclasses_f90.pyi index 1577dce2c..5eef0d5b3 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fclasses_f90/fclasses_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fclasses_f90/fclasses_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Allocatable, Annotated, Arg, Float64, Int64, Pass, Polymorphic, bind, native_call -class vector: +class Vector: def __init__( self, *, @@ -27,7 +27,7 @@ class vector: def magnitude(self) -> Float64: ... -class vector_store: +class Vector_Store: def __init__(self) -> None: ... values: Allocatable[Float64[:]] @@ -62,45 +62,45 @@ class vector_store: def make( n: Int64, fill_value: Float64 - ) -> vector_store: ... + ) -> Vector_Store: ... @native_call([Arg(0), Addr(Arg(1))]) def scale( - self: Annotated[vector, Polymorphic], + self: Annotated[Vector, Polymorphic], factor: Float64 ) -> None: ... @native_call([Addr(Arg(0)), Arg(1), Addr(Arg(2))]) def shift_vector( dx: Float64, - owner: Annotated[vector, Polymorphic], + owner: Annotated[Vector, Polymorphic], dy: Float64 ) -> None: ... def magnitude( - self: Annotated[vector, Polymorphic] + self: Annotated[Vector, Polymorphic] ) -> Float64: ... @native_call([Arg(0), Addr(Arg(1))]) def allocate_values( - self: Annotated[vector_store, Polymorphic], + self: Annotated[Vector_Store, Polymorphic], n: Int64 ) -> None: ... def set_values( - self: Annotated[vector_store, Polymorphic], + self: Annotated[Vector_Store, Polymorphic], source: Float64[::] ) -> None: ... @native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) def allocate_matrix( - self: Annotated[vector_store, Polymorphic], + self: Annotated[Vector_Store, Polymorphic], rows: Int64, cols: Int64 ) -> None: ... def set_matrix( - self: Annotated[vector_store, Polymorphic], + self: Annotated[Vector_Store, Polymorphic], source: Float64[::, ::] ) -> None: ... @@ -108,4 +108,17 @@ def set_matrix( def make_vector_store( n: Int64, fill_value: Float64 -) -> vector_store: ... +) -> Vector_Store: ... + +__all__ = [ + "Vector", + "Vector_Store", + "scale", + "shift_vector", + "magnitude", + "allocate_values", + "set_values", + "allocate_matrix", + "set_matrix", + "make_vector_store", +] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fconstructors_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fconstructors_f90/__init__.pyi index 544c88188..bec8b65e8 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fconstructors_f90/__init__.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fconstructors_f90/__init__.pyi @@ -1 +1,3 @@ from . import fconstructors_f90 + +__all__ = ["fconstructors_f90"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fconstructors_f90/fconstructors_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fconstructors_f90/fconstructors_f90.pyi index 59b81635e..ad28c4c5c 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fconstructors_f90/fconstructors_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fconstructors_f90/fconstructors_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Float64, Int32, destroy -class initialized: +class Initialized: def __init__( self, *, @@ -17,3 +17,5 @@ class initialized: def get_final_count() -> Int32: ... def reset_final_count() -> None: ... + +__all__ = ["Initialized", "get_final_count", "reset_final_count"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fderived_boundary_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fderived_boundary_f90/__init__.pyi index d78156d8f..e7303fbfa 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fderived_boundary_f90/__init__.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fderived_boundary_f90/__init__.pyi @@ -1 +1,3 @@ from . import fderived_boundary_f90 + +__all__ = ["fderived_boundary_f90"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fderived_boundary_f90/fderived_boundary_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fderived_boundary_f90/fderived_boundary_f90.pyi index 4040def79..89880a24d 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fderived_boundary_f90/fderived_boundary_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fderived_boundary_f90/fderived_boundary_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Arg, Float64, native_call -class point: +class Point: def __init__( self, *, @@ -11,30 +11,30 @@ class point: x: Float64 y: Float64 -class holder: +class Holder: def __init__( self, *, scale: Float64 = ... ) -> None: ... - origin: point + origin: Point scale: Float64 def point_sum( - p: point + p: Point ) -> Float64: ... @native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) def move_point( - p: point, + p: Point, dx: Float64, dy: Float64 ) -> None: ... @native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) def make_point_out( - p: point, + p: Point, x: Float64, y: Float64 ) -> None: ... @@ -43,13 +43,24 @@ def make_point_out( def make_point( x: Float64, y: Float64 -) -> point: ... +) -> Point: ... def set_holder_origin( - h: holder, - p: point + h: Holder, + p: Point ) -> None: ... def holder_origin_x( - h: holder + h: Holder ) -> Float64: ... + +__all__ = [ + "Point", + "Holder", + "point_sum", + "move_point", + "make_point_out", + "make_point", + "set_holder_origin", + "holder_origin_x", +] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/finheritance_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/finheritance_f90/__init__.pyi index 51e80c088..262ef465c 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/finheritance_f90/__init__.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/finheritance_f90/__init__.pyi @@ -1 +1,3 @@ from . import finheritance_f90 + +__all__ = ["finheritance_f90"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/finheritance_f90/finheritance_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/finheritance_f90/finheritance_f90.pyi index c4756c18d..9f0a8fb02 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/finheritance_f90/finheritance_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/finheritance_f90/finheritance_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Annotated, Arg, Float64, Pass, Polymorphic, bind, native_call -class base_shape: +class Base_Shape: def __init__( self, *, @@ -19,7 +19,7 @@ class base_shape: value: Float64 ) -> None: ... -class circle(base_shape): +class Circle(Base_Shape): def __init__( self, *, @@ -31,7 +31,7 @@ class circle(base_shape): @bind("circle_area") def area(self) -> Float64: ... -class box(base_shape): +class Box(Base_Shape): def __init__( self, *, @@ -44,23 +44,25 @@ class box(base_shape): def area(self) -> Float64: ... def base_area( - self: Annotated[base_shape, Polymorphic] + self: Annotated[Base_Shape, Polymorphic] ) -> Float64: ... @native_call([Arg(0), Addr(Arg(1))]) def base_set_size( - self: Annotated[base_shape, Polymorphic], + self: Annotated[Base_Shape, Polymorphic], value: Float64 ) -> None: ... def circle_area( - self: Annotated[circle, Polymorphic] + self: Annotated[Circle, Polymorphic] ) -> Float64: ... def box_area( - self: Annotated[box, Polymorphic] + self: Annotated[Box, Polymorphic] ) -> Float64: ... def describe_shape( - item: Annotated[base_shape, Polymorphic] + item: Annotated[Base_Shape, Polymorphic] ) -> Float64: ... + +__all__ = ["Base_Shape", "Circle", "Box", "base_area", "base_set_size", "circle_area", "box_area", "describe_shape"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fmodule_derived_alias_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fmodule_derived_alias_f90/__init__.pyi index 687107bfc..30261dca9 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fmodule_derived_alias_f90/__init__.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fmodule_derived_alias_f90/__init__.pyi @@ -1 +1,3 @@ from . import fmodule_derived_alias_f90 + +__all__ = ["fmodule_derived_alias_f90"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fmodule_derived_alias_f90/fmodule_derived_alias_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fmodule_derived_alias_f90/fmodule_derived_alias_f90.pyi index c8ce690a0..634834466 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fmodule_derived_alias_f90/fmodule_derived_alias_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fmodule_derived_alias_f90/fmodule_derived_alias_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Aliased, Allocatable, Annotated, Arg, Float64, Int32, Pass, native_call -class box: +class Box: def __init__(self) -> None: ... values: Allocatable[Float64[:]] @@ -13,7 +13,7 @@ class box: def values_sum(self) -> Float64: ... -current: Annotated[box, Aliased] +current: Annotated[Box, Aliased] @native_call([Addr(Arg(0))]) def allocate_current( @@ -23,3 +23,5 @@ def allocate_current( def deallocate_current() -> None: ... def current_sum() -> Float64: ... + +__all__ = ["Box", "current", "allocate_current", "deallocate_current", "current_sum"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/__init__.pyi index f370af40b..048039afe 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import derived_types_direct_bind_c_f90 + +__all__ = ["derived_types_direct_bind_c_f90"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/derived_types_direct_bind_c_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/derived_types_direct_bind_c_f90.pyi index 9ab5f98eb..ea96225ca 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/derived_types_direct_bind_c_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/derived_types_direct_bind_c_f90.pyi @@ -1,7 +1,7 @@ from prik.contracts import Float64, native_abi @native_abi("c") -class point: +class Point: def __init__( self, *, @@ -14,11 +14,13 @@ class point: @native_abi("c") def direct_sum( - value: point + value: Point ) -> Float64: ... @native_abi("c") def direct_shift( - value: point, + value: Point, delta: Float64 ) -> None: ... + +__all__ = ["Point", "direct_sum", "direct_shift"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/__init__.pyi index bfa13be19..59dd52705 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import derived_types_mixed_bind_c_f90 + +__all__ = ["derived_types_mixed_bind_c_f90"] diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/derived_types_mixed_bind_c_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/derived_types_mixed_bind_c_f90.pyi index 3f20c08a5..34bd41cb2 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/derived_types_mixed_bind_c_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/derived_types_mixed_bind_c_f90.pyi @@ -1,7 +1,7 @@ from prik.contracts import Arg, Float64, Value, native_abi, native_call @native_abi("c") -class point: +class Point: def __init__( self, *, @@ -14,11 +14,13 @@ class point: @native_abi("c") def direct_sum( - value: point + value: Point ) -> Float64: ... @native_abi("c") @native_call([Value(Arg(0))]) def adapted_sum_by_value( - value: point + value: Point ) -> Float64: ... + +__all__ = ["Point", "direct_sum", "adapted_sum_by_value"] diff --git a/tests/fortran/derived_types/end_to_end/test_abstract_hierarchy.py b/tests/fortran/derived_types/end_to_end/test_abstract_hierarchy.py index 4c13c1527..09ec762c8 100644 --- a/tests/fortran/derived_types/end_to_end/test_abstract_hierarchy.py +++ b/tests/fortran/derived_types/end_to_end/test_abstract_hierarchy.py @@ -27,24 +27,24 @@ def module(tmp_path_factory): def test_abstract_type_cannot_be_instantiated(module): """`type, abstract ::` has no instances, so its Python class has no constructor.""" with pytest.raises(TypeError, match="abstract native type and cannot be instantiated"): - module.shape_base() + module.Shape_Base() - assert "__init__" not in module.shape_base.__dict__ + assert "__init__" not in module.Shape_Base.__dict__ def test_extensions_are_python_subclasses_of_the_abstract_base(module): """Fortran `extends` becomes real Python inheritance, not copied members.""" - assert issubclass(module.circle, module.shape_base) - assert issubclass(module.square, module.shape_base) - assert module.circle.__mro__[:2] == (module.circle, module.shape_base) + assert issubclass(module.Circle, module.Shape_Base) + assert issubclass(module.Square, module.Shape_Base) + assert module.Circle.__mro__[:2] == (module.Circle, module.Shape_Base) - assert isinstance(module.circle(radius=np.float64(1.0)), module.shape_base) + assert isinstance(module.Circle(radius=np.float64(1.0)), module.Shape_Base) def test_deferred_bindings_dispatch_to_each_concrete_override(module): """A deferred binding names a contract; the dynamic type selects the body.""" - circle = module.circle(radius=np.float64(2.0)) - square = module.square(side=np.float64(3.0)) + circle = module.Circle(radius=np.float64(2.0)) + square = module.Square(side=np.float64(3.0)) assert circle.area() == pytest.approx(12.566370614, rel=1e-9) assert square.area() == pytest.approx(9.0) @@ -53,13 +53,13 @@ def test_deferred_bindings_dispatch_to_each_concrete_override(module): # The base declares the same bindings, and they resolve through the caller's # concrete type rather than through anything the abstract type implements. - assert module.shape_base.area(circle) == pytest.approx(circle.area()) - assert module.shape_base.area(square) == pytest.approx(square.area()) + assert module.Shape_Base.area(circle) == pytest.approx(circle.area()) + assert module.Shape_Base.area(square) == pytest.approx(square.area()) def test_inherited_bindings_and_components_reach_every_extension(module): """An implemented binding on the abstract base serves its extensions.""" - circle = module.circle(radius=np.float64(1.0)) + circle = module.Circle(radius=np.float64(1.0)) assert circle.side_count() == np.int32(0) circle.bump_sides() @@ -69,13 +69,13 @@ def test_inherited_bindings_and_components_reach_every_extension(module): def test_private_components_stay_off_the_generated_classes(module): """The hierarchy publishes only what its `private` statements allow.""" - assert {name for name in dir(module.shape_base) if not name.startswith("_")} == { + assert {name for name in dir(module.Shape_Base) if not name.startswith("_")} == { "area", "label", "side_count", "bump_sides", } - assert {name for name in dir(module.circle) if not name.startswith("_")} == { + assert {name for name in dir(module.Circle) if not name.startswith("_")} == { "area", "label", "side_count", @@ -86,7 +86,7 @@ def test_private_components_stay_off_the_generated_classes(module): def test_interoperable_type_keeps_its_layout_beside_the_hierarchy(module): """A `bind(c)` type in the same module still wraps through its own accessors.""" - box = module.extent(width=np.float64(3.0), height=np.float64(4.0)) + box = module.Extent(width=np.float64(3.0), height=np.float64(4.0)) assert box.width == np.float64(3.0) assert module.describe(box) == pytest.approx(12.0) diff --git a/tests/fortran/derived_types/end_to_end/test_borrowed_components.py b/tests/fortran/derived_types/end_to_end/test_borrowed_components.py index 44a364511..8f5992679 100644 --- a/tests/fortran/derived_types/end_to_end/test_borrowed_components.py +++ b/tests/fortran/derived_types/end_to_end/test_borrowed_components.py @@ -38,7 +38,7 @@ def test_borrowed_child_wrapper_never_finalizes_native_component( module = compiled_borrowed_component_module module.reset_final_count() - owner = module.parent() + owner = module.Parent() borrowed = owner.value del borrowed diff --git a/tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py b/tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py index 18d48cbf7..70c40f495 100644 --- a/tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py +++ b/tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py @@ -34,15 +34,15 @@ def test_fortran_default_constructor_keywords_and_finalization( module.reset_final_count() - defaulted = module.initialized() + defaulted = module.Initialized() assert defaulted.id == np.int32(7) assert defaulted.scale == np.float64(2.5) - partial = module.initialized(id=np.int32(11)) + partial = module.Initialized(id=np.int32(11)) assert partial.id == np.int32(11) assert partial.scale == np.float64(2.5) - keyword = module.initialized(id=np.int32(4), scale=np.float64(6.5)) + keyword = module.Initialized(id=np.int32(4), scale=np.float64(6.5)) assert keyword.id == np.int32(4) assert keyword.scale == np.float64(6.5) @@ -58,12 +58,12 @@ def test_fortran_default_constructor_keywords_and_finalization( assert module.get_final_count() == np.int32(3) with pytest.raises(TypeError): - module.initialized(np.int32(1)) + module.Initialized(np.int32(1)) gc.collect() assert module.get_final_count() == np.int32(4) with pytest.raises(TypeError): - module.initialized(missing=np.int32(1)) + module.Initialized(missing=np.int32(1)) gc.collect() assert module.get_final_count() == np.int32(5) diff --git a/tests/fortran/derived_types/end_to_end/test_derived_boundaries.py b/tests/fortran/derived_types/end_to_end/test_derived_boundaries.py index 987f52298..cce197d1e 100644 --- a/tests/fortran/derived_types/end_to_end/test_derived_boundaries.py +++ b/tests/fortran/derived_types/end_to_end/test_derived_boundaries.py @@ -32,7 +32,7 @@ def test_scalar_derived_types_cross_procedure_boundaries( pyi_parity_build_mode, ) - point = module.point() + point = module.Point() point.x = np.float64(1.0) point.y = np.float64(2.0) assert not hasattr(point, "hidden") @@ -44,21 +44,21 @@ def test_scalar_derived_types_cross_procedure_boundaries( assert point.x == np.float64(5.0) assert point.y == np.float64(7.0) - out_point = module.point() + out_point = module.Point() assert module.make_point_out(out_point, np.float64(8.0), np.float64(9.0)) is None assert out_point.x == np.float64(8.0) assert out_point.y == np.float64(9.0) result_point = module.make_point(np.float64(10.0), np.float64(11.0)) - assert isinstance(result_point, module.point) + assert isinstance(result_point, module.Point) assert result_point.x == np.float64(10.0) assert result_point.y == np.float64(11.0) - holder = module.holder() + holder = module.Holder() holder.scale = np.float64(2.5) assert module.set_holder_origin(holder, result_point) is None origin = holder.origin - assert isinstance(origin, module.point) + assert isinstance(origin, module.Point) assert origin.x == np.float64(10.0) origin.x = np.float64(12.0) assert module.holder_origin_x(holder) == np.float64(12.0) diff --git a/tests/fortran/derived_types/end_to_end/test_derived_direct_entrypoint_routing.py b/tests/fortran/derived_types/end_to_end/test_derived_direct_entrypoint_routing.py index 12814e415..dc6e37b43 100644 --- a/tests/fortran/derived_types/end_to_end/test_derived_direct_entrypoint_routing.py +++ b/tests/fortran/derived_types/end_to_end/test_derived_direct_entrypoint_routing.py @@ -32,7 +32,7 @@ def test_derived_all_direct_route_keeps_generated_type_support_separate( pyi_parity_build_mode, ) - value = module.point(x=np.float64(1.5), y=np.float64(2.5)) + value = module.Point(x=np.float64(1.5), y=np.float64(2.5)) references_before = sys.getrefcount(value) assert module.direct_sum(value) == np.float64(4.0) shifted = module.direct_shift(value, np.float64(2.0)) @@ -74,7 +74,7 @@ def test_derived_mixed_route_adapts_only_by_value_aggregate( pyi_parity_build_mode, ) - value = module.point(x=np.float64(2.0), y=np.float64(3.0)) + value = module.Point(x=np.float64(2.0), y=np.float64(3.0)) assert module.direct_sum(value) == np.float64(5.0) assert module.adapted_sum_by_value(value) == np.float64(5.0) @@ -100,7 +100,7 @@ def test_derived_mixed_route_matches_edited_source_free_contract(tmp_path: Path) tmp_path, module_name=stem, source_text=source, contract_text=contract ) - value = module.point(x=np.float64(2.0), y=np.float64(3.0)) + value = module.Point(x=np.float64(2.0), y=np.float64(3.0)) assert module.direct_sum(value) == np.float64(5.0) assert module.adapted_sum_by_value(value) == np.float64(5.0) bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() diff --git a/tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py b/tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py index c29886382..096ada377 100644 --- a/tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py +++ b/tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py @@ -445,7 +445,7 @@ def test_value_copy_and_optional_derived_inputs_match_source_oracle(tmp_path: Pa assert source_module.update_point(source_point) is None assert source_point.x == np.float64(11.0) assert source_point.y == np.float64(22.0) - source_filled = source_module.point() + source_filled = source_module.Point() assert source_module.fill_point(source_filled) is None assert source_filled.x == np.float64(31.0) assert source_filled.y == np.float64(32.0) diff --git a/tests/fortran/derived_types/end_to_end/test_generic_constructor.py b/tests/fortran/derived_types/end_to_end/test_generic_constructor.py index 6d9d4fd9f..2e2f90f08 100644 --- a/tests/fortran/derived_types/end_to_end/test_generic_constructor.py +++ b/tests/fortran/derived_types/end_to_end/test_generic_constructor.py @@ -26,16 +26,16 @@ def module(tmp_path_factory): def test_type_without_a_constructor_interface_keeps_keyword_fields(module): """No user constructor: the keyword-field `__init__` is unchanged.""" - value = module.plain(tag=np.int32(5)) + value = module.Plain(tag=np.int32(5)) assert value.tag == np.int32(5) def test_constructor_interface_overloads_init_from_its_specifics(module): """`interface `: each specific becomes an accepted signature.""" - empty = module.box() - from_count = module.box(np.int32(7)) - from_value = module.box(np.float64(2.5)) + empty = module.Box() + from_count = module.Box(np.int32(7)) + from_value = module.Box(np.float64(2.5)) assert (empty.count, empty.value) == (np.int32(0), np.float64(0.0)) assert (from_count.count, from_count.value) == (np.int32(7), np.float64(7.0)) @@ -45,13 +45,13 @@ def test_constructor_interface_overloads_init_from_its_specifics(module): def test_constructor_overload_rejects_an_unmatched_signature(module): """A call matching no specific is refused rather than guessed at.""" with pytest.raises(TypeError, match="no matching overload"): - module.box("not a supported signature") + module.Box("not a supported signature") def test_constructed_instances_are_independent_wrapper_objects(module): """Each accepted signature produces its own wrapper-owned instance.""" - first = module.box(np.int32(1)) - second = module.box(np.int32(2)) + first = module.Box(np.int32(1)) + second = module.Box(np.int32(2)) assert first is not second first.count = np.int32(9) diff --git a/tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py b/tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py index 273d486ac..281f4821f 100644 --- a/tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py +++ b/tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py @@ -31,16 +31,16 @@ def test_fortran_extension_types_generate_python_inheritance( pyi_parity_build_mode, ) - assert issubclass(module.circle, module.base_shape) - assert issubclass(module.box, module.base_shape) + assert issubclass(module.Circle, module.Base_Shape) + assert issubclass(module.Box, module.Base_Shape) - base = module.base_shape() + base = module.Base_Shape() base.size = np.float64(3.0) assert base.area() == np.float64(3.0) assert module.describe_shape(base) == np.float64(3.0) - circle = module.circle() - assert isinstance(circle, module.base_shape) + circle = module.Circle() + assert isinstance(circle, module.Base_Shape) circle.set_size(np.float64(5.0)) circle.radius = np.float64(2.0) assert circle.size == np.float64(5.0) @@ -48,11 +48,11 @@ def test_fortran_extension_types_generate_python_inheritance( np.testing.assert_allclose(circle.area(), expected_circle_area) np.testing.assert_allclose(module.describe_shape(circle), expected_circle_area) - module.base_shape.set_size(circle, np.float64(7.0)) + module.Base_Shape.set_size(circle, np.float64(7.0)) assert circle.size == np.float64(7.0) - box = module.box() - assert isinstance(box, module.base_shape) + box = module.Box() + assert isinstance(box, module.Base_Shape) box.set_size(np.float64(2.0)) box.width = np.float64(3.0) assert box.area() == np.float64(32.0) diff --git a/tests/fortran/derived_types/end_to_end/test_module_derived_aliases.py b/tests/fortran/derived_types/end_to_end/test_module_derived_aliases.py index 3b7c282a1..c077c48d1 100644 --- a/tests/fortran/derived_types/end_to_end/test_module_derived_aliases.py +++ b/tests/fortran/derived_types/end_to_end/test_module_derived_aliases.py @@ -40,7 +40,7 @@ def test_aliased_derived_module_object_borrows_native_state( ) current = module.current - assert isinstance(current, module.box) + assert isinstance(current, module.Box) values = current.values assert isinstance(values, AllocatableArray) assert values.owner is current @@ -55,7 +55,7 @@ def test_aliased_derived_module_object_borrows_native_state( assert module.current_sum() == np.float64(15.0) assert module.current.values_sum() == np.float64(15.0) - owned = module.box() + owned = module.Box() owned.allocate_values(np.int32(2)) owned.values.to_numpy()[0] = np.float64(20.0) assert owned.values_sum() == np.float64(22.0) diff --git a/tests/fortran/derived_types/end_to_end/test_opaque_layout.py b/tests/fortran/derived_types/end_to_end/test_opaque_layout.py index 0578d6f5c..abe0b62b1 100644 --- a/tests/fortran/derived_types/end_to_end/test_opaque_layout.py +++ b/tests/fortran/derived_types/end_to_end/test_opaque_layout.py @@ -42,7 +42,7 @@ def test_bind_c_derived_types_use_accessors_and_fortran_value_copy( assert "type(prik_type_tagged_point), pointer :: value" in bridge_source assert "result = native_score_by_value(value)" in bridge_source - value = module.tagged_point() + value = module.Tagged_Point() module.populate( value, np.float64(2.5), diff --git a/tests/fortran/derived_types/end_to_end/test_type_accessibility.py b/tests/fortran/derived_types/end_to_end/test_type_accessibility.py index ae7a8260b..7d6e1d9a4 100644 --- a/tests/fortran/derived_types/end_to_end/test_type_accessibility.py +++ b/tests/fortran/derived_types/end_to_end/test_type_accessibility.py @@ -7,7 +7,10 @@ import numpy as np import pytest -from tests.fortran._support.wrapper_build import _build_source_and_import +from prik.parsers.fortran import parse_fortran_project +from prik.pipeline.pyi import emit_module_stubs +from prik.semantics.fortran2ir import fortran_project_to_semantic_modules +from tests.fortran._support.wrapper_build import _build_source_and_import, _build_text_and_import pytestmark = pytest.mark.fortran_end_to_end @@ -18,6 +21,25 @@ "type_accessibility_wrapper.h", } +DEPENDENCY_SOURCE = """ +module dependency_home + implicit none + type :: box + integer :: value + end type box +end module dependency_home + +module dependency_consumer + use dependency_home, only : crate => box + implicit none +contains + integer function crate_value(item) result(value) + type(crate), intent(in) :: item + value = item%value + end function crate_value +end module dependency_consumer +""" + def test_accessibility_statements_shape_the_generated_class(tmp_path: Path): """Only components and bindings the type publishes reach Python. @@ -28,12 +50,42 @@ def test_accessibility_statements_shape_the_generated_class(tmp_path: Path): """ module = _build_source_and_import(SOURCE, tmp_path, GENERATED) - assert hasattr(module, "gated") - members = {name for name in dir(module.gated) if not name.startswith("_")} + assert hasattr(module, "Gated") + members = {name for name in dir(module.Gated) if not name.startswith("_")} assert members == {"shown", "step", "peek"} - instance = module.gated(shown=np.int32(5)) + instance = module.Gated(shown=np.int32(5)) assert instance.shown == np.int32(5) assert instance.peek() == np.int32(7) instance.step() assert instance.peek() == np.int32(8) + + +def test_declaration_dependency_accessibility_and_python_publication_are_separate(tmp_path: Path): + """The semantic route remains valid while runtime and contract omit its alias.""" + source = tmp_path / "dependency_accessibility.f90" + module = _build_text_and_import( + DEPENDENCY_SOURCE, + source.name, + tmp_path, + { + "bind_c_dependency_accessibility_wrapper.f90", + "dependency_accessibility_wrapper.c", + "dependency_accessibility_wrapper.h", + }, + ) + stubs = emit_module_stubs( + fortran_project_to_semantic_modules(parse_fortran_project([source])), + normalize_public_names=True, + ) + + consumer_contract = stubs["dependency_consumer"] + # A renamed type is still a class, spelled as one wherever the contract + # writes it: in its import and in the annotations naming it. + assert "from .dependency_home import Box as Crate" in consumer_contract + assert "item: Crate" in consumer_contract + assert consumer_contract.rstrip().endswith('__all__ = ["crate_value"]') + assert not any(name.casefold() == "crate" for name in vars(module.dependency_consumer)) + + item = module.dependency_home.Box(value=np.int32(7)) + assert module.dependency_consumer.crate_value(item) == np.int32(7) diff --git a/tests/fortran/derived_types/end_to_end/test_type_bound_methods.py b/tests/fortran/derived_types/end_to_end/test_type_bound_methods.py index d3afd24c2..01129d999 100644 --- a/tests/fortran/derived_types/end_to_end/test_type_bound_methods.py +++ b/tests/fortran/derived_types/end_to_end/test_type_bound_methods.py @@ -31,7 +31,7 @@ def test_modern_fortran_derived_type_exposes_class_and_type_bound_methods( pyi_parity_build_mode, ) - assert "make(n, fill_value) -> vector_store" in module.vector_store.make.__doc__ - assert "n : int64" in module.vector_store.make.__doc__ - assert "wrapped native instance" not in module.vector_store.make.__doc__ + assert "make(n, fill_value) -> Vector_Store" in module.Vector_Store.make.__doc__ + assert "n : int64" in module.Vector_Store.make.__doc__ + assert "wrapped native instance" not in module.Vector_Store.make.__doc__ _assert_modern_class_examples(module) diff --git a/tests/fortran/derived_types/end_to_end/test_types_across_modules.py b/tests/fortran/derived_types/end_to_end/test_types_across_modules.py new file mode 100644 index 000000000..82fd4370b --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/test_types_across_modules.py @@ -0,0 +1,292 @@ +"""A type is one type wherever a procedure of another module takes or returns it. + +Each module becomes its own namespace, and a type's class and the helpers +wrapping it are defined in the namespace of the module declaring it. A +procedure using the type from another module reaches them there, and two +modules may each declare a type spelled alike without either replacing the +other. +""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_sources_and_import + +pytestmark = pytest.mark.fortran_end_to_end + +SHAPES_SOURCE = """\ +module shapes + implicit none + type :: box + integer :: value = 0 + end type box + type, extends(box) :: tagged_box + integer :: tag = 0 + end type tagged_box +end module shapes +""" + +OPS_SOURCE = """\ +module ops + use shapes, only: box, tagged_box + implicit none + private + public :: holder, boxed, total, visit, describe, weigh, maybe_box, producer, consumer + type :: holder + type(box) :: inner + end type holder + abstract interface + function producer() result(out) + import :: box + type(box) :: out + end function producer + subroutine consumer(item) + import :: box + type(box), intent(in) :: item + end subroutine consumer + end interface + interface weigh + module procedure weigh_box, weigh_int + end interface weigh +contains + function boxed(v) result(out) + integer, intent(in) :: v + type(box) :: out + out%value = v + end function boxed + + integer function total(make) + procedure(producer) :: make + type(box) :: item + item = make() + total = item%value + end function total + + subroutine visit(fn) + procedure(consumer) :: fn + type(box) :: item + item%value = 41 + call fn(item) + end subroutine visit + + integer function describe(item) + class(box), intent(in) :: item + select type (item) + type is (tagged_box) + describe = 2 + class default + describe = 1 + end select + end function describe + + integer function weigh_box(item) + type(box), intent(in) :: item + weigh_box = item%value + end function weigh_box + + integer function weigh_int(n) + integer, intent(in) :: n + weigh_int = -n + end function weigh_int + + function maybe_box(v) result(out) + integer, intent(in) :: v + type(box), allocatable :: out + allocate(out) + out%value = v + end function maybe_box +end module ops +""" + +FIRST_SOURCE = """\ +module first_mod + implicit none + type :: box + integer :: value = 1 + end type box +contains + function make_first() result(out) + type(box) :: out + out%value = 10 + end function make_first +end module first_mod +""" + +SECOND_SOURCE = """\ +module second_mod + implicit none + type :: box + real(8) :: weight = 2.0d0 + end type box + abstract interface + function producer() result(out) + import :: box + type(box) :: out + end function producer + end interface +contains + real(8) function weigh(make) result(total) + procedure(producer) :: make + type(box) :: item + item = make() + total = item%weight + end function weigh +end module second_mod +""" + + +BASE_SOURCE = """\ +module zeta_base + implicit none + type :: shape + integer :: sides = 0 + end type shape +contains + integer function sides_of(item) + class(shape), intent(in) :: item + sides_of = item%sides + end function sides_of +end module zeta_base +""" + +EXTENSION_SOURCE = """\ +module alpha_child + use zeta_base, only: shape + implicit none + type, extends(shape) :: square + integer :: edge = 1 + end type square +contains + function make_square(edge) result(out) + integer, intent(in) :: edge + type(square) :: out + out%sides = 4 + out%edge = edge + end function make_square +end module alpha_child +""" + + +@pytest.fixture(scope="module") +def modules(tmp_path_factory: pytest.TempPathFactory): + """Build `shapes` and the `ops` module using its types once.""" + module, _ = _build_sources_and_import( + [("shapes.f90", SHAPES_SOURCE), ("ops.f90", OPS_SOURCE)], + tmp_path_factory.mktemp("across"), + ) + return module.shapes, module.ops + + +def test_a_returned_type_is_the_declaring_module_class(modules): + shapes, ops = modules + + item = ops.boxed(np.int32(3)) + + assert type(item) is shapes.Box + assert item.value == 3 + + +def test_an_allocatable_result_is_the_declaring_module_class(modules): + shapes, ops = modules + + item = ops.maybe_box(np.int32(4)) + + assert type(item) is shapes.Box + assert item.value == 4 + + +def test_a_callback_result_is_checked_against_the_declaring_module_class(modules): + shapes, ops = modules + + assert ops.total(lambda: shapes.Box(value=np.int32(9))) == 9 + + +def test_a_callback_argument_is_the_declaring_module_class(modules): + shapes, ops = modules + seen = [] + + ops.visit(lambda item: seen.append((type(item), int(item.value)))) + + assert seen == [(shapes.Box, 41)] + + +def test_a_polymorphic_argument_accepts_each_declaring_module_class(modules): + shapes, ops = modules + + assert ops.describe(shapes.Box()) == 1 + assert ops.describe(shapes.Tagged_Box()) == 2 + # A rejection names each accepted class the way its contract declares it. + with pytest.raises(TypeError, match=r"wrapper type: Tagged_Box, Box$"): + ops.describe(ops.Holder()) + + +def test_documentation_names_another_module_type_as_it_is_published(modules): + """The build is named after its first source, `shapes`, like that module. + + Completing the merged build counted `ops`'s use of `box` as an import even + though the build declares `box`, so the class took `Box_2` and every + docstring naming it disagreed with the published `Box`. + """ + shapes, ops = modules + + assert ops.boxed.__doc__.splitlines()[0] == "boxed(v) -> Box" + assert shapes.Box.__doc__.splitlines()[0] == "Box" + + +def test_a_generic_dispatches_on_the_declaring_module_class(modules): + shapes, ops = modules + + assert ops.weigh(shapes.Box(value=np.int32(5))) == 5 + assert ops.weigh(np.int32(5)) == -5 + + +def test_a_component_of_another_module_type_is_that_module_class(modules): + shapes, ops = modules + holder = ops.Holder() + + assert type(holder.inner) is shapes.Box + holder.inner = shapes.Box(value=np.int32(12)) + assert holder.inner.value == 12 + + +def test_two_modules_may_each_declare_a_type_spelled_alike(tmp_path: Path): + """Each `box` keeps its own class, constructor, and helpers. + + Keying them by the native spelling alone gave both types one constructor + symbol, and the build stopped there. + """ + module, _ = _build_sources_and_import( + [("first.f90", FIRST_SOURCE), ("second.f90", SECOND_SOURCE)], + tmp_path, + ) + first, second = module.first_mod, module.second_mod + + assert first.Box is not second.Box + made = first.make_first() + assert type(made) is first.Box + assert made.value == 10 + assert second.weigh(lambda: second.Box(weight=np.float64(3.5))) == 3.5 + + +def test_a_type_may_extend_one_another_module_declares(tmp_path: Path): + """The extension is a subclass of the base where the base is defined. + + `alpha_child` sorts before `zeta_base`, so its namespace is set up after + the base's only because inheritance orders them. Its class names the base + there instead of looking for it among its own. + """ + module, _ = _build_sources_and_import( + [("zeta_base.f90", BASE_SOURCE), ("alpha_child.f90", EXTENSION_SOURCE)], + tmp_path, + ) + base, child = module.zeta_base, module.alpha_child + + square = child.make_square(np.int32(3)) + + assert type(square) is child.Square + assert issubclass(child.Square, base.Shape) + assert (square.sides, square.edge) == (4, 3) + assert base.sides_of(square) == 4 + assert base.sides_of(base.Shape(sides=np.int32(2))) == 2 diff --git a/tests/fortran/derived_types/policy/test_merged_contract_names.py b/tests/fortran/derived_types/policy/test_merged_contract_names.py new file mode 100644 index 000000000..3495d0ccf --- /dev/null +++ b/tests/fortran/derived_types/policy/test_merged_contract_names.py @@ -0,0 +1,56 @@ +"""A build names a type once, however many of its modules use it. + +A build merges its source modules into one, so a type one of them imports from +another is declared by the merged module itself. It is not an import there, +and it must not compete with its own declaration for a name. +""" + +from pathlib import Path + +from prik.parsers.fortran import parse_fortran_project +from prik.pipeline.build import _apply_source_python_exports, _merge_wrapper_modules +from prik.policy.exports import complete_python_export_policy +from prik.semantics import models +from prik.semantics.fortran2ir import fortran_project_to_semantic_modules + +SOURCES = """\ +module shapes + implicit none + type :: box + integer :: value = 0 + end type box +end module shapes + +module ops + use shapes, only: box + implicit none + private + public :: boxed +contains + function boxed(v) result(out) + integer, intent(in) :: v + type(box) :: out + out%value = v + end function boxed +end module ops +""" + + +def test_a_merged_build_names_a_type_its_modules_share_once(tmp_path: Path): + """Named like the module declaring the type, the build still spells it `Box`. + + `ops` uses `box` without publishing it. Counting that use as an import put + a second `Box` in the ledger ahead of the declaration, which took `Box_2`. + """ + (tmp_path / "project.f90").write_text(SOURCES, encoding="utf-8") + modules = fortran_project_to_semantic_modules(parse_fortran_project(str(tmp_path))) + _apply_source_python_exports(modules) + merged = _merge_wrapper_modules(modules, name="shapes") + + complete_python_export_policy(merged) + + box = next(item for item in merged.classes if item.name == "box") + boxed = next(item for item in merged.functions if item.name == "boxed") + assert models.completed_contract_name(box) == "Box" + assert boxed.return_type.metadata[models.CONTRACT_NAME_METADATA] == "Box" + assert "box" not in merged.metadata[models.CONTRACT_IMPORT_NAMES_METADATA] diff --git a/tests/fortran/derived_types/semantics/test_derived_type_identity.py b/tests/fortran/derived_types/semantics/test_derived_type_identity.py index c855d47aa..36834dff8 100644 --- a/tests/fortran/derived_types/semantics/test_derived_type_identity.py +++ b/tests/fortran/derived_types/semantics/test_derived_type_identity.py @@ -1,6 +1,9 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" -from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik.semantics.fortran2ir import ( + fortran_file_to_semantic_modules, + fortran_module_to_semantic_module, +) from tests.fortran._support.semantic_conversion import get_function from prik.parsers.fortran import parse_fortran_file as parse_fortran_source @@ -30,3 +33,94 @@ def test_procedure_local_derived_type_rename_uses_origin_type_identity(): "representation": "opaque", "import_scope": "procedure", } + + +def test_non_only_rename_does_not_choose_between_derived_type_routes(): + """An ambiguous name and a renamed-away name have no invented type owner.""" + parsed = parse_fortran_source( + """ +module types_mod + type :: x + integer :: value + end type x + type :: y + integer :: value + end type y +end module types_mod + +module consumer + use types_mod, x => y +contains + subroutine take_x(value) + type(x), intent(in) :: value + end subroutine take_x + subroutine take_y(value) + type(y), intent(in) :: value + end subroutine take_y +end module consumer +""" + ) + modules = {module.name: module for module in fortran_file_to_semantic_modules(parsed)} + + type_x = get_function(modules["consumer"], "take_x").arguments[0].semantic_type + type_y = get_function(modules["consumer"], "take_y").arguments[0].semantic_type + + assert type_x.name == "x" + assert "external_type_ref" not in type_x.metadata + assert type_y.name == "y" + assert "external_type_ref" not in type_y.metadata + + +def test_unindexed_non_only_rename_does_not_resurrect_the_source_spelling(): + parsed = parse_fortran_source( + """ +module consumer + use unavailable_types, x => y +contains + subroutine take_x(value) + type(x), intent(in) :: value + end subroutine take_x + subroutine take_y(value) + type(y), intent(in) :: value + end subroutine take_y +end module consumer +""" + ) + module = fortran_module_to_semantic_module(parsed) + + type_x = get_function(module, "take_x").arguments[0].semantic_type + type_y = get_function(module, "take_y").arguments[0].semantic_type + + assert type_x.metadata["external_type_ref"]["origin_module"] == "unavailable_types" + assert type_x.metadata["external_type_ref"]["name"] == "y" + assert "external_type_ref" not in type_y.metadata + + +def test_parsed_module_without_types_is_not_an_opaque_type_route(): + parsed = parse_fortran_source( + """ +module types_mod + type :: point + integer :: value + end type point +end module types_mod + +module constants_mod + integer, parameter :: count = 1 +end module constants_mod + +module consumer + use types_mod, only : point + use constants_mod +contains + subroutine take(value) + type(point), intent(in) :: value + end subroutine take +end module consumer +""" + ) + modules = {module.name: module for module in fortran_file_to_semantic_modules(parsed)} + + point = get_function(modules["consumer"], "take").arguments[0].semantic_type + + assert point.metadata["external_type_ref"]["origin_module"] == "types_mod" diff --git a/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py b/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py index 23927f01a..23d50d4f7 100644 --- a/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py +++ b/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py @@ -10,6 +10,7 @@ FortranProcedureSignature, FortranProject, FortranUseMapping, + FortranUseStatement, FortranVariable, ) from prik.semantics.fortran2ir import ( @@ -46,10 +47,10 @@ def test_converter_preserves_imported_derived_contexts_through_dispatch_paths(): ) module = FortranModule( name="consumer", - uses={ - "plain_mod": [], - "types_mod": [FortranUseMapping(source="state_t", target="local_state")], - }, + uses=[ + FortranUseStatement("plain_mod"), + FortranUseStatement("types_mod", True, (FortranUseMapping(source="state_t", target="local_state"),)), + ], variables=[FortranVariable(name="module_state", base_type="derived", kind="local_state")], procedures=[proc], derived_types=[dtype], @@ -142,7 +143,7 @@ def test_abstract_type_identity_is_module_qualified_and_available_project_wide() ) consumer = FortranModule( name="consumer", - uses={"abstract_owner": [FortranUseMapping(source="item_t")]}, + uses=[FortranUseStatement("abstract_owner", True, (FortranUseMapping(source="item_t"),))], procedures=[ FortranProcedureSignature( name="consume", diff --git a/tests/fortran/enumerations/end_to_end/fixtures/contracts/fenums_f90/__init__.pyi b/tests/fortran/enumerations/end_to_end/fixtures/contracts/fenums_f90/__init__.pyi index 4fad6563f..3073c975f 100644 --- a/tests/fortran/enumerations/end_to_end/fixtures/contracts/fenums_f90/__init__.pyi +++ b/tests/fortran/enumerations/end_to_end/fixtures/contracts/fenums_f90/__init__.pyi @@ -1 +1,3 @@ from . import fenums_f90 + +__all__ = ["fenums_f90"] diff --git a/tests/fortran/enumerations/end_to_end/fixtures/contracts/fenums_f90/fenums_f90.pyi b/tests/fortran/enumerations/end_to_end/fixtures/contracts/fenums_f90/fenums_f90.pyi index 67831c97b..3111cce95 100644 --- a/tests/fortran/enumerations/end_to_end/fixtures/contracts/fenums_f90/fenums_f90.pyi +++ b/tests/fortran/enumerations/end_to_end/fixtures/contracts/fenums_f90/fenums_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Arg, Final, Int32, native_call -class paint: +class Paint: def __init__( self, *, @@ -21,3 +21,5 @@ yellow: Final[Int32] = 11 def round_trip_color( color: Int32 ) -> Int32: ... + +__all__ = ["Paint", "red", "blue", "green", "yellow", "round_trip_color"] diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/__init__.pyi b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/__init__.pyi index 66217b5f6..94969905f 100644 --- a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import enumerations_direct_bind_c_f90 + +__all__ = ["enumerations_direct_bind_c_f90"] diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/enumerations_direct_bind_c_f90.pyi b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/enumerations_direct_bind_c_f90.pyi index 9765eb383..77bff3d4d 100644 --- a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/enumerations_direct_bind_c_f90.pyi +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/enumerations_direct_bind_c_f90.pyi @@ -18,3 +18,5 @@ def direct_round_trip( def direct_next( state: Int32 ) -> Int32: ... + +__all__ = ["terminal", "stopped", "ready", "running", "direct_round_trip", "direct_next"] diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/__init__.pyi b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/__init__.pyi index 403943be5..0fe15de44 100644 --- a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import enumerations_mixed_bind_c_f90 + +__all__ = ["enumerations_mixed_bind_c_f90"] diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/enumerations_mixed_bind_c_f90.pyi b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/enumerations_mixed_bind_c_f90.pyi index 4e1caf98f..25cf80aa3 100644 --- a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/enumerations_mixed_bind_c_f90.pyi +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/enumerations_mixed_bind_c_f90.pyi @@ -15,3 +15,5 @@ def direct_round_trip( def adapted_next( state: Int32 ) -> Int32: ... + +__all__ = ["stopped", "ready", "running", "direct_round_trip", "adapted_next"] diff --git a/tests/fortran/enumerations/end_to_end/test_enum_runtime.py b/tests/fortran/enumerations/end_to_end/test_enum_runtime.py index 52e04ddee..ad59ca6df 100644 --- a/tests/fortran/enumerations/end_to_end/test_enum_runtime.py +++ b/tests/fortran/enumerations/end_to_end/test_enum_runtime.py @@ -39,7 +39,7 @@ def test_fortran_enums_preserve_integer_runtime_surface( assert not hasattr(module, "Enum") assert not hasattr(module, "IntEnum") - sample = module.paint() + sample = module.Paint() assert sample.color == np.int32(-1) sample.color = np.int32(module.yellow) assert sample.color == np.int32(11) diff --git a/tests/fortran/enumerations/semantics/test_enum_semantics.py b/tests/fortran/enumerations/semantics/test_enum_semantics.py index afae9ebfb..918ca9a75 100644 --- a/tests/fortran/enumerations/semantics/test_enum_semantics.py +++ b/tests/fortran/enumerations/semantics/test_enum_semantics.py @@ -4,6 +4,7 @@ from prik.parsers.fortran import parse_fortran_file as parse_fortran_source +from prik.policy.exports import complete_python_export_policy from prik.printers import emit_module from prik.semantics.fortran2ir import fortran_module_to_semantic_module @@ -22,6 +23,7 @@ def test_fortran_enums_preserve_values_in_generated_pyi_contract(): ("yellow", "11"), ] assert constants["red"].semantic_type.metadata["fortran_bind_c"] is True + complete_python_export_policy(semantic) stub = emit_module(semantic) assert "color: Int32 = red" not in stub assert "color: Int32 = ..." in stub diff --git a/tests/fortran/error_handling/end_to_end/fixtures/contracts/fopenmp_runtime_f90/__init__.pyi b/tests/fortran/error_handling/end_to_end/fixtures/contracts/fopenmp_runtime_f90/__init__.pyi index 282a23bc4..dfe8ecebb 100644 --- a/tests/fortran/error_handling/end_to_end/fixtures/contracts/fopenmp_runtime_f90/__init__.pyi +++ b/tests/fortran/error_handling/end_to_end/fixtures/contracts/fopenmp_runtime_f90/__init__.pyi @@ -1 +1,3 @@ from . import fopenmp_runtime_f90 + +__all__ = ["fopenmp_runtime_f90"] diff --git a/tests/fortran/error_handling/end_to_end/fixtures/contracts/fopenmp_runtime_f90/fopenmp_runtime_f90.pyi b/tests/fortran/error_handling/end_to_end/fixtures/contracts/fopenmp_runtime_f90/fopenmp_runtime_f90.pyi index f64f6ca2b..a7eace268 100644 --- a/tests/fortran/error_handling/end_to_end/fixtures/contracts/fopenmp_runtime_f90/fopenmp_runtime_f90.pyi +++ b/tests/fortran/error_handling/end_to_end/fixtures/contracts/fopenmp_runtime_f90/fopenmp_runtime_f90.pyi @@ -3,3 +3,5 @@ from prik.contracts import Float64 def parallel_sum( values: Float64[::] ) -> Float64: ... + +__all__ = ["parallel_sum"] diff --git a/tests/fortran/error_handling/end_to_end/fixtures/contracts/fruntime_recursion_f90/__init__.pyi b/tests/fortran/error_handling/end_to_end/fixtures/contracts/fruntime_recursion_f90/__init__.pyi index fa20da782..b531762f4 100644 --- a/tests/fortran/error_handling/end_to_end/fixtures/contracts/fruntime_recursion_f90/__init__.pyi +++ b/tests/fortran/error_handling/end_to_end/fixtures/contracts/fruntime_recursion_f90/__init__.pyi @@ -1 +1,3 @@ from . import fruntime_recursion_f90 + +__all__ = ["fruntime_recursion_f90"] diff --git a/tests/fortran/error_handling/end_to_end/fixtures/contracts/fruntime_recursion_f90/fruntime_recursion_f90.pyi b/tests/fortran/error_handling/end_to_end/fixtures/contracts/fruntime_recursion_f90/fruntime_recursion_f90.pyi index b768d70dc..c136269e6 100644 --- a/tests/fortran/error_handling/end_to_end/fixtures/contracts/fruntime_recursion_f90/fruntime_recursion_f90.pyi +++ b/tests/fortran/error_handling/end_to_end/fixtures/contracts/fruntime_recursion_f90/fruntime_recursion_f90.pyi @@ -9,3 +9,5 @@ def factorial( def add_one( n: Int32 ) -> Int32: ... + +__all__ = ["factorial", "add_one"] diff --git a/tests/fortran/functions/end_to_end/fixtures/contracts/basic_subroutine/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/contracts/basic_subroutine/__init__.pyi index c301d4d35..e7dd36493 100644 --- a/tests/fortran/functions/end_to_end/fixtures/contracts/basic_subroutine/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/contracts/basic_subroutine/__init__.pyi @@ -1 +1,3 @@ from . import m1 + +__all__ = ["m1"] diff --git a/tests/fortran/functions/end_to_end/fixtures/contracts/basic_subroutine/m1.pyi b/tests/fortran/functions/end_to_end/fixtures/contracts/basic_subroutine/m1.pyi index 96c7bb126..676d8cf7d 100644 --- a/tests/fortran/functions/end_to_end/fixtures/contracts/basic_subroutine/m1.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/contracts/basic_subroutine/m1.pyi @@ -5,3 +5,5 @@ def add1( n: Int32, x: Float64[n] ) -> None: ... + +__all__ = ["add1"] diff --git a/tests/fortran/functions/end_to_end/fixtures/contracts/blas_like/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/contracts/blas_like/__init__.pyi index 6f3ea33a8..83b9b0ec0 100644 --- a/tests/fortran/functions/end_to_end/fixtures/contracts/blas_like/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/contracts/blas_like/__init__.pyi @@ -16,3 +16,5 @@ def ddot_like( x: Float64[n], y: Float64[n] ) -> Float64: ... + +__all__ = ["daxpy_like", "ddot_like"] diff --git a/tests/fortran/functions/end_to_end/fixtures/contracts/external_bundle/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/contracts/external_bundle/__init__.pyi index e9a67a5d4..d85ff606f 100644 --- a/tests/fortran/functions/end_to_end/fixtures/contracts/external_bundle/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/contracts/external_bundle/__init__.pyi @@ -11,3 +11,5 @@ def triple_value( def offset_value( value: Int32 ) -> Int32: ... + +__all__ = ["triple_value", "offset_value"] diff --git a/tests/fortran/functions/end_to_end/fixtures/contracts/fixed_external/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/contracts/fixed_external/__init__.pyi index ad7eef082..800ca05af 100644 --- a/tests/fortran/functions/end_to_end/fixtures/contracts/fixed_external/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/contracts/fixed_external/__init__.pyi @@ -5,3 +5,5 @@ from prik.contracts import Addr, Arg, Int32, Returns, native_call, standalone def fixed_add( value: Int32 ) -> tuple[Int32, Returns["value", Int32]]: ... + +__all__ = ["fixed_add"] diff --git a/tests/fortran/functions/end_to_end/fixtures/contracts/free_external/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/contracts/free_external/__init__.pyi index 3468b30b6..7ffc47620 100644 --- a/tests/fortran/functions/end_to_end/fixtures/contracts/free_external/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/contracts/free_external/__init__.pyi @@ -5,3 +5,5 @@ from prik.contracts import Addr, Arg, Int32, native_call, standalone def free_square( value: Int32 ) -> Int32: ... + +__all__ = ["free_square"] diff --git a/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_direct_bind_c_f90/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_direct_bind_c_f90/__init__.pyi index 8aa3d1f26..741e47720 100644 --- a/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_direct_bind_c_f90/__init__.pyi @@ -13,3 +13,5 @@ def standalone_direct( def standalone_output( value: Int32 ) -> Int32: ... + +__all__ = ["standalone_direct", "standalone_output"] diff --git a/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_mixed_bind_c_f90/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_mixed_bind_c_f90/__init__.pyi index c0e58fd2f..15e28dad1 100644 --- a/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_mixed_bind_c_f90/__init__.pyi @@ -12,3 +12,5 @@ def standalone_direct( def standalone_adapted( value: Int32 ) -> Int32: ... + +__all__ = ["standalone_direct", "standalone_adapted"] diff --git a/tests/fortran/functions/end_to_end/test_bind_c_label_case.py b/tests/fortran/functions/end_to_end/test_bind_c_label_case.py new file mode 100644 index 000000000..ba15aa520 --- /dev/null +++ b/tests/fortran/functions/end_to_end/test_bind_c_label_case.py @@ -0,0 +1,75 @@ +"""A `bind(C)` label is an external symbol, not a Fortran identifier.""" + +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import, _compiler, _import_from_build_dir +from prik import build_pyi_extension + +pytestmark = pytest.mark.fortran_end_to_end + +BIND_C_LABEL_SOURCE = """ +module label_mod + use iso_c_binding, only : c_int + implicit none +contains + subroutine scale(x) bind(C, name="SCALE") + integer(c_int), intent(inout) :: x + x = x * 3 + end subroutine scale +end module label_mod +""" + + +def test_bind_c_label_keeps_its_exact_spelling_through_a_generated_contract(tmp_path: Path): + """A C binding label differing only in case from its procedure survives. + + Fortran names `scale` without regard to case, so nothing about that name + needs recording. The label `SCALE` is a C external symbol instead, which is + spelled exactly, and the wrapper links against it rather than the Fortran + identifier it happens to resemble. + """ + source = tmp_path / "label.f90" + source.write_text(BIND_C_LABEL_SOURCE, encoding="utf-8") + + source_module = _build_source_and_import( + source, + tmp_path / "source_build", + {"label_wrapper.c", "label_wrapper.h"}, + ) + assert source_module.scale(np.int32(5)) == np.int32(15) + + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + str(source), + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + contract = (contracts / "label_mod.pyi").read_text(encoding="utf-8") + assert '@bind("SCALE")' in contract + assert "def scale(" in contract + + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(source)], + output_dir=tmp_path / "contract_build", + output_name="label_contract", + ) + rebuilt = _import_from_build_dir(result.module_name, result.output_dir) + assert rebuilt.label_mod.scale(np.int32(5)) == np.int32(15) diff --git a/tests/fortran/functions/parsing/test_procedure_and_interface_regressions.py b/tests/fortran/functions/parsing/test_procedure_and_interface_regressions.py index 901400bb7..1e71a7e15 100644 --- a/tests/fortran/functions/parsing/test_procedure_and_interface_regressions.py +++ b/tests/fortran/functions/parsing/test_procedure_and_interface_regressions.py @@ -1,6 +1,7 @@ """Tests split by stable ownership concept from `test_source_form_and_diagnostics_regressions.py`.""" from prik.parsers.fortran import parse_fortran_file +from prik.parsers.fortran.models import FortranUseStatement from prik.parsers.fortran.models import ( FortranArgument, FortranProcedureSignature, @@ -91,7 +92,7 @@ def test_finalize_proc_resolves_signature_arguments_imports_and_uses_without_exp signature, symbols={argument.name.lower(): argument for argument in signature.arguments}, ) - state.uses = {"precision_mod": []} + state.uses = [FortranUseStatement("precision_mod")] state.local_params = {"rk": "8", "count": "4"} state.imports = {"state_t", "callback"} state.filename = "finalize_contract.f90" @@ -104,5 +105,5 @@ def test_finalize_proc_resolves_signature_arguments_imports_and_uses_without_exp ("values", "real", "8", ["4"]), ] assert finalized.attributes == ["import(callback)", "import(state_t)"] - assert finalized.uses == {"precision_mod": []} + assert [statement.module for statement in finalized.uses] == ["precision_mod"] assert finalized.variables == {} diff --git a/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py b/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py index 2c5a5f326..84379cd5a 100644 --- a/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py +++ b/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py @@ -177,3 +177,46 @@ def test_semantic_function_projection_equality_and_placeholders(): ) assert left == right + + +def test_semantic_function_equality_renames_references_not_literal_contents(): + """Argument renaming compares shape; a string default states characters. + + Two procedures whose string defaults happen to spell their own argument + names default to different text, so canonicalizing the reference must stop + at the quotes. + """ + + def report(extent: str, default_value: str) -> SemanticFunction: + return SemanticFunction( + name="report", + native_name="report", + arguments=[ + SemanticArgument(extent, SemanticType("Int32", dtype="Int32")), + SemanticArgument( + "label", + SemanticType("String", dtype="String"), + default_value=default_value, + ), + ], + ) + + assert report("n", "'n'") != report("m", "'m'") + # Renaming still makes two identically shaped procedures compare equal. + assert report("n", "'fixed'") == report("m", "'fixed'") + + +def test_semantic_function_equality_still_canonicalizes_shape_references(): + """A shape naming an argument compares by position, not by that name.""" + + def scale(extent: str, array: str) -> SemanticFunction: + return SemanticFunction( + name="scale", + native_name="scale", + arguments=[ + SemanticArgument(extent, SemanticType("Int32", dtype="Int32")), + SemanticArgument(array, SemanticType("Float64", dtype="Float64", rank=1, shape=[f"1:{extent}"])), + ], + ) + + assert scale("n", "x") == scale("m", "y") diff --git a/tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py b/tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py index f3188d803..0dbcc9440 100644 --- a/tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py +++ b/tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py @@ -60,7 +60,7 @@ def test_policy_completes_builtin_scalar_family_only_for_reflected_dispatch(): surface for namespace in plan.namespaces for surface in namespace.classes - if surface.type_identity[1] == "vector" + if surface.type_identity[1] == "Vector" ) overloads = {overload.python_name: overload for overload in vector.overloads} diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/__init__.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/__init__.pyi index b35830728..65d035f3e 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/__init__.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/__init__.pyi @@ -1 +1,3 @@ from . import foperators_f90 + +__all__ = ["foperators_f90"] diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/foperators_f90.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/foperators_f90.pyi index 24c5606e0..2af65a6ed 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/foperators_f90.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/foperators_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Annotated, Arg, Bool32, Float64, Int32, Pass, Polymorphic, Returns, bind, native_call, overload, private -class vector: +class Vector: def __init__( self, *, @@ -12,91 +12,91 @@ class vector: @overload("add_vectors") def __add__( self, - right: vector - ) -> vector: ... + right: Vector + ) -> Vector: ... @overload("add_vector_integer") def __add__( self, right: Int32 - ) -> vector: ... + ) -> Vector: ... @overload("add_vector_real") def __add__( self, right: Float64 - ) -> vector: ... + ) -> Vector: ... @overload("add_real_vector") def __radd__( self, left: Float64 - ) -> vector: ... + ) -> Vector: ... @overload("add_vector_array") def __add__( self, right: Float64[::] - ) -> vector: ... + ) -> Vector: ... @overload("add_vector_offset") def __add__( self, - right: offset - ) -> vector: ... + right: Offset + ) -> Vector: ... @overload("positive_vector") - def __pos__(self) -> vector: ... + def __pos__(self) -> Vector: ... @overload("subtract_vector_real") def __sub__( self, right: Float64 - ) -> vector: ... + ) -> Vector: ... @overload("subtract_real_vector") def __rsub__( self, left: Float64 - ) -> vector: ... + ) -> Vector: ... @overload("negative_vector") - def __neg__(self) -> vector: ... + def __neg__(self) -> Vector: ... @overload("multiply_vector_real") def __mul__( self, right: Float64 - ) -> vector: ... + ) -> Vector: ... @overload("divide_vector_real") def __truediv__( self, right: Float64 - ) -> vector: ... + ) -> Vector: ... @overload("power_vector_integer") def __pow__( self, right: Int32 - ) -> vector: ... + ) -> Vector: ... @overload("equal_vectors") def __eq__( self, - right: vector + right: Vector ) -> Bool32: ... @overload("equivalent_vector_offset", generic="operator(.eqv.)") def __eq__( self, - right: offset + right: Offset ) -> Bool32: ... @overload("not_equal_vectors") def __ne__( self, - right: vector + right: Vector ) -> Bool32: ... @overload("not_equivalent_vector_integer", generic="operator(.neqv.)") @@ -108,7 +108,7 @@ class vector: @overload("less_vectors") def __lt__( self, - right: vector + right: Vector ) -> Bool32: ... @overload("less_vector_real") @@ -126,31 +126,31 @@ class vector: @overload("greater_vectors") def __gt__( self, - right: vector + right: Vector ) -> Bool32: ... @overload("less_equal_vectors") def __le__( self, - right: vector + right: Vector ) -> Bool32: ... @overload("greater_equal_vectors") def __ge__( self, - right: vector + right: Vector ) -> Bool32: ... @overload("and_vectors") def __and__( self, - right: vector + right: Vector ) -> Bool32: ... @overload("or_vectors") def __or__( self, - right: vector + right: Vector ) -> Bool32: ... @overload("not_vector") @@ -159,28 +159,28 @@ class vector: @overload("dot_vectors") def operator_dot( self, - right: vector + right: Vector ) -> Float64: ... @overload("shift_real_vector") def r_operator_shift( self, left: Float64 - ) -> vector: ... + ) -> Vector: ... @overload("assign_vector_integer") def assign( self, right: Int32 - ) -> vector: ... + ) -> Vector: ... @overload("assign_vector_real") def assign( self, right: Float64 - ) -> vector: ... + ) -> Vector: ... -class offset: +class Offset: def __init__( self, *, @@ -192,16 +192,16 @@ class offset: @overload("add_vector_offset") def __radd__( self, - left: vector - ) -> vector: ... + left: Vector + ) -> Vector: ... @overload("equivalent_vector_offset", generic="operator(.eqv.)") def __eq__( self, - left: vector + left: Vector ) -> Bool32: ... -class counter: +class Counter: def __init__( self, *, @@ -216,13 +216,13 @@ class counter: def add_integer( self, right: Int32 - ) -> counter: ... + ) -> Counter: ... @overload("counter_add_integer") def __add__( self, right: Int32 - ) -> counter: ... + ) -> Counter: ... @private @native_call([Addr(Arg(0))]) @@ -238,110 +238,110 @@ def convert_real( @private def add_vectors( - left: vector, - right: vector -) -> vector: ... + left: Vector, + right: Vector +) -> Vector: ... @private @native_call([Arg(0), Addr(Arg(1))]) def add_vector_integer( - left: vector, + left: Vector, right: Int32 -) -> vector: ... +) -> Vector: ... @private @native_call([Arg(0), Addr(Arg(1))]) def add_vector_real( - left: vector, + left: Vector, right: Float64 -) -> vector: ... +) -> Vector: ... @private @native_call([Addr(Arg(0)), Arg(1)]) def add_real_vector( left: Float64, - right: vector -) -> vector: ... + right: Vector +) -> Vector: ... @private def add_vector_array( - left: vector, + left: Vector, right: Float64[::] -) -> vector: ... +) -> Vector: ... @private def add_vector_offset( - left: vector, - right: offset -) -> vector: ... + left: Vector, + right: Offset +) -> Vector: ... @private def positive_vector( - value: vector -) -> vector: ... + value: Vector +) -> Vector: ... @private @native_call([Arg(0), Addr(Arg(1))]) def subtract_vector_real( - left: vector, + left: Vector, right: Float64 -) -> vector: ... +) -> Vector: ... @private @native_call([Addr(Arg(0)), Arg(1)]) def subtract_real_vector( left: Float64, - right: vector -) -> vector: ... + right: Vector +) -> Vector: ... @private def negative_vector( - value: vector -) -> vector: ... + value: Vector +) -> Vector: ... @private @native_call([Arg(0), Addr(Arg(1))]) def multiply_vector_real( - left: vector, + left: Vector, right: Float64 -) -> vector: ... +) -> Vector: ... @private @native_call([Arg(0), Addr(Arg(1))]) def divide_vector_real( - left: vector, + left: Vector, right: Float64 -) -> vector: ... +) -> Vector: ... @private @native_call([Arg(0), Addr(Arg(1))]) def power_vector_integer( - left: vector, + left: Vector, right: Int32 -) -> vector: ... +) -> Vector: ... @private def equal_vectors( - left: vector, - right: vector + left: Vector, + right: Vector ) -> Bool32: ... @private def not_equal_vectors( - left: vector, - right: vector + left: Vector, + right: Vector ) -> Bool32: ... @private def less_vectors( - left: vector, - right: vector + left: Vector, + right: Vector ) -> Bool32: ... @private @native_call([Arg(0), Addr(Arg(1))]) def less_vector_real( - left: vector, + left: Vector, right: Float64 ) -> Bool32: ... @@ -349,90 +349,90 @@ def less_vector_real( @native_call([Addr(Arg(0)), Arg(1)]) def less_real_vector( left: Float64, - right: vector + right: Vector ) -> Bool32: ... @private def less_equal_vectors( - left: vector, - right: vector + left: Vector, + right: Vector ) -> Bool32: ... @private def greater_vectors( - left: vector, - right: vector + left: Vector, + right: Vector ) -> Bool32: ... @private def greater_equal_vectors( - left: vector, - right: vector + left: Vector, + right: Vector ) -> Bool32: ... @private def and_vectors( - left: vector, - right: vector + left: Vector, + right: Vector ) -> Bool32: ... @private def or_vectors( - left: vector, - right: vector + left: Vector, + right: Vector ) -> Bool32: ... @private def not_vector( - value: vector + value: Vector ) -> Bool32: ... @private def equivalent_vector_offset( - left: vector, - right: offset + left: Vector, + right: Offset ) -> Bool32: ... @private @native_call([Arg(0), Addr(Arg(1))]) def not_equivalent_vector_integer( - left: vector, + left: Vector, right: Int32 ) -> Bool32: ... @private def dot_vectors( - left: vector, - right: vector + left: Vector, + right: Vector ) -> Float64: ... @private @native_call([Addr(Arg(0)), Arg(1)]) def shift_real_vector( left: Float64, - right: vector -) -> vector: ... + right: Vector +) -> Vector: ... @private @native_call([Arg(0), Addr(Arg(1))]) def assign_vector_integer( - left: vector, + left: Vector, right: Int32 -) -> Returns["left", vector]: ... +) -> Returns["left", Vector]: ... @private @native_call([Arg(0), Addr(Arg(1))]) def assign_vector_real( - left: vector, + left: Vector, right: Float64 -) -> Returns["left", vector]: ... +) -> Returns["left", Vector]: ... @private @native_call([Arg(0), Addr(Arg(1))]) def counter_add_integer( - self: Annotated[counter, Polymorphic], + self: Annotated[Counter, Polymorphic], right: Int32 -) -> counter: ... +) -> Counter: ... @bind("convert") @overload("convert_integer") @@ -445,3 +445,5 @@ def convert( def convert( value: Float64 ) -> Float64: ... + +__all__ = ["Vector", "Offset", "Counter", "convert"] diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_f90/__init__.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_f90/__init__.pyi index 07cef5ed9..2c60219f9 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_f90/__init__.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_f90/__init__.pyi @@ -1 +1,3 @@ from . import foverloads_f90 + +__all__ = ["foverloads_f90"] diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_f90/foverloads_f90.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_f90/foverloads_f90.pyi index 21cfdf8e1..173490b6b 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_f90/foverloads_f90.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_f90/foverloads_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Annotated, Arg, Complex128, Float64, Int32, Pass, Polymorphic, bind, native_call, overload, private -class accumulator: +class Accumulator: def __init__( self, *, @@ -39,7 +39,7 @@ class accumulator: value: Float64 ) -> None: ... -class sample: +class Sample: def __init__( self, *, @@ -79,25 +79,25 @@ def summarize_vector( @private def inspect_accumulator( - value: accumulator + value: Accumulator ) -> Float64: ... @private def inspect_sample( - value: sample + value: Sample ) -> Float64: ... @private @native_call([Arg(0), Addr(Arg(1))]) def accumulator_add_integer( - self: Annotated[accumulator, Polymorphic], + self: Annotated[Accumulator, Polymorphic], value: Int32 ) -> None: ... @private @native_call([Arg(0), Addr(Arg(1))]) def accumulator_add_real( - self: Annotated[accumulator, Polymorphic], + self: Annotated[Accumulator, Polymorphic], value: Float64 ) -> None: ... @@ -134,11 +134,13 @@ def summarize( @bind("inspect") @overload("inspect_accumulator") def inspect( - value: accumulator + value: Accumulator ) -> Float64: ... @bind("inspect") @overload("inspect_sample") def inspect( - value: sample + value: Sample ) -> Float64: ... + +__all__ = ["Accumulator", "Sample", "convert", "summarize", "inspect"] diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_fixed/__init__.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_fixed/__init__.pyi index 30aa9d2c0..03a54992e 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_fixed/__init__.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_fixed/__init__.pyi @@ -1 +1,3 @@ from . import foverloads_fixed + +__all__ = ["foverloads_fixed"] diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_fixed/foverloads_fixed.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_fixed/foverloads_fixed.pyi index 500b66162..8b4c0ff71 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_fixed/foverloads_fixed.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foverloads_fixed/foverloads_fixed.pyi @@ -23,3 +23,5 @@ def convert( def convert( value: Float64 ) -> Float64: ... + +__all__ = ["convert"] diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/__init__.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/__init__.pyi index 00d25fbe1..63190ddd6 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import generic_interfaces_direct_bind_c_f90 + +__all__ = ["generic_interfaces_direct_bind_c_f90"] diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/generic_interfaces_direct_bind_c_f90.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/generic_interfaces_direct_bind_c_f90.pyi index 4452092ff..09b1529a2 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/generic_interfaces_direct_bind_c_f90.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/generic_interfaces_direct_bind_c_f90.pyi @@ -53,3 +53,5 @@ def increment( def increment( value: Float64 ) -> Returns["value", Float64]: ... + +__all__ = ["convert_integer", "convert_real", "increment_integer", "increment_real", "convert", "increment"] diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/__init__.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/__init__.pyi index 62ef2d75f..b34223fee 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import generic_interfaces_mixed_bind_c_f90 + +__all__ = ["generic_interfaces_mixed_bind_c_f90"] diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/generic_interfaces_mixed_bind_c_f90.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/generic_interfaces_mixed_bind_c_f90.pyi index 9cfbeffba..cbc88bea6 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/generic_interfaces_mixed_bind_c_f90.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/generic_interfaces_mixed_bind_c_f90.pyi @@ -22,3 +22,5 @@ def convert( def convert( value: Float64 ) -> Float64: ... + +__all__ = ["convert_integer", "convert_real", "convert"] diff --git a/tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py b/tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py index 6ceca3ec7..5d658ffc2 100644 --- a/tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py +++ b/tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py @@ -33,21 +33,21 @@ def test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extens ) def vector(value): - result = module.vector() + result = module.Vector() result.value = np.float64(value) return result def offset(value): - result = module.offset() + result = module.Offset() result.value = np.float64(value) return result left = vector(5.0) right = vector(2.0) - assert "__add__(*args, **kwargs)" in module.vector.__doc__ - assert "__add__(right: vector) -> vector" in module.vector.__add__.__doc__ - assert "add_vectors" not in module.vector.__add__.__doc__ + assert "__add__(*args, **kwargs)" in module.Vector.__doc__ + assert "__add__(right: Vector) -> Vector" in module.Vector.__add__.__doc__ + assert "add_vectors" not in module.Vector.__add__.__doc__ assert module.convert(np.int32(2)) == np.int32(12) assert module.convert(np.float64(2.0)) == np.float64(2.5) @@ -96,7 +96,7 @@ def offset(value): assert assigned.assign(assigned) is assigned assert assigned.value == np.float64(3.5) - counter = module.counter() + counter = module.Counter() counter.value = np.int32(4) assert (counter + np.int32(3)).value == np.int32(7) diff --git a/tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py b/tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py index de7d6ad26..407722285 100644 --- a/tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py +++ b/tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py @@ -6,6 +6,7 @@ import pytest from tests.fortran._support.wrapper_build import ( + _build_source_and_import, _build_source_or_generated_pyi_and_import, _build_sources_and_import, ) @@ -89,13 +90,13 @@ def test_fortran_generic_interfaces_dispatch_in_generated_c_extension( assert module.summarize(np.float64(2.5)) == np.float64(2.5) assert module.summarize(np.array([1.0, 2.0, 3.0], dtype=np.float64)) == np.float64(6.0) - value = module.accumulator() + value = module.Accumulator() value.add(np.int32(2)) value.add(value=np.float64(0.5)) assert value.total == np.float64(2.5) assert module.inspect(value) == np.float64(2.5) - sample = module.sample() + sample = module.Sample() sample.value = np.float64(7.25) assert module.inspect(sample) == np.float64(7.25) @@ -123,3 +124,63 @@ def test_public_generic_dispatches_to_private_inline_submodule_specifics(tmp_pat assert "native__prik_overload_shift_1 => shift" in bridge assert "=> shift_integer" not in bridge assert "=> shift_real" not in bridge + + +EXTENDED_GENERIC_SOURCE = """ +module gen_base_mod + implicit none + interface report + module procedure report_int + end interface report +contains + subroutine report_int(value, seen) + integer, intent(in) :: value + integer, intent(out) :: seen + seen = value + end subroutine report_int +end module gen_base_mod + +module gen_extended_mod + use gen_base_mod, only : report + implicit none + interface report + module procedure report_real + end interface report +contains + subroutine report_real(value, seen) + real(8), intent(in) :: value + integer, intent(out) :: seen + seen = int(value) * 10 + end subroutine report_real +end module gen_extended_mod +""" + + +def test_generic_extended_across_modules_dispatches_to_every_specific(tmp_path: Path): + """A local interface block extends the generic it imports, not replaces it. + + The extending module resolves both the specific it declares and the one + that reached it through the import, while the declaring module keeps only + its own: a generic accumulates along the `use` chain in one direction. + """ + source = tmp_path / "gen_extended.f90" + source.write_text(EXTENDED_GENERIC_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_gen_extended_wrapper.f90", + "gen_extended_wrapper.c", + "gen_extended_wrapper.h", + }, + ) + + assert module.gen_extended_mod.report(np.int32(3)) == np.int32(3) + assert module.gen_extended_mod.report(np.float64(4.0)) == np.int32(40) + assert module.gen_base_mod.report(np.int32(3)) == np.int32(3) + + # The inherited specific is reachable only through the generic, because + # `use gen_base_mod, only : report` never bound its own name. + assert "report_int" not in dir(module.gen_extended_mod) + with pytest.raises(TypeError, match="no matching overload"): + module.gen_base_mod.report(np.float64(4.0)) diff --git a/tests/fortran/generic_interfaces/end_to_end/test_merged_generic_replay.py b/tests/fortran/generic_interfaces/end_to_end/test_merged_generic_replay.py new file mode 100644 index 000000000..eab442e91 --- /dev/null +++ b/tests/fortran/generic_interfaces/end_to_end/test_merged_generic_replay.py @@ -0,0 +1,50 @@ +"""A generic merged from several modules builds the same from source and from its contracts.""" + +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_fortran_extension +from tests.fortran._support.wrapper_build import ( + _build_generated_pyi_and_import, + _import_from_build_dir, + _sole_native_module, +) +from tests.fortran.generic_interfaces.semantics.test_generic_contributor_merging import ( + CONTRIBUTORS, + LOCAL_EXTENSION, +) + +pytestmark = pytest.mark.fortran_end_to_end + +MERGED_SOURCE = ( + CONTRIBUTORS + + """ +module facade_mod + use ints_mod, only : convert + use reals_mod, only : convert + implicit none +""" + + LOCAL_EXTENSION +) + + +def _source_build(source: Path, build_dir: Path): + result = build_fortran_extension(source, output_dir=build_dir, output_name="merged_generic") + return _import_from_build_dir(result.module_name, result.output_dir) + + +@pytest.mark.parametrize("lane", ["source", "generated_pyi"]) +def test_merged_generic_dispatches_every_contributor(tmp_path: Path, lane: str): + source = tmp_path / "merged_generic.f90" + source.write_text(MERGED_SOURCE, encoding="utf-8") + if lane == "source": + package = _source_build(source, tmp_path / "source_build") + else: + package = _build_generated_pyi_and_import(source, tmp_path / "pyi_build") + facade = _sole_native_module(package).facade_mod if not hasattr(package, "facade_mod") else package.facade_mod + + assert facade.convert(np.int32(3)) == np.int32(3) + assert facade.convert(np.float32(2.5)) == np.float32(2.5) + assert facade.convert(np.bool_(True)) diff --git a/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py b/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py index a2b909b6a..c3f0898fc 100644 --- a/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py +++ b/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py @@ -8,6 +8,7 @@ from tests.fortran._support.parser_procedures import ( parse_fortran_interfaces, parse_fortran_module, + parse_fortran_modules, ) from prik.parsers.fortran.models import FortranParseError @@ -105,3 +106,193 @@ def test_assumed_type_generic_candidate_is_rejected_at_parsing(): parse_fortran_file(source, filename="unsupported_generic.f90") assert exc_info.value.code == "PARSE_UNSUPPORTED_DECLARATION" + + +def test_generic_interface_declared_in_several_blocks_becomes_one_generic(): + """Fortran builds one generic from as many blocks as a scope declares. + + Real sources split a generic across preprocessor-guarded blocks, adding + specifics only for the kinds a build supports, so repeated blocks name one + generic rather than redeclaring it. + """ + source = """ +module huge_mod + implicit none + private + public :: huge_value + + interface huge_value + module procedure huge_value_sp, huge_value_dp + end interface huge_value + + interface huge_value + module procedure huge_value_qp + end interface huge_value +contains + real function huge_value_sp(x) + real, intent(in) :: x + huge_value_sp = huge(x) + end function huge_value_sp + real(8) function huge_value_dp(x) + real(8), intent(in) :: x + huge_value_dp = huge(x) + end function huge_value_dp + real(16) function huge_value_qp(x) + real(16), intent(in) :: x + huge_value_qp = huge(x) + end function huge_value_qp +end module huge_mod +""" + + module = parse_fortran_module(source) + + generics = [interface for interface in module.interfaces if interface.name] + assert len(generics) == 1 + assert generics[0].name == "huge_value" + assert generics[0].specific_procedures == ["huge_value_sp", "huge_value_dp", "huge_value_qp"] + + +def test_repeated_generic_names_stay_separate_per_module(): + """Two modules in one file each own their generic of the same name.""" + source = """ +module first_mod + implicit none + interface report + module procedure report_first + end interface report +contains + subroutine report_first() + end subroutine report_first +end module first_mod + +module second_mod + implicit none + interface report + module procedure report_second + end interface report +contains + subroutine report_second() + end subroutine report_second +end module second_mod +""" + + modules = {module.name: module for module in parse_fortran_modules(source)} + + assert [item.specific_procedures for item in modules["first_mod"].interfaces if item.name] == [["report_first"]] + assert [item.specific_procedures for item in modules["second_mod"].interfaces if item.name] == [["report_second"]] + + +def test_type_bound_generic_declared_in_several_statements_becomes_one_binding(): + """A type-bound generic collects specifics from as many statements as it takes. + + A derived type may name one generic binding over several ``generic ::`` + statements, and every statement contributes specifics to that one binding + rather than declaring another of the same name. + """ + source = """ +module shape_mod + implicit none + type :: shape_t + real(8) :: v + contains + procedure :: area_int + procedure :: area_real + generic :: area => area_int + generic :: area => area_real + end type shape_t +contains + real(8) function area_int(self, k) + class(shape_t), intent(in) :: self + integer, intent(in) :: k + area_int = self%v * k + end function area_int + real(8) function area_real(self, k) + class(shape_t), intent(in) :: self + real(8), intent(in) :: k + area_real = self%v * k + end function area_real +end module shape_mod +""" + + module = parse_fortran_module(source) + + assert [binding["name"] for binding in module.derived_types[0].generic_bindings] == ["area"] + assert module.derived_types[0].generic_bindings[0]["targets"] == ["area_int", "area_real"] + + +def test_type_bound_operator_generic_merges_across_statements_and_spacing(): + """One defined operator binding survives being split across statements.""" + source = """ +module vec_mod + implicit none + type :: vec_t + real(8) :: v + contains + procedure :: add_int + procedure :: add_real + generic :: operator(+) => add_int + generic :: operator (+) => add_real + end type vec_t +contains + type(vec_t) function add_int(self, k) + class(vec_t), intent(in) :: self + integer, intent(in) :: k + add_int%v = self%v + k + end function add_int + type(vec_t) function add_real(self, k) + class(vec_t), intent(in) :: self + real(8), intent(in) :: k + add_real%v = self%v + k + end function add_real +end module vec_mod +""" + + module = parse_fortran_module(source) + + assert [binding["name"] for binding in module.derived_types[0].generic_bindings] == ["operator(+)"] + assert module.derived_types[0].generic_bindings[0]["targets"] == ["add_int", "add_real"] + + +def test_same_generic_name_in_two_procedures_declares_two_generics(): + """A generic belongs to the scope declaring it, and procedures are scopes. + + Two procedures of one module may each declare an interface of the same + name, and they name different generics. Merging them on the module they + share would let one procedure's specifics answer the other's calls. + """ + source = """ +module scoped_mod + implicit none +contains + subroutine first(x) + real(8), intent(in) :: x + interface local_generic + subroutine first_impl(a) + real(8), intent(in) :: a + end subroutine first_impl + end interface + call local_generic(x) + end subroutine first + + subroutine second(n) + integer, intent(in) :: n + interface local_generic + subroutine second_impl(b) + integer, intent(in) :: b + end subroutine second_impl + end interface + call local_generic(n) + end subroutine second +end module scoped_mod +""" + + module = parse_fortran_module(source) + + assert [ + (interface.name, [signature.name for signature in interface.procedures]) + for interface in module.interfaces + if interface.name + ] == [ + ("local_generic", ["first_impl"]), + ("local_generic", ["second_impl"]), + ] diff --git a/tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py b/tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py index ad19772eb..f6666d612 100644 --- a/tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py +++ b/tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py @@ -6,6 +6,9 @@ import pytest +from prik.parsers.fortran import parse_fortran_file as parse_fortran_source +from prik.pipeline.pyi import emit_module_stubs +from prik.semantics.fortran2ir import fortran_module_to_semantic_module from tests.fortran._support.generated_contracts import ( GeneratedContractCase, assert_generated_contract_matches_fixture, @@ -39,3 +42,37 @@ def test_generated_generic_contract_matches_fixture( tmp_path: Path, ): assert_generated_contract_matches_fixture(case, tmp_path) + + +def test_overload_names_its_specific_as_the_contract_declares_it(): + """An overload target names a declaration this contract holds. + + A specific whose Fortran spelling carries capitals is declared under its + Python name, so the overload naming it is written the same way; the source + spelling would name no declaration in the contract at all. + """ + source = """ +module powalg_mod +implicit none +private +public :: qradd +interface qradd +module procedure qradd_Rdiag +end interface qradd +contains +subroutine qradd_Rdiag(x) +real(8), intent(inout) :: x +end subroutine qradd_Rdiag +end module powalg_mod +""" + + # Contract names are completed by policy, never by the printer, so the + # module is emitted through the stage that completes them first. + code = emit_module_stubs( + fortran_module_to_semantic_module(parse_fortran_source(source)), + normalize_public_names=True, + )["powalg_mod"] + + assert "def qradd_rdiag(" in code + assert '@overload("qradd_rdiag")' in code + assert "qradd_Rdiag" not in code diff --git a/tests/fortran/generic_interfaces/policy/test_generic_policy.py b/tests/fortran/generic_interfaces/policy/test_generic_policy.py index fffeb263f..c3d550294 100644 --- a/tests/fortran/generic_interfaces/policy/test_generic_policy.py +++ b/tests/fortran/generic_interfaces/policy/test_generic_policy.py @@ -51,3 +51,35 @@ def convert(value: Float64) -> Float64: ... for procedure in module.overload_sets[0].procedures ] assert [policy.native_name for policy in policies] == ["convert_integer", "convert"] + + +def test_a_generic_owns_its_export_decision_like_every_declaration(): + """The decision lives on the generic, not on whichever specific came first. + + Keeping it on the first candidate made a generic without candidates unable + to record one at all, and left the generic's own metadata empty. + """ + from prik.policy.exports import complete_python_export_policy + from prik.semantics.models import PYTHON_EXPORTS_METADATA, ProcedureOverloadSet, SemanticModule + + module = parse_pyi_text( + """ +from prik.contracts import Addr, Arg, Int32, native_call, overload + +@native_call([Addr(Arg(0))]) +def convert_i(x: Int32) -> Int32: ... + +@overload("convert_i") +def convert(x: Int32) -> Int32: ... +""", + module_name="owned_generic", + ) + complete_python_export_policy(module) + generic = module.overload_sets[0] + + assert generic.metadata[PYTHON_EXPORTS_METADATA] == [{"namespace": (), "name": "convert"}] + assert module.functions[0].metadata[PYTHON_EXPORTS_METADATA] == [{"namespace": (), "name": "convert_i"}] + + empty = SemanticModule(name="placeholder", overload_sets=[ProcedureOverloadSet(name="later", procedures=[])]) + complete_python_export_policy(empty) + assert empty.overload_sets[0].metadata[PYTHON_EXPORTS_METADATA] == [{"namespace": (), "name": "later"}] diff --git a/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py b/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py index dd1f0b5b1..bc2b4ff6c 100644 --- a/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py +++ b/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py @@ -169,3 +169,82 @@ def test_converter_preserves_defined_operators_assignment_and_type_bound_operato assert [ (item.name, [procedure.name for procedure in item.procedures]) for item in classes["counter"].overload_sets ] == [("__add__", ["counter_add_integer"])] + + +def test_type_bound_generic_split_across_statements_reaches_one_overload_set(): + """Every specific a split generic binding names stays reachable. + + A type-bound generic built from several ``generic ::`` statements means one + binding, so the class carries a single overload set holding every specific + -- not one set per statement, which leaves all but the first unreachable at + dispatch. + """ + source = """ +module shape_mod + implicit none + type :: shape_t + real(8) :: v + contains + procedure :: area_integer + procedure :: area_real + generic :: area => area_integer + generic :: area => area_real + end type shape_t +contains + real(8) function area_integer(self, scale) + class(shape_t), intent(in) :: self + integer, intent(in) :: scale + area_integer = self%v * scale + end function area_integer + real(8) function area_real(self, scale) + class(shape_t), intent(in) :: self + real(8), intent(in) :: scale + area_real = self%v * scale + end function area_real +end module shape_mod +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + + shape = module.classes[0] + assert [(item.name, [proc.name for proc in item.procedures]) for item in shape.overload_sets] == [ + ("area", ["area_integer", "area_real"]) + ] + + +def test_a_generic_declared_inside_a_procedure_is_not_a_module_generic(): + """A generic belongs to the scope declaring it, and a procedure is a scope. + + An interface written inside a procedure names a generic of that procedure. + Reading it as one of the module's own would publish it, and two procedures + naming one generic would each answer for the other. + """ + source = """ +module scoped_mod + implicit none +contains + subroutine first(x) + real(8), intent(in) :: x + interface local_generic + subroutine first_impl(a) + real(8), intent(in) :: a + end subroutine first_impl + end interface + call local_generic(x) + end subroutine first + + subroutine second(n) + integer, intent(in) :: n + interface local_generic + subroutine second_impl(b) + integer, intent(in) :: b + end subroutine second_impl + end interface + call local_generic(n) + end subroutine second +end module scoped_mod +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + + assert module.overload_sets == [] diff --git a/tests/fortran/generic_interfaces/semantics/test_generic_contributor_merging.py b/tests/fortran/generic_interfaces/semantics/test_generic_contributor_merging.py new file mode 100644 index 000000000..15843a6fb --- /dev/null +++ b/tests/fortran/generic_interfaces/semantics/test_generic_contributor_merging.py @@ -0,0 +1,373 @@ +"""An accessible generic is assembled from every interface that contributes to it. + +An ordinary entity has one declaration, so two routes naming different ones +leave a local name ambiguous. A generic is the exception the language makes: +accessible generic interfaces sharing an identifier all contribute their +specific procedures to one generic, so every contributing route is read rather +than the first that matches. +""" + +from pathlib import Path + +from prik.parsers.fortran import parse_fortran_project +from prik.semantics.fortran2ir import fortran_project_to_semantic_modules + +CONTRIBUTORS = """\ +module ints_mod + implicit none + interface convert + module procedure convert_i + end interface +contains + integer function convert_i(x) + integer, intent(in) :: x + convert_i = x + end function convert_i +end module ints_mod + +module reals_mod + implicit none + interface convert + module procedure convert_r + end interface +contains + real function convert_r(x) + real, intent(in) :: x + convert_r = x + end function convert_r +end module reals_mod +""" + +LOCAL_EXTENSION = """\ + interface convert + module procedure convert_l + end interface + +contains + logical function convert_l(x) + logical, intent(in) :: x + convert_l = x + end function convert_l +end module facade_mod +""" + + +def _modules(tmp_path: Path, *sources: str): + """Parse one throwaway project and return its semantic modules by name.""" + (tmp_path / "project.f90").write_text("\n".join(sources), encoding="utf-8") + return {module.name: module for module in fortran_project_to_semantic_modules(parse_fortran_project(str(tmp_path)))} + + +def _specifics(module, generic_name: str) -> list[str]: + """Return the specific procedures one module's generic dispatches over.""" + return [ + procedure.name + for overload_set in module.overload_sets + if overload_set.name == generic_name + for procedure in overload_set.procedures + ] + + +def test_two_imported_generics_both_contribute_their_specifics(tmp_path: Path): + """Neither import replaces the other, so the generic dispatches over both.""" + modules = _modules( + tmp_path, + CONTRIBUTORS, + """\ +module facade_mod + use ints_mod, only : convert + use reals_mod, only : convert + implicit none +""" + + LOCAL_EXTENSION, + ) + + assert _specifics(modules["facade_mod"], "convert") == ["convert_i", "convert_r", "convert_l"] + + +def test_an_imported_generic_reached_twice_contributes_once(tmp_path: Path): + """One declaration is one contributor however many routes reach it.""" + modules = _modules( + tmp_path, + CONTRIBUTORS, + """\ +module hop_mod + use ints_mod, only : convert + implicit none +end module hop_mod + +module facade_mod + use ints_mod, only : convert + use hop_mod, only : convert + implicit none +""" + + LOCAL_EXTENSION, + ) + + assert _specifics(modules["facade_mod"], "convert") == ["convert_i", "convert_l"] + + +def test_a_private_generic_route_contributes_nothing(tmp_path: Path): + """Accessibility applies to a generic route as it does to any other.""" + modules = _modules( + tmp_path, + CONTRIBUTORS, + """\ +module hop_mod + use reals_mod, only : convert + implicit none + private :: convert +end module hop_mod + +module facade_mod + use ints_mod, only : convert + use hop_mod, only : convert + implicit none +""" + + LOCAL_EXTENSION, + ) + + assert _specifics(modules["facade_mod"], "convert") == ["convert_i", "convert_l"] + + +def test_generic_contributors_survive_a_transitive_chain(tmp_path: Path): + """A module extending a merged generic inherits everything it reaches.""" + modules = _modules( + tmp_path, + CONTRIBUTORS, + """\ +module middle_mod + use ints_mod, only : convert + use reals_mod, only : convert + implicit none + interface convert + module procedure convert_m + end interface +contains + double precision function convert_m(x) + double precision, intent(in) :: x + convert_m = x + end function convert_m +end module middle_mod + +module facade_mod + use middle_mod, only : convert + implicit none +""" + + LOCAL_EXTENSION, + ) + + assert _specifics(modules["middle_mod"], "convert") == ["convert_i", "convert_r", "convert_m"] + assert sorted(_specifics(modules["facade_mod"], "convert")) == [ + "convert_i", + "convert_l", + "convert_m", + "convert_r", + ] + + +def test_two_imported_generics_remain_one_accessible_name(tmp_path: Path): + """Generic routes are contributors, so they do not cancel each other out.""" + modules = _modules( + tmp_path, + CONTRIBUTORS, + """\ +module facade_mod + use ints_mod, only : convert + use reals_mod, only : convert + implicit none +end module facade_mod +""", + ) + + reexports = {item.local_name: item for item in modules["facade_mod"].reexports} + assert reexports["convert"].entity_kind == "generic" + + +def test_a_generic_and_a_variable_of_one_name_are_not_merged(tmp_path: Path): + """Different kinds of entity are a genuine ambiguity, not a contribution.""" + modules = _modules( + tmp_path, + CONTRIBUTORS, + """\ +module holder_mod + implicit none + integer :: convert = 3 +end module holder_mod + +module facade_mod + use ints_mod, only : convert + use holder_mod, only : convert + implicit none +end module facade_mod +""", + ) + + assert [item.local_name for item in modules["facade_mod"].reexports] == [] + + +def test_a_procedure_local_generic_stays_inside_its_procedure(tmp_path: Path): + """A block written inside a procedure is not part of the module's interface.""" + modules = _modules( + tmp_path, + """\ +module owner_mod + implicit none +contains + subroutine run() + interface convert + module procedure convert_p + end interface + end subroutine run + + integer function convert_p(x) + integer, intent(in) :: x + convert_p = x + end function convert_p +end module owner_mod + +module facade_mod + use owner_mod, only : convert + implicit none +end module facade_mod +""", + ) + + assert [item.entity_kind for item in modules["facade_mod"].reexports if item.local_name == "convert"] == ["unknown"] + + +SAME_NAMED_SPECIFICS = """\ +module ints_mod + implicit none + interface convert + module procedure to_value + end interface +contains + integer function to_value(x) + integer, intent(in) :: x + to_value = x + end function to_value +end module ints_mod + +module reals_mod + implicit none + interface convert + module procedure to_value + end interface +contains + real function to_value(x) + real, intent(in) :: x + to_value = x + end function to_value +end module reals_mod + +module facade_mod + use ints_mod, only : convert + use reals_mod, only : convert + implicit none + interface convert + module procedure to_value_l + end interface +contains + logical function to_value_l(x) + logical, intent(in) :: x + to_value_l = x + end function to_value_l +end module facade_mod +""" + + +def test_contributors_spelling_a_specific_alike_stay_two_procedures(tmp_path: Path): + """A specific is identified by the module declaring it, not by its spelling. + + Two contributors each declare `to_value`. Keying them by name alone made + the second look like the first and dropped it, so the merged generic lost a + signature it must dispatch over. + """ + modules = _modules(tmp_path, SAME_NAMED_SPECIFICS) + overload_set = next(item for item in modules["facade_mod"].overload_sets if item.name == "convert") + + identities = [ + (procedure.origin.native_scope, procedure.arguments[0].semantic_type.name) + for procedure in overload_set.procedures + ] + assert identities == [("ints_mod", "Int32"), ("reals_mod", "Float32"), ("facade_mod", "Bool")] + + +def test_a_contract_names_each_merged_specific_distinctly(tmp_path: Path): + """Two specifics spelled alike need two Python names and two targets.""" + from prik.policy.exports import complete_python_export_policy + from prik.printers.pyi import PyiPrinter + + modules = _modules(tmp_path, SAME_NAMED_SPECIFICS) + facade = modules["facade_mod"] + complete_python_export_policy(facade) + contract = PyiPrinter(normalize_public_names=True).emit(facade) + + assert contract.count("def to_value(") == 1 + assert contract.count("def to_value_2(") == 1 + # Each dispatcher names the declaration this contract actually writes. + assert '@overload("to_value")' in contract + assert '@overload("to_value_2")' in contract + + +def test_a_type_bound_assignment_reaches_the_method_it_projects(tmp_path: Path): + """The generic's candidate and the method it names are one declaration. + + A defined assignment projects its bound object as the result. The original + method has to carry that projection too, so both the generic call and a + direct call behave the same way. + """ + modules = _modules( + tmp_path, + """\ +module asg_mod + implicit none + type :: box_t + integer :: value = 0 + contains + procedure :: assign_value + generic :: assignment(=) => assign_value + end type box_t +contains + subroutine assign_value(self, other) + class(box_t), intent(inout) :: self + integer, intent(in) :: other + self%value = other + end subroutine assign_value +end module asg_mod +""", + ) + declared = modules["asg_mod"].classes[0] + method = next(item for item in declared.methods if item.name == "assign_value") + + assert [(item.python_name, item.result_position) for item in method.projection] == [ + ("self", 0), + ("other", None), + ] + + +def test_a_contract_binds_a_merged_generic_only_by_declaring_it(tmp_path: Path): + """The facade writes the merged generic, so no `use` of a contributor is imported. + + Mirroring each `use` bound `convert` once per contributor as well as by the + facade's own declaration, and a package binding one name three ways cannot + be read back. + """ + from prik.pipeline.pyi import emit_module_stubs + + modules = _modules( + tmp_path, + CONTRIBUTORS, + """\ +module facade_mod + use ints_mod, only : convert + use reals_mod, only : convert + implicit none +""" + + LOCAL_EXTENSION, + ) + contract = emit_module_stubs(list(modules.values()), normalize_public_names=True)["facade_mod"] + + assert [line for line in contract.splitlines() if line.startswith("from .")] == [] + assert contract.count("def convert(") == 3 diff --git a/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py b/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py index c610f0129..8cc96bf54 100644 --- a/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py +++ b/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py @@ -108,3 +108,88 @@ def set(self, value: Int32) -> None: ... def test_convert_pyi_to_ir_rejects_invalid_prik_overload_links(source: str, message: str): with pytest.raises(ValueError, match=message): parse_pyi_text(source, module_name="generic_mod") + + +def test_overload_accepts_a_specific_that_projects_an_output_array_to_its_result(): + """A projected array output matches a declared result that states no mutability. + + An `intent(out)` allocatable dummy is written through as an argument, and a + projection returns it as an ordinary result. That write-through belongs to + the argument passing, so a declared result type -- which states no such + thing -- still names the same value. + """ + module = parse_pyi_text( + """ +@native_call([Return('x', 0), Addr(Arg(0))]) +def alloc_vector(n: Int32) -> Allocatable[Int32[:]]: ... + +@bind("safealloc") +@overload("alloc_vector") +def safealloc(n: Int32) -> Allocatable[Int32[:]]: ... +""", + module_name="memory_mod", + ) + + assert [(item.name, [procedure.name for procedure in item.procedures]) for item in module.overload_sets] == [ + ("safealloc", ["alloc_vector"]) + ] + + +@pytest.mark.parametrize( + "declared_result", + ["Allocatable[Float64[:]]", "Allocatable[Int32[:, :]]", "Int32"], +) +def test_overload_still_rejects_a_projected_result_of_another_type(declared_result: str): + """Neutralizing write-through leaves every other result difference compared.""" + source = f""" +@native_call([Return('x', 0), Addr(Arg(0))]) +def alloc_vector(n: Int32) -> Allocatable[Int32[:]]: ... + +@bind("safealloc") +@overload("alloc_vector") +def safealloc(n: Int32) -> {declared_result}: ... +""" + + with pytest.raises(ValueError, match="declaration 'safealloc' is incompatible"): + parse_pyi_text(source, module_name="memory_mod") + + +def test_overload_accepts_a_specific_that_projects_a_scalar_descriptor_to_its_result(): + """A nullable descriptor result is the only form an overload can restate. + + A native scalar descriptor result is written as a nullable value plus a + `native_call` result wrapper, and an overload declaration may carry no + `native_call`. The declaration therefore spells the visible value alone, as + the contract printer emits it. + """ + module = parse_pyi_text( + """ +@native_call([Allocatable(Return('x', 0)), Addr(Arg(0))]) +def alloc_character(n: Int32) -> String[:] | None: ... + +@bind("safealloc") +@overload("alloc_character") +def safealloc(n: Int32) -> String[:] | None: ... +""", + module_name="memory_mod", + ) + + assert [(item.name, [procedure.name for procedure in item.procedures]) for item in module.overload_sets] == [ + ("safealloc", ["alloc_character"]) + ] + + +@pytest.mark.parametrize("declared_result", ["String", "Int32[:] | None", "Float64[:] | None"]) +def test_overload_still_rejects_a_projected_descriptor_of_another_type(declared_result: str): + """Reading a descriptor result as nullable leaves every other difference compared.""" + source = f""" +@native_call([Allocatable(Return('x', 0)), Addr(Arg(0))]) +def alloc_character(n: Int32) -> String[:] | None: ... + +@bind("safealloc") +@overload("alloc_character") +def safealloc(n: Int32) -> {declared_result}: ... +""" + + with pytest.raises(ValueError, match="declaration 'safealloc' is incompatible"): + parse_pyi_text(source, module_name="memory_mod") diff --git a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/__init__.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/__init__.pyi index d7c63d6cf..91c482ff8 100644 --- a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/__init__.pyi +++ b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/__init__.pyi @@ -2,3 +2,5 @@ from . import first_math from . import shared_types from . import second_math from . import box_ops + +__all__ = ["first_math", "shared_types", "second_math", "box_ops"] diff --git a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi index ffc6ff07d..bac8d5a7e 100644 --- a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi +++ b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi @@ -1,6 +1,13 @@ -from prik.contracts import Int32 -from shared_types import box +from prik.contracts import Addr, Arg, Int32, native_call +from .shared_types import Box def box_value( - item: box + item: Box ) -> Int32: ... + +@native_call([Addr(Arg(0))]) +def boxed( + value: Int32 +) -> Box: ... + +__all__ = ["box_value", "boxed"] diff --git a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/first_math.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/first_math.pyi index 6f66faa97..cc965ca9b 100644 --- a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/first_math.pyi +++ b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/first_math.pyi @@ -4,3 +4,5 @@ from prik.contracts import Addr, Arg, Int32, native_call def add_one( value: Int32 ) -> Int32: ... + +__all__ = ["add_one"] diff --git a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi index bb8c307a4..8a6aaf9dd 100644 --- a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi +++ b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi @@ -1,7 +1,9 @@ from prik.contracts import Addr, Arg, Int32, native_call -from first_math import add_one +from .first_math import add_one @native_call([Addr(Arg(0))]) def double_after_add( value: Int32 ) -> Int32: ... + +__all__ = ["double_after_add", "add_one"] diff --git a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/shared_types.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/shared_types.pyi index 46068bb75..a2f48835e 100644 --- a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/shared_types.pyi +++ b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/shared_types.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Arg, Int32, native_call -class box: +class Box: def __init__( self, *, @@ -12,4 +12,6 @@ class box: @native_call([Addr(Arg(0))]) def make_box( value: Int32 -) -> box: ... +) -> Box: ... + +__all__ = ["Box", "make_box"] diff --git a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/__init__.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/__init__.pyi index 2907a6325..ee22f99de 100644 --- a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/__init__.pyi +++ b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/__init__.pyi @@ -1 +1,3 @@ from . import fruntime_abi_f90 + +__all__ = ["fruntime_abi_f90"] diff --git a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/fruntime_abi_f90.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/fruntime_abi_f90.pyi index fd324298a..48a2a532e 100644 --- a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/fruntime_abi_f90.pyi +++ b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/fruntime_abi_f90.pyi @@ -5,3 +5,5 @@ def scale( value: Float64, factor: Float64 ) -> Float64: ... + +__all__ = ["scale"] diff --git a/tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py b/tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py index 9142b41c7..58f2868cd 100644 --- a/tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py +++ b/tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py @@ -64,6 +64,11 @@ type(box), intent(in) :: item out = item%value end function box_value +function boxed(value) result(out) + integer, intent(in) :: value + type(box) :: out + out%value = value +end function boxed end module box_ops """ @@ -180,9 +185,13 @@ def _build_contract( def _assert_combined_runtime(module) -> None: assert module.first_math.add_one(np.int32(4)) == np.int32(5) + assert module.second_math.add_one is module.first_math.add_one assert module.second_math.double_after_add(np.int32(4)) == np.int32(10) box = module.shared_types.make_box(np.int32(7)) assert module.box_ops.box_value(box) == np.int32(7) + # `box_ops` returns a type `shared_types` defines, so the result is built + # from that namespace's class rather than looked for in its own. + assert type(module.box_ops.boxed(np.int32(8))) is module.shared_types.Box def test_multi_file_modules_build_one_merged_extension(tmp_path: Path): @@ -296,9 +305,11 @@ def test_multi_source_pyi_out_writes_one_flat_combined_package(tmp_path: Path): assert not (package / "second_api").exists() assert not (package / "combined_extensions").exists() assert entry.read_text(encoding="utf-8") == ( - "from . import first_math\nfrom . import shared_types\nfrom . import second_math\nfrom . import box_ops\n" + "from . import first_math\nfrom . import shared_types\nfrom . import second_math\nfrom . import box_ops\n\n" + '__all__ = ["first_math", "shared_types", "second_math", "box_ops"]\n' ) - assert "shared_types" in (package / "box_ops.pyi").read_text(encoding="utf-8") + assert "from .shared_types import Box\n" in (package / "box_ops.pyi").read_text(encoding="utf-8") + assert "from .first_math import add_one" in (package / "second_math.pyi").read_text(encoding="utf-8") def test_multi_source_generated_contract_build_matches_source_runtime_and_link_order(tmp_path: Path): @@ -331,6 +342,38 @@ def test_multi_source_generated_contract_build_matches_source_runtime_and_link_o ] _assert_combined_runtime(source_module) _assert_combined_runtime(generated_module) + # `box_ops` imports the type to express its own signature and publishes no + # name of its own, so neither route adds one. The type stays where it is + # declared, and both builds agree on that. + assert not hasattr(generated_module.box_ops, "Box") + assert not hasattr(source_module.box_ops, "Box") + assert generated_module.shared_types.Box is not None + + +def test_generated_module_leaf_loads_sibling_type_contract(tmp_path: Path): + sources = _write_combined_sources(tmp_path) + entry = _generate_combined_contract(sources, tmp_path / "contracts") + native_objects = _compile_native_objects(sources, tmp_path / "native") + + module, payload = _build_contract( + entry.parent / "box_ops.pyi", + native_objects, + tmp_path / "leaf_build", + output_name="box_leaf", + ) + + assert payload["sources"] == [ + str(entry.parent / "box_ops.pyi"), + str(entry.parent / "shared_types.pyi"), + ] + # The leaf publishes what its `__all__` states. The sibling type it + # imports for its signatures is bound under no name of its own, yet it is + # a real class its procedures return and accept. + assert not hasattr(module, "Box") + assert not hasattr(module, "box") + box = module.boxed(np.int32(7)) + assert type(box).__name__ == "Box" + assert module.box_value(box) == np.int32(7) def test_multi_source_modified_entry_preserves_modules_and_adds_documented_alias(tmp_path: Path): @@ -347,7 +390,8 @@ def test_multi_source_modified_entry_preserves_modules_and_adds_documented_alias "from . import shared_types\n" "from . import second_math\n" "from . import box_ops\n" - "from .second_math import double_after_add as fused_value\n", + "from .second_math import double_after_add as fused_value\n\n" + '__all__ = ["first_math", "shared_types", "second_math", "box_ops", "fused_value"]\n', encoding="utf-8", ) @@ -426,3 +470,81 @@ def test_makefile_mode_reproduces_multi_source_build(tmp_path: Path): assert module.second_api.double_value(np.int32(4)) == 10 finally: sys.path.remove(str(tmp_path)) + + +REEXPORT_OWNERSHIP_SOURCE = """\ +module owner_mod + implicit none +contains + subroutine scale_twice(value, scaled) + integer, intent(in) :: value + integer, intent(out) :: scaled + scaled = value * 2 + end subroutine scale_twice +end module owner_mod + +module facade_mod + use owner_mod, only : scale_twice + implicit none + private + public :: scale_twice +end module facade_mod + +module renaming_mod + use owner_mod, only : doubled => scale_twice + implicit none + private + public :: doubled +end module renaming_mod +""" + + +def _entry_listing(package: Path, modules: list[str]) -> None: + """Rewrite a package entry so it imports its modules in one stated order.""" + lines = "".join(f"from . import {name}\n" for name in modules) + stated = ", ".join(f'"{name}"' for name in modules) + (package / "__init__.pyi").write_text(f"{lines}\n__all__ = [{stated}]\n", encoding="utf-8") + + +@pytest.mark.parametrize( + "order", + [ + pytest.param(["owner_mod", "facade_mod", "renaming_mod"], id="declaration-first"), + pytest.param(["renaming_mod", "facade_mod", "owner_mod"], id="declaration-last"), + ], +) +def test_reexport_is_owned_by_its_declaring_contract_whatever_the_entry_lists_first( + order: list[str], + tmp_path: Path, +): + """The contract declaring a procedure owns it, whichever entry names it first. + + An entry composes a package by importing from it, and the order it does so + is not a statement about where anything is declared. Reading ownership from + that order lets a facade own what it only republishes, and the wrapper then + belongs to the wrong namespace. + """ + source = tmp_path / "ownership.f90" + source.write_text(REEXPORT_OWNERSHIP_SOURCE, encoding="utf-8") + package = tmp_path / "contracts" + subprocess.run( + [sys.executable, "-m", "prik", "generate", "--pyi", str(source), "--out", str(package)], + capture_output=True, + text=True, + check=True, + ) + entry = package / "__init__.pyi" + _entry_listing(package, order) + + native_objects = _compile_native_objects((source,), tmp_path / "native") + module, _payload = _build_contract(entry, native_objects, tmp_path / "build", output_name="ownership") + + assert module.facade_mod.scale_twice is module.owner_mod.scale_twice + assert module.renaming_mod.doubled is module.owner_mod.scale_twice + assert module.facade_mod.scale_twice(np.int32(21)) == np.int32(42) + + # One wrapper defines the procedure, and the declaring namespace holds it. + generated = next((tmp_path / "build").rglob("*_wrapper.c")).read_text(encoding="utf-8") + assert generated.count("static PyObject * wrap_scale_twice") == 1 + assert 'prik_bind_namespace_alias(namespace_facade_mod, "scale_twice", namespace_owner_mod' in generated + assert 'prik_bind_namespace_alias(namespace_renaming_mod, "doubled", namespace_owner_mod' in generated diff --git a/tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py b/tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py index 502b60480..7f6d8ba71 100644 --- a/tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py +++ b/tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py @@ -323,7 +323,7 @@ def test_documented_readme_points_example_builds_and_imports(tmp_path: Path): points = geometry.points assert points.__name__ == "geometry.points" assert points.norm_squared.__module__ == "geometry.points" - item = points.point(x=np.float64(3.0), y=np.float64(4.0)) + item = points.Point(x=np.float64(3.0), y=np.float64(4.0)) points.move(item, np.float64(1.0), np.float64(-2.0)) assert item.x == np.float64(4.0) assert item.y == np.float64(2.0) diff --git a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi index e191cf56a..9a8bc393e 100644 --- a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi +++ b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi @@ -5,3 +5,5 @@ from prik.contracts import Addr, Arg, Int32, Returns, native_call, standalone def add_one( value: Int32 ) -> tuple[Int32, Returns["value", Int32]]: ... + +__all__ = ["add_one"] diff --git a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/__init__.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/__init__.pyi index 2907a6325..ee22f99de 100644 --- a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/__init__.pyi +++ b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/__init__.pyi @@ -1 +1,3 @@ from . import fruntime_abi_f90 + +__all__ = ["fruntime_abi_f90"] diff --git a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/fruntime_abi_f90.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/fruntime_abi_f90.pyi index fd324298a..48a2a532e 100644 --- a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/fruntime_abi_f90.pyi +++ b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/fruntime_abi_f90.pyi @@ -5,3 +5,5 @@ def scale( value: Float64, factor: Float64 ) -> Float64: ... + +__all__ = ["scale"] diff --git a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/__init__.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/__init__.pyi index 3b46f889d..8085c100d 100644 --- a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/__init__.pyi +++ b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/__init__.pyi @@ -1 +1,3 @@ from . import verbose_api + +__all__ = ["verbose_api"] diff --git a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/verbose_api.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/verbose_api.pyi index 824579ad3..abe0482e9 100644 --- a/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/verbose_api.pyi +++ b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/verbose_api.pyi @@ -1 +1,3 @@ def ping() -> None: ... + +__all__ = ["ping"] diff --git a/tests/fortran/infrastructure/cli/pipeline/test_output_contract.py b/tests/fortran/infrastructure/cli/pipeline/test_output_contract.py index 7b99f160e..c5c793a9f 100644 --- a/tests/fortran/infrastructure/cli/pipeline/test_output_contract.py +++ b/tests/fortran/infrastructure/cli/pipeline/test_output_contract.py @@ -288,7 +288,7 @@ def test_cli_pyi_out_writes_adjacent_contract_package(tmp_path: Path): assert res.stdout == "" package = tmp_path / "mini" - assert (package / "mini.pyi").read_text(encoding="utf-8") == "from . import m\n" + assert (package / "mini.pyi").read_text(encoding="utf-8") == 'from . import m\n\n__all__ = ["m"]\n' assert "def add1" in (package / "m.pyi").read_text(encoding="utf-8") @@ -316,7 +316,7 @@ def test_cli_pyi_out_writes_modules_inside_source_contract_package(tmp_path: Pat assert result.stdout == "" package = tmp_path / "combined" assert (package / "combined.pyi").read_text(encoding="utf-8") == ( - "from . import first_mod\nfrom . import second_mod\n" + 'from . import first_mod\nfrom . import second_mod\n\n__all__ = ["first_mod", "second_mod"]\n' ) assert "def first(" in (package / "first_mod.pyi").read_text(encoding="utf-8") assert "def second(" in (package / "second_mod.pyi").read_text(encoding="utf-8") @@ -341,7 +341,7 @@ def test_cli_pyi_out_uses_explicit_contract_package_from_inline_code(tmp_path: P assert res.stdout == "" text = (out / "__init__.pyi").read_text(encoding="utf-8") - assert text == "from . import explicit_mod\n" + assert text == 'from . import explicit_mod\n\n__all__ = ["explicit_mod"]\n' leaf_text = (out / "explicit_mod.pyi").read_text(encoding="utf-8") assert "@native_call([Return('x', 0)])" in leaf_text assert "def set_value(" in leaf_text @@ -1026,3 +1026,38 @@ def test_assume_intent_in_scalars_removes_them_from_the_generated_contract(tmp_p assert "Returns" not in text assert "-> Float64: ..." in text + + +def test_fortran_parser_cli_pyi_is_the_contract_generate_writes(tmp_path: Path): + """The parser CLI shows the generated contract, not an unplanned rendering of its own. + + Its report converted and printed each module alone, so a module importing + from another file lost the import completion plans and the spelling + completion gives each name. + """ + helpers = tmp_path / "helpers.f90" + helpers.write_text( + "module helpers\ncontains\n" + "pure integer function lambda(n)\ninteger, intent(in) :: n\nlambda = n\nend function lambda\n" + "end module helpers\n", + encoding="utf-8", + ) + user = tmp_path / "user.f90" + user.write_text( + "module user_mod\nuse helpers, only : lambda\ncontains\n" + "subroutine fill(n, x)\ninteger, intent(in) :: n\nreal(8), intent(out) :: x(lambda(n))\nend subroutine fill\n" + "end module user_mod\n", + encoding="utf-8", + ) + contracts = tmp_path / "contracts" + subprocess.run( + [sys.executable, "-m", "prik", "generate", "--pyi", str(helpers), str(user), "--out", str(contracts)], + check=True, + capture_output=True, + ) + + report = fortran_parser_cli._semantic_report([str(helpers), str(user)]) + + assert report[str(helpers)]["pyi"] == (contracts / "helpers.pyi").read_text(encoding="utf-8").strip() + assert report[str(user)]["pyi"] == (contracts / "user_mod.pyi").read_text(encoding="utf-8").strip() + assert "from .helpers import lambda_" in report[str(user)]["pyi"] diff --git a/tests/fortran/infrastructure/cli/pipeline/test_stage_dispatch.py b/tests/fortran/infrastructure/cli/pipeline/test_stage_dispatch.py index be2e7e550..95dfc8e74 100644 --- a/tests/fortran/infrastructure/cli/pipeline/test_stage_dispatch.py +++ b/tests/fortran/infrastructure/cli/pipeline/test_stage_dispatch.py @@ -320,16 +320,18 @@ def test_prik_pyi_report_writes_opaque_dependency_stub_for_external_type(tmp_pat payload = prik_cli._semantic_report([str(physics)]) assert payload[str(physics)]["pyi_dependencies"] == { - "types_mod": "from prik.contracts import Opaque\n\nclass particle(Opaque):\n pass" + "types_mod": 'from prik.contracts import Opaque\n\nclass particle(Opaque):\n pass\n\n__all__ = ["particle"]' } monkeypatch.setattr(sys, "argv", ["prik", "generate", "--pyi", str(physics), "--out"]) assert prik_cli.main() == 0 package = tmp_path / "physics" - assert (package / "__init__.pyi").read_text(encoding="utf-8") == "from . import physics\n" + assert (package / "__init__.pyi").read_text(encoding="utf-8") == ( + 'from . import physics\n\n__all__ = ["physics"]\n' + ) assert (package / "types_mod.pyi").read_text( encoding="utf-8" - ) == "from prik.contracts import Opaque\n\nclass particle(Opaque):\n pass\n" + ) == 'from prik.contracts import Opaque\n\nclass particle(Opaque):\n pass\n\n__all__ = ["particle"]\n' @pytest.mark.parametrize( diff --git a/tests/fortran/infrastructure/codegen/test_planner.py b/tests/fortran/infrastructure/codegen/test_planner.py index f923c976f..4d4fb541b 100644 --- a/tests/fortran/infrastructure/codegen/test_planner.py +++ b/tests/fortran/infrastructure/codegen/test_planner.py @@ -72,6 +72,91 @@ def right_value(x: Int32) -> Int32: ... assert plan.namespaces[2].functions[0].symbol_name == "right_shared_value" +def test_planner_keeps_one_module_variable_plan_for_multiple_publications(): + """Namespace publications reference one plan that owns native access.""" + module = parse_pyi_text("counter: Int32\n", module_name="state") + module.variables[0].metadata[PYTHON_EXPORTS_METADATA] = [ + {"namespace": (), "name": "counter"}, + {"namespace": ("facade",), "name": "counter"}, + ] + complete_semantic_policies(module) + + plan = WrapperPlanner().build(module) + + variables = list(plan.variables) + publications = [ + (namespace.python_path, publication.variable, publication.python_names) + for namespace in plan.namespaces + for publication in namespace.variable_publications + ] + assert len(variables) == 1 + assert [(path, names) for path, _variable, names in publications] == [ + ((), ("counter",)), + (("facade",), ("counter",)), + ] + assert all(variable is variables[0] for _path, variable, _names in publications) + + +def test_module_variable_owner_is_its_native_identity_not_a_publication_path(): + """Adding a facade changes publications without moving native ownership.""" + + def planned_owner(*namespaces: str): + module = parse_pyi_text("values: Int32\n", module_name="package") + variable = module.variables[0] + variable.origin.native_scope = "home" + variable.origin.native_name = "values" + variable.metadata[PYTHON_EXPORTS_METADATA] = [ + {"namespace": (namespace,), "name": "values"} for namespace in namespaces + ] + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + facade_only = planned_owner("facade") + facade_and_api = planned_owner("facade", "api") + + assert [variable.owner_path for variable in facade_only.variables] == ["home.values"] + assert [variable.owner_path for variable in facade_and_api.variables] == ["home.values"] + assert [variable.binding.support_namespace for variable in facade_only.variables] == [()] + assert [variable.binding.support_namespace for variable in facade_and_api.variables] == [()] + assert facade_only.entrypoint.support_procedures + assert [ + (procedure.owner_path, procedure.role, procedure.symbol_name) + for procedure in facade_only.entrypoint.support_procedures + ] == [ + (procedure.owner_path, procedure.role, procedure.symbol_name) + for procedure in facade_and_api.entrypoint.support_procedures + ] + assert { + (namespace.python_path, publication.variable.owner_path) + for namespace in facade_and_api.namespaces + for publication in namespace.variable_publications + } == { + (("api",), "home.values"), + (("facade",), "home.values"), + } + + +def test_two_python_names_one_folded_stem_get_separate_generated_symbols(): + """A generated symbol is shared with Fortran, which folds the two together.""" + module = parse_pyi_text( + """ +def left_value(x: Int32) -> Int32: ... +def right_value(x: Int32) -> Int32: ... +""", + module_name="folded", + ) + module.functions[0].metadata[PYTHON_EXPORTS_METADATA] = [{"namespace": (), "name": "Foo"}] + module.functions[1].metadata[PYTHON_EXPORTS_METADATA] = [{"namespace": (), "name": "foo"}] + complete_semantic_policies(module) + + plan = WrapperPlanner().build(module) + + functions = plan.namespaces[0].functions + assert [function.binding.python_name for function in functions] == ["Foo", "foo"] + stems = [function.symbol_name for function in functions] + assert len({stem.casefold() for stem in stems}) == len(stems) + + def test_binding_registers_child_namespaces_as_importable_submodules(): module = parse_pyi_text( """ @@ -161,7 +246,13 @@ def move(self, dx: Float64) -> None: ... plan = WrapperPlanner().build(module) generated = WrapperGenerator().generate(plan) - assert tuple(derived.type_name for derived in plan.namespaces[0].derived_types) == ("outer", "inner") + planned_outer, planned_inner = plan.namespaces[0].derived_types + assert (planned_outer.native_type_name, planned_inner.native_type_name) == ("outer", "inner") + # The nested type is defined beside its parent and bound on it, not here. + assert planned_outer.python_names == ("outer",) + assert planned_inner.python_names == () + assert planned_inner.nested_in == planned_outer.type_identity + assert planned_inner.contract_name == "inner" assert {source.path.suffix for source in generated.sources} == {".c", ".h", ".f90"} diff --git a/tests/fortran/infrastructure/jupyter/end_to_end/test_fortran_magic_runtime.py b/tests/fortran/infrastructure/jupyter/end_to_end/test_fortran_magic_runtime.py index 7c6c0e76b..26073a378 100644 --- a/tests/fortran/infrastructure/jupyter/end_to_end/test_fortran_magic_runtime.py +++ b/tests/fortran/infrastructure/jupyter/end_to_end/test_fortran_magic_runtime.py @@ -139,6 +139,8 @@ def counting_build(*args, **kwargs): ) contract = contract.replace("@native_call", '@bind("square")\n@native_call') contract = contract.replace("def square(", "def squared(") + # Renaming a declaration renames what the contract publishes. + contract = contract.replace('__all__ = ["square"]', '__all__ = ["squared"]') line = magic_line.removeprefix("%%pyi").strip() shell.run_cell_magic("pyi", line, contract) diff --git a/tests/fortran/infrastructure/naming/test_policy.py b/tests/fortran/infrastructure/naming/test_policy.py index 1a4f1f976..db655f640 100644 --- a/tests/fortran/infrastructure/naming/test_policy.py +++ b/tests/fortran/infrastructure/naming/test_policy.py @@ -3,7 +3,61 @@ import pytest from prik.naming import NamingPolicy -from prik.naming import normalize_public_name +from prik.naming import normalize_public_name, preserves_source_case + + +def test_only_a_case_insensitive_language_gives_up_its_own_spelling(): + """Case is a name's identity everywhere a source distinguishes two spellings.""" + assert preserves_source_case("c") is True + assert preserves_source_case("pyi") is True + assert preserves_source_case(None) is True + assert preserves_source_case("fortran") is False + assert preserves_source_case("FORTRAN") is False + + +def test_a_folded_name_loses_a_spelling_a_preserved_one_keeps(): + """Folding is right only where the source never meant the two to differ.""" + assert normalize_public_name("BarBaz").name == "barbaz" + assert normalize_public_name("BarBaz", preserve_case=True).name == "BarBaz" + # Python still cannot bind a keyword, whichever rule names the declaration. + assert normalize_public_name("lambda", preserve_case=True).name == "lambda_" + # Casing alone is not a rename, so strict naming has nothing to reject. + assert normalize_public_name("BarBaz", preserve_case=True).needs_fix is False + + +def test_two_spellings_collide_only_where_the_source_folds_them(): + """A case-sensitive source names two declarations; folding invents a collision.""" + folding = NamingPolicy() + assert folding.reserve_public_name((), "Foo", category="function") == "foo" + assert folding.reserve_public_name((), "foo", category="function") == "foo_2" + + preserving = NamingPolicy(preserve_case=True) + assert preserving.reserve_public_name((), "Foo", category="function") == "Foo" + assert preserving.reserve_public_name((), "foo", category="function") == "foo" + + +def test_a_wrapped_type_is_named_as_a_python_class(): + """A derived type reaches Python as a class, so PRIK spells it like one.""" + assert normalize_public_name("point_t", category="class").name == "Point_T" + assert normalize_public_name("my_particle_type", category="class").name == "My_Particle_Type" + assert normalize_public_name("accumulator", category="class").name == "Accumulator" + # Fortran writes one type under many spellings, so the style does not + # depend on which one the source happened to use. + assert normalize_public_name("POINT_T", category="class").name == "Point_T" + # Every other declaration keeps the lower-case form. + assert normalize_public_name("point_t").name == "point_t" + + +def test_a_source_that_spells_its_own_types_keeps_that_spelling(): + """C names each declaration exactly, so PRIK has no spelling to choose.""" + assert normalize_public_name("point", preserve_case=True, category="class").name == "point" + assert normalize_public_name("Point", preserve_case=True, category="class").name == "Point" + + +def test_a_chosen_class_style_is_not_a_name_python_forced(): + """Strict naming rejects what Python cannot spell, not how PRIK cases it.""" + assert normalize_public_name("point_t", category="class").needs_fix is False + assert normalize_public_name("point t", category="class").needs_fix is True def test_public_python_names_escape_keywords_and_collisions(): diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/assumed_shape_and_derived_args.json b/tests/fortran/infrastructure/parsing/fixtures/general/assumed_shape_and_derived_args.json index bb2d6eb98..e099a08b1 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/assumed_shape_and_derived_args.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/assumed_shape_and_derived_args.json @@ -47,7 +47,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -91,7 +91,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -126,7 +126,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -177,7 +177,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -221,7 +221,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -256,7 +256,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.json b/tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.json index 83ddbc569..da3d89d3e 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.json @@ -7,7 +7,7 @@ { "name": "m1", "filename": "basic_subroutine.f90", - "uses": {}, + "uses": [], "variables": [], "procedures": [ { @@ -67,7 +67,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -95,7 +95,7 @@ "m1": { "name": "m1", "filename": "basic_subroutine.f90", - "uses": {}, + "uses": [], "variables": [], "procedures": [ { @@ -155,7 +155,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_all_exprs.json b/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_all_exprs.json index ce047d5c0..ad78fded7 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_all_exprs.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_all_exprs.json @@ -7,7 +7,7 @@ { "name": "expr_mod", "filename": "compile_time_all_exprs.f90", - "uses": {}, + "uses": [], "variables": [ { "name": "a", @@ -452,7 +452,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -480,7 +480,7 @@ "expr_mod": { "name": "expr_mod", "filename": "compile_time_all_exprs.f90", - "uses": {}, + "uses": [], "variables": [ { "name": "a", @@ -925,7 +925,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_shape_exprs.json b/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_shape_exprs.json index dfe617d74..affa73cba 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_shape_exprs.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_shape_exprs.json @@ -7,7 +7,7 @@ { "name": "dims_mod", "filename": "compile_time_shape_exprs.f90", - "uses": {}, + "uses": [], "variables": [ { "name": "n0", @@ -116,7 +116,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -144,7 +144,7 @@ "dims_mod": { "name": "dims_mod", "filename": "compile_time_shape_exprs.f90", - "uses": {}, + "uses": [], "variables": [ { "name": "n0", @@ -253,7 +253,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/derived_type.json b/tests/fortran/infrastructure/parsing/fixtures/general/derived_type.json index eeabfd7f2..086f2efd9 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/derived_type.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/derived_type.json @@ -7,7 +7,7 @@ { "name": "particle_mod", "filename": "derived_type.f90", - "uses": {}, + "uses": [], "variables": [], "procedures": [ { @@ -40,7 +40,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -145,7 +145,7 @@ "particle_mod": { "name": "particle_mod", "filename": "derived_type.f90", - "uses": {}, + "uses": [], "variables": [], "procedures": [ { @@ -178,7 +178,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/derived_types_and_methods.json b/tests/fortran/infrastructure/parsing/fixtures/general/derived_types_and_methods.json index d886875fc..609631403 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/derived_types_and_methods.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/derived_types_and_methods.json @@ -7,7 +7,7 @@ { "name": "mesh_mod", "filename": "derived_types_and_methods.f90", - "uses": {}, + "uses": [], "variables": [], "procedures": [], "derived_types": [ @@ -179,7 +179,7 @@ "mesh_mod": { "name": "mesh_mod", "filename": "derived_types_and_methods.f90", - "uses": {}, + "uses": [], "variables": [], "procedures": [], "derived_types": [ diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/f77_subroutine.json b/tests/fortran/infrastructure/parsing/fixtures/general/f77_subroutine.json index 151c65f29..b9299654e 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/f77_subroutine.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/f77_subroutine.json @@ -113,7 +113,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -230,7 +230,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.json b/tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.json index 72d349fbc..f6c902f0b 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.json @@ -7,7 +7,7 @@ { "name": "modern_math_physics", "filename": "modern_pyi_example.f90", - "uses": {}, + "uses": [], "variables": [ { "name": "counter", @@ -188,7 +188,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -306,7 +306,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -368,7 +368,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -456,7 +456,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -500,7 +500,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -535,7 +535,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -570,7 +570,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -768,7 +768,7 @@ "modern_math_physics": { "name": "modern_math_physics", "filename": "modern_pyi_example.f90", - "uses": {}, + "uses": [], "variables": [ { "name": "counter", @@ -949,7 +949,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -1067,7 +1067,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -1129,7 +1129,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -1217,7 +1217,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -1261,7 +1261,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -1296,7 +1296,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -1331,7 +1331,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/module_vars_use.json b/tests/fortran/infrastructure/parsing/fixtures/general/module_vars_use.json index dd4fa0c4d..9cc188849 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/module_vars_use.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/module_vars_use.json @@ -7,18 +7,22 @@ { "name": "constants_mod", "filename": "module_vars_use.f90", - "uses": { - "iso_c_binding": [ - { - "source": "c_int", - "target": null - }, - { - "source": "c_double", - "target": null - } - ] - }, + "uses": [ + { + "module": "iso_c_binding", + "only": true, + "mappings": [ + { + "source": "c_int", + "target": null + }, + { + "source": "c_double", + "target": null + } + ] + } + ], "variables": [ { "name": "nmax", @@ -92,18 +96,22 @@ "constants_mod": { "name": "constants_mod", "filename": "module_vars_use.f90", - "uses": { - "iso_c_binding": [ - { - "source": "c_int", - "target": null - }, - { - "source": "c_double", - "target": null - } - ] - }, + "uses": [ + { + "module": "iso_c_binding", + "only": true, + "mappings": [ + { + "source": "c_int", + "target": null + }, + { + "source": "c_double", + "target": null + } + ] + } + ], "variables": [ { "name": "nmax", diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/procedures_and_functions.json b/tests/fortran/infrastructure/parsing/fixtures/general/procedures_and_functions.json index 8c0141157..48cec8b72 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/procedures_and_functions.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/procedures_and_functions.json @@ -7,7 +7,7 @@ { "name": "math_mod", "filename": "procedures_and_functions.f90", - "uses": {}, + "uses": [], "variables": [], "procedures": [ { @@ -66,7 +66,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -128,7 +128,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -156,7 +156,7 @@ "math_mod": { "name": "math_mod", "filename": "procedures_and_functions.f90", - "uses": {}, + "uses": [], "variables": [], "procedures": [ { @@ -215,7 +215,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -277,7 +277,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] diff --git a/tests/fortran/infrastructure/parsing/fixtures/general/scope_name_reuse_combinations.json b/tests/fortran/infrastructure/parsing/fixtures/general/scope_name_reuse_combinations.json index 4b3cfcfeb..34be05db6 100644 --- a/tests/fortran/infrastructure/parsing/fixtures/general/scope_name_reuse_combinations.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/scope_name_reuse_combinations.json @@ -7,7 +7,7 @@ { "name": "scope_name_reuse_combinations", "filename": "scope_name_reuse_combinations.f90", - "uses": {}, + "uses": [], "variables": [ { "name": "same_name_i", @@ -146,7 +146,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -181,7 +181,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -216,7 +216,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -251,7 +251,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -286,7 +286,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -341,7 +341,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -396,7 +396,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -451,7 +451,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -504,7 +504,11 @@ "do_work_r", "do_work_l" ], - "abstract": false + "abstract": false, + "declaring_scope_kind": "module", + "declaring_scope_path": [ + "scope_name_reuse_combinations" + ] } ], "enums": [], @@ -527,7 +531,7 @@ "scope_name_reuse_combinations": { "name": "scope_name_reuse_combinations", "filename": "scope_name_reuse_combinations.f90", - "uses": {}, + "uses": [], "variables": [ { "name": "same_name_i", @@ -666,7 +670,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -701,7 +705,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -736,7 +740,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -771,7 +775,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -806,7 +810,7 @@ "result": null, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -861,7 +865,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -916,7 +920,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -971,7 +975,7 @@ }, "attributes": [], "bind_name": null, - "uses": {}, + "uses": [], "in_interface": false, "variables": {}, "common_variables": [] @@ -1024,7 +1028,11 @@ "do_work_r", "do_work_l" ], - "abstract": false + "abstract": false, + "declaring_scope_kind": "module", + "declaring_scope_path": [ + "scope_name_reuse_combinations" + ] } ], "enums": [], diff --git a/tests/fortran/infrastructure/parsing/test_declaration_and_interface_edges.py b/tests/fortran/infrastructure/parsing/test_declaration_and_interface_edges.py index 8c407d474..aa8c7199a 100644 --- a/tests/fortran/infrastructure/parsing/test_declaration_and_interface_edges.py +++ b/tests/fortran/infrastructure/parsing/test_declaration_and_interface_edges.py @@ -3,6 +3,7 @@ import pytest from prik.parsers.fortran.models import FortranModule +from prik.parsers.fortran.scope import ScopeUses from prik.parsers.fortran.parser import FortranParser, _ParserScope from prik.parsers.fortran import FortranParseError, parse_fortran_file, parse_fortran_project @@ -137,11 +138,18 @@ def test_use_rename_and_intrinsic_forms_are_recorded(): module = parse_fortran_file(code).modules[0] - assert module.uses["list_input"] == ["delete_input"] - assert module.uses["list_input"][0].source == "delete_input_list" - assert module.uses["list_input"][0].target == "delete_input" - assert module.uses["iso_c_binding"] == ["c_int", "c_double"] - assert [(item.source, item.target) for item in module.uses["iso_c_binding"]] == [ + scope = ScopeUses(module.uses) + # A rename without `only` binds the new name and still imports the rest. + assert scope.imports_all("list_input") is True + assert list(scope.mappings("list_input")) == ["delete_input"] + assert (scope.mappings("list_input")[0].source, scope.mappings("list_input")[0].target) == ( + "delete_input_list", + "delete_input", + ) + + assert scope.imports_all("iso_c_binding") is False + assert list(scope.mappings("iso_c_binding")) == ["c_int", "c_double"] + assert [(item.source, item.target) for item in scope.mappings("iso_c_binding")] == [ ("c_int", None), ("c_double", None), ] @@ -319,7 +327,7 @@ def test_use_statement_empty_only_items_are_ignored(): module = parse_fortran_file(code, filename="use_empty_items.f90").modules[0] - assert [item.local_name for item in module.uses["constants_mod"]] == ["rk", "ik"] + assert [item.local_name for item in ScopeUses(module.uses).mappings("constants_mod")] == ["rk", "ik"] def test_type_field_spec_variants_and_empty_entities_from_public_source(): diff --git a/tests/fortran/infrastructure/parsing/test_fortran_fixture_suite.py b/tests/fortran/infrastructure/parsing/test_fortran_fixture_suite.py index 69c6aed3a..f3567aed7 100644 --- a/tests/fortran/infrastructure/parsing/test_fortran_fixture_suite.py +++ b/tests/fortran/infrastructure/parsing/test_fortran_fixture_suite.py @@ -63,7 +63,12 @@ def _strip_parent_fields(value): def _to_dict(value): - return _strip_parent_fields(asdict(value)) + """Return one parsed file as the golden records it. + + The golden is JSON, where a tuple and a list are the same array, so the + parsed model is compared in that form rather than as Python objects. + """ + return json.loads(json.dumps(_strip_parent_fields(asdict(value)))) def _dump_expected(path: Path, parsed: dict) -> None: diff --git a/tests/fortran/infrastructure/parsing/test_fortran_parser_procedures_and_interfaces.py b/tests/fortran/infrastructure/parsing/test_fortran_parser_procedures_and_interfaces.py index 0368eea6f..8ac4428b4 100644 --- a/tests/fortran/infrastructure/parsing/test_fortran_parser_procedures_and_interfaces.py +++ b/tests/fortran/infrastructure/parsing/test_fortran_parser_procedures_and_interfaces.py @@ -2,6 +2,7 @@ import pytest from prik.parsers.fortran import parse_fortran_file, parse_fortran_project +from prik.parsers.fortran.scope import ScopeUses from prik.parsers.fortran.models import ( FortranFunctionCall, FortranSlice, @@ -62,7 +63,7 @@ def test_function_result_and_use_statement(): assert sig.result is not None assert sig.result.name == "res" assert sig.result.base_type == "real" - assert sig.uses["iso_c_binding"] == ["c_double"] + assert list(ScopeUses(sig.uses).mappings("iso_c_binding")) == ["c_double"] assert sig.arguments[0].shape == [":"] @@ -447,14 +448,14 @@ def test_submodule_module_procedure_stub_and_additional_program_units(): submodule = submodules[0] assert submodule.parent == "parent_impl" assert submodule.ancestor == "ancestor_mod" - assert submodule.uses["iso_c_binding"] == ["c_int"] + assert list(ScopeUses(submodule.uses).mappings("iso_c_binding")) == ["c_int"] assert [v.name for v in submodule.variables] == ["counter"] assert [(p.name, p.kind) for p in submodule.procedures] == [("reset_counter", "module procedure")] programs = parse_fortran_programs(code) assert len(programs) == 1 assert programs[0].name == "driver" - assert programs[0].uses["ancestor_mod"] == [] + assert ScopeUses(programs[0].uses).imports_all("ancestor_mod") is True assert [v.name for v in programs[0].variables] == ["ierr"] block_data = parse_fortran_block_data(code) diff --git a/tests/fortran/infrastructure/policy/test_wrapper_policy.py b/tests/fortran/infrastructure/policy/test_wrapper_policy.py index 3018f220f..7216d7213 100644 --- a/tests/fortran/infrastructure/policy/test_wrapper_policy.py +++ b/tests/fortran/infrastructure/policy/test_wrapper_policy.py @@ -122,6 +122,7 @@ def hidden_status() -> Int32: ... policy = build_function_wrapper_policy( function, owner_path="missing_hidden_projection.hidden_status", + module_export=True, ) assert policy.results == () @@ -243,7 +244,9 @@ def test_fmath_scalar_policy_records_address_projected_call_slots(): assert policy.owner_path == "fmath.add_r8" assert [(export.namespace, export.name) for export in policy.python_exports] == [((), "add_r8")] - assert policy.native_name == "ADD_R8" + # The contract states no separate native name: `add_r8` reaches Fortran's + # `ADD_R8`, which is named without regard to case. + assert policy.native_name == "add_r8" assert policy.standalone is True assert [argument.name for argument in policy.arguments] == ["X", "Y"] diff --git a/tests/fortran/infrastructure/printers/test_source_printers.py b/tests/fortran/infrastructure/printers/test_source_printers.py index 22e2bedd9..71584fd12 100644 --- a/tests/fortran/infrastructure/printers/test_source_printers.py +++ b/tests/fortran/infrastructure/printers/test_source_printers.py @@ -129,6 +129,7 @@ def test_source_printers_reject_wrapper_plan_models(): binding=BindingModulePlan("demo", "demo"), entrypoint=NativeEntrypointModulePlan("demo"), bridge=BridgeModulePlan("demo"), + variables=(), namespaces=(NamespacePlan(owner_path="demo", python_path=()),), ) @@ -156,6 +157,34 @@ def test_fortran_source_printer_wraps_long_parenthesized_call_arguments(): assert max(map(len, source.splitlines())) <= 124 +def test_fortran_source_printer_never_continues_inside_a_character_literal(): + """A literal's commas are its characters, so no continuation may split it. + + Fortran resumes a continued literal after the next line's `&`, so a break + placed at a comma inside quotes changes the characters the literal states + while still compiling. + """ + padding = "x" * 40 + expression = f"build_message(prefix_{padding}, 'alpha, beta', suffix_{padding})" + + source = FortranSourcePrinter().doprint(FortranAssignment("destination", CodeExpression(expression))) + + assert "'alpha, beta'" in source + assert "'alpha, &" not in source + assert max(map(len, source.splitlines())) <= 132 + + +def test_fortran_source_printer_breaks_a_call_at_its_own_arguments(): + """A nested call's commas belong to it, so the outer break skips them.""" + padding = "y" * 40 + expression = f"compute_total(first_{padding}, max(second_term, third_term), fourth_{padding})" + + source = FortranSourcePrinter().doprint(FortranAssignment("destination", CodeExpression(expression))) + + assert "& max(second_term, third_term), &" in source + assert max(map(len, source.splitlines())) <= 132 + + def test_fortran_source_printer_wraps_long_pointer_array_sections(): slices = ", ".join(f"1:values_upper_bound_{axis} + 1:values_stride_{axis}" for axis in range(4)) diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json index 14299dc23..f87810960 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json @@ -1,256 +1,258 @@ { "semantic_modules": [ { - "name": "m1", + "classes": [], + "exported_names": null, "functions": [ { - "name": "add1", - "native_name": "add1", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "n", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "n", + "native_scope": "add1", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "n", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "n", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "n", - "native_abi": null, - "native_symbol": null, - "native_scope": "add1", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + "1" + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "n" + ], + "target": false, + "upper_bounds": [ + "n" + ], "value": false - } + }, + "native_abi": null, + "native_name": "x", + "native_scope": "add1", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - }, - { - "name": "x", "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "n" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ "n" ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ + "target": false, + "upper_bounds": [ "n" ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x", "native_abi": null, - "native_symbol": null, + "native_name": "x", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "n" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ "n" ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ + "source_shape": [ "n" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "x", - "native_abi": null, - "native_symbol": null, - "native_scope": "add1", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "n" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "n" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], + "locals": [], + "metadata": {}, + "name": "add1", + "native_name": "add1", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "add1", + "native_scope": "m1", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "projection": [ { - "python_name": "n", + "native_c_identity": null, "native_name": "n", "native_position": 0, + "python_name": "n", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x", + "native_c_identity": null, "native_name": "x", "native_position": 1, + "python_name": "x", "python_position": 1, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "add1", - "native_abi": null, - "native_symbol": null, - "native_scope": "m1", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" } ], - "prototypes": [], - "overload_sets": [], - "classes": [], - "variables": [], "imports": [], "metadata": {}, + "name": "m1", "origin": { - "source_language": "fortran", - "native_name": "m1", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "m1", "native_scope": "m1", + "native_symbol": null, "source_kind": "module", - "source_type": null, + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "overload_sets": [], + "prototypes": [], + "reexports": [], + "variables": [] } ] } diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json index 6cc74d754..4978c5c17 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json @@ -1,1817 +1,1819 @@ { "semantic_modules": [ { - "name": "expr_mod", + "classes": [], + "exported_names": null, "functions": [ { - "name": "all_exprs", - "native_name": "all_exprs", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "x1", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "1:p_add" + ], + "target": false, + "upper_bounds": [ + "p_add" + ], + "value": false + }, + "native_abi": null, + "native_name": "x1", + "native_scope": "all_exprs", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 1, - "dtype": "Int32", - "shape": [ - "p_add" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "p_add" + "1:p_add" ], - "lower_bounds": [], + "target": false, "upper_bounds": [ "p_add" ], - "source_shape": [ - "1:p_add" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x1", "native_abi": null, - "native_symbol": null, + "native_name": "x1", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "p_add" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ - "1:p_add" + "p_add" ], - "lower_bounds": [ - "1" + "source_shape": [ + "1:p_add" ], "upper_bounds": [ "p_add" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x2", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x1", - "native_abi": null, - "native_symbol": null, - "native_scope": "all_exprs", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 1, - "shape": [ - "1:p_add" - ], + "allocatable": false, + "contiguous": false, "lower_bounds": [ "1" ], - "upper_bounds": [ - "p_add" - ], - "allocatable": false, + "optional": false, "pointer": false, + "rank": 1, + "shape": [ + "1:p_sub" + ], "target": false, - "contiguous": false, - "optional": false, + "upper_bounds": [ + "p_sub" + ], "value": false - } + }, + "native_abi": null, + "native_name": "x2", + "native_scope": "all_exprs", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "x2", "semantic_type": { - "name": "Int32", - "rank": 1, - "dtype": "Int32", - "shape": [ - "p_sub" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "p_sub" + "1:p_sub" ], - "lower_bounds": [], + "target": false, "upper_bounds": [ "p_sub" ], - "source_shape": [ - "1:p_sub" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x2", "native_abi": null, - "native_symbol": null, + "native_name": "x2", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "p_sub" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ - "1:p_sub" + "p_sub" ], - "lower_bounds": [ - "1" + "source_shape": [ + "1:p_sub" ], "upper_bounds": [ "p_sub" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x3", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x2", - "native_abi": null, - "native_symbol": null, - "native_scope": "all_exprs", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 1, - "shape": [ - "1:p_sub" - ], + "allocatable": false, + "contiguous": false, "lower_bounds": [ "1" ], - "upper_bounds": [ - "p_sub" - ], - "allocatable": false, + "optional": false, "pointer": false, + "rank": 1, + "shape": [ + "1:p_mul" + ], "target": false, - "contiguous": false, - "optional": false, + "upper_bounds": [ + "p_mul" + ], "value": false - } + }, + "native_abi": null, + "native_name": "x3", + "native_scope": "all_exprs", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "x3", "semantic_type": { - "name": "Int32", - "rank": 1, - "dtype": "Int32", - "shape": [ - "p_mul" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "p_mul" + "1:p_mul" ], - "lower_bounds": [], + "target": false, "upper_bounds": [ "p_mul" ], - "source_shape": [ - "1:p_mul" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x3", "native_abi": null, - "native_symbol": null, + "native_name": "x3", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "p_mul" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ - "1:p_mul" + "p_mul" ], - "lower_bounds": [ - "1" + "source_shape": [ + "1:p_mul" ], "upper_bounds": [ "p_mul" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x4", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x3", - "native_abi": null, - "native_symbol": null, - "native_scope": "all_exprs", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 1, - "shape": [ - "1:p_mul" - ], + "allocatable": false, + "contiguous": false, "lower_bounds": [ "1" ], - "upper_bounds": [ - "p_mul" - ], - "allocatable": false, + "optional": false, "pointer": false, + "rank": 1, + "shape": [ + "1:p_div" + ], "target": false, - "contiguous": false, - "optional": false, + "upper_bounds": [ + "p_div" + ], "value": false - } + }, + "native_abi": null, + "native_name": "x4", + "native_scope": "all_exprs", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "x4", "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Int32", + "metadata": {}, "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "1:p_div" + ], + "target": false, + "upper_bounds": [ + "p_div" + ], + "value": false + }, + "native_abi": null, + "native_name": "x4", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, "rank": 1, - "dtype": "Int32", "shape": [ "p_div" ], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, - "metadata": {}, "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", "array": { - "rank": 1, - "shape": [ - "p_div" + "allocatable": false, + "axes": [ + "dense" ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, "lower_bounds": [], - "upper_bounds": [ + "metadata": {}, + "order": null, + "pointer": false, + "rank": 1, + "shape": [ "p_div" ], "source_shape": [ "1:p_div" ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} - }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x4", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "1:p_div" - ], - "lower_bounds": [ - "1" - ], "upper_bounds": [ "p_div" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x5", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x4", - "native_abi": null, - "native_symbol": null, - "native_scope": "all_exprs", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 1, - "shape": [ - "1:p_div" - ], + "allocatable": false, + "contiguous": false, "lower_bounds": [ "1" ], - "upper_bounds": [ - "p_div" - ], - "allocatable": false, + "optional": false, "pointer": false, + "rank": 1, + "shape": [ + "1:p_pow" + ], "target": false, - "contiguous": false, - "optional": false, + "upper_bounds": [ + "p_pow" + ], "value": false - } + }, + "native_abi": null, + "native_name": "x5", + "native_scope": "all_exprs", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "x5", "semantic_type": { - "name": "Int32", - "rank": 1, - "dtype": "Int32", - "shape": [ - "p_pow" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "p_pow" + "1:p_pow" ], - "lower_bounds": [], + "target": false, "upper_bounds": [ "p_pow" ], - "source_shape": [ - "1:p_pow" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x5", "native_abi": null, - "native_symbol": null, + "native_name": "x5", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "p_pow" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ - "1:p_pow" + "p_pow" ], - "lower_bounds": [ - "1" + "source_shape": [ + "1:p_pow" ], "upper_bounds": [ "p_pow" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x6", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x5", - "native_abi": null, - "native_symbol": null, - "native_scope": "all_exprs", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "0" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "1:p_pow" - ], - "lower_bounds": [ - "1" + "0:p_mix" ], + "target": false, "upper_bounds": [ - "p_pow" + "p_mix" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, "value": false - } + }, + "native_abi": null, + "native_name": "x6", + "native_scope": "all_exprs", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "x6", "semantic_type": { - "name": "Int32", - "rank": 1, - "dtype": "Int32", - "shape": [ - "p_mix + 1" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { - "rank": 1, - "shape": [ - "p_mix + 1" - ], + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, "lower_bounds": [ "0" ], + "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "0:p_mix" + ], + "target": false, "upper_bounds": [ "p_mix" ], - "source_shape": [ - "0:p_mix" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x6", "native_abi": null, - "native_symbol": null, + "native_name": "x6", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "0:p_mix" + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "p_mix + 1" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, "lower_bounds": [ "0" ], + "metadata": {}, + "order": null, + "pointer": false, + "rank": 1, + "shape": [ + "p_mix + 1" + ], + "source_shape": [ + "0:p_mix" + ], "upper_bounds": [ "p_mix" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x7", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x6", - "native_abi": null, - "native_symbol": null, - "native_scope": "all_exprs", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "0:p_mix" - ], - "lower_bounds": [ - "0" + "1:-(-a + b)" ], + "target": false, "upper_bounds": [ - "p_mix" + "-(-a + b)" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, "value": false - } + }, + "native_abi": null, + "native_name": "x7", + "native_scope": "all_exprs", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "x7", "semantic_type": { - "name": "Int32", - "rank": 1, - "dtype": "Int32", - "shape": [ - "a - b" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "a - b" + "1:-(-a + b)" ], - "lower_bounds": [], + "target": false, "upper_bounds": [ "-(-a + b)" ], - "source_shape": [ - "1:-(-a + b)" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x7", "native_abi": null, - "native_symbol": null, + "native_name": "x7", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "a - b" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ - "1:-(-a + b)" + "a - b" ], - "lower_bounds": [ - "1" + "source_shape": [ + "1:-(-a + b)" ], "upper_bounds": [ "-(-a + b)" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x8", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x7", - "native_abi": null, - "native_symbol": null, - "native_scope": "all_exprs", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 1, - "shape": [ - "1:-(-a + b)" - ], + "allocatable": false, + "contiguous": false, "lower_bounds": [ "1" ], - "upper_bounds": [ - "-(-a + b)" - ], - "allocatable": false, + "optional": false, "pointer": false, + "rank": 1, + "shape": [ + "1:(a+b)*(c+1)-1" + ], "target": false, - "contiguous": false, - "optional": false, + "upper_bounds": [ + "(a+b)*(c+1)-1" + ], "value": false - } + }, + "native_abi": null, + "native_name": "x8", + "native_scope": "all_exprs", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "x8", "semantic_type": { - "name": "Int32", - "rank": 1, - "dtype": "Int32", - "shape": [ - "(a + b) * (c + 1) - 1" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "(a + b) * (c + 1) - 1" + "1:(a+b)*(c+1)-1" ], - "lower_bounds": [], + "target": false, "upper_bounds": [ "(a+b)*(c+1)-1" ], - "source_shape": [ - "1:(a+b)*(c+1)-1" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x8", "native_abi": null, - "native_symbol": null, + "native_name": "x8", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "(a + b) * (c + 1) - 1" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ - "1:(a+b)*(c+1)-1" + "(a + b) * (c + 1) - 1" ], - "lower_bounds": [ - "1" + "source_shape": [ + "1:(a+b)*(c+1)-1" ], "upper_bounds": [ "(a+b)*(c+1)-1" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x9", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x8", - "native_abi": null, - "native_symbol": null, - "native_scope": "all_exprs", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 1, - "shape": [ - "1:(a+b)*(c+1)-1" - ], + "allocatable": false, + "contiguous": false, "lower_bounds": [ "1" ], - "upper_bounds": [ - "(a+b)*(c+1)-1" - ], - "allocatable": false, + "optional": false, "pointer": false, + "rank": 1, + "shape": [ + "1:(a-b)*(a-c)" + ], "target": false, - "contiguous": false, - "optional": false, + "upper_bounds": [ + "(a-b)*(a-c)" + ], "value": false - } + }, + "native_abi": null, + "native_name": "x9", + "native_scope": "all_exprs", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "x9", "semantic_type": { - "name": "Int32", - "rank": 1, - "dtype": "Int32", - "shape": [ - "(a - b) * (a - c)" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "(a - b) * (a - c)" + "1:(a-b)*(a-c)" ], - "lower_bounds": [], + "target": false, "upper_bounds": [ "(a-b)*(a-c)" ], - "source_shape": [ - "1:(a-b)*(a-c)" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x9", "native_abi": null, - "native_symbol": null, + "native_name": "x9", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "(a - b) * (a - c)" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ - "1:(a-b)*(a-c)" + "(a - b) * (a - c)" ], - "lower_bounds": [ - "1" + "source_shape": [ + "1:(a-b)*(a-c)" ], "upper_bounds": [ "(a-b)*(a-c)" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "x9", - "native_abi": null, - "native_symbol": null, - "native_scope": "all_exprs", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "1:(a-b)*(a-c)" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "(a-b)*(a-c)" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], - "projection": [ - { - "python_name": "x1", - "native_name": "x1", - "native_position": 0, - "python_position": 0, + "locals": [], + "metadata": {}, + "name": "all_exprs", + "native_name": "all_exprs", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "all_exprs", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "x1", + "native_position": 0, + "python_name": "x1", + "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x2", + "native_c_identity": null, "native_name": "x2", "native_position": 1, + "python_name": "x2", "python_position": 1, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x3", + "native_c_identity": null, "native_name": "x3", "native_position": 2, + "python_name": "x3", "python_position": 2, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x4", + "native_c_identity": null, "native_name": "x4", "native_position": 3, + "python_name": "x4", "python_position": 3, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x5", + "native_c_identity": null, "native_name": "x5", "native_position": 4, + "python_name": "x5", "python_position": 4, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x6", + "native_c_identity": null, "native_name": "x6", "native_position": 5, + "python_name": "x6", "python_position": 5, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x7", + "native_c_identity": null, "native_name": "x7", "native_position": 6, + "python_name": "x7", "python_position": 6, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x8", + "native_c_identity": null, "native_name": "x8", "native_position": 7, + "python_name": "x8", "python_position": 7, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x9", + "native_c_identity": null, "native_name": "x9", "native_position": 8, + "python_name": "x9", "python_position": 8, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "all_exprs", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" } ], - "prototypes": [], + "imports": [], + "metadata": {}, + "name": "expr_mod", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "expr_mod", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "module", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "overload_sets": [], - "classes": [], + "prototypes": [], + "reexports": [], "variables": [ { - "name": "a", - "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [ - { - "name": "Constant", - "arguments": [] - } - ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, - "metadata": {}, - "storage": null, - "origin": { - "source_language": "fortran", - "native_name": "a", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false, - "constant": true - } - } - }, - "visibility": "public", "default_value": "8", "metadata": { "fortran_initializer": "8" }, + "name": "a", "origin": { - "source_language": "fortran", - "native_name": "a", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "b", + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "a", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "b", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "a", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": "3", "metadata": { "fortran_initializer": "3" }, + "name": "b", "origin": { - "source_language": "fortran", - "native_name": "b", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "c", + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "b", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "c", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "b", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": "2", "metadata": { "fortran_initializer": "2" }, + "name": "c", "origin": { - "source_language": "fortran", - "native_name": "c", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "p_add", + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "c", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "p_add", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "c", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": "11", "metadata": { "fortran_initializer": "a + b" }, + "name": "p_add", "origin": { - "source_language": "fortran", - "native_name": "p_add", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "p_sub", + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_add", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "p_sub", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_add", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": "5", "metadata": { "fortran_initializer": "a - b" }, + "name": "p_sub", "origin": { - "source_language": "fortran", - "native_name": "p_sub", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "p_mul", + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_sub", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "p_mul", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_sub", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": "6", "metadata": { "fortran_initializer": "b * c" }, + "name": "p_mul", "origin": { - "source_language": "fortran", - "native_name": "p_mul", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "p_div", + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_mul", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "p_div", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_mul", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": "4", "metadata": { "fortran_initializer": "a / c" }, + "name": "p_div", "origin": { - "source_language": "fortran", - "native_name": "p_div", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "p_pow", + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_div", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "p_pow", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_div", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": "8", "metadata": { "fortran_initializer": "c ** b" }, + "name": "p_pow", "origin": { - "source_language": "fortran", - "native_name": "p_pow", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "p_mix", + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_pow", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "p_mix", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_pow", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": "21", "metadata": { "fortran_initializer": "(a + b) * c - 1" }, + "name": "p_mix", "origin": { - "source_language": "fortran", - "native_name": "p_mix", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_mix", + "native_scope": "expr_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "semantic_type": { + "coercions": [], + "constraints": [ + { + "arguments": [], + "name": "Constant" + } + ], + "dtype": "Int32", + "metadata": {}, + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "constant": true, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p_mix", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null + }, + "visibility": "public" } - ], - "imports": [], - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "expr_mod", - "native_abi": null, - "native_symbol": null, - "native_scope": "expr_mod", - "source_kind": "module", - "source_type": null, - "source_location": {}, - "metadata": {} - } + ] } ] } diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json index 468c95111..674c658a5 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json @@ -1,445 +1,447 @@ { "semantic_modules": [ { - "name": "dims_mod", + "classes": [], + "exported_names": null, "functions": [ { - "name": "use_expr", - "native_name": "use_expr", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "x", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "0" + ], + "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "0:n1-1" + ], + "target": false, + "upper_bounds": [ + "n1-1" + ], + "value": false + }, + "native_abi": null, + "native_name": "x", + "native_scope": "use_expr", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 1, - "dtype": "Int32", - "shape": [ - "n1" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { - "rank": 1, - "shape": [ - "n1" - ], + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, "lower_bounds": [ "0" ], - "upper_bounds": [ - "n1-1" - ], - "source_shape": [ + "optional": false, + "pointer": false, + "rank": 1, + "shape": [ "0:n1-1" ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" + "target": false, + "upper_bounds": [ + "n1-1" ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x", "native_abi": null, - "native_symbol": null, + "native_name": "x", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "0:n1-1" + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "n1" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, "lower_bounds": [ "0" ], + "metadata": {}, + "order": null, + "pointer": false, + "rank": 1, + "shape": [ + "n1" + ], + "source_shape": [ + "0:n1-1" + ], "upper_bounds": [ "n1-1" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "y", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x", - "native_abi": null, - "native_symbol": null, - "native_scope": "use_expr", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "0:n1-1" - ], - "lower_bounds": [ - "0" + "1:n0*2" ], + "target": false, "upper_bounds": [ - "n1-1" + "n0*2" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, "value": false - } + }, + "native_abi": null, + "native_name": "y", + "native_scope": "use_expr", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" }, - "optional": false - }, - { - "name": "y", "semantic_type": { - "name": "Float32", - "rank": 1, - "dtype": "Float32", - "shape": [ - "n0 * 2" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Float32", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Float32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "n0 * 2" + "1:n0*2" ], - "lower_bounds": [], + "target": false, "upper_bounds": [ "n0*2" ], - "source_shape": [ - "1:n0*2" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "y", "native_abi": null, - "native_symbol": null, + "native_name": "y", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real", + "source_language": "fortran", "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "1:n0*2" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "n0*2" - ], + "source_type": "real" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "n0 * 2" + ], + "storage": { + "array": { "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "y", - "native_abi": null, - "native_symbol": null, - "native_scope": "use_expr", - "source_kind": "argument", - "source_type": "real", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "1:n0*2" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "n0*2" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, + "rank": 1, + "shape": [ + "n0 * 2" + ], + "source_shape": [ + "1:n0*2" + ], + "upper_bounds": [ + "n0*2" + ] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], + "locals": [], + "metadata": {}, + "name": "use_expr", + "native_name": "use_expr", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "use_expr", + "native_scope": "dims_mod", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "projection": [ { - "python_name": "x", + "native_c_identity": null, "native_name": "x", "native_position": 0, + "python_name": "x", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "y", + "native_c_identity": null, "native_name": "y", "native_position": 1, + "python_name": "y", "python_position": 1, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "use_expr", - "native_abi": null, - "native_symbol": null, - "native_scope": "dims_mod", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" } ], - "prototypes": [], + "imports": [], + "metadata": {}, + "name": "dims_mod", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "dims_mod", + "native_scope": "dims_mod", + "native_symbol": null, + "source_kind": "module", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "overload_sets": [], - "classes": [], + "prototypes": [], + "reexports": [], "variables": [ { + "default_value": "4", + "metadata": { + "fortran_initializer": "4" + }, "name": "n0", + "origin": { + "metadata": { + "allocatable": false, + "constant": true, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "n0", + "native_scope": "dims_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "n0", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "n0", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", - "default_value": "4", + "visibility": "public" + }, + { + "default_value": "6", "metadata": { - "fortran_initializer": "4" + "fortran_initializer": "n0 + 2" }, + "name": "n1", "origin": { - "source_language": "fortran", - "native_name": "n0", - "native_abi": null, - "native_symbol": null, - "native_scope": "dims_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "n1", + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "n1", + "native_scope": "dims_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "n1", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - "visibility": "public", - "default_value": "6", - "metadata": { - "fortran_initializer": "n0 + 2" + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "n1", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "origin": { - "source_language": "fortran", - "native_name": "n1", - "native_abi": null, - "native_symbol": null, - "native_scope": "dims_mod", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false, - "constant": true - } - } + "visibility": "public" } - ], - "imports": [], - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "dims_mod", - "native_abi": null, - "native_symbol": null, - "native_scope": "dims_mod", - "source_kind": "module", - "source_type": null, - "source_location": {}, - "metadata": {} - } + ] } ] } diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json index f87a1fca7..057bba4cc 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json @@ -1,366 +1,368 @@ { "semantic_modules": [ { - "name": "particle_mod", - "functions": [ + "classes": [ { - "name": "touch", - "native_name": "touch", - "arguments": [ + "base_classes": [], + "contracts": [], + "destructors": [], + "fields": [ { - "name": "p", - "semantic_type": { - "name": "particle", - "rank": 0, - "dtype": "particle", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": false, - "mutable": true, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "p", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "type(particle)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", "default_value": null, "metadata": {}, + "name": "id", "origin": { - "source_language": "fortran", - "native_name": "p", - "native_abi": null, - "native_symbol": null, - "native_scope": "touch", - "source_kind": "argument", - "source_type": "type(particle)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "id", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - } - ], - "return_type": null, - "locals": [], - "contracts": [], - "projection": [ - { - "python_name": "p", - "native_name": "p", - "native_position": 0, - "python_position": 0, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "touch", - "native_abi": null, - "native_symbol": null, - "native_scope": "particle_mod", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } - } - ], - "prototypes": [], - "overload_sets": [], - "classes": [ - { - "name": "particle", - "native_name": "particle", - "fields": [ - { - "name": "id", "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "id", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "id", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x", "origin": { - "source_language": "fortran", - "native_name": "id", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + "1" + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "target": false, + "upper_bounds": [ + "3" + ], "value": false - } - } - }, - { - "name": "x", + }, + "native_abi": null, + "native_name": "x", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "3" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": false, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ "3" ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ + "target": false, + "upper_bounds": [ "3" ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x", "native_abi": null, - "native_symbol": null, + "native_name": "x", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "3" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ "3" ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ + "source_shape": [ "3" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "x", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "3" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "visibility": "public" } ], - "methods": [], - "destructors": [], - "overload_sets": [], - "base_classes": [], - "contracts": [], "metadata": { - "fortran_type_attributes": [], - "fortran_component_order": [ - "id", - "x" - ], "fortran_component_facts": [ { - "name": "id", - "source_type": "integer", + "allocatable": false, "kind": "", + "name": "id", + "pointer": false, "rank": 0, "shape": [], - "allocatable": false, - "pointer": false, + "source_type": "integer", "target": false }, { - "name": "x", - "source_type": "real(kind=8)", + "allocatable": false, "kind": "8", + "name": "x", + "pointer": false, "rank": 1, "shape": [ "3" ], - "allocatable": false, - "pointer": false, + "source_type": "real(kind=8)", "target": false } ], + "fortran_component_order": [ + "id", + "x" + ], + "fortran_direct_layout": false, "fortran_layout_policy": "accessors", - "fortran_direct_layout": false + "fortran_type_attributes": [] }, - "visibility": "public", + "methods": [], + "name": "particle", + "native_name": "particle", "origin": { - "source_language": "fortran", - "native_name": "particle", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "particle", "native_scope": "particle_mod", + "native_symbol": null, "source_kind": "derived_type", - "source_type": null, + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "overload_sets": [], + "visibility": "public" + } + ], + "exported_names": null, + "functions": [ + { + "arguments": [ + { + "default_value": null, + "metadata": {}, + "name": "p", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p", + "native_scope": "touch", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "type(particle)" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "particle", + "metadata": {}, + "name": "particle", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "type(particle)" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": false + } + }, + "visibility": "public" + } + ], + "contracts": [], + "locals": [], + "metadata": {}, + "name": "touch", + "native_name": "touch", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "touch", + "native_scope": "particle_mod", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "p", + "native_position": 0, + "python_name": "p", + "python_position": 0, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + } + ], + "return_type": null, + "visibility": "public" } ], - "variables": [], "imports": [], "metadata": {}, + "name": "particle_mod", "origin": { - "source_language": "fortran", - "native_name": "particle_mod", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "particle_mod", "native_scope": "particle_mod", + "native_symbol": null, "source_kind": "module", - "source_type": null, + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "overload_sets": [], + "prototypes": [], + "reexports": [], + "variables": [] } ] } diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json index afd9fb1a0..7c00c8bc9 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json @@ -1,484 +1,486 @@ { "semantic_modules": [ { - "name": "mesh_mod", - "functions": [], - "prototypes": [], - "overload_sets": [], "classes": [ { - "name": "node", - "native_name": "node", + "base_classes": [], + "contracts": [], + "destructors": [], "fields": [ { + "default_value": null, + "metadata": {}, "name": "id", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "id", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "id", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "id", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "xyz", "origin": { - "source_language": "fortran", - "native_name": "id", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + "1" + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "target": false, + "upper_bounds": [ + "3" + ], "value": false - } - } - }, - { - "name": "xyz", + }, + "native_abi": null, + "native_name": "xyz", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "3" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": false, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ "3" ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ + "target": false, + "upper_bounds": [ "3" ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "xyz", "native_abi": null, - "native_symbol": null, + "native_name": "xyz", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "3" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ "3" ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ + "source_shape": [ "3" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "xyz", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "3" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "visibility": "public" } ], - "methods": [], - "destructors": [], - "overload_sets": [], - "base_classes": [], - "contracts": [], "metadata": { - "fortran_type_attributes": [], - "fortran_component_order": [ - "id", - "xyz" - ], "fortran_component_facts": [ { - "name": "id", - "source_type": "integer", + "allocatable": false, "kind": "", + "name": "id", + "pointer": false, "rank": 0, "shape": [], - "allocatable": false, - "pointer": false, + "source_type": "integer", "target": false }, { - "name": "xyz", - "source_type": "real(kind=8)", + "allocatable": false, "kind": "8", + "name": "xyz", + "pointer": false, "rank": 1, "shape": [ "3" ], - "allocatable": false, - "pointer": false, + "source_type": "real(kind=8)", "target": false } ], + "fortran_component_order": [ + "id", + "xyz" + ], + "fortran_direct_layout": false, "fortran_layout_policy": "accessors", - "fortran_direct_layout": false + "fortran_type_attributes": [] }, - "visibility": "public", + "methods": [], + "name": "node", + "native_name": "node", "origin": { - "source_language": "fortran", - "native_name": "node", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "node", "native_scope": "mesh_mod", + "native_symbol": null, "source_kind": "derived_type", - "source_type": null, + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "overload_sets": [], + "visibility": "public" }, { - "name": "mesh", - "native_name": "mesh", + "base_classes": [], + "contracts": [], + "destructors": [], "fields": [ { + "default_value": null, + "metadata": {}, "name": "nnodes", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "nnodes", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "nnodes", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "nnodes", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "nodes", "origin": { - "source_language": "fortran", - "native_name": "nnodes", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, + "allocatable": true, "contiguous": false, + "lower_bounds": [ + null + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + ":" + ], + "target": false, + "upper_bounds": [ + null + ], "value": false - } - } - }, - { - "name": "nodes", + }, + "native_abi": null, + "native_name": "nodes", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "type(node)" + }, "semantic_type": { - "name": "node", - "rank": 1, - "dtype": "node", - "shape": [ - ":" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "node", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": false, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "node", + "origin": { + "metadata": { + "allocatable": true, + "contiguous": false, + "lower_bounds": [ + null + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ ":" ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ - ":" - ], - "category": "deferred_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" + "target": false, + "upper_bounds": [ + null ], - "contiguous": true, - "allocatable": true, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "nodes", "native_abi": null, - "native_symbol": null, + "native_name": "nodes", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "type(node)", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "type(node)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + ":" + ], + "storage": { + "array": { + "allocatable": true, + "axes": [ + "dense" + ], + "category": "deferred_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ ":" ], - "lower_bounds": [ - null - ], - "upper_bounds": [ - null + "source_shape": [ + ":" ], - "allocatable": true, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "nodes", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "type(node)", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - ":" - ], - "lower_bounds": [ - null - ], - "upper_bounds": [ - null - ], - "allocatable": true, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "visibility": "public" } ], - "methods": [], - "destructors": [], - "overload_sets": [], - "base_classes": [], - "contracts": [], "metadata": { - "fortran_type_attributes": [], - "fortran_component_order": [ - "nnodes", - "nodes" - ], "fortran_component_facts": [ { - "name": "nnodes", - "source_type": "integer", + "allocatable": false, "kind": "", + "name": "nnodes", + "pointer": false, "rank": 0, "shape": [], - "allocatable": false, - "pointer": false, + "source_type": "integer", "target": false }, { - "name": "nodes", - "source_type": "type(node)", + "allocatable": true, "kind": "node", + "name": "nodes", + "pointer": false, "rank": 1, "shape": [ ":" ], - "allocatable": true, - "pointer": false, + "source_type": "type(node)", "target": false } ], + "fortran_component_order": [ + "nnodes", + "nodes" + ], + "fortran_direct_layout": false, "fortran_layout_policy": "accessors", - "fortran_direct_layout": false + "fortran_type_attributes": [] }, - "visibility": "public", + "methods": [], + "name": "mesh", + "native_name": "mesh", "origin": { - "source_language": "fortran", - "native_name": "mesh", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "mesh", "native_scope": "mesh_mod", + "native_symbol": null, "source_kind": "derived_type", - "source_type": null, + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "overload_sets": [], + "visibility": "public" } ], - "variables": [], + "exported_names": null, + "functions": [], "imports": [], "metadata": {}, + "name": "mesh_mod", "origin": { - "source_language": "fortran", - "native_name": "mesh_mod", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "mesh_mod", "native_scope": "mesh_mod", + "native_symbol": null, "source_kind": "module", - "source_type": null, + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "overload_sets": [], + "prototypes": [], + "reexports": [], + "variables": [] } ] } diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json index b6dcc3b14..0520ccd5e 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json @@ -1,2572 +1,2574 @@ { "semantic_modules": [ { - "name": "modern_math_physics", - "functions": [ + "classes": [ { - "name": "init_particle", - "native_name": "init_particle", - "arguments": [ + "base_classes": [], + "contracts": [], + "destructors": [], + "fields": [ { - "name": "p", - "semantic_type": { - "name": "particle", - "rank": 0, - "dtype": "particle", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": false, - "mutable": true, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "p", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "type(particle)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", "default_value": null, "metadata": {}, + "name": "id", "origin": { - "source_language": "fortran", - "native_name": "p", - "native_abi": null, - "native_symbol": null, - "native_scope": "init_particle", - "source_kind": "argument", - "source_type": "type(particle)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "id", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "pid", "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "pid", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "id", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "mass", "origin": { - "source_language": "fortran", - "native_name": "pid", - "native_abi": null, - "native_symbol": null, - "native_scope": "init_particle", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "mass", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - }, - { - "name": "mass", "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Float64", "origin": { - "source_language": "fortran", - "native_name": "mass", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "mass", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "position", "origin": { - "source_language": "fortran", - "native_name": "mass", - "native_abi": null, - "native_symbol": null, - "native_scope": "init_particle", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + "1" + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "target": false, + "upper_bounds": [ + "3" + ], "value": false - } + }, + "native_abi": null, + "native_name": "position", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - }, - { - "name": "x", "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Float64", "origin": { - "source_language": "fortran", - "native_name": "x", + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "target": false, + "upper_bounds": [ + "3" + ], + "value": false + }, "native_abi": null, - "native_symbol": null, + "native_name": "position", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "3" + ], + "storage": { + "array": { "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "rank": 1, + "shape": [ + "3" + ], + "source_shape": [ + "3" + ], + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + } + ], + "metadata": { + "fortran_component_facts": [ + { + "allocatable": false, + "kind": "", + "name": "id", + "pointer": false, + "rank": 0, + "shape": [], + "source_type": "integer", + "target": false + }, + { + "allocatable": false, + "kind": "8", + "name": "mass", + "pointer": false, + "rank": 0, + "shape": [], + "source_type": "real(kind=8)", + "target": false + }, + { + "allocatable": false, + "kind": "8", + "name": "position", + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "source_type": "real(kind=8)", + "target": false + } + ], + "fortran_component_order": [ + "id", + "mass", + "position" + ], + "fortran_direct_layout": false, + "fortran_layout_policy": "accessors", + "fortran_type_attributes": [] + }, + "methods": [], + "name": "particle", + "native_name": "particle", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "particle", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "derived_type", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "overload_sets": [], + "visibility": "public" + }, + { + "base_classes": [], + "contracts": [], + "destructors": [], + "fields": [ + { "default_value": null, "metadata": {}, + "name": "values", "origin": { - "source_language": "fortran", - "native_name": "x", - "native_abi": null, - "native_symbol": null, - "native_scope": "init_particle", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + "1" + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "target": false, + "upper_bounds": [ + "3" + ], "value": false - } + }, + "native_abi": null, + "native_name": "values", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - }, - { - "name": "y", "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Float64", "origin": { - "source_language": "fortran", - "native_name": "y", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + "1" + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "target": false, + "upper_bounds": [ + "3" + ], "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "y", - "native_abi": null, - "native_symbol": null, - "native_scope": "init_particle", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - }, - "optional": false - }, - { - "name": "z", - "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "z", + }, "native_abi": null, - "native_symbol": null, + "native_name": "values", "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "z", - "native_abi": null, - "native_symbol": null, - "native_scope": "init_particle", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - }, - "optional": false - } - ], - "return_type": null, - "locals": [], - "contracts": [], - "projection": [ - { - "python_name": "p", - "native_name": "p", - "native_position": 0, - "python_position": 0, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - }, - { - "python_name": "pid", - "native_name": "pid", - "native_position": 1, - "python_position": 1, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - }, - { - "python_name": "mass", - "native_name": "mass", - "native_position": 2, - "python_position": 2, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - }, - { - "python_name": "x", - "native_name": "x", - "native_position": 3, - "python_position": 3, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - }, - { - "python_name": "y", - "native_name": "y", - "native_position": 4, - "python_position": 4, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - }, - { - "python_name": "z", - "native_name": "z", - "native_position": 5, - "python_position": 5, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "init_particle", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } - }, - { - "name": "kinetic_energy", - "native_name": "kinetic_energy", - "arguments": [ - { - "name": "p", - "semantic_type": { - "name": "particle", - "rank": 0, - "dtype": "particle", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "p", - "native_abi": null, "native_symbol": null, - "native_scope": null, "source_kind": "variable", - "source_type": "type(particle)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "p", - "native_abi": null, - "native_symbol": null, - "native_scope": "kinetic_energy", - "source_kind": "argument", - "source_type": "type(particle)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - }, - "optional": false - }, - { - "name": "vx", - "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { "source_language": "fortran", - "native_name": "vx", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "vx", - "native_abi": null, - "native_symbol": null, - "native_scope": "kinetic_energy", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - }, - "optional": false - }, - { - "name": "vy", - "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], - "coercions": [], + "source_type": "real(kind=8)" + }, "ownership": { - "ownership": "borrowed", + "aliasing": true, "mutable": false, - "aliasing": true + "ownership": "borrowed" }, - "metadata": {}, + "rank": 1, + "shape": [ + "3" + ], "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "vy", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, + "metadata": {}, + "order": null, "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "rank": 1, + "shape": [ + "3" + ], + "source_shape": [ + "3" + ], + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + } + ], + "metadata": { + "fortran_component_facts": [ + { + "allocatable": false, + "kind": "8", + "name": "values", + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "source_type": "real(kind=8)", + "target": false + } + ], + "fortran_component_order": [ + "values" + ], + "fortran_direct_layout": false, + "fortran_layout_policy": "accessors", + "fortran_type_attributes": [] + }, + "methods": [], + "name": "vector3", + "native_name": "vector3", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "vector3", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "derived_type", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "overload_sets": [], + "visibility": "public" + }, + { + "base_classes": [], + "contracts": [], + "destructors": [], + "fields": [ + { "default_value": null, "metadata": {}, + "name": "code", "origin": { - "source_language": "fortran", - "native_name": "vy", - "native_abi": null, - "native_symbol": null, - "native_scope": "kinetic_energy", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "code", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" }, - "optional": false - }, - { - "name": "vz", "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "vz", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "vz", - "native_abi": null, - "native_symbol": null, - "native_scope": "kinetic_energy", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + }, + "native_abi": null, + "native_name": "code", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "optional": false + "visibility": "public" } ], - "return_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, - "metadata": {}, - "storage": null, - "origin": { - "source_language": "fortran", - "native_name": "e", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], + "metadata": { + "fortran_component_facts": [ + { "allocatable": false, + "kind": "", + "name": "code", "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "locals": [], - "contracts": [], - "projection": [ - { - "python_name": "p", - "native_name": "p", - "native_position": 0, - "python_position": 0, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - }, - { - "python_name": "vx", - "native_name": "vx", - "native_position": 1, - "python_position": 1, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - }, - { - "python_name": "vy", - "native_name": "vy", - "native_position": 2, - "python_position": 2, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - }, - { - "python_name": "vz", - "native_name": "vz", - "native_position": 3, - "python_position": 3, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], - "metadata": {}, - "visibility": "public", + "rank": 0, + "shape": [], + "source_type": "integer", + "target": false + } + ], + "fortran_component_order": [ + "code" + ], + "fortran_direct_layout": false, + "fortran_layout_policy": "accessors", + "fortran_type_attributes": [] + }, + "methods": [], + "name": "hidden_state", + "native_name": "hidden_state", "origin": { - "source_language": "fortran", - "native_name": "kinetic_energy", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "hidden_state", "native_scope": "modern_math_physics", - "source_kind": "function", - "source_type": null, + "native_symbol": null, + "source_kind": "derived_type", + "source_language": "fortran", "source_location": {}, - "metadata": {} - } - }, + "source_type": null + }, + "overload_sets": [], + "visibility": "private" + } + ], + "exported_names": null, + "functions": [ { - "name": "scale_vector", - "native_name": "scale_vector", "arguments": [ { - "name": "v", + "default_value": null, + "metadata": {}, + "name": "p", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p", + "native_scope": "init_particle", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "type(particle)" + }, "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "::Strided" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "particle", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { - "rank": 1, - "shape": [ - "::Strided" - ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ - ":" - ], - "category": "assumed_shape", - "order": null, - "copy_order": null, - "axes": [ - "strided" - ], - "contiguous": false, + "name": "particle", + "origin": { + "metadata": { "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, "pointer": false, - "metadata": {} + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "v", "native_abi": null, - "native_symbol": null, + "native_name": "p", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, + "source_type": "type(particle)" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": false + } + }, + "visibility": "public" + }, + { + "default_value": null, + "metadata": {}, + "name": "pid", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "pid", + "native_scope": "init_particle", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Int32", + "metadata": {}, + "name": "Int32", + "origin": { "metadata": { - "rank": 1, - "shape": [ - ":" - ], - "lower_bounds": [ - null - ], - "upper_bounds": [ - null - ], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "pid", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "mass", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "v", - "native_abi": null, - "native_symbol": null, - "native_scope": "scale_vector", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 1, - "shape": [ - ":" - ], - "lower_bounds": [ - null - ], - "upper_bounds": [ - null - ], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "mass", + "native_scope": "init_particle", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - }, - { - "name": "alpha", "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], "coercions": [], + "constraints": [], + "dtype": "Float64", + "metadata": {}, + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "mass", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, "ownership": { - "ownership": "borrowed", + "aliasing": true, "mutable": false, - "aliasing": true + "ownership": "borrowed" }, - "metadata": {}, + "rank": 0, + "shape": [], "storage": { + "array": null, + "calling_convention": null, "kind": "reference", - "read_only": true, + "metadata": {}, "mutable": false, - "pointer_depth": 1, "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "alpha", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "pointer_depth": 1, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "alpha", - "native_abi": null, - "native_symbol": null, - "native_scope": "scale_vector", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "x", + "native_scope": "init_particle", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - } - ], - "return_type": null, - "locals": [], - "contracts": [], - "projection": [ - { - "python_name": "v", - "native_name": "v", - "native_position": 0, - "python_position": 0, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - }, - { - "python_name": "alpha", - "native_name": "alpha", - "native_position": 1, - "python_position": 1, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "scale_vector", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } - }, - { - "name": "dot3", - "native_name": "dot3", - "arguments": [ - { - "name": "a", "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "3" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": true, - "mutable": false, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ - "3" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, + "name": "Float64", + "origin": { + "metadata": { "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, "pointer": false, - "metadata": {} + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "a", "native_abi": null, - "native_symbol": null, + "native_name": "x", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "3" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "y", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "a", - "native_abi": null, - "native_symbol": null, - "native_scope": "dot3", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "3" - ], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "y", + "native_scope": "init_particle", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - }, - { - "name": "b", "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "3" - ], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "coercions": [], + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": true, - "mutable": false, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ - "3" - ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, + "name": "Float64", + "origin": { + "metadata": { "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, "pointer": false, - "metadata": {} + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "b", "native_abi": null, - "native_symbol": null, + "native_name": "y", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "3" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "z", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "b", - "native_abi": null, - "native_symbol": null, - "native_scope": "dot3", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "3" - ], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false + }, + "native_abi": null, + "native_name": "z", + "native_scope": "init_particle", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Float64", + "metadata": {}, + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "z", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "optional": false + "visibility": "public" } ], - "return_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "contracts": [], + "locals": [], + "metadata": {}, + "name": "init_particle", + "native_name": "init_particle", + "origin": { "metadata": {}, - "storage": null, - "origin": { - "source_language": "fortran", - "native_name": "s", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "native_abi": null, + "native_name": "init_particle", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null }, - "locals": [], - "contracts": [], "projection": [ { - "python_name": "a", - "native_name": "a", + "native_c_identity": null, + "native_name": "p", "native_position": 0, + "python_name": "p", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "b", - "native_name": "b", + "native_c_identity": null, + "native_name": "pid", "native_position": 1, + "python_name": "pid", "python_position": 1, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" + }, + { + "native_c_identity": null, + "native_name": "mass", + "native_position": 2, + "python_name": "mass", + "python_position": 2, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + }, + { + "native_c_identity": null, + "native_name": "x", + "native_position": 3, + "python_name": "x", + "python_position": 3, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + }, + { + "native_c_identity": null, + "native_name": "y", + "native_position": 4, + "python_name": "y", + "python_position": 4, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + }, + { + "native_c_identity": null, + "native_name": "z", + "native_position": 5, + "python_name": "z", + "python_position": 5, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "dot3", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "function", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" }, - { - "name": "fill_identity3", - "native_name": "fill_identity3", + { "arguments": [ { - "name": "a", + "default_value": null, + "metadata": {}, + "name": "p", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p", + "native_scope": "kinetic_energy", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "type(particle)" + }, "semantic_type": { - "name": "Float64", - "rank": 2, - "dtype": "Float64", - "shape": [ - "3", - "3" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "particle", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { - "rank": 2, - "shape": [ - "3", - "3" - ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ - "3", - "3" - ], - "category": "explicit_shape", - "order": "ORDER_F", - "copy_order": null, - "axes": [ - "dense", - "dense" - ], - "contiguous": true, + "name": "particle", + "origin": { + "metadata": { "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, "pointer": false, - "metadata": {} + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "a", "native_abi": null, - "native_symbol": null, + "native_name": "p", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, + "source_type": "type(particle)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true + } + }, + "visibility": "public" + }, + { + "default_value": null, + "metadata": {}, + "name": "vx", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "vx", + "native_scope": "kinetic_energy", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Float64", + "metadata": {}, + "name": "Float64", + "origin": { "metadata": { - "rank": 2, - "shape": [ - "3", - "3" - ], - "lower_bounds": [ - "1", - "1" - ], - "upper_bounds": [ - "3", - "3" - ], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "vx", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "vy", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "a", - "native_abi": null, - "native_symbol": null, - "native_scope": "fill_identity3", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 2, - "shape": [ - "3", - "3" - ], - "lower_bounds": [ - "1", - "1" - ], - "upper_bounds": [ - "3", - "3" - ], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "vy", + "native_scope": "kinetic_energy", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - } - ], - "return_type": null, - "locals": [], - "contracts": [], - "projection": [ - { - "python_name": "a", - "native_name": "a", - "native_position": 0, - "python_position": 0, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "fill_identity3", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } - }, - { - "name": "normalize_particle", - "native_name": "normalize_particle", - "arguments": [ - { - "name": "p", "semantic_type": { - "name": "particle", - "rank": 0, - "dtype": "particle", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": false, - "mutable": true, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Float64", "origin": { - "source_language": "fortran", - "native_name": "p", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "type(particle)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "vy", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "vz", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "p", - "native_abi": null, - "native_symbol": null, - "native_scope": "normalize_particle", - "source_kind": "argument", - "source_type": "type(particle)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false + }, + "native_abi": null, + "native_name": "vz", + "native_scope": "kinetic_energy", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Float64", + "metadata": {}, + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "vz", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], + "locals": [], + "metadata": {}, + "name": "kinetic_energy", + "native_name": "kinetic_energy", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "kinetic_energy", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "function", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "projection": [ { - "python_name": "p", + "native_c_identity": null, "native_name": "p", "native_position": 0, + "python_name": "p", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" + }, + { + "native_c_identity": null, + "native_name": "vx", + "native_position": 1, + "python_name": "vx", + "python_position": 1, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + }, + { + "native_c_identity": null, + "native_name": "vy", + "native_position": 2, + "python_name": "vy", + "python_position": 2, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + }, + { + "native_c_identity": null, + "native_name": "vz", + "native_position": 3, + "python_name": "vz", + "python_position": 3, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "normalize_particle", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } - }, - { - "name": "hidden_proc", - "native_name": "hidden_proc", - "arguments": [ - { - "name": "x", - "semantic_type": { - "name": "Int32", + "return_type": { + "coercions": [], + "constraints": [], + "dtype": "Float64", + "metadata": {}, + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, "rank": 0, - "dtype": "Int32", "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "target": false, + "upper_bounds": [], + "value": false }, - "visibility": "public", + "native_abi": null, + "native_name": "e", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null + }, + "visibility": "public" + }, + { + "arguments": [ + { "default_value": null, "metadata": {}, + "name": "v", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "x", - "native_abi": null, - "native_symbol": null, - "native_scope": "hidden_proc", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + null + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + ":" + ], + "target": false, + "upper_bounds": [ + null + ], "value": false - } + }, + "native_abi": null, + "native_name": "v", + "native_scope": "scale_vector", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - } - ], - "return_type": null, - "locals": [], - "contracts": [], - "projection": [ - { - "python_name": "x", - "native_name": "x", - "native_position": 0, - "python_position": 0, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], - "metadata": {}, - "visibility": "private", - "origin": { - "source_language": "fortran", - "native_name": "hidden_proc", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } - } - ], - "prototypes": [], - "overload_sets": [], - "classes": [ - { - "name": "particle", - "native_name": "particle", - "fields": [ - { - "name": "id", "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": null, + "name": "Float64", "origin": { - "source_language": "fortran", - "native_name": "id", + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + null + ], + "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + ":" + ], + "target": false, + "upper_bounds": [ + null + ], + "value": false + }, "native_abi": null, - "native_symbol": null, + "native_name": "v", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "integer", + "source_language": "fortran", "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "::" + ], + "storage": { + "array": { "allocatable": false, - "pointer": false, - "target": false, + "axes": [ + "strided" + ], + "category": "assumed_shape", "contiguous": false, - "optional": false, - "value": false - } + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, + "rank": 1, + "shape": [ + "::" + ], + "source_shape": [ + ":" + ], + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "alpha", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "id", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - { - "name": "mass", + }, + "native_abi": null, + "native_name": "alpha", + "native_scope": "scale_vector", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": null, + "name": "Float64", "origin": { - "source_language": "fortran", - "native_name": "mass", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "alpha", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + } + ], + "contracts": [], + "locals": [], + "metadata": {}, + "name": "scale_vector", + "native_name": "scale_vector", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "scale_vector", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "v", + "native_position": 0, + "python_name": "v", + "python_position": 0, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + }, + { + "native_c_identity": null, + "native_name": "alpha", + "native_position": 1, + "python_name": "alpha", + "python_position": 1, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + } + ], + "return_type": null, + "visibility": "public" + }, + { + "arguments": [ + { "default_value": null, "metadata": {}, + "name": "a", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "mass", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + "1" + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "target": false, + "upper_bounds": [ + "3" + ], "value": false - } - } - }, - { - "name": "position", + }, + "native_abi": null, + "native_name": "a", + "native_scope": "dot3", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "3" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": false, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ "3" ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ + "target": false, + "upper_bounds": [ "3" ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "position", "native_abi": null, - "native_symbol": null, + "native_name": "a", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "3" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ "3" ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ + "source_shape": [ "3" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "b", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "position", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ "3" ], - "lower_bounds": [ - "1" - ], + "target": false, "upper_bounds": [ "3" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, "value": false + }, + "native_abi": null, + "native_name": "b", + "native_scope": "dot3", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Float64", + "metadata": {}, + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "target": false, + "upper_bounds": [ + "3" + ], + "value": false + }, + "native_abi": null, + "native_name": "b", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "3" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "source_shape": [ + "3" + ], + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": true } - } + }, + "visibility": "public" } ], - "methods": [], - "destructors": [], - "overload_sets": [], - "base_classes": [], "contracts": [], - "metadata": { - "fortran_type_attributes": [], - "fortran_component_order": [ - "id", - "mass", - "position" - ], - "fortran_component_facts": [ - { - "name": "id", - "source_type": "integer", - "kind": "", - "rank": 0, - "shape": [], + "locals": [], + "metadata": {}, + "name": "dot3", + "native_name": "dot3", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "dot3", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "function", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "a", + "native_position": 0, + "python_name": "a", + "python_position": 0, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + }, + { + "native_c_identity": null, + "native_name": "b", + "native_position": 1, + "python_name": "b", + "python_position": 1, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + } + ], + "return_type": { + "coercions": [], + "constraints": [], + "dtype": "Float64", + "metadata": {}, + "name": "Float64", + "origin": { + "metadata": { "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, "pointer": false, - "target": false - }, - { - "name": "mass", - "source_type": "real(kind=8)", - "kind": "8", "rank": 0, "shape": [], - "allocatable": false, - "pointer": false, - "target": false + "target": false, + "upper_bounds": [], + "value": false }, - { - "name": "position", - "source_type": "real(kind=8)", - "kind": "8", - "rank": 1, - "shape": [ - "3" - ], - "allocatable": false, - "pointer": false, - "target": false - } - ], - "fortran_layout_policy": "accessors", - "fortran_direct_layout": false + "native_abi": null, + "native_name": "s", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "particle", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "derived_type", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "visibility": "public" }, { - "name": "vector3", - "native_name": "vector3", - "fields": [ + "arguments": [ { - "name": "values", + "default_value": null, + "metadata": {}, + "name": "a", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1", + "1" + ], + "optional": false, + "pointer": false, + "rank": 2, + "shape": [ + "3", + "3" + ], + "target": false, + "upper_bounds": [ + "3", + "3" + ], + "value": false + }, + "native_abi": null, + "native_name": "a", + "native_scope": "fill_identity3", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "3" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": false, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { - "rank": 1, + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1", + "1" + ], + "optional": false, + "pointer": false, + "rank": 2, "shape": [ + "3", "3" ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ + "target": false, + "upper_bounds": [ + "3", "3" ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "values", "native_abi": null, - "native_symbol": null, + "native_name": "a", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { - "rank": 1, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 2, + "shape": [ + "3", + "3" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense", + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": "ORDER_F", + "pointer": false, + "rank": 2, "shape": [ + "3", "3" ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ + "source_shape": [ + "3", "3" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "values", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "3" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "visibility": "public" } ], - "methods": [], - "destructors": [], - "overload_sets": [], - "base_classes": [], "contracts": [], - "metadata": { - "fortran_type_attributes": [], - "fortran_component_order": [ - "values" - ], - "fortran_component_facts": [ - { - "name": "values", - "source_type": "real(kind=8)", - "kind": "8", - "rank": 1, - "shape": [ - "3" - ], - "allocatable": false, - "pointer": false, - "target": false - } - ], - "fortran_layout_policy": "accessors", - "fortran_direct_layout": false - }, - "visibility": "public", + "locals": [], + "metadata": {}, + "name": "fill_identity3", + "native_name": "fill_identity3", "origin": { - "source_language": "fortran", - "native_name": "vector3", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "fill_identity3", "native_scope": "modern_math_physics", - "source_kind": "derived_type", - "source_type": null, + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "a", + "native_position": 0, + "python_name": "a", + "python_position": 0, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + } + ], + "return_type": null, + "visibility": "public" }, { - "name": "hidden_state", - "native_name": "hidden_state", - "fields": [ + "arguments": [ { - "name": "code", + "default_value": null, + "metadata": {}, + "name": "p", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "p", + "native_scope": "normalize_particle", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "type(particle)" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "particle", "metadata": {}, - "storage": null, + "name": "particle", "origin": { - "source_language": "fortran", - "native_name": "code", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "p", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "type(particle)" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": false } }, - "visibility": "public", + "visibility": "public" + } + ], + "contracts": [], + "locals": [], + "metadata": {}, + "name": "normalize_particle", + "native_name": "normalize_particle", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "normalize_particle", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "p", + "native_position": 0, + "python_name": "p", + "python_position": 0, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + } + ], + "return_type": null, + "visibility": "public" + }, + { + "arguments": [ + { "default_value": null, "metadata": {}, + "name": "x", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "code", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false + }, + "native_abi": null, + "native_name": "x", + "native_scope": "hidden_proc", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Int32", + "metadata": {}, + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "x", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } - } + }, + "visibility": "public" } ], - "methods": [], - "destructors": [], - "overload_sets": [], - "base_classes": [], "contracts": [], - "metadata": { - "fortran_type_attributes": [], - "fortran_component_order": [ - "code" - ], - "fortran_component_facts": [ - { - "name": "code", - "source_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "allocatable": false, - "pointer": false, - "target": false - } - ], - "fortran_layout_policy": "accessors", - "fortran_direct_layout": false - }, - "visibility": "private", + "locals": [], + "metadata": {}, + "name": "hidden_proc", + "native_name": "hidden_proc", "origin": { - "source_language": "fortran", - "native_name": "hidden_state", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "hidden_proc", "native_scope": "modern_math_physics", - "source_kind": "derived_type", - "source_type": null, + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "x", + "native_position": 0, + "python_name": "x", + "python_position": 0, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + } + ], + "return_type": null, + "visibility": "private" } ], + "imports": [], + "metadata": {}, + "name": "modern_math_physics", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "modern_math_physics", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "module", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "overload_sets": [], + "prototypes": [], + "reexports": [], "variables": [ { + "default_value": null, + "metadata": {}, "name": "counter", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "counter", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "counter", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "counter", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "hidden_scale", "origin": { - "source_language": "fortran", - "native_name": "counter", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - { - "name": "hidden_scale", + }, + "native_abi": null, + "native_name": "hidden_scale", + "native_scope": "modern_math_physics", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": null, + "name": "Float64", "origin": { - "source_language": "fortran", - "native_name": "hidden_scale", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "hidden_scale", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "private", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "hidden_scale", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "visibility": "private" } - ], - "imports": [], - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "modern_math_physics", - "native_abi": null, - "native_symbol": null, - "native_scope": "modern_math_physics", - "source_kind": "module", - "source_type": null, - "source_location": {}, - "metadata": {} - } + ] } ] } diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json index 22dbd792a..8db29890a 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json @@ -1,222 +1,239 @@ { "semantic_modules": [ { - "name": "constants_mod", + "classes": [], + "exported_names": null, "functions": [], - "prototypes": [], + "imports": [], + "metadata": {}, + "name": "constants_mod", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "constants_mod", + "native_scope": "constants_mod", + "native_symbol": null, + "source_kind": "module", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "overload_sets": [], - "classes": [], + "prototypes": [], + "reexports": [ + { + "access_modules": [ + "iso_c_binding" + ], + "declaration_dependency": true, + "entity_kind": "intrinsic", + "explicitly_public": false, + "local_name": "c_int", + "module": "constants_mod", + "origin_module": "iso_c_binding", + "python_exported": null, + "python_name": "", + "source_name": "c_int" + }, + { + "access_modules": [ + "iso_c_binding" + ], + "declaration_dependency": true, + "entity_kind": "intrinsic", + "explicitly_public": false, + "local_name": "c_double", + "module": "constants_mod", + "origin_module": "iso_c_binding", + "python_exported": null, + "python_name": "", + "source_name": "c_double" + } + ], "variables": [ { + "default_value": "100", + "metadata": { + "fortran_initializer": "100" + }, "name": "nmax", + "origin": { + "metadata": { + "allocatable": false, + "constant": true, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "nmax", + "native_scope": "constants_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer(kind=c_int)" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], + "coercions": [], "constraints": [ { - "name": "Constant", - "arguments": [] + "arguments": [], + "name": "Constant" } ], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "nmax", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer(kind=c_int)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, + "constant": true, "contiguous": false, + "lower_bounds": [], "optional": false, - "value": false, - "constant": true - } - } - }, - "visibility": "public", - "default_value": "100", - "metadata": { - "fortran_initializer": "100" + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "nmax", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer(kind=c_int)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, + "visibility": "public" + }, + { + "default_value": null, + "metadata": {}, + "name": "origin", "origin": { - "source_language": "fortran", - "native_name": "nmax", - "native_abi": null, - "native_symbol": null, - "native_scope": "constants_mod", - "source_kind": "variable", - "source_type": "integer(kind=c_int)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + "1" + ], "optional": false, - "value": false, - "constant": true - } - } - }, - { - "name": "origin", + "pointer": false, + "rank": 1, + "shape": [ + "3" + ], + "target": false, + "upper_bounds": [ + "3" + ], + "value": false + }, + "native_abi": null, + "native_name": "origin", + "native_scope": "constants_mod", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=c_double)" + }, "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "3" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": false, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + "1" + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ "3" ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ + "target": false, + "upper_bounds": [ "3" ], - "category": "explicit_shape", - "order": null, - "copy_order": null, - "axes": [ - "dense" - ], - "contiguous": true, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "origin", "native_abi": null, - "native_symbol": null, + "native_name": "origin", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=c_double)", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "real(kind=c_double)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "3" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "dense" + ], + "category": "explicit_shape", + "contiguous": true, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ "3" ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ + "source_shape": [ "3" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "origin", - "native_abi": null, - "native_symbol": null, - "native_scope": "constants_mod", - "source_kind": "variable", - "source_type": "real(kind=c_double)", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - "3" - ], - "lower_bounds": [ - "1" - ], - "upper_bounds": [ - "3" - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "visibility": "public" } - ], - "imports": [ - { - "module": "iso_c_binding", - "items": [ - { - "source": "c_int", - "target": null - }, - { - "source": "c_double", - "target": null - } - ] - } - ], - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "constants_mod", - "native_abi": null, - "native_symbol": null, - "native_scope": "constants_mod", - "source_kind": "module", - "source_type": null, - "source_location": {}, - "metadata": {} - } + ] } ] } diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json index 638bca564..94bc75b32 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json @@ -1,436 +1,438 @@ { "semantic_modules": [ { - "name": "math_mod", + "classes": [], + "exported_names": null, "functions": [ { - "name": "norm2", - "native_name": "norm2", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "x", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + null + ], + "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + ":" + ], + "target": false, + "upper_bounds": [ + null + ], + "value": false + }, + "native_abi": null, + "native_name": "x", + "native_scope": "norm2", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "::Strided" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": true, - "mutable": false, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + null + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "::Strided" - ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ ":" ], - "category": "assumed_shape", - "order": null, - "copy_order": null, - "axes": [ - "strided" + "target": false, + "upper_bounds": [ + null ], - "contiguous": false, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x", "native_abi": null, - "native_symbol": null, + "native_name": "x", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "::" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "strided" + ], + "category": "assumed_shape", + "contiguous": false, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ - ":" - ], - "lower_bounds": [ - null + "::" ], - "upper_bounds": [ - null + "source_shape": [ + ":" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "x", - "native_abi": null, - "native_symbol": null, - "native_scope": "norm2", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - ":" - ], - "lower_bounds": [ - null - ], - "upper_bounds": [ - null - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": true } }, - "optional": false + "visibility": "public" } ], - "return_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "contracts": [], + "locals": [], + "metadata": {}, + "name": "norm2", + "native_name": "norm2", + "origin": { "metadata": {}, - "storage": null, - "origin": { - "source_language": "fortran", - "native_name": "res", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "native_abi": null, + "native_name": "norm2", + "native_scope": "math_mod", + "native_symbol": null, + "source_kind": "function", + "source_language": "fortran", + "source_location": {}, + "source_type": null }, - "locals": [], - "contracts": [], "projection": [ { - "python_name": "x", + "native_c_identity": null, "native_name": "x", "native_position": 0, + "python_name": "x", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "norm2", - "native_abi": null, - "native_symbol": null, - "native_scope": "math_mod", - "source_kind": "function", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": { + "coercions": [], + "constraints": [], + "dtype": "Float64", + "metadata": {}, + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "res", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null + }, + "visibility": "public" }, { - "name": "scale", - "native_name": "scale", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "a", - "semantic_type": { - "name": "Float64", - "rank": 0, - "dtype": "Float64", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false }, + "native_abi": null, + "native_name": "a", + "native_scope": "scale", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Float64", "origin": { - "source_language": "fortran", - "native_name": "a", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } + }, + "native_abi": null, + "native_name": "a", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "x", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "a", - "native_abi": null, - "native_symbol": null, - "native_scope": "scale", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [ + null + ], "optional": false, + "pointer": false, + "rank": 1, + "shape": [ + ":" + ], + "target": false, + "upper_bounds": [ + null + ], "value": false - } + }, + "native_abi": null, + "native_name": "x", + "native_scope": "scale", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real(kind=8)" }, - "optional": false - }, - { - "name": "x", "semantic_type": { - "name": "Float64", - "rank": 1, - "dtype": "Float64", - "shape": [ - "::Strided" - ], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Float64", "metadata": {}, - "storage": { - "kind": "array", - "read_only": false, - "mutable": true, - "pointer_depth": 0, - "ownership": "borrowed", - "array": { + "name": "Float64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [ + null + ], + "optional": false, + "pointer": false, "rank": 1, "shape": [ - "::Strided" - ], - "lower_bounds": [], - "upper_bounds": [], - "source_shape": [ ":" ], - "category": "assumed_shape", - "order": null, - "copy_order": null, - "axes": [ - "strided" + "target": false, + "upper_bounds": [ + null ], - "contiguous": false, - "allocatable": false, - "pointer": false, - "metadata": {} + "value": false }, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "x", "native_abi": null, - "native_symbol": null, + "native_name": "x", "native_scope": null, + "native_symbol": null, "source_kind": "variable", - "source_type": "real(kind=8)", + "source_language": "fortran", "source_location": {}, - "metadata": { + "source_type": "real(kind=8)" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 1, + "shape": [ + "::" + ], + "storage": { + "array": { + "allocatable": false, + "axes": [ + "strided" + ], + "category": "assumed_shape", + "contiguous": false, + "copy_order": null, + "lower_bounds": [], + "metadata": {}, + "order": null, + "pointer": false, "rank": 1, "shape": [ - ":" - ], - "lower_bounds": [ - null + "::" ], - "upper_bounds": [ - null + "source_shape": [ + ":" ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "x", - "native_abi": null, - "native_symbol": null, - "native_scope": "scale", - "source_kind": "argument", - "source_type": "real(kind=8)", - "source_location": {}, - "metadata": { - "rank": 1, - "shape": [ - ":" - ], - "lower_bounds": [ - null - ], - "upper_bounds": [ - null - ], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + "upper_bounds": [] + }, + "calling_convention": null, + "kind": "array", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 0, + "read_only": false } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], + "locals": [], + "metadata": {}, + "name": "scale", + "native_name": "scale", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "scale", + "native_scope": "math_mod", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "projection": [ { - "python_name": "a", + "native_c_identity": null, "native_name": "a", "native_position": 0, + "python_name": "a", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" }, { - "python_name": "x", + "native_c_identity": null, "native_name": "x", "native_position": 1, + "python_name": "x", "python_position": 1, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "scale", - "native_abi": null, - "native_symbol": null, - "native_scope": "math_mod", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" } ], - "prototypes": [], - "overload_sets": [], - "classes": [], - "variables": [], "imports": [], "metadata": {}, + "name": "math_mod", "origin": { - "source_language": "fortran", - "native_name": "math_mod", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "math_mod", "native_scope": "math_mod", + "native_symbol": null, "source_kind": "module", - "source_type": null, + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "overload_sets": [], + "prototypes": [], + "reexports": [], + "variables": [] } ] } diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json index 6dcbbac10..367296a8a 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json @@ -1,1811 +1,1816 @@ { "semantic_modules": [ { - "name": "scope_name_reuse_combinations", - "functions": [ + "classes": [ { - "name": "do_work_i", - "native_name": "do_work_i", - "arguments": [ + "base_classes": [], + "contracts": [], + "destructors": [], + "fields": [ { - "name": "same_name", + "default_value": null, + "metadata": {}, + "name": "payload", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "payload", + "native_scope": null, + "native_symbol": null, + "source_kind": "field", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": false, - "mutable": true, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "payload", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + } + ], + "metadata": { + "fortran_component_facts": [ + { + "allocatable": false, + "kind": "", + "name": "payload", + "pointer": false, + "rank": 0, + "shape": [], + "source_type": "integer", + "target": false + } + ], + "fortran_component_order": [ + "payload" + ], + "fortran_direct_layout": false, + "fortran_layout_policy": "accessors", + "fortran_type_attributes": [] + }, + "methods": [], + "name": "same_name", + "native_name": "same_name", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "same_name", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "derived_type", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "overload_sets": [], + "visibility": "public" + } + ], + "exported_names": null, + "functions": [ + { + "arguments": [ + { "default_value": null, "metadata": { "projected_output": true }, + "name": "same_name", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "do_work_i", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "do_work_i", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Int32", + "metadata": {}, + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": false } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], + "locals": [], + "metadata": {}, + "name": "do_work_i", + "native_name": "do_work_i", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "do_work_i", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "projection": [ { - "python_name": "same_name", + "native_c_identity": null, "native_name": "same_name", "native_position": 0, + "python_name": "same_name", "python_position": 0, "result_position": 0, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "do_work_i", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" }, { - "name": "do_work_r", - "native_name": "do_work_r", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "same_name", - "semantic_type": { - "name": "Float32", - "rank": 0, - "dtype": "Float32", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "do_work_r", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Float32", + "metadata": {}, + "name": "Float32", "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "do_work_r", - "source_kind": "argument", - "source_type": "real", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], + "locals": [], + "metadata": {}, + "name": "do_work_r", + "native_name": "do_work_r", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "do_work_r", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "projection": [ { - "python_name": "same_name", + "native_c_identity": null, "native_name": "same_name", "native_position": 0, + "python_name": "same_name", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "do_work_r", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" }, { - "name": "do_work_l", - "native_name": "do_work_l", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "same_name", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "do_work_l", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "logical" + }, "semantic_type": { - "name": "Bool", - "rank": 0, - "dtype": "Bool", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Bool", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Bool", "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "logical", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "do_work_l", - "source_kind": "argument", - "source_type": "logical", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "logical" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], + "locals": [], + "metadata": {}, + "name": "do_work_l", + "native_name": "do_work_l", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "do_work_l", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "projection": [ { - "python_name": "same_name", + "native_c_identity": null, "native_name": "same_name", "native_position": 0, + "python_name": "same_name", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "do_work_l", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" }, { - "name": "host_one", - "native_name": "host_one", "arguments": [ { - "name": "same_name", - "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": false, - "mutable": true, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", "default_value": null, "metadata": { "projected_output": true }, + "name": "same_name", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "host_one", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "host_one", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Int32", + "metadata": {}, + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": false } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], + "locals": [], + "metadata": {}, + "name": "host_one", + "native_name": "host_one", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "host_one", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "projection": [ { - "python_name": "same_name", + "native_c_identity": null, "native_name": "same_name", "native_position": 0, + "python_name": "same_name", "python_position": 0, "result_position": 0, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "host_one", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" }, { - "name": "host_two", - "native_name": "host_two", "arguments": [ { - "name": "same_name", - "semantic_type": { - "name": "Float32", - "rank": 0, - "dtype": "Float32", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": false, - "mutable": true, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", "default_value": null, "metadata": { "projected_output": true }, + "name": "same_name", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "host_two", - "source_kind": "argument", - "source_type": "real", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "host_two", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Float32", + "metadata": {}, + "name": "Float32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": false } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], - "projection": [ + "locals": [], + "metadata": {}, + "name": "host_two", + "native_name": "host_two", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "host_two", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "subroutine", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "projection": [ { - "python_name": "same_name", + "native_c_identity": null, "native_name": "same_name", "native_position": 0, + "python_name": "same_name", "python_position": 0, "result_position": 0, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "host_two", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": null, + "visibility": "public" }, { - "name": "convert_to_complex", - "native_name": "convert_to_complex", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "same_name", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "convert_to_complex", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "convert_to_complex", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "optional": false + "visibility": "public" } ], - "return_type": { - "name": "Complex64", - "rank": 0, - "dtype": "Complex64", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "contracts": [], + "locals": [], + "metadata": {}, + "name": "convert_to_complex", + "native_name": "convert_to_complex", + "origin": { "metadata": {}, - "storage": null, - "origin": { - "source_language": "fortran", - "native_name": "shared", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "complex", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "native_abi": null, + "native_name": "convert_to_complex", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "function", + "source_language": "fortran", + "source_location": {}, + "source_type": null }, - "locals": [], - "contracts": [], "projection": [ { - "python_name": "same_name", + "native_c_identity": null, "native_name": "same_name", "native_position": 0, + "python_name": "same_name", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "convert_to_complex", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "function", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": { + "coercions": [], + "constraints": [], + "dtype": "Complex64", + "metadata": {}, + "name": "Complex64", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "shared", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "complex" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null + }, + "visibility": "public" }, { - "name": "convert_to_char", - "native_name": "convert_to_char", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "same_name", - "semantic_type": { - "name": "Float32", - "rank": 0, - "dtype": "Float32", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "convert_to_char", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Float32", + "metadata": {}, + "name": "Float32", "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "convert_to_char", - "source_kind": "argument", - "source_type": "real", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "optional": false + "visibility": "public" + } + ], + "contracts": [], + "locals": [], + "metadata": {}, + "name": "convert_to_char", + "native_name": "convert_to_char", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "convert_to_char", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "function", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "same_name", + "native_position": 0, + "python_name": "same_name", + "python_position": 0, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" } ], "return_type": { - "name": "String", - "rank": 0, - "dtype": "String", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "String", "metadata": { "fortran_character_length": "16" }, - "storage": null, + "name": "String", "origin": { - "source_language": "fortran", - "native_name": "shared", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "character(kind=len=16)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "shared", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "character(kind=len=16)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "locals": [], - "contracts": [], - "projection": [ - { - "python_name": "same_name", - "native_name": "same_name", - "native_position": 0, - "python_position": 0, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "convert_to_char", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "function", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "visibility": "public" }, { - "name": "convert_to_logical", - "native_name": "convert_to_logical", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "same_name", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "convert_to_logical", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "character(kind=len=*)" + }, "semantic_type": { - "name": "String", - "rank": 0, - "dtype": "String", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "String", "metadata": { "fortran_character_length": "*" }, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "String", "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "character(kind=len=*)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "convert_to_logical", - "source_kind": "argument", - "source_type": "character(kind=len=*)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "character(kind=len=*)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "optional": false + "visibility": "public" } ], - "return_type": { - "name": "Bool", - "rank": 0, - "dtype": "Bool", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "contracts": [], + "locals": [], + "metadata": {}, + "name": "convert_to_logical", + "native_name": "convert_to_logical", + "origin": { "metadata": {}, - "storage": null, - "origin": { - "source_language": "fortran", - "native_name": "shared", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "logical", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "native_abi": null, + "native_name": "convert_to_logical", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "function", + "source_language": "fortran", + "source_location": {}, + "source_type": null }, - "locals": [], - "contracts": [], "projection": [ { - "python_name": "same_name", + "native_c_identity": null, "native_name": "same_name", "native_position": 0, + "python_name": "same_name", "python_position": 0, "result_position": null, - "value_kind": "", "value": null, "value_cast": null, - "native_c_identity": null + "value_kind": "" } ], - "metadata": {}, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "convert_to_logical", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "function", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "return_type": { + "coercions": [], + "constraints": [], + "dtype": "Bool", + "metadata": {}, + "name": "Bool", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "shared", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "logical" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null + }, + "visibility": "public" } ], - "prototypes": [], + "imports": [], + "metadata": {}, + "name": "scope_name_reuse_combinations", + "origin": { + "metadata": {}, + "native_abi": null, + "native_name": "scope_name_reuse_combinations", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "module", + "source_language": "fortran", + "source_location": {}, + "source_type": null + }, "overload_sets": [ { + "metadata": {}, "name": "do_work", + "native_scope": "scope_name_reuse_combinations", "procedures": [ { - "name": "do_work_i", - "native_name": "do_work_i", "arguments": [ { - "name": "same_name", - "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": true, - "aliasing": true - }, - "metadata": {}, - "storage": { - "kind": "reference", - "read_only": false, - "mutable": true, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } - }, - "visibility": "public", "default_value": null, "metadata": { "projected_output": true }, + "name": "same_name", + "optional": false, "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "do_work_i", - "source_kind": "argument", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "do_work_i", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "semantic_type": { + "coercions": [], + "constraints": [], + "dtype": "Int32", + "metadata": {}, + "name": "Int32", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": true, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": true, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": false } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], - "projection": [ - { - "python_name": "same_name", - "native_name": "same_name", - "native_position": 0, - "python_position": 0, - "result_position": 0, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], + "locals": [], "metadata": { "fortran_generic_name": "do_work", "overload_kind": "generic", "overload_target": "do_work_i" }, - "visibility": "public", + "name": "do_work_i", + "native_name": "do_work_i", "origin": { - "source_language": "fortran", - "native_name": "do_work_i", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "do_work_i", "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, "source_kind": "subroutine", - "source_type": null, + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "same_name", + "native_position": 0, + "python_name": "same_name", + "python_position": 0, + "result_position": 0, + "value": null, + "value_cast": null, + "value_kind": "" + } + ], + "return_type": null, + "visibility": "public" }, { - "name": "do_work_r", - "native_name": "do_work_r", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "same_name", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "do_work_r", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, "semantic_type": { - "name": "Float32", - "rank": 0, - "dtype": "Float32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float32", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Float32", "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "do_work_r", - "source_kind": "argument", - "source_type": "real", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true } }, - "optional": false + "visibility": "public" } ], - "return_type": null, - "locals": [], "contracts": [], - "projection": [ - { - "python_name": "same_name", - "native_name": "same_name", - "native_position": 0, - "python_position": 0, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], + "locals": [], "metadata": { "fortran_generic_name": "do_work", "overload_kind": "generic", "overload_target": "do_work_r" }, - "visibility": "public", + "name": "do_work_r", + "native_name": "do_work_r", "origin": { - "source_language": "fortran", - "native_name": "do_work_r", + "metadata": {}, "native_abi": null, - "native_symbol": null, + "native_name": "do_work_r", "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, "source_kind": "subroutine", - "source_type": null, + "source_language": "fortran", "source_location": {}, - "metadata": {} - } + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "same_name", + "native_position": 0, + "python_name": "same_name", + "python_position": 0, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" + } + ], + "return_type": null, + "visibility": "public" }, { - "name": "do_work_l", - "native_name": "do_work_l", "arguments": [ { + "default_value": null, + "metadata": {}, "name": "same_name", + "optional": false, + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": "do_work_l", + "native_symbol": null, + "source_kind": "argument", + "source_language": "fortran", + "source_location": {}, + "source_type": "logical" + }, "semantic_type": { - "name": "Bool", - "rank": 0, - "dtype": "Bool", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Bool", "metadata": {}, - "storage": { - "kind": "reference", - "read_only": true, - "mutable": false, - "pointer_depth": 1, - "ownership": "borrowed", - "array": null, - "calling_convention": null, - "metadata": {} - }, + "name": "Bool", "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "logical", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "do_work_l", - "source_kind": "argument", - "source_type": "logical", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - }, - "optional": false - } - ], - "return_type": null, - "locals": [], - "contracts": [], - "projection": [ - { - "python_name": "same_name", - "native_name": "same_name", - "native_position": 0, - "python_position": 0, - "result_position": null, - "value_kind": "", - "value": null, - "value_cast": null, - "native_c_identity": null - } - ], - "metadata": { - "fortran_generic_name": "do_work", - "overload_kind": "generic", - "overload_target": "do_work_l" - }, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "do_work_l", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "subroutine", - "source_type": null, - "source_location": {}, - "metadata": {} - } - } - ] - } - ], - "classes": [ - { - "name": "same_name", - "native_name": "same_name", - "fields": [ - { - "name": "payload", - "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], - "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, - "metadata": {}, - "storage": null, - "origin": { - "source_language": "fortran", - "native_name": "payload", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, - "metadata": { + }, + "native_abi": null, + "native_name": "same_name", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "logical" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, "rank": 0, "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } + "storage": { + "array": null, + "calling_convention": null, + "kind": "reference", + "metadata": {}, + "mutable": false, + "ownership": "borrowed", + "pointer_depth": 1, + "read_only": true + } + }, + "visibility": "public" } + ], + "contracts": [], + "locals": [], + "metadata": { + "fortran_generic_name": "do_work", + "overload_kind": "generic", + "overload_target": "do_work_l" }, - "visibility": "public", - "default_value": null, - "metadata": {}, + "name": "do_work_l", + "native_name": "do_work_l", "origin": { - "source_language": "fortran", - "native_name": "payload", + "metadata": {}, "native_abi": null, + "native_name": "do_work_l", + "native_scope": "scope_name_reuse_combinations", "native_symbol": null, - "native_scope": null, - "source_kind": "field", - "source_type": "integer", + "source_kind": "subroutine", + "source_language": "fortran", "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false + "source_type": null + }, + "projection": [ + { + "native_c_identity": null, + "native_name": "same_name", + "native_position": 0, + "python_name": "same_name", + "python_position": 0, + "result_position": null, + "value": null, + "value_cast": null, + "value_kind": "" } - } + ], + "return_type": null, + "visibility": "public" } ], - "methods": [], - "destructors": [], - "overload_sets": [], - "base_classes": [], - "contracts": [], - "metadata": { - "fortran_type_attributes": [], - "fortran_component_order": [ - "payload" - ], - "fortran_component_facts": [ - { - "name": "payload", - "source_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "allocatable": false, - "pointer": false, - "target": false - } - ], - "fortran_layout_policy": "accessors", - "fortran_direct_layout": false - }, - "visibility": "public", - "origin": { - "source_language": "fortran", - "native_name": "same_name", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "derived_type", - "source_type": null, - "source_location": {}, - "metadata": {} - } + "visibility": "public" } ], + "prototypes": [], + "reexports": [], "variables": [ { + "default_value": null, + "metadata": {}, "name": "same_name_i", + "origin": { + "metadata": { + "allocatable": false, + "contiguous": false, + "lower_bounds": [], + "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], + "value": false + }, + "native_abi": null, + "native_name": "same_name_i", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, "semantic_type": { - "name": "Int32", - "rank": 0, - "dtype": "Int32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Int32", "metadata": {}, - "storage": null, + "name": "Int32", "origin": { - "source_language": "fortran", - "native_name": "same_name_i", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "same_name_i", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "integer" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "same_name_r", "origin": { - "source_language": "fortran", - "native_name": "same_name_i", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "variable", - "source_type": "integer", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - { - "name": "same_name_r", + }, + "native_abi": null, + "native_name": "same_name_r", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, "semantic_type": { - "name": "Float32", - "rank": 0, - "dtype": "Float32", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Float32", "metadata": {}, - "storage": null, + "name": "Float32", "origin": { - "source_language": "fortran", - "native_name": "same_name_r", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "real", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "same_name_r", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "real" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "same_name_l", "origin": { - "source_language": "fortran", - "native_name": "same_name_r", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "variable", - "source_type": "real", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - { - "name": "same_name_l", + }, + "native_abi": null, + "native_name": "same_name_l", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "logical" + }, "semantic_type": { - "name": "Bool", - "rank": 0, - "dtype": "Bool", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Bool", "metadata": {}, - "storage": null, + "name": "Bool", "origin": { - "source_language": "fortran", - "native_name": "same_name_l", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "logical", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "same_name_l", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "logical" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "same_name_c", "origin": { - "source_language": "fortran", - "native_name": "same_name_l", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "variable", - "source_type": "logical", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - { - "name": "same_name_c", + }, + "native_abi": null, + "native_name": "same_name_c", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "complex" + }, "semantic_type": { - "name": "Complex64", - "rank": 0, - "dtype": "Complex64", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "Complex64", "metadata": {}, - "storage": null, + "name": "Complex64", "origin": { - "source_language": "fortran", - "native_name": "same_name_c", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "complex", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "same_name_c", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "complex" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", + "visibility": "public" + }, + { "default_value": null, "metadata": {}, + "name": "same_name_s", "origin": { - "source_language": "fortran", - "native_name": "same_name_c", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "variable", - "source_type": "complex", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } - }, - { - "name": "same_name_s", + }, + "native_abi": null, + "native_name": "same_name_s", + "native_scope": "scope_name_reuse_combinations", + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "character(kind=len=8)" + }, "semantic_type": { - "name": "String", - "rank": 0, - "dtype": "String", - "shape": [], - "constraints": [], "coercions": [], - "ownership": { - "ownership": "borrowed", - "mutable": false, - "aliasing": true - }, + "constraints": [], + "dtype": "String", "metadata": { "fortran_character_length": "8" }, - "storage": null, + "name": "String", "origin": { - "source_language": "fortran", - "native_name": "same_name_s", - "native_abi": null, - "native_symbol": null, - "native_scope": null, - "source_kind": "variable", - "source_type": "character(kind=len=8)", - "source_location": {}, "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], "allocatable": false, - "pointer": false, - "target": false, "contiguous": false, + "lower_bounds": [], "optional": false, + "pointer": false, + "rank": 0, + "shape": [], + "target": false, + "upper_bounds": [], "value": false - } - } + }, + "native_abi": null, + "native_name": "same_name_s", + "native_scope": null, + "native_symbol": null, + "source_kind": "variable", + "source_language": "fortran", + "source_location": {}, + "source_type": "character(kind=len=8)" + }, + "ownership": { + "aliasing": true, + "mutable": false, + "ownership": "borrowed" + }, + "rank": 0, + "shape": [], + "storage": null }, - "visibility": "public", - "default_value": null, - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "same_name_s", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "variable", - "source_type": "character(kind=len=8)", - "source_location": {}, - "metadata": { - "rank": 0, - "shape": [], - "lower_bounds": [], - "upper_bounds": [], - "allocatable": false, - "pointer": false, - "target": false, - "contiguous": false, - "optional": false, - "value": false - } - } + "visibility": "public" } - ], - "imports": [], - "metadata": {}, - "origin": { - "source_language": "fortran", - "native_name": "scope_name_reuse_combinations", - "native_abi": null, - "native_symbol": null, - "native_scope": "scope_name_reuse_combinations", - "source_kind": "module", - "source_type": null, - "source_location": {}, - "metadata": {} - } + ] } ] } diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/test_compile_time_values.py b/tests/fortran/infrastructure/semantic_ir/semantics/test_compile_time_values.py index 0d655502c..113ad4469 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/test_compile_time_values.py +++ b/tests/fortran/infrastructure/semantic_ir/semantics/test_compile_time_values.py @@ -352,6 +352,9 @@ def test_semantic_compile_time_requirements_cover_all_parser_contexts(): ) == [] ) + # A character model states its selector the way every producer records it. + bad_character = FortranVariable(name="bad_character", base_type="character", kind="bad") + bad_character.record_character_selector("(kind=bad)") unsupported = collect_semantic_compile_time_requirements( FortranFile( variables=[ @@ -359,7 +362,7 @@ def test_semantic_compile_time_requirements_cover_all_parser_contexts(): FortranVariable(name="bad_real", base_type="real", kind="bad"), FortranVariable(name="bad_complex", base_type="complex", kind="bad"), FortranVariable(name="bad_logical", base_type="logical", kind="bad"), - FortranVariable(name="bad_character", base_type="character", kind="bad"), + bad_character, FortranVariable(name="callback", base_type="procedure", kind="f_iface"), ] ) @@ -393,7 +396,37 @@ def test_semantic_compile_time_requirements_cover_all_parser_contexts(): assert _compile_time_requirement_message("other", "n", "n + 1") == "Compile-time value required for 'n'." -def test_resolve_semantic_compile_time_values_rewrites_shapes_and_constraints(): +def test_resolve_semantic_compile_time_values_leaves_recorded_decisions_opaque(): + """A metadata tag is a decision already taken, not text awaiting a value. + + `fortran_pointer_association="runtime"` records how a pointer is + associated. A module that also declares `runtime` as a parameter must not + turn that recorded choice into the parameter's value. + """ + module = SemanticModule( + name="tagged_mod", + variables=[ + SemanticArgument( + name="view", + semantic_type=SemanticType( + name="Float64", + dtype="Float64", + rank=1, + shape=["runtime"], + metadata={"fortran_pointer_association": "runtime"}, + ), + ) + ], + ) + + resolved = resolve_semantic_compile_time_values(module, {"runtime": 4}) + + semantic_type = resolved.variables[0].semantic_type + assert semantic_type.shape == ["4"] + assert semantic_type.metadata == {"fortran_pointer_association": "runtime"} + + +def test_resolve_semantic_compile_time_values_rewrites_shapes(): module = SemanticModule( name="shape_mod", variables=[ @@ -425,6 +458,11 @@ def test_resolve_semantic_compile_time_values_rewrites_shapes_and_constraints(): def test_resolve_semantic_compile_time_values_handles_nested_modules(): + """Specialization reaches every nesting level and touches only expression fields. + + A metadata value that happens to spell a parameter name is a recorded + decision, not text to evaluate, so it survives at every level. + """ module = SemanticModule( name="nested_mod", variables=[ @@ -445,10 +483,10 @@ def test_resolve_semantic_compile_time_values_handles_nested_modules(): upper_bounds=["n"], ), ), - metadata={"bounds": ("n", ["m"])}, + metadata={"fortran_character_length": "n", "fortran_pointer_association": "n"}, ), default_value="n", - metadata={"alias": "m"}, + metadata={"fortran_initializer": "m", "address_role": "m"}, ) ], functions=[ @@ -458,15 +496,15 @@ def test_resolve_semantic_compile_time_values_handles_nested_modules(): SemanticArgument( name="x", semantic_type=SemanticType("Float64", rank=1, shape=["m"]), - metadata={"scale": "n"}, + metadata={"native_callback_kind": "n"}, ) ], - projection=[ProjectionMapping(value={"shape": ["n", ("m",)]})], - metadata={"work": ["n", {"inner": "m"}]}, + projection=[ProjectionMapping(value={"kind": "return", "name": "n", "position": 0})], + metadata={"import_scope": "n"}, ), SemanticFunction( name="with_result", - return_type=SemanticType("Int32", metadata={"extent": "n"}), + return_type=SemanticType("Int32", metadata={"fortran_character_length": "n"}), ), ], classes=[ @@ -482,43 +520,47 @@ def test_resolve_semantic_compile_time_values_handles_nested_modules(): methods=[ SemanticMethod( name="touch", - arguments=[SemanticArgument("self", SemanticType("state_t", metadata={"n": "n"}))], - return_type=SemanticType("Int32", metadata={"m": "m"}), - projection=[ProjectionMapping(value=("n", {"m": "m"}))], - metadata={"method": "n"}, + arguments=[SemanticArgument("self", SemanticType("state_t", metadata={"c_kind": "n"}))], + return_type=SemanticType("Int32", metadata={"fortran_character_length": "m"}), + metadata={"fortran_type_bound_target": "n"}, ) ], - metadata={"class": "m"}, + metadata={"fortran_attributes": "m"}, ) ], - metadata={"module": ["n", ("m",)]}, + metadata={"fortran_bind_c": "n"}, ) resolved = resolve_semantic_compile_time_values([module], {"n": 4, "m": 2}) assert module.variables[0].semantic_type.shape == ["n"] resolved_module = resolved[0] + + # Every level's declaration expressions are specialized. assert resolved_module.variables[0].semantic_type.shape == ["4"] assert resolved_module.variables[0].semantic_type.storage.array.shape == ["4"] assert resolved_module.variables[0].semantic_type.storage.array.source_shape == ["1:4"] assert resolved_module.variables[0].semantic_type.storage.array.lower_bounds == ["4"] assert resolved_module.variables[0].semantic_type.storage.array.upper_bounds == ["4"] - assert resolved_module.variables[0].semantic_type.metadata == {"bounds": ("4", ["2"])} + assert resolved_module.variables[0].semantic_type.metadata["fortran_character_length"] == "4" assert resolved_module.variables[0].default_value == "4" - assert resolved_module.variables[0].metadata == {"alias": "2"} + assert resolved_module.variables[0].metadata["fortran_initializer"] == "2" assert resolved_module.functions[0].arguments[0].semantic_type.shape == ["2"] - assert resolved_module.functions[0].arguments[0].metadata == {"scale": "4"} - assert resolved_module.functions[0].projection[0].value == {"shape": ["4", ("2",)]} - assert resolved_module.functions[0].metadata == {"work": ["4", {"inner": "2"}]} - assert resolved_module.functions[1].return_type.metadata == {"extent": "4"} + assert resolved_module.functions[1].return_type.metadata["fortran_character_length"] == "4" assert resolved_module.classes[0].fields[0].semantic_type.shape == ["4"] assert resolved_module.classes[0].fields[0].default_value == "2" - assert resolved_module.classes[0].methods[0].arguments[0].semantic_type.metadata == {"n": "4"} - assert resolved_module.classes[0].methods[0].return_type.metadata == {"m": "2"} - assert resolved_module.classes[0].methods[0].projection[0].value == ("4", {"m": "2"}) - assert resolved_module.classes[0].methods[0].metadata == {"method": "4"} - assert resolved_module.classes[0].metadata == {"class": "2"} - assert resolved_module.metadata == {"module": ["4", ("2",)]} + assert resolved_module.classes[0].methods[0].return_type.metadata["fortran_character_length"] == "2" + + # Recorded decisions are opaque at every level, however they are spelled. + assert resolved_module.variables[0].semantic_type.metadata["fortran_pointer_association"] == "n" + assert resolved_module.variables[0].metadata["address_role"] == "m" + assert resolved_module.functions[0].arguments[0].metadata == {"native_callback_kind": "n"} + assert resolved_module.functions[0].projection[0].value == {"kind": "return", "name": "n", "position": 0} + assert resolved_module.functions[0].metadata == {"import_scope": "n"} + assert resolved_module.classes[0].methods[0].arguments[0].semantic_type.metadata == {"c_kind": "n"} + assert resolved_module.classes[0].methods[0].metadata == {"fortran_type_bound_target": "n"} + assert resolved_module.classes[0].metadata == {"fortran_attributes": "m"} + assert resolved_module.metadata == {"fortran_bind_c": "n"} def test_module_parameters_preserve_literal_values_in_semantic_ir(): diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/test_semantic_specialization_properties.py b/tests/fortran/infrastructure/semantic_ir/semantics/test_semantic_specialization_properties.py index b28f416e0..f3d2221a8 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/test_semantic_specialization_properties.py +++ b/tests/fortran/infrastructure/semantic_ir/semantics/test_semantic_specialization_properties.py @@ -53,39 +53,52 @@ def test_generated_semantic_specialization_is_non_mutating_and_idempotent(n, m): rank=2, shape=["1:n", "m + 1"], constraints=[SemanticConstraint("Extent", ["n", {"upper": "m"}])], - metadata={"bounds": ("n", ["m"])}, + metadata={"fortran_character_length": "n", "enum_name": "n"}, storage=SemanticStorageContract( kind="array", - metadata={"extent": "n"}, + metadata={"address_role": "n"}, array=SemanticArrayContract( rank=2, shape=["1:n", "m + 1"], lower_bounds=["1", "0"], upper_bounds=["n", "m"], source_shape=["1:n", "0:m"], - metadata={"extent": {"first": "n", "second": "m"}}, + metadata={"representation": "m"}, ), ), ), + default_value="n + m", + metadata={"fortran_initializer": "m", "fortran_pointer_association": "n"}, ) ], - metadata={"shape": ["n", "m"]}, + metadata={"import_scope": "n"}, ) original = asdict(module) resolved = resolve_semantic_compile_time_values(module, {"n": n, "m": m}) assert asdict(module) == original - semantic_type = resolved.variables[0].semantic_type + variable = resolved.variables[0] + semantic_type = variable.semantic_type assert semantic_type.shape == [f"1:{n}", f"{m} + 1"] - assert semantic_type.constraints[0].arguments == [str(n), {"upper": str(m)}] - assert semantic_type.metadata == {"bounds": (str(n), [str(m)])} assert semantic_type.storage is not None - assert semantic_type.storage.metadata == {"extent": str(n)} assert semantic_type.storage.array is not None assert semantic_type.storage.array.shape == [f"1:{n}", f"{m} + 1"] assert semantic_type.storage.array.lower_bounds == ["1", "0"] assert semantic_type.storage.array.upper_bounds == [str(n), str(m)] assert semantic_type.storage.array.source_shape == [f"1:{n}", f"0:{m}"] - assert semantic_type.storage.array.metadata == {"extent": {"first": str(n), "second": str(m)}} + + # Declared expression fields are specialized; the default value is one too. + assert semantic_type.metadata["fortran_character_length"] == str(n) + assert variable.metadata["fortran_initializer"] == str(m) + assert variable.default_value == f"{n} + {m}" + + # Everything else is an opaque recorded decision, whatever it spells. + assert semantic_type.metadata["enum_name"] == "n" + assert semantic_type.constraints[0].arguments == ["n", {"upper": "m"}] + assert semantic_type.storage.metadata == {"address_role": "n"} + assert semantic_type.storage.array.metadata == {"representation": "m"} + assert variable.metadata["fortran_pointer_association"] == "n" + assert resolved.metadata == {"import_scope": "n"} + assert asdict(resolve_semantic_compile_time_values(resolved, {"n": n, "m": m})) == asdict(resolved) diff --git a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/__init__.pyi index 3d5f79513..f325e1b3e 100644 --- a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/__init__.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/__init__.pyi @@ -1 +1,3 @@ from . import fnaming_f90 + +__all__ = ["fnaming_f90"] diff --git a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/fnaming_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/fnaming_f90.pyi index 5085a45b1..4116a049a 100644 --- a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/fnaming_f90.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/fnaming_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Annotated, Arg, Int32, SourceName, bind, native_call -class visible_t: +class Visible_T: def __init__( self, *, @@ -11,7 +11,7 @@ class visible_t: lambda_: Annotated[Int32, SourceName("lambda")] = 3 lambda__2: Annotated[Int32, SourceName("lambda_")] = 4 - @bind("visible_t.from") + @bind("Visible_T.from") def from_(self) -> Int32: ... value: Int32 @@ -29,3 +29,5 @@ def lambda__2( ) -> Int32: ... def get_value() -> Int32: ... + +__all__ = ["Visible_T", "value", "lambda_", "lambda__2", "get_value"] diff --git a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_contract_names_match_build.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_contract_names_match_build.py new file mode 100644 index 000000000..77465ac9e --- /dev/null +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_contract_names_match_build.py @@ -0,0 +1,137 @@ +"""The contract a source build writes names the declarations that build published.""" + +import ast +from pathlib import Path + +import numpy as np +import pytest + +from prik.pipeline.build import BUILD_CONTRACT_DIRECTORY_NAME, build_fortran_extension +from tests.fortran._support.wrapper_build import _build_inline_pyi_contract_module, _sole_native_module + +pytestmark = pytest.mark.fortran_end_to_end + +# A module variable and a module procedure whose source names both want the +# Python name `lambda_`. The collision crosses declaration categories, so any +# stage naming them in a different order settles the pair the other way round. +CROSS_CATEGORY_COLLISION = """ +module collide_mod + implicit none + integer :: lambda = 7 +contains + integer function lambda_() + lambda_ = 1 + end function lambda_ +end module collide_mod +""" + + +def _declared_names(contract: Path) -> dict[str, str]: + """Map each declaration in one contract to the kind of statement declaring it.""" + module = ast.parse(contract.read_text(encoding="utf-8"), filename=str(contract)) + declared: dict[str, str] = {} + for statement in module.body: + if isinstance(statement, ast.FunctionDef): + declared[statement.name] = "function" + elif isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name): + declared[statement.target.id] = "variable" + elif isinstance(statement, ast.ClassDef): + declared[statement.name] = "class" + return declared + + +def _stated_exports(contract: Path) -> list[str]: + """Return the ``__all__`` a generated contract states about itself.""" + module = ast.parse(contract.read_text(encoding="utf-8"), filename=str(contract)) + for statement in module.body: + targets = getattr(statement, "targets", []) + if any(isinstance(target, ast.Name) and target.id == "__all__" for target in targets): + return [ast.literal_eval(element) for element in statement.value.elts] + raise AssertionError(f"{contract} states no __all__") + + +def test_a_cross_category_collision_settles_the_same_way_for_both(tmp_path: Path): + """One stage owns the name, so the contract cannot bind the pair the other way.""" + source = tmp_path / "collide_mod.f90" + source.write_text(CROSS_CATEGORY_COLLISION, encoding="utf-8") + + result = build_fortran_extension( + source, + output_dir=tmp_path / "build", + output_name="collide_api", + ) + namespace = _sole_native_module(result.import_module()) + contract = result.output_dir / BUILD_CONTRACT_DIRECTORY_NAME / "collide_mod.pyi" + + declared = _declared_names(contract) + assert set(declared) == set(_stated_exports(contract)) + + # The build decides which declaration each name reaches; naming the same + # pair in a different order would swap these two and leave every individual + # assertion above still passing. + for name, kind in declared.items(): + published = getattr(namespace, name) + if kind == "function": + assert callable(published), f"contract declares {name} a function; the build published {published!r}" + assert published() == np.int32(1) + else: + assert not callable(published), f"contract declares {name} a variable; the build published {published!r}" + assert published == np.int32(7) + + +def test_a_derived_type_is_named_once_for_the_build_and_its_contract(tmp_path: Path): + """A class name is a public name too, so the same owner settles it.""" + source = tmp_path / "typed_mod.f90" + source.write_text( + """ +module typed_mod + implicit none + type :: Point_T + integer :: x = 3 + end type Point_T +end module typed_mod +""", + encoding="utf-8", + ) + + result = build_fortran_extension( + source, + output_dir=tmp_path / "build", + output_name="typed_api", + ) + namespace = _sole_native_module(result.import_module()) + contract = result.output_dir / BUILD_CONTRACT_DIRECTORY_NAME / "typed_mod.pyi" + + declared = _declared_names(contract) + assert set(declared) == set(_stated_exports(contract)) + for name in declared: + assert hasattr(namespace, name), f"contract declares {name}; the build published {dir(namespace)}" + + +def test_an_edited_contract_keeps_arbitrary_class_capitalization(tmp_path: Path): + """The generated class style is a default; an edited contract owns its spelling.""" + module, _result = _build_inline_pyi_contract_module( + tmp_path, + module_name="mixed_case_contract_mod", + source_text=""" +module mixed_case_contract_mod + implicit none + type :: point + integer :: x = 3 + end type point +end module mixed_case_contract_mod +""", + contract_text=""" +from prik.contracts import Int32 + +class pOiNt: + def __init__(self, *, x: Int32 = 3) -> None: ... + + x: Int32 + +__all__ = ["pOiNt"] +""", + ) + + value = module.pOiNt() + assert value.x == np.int32(3) diff --git a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py index 027e682fe..c22daa49d 100644 --- a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py @@ -66,8 +66,9 @@ def test_entry_contract_selects_child_flattened_aliased_and_bound_exports(tmp_pa flattened_package = _editable_package(tmp_path, "flattened_api", "flatten.pyi") for leaf_name in ("module1.pyi", "module2.pyi"): leaf = flattened_package / leaf_name + # Removing a declaration removes what the contract publishes with it. leaf.write_text( - leaf.read_text(encoding="utf-8").replace(UPDATE_DECLARATION, "\n"), + leaf.read_text(encoding="utf-8").replace(UPDATE_DECLARATION, "\n").replace(', "update"]', "]"), encoding="utf-8", ) flattened = _build(flattened_package, native_object) diff --git a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_reexport_alias_linking.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_reexport_alias_linking.py new file mode 100644 index 000000000..e8d2620d4 --- /dev/null +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_reexport_alias_linking.py @@ -0,0 +1,61 @@ +"""A namespace alias reaches its target in a module that publishes nothing else. + +Binding an alias calls a bundled native helper, so a module whose only use of +those helpers is the alias still has to carry them. Nothing else in such a +module asks for the support header, which is what made this fail to link. +""" + +from pathlib import Path + +import pytest + +from prik.pipeline.build import build_fortran_extension +from tests.fortran._support.wrapper_build import _import_from_build_dir + +pytestmark = pytest.mark.fortran_end_to_end + +# Neither procedure takes an argument, returns a result, or touches a derived +# type or module variable, so the alias is the module's only helper use. +HOME = """\ +module alias_home + implicit none +contains + subroutine target() + end subroutine target +end module alias_home +""" + +FACADE = """\ +module alias_facade + use alias_home, only : lambda => target + implicit none + public :: lambda + +contains + + subroutine lambda_() + end subroutine lambda_ +end module alias_facade +""" + + +def test_an_alias_is_the_only_helper_a_module_needs(tmp_path: Path): + """The alias binds its target, so the module links and both names resolve.""" + home = tmp_path / "alias_home.f90" + home.write_text(HOME, encoding="utf-8") + facade = tmp_path / "alias_facade.f90" + facade.write_text(FACADE, encoding="utf-8") + + result = build_fortran_extension( + [home, facade], + output_dir=tmp_path / "build", + output_name="alias_api", + ) + module = _import_from_build_dir(result.module_name, result.output_dir) + + published = {name for name in dir(module.alias_facade) if not name.startswith("_")} + assert published == {"lambda_", "lambda__2"} + # The module's own declaration keeps the name it would have had, and the + # imported alias is the one export policy moved aside. + assert module.alias_facade.lambda_.__name__ == "lambda_" + assert module.alias_facade.lambda__2 is module.alias_home.target diff --git a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_naming.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_naming.py index 53b1723b3..46730f207 100644 --- a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_naming.py +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_naming.py @@ -43,10 +43,10 @@ def test_visibility_and_default_python_name_fixing_policy( assert not hasattr(module, "get_value_2") assert not hasattr(module, "set_value") - assert not hasattr(module, "hidden_t") + assert not hasattr(module, "Hidden_T") assert not hasattr(module, "hidden_proc") - item = module.visible_t(lambda_=np.int32(5), lambda__2=np.int32(6)) + item = module.Visible_T(lambda_=np.int32(5), lambda__2=np.int32(6)) assert item.lambda_ == 5 assert item.lambda__2 == 6 assert item.from_() == 11 diff --git a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/pipeline/test_declaring_namespace_publication.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/pipeline/test_declaring_namespace_publication.py new file mode 100644 index 000000000..0cfa80d8e --- /dev/null +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/pipeline/test_declaring_namespace_publication.py @@ -0,0 +1,174 @@ +"""Which namespace may publish a name, by the kind of declaration it names. + +A procedure or a derived type reaches Python as one object, so another +namespace can bind it and PRIK re-exports it through an alias. A module +variable likewise permits multiple publications, but all of them refer to the +one variable plan and its live native state. A generic remains a dispatch +surface rather than one object, so only its declaring namespace can publish it. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from prik.pipeline.build import build_pyi_extension + +GENERIC_SOURCE = """\ +module home + implicit none + integer :: counter = 5 + interface area + module procedure area_i, area_r + end interface area +contains + integer function area_i(v) + integer, intent(in) :: v + area_i = v + end function area_i + real(8) function area_r(v) + real(8), intent(in) :: v + area_r = v + end function area_r + integer function scale_value(v) + integer, intent(in) :: v + scale_value = v * 2 + end function scale_value +end module home +""" + +HOME_CONTRACT = """\ +from prik.contracts import Addr, Arg, Float64, Int32, native_call, overload + +counter: Int32 + +@native_call([Addr(Arg(0))]) +def area_i( + v: Int32 +) -> Int32: ... + +@native_call([Addr(Arg(0))]) +def area_r( + v: Float64 +) -> Float64: ... + +@native_call([Addr(Arg(0))]) +def scale_value( + v: Int32 +) -> Int32: ... + +@overload("area_i") +def area( + v: Int32 +) -> Int32: ... + +@overload("area_r") +def area( + v: Float64 +) -> Float64: ... + +__all__ = {home_exports} +""" + + +def _package(tmp_path: Path, *, home_exports: list[str], facade: str) -> Path: + """Write a two-namespace contract package and return its entry contract.""" + (tmp_path / "home.f90").write_text(GENERIC_SOURCE, encoding="utf-8") + package = tmp_path / "contracts" + package.mkdir() + (package / "home.pyi").write_text(HOME_CONTRACT.format(home_exports=home_exports), encoding="utf-8") + (package / "facade.pyi").write_text(facade, encoding="utf-8") + (package / "__init__.pyi").write_text( + 'from . import home\nfrom . import facade\n\n__all__ = ["home", "facade"]\n', + encoding="utf-8", + ) + return package / "__init__.pyi" + + +def _plan(entry: Path, tmp_path: Path, name: str): + """Plan a build from one contract package without compiling it.""" + return build_pyi_extension( + entry, + native_fortran_sources=[str(tmp_path / "home.f90")], + output_dir=tmp_path / "build", + output_name=name, + generate_sources=True, + ) + + +ALL_NAMES = ["counter", "area_i", "area_r", "scale_value", "area"] + + +@pytest.mark.parametrize("name", ["counter", "area"]) +def test_the_declaring_namespace_may_publish_either_kind(name: str, tmp_path: Path): + """Publishing one where it is declared is what a source build already does.""" + entry = _package(tmp_path, home_exports=ALL_NAMES, facade="__all__ = []\n") + + result = _plan(entry, tmp_path, f"declaring_only_{name}") + + assert result.output_dir.is_dir() + + +def test_a_facade_may_publish_the_declaring_namespaces_variable(tmp_path: Path): + """A second publication reads the declaring variable's completed plan.""" + entry = _package( + tmp_path, + home_exports=ALL_NAMES, + facade='from .home import counter\n\n__all__ = ["counter"]\n', + ) + + result = _plan(entry, tmp_path, "both_counter") + + generated = (result.output_dir / "both_counter_wrapper.c").read_text(encoding="utf-8") + assert generated.count("static PyObject * module_get_counter(void) {") == 1 + assert generated.count("static int module_set_counter(PyObject * value_obj) {") == 1 + + +def test_a_facade_may_be_the_only_publication_of_a_declared_variable(tmp_path: Path): + """Withholding the declaring name changes publication, not ownership.""" + entry = _package( + tmp_path, + home_exports=[item for item in ALL_NAMES if item != "counter"], + facade='from .home import counter\n\n__all__ = ["counter"]\n', + ) + + result = _plan(entry, tmp_path, "facade_only_counter") + + generated = (result.output_dir / "facade_only_counter_wrapper.c").read_text(encoding="utf-8") + assert generated.count("static PyObject * module_get_counter(void) {") == 1 + assert generated.count("static int module_set_counter(PyObject * value_obj) {") == 1 + + +@pytest.mark.parametrize("home_exports", [ALL_NAMES, [item for item in ALL_NAMES if item != "area"]]) +def test_a_generic_cannot_be_published_from_a_facade(home_exports: list[str], tmp_path: Path): + """A generic is not one native entity that another namespace can bind. + + The restriction holds whether its declaring namespace also publishes it or + the facade is its only requested publication. + """ + entry = _package( + tmp_path, + home_exports=home_exports, + facade='from .home import area\n\n__all__ = ["area"]\n', + ) + + with pytest.raises(ValueError) as error: + _plan(entry, tmp_path, "facade_area") + + message = str(error.value) + assert "generic 'area' is declared in home and published in facade" in message + assert "publishable only by the namespace declaring it" in message + + +def test_a_procedure_still_reaches_python_through_a_facade(tmp_path: Path): + """A procedure is one object, so a second namespace binds the same one.""" + entry = _package( + tmp_path, + home_exports=ALL_NAMES, + facade='from .home import scale_value\n\n__all__ = ["scale_value"]\n', + ) + + result = _plan(entry, tmp_path, "procedure_facade") + + assert result.output_dir.is_dir() diff --git a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py index 75d7c4de1..0201d68a7 100644 --- a/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py @@ -8,15 +8,43 @@ RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA, SemanticFunction, SemanticModule, + SemanticReexport, SemanticType, SemanticVariable, ) +from prik.policy.exports import complete_python_export_policy from prik.policy.ownership import SetterAction from prik.policy.completion import complete_semantic_policies from tests.fortran._support.ownership_policy import _scalar_type from prik.semantics.models import RESOLVED_MODULE_VARIABLE_POLICY_METADATA +def test_reexport_policy_separates_fortran_accessibility_from_python_publication(): + dependency = SemanticReexport( + "box", + "home", + "box", + "consumer", + entity_kind="derived_type", + declaration_dependency=True, + ) + explicit = SemanticReexport( + "item", + "home", + "box", + "consumer", + entity_kind="derived_type", + declaration_dependency=True, + explicitly_public=True, + ) + module = SemanticModule("consumer", reexports=[dependency, explicit]) + + complete_python_export_policy(module) + + assert dependency.python_exported is False + assert explicit.python_exported is True + + def test_module_variable_initializer_policy_is_complete_before_ir_lowering(): module = SemanticModule( name="state", diff --git a/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py index 14172901b..cdec34766 100644 --- a/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py @@ -6,6 +6,7 @@ import pytest from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, _compile_native_object, _import_from_build_dir, _sole_native_module, @@ -127,3 +128,41 @@ def test_private_native_specific_without_overload_bind_fails_at_build( for target in missing_targets: assert target in error assert "not found in module" in error + + +def test_nested_class_is_bound_on_its_parent_not_the_namespace(tmp_path: Path): + module, _ = _build_inline_pyi_contract_module( + tmp_path, + module_name="nesting", + source_text="""\ +module nesting + type :: outer + integer :: value + end type outer + type :: inner + integer :: value + end type inner +end module nesting +""", + contract_text="""\ +from prik.contracts import Int32 + +class outer: + value: Int32 + + class inner: + def __init__( + self, + *, + value: Int32 = ... + ) -> None: ... + + value: Int32 +""", + ) + + assert not hasattr(module, "inner") + assert module.outer.inner.__qualname__ == "outer.inner" + item = module.outer.inner(value=np.int32(3)) + assert type(item) is module.outer.inner + assert item.value == np.int32(3) diff --git a/tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py b/tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py index 15bcff10c..d4d754258 100644 --- a/tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py +++ b/tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py @@ -9,7 +9,7 @@ import pytest from tests.fortran._support.pyi_fixtures import assert_generated_pyi_package_matches_fixture -from tests.fortran._support.wrapper_build import _compiler +from tests.fortran._support.wrapper_build import _compiler, _import_from_build_dir from prik import build_pyi_extension from prik.compiler.objects import ObjectFile from prik.pipeline.build import _new_compiler @@ -95,3 +95,81 @@ def test_generated_contract_rebuilds_without_native_source_fallback(compiled_con assert not hasattr(module, "module_increment") assert module.contract_math_mod.module_increment(np.int32(4)) == np.int32(5) assert module.external_double(np.int32(4)) == np.int32(8) + + +WILDCARD_SOURCE = """\ +module wild_home + implicit none +contains + subroutine one(value, out) + integer, intent(in) :: value + integer, intent(out) :: out + out = value + 1 + end subroutine one + subroutine two(value, out) + integer, intent(in) :: value + integer, intent(out) :: out + out = value + 2 + end subroutine two +end module wild_home +""" + + +def _wildcard_contracts(tmp_path: Path, consumer: str) -> Path: + """Generate contracts, withhold `two` from the home surface, add a consumer.""" + source = tmp_path / "wild.f90" + source.write_text(WILDCARD_SOURCE, encoding="utf-8") + package = tmp_path / "contracts" + subprocess.run( + [sys.executable, "-m", "prik", "generate", "--pyi", str(source), "--out", str(package)], + capture_output=True, + text=True, + check=True, + ) + home = package / "wild_home.pyi" + home.write_text(home.read_text(encoding="utf-8").replace('["one", "two"]', '["one"]'), encoding="utf-8") + package.joinpath("wild_reader.pyi").write_text(consumer, encoding="utf-8") + package.joinpath("__init__.pyi").write_text( + 'from . import wild_home\nfrom . import wild_reader\n\n__all__ = ["wild_home", "wild_reader"]\n', + encoding="utf-8", + ) + return package / "__init__.pyi" + + +def _build_wildcard(entry: Path, tmp_path: Path, name: str): + result = build_pyi_extension( + entry, + input_compiler=_compiler(), + native_fortran_sources=[str(tmp_path / "wild.f90")], + output_dir=tmp_path / name, + output_name=name, + ) + return _import_from_build_dir(result.module_name, result.output_dir) + + +def test_wildcard_import_reads_only_the_surface_its_dependency_publishes(tmp_path: Path): + """A wildcard takes what a contract publishes, not everything it holds. + + The dependency stated its surface, and a name left off it is not part of + what writing `*` asks for. + """ + entry = _wildcard_contracts(tmp_path, "from .wild_home import *\n") + module = _build_wildcard(entry, tmp_path, "wildcard_star") + + assert hasattr(module.wild_home, "one") + assert not hasattr(module.wild_home, "two") + assert hasattr(module.wild_reader, "one") + assert not hasattr(module.wild_reader, "two") + + +def test_explicit_import_reaches_and_can_republish_a_withheld_name(tmp_path: Path): + """A withheld name stays reachable, because a contract may still need it. + + Expressing a declaration or publishing the name again both require asking + for it, which is exactly what naming it in an import does. + """ + entry = _wildcard_contracts(tmp_path, 'from .wild_home import two\n\n__all__ = ["two"]\n') + module = _build_wildcard(entry, tmp_path, "wildcard_named") + + assert not hasattr(module.wild_home, "two") + assert module.wild_reader.two(np.int32(5)) == np.int32(7) diff --git a/tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py b/tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py index 7700ef28a..2398a87eb 100644 --- a/tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py +++ b/tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py @@ -79,7 +79,8 @@ def test_pyi_parser_preserves_generic_constraints_as_annotation_metadata(): SemanticConstraint("Bounded", [1, 8]), SemanticConstraint("Finite"), ] - assert module.variables[1].name == "native_alias" + assert module.variables[1].name == "alias" + assert module.variables[1].origin.native_name == "native_alias" assert module.variables[1].semantic_type.constraints == [SemanticConstraint("Finite")] emitted = emit_module(SemanticModule(name="constraints", variables=[module.variables[0]])) assert "value: Annotated[Int32, Bounded(1, 8), Finite]" in emitted diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/__init__.pyi index 773570128..94f9d680a 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/__init__.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/__init__.pyi @@ -1,2 +1,4 @@ from . import m1 from . import deep + +__all__ = ["m1", "deep"] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/deep.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/deep.pyi index 1710cf9ce..c156582f4 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/deep.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/deep.pyi @@ -4,3 +4,5 @@ from prik.contracts import Addr, Arg, Int32, native_call def deep_func( value: Int32 ) -> Int32: ... + +__all__ = ["deep_func"] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/m1.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/m1.pyi index 65d26870a..96c089dc2 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/m1.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/m1.pyi @@ -4,3 +4,5 @@ from prik.contracts import Addr, Arg, Int32, native_call def func( value: Int32 ) -> Int32: ... + +__all__ = ["func"] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi index b7dc4f895..27fa1060b 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi @@ -6,3 +6,5 @@ from . import contract_math_mod def external_double( value: Int32 ) -> Int32: ... + +__all__ = ["contract_math_mod", "external_double"] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/contract_math_mod.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/contract_math_mod.pyi index fd60da6b7..f88633754 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/contract_math_mod.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/contract_math_mod.pyi @@ -4,3 +4,5 @@ from prik.contracts import Addr, Arg, Int32, native_call def module_increment( value: Int32 ) -> Int32: ... + +__all__ = ["module_increment"] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi index d2ff52ec9..387080e03 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi @@ -3,3 +3,5 @@ from . import contract_same_name @standalone def external_ping() -> None: ... + +__all__ = ["contract_same_name", "external_ping"] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/contract_same_name.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/contract_same_name.pyi index 7e3962687..d6becc630 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/contract_same_name.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/contract_same_name.pyi @@ -1 +1,3 @@ def module_ping() -> None: ... + +__all__ = ["module_ping"] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi index af3318765..1b5b38dde 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi @@ -8,3 +8,5 @@ def standalone_ping() -> None: ... def standalone_double( value: Int32 ) -> Int32: ... + +__all__ = ["standalone_ping", "standalone_double"] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/modern_math_physics.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/modern_math_physics.pyi index 7c7aeff28..5ff22953b 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/modern_math_physics.pyi +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/modern_math_physics.pyi @@ -55,3 +55,15 @@ def fill_identity3( def normalize_particle( p: particle ) -> None: ... + +__all__ = [ + "particle", + "vector3", + "counter", + "init_particle", + "kinetic_energy", + "scale_vector", + "dot3", + "fill_identity3", + "normalize_particle", +] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py index ee534f976..bd5ef3be4 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py @@ -151,3 +151,63 @@ def test_pyi_python_api_rejects_invalid_projection_before_codegen(tmp_path: Path build_pyi_extension(INVALID_NATIVE_CALL_PYI, native_objects=[native_object], output_dir=tmp_path / "build") assert not list((tmp_path / "build").glob("*_wrapper.*")) + + +STALE_PACKAGE_HOME = ( + "from prik.contracts import Int32\n\ndef calculate(value: Int32) -> Int32: ...\n\n__all__ = [{names}]\n" +) + + +def _stale_package(tmp_path: Path, home: str, reader: str | None = None) -> Path: + package = tmp_path / "pkg" + package.mkdir(parents=True) + package.joinpath("home_mod.pyi").write_text(home, encoding="utf-8") + modules = ["home_mod"] + if reader is not None: + package.joinpath("reader_mod.pyi").write_text(reader, encoding="utf-8") + modules.append("reader_mod") + lines = "".join(f"from . import {name}\n" for name in modules) + stated = ", ".join(f'"{name}"' for name in modules) + package.joinpath("__init__.pyi").write_text(f"{lines}\n__all__ = [{stated}]\n", encoding="utf-8") + return package / "__init__.pyi" + + +def _loaded_modules(entry: Path) -> dict[Path, object]: + """Load one contract package the way a wrapper build loads it.""" + cache = pyi_pipeline._PyiSemanticModuleCache() + paths = tuple(sorted({entry, *_discover_pyi_imports(entry, cache)})) + return dict(zip(paths, cache.paths_to_semantic_modules(paths), strict=True)) + + +def test_all_naming_a_renamed_declaration_is_rejected(tmp_path: Path): + """`__all__` asserts a surface, so a name it states has to exist. + + Renaming a declaration renames what the contract publishes. Dropping the + stale name instead would leave the contract publishing nothing and say so + nowhere, which is far harder to find than a refused build. + """ + entry = _stale_package(tmp_path, STALE_PACKAGE_HOME.format(names='"old_name"')) + + with pytest.raises(ValueError, match=r"__all__ names nothing this contract declares or imports"): + build_pipeline._apply_pyi_python_exports(entry, _loaded_modules(entry)) + + +def test_all_naming_a_stale_imported_alias_is_rejected(tmp_path: Path): + """An imported name that no longer arrives under that alias is stale too.""" + entry = _stale_package( + tmp_path, + STALE_PACKAGE_HOME.format(names='"calculate"'), + 'from .home_mod import calculate as renamed\n\n__all__ = ["calculate"]\n', + ) + + with pytest.raises(ValueError, match=r"__all__ names nothing this contract declares or imports"): + build_pipeline._apply_pyi_python_exports(entry, _loaded_modules(entry)) + + +def test_all_accepts_an_empty_list_and_repeated_names(tmp_path: Path): + """Publishing nothing is a statement; naming one entity twice states it once.""" + entry = _stale_package(tmp_path, STALE_PACKAGE_HOME.format(names="")) + build_pipeline._apply_pyi_python_exports(entry, _loaded_modules(entry)) + + entry = _stale_package(tmp_path / "again", STALE_PACKAGE_HOME.format(names='"calculate", "calculate"')) + build_pipeline._apply_pyi_python_exports(entry, _loaded_modules(entry)) diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py index 4dda9227a..df755ee5a 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py @@ -85,7 +85,8 @@ def test_same_named_module_uses_init_entry_and_keeps_externals_at_root(tmp_path: "from prik.contracts import standalone\n" "from . import contract_same_name\n\n" "@standalone\n" - "def external_ping() -> None: ...\n" + "def external_ping() -> None: ...\n\n" + '__all__ = ["contract_same_name", "external_ping"]\n' ) assert "def module_ping() -> None: ..." in (entry.parent / "contract_same_name.pyi").read_text(encoding="utf-8") @@ -98,7 +99,7 @@ def test_import_graph_generation_writes_entry_and_native_leaves(tmp_path: Path): assert entry == tmp_path / "contracts" / "contract_import_graph" / "__init__.pyi" assert {path.name for path in entry.parent.iterdir()} == {"__init__.pyi", "deep.pyi", "m1.pyi"} - assert entry.read_text(encoding="utf-8") == "from . import m1\nfrom . import deep\n" + assert entry.read_text(encoding="utf-8") == ('from . import m1\nfrom . import deep\n\n__all__ = ["m1", "deep"]\n') def test_multi_module_generation_keeps_each_native_namespace(tmp_path: Path): @@ -125,5 +126,6 @@ def test_multi_module_generation_keeps_each_native_namespace(tmp_path: Path): "contract_right_mod.pyi", } assert (package / "__init__.pyi").read_text(encoding="utf-8") == ( - "from . import contract_left_mod\nfrom . import contract_right_mod\n" + "from . import contract_left_mod\nfrom . import contract_right_mod\n\n" + '__all__ = ["contract_left_mod", "contract_right_mod"]\n' ) diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_publication_round_trip.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_publication_round_trip.py new file mode 100644 index 000000000..d7dc0c972 --- /dev/null +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_publication_round_trip.py @@ -0,0 +1,189 @@ +"""A reloaded contract publishes what its `__all__` states, and nothing else. + +A generated contract writes a private prototype and a private generic into its +body, because annotations and dispatch resolve against them, while leaving both +out of `__all__`. Reading that contract back must not turn either into a public +declaration merely because it is still written there. +""" + +from pathlib import Path + +import pytest + +from prik.pipeline.pyi import pyi_paths_to_semantic_modules +from prik.policy.exports import complete_python_export_policy, contract_names_by_source +from prik.printers.pyi import PyiPrinter +from prik.semantics.models import ( + PYTHON_EXPORTS_METADATA, + SemanticArgument, + SemanticClass, + SemanticFunction, + SemanticModule, + SemanticOrigin, + SemanticType, +) +from prik.semantics.fortran2ir import fortran_file_to_semantic_modules +from prik.parsers.fortran import parse_fortran_file + +SOURCE = """\ +module v_mod + implicit none + private + + abstract interface + subroutine cb() + end subroutine + end interface + + interface hidden_generic + module procedure hidden_one + end interface + + public :: run +contains + subroutine run(f) + procedure(cb) :: f + call f() + end subroutine run + + subroutine hidden_one(a) + integer, intent(in) :: a + print *, a + end subroutine hidden_one +end module v_mod +""" + + +@pytest.fixture +def generated_contract(tmp_path: Path) -> Path: + """Write the source-derived contract for the shared module.""" + source = tmp_path / "v.f90" + source.write_text(SOURCE, encoding="utf-8") + module = fortran_file_to_semantic_modules(parse_fortran_file(SOURCE, filename=str(source)))[0] + contract = tmp_path / "v_mod.pyi" + contract.write_text(PyiPrinter().emit(module), encoding="utf-8") + return contract + + +def test_a_contract_states_the_surface_without_publishing_its_helpers(generated_contract: Path): + """The prototype and the generic are written but not exported.""" + text = generated_contract.read_text(encoding="utf-8") + + assert "def cb() -> None: ..." in text + assert "def hidden_generic(" in text + assert '__all__ = ["run"]' in text + + +def test_reloading_a_contract_does_not_publish_what_all_leaves_out(generated_contract: Path): + """`__all__` is the stated surface, so no export is completed outside it. + + A prototype and a generic read back public by default, and the declarations + stay written because annotations and imports resolve against them. Completed + export policy is what decides publication, and it names only the surface the + contract stated. + """ + reloaded = pyi_paths_to_semantic_modules([generated_contract])[0] + complete_python_export_policy(reloaded) + + assert reloaded.exported_names == ["run"] + assert [item.name for item in reloaded.prototypes] == ["cb"] + assert [item.name for item in reloaded.overload_sets] == ["hidden_generic"] + + # A generic owns its decision the way every other declaration does. + published = { + str(owner.name): owner.metadata.get(PYTHON_EXPORTS_METADATA) + for owner in (*reloaded.functions, *reloaded.overload_sets) + } + assert published["run"] == [{"namespace": (), "name": "run"}] + # Completion records the decision to publish nowhere, not an absence. + assert published["hidden_generic"] == [] + assert published["hidden_one"] == [] + + +def test_a_contract_read_back_and_written_again_states_the_same_surface(generated_contract: Path): + """Publication survives the round trip rather than drifting with each pass.""" + original = generated_contract.read_text(encoding="utf-8") + reloaded = pyi_paths_to_semantic_modules([generated_contract])[0] + + assert PyiPrinter().emit(reloaded).strip() == original.strip() + + +def test_a_withheld_declaration_is_still_reachable_for_naming(generated_contract: Path): + """Leaving a name out of `__all__` withholds publication, not reachability. + + Another module's annotation may still name the prototype, so the spelling + this contract writes it under stays readable; what `__all__` decides is + whether the module publishes it, which completed export policy settles. + """ + reloaded = pyi_paths_to_semantic_modules([generated_contract])[0] + complete_python_export_policy(reloaded) + + assert contract_names_by_source(reloaded)["cb"] == "cb" + + +def test_a_stated_name_selects_a_declaration_by_exact_spelling(tmp_path: Path): + """A contract is Python, where `Foo` and `foo` are different names. + + A list naming `Foo` beside a declaration written `foo` names something the + module does not define, so it publishes nothing. + """ + contract = tmp_path / "cased_mod.pyi" + contract.write_text( + 'from prik.contracts import Int32\n\ndef foo(\n a: Int32\n) -> None: ...\n\n__all__ = ["Foo"]\n', + encoding="utf-8", + ) + module = pyi_paths_to_semantic_modules([contract])[0] + + complete_python_export_policy(module) + + assert [item.name for item in module.functions] == ["foo"] + assert module.functions[0].metadata.get(PYTHON_EXPORTS_METADATA) == [] + + +def test_a_declaration_already_projected_to_nothing_keeps_that_decision(): + """An empty export list is a decision taken, not a missing one. + + A stage that has projected a declaration to no Python namespace records an + empty list. Reading that as "nothing decided yet" and substituting a + default publication would reverse it. + """ + withheld = SemanticFunction(name="withheld", native_name="withheld") + withheld.metadata[PYTHON_EXPORTS_METADATA] = [] + fresh = SemanticFunction(name="fresh", native_name="fresh") + module = SemanticModule( + name="mod", + functions=[withheld, fresh], + origin=SemanticOrigin(source_language="fortran", source_kind="module"), + ) + + complete_python_export_policy(module) + + assert withheld.metadata[PYTHON_EXPORTS_METADATA] == [] + # A declaration no stage has projected still publishes itself. + assert fresh.metadata[PYTHON_EXPORTS_METADATA] == [{"namespace": (), "name": "fresh"}] + + +def test_a_withheld_class_keeps_one_contract_identity_for_its_annotations(): + """Publication does not own the spelling needed by contract references.""" + origin = SemanticOrigin(source_language="fortran", native_scope="hidden_types") + hidden = SemanticClass(name="box_t", origin=origin) + hidden.metadata[PYTHON_EXPORTS_METADATA] = [] + inspect = SemanticFunction( + name="inspect_box", + arguments=[SemanticArgument("value", SemanticType("box_t"))], + origin=origin, + ) + module = SemanticModule( + name="hidden_types", + classes=[hidden], + functions=[inspect], + exported_names=["inspect_box"], + origin=origin, + ) + + complete_python_export_policy(module) + contract = PyiPrinter(normalize_public_names=True).emit(module) + + assert "class Box_T:" in contract + assert "value: Box_T" in contract + assert '__all__ = ["inspect_box"]' in contract diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_conversion_smoke.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_conversion_smoke.py index adc55e585..d41226138 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_conversion_smoke.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_conversion_smoke.py @@ -3,6 +3,7 @@ import pytest from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik.policy.exports import complete_python_export_policy from prik.printers import emit_module from tests.fortran._support.fixture_outputs import parse_fixture @@ -24,4 +25,5 @@ def test_pyi_printer_conversion_smoke(fixture: Path): for module in parsed.modules: semantic_module = fortran_module_to_semantic_module(module) + complete_python_export_policy(semantic_module) emit_module(semantic_module) diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py index eb989adc3..29477554a 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py @@ -1,8 +1,11 @@ """Tests split by stable ownership concept from `test_imports_and_packages.py`.""" +import json import pytest import prik.pipeline.pyi as pyi_pipeline from prik.parsers.fortran import parse_fortran_file as parse_fortran_source +from prik.policy.contract_imports import complete_contract_imports +from prik.policy.exports import contract_name_for_source from prik.printers import ( PyiPrinter, emit_module, @@ -12,6 +15,8 @@ opaque_dependency_modules, pyi_text_to_semantic_module as _parse_pyi_text, ) +from prik.policy.exports import complete_python_export_policy +from prik.semantics import fortran_file_to_semantic_modules from prik.semantics.fortran2ir import fortran_module_to_semantic_module from prik.semantics.models import ( SemanticArgument, @@ -21,6 +26,7 @@ SemanticField, SemanticFunction, SemanticImport, + SemanticImportItem, SemanticModule, SemanticOrigin, SemanticStorageContract, @@ -71,8 +77,9 @@ def test_fortran_generated_contracts_reserve_colliding_public_names_by_namespace ], origin=origin, ) + complete_python_export_policy(module) - code = emit_module(module, normalize_fortran_public_names=True) + code = emit_module(module, normalize_public_names=True) assert 'lambda_: Annotated[Int32, SourceName("lambda")]' in code assert 'lambda__2: Annotated[Int32, SourceName("lambda_")]' in code @@ -82,7 +89,7 @@ def test_fortran_generated_contracts_reserve_colliding_public_names_by_namespace def test_pyi_emission_context_isolates_modules_and_shares_nested_imports(): - printer = PyiPrinter(normalize_fortran_public_names=True) + printer = PyiPrinter(normalize_public_names=True) first = printer._emission_context(SemanticModule(name="first")) second = printer._emission_context(SemanticModule(name="second")) nested = first.inside_class("record_t") @@ -97,6 +104,15 @@ def test_pyi_emission_context_isolates_modules_and_shares_nested_imports(): assert second.contract_import() == "" +def test_printing_loaded_contract_preserves_absolute_support_imports(): + module = _parse_pyi_text( + "from typing import Any\nfrom prik.contracts import Int32\n\ndef identity(value: Int32) -> Int32: ...\n", + module_name="identity", + ) + + assert "from typing import Any" in emit_module(module) + + def test_printer_validation_and_opaque_dependency_edge_cases(): printer = PyiPrinter() @@ -118,10 +134,10 @@ def test_printer_validation_and_opaque_dependency_edge_cases(): } }, ) - assert ( - printer._effective_imports(SemanticModule(name="api", variables=[SemanticArgument("value", malformed_import)])) - == [] - ) + malformed_module = SemanticModule(name="api", variables=[SemanticVariable("value", malformed_import)]) + complete_python_export_policy(malformed_module) + complete_contract_imports([malformed_module]) + assert malformed_module.imports == [] invalid_opaque_ref = SemanticType( "external_type", @@ -250,6 +266,34 @@ def test_emit_module_stubs_honors_available_opaque_dependency_modules(): assert set(stubs) == {"api"} +def test_emit_module_stubs_uses_available_module_public_names_in_imports(): + origin = SemanticOrigin(source_language="fortran") + available_type = SemanticModule( + name="types", + classes=[SemanticClass(name="point_t", origin=origin)], + origin=origin, + ) + consumer = SemanticModule( + name="consumer", + imports=[ + SemanticImport( + module="types", + items=[SemanticImportItem(source="point_t")], + ) + ], + origin=origin, + ) + + stubs = emit_module_stubs( + consumer, + available_modules=[available_type, consumer], + normalize_public_names=True, + ) + + assert set(stubs) == {"consumer"} + assert "from .types import Point_T as point_t" in stubs["consumer"] + + def test_emit_omits_resolved_source_kind_imports(): source = """ module user_mod @@ -283,7 +327,7 @@ def test_emit_import_renames(): code = generate_pyi(source) - assert "from list_input import delete_input_list as delete_input" in code + assert "from .list_input import delete_input_list as delete_input" in code def test_emit_imported_derived_type_reference_without_reexporting_class(): @@ -302,12 +346,12 @@ def test_emit_imported_derived_type_reference_without_reexporting_class(): stubs = emit_module_stubs(module) code = stubs["physics"] - assert "from types_mod import particle" in code + assert "from .types_mod import particle" in code assert "from . import types_mod" not in code assert "p: particle" in code assert "Addr(particle)" not in code assert "class particle" not in code - assert stubs["types_mod"].endswith("class particle(Opaque):\n pass") + assert stubs["types_mod"].endswith('class particle(Opaque):\n pass\n\n__all__ = ["particle"]') def test_emit_procedure_local_imported_derived_types_as_qualified_module_refs(): @@ -328,8 +372,7 @@ def test_emit_procedure_local_imported_derived_types_as_qualified_module_refs(): """ ) - module = fortran_module_to_semantic_module(parsed) - code = emit_module(module) + code = emit_module_stubs(fortran_module_to_semantic_module(parsed))["physics"] assert "from . import a_types, b_types" in code assert "p: a_types.state" in code @@ -358,9 +401,10 @@ def test_emit_procedure_local_import_namespace_collision_fails_without_alias(): ) module = fortran_module_to_semantic_module(parsed) + complete_python_export_policy(module) - with pytest.raises(ValueError, match="Procedure-local Fortran import namespace collides"): - emit_module(module) + with pytest.raises(ValueError, match="cannot bind 'a_types'"): + complete_contract_imports([module]) def test_emit_procedure_local_import_namespace_collision_with_synthetic_import_fails(): @@ -397,8 +441,10 @@ def test_emit_procedure_local_import_namespace_collision_with_synthetic_import_f ], ) - with pytest.raises(ValueError, match="Procedure-local Fortran import namespace collides"): - emit_module(module) + complete_python_export_policy(module) + + with pytest.raises(ValueError, match="cannot bind 'a_types'"): + complete_contract_imports([module]) def test_emit_bare_use_adds_import_for_opaque_dependency_type(): @@ -415,20 +461,10 @@ def test_emit_bare_use_adds_import_for_opaque_dependency_type(): ) stubs = emit_module_stubs(fortran_module_to_semantic_module(parsed)) - assert "import types_mod" in stubs["physics"] - assert "from types_mod import particle" in stubs["physics"] - assert stubs["types_mod"].endswith("class particle(Opaque):\n pass") - - -def test_emit_omits_structured_source_kind_import_without_items(): - module = SemanticModule( - name="imports", - imports=[SemanticImport(module="iso_c_binding")], - ) - - code = emit_module(module) - - assert code == "" + # The contract binds the type a declaration names, not the `use` itself. + assert "import types_mod" not in stubs["physics"].splitlines() + assert "from .types_mod import particle" in stubs["physics"] + assert stubs["types_mod"].endswith('class particle(Opaque):\n pass\n\n__all__ = ["particle"]') def test_emit_module_aliases_contract_import_when_user_name_collides(): @@ -505,3 +541,609 @@ def test_emit_module_aliases_standalone_only_for_actual_name_collisions(): assert "@prik_standalone\ndef standalone() -> Int32: ..." in colliding assert "standalone as prik_standalone_2" in twice_colliding.splitlines()[0] assert "@prik_standalone_2\ndef standalone() -> Int32: ..." in twice_colliding + + +def test_generated_contract_imports_a_name_under_the_spelling_its_definition_uses(): + """An import binds the name the module it reads from actually defines. + + A source-derived contract writes its declarations under Python names, so a + Fortran entity spelled in capitals is declared lower case. An import asking + for the source spelling names nothing the dependency contract defines, and + loading the package back fails on it. + """ + consts = parse_fortran_source(""" +module consts_mod +implicit none +integer, parameter :: IK = 4 +end module consts_mod +""") + infos = parse_fortran_source(""" +module infos_mod +use consts_mod, only : IK +implicit none +end module infos_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(consts), fortran_module_to_semantic_module(infos)], + normalize_public_names=True, + ) + + assert "ik: Final[Int32]" in stubs["consts_mod"] + assert "from .consts_mod import ik" in stubs["infos_mod"] + assert "import IK" not in stubs["infos_mod"] + + +def test_generated_contract_renames_an_imported_name_under_both_spellings(): + """A renamed import binds the defined name to this contract's own name.""" + consts = parse_fortran_source(""" +module consts_mod +implicit none +integer, parameter :: IK = 4 +end module consts_mod +""") + renaming = parse_fortran_source(""" +module renaming_mod +use consts_mod, only : MY_IK => IK +implicit none +end module renaming_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(consts), fortran_module_to_semantic_module(renaming)], + normalize_public_names=True, + ) + + assert "from .consts_mod import ik as my_ik" in stubs["renaming_mod"] + + +def test_generated_contract_imports_a_prototype_under_its_declared_spelling(): + """A prototype keeps its spelling, so the import that binds it keeps it too. + + A contract writes a prototype under the name its own declaration states, and + an annotation naming that prototype is written the same way, so normalizing + the import would bind a name no declaration defines. + """ + declares = parse_fortran_source(""" +module pintrf_mod +implicit none +private +public :: OBJ +abstract interface +subroutine OBJ(x) +implicit none +real(8), intent(in) :: x(:) +end subroutine OBJ +end interface +end module pintrf_mod +""") + solver = parse_fortran_source(""" +module solver_mod +use pintrf_mod, only : OBJ +implicit none +contains +subroutine solve(calfun, x) +procedure(OBJ) :: calfun +real(8), intent(inout) :: x(:) +end subroutine solve +end module solver_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(declares), fortran_module_to_semantic_module(solver)], + normalize_public_names=True, + ) + + assert "def OBJ(" in stubs["pintrf_mod"] + assert "from .pintrf_mod import OBJ" in stubs["solver_mod"] + assert "calfun: OBJ" in stubs["solver_mod"] + + +def test_fortran_contract_records_no_source_name_for_a_case_only_python_name(): + """Writing a Fortran entity in lower case renames nothing worth recording. + + Fortran names entities without regard to case, so a capitalized source + spelling and the lower-case Python name are the same entity and the + generated Fortran reaches it either way. + """ + source = """ +module consts_mod +implicit none +integer, parameter :: IK = 4 +contains +subroutine SCALE_VALUE(x) +integer, intent(in) :: x +end subroutine SCALE_VALUE +end module consts_mod +""" + + module = fortran_module_to_semantic_module(parse_fortran_source(source)) + complete_python_export_policy(module) + code = emit_module(module, normalize_public_names=True) + + assert "ik: Final[Int32]" in code + assert "def scale_value(" in code + assert "SourceName" not in code + assert "@bind(" not in code + + +def test_fortran_contract_records_a_source_name_python_cannot_spell(): + """A name Python cannot hold as written keeps the spelling it came from.""" + source = """ +module naming_mod +implicit none +integer :: lambda +integer :: LAMBDA_ +contains +subroutine ASSERT(x) +integer, intent(in) :: x +end subroutine ASSERT +end module naming_mod +""" + + module = fortran_module_to_semantic_module(parse_fortran_source(source)) + complete_python_export_policy(module) + code = emit_module(module, normalize_public_names=True) + + assert 'lambda_: Annotated[Int32, SourceName("lambda")]' in code + assert 'lambda__2: Annotated[Int32, SourceName("LAMBDA_")]' in code + assert '@bind("ASSERT")\n@native_call([Addr(Arg(0))])\ndef assert_(' in code + + +def test_non_fortran_declaration_compares_its_native_spelling_exactly(): + """Every other source language names its entities exactly, case included.""" + origin = SemanticOrigin(source_language="c", native_scope="c_mod") + module = SemanticModule( + name="c_mod", + functions=[ + SemanticFunction( + "scale_value", + native_name="ScaleValue", + return_type=SemanticType("Int32"), + origin=origin, + ) + ], + origin=origin, + ) + complete_python_export_policy(module) + + code = emit_module(module, normalize_public_names=True) + + assert '@bind("ScaleValue")' in code + + +def test_generated_contract_binds_a_class_whose_python_name_renames_its_type(): + """A renamed class states its native type so the contract reads back.""" + origin = SemanticOrigin(source_language="fortran", native_scope="shapes_mod") + module = SemanticModule( + name="shapes_mod", + classes=[ + SemanticClass( + name="PointType", + native_name="POINT_T", + fields=[SemanticField("x", SemanticType("Float64"))], + origin=origin, + ) + ], + origin=origin, + ) + complete_python_export_policy(module) + + code = emit_module(module, normalize_public_names=True) + + assert '@bind("POINT_T")\nclass Pointtype:' in code + + +def test_generated_contract_omits_a_class_bind_for_a_case_only_python_name(): + """A class named without regard to case states no separate native type.""" + origin = SemanticOrigin(source_language="fortran", native_scope="shapes_mod") + module = SemanticModule( + name="shapes_mod", + classes=[ + SemanticClass( + name="point_t", + native_name="POINT_T", + fields=[SemanticField("x", SemanticType("Float64"))], + origin=origin, + ) + ], + origin=origin, + ) + complete_python_export_policy(module) + + code = emit_module(module, normalize_public_names=True) + + assert "class Point_T:" in code + assert "@bind(" not in code + + +def test_prototype_spelling_is_kept_only_for_the_module_that_declares_one(): + """A prototype identity names its module, not a spelling used anywhere. + + One module may declare a prototype while another spells an ordinary + declaration the same way. The second follows Python naming, so an import + reading from it asks for the name that module actually defines. + """ + callbacks = parse_fortran_source(""" +module callback_mod +implicit none +private +public :: OBJ +abstract interface +subroutine OBJ(x) +implicit none +real(8), intent(in) :: x +end subroutine OBJ +end interface +end module callback_mod +""") + values = parse_fortran_source(""" +module values_mod +implicit none +integer, parameter :: OBJ = 1 +end module values_mod +""") + consumer = parse_fortran_source(""" +module consumer_mod +use values_mod, only : OBJ +implicit none +end module consumer_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(item) for item in (callbacks, values, consumer)], + normalize_public_names=True, + ) + + assert "def OBJ(" in stubs["callback_mod"] + assert "obj: Final[Int32]" in stubs["values_mod"] + assert "from .values_mod import obj" in stubs["consumer_mod"] + assert "import OBJ" not in stubs["consumer_mod"] + + +def test_prototype_import_uses_the_declared_spelling_whatever_case_names_it(): + """Fortran reaches a prototype without regard to case; a contract does not. + + A module may write `use callback_mod, only : obj` for a prototype declared + as `OBJ`, and the annotation then names it that way. The import binds the + declared spelling under the name this contract uses. + """ + callbacks = parse_fortran_source(""" +module callback_mod +implicit none +public :: OBJ +abstract interface +subroutine OBJ(x) +implicit none +real(8), intent(in) :: x +end subroutine OBJ +end interface +end module callback_mod +""") + user = parse_fortran_source(""" +module user_mod +use callback_mod, only : obj +implicit none +contains +subroutine run(f, v) +procedure(obj) :: f +real(8), intent(in) :: v +end subroutine run +end module user_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(item) for item in (callbacks, user)], + normalize_public_names=True, + ) + + assert "def OBJ(" in stubs["callback_mod"] + assert "from .callback_mod import OBJ as obj" in stubs["user_mod"] + assert "f: obj" in stubs["user_mod"] + + +def test_import_binds_the_name_a_collision_made_the_declaring_contract_use(): + """A collision moves a name aside, and the import follows it there.""" + home = parse_fortran_source(""" +module collide_home +implicit none +contains +subroutine lambda(x) +integer, intent(inout) :: x +end subroutine lambda +subroutine lambda_(x) +integer, intent(inout) :: x +end subroutine lambda_ +end module collide_home +""") + user = parse_fortran_source(""" +module collide_user +use collide_home, only : lambda_ +implicit none +private +public :: lambda_ +end module collide_user +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(item) for item in (home, user)], + normalize_public_names=True, + ) + + assert "def lambda__2(" in stubs["collide_home"] + assert "from .collide_home import lambda__2" in stubs["collide_user"] + assert '__all__ = ["lambda_"]' in stubs["collide_user"] + + +def test_generated_contract_states_the_names_its_source_publishes(): + """A contract names its whole public surface, not only its re-exports. + + An import cannot say whether a name is needed to express a declaration or + meant to be published, because a rename reads the same either way. The list + settles it, and is written to be edited. + """ + home = parse_fortran_source(""" +module surface_home +implicit none +type :: box +integer :: value +end type box +contains +subroutine scale_value(x) +integer, intent(inout) :: x +end subroutine scale_value +end module surface_home +""") + facade = parse_fortran_source(""" +module surface_facade +use surface_home, only : scale_value +implicit none +private +public :: scale_value +end module surface_facade +""") + consumer = parse_fortran_source(""" +module surface_consumer +use surface_home, only : crate => box +implicit none +contains +integer function crate_value(item) result(out) +type(crate), intent(in) :: item +out = item%value +end function crate_value +end module surface_consumer +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(item) for item in (home, facade, consumer)], + normalize_public_names=True, + ) + + # The publishing module names the import; the consuming one does not. + assert stubs["surface_facade"].rstrip().endswith('__all__ = ["scale_value"]') + assert stubs["surface_consumer"].rstrip().endswith('__all__ = ["crate_value"]') + assert "from .surface_home import Box as Crate" in stubs["surface_consumer"] + assert '__all__ = ["Box", "scale_value"]' in stubs["surface_home"] + + +def test_generated_contract_honors_used_module_accessibility_routes(): + """A module-name access statement controls names carried through that route.""" + modules = fortran_file_to_semantic_modules( + parse_fortran_source(""" +module route_home +integer :: x +end module route_home + +module route_hidden +use route_home +private :: route_home +end module route_hidden + +module route_visible +use route_home +private +public :: route_home +end module route_visible +""") + ) + + stubs = emit_module_stubs(modules, normalize_public_names=True) + + # The route carries nothing a declaration uses or the module publishes, so + # its contract has nothing to write. + assert stubs["route_hidden"] == "" + assert "from .route_home import x" in stubs["route_visible"] + assert stubs["route_visible"].rstrip().endswith('__all__ = ["x"]') + + +def test_a_published_intrinsic_name_states_no_contract_import(): + """Publishing a name from an intrinsic module publishes nothing here. + + A module may name an intrinsic constant in its `public` statement, and the + module it came from has no contract to read it from. The declaration was + never found, so there is nothing to import and nothing to publish. + """ + source = """ +module kinds_mod +use iso_fortran_env, only : REAL64, INT32 +implicit none +private +public :: REAL64, INT32 +public :: rate +real(REAL64), parameter :: rate = 2.0d0 +end module kinds_mod +""" + + module = fortran_module_to_semantic_module(parse_fortran_source(source)) + complete_python_export_policy(module) + code = emit_module(module, normalize_public_names=True) + + assert [reexport.origin_module for reexport in module.reexports] == ["iso_fortran_env", "iso_fortran_env"] + assert "iso_fortran_env" not in code + assert '__all__ = ["rate"]' in code + + +def test_generated_contract_publishes_a_module_variable_reexport(): + """A re-exporting contract names the declaring variable as public state.""" + parsed = parse_fortran_source(""" +module state_home +implicit none +integer, save :: counter = 7 +contains +subroutine bump() +counter = counter + 1 +end subroutine bump +end module state_home + +module state_facade +use state_home, only : counter, bump +implicit none +private +public :: counter, bump +end module state_facade +""") + + modules = fortran_file_to_semantic_modules(parsed) + facade = next(module for module in modules if module.name == "state_facade") + stubs = emit_module_stubs(modules, normalize_public_names=True) + + assert sorted((item.local_name, item.entity_kind) for item in facade.reexports) == [ + ("bump", "procedure"), + ("counter", "variable"), + ] + assert "from .state_home import counter, bump" in stubs["state_facade"] + assert stubs["state_facade"].rstrip().endswith('__all__ = ["counter", "bump"]') + + +def test_two_spellings_a_case_sensitive_source_keep_distinct_contract_names(): + """A contract records each source name as written, so neither displaces the other. + + Keying contract spellings by a folded source name loses one of a pair only + a case-sensitive source distinguishes, and an importer then binds whichever + was recorded first. + """ + completed = {"Foo": "Foo", "foo": "foo", "SCALE": "scale"} + + assert contract_name_for_source(completed, "Foo") == "Foo" + assert contract_name_for_source(completed, "foo") == "foo" + # A case-insensitive source still reaches its name under any spelling. + assert contract_name_for_source(completed, "scale") == "scale" + assert contract_name_for_source(completed, "Scale") == "scale" + assert contract_name_for_source(completed, "missing") is None + # `FOO` could mean either declaration, and which one a folded lookup found + # would depend on the order they were recorded in, so it names neither. + assert contract_name_for_source(completed, "FOO") is None + assert contract_name_for_source({"foo": "foo", "Foo": "Foo"}, "FOO") is None + + +SHAPES_SOURCE = """ +module shapes +implicit none +type :: point + integer :: x +end type point +end module shapes +""" + + +@pytest.mark.parametrize( + ("association", "published", "spelling"), + [ + ("point", True, "Point"), + ("MyPoint => point", True, "Mypoint"), + ("MyPoint => point", False, "Mypoint"), + ], + ids=["published", "renamed-published", "renamed-dependency"], +) +def test_an_imported_type_is_spelled_one_way_throughout_its_contract(association, published, spelling): + """The import, the annotation, and `__all__` write the name the module publishes. + + Naming completion spelled a published type as a class for `__all__` while + the annotation wrote the `use` statement's spelling, so one of them always + named something the contract never bound. A type is spelled as a class + whether or not the module publishes it, and every place reads that name. + """ + local = association.split(" => ")[0] + user = parse_fortran_source(f""" +module user_mod +use shapes, only : {association} +implicit none +private +public :: {f"{local}, " if published else ""}move +contains +subroutine move(p) +type({local}), intent(inout) :: p +end subroutine move +end module user_mod +""") + + stubs = emit_module_stubs( + [ + fortran_module_to_semantic_module(parse_fortran_source(SHAPES_SOURCE)), + fortran_module_to_semantic_module(user), + ], + normalize_public_names=True, + ) + contract = stubs["user_mod"] + + bound = "Point" if spelling == "Point" else f"Point as {spelling}" + assert f"from .shapes import {bound}\n" in contract + assert f"p: {spelling}\n" in contract + expected_all = ["move", spelling] if published else ["move"] + assert contract.rstrip().endswith(f"__all__ = {json.dumps(expected_all)}") + + +def test_a_declaration_expression_calls_its_callee_by_the_name_the_contract_binds(tmp_path): + """The call in a shape and the import binding its callee are one spelling. + + `lambda` is a Python keyword and `lambda_` then collides with its escaped + spelling, so the helpers contract writes `lambda_` and `lambda__2`. The + expression kept the Fortran spelling while the import bound the completed + one, leaving a call that was unbound or not Python at all. + """ + helpers = parse_fortran_source(""" +module helpers +implicit none +contains +pure integer function lambda(n) + integer, intent(in) :: n + lambda = n +end function lambda +pure integer function lambda_(n) + integer, intent(in) :: n + lambda_ = n + 1 +end function lambda_ +end module helpers +""") + user = parse_fortran_source(""" +module user_mod +use helpers, only : lambda, lambda_ +implicit none +contains +subroutine fill(n, x, y) + integer, intent(in) :: n + real(8), intent(out) :: x(lambda(n)) + real(8), intent(out) :: y(2*lambda_(n) + n) +end subroutine fill +end module user_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(item) for item in (helpers, user)], + normalize_public_names=True, + ) + contract = stubs["user_mod"] + + assert "from .helpers import lambda_, lambda__2\n" in contract + assert "x: Float64[lambda_(n)]" in contract + assert "y: Float64[2 * lambda__2(n) + n]" in contract + # Read back as a package, each call reaches the native function it names. + for name, text in stubs.items(): + (tmp_path / f"{name}.pyi").write_text(text, encoding="utf-8") + reloaded = {module.name: module for module in pyi_pipeline.pyi_paths_to_semantic_modules(tmp_path)} + assert [ + (reference.name, reference.native_scope, reference.native_name) + for argument in reloaded["user_mod"].functions[0].arguments[1:] + for axis in argument.semantic_type.storage.array.expression_callables + for reference in axis + ] == [("lambda_", "helpers", "lambda"), ("lambda__2", "helpers", "lambda_")] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py index d3ea0d06b..84b292a8d 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py @@ -2,6 +2,7 @@ import pytest from prik.parsers.fortran import parse_fortran_file as parse_fortran_source +from prik.policy.exports import complete_python_export_policy from prik.printers import ( PyiPrinter, emit_module, @@ -55,7 +56,12 @@ def test_emit_basic_scalar_function(): assert ") -> Float64: ..." in code -def test_fortran_generated_contracts_emit_python_name_and_bind_original_name(): +def test_fortran_generated_contracts_emit_python_name_without_binding_the_same_name(): + """A capitalized Fortran procedure is written lower case and binds nothing. + + Fortran reaches a procedure without regard to case, so the lower-case + Python name already names it and no original spelling has to be recorded. + """ module = SemanticModule( name="math_mod", functions=[ @@ -69,10 +75,12 @@ def test_fortran_generated_contracts_emit_python_name_and_bind_original_name(): ], origin=SemanticOrigin(source_language="fortran", source_kind="module"), ) + complete_python_export_policy(module) - code = emit_module(module, normalize_fortran_public_names=True) + code = emit_module(module, normalize_public_names=True) - assert '@bind("SQUARE_R4")\ndef square_r4(' in code + assert "def square_r4(" in code + assert "@bind(" not in code def test_emit_rejects_unknown_semantic_type(): diff --git a/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py index e6ba0bc49..74b3b17fa 100644 --- a/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py +++ b/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py @@ -872,7 +872,7 @@ def test_convert_pyi_to_ir_handles_pointer_and_array_storage_variants(): assert rank_any.storage.array.category == "assumed_rank" assert rank_any.storage.array.source_shape == [".."] assert rank_any.rank == 1 - assert strided.shape == ["0:n:Strided"] + assert strided.shape == ["0:n:"] assert strided.storage.array.contiguous is False assert computed.shape == ["xl.size"] assert bounded.constraints == [ diff --git a/tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py index 13cae8693..3cf50ff57 100644 --- a/tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py +++ b/tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py @@ -5,6 +5,8 @@ given, strategies as st, ) +from prik.policy.contract_imports import complete_contract_imports +from prik.policy.exports import complete_python_export_policy from prik.printers import emit_module from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text from prik.semantics.models import ( @@ -71,9 +73,11 @@ def import_lines(names): for index, type_name in enumerate(names) ], ) + complete_python_export_policy(module) + complete_contract_imports([module]) return [line for line in emit_module(module).splitlines() if line.startswith("from ")] - expected = [f"from types import {', '.join(sorted(type_names))}"] + expected = [f"from .types import {', '.join(sorted(type_names))}"] assert import_lines(type_names) == expected assert import_lines(reversed(type_names)) == expected diff --git a/tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py index 8a3cbba51..b002003de 100644 --- a/tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py +++ b/tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py @@ -194,7 +194,10 @@ def f() -> tuple[F64, Gives["y", F64]]: ... module_name="edited", ) - assert module.variables[0].name == "native_alias" + # The declared name stays the Python name; SourceName states the native + # entity it reaches, as bind does for a callable. + assert module.variables[0].name == "alias" + assert module.variables[0].origin.native_name == "native_alias" assert module.variables[0].semantic_type.shape == ["1:n"] assert module.functions[0].return_type is not None assert module.functions[0].return_type.name == "Float64" @@ -242,7 +245,7 @@ def apply( assert args["A"].source_shape == ["LDA", "N"] assert args["A"].lower_bounds == [None, None] assert args["A"].upper_bounds == [None, None] - assert args["work"].shape == ["::Strided"] + assert args["work"].shape == ["::"] assert args["work"].axes == ["strided"] assert args["work"].contiguous is False assert args["work"].source_shape == [] @@ -252,21 +255,31 @@ def apply( assert args["scratch"].source_shape == [] -def test_convert_pyi_to_ir_accepts_explicit_strided_marker_for_edited_contracts(): +def test_convert_pyi_to_ir_reads_a_strided_axis_from_its_empty_step(): + """An empty step marks a strided axis; a bounded axis keeps its bounds.""" module = parse_pyi_text( """ -current: Float64[::] -explicit: Float64[::Strided] +unbounded: Float64[::] bounded: Float64[0:n:] -explicit_bounded: Float64[0:n:Strided] """, module_name="strided_axes", ) arrays = [variable.semantic_type.storage.array for variable in module.variables] - assert [array.shape for array in arrays] == [["::Strided"], ["::Strided"], ["0:n:Strided"], ["0:n:Strided"]] - assert [array.axes for array in arrays] == [["strided"], ["strided"], ["strided"], ["strided"]] - assert [array.contiguous for array in arrays] == [False, False, False, False] + assert [array.shape for array in arrays] == [["::"], ["0:n:"]] + assert [array.axes for array in arrays] == [["strided"], ["strided"]] + assert [array.contiguous for array in arrays] == [False, False] + + +@pytest.mark.parametrize("dimension", ["Float64[::Strided]", "Float64[0:n:Strided]", "Float64[::2]"]) +def test_convert_pyi_to_ir_rejects_a_dimension_step(dimension: str): + """A dimension carries bounds only, so the step position spells nothing. + + `T[::]` already says strided, so the longer explicit form it replaced is + refused rather than kept as a second way to write the same contract. + """ + with pytest.raises(ValueError, match="not part of the contract grammar"): + parse_pyi_text(f"x: {dimension}\n", module_name="rejected_step") def test_convert_pyi_to_ir_uses_fortran_native_array_defaults(): @@ -585,3 +598,44 @@ def test_native_contract_structurally_accepts_declared_type_and_constraint_edits assert native_contract_issues(parse_pyi_text(constrained, module_name="solver_mod")) == [] assert native_contract_issues(parse_pyi_text(changed_abi, module_name="solver_mod")) == [] + + +def test_source_name_binds_a_native_entity_without_taking_the_declared_name(): + """`SourceName` states what a declaration reaches, like `bind` on a callable. + + A contract is edited to give an entity the name Python should call it, and + that name has to survive. Reading the source spelling as the declaration's + own name discards the edit and exports the native spelling instead. + """ + module = pyi_text_to_semantic_module( + """ +from prik.contracts import Annotated, Final, Int32, SourceName + +tally: Annotated[Int32, SourceName("COUNTER")] + +limit: Final[Annotated[Int32, SourceName("MAXFUN")]] +""", + module_name="edited", + ) + + assert [(item.name, item.origin.native_name) for item in module.variables] == [ + ("tally", "COUNTER"), + ("limit", "MAXFUN"), + ] + assert [constraint.name for constraint in module.variables[1].semantic_type.constraints] == ["Constant"] + + +def test_class_binds_a_native_type_under_its_own_python_name(): + """A class states the native type it reaches when the two names differ.""" + module = pyi_text_to_semantic_module( + """ +from prik.contracts import Float64, bind + +@bind("POINT_T") +class PointType: + x: Float64 +""", + module_name="edited", + ) + + assert (module.classes[0].name, module.classes[0].native_name) == ("PointType", "POINT_T") diff --git a/tests/fortran/memory_management/codegen/test_native_handle_planning.py b/tests/fortran/memory_management/codegen/test_native_handle_planning.py index fed0c2472..9b42d665b 100644 --- a/tests/fortran/memory_management/codegen/test_native_handle_planning.py +++ b/tests/fortran/memory_management/codegen/test_native_handle_planning.py @@ -311,7 +311,7 @@ def test_native_handle_plans_keep_datatype_specific_state(): def test_module_variables_use_borrowed_handle_plans_and_operation_sets(): plan = _module_handle_plan() - variables = {variable.symbol_name: variable for variable in plan.namespaces[0].variables} + variables = {variable.symbol_name: variable for variable in plan.variables} allocatable = variables["module_allocatable"].native_array_handle plain = variables["plain_allocatable"].native_array_handle names = variables["module_names"].native_array_handle @@ -488,7 +488,7 @@ def test_native_handle_plan_edits_fail_central_validation(edit: str, diagnostic: def test_plain_module_descriptor_view_requires_matching_completed_interop(): plan = _module_handle_plan() - plain = next(variable for variable in plan.namespaces[0].variables if variable.symbol_name == "plain_allocatable") + plain = next(variable for variable in plan.variables if variable.symbol_name == "plain_allocatable") assert plain.native_array_handle is not None plain.native_array_handle.descriptor_interop = NativeArrayDescriptorInterop.NONE plain.native_array_handle.required_headers = () diff --git a/tests/fortran/modules/codegen/test_module_array_view_lowering.py b/tests/fortran/modules/codegen/test_module_array_view_lowering.py index 841c41f7c..e137929a5 100644 --- a/tests/fortran/modules/codegen/test_module_array_view_lowering.py +++ b/tests/fortran/modules/codegen/test_module_array_view_lowering.py @@ -58,11 +58,7 @@ def _lowered_getters(): bridge = FortranBridgeGenerator() bridge.visit(plan) printer = FortranSourcePrinter() - return { - variable.binding.python_names[0]: printer.visit(bridge.visit(variable)[0]) - for namespace in plan.namespaces - for variable in namespace.variables - } + return {variable.bridge.native_name: printer.visit(bridge.visit(variable)[0]) for variable in plan.variables} def test_addressable_module_array_takes_its_address_directly(): @@ -148,15 +144,14 @@ def _undecided_plan(): module = parse_pyi_text("plain: Float64[3]\n", module_name="array_state") complete_semantic_policies(module) plan = WrapperPlanner().build(module) - variable = plan.namespaces[0].variables[0] + variable = plan.variables[0] return plan, variable, replace(variable, array_address=None) def test_module_array_view_plan_rejects_a_missing_address_mechanism(): """The plan boundary reports the gap rather than letting lowering guess.""" plan, _variable, undecided = _undecided_plan() - namespace = plan.namespaces[0] - namespace.variables = (undecided,) + plan.variables = (undecided,) diagnostics = WrapperGenerator()._plan_diagnostics(plan) diff --git a/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py b/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py index 30743a01d..3445fe321 100644 --- a/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py +++ b/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py @@ -78,16 +78,25 @@ def _source(artifacts, suffix: str) -> str: def _replace_variable(plan, python_name: str, edit): - root = plan.namespaces[0] - variables = tuple( - edit(variable) if variable.binding.python_names == (python_name,) else variable for variable in root.variables + current = next(variable for variable in plan.variables if variable.bridge.native_name == python_name) + replacement = edit(current) + variables = tuple(replacement if variable is current else variable for variable in plan.variables) + namespaces = tuple( + replace( + namespace, + variable_publications=tuple( + replace(publication, variable=replacement) if publication.variable is current else publication + for publication in namespace.variable_publications + ), + ) + for namespace in plan.namespaces ) - return replace(plan, namespaces=(replace(root, variables=variables), *plan.namespaces[1:])) + return replace(plan, variables=variables, namespaces=namespaces) def test_module_variable_plan_contains_only_completed_dispatch_facts(): plan = _plan() - variables = {variable.binding.python_names[0]: variable for variable in plan.namespaces[0].variables} + variables = {variable.bridge.native_name: variable for variable in plan.variables} assert variables["limit"].binding.getter_action is ModuleGetterAction.CONSTANT_VALUE assert variables["limit"].binding.setter_action is SetterAction.OMIT @@ -108,7 +117,7 @@ def test_module_variable_plan_contains_only_completed_dispatch_facts(): def test_symbolic_source_parameter_reuses_scalar_bridge_getter_for_module_initialization(): plan = _computed_constant_plan() - variables = {variable.binding.python_names[0]: variable for variable in plan.namespaces[1].variables} + variables = {variable.bridge.native_name: variable for variable in plan.variables} computed = variables["computed"] assert computed.binding.getter_action is ModuleGetterAction.NATIVE_CONSTANT_VALUE assert computed.binding.constant_value is None @@ -119,7 +128,7 @@ def test_symbolic_source_parameter_reuses_scalar_bridge_getter_for_module_initia c_source = _source(artifacts, ".c") fortran_source = _source(artifacts, ".f90") assert "int32_t bind_c_get_computed(void);" in c_source - assert "int32_t constant_computed_value_0 = bind_c_get_computed();" in c_source + assert "int32_t constant_computed_constants_computed_value_0 = bind_c_get_computed();" in c_source assert 'PyUnicode_FromString("D")' in c_source assert "native_computed => computed" in fortran_source assert "function bind_c_get_computed()" in fortran_source @@ -130,12 +139,7 @@ def test_symbolic_source_parameter_reuses_scalar_bridge_getter_for_module_initia def test_parameter_array_uses_one_immutable_python_owned_import_snapshot(): plan = _parameter_array_plan() - variable = next( - variable - for namespace in plan.namespaces - for variable in namespace.variables - if variable.binding.python_names == ("dpmpar",) - ) + variable = next(variable for variable in plan.variables if variable.bridge.native_name == "dpmpar") assert variable.binding.getter_action is ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE assert variable.binding.setter_action is SetterAction.OMIT assert variable.binding.constant_value is None @@ -146,10 +150,14 @@ def test_parameter_array_uses_one_immutable_python_owned_import_snapshot(): c_source = _source(artifacts, ".c") fortran_source = _source(artifacts, ".f90") assert "void * bind_c_get_dpmpar(int64_t * extent_0);" in c_source - assert "PyArray_EMPTY(1, constant_dpmpar_value_0_dimensions, NPY_FLOAT64, 1)" in c_source - assert "memcpy(PyArray_DATA((PyArrayObject *)constant_dpmpar_object_0)" in c_source - assert "PyArray_CLEARFLAGS((PyArrayObject *)constant_dpmpar_object_0, NPY_ARRAY_WRITEABLE)" in c_source - assert 'PyModule_AddObject(namespace_parameter_array, "dpmpar", constant_dpmpar_object_0)' in c_source + assert "PyArray_EMPTY(1, constant_parameter_array_dpmpar_value_0_dimensions, NPY_FLOAT64, 1)" in c_source + assert "memcpy(PyArray_DATA((PyArrayObject *)constant_parameter_array_dpmpar_object_0)" in c_source + assert ( + "PyArray_CLEARFLAGS((PyArrayObject *)constant_parameter_array_dpmpar_object_0, NPY_ARRAY_WRITEABLE)" in c_source + ) + assert ( + 'PyModule_AddObject(namespace_parameter_array, "dpmpar", constant_parameter_array_dpmpar_object_0)' in c_source + ) assert "real(c_double), allocatable, target, save, dimension(:) :: parameter_snapshot" in fortran_source assert "parameter_snapshot = native_dpmpar" in fortran_source assert "result = c_loc(parameter_snapshot)" in fortran_source @@ -157,9 +165,7 @@ def test_parameter_array_uses_one_immutable_python_owned_import_snapshot(): def test_module_variable_visitors_consume_their_backend_owned_actions(): plan = _plan() - counter = next( - variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",) - ) + counter = next(variable for variable in plan.variables if variable.bridge.native_name == "counter") split_actions = replace( counter, binding=replace( @@ -185,9 +191,7 @@ def test_module_variable_visitors_consume_their_backend_owned_actions(): def test_fortran_module_setter_rejects_unsupported_bridge_assignment(): plan = _plan() - counter = next( - variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",) - ) + counter = next(variable for variable in plan.variables if variable.bridge.native_name == "counter") invalid = replace(counter, bridge=replace(counter.bridge, native_assignment=AssignmentMode.ALIAS)) bridge = FortranBridgeGenerator() @@ -314,20 +318,14 @@ def test_missing_generated_support_procedure_fails_before_lowering(): def test_bridge_local_module_target_edit_does_not_change_the_c_boundary(): plan = _plan() baseline = _source(WrapperGenerator().generate(plan), ".c") - counter = next( - variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",) - ) - edited_counter = replace( - counter, - bridge=replace(counter.bridge, native_name="counter_alternate"), - ) - root = replace( - plan.namespaces[0], - variables=tuple( - edited_counter if variable is counter else variable for variable in plan.namespaces[0].variables + edited = _replace_variable( + plan, + "counter", + lambda variable: replace( + variable, + bridge=replace(variable.bridge, native_name="counter_alternate"), ), ) - edited = replace(plan, namespaces=(root, *plan.namespaces[1:])) artifacts = WrapperGenerator().generate(edited) @@ -336,20 +334,12 @@ def test_bridge_local_module_target_edit_does_not_change_the_c_boundary(): def test_generator_rejects_python_module_setter_without_bridge_handoff(): - plan = _plan() - counter = next( - variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",) - ) - invalid_counter = replace(counter, entrypoint=replace(counter.entrypoint, setter_role=None)) - invalid = replace( - plan, - namespaces=( - replace( - plan.namespaces[0], - variables=tuple( - invalid_counter if variable is counter else variable for variable in plan.namespaces[0].variables - ), - ), + invalid = _replace_variable( + _plan(), + "counter", + lambda variable: replace( + variable, + entrypoint=replace(variable.entrypoint, setter_role=None), ), ) diff --git a/tests/fortran/modules/end_to_end/fixtures/contracts/fcommon_block_f90/__init__.pyi b/tests/fortran/modules/end_to_end/fixtures/contracts/fcommon_block_f90/__init__.pyi index 328c8841b..7fc4c9cde 100644 --- a/tests/fortran/modules/end_to_end/fixtures/contracts/fcommon_block_f90/__init__.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/contracts/fcommon_block_f90/__init__.pyi @@ -1 +1,3 @@ from . import fcommon_block_f90 + +__all__ = ["fcommon_block_f90"] diff --git a/tests/fortran/modules/end_to_end/fixtures/contracts/fcommon_block_f90/fcommon_block_f90.pyi b/tests/fortran/modules/end_to_end/fixtures/contracts/fcommon_block_f90/fcommon_block_f90.pyi index 8e12c3880..a2136c4fc 100644 --- a/tests/fortran/modules/end_to_end/fixtures/contracts/fcommon_block_f90/fcommon_block_f90.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/contracts/fcommon_block_f90/fcommon_block_f90.pyi @@ -6,3 +6,5 @@ def write_shared( ) -> None: ... def read_shared() -> Int32: ... + +__all__ = ["write_shared", "read_shared"] diff --git a/tests/fortran/modules/end_to_end/fixtures/contracts/fmodule_vars_f90/__init__.pyi b/tests/fortran/modules/end_to_end/fixtures/contracts/fmodule_vars_f90/__init__.pyi index 9dcc911b3..668208ce1 100644 --- a/tests/fortran/modules/end_to_end/fixtures/contracts/fmodule_vars_f90/__init__.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/contracts/fmodule_vars_f90/__init__.pyi @@ -1 +1,3 @@ from . import fmodule_vars_f90 + +__all__ = ["fmodule_vars_f90"] diff --git a/tests/fortran/modules/end_to_end/fixtures/contracts/fmodule_vars_f90/fmodule_vars_f90.pyi b/tests/fortran/modules/end_to_end/fixtures/contracts/fmodule_vars_f90/fmodule_vars_f90.pyi index 97ebf51a2..ecb394757 100644 --- a/tests/fortran/modules/end_to_end/fixtures/contracts/fmodule_vars_f90/fmodule_vars_f90.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/contracts/fmodule_vars_f90/fmodule_vars_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Final, Float64, Int32 -class rgb_color: +class Rgb_Color: def __init__( self, *, @@ -15,7 +15,7 @@ class rgb_color: nmax: Final[Int32] = 12 -black: Final[rgb_color] +black: Final[Rgb_Color] counter: Int32 @@ -30,3 +30,16 @@ def scaled_counter() -> Float64: ... def next_local() -> Int32: ... def black_sum() -> Int32: ... + +__all__ = [ + "Rgb_Color", + "nmax", + "black", + "counter", + "scale", + "saved_counter", + "summarize", + "scaled_counter", + "next_local", + "black_sum", +] diff --git a/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/__init__.pyi b/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/__init__.pyi index c74019363..7a1d8c9b3 100644 --- a/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/__init__.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/__init__.pyi @@ -4,3 +4,5 @@ from . import module2 @prik_standalone def standalone() -> Int32: ... + +__all__ = ["module1", "module2", "standalone"] diff --git a/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/module1.pyi b/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/module1.pyi index 05fbb6358..84aacf5b2 100644 --- a/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/module1.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/module1.pyi @@ -3,3 +3,5 @@ from prik.contracts import Int32 def func1() -> Int32: ... def update() -> Int32: ... + +__all__ = ["func1", "update"] diff --git a/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/module2.pyi b/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/module2.pyi index f3586d831..de26aa5c9 100644 --- a/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/module2.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/module2.pyi @@ -3,3 +3,5 @@ from prik.contracts import Int32 def func2() -> Int32: ... def update() -> Int32: ... + +__all__ = ["func2", "update"] diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/__init__.pyi b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/__init__.pyi index 2ccea4308..b912803a4 100644 --- a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import modules_direct_bind_c_f90 + +__all__ = ["modules_direct_bind_c_f90"] diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/modules_direct_bind_c_f90.pyi b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/modules_direct_bind_c_f90.pyi index fea5b1f7f..9f76b9750 100644 --- a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/modules_direct_bind_c_f90.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/modules_direct_bind_c_f90.pyi @@ -13,3 +13,5 @@ def direct_total( def direct_set_counter( value: Int32 ) -> None: ... + +__all__ = ["limit", "counter", "direct_total", "direct_set_counter"] diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/__init__.pyi b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/__init__.pyi index f2ea0d701..ae01b0a58 100644 --- a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import modules_mixed_bind_c_f90 + +__all__ = ["modules_mixed_bind_c_f90"] diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/modules_mixed_bind_c_f90.pyi b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/modules_mixed_bind_c_f90.pyi index 9a85fa681..785699d9c 100644 --- a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/modules_mixed_bind_c_f90.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/modules_mixed_bind_c_f90.pyi @@ -11,3 +11,5 @@ def direct_total( def adapted_total( value: Int32 ) -> Int32: ... + +__all__ = ["counter", "direct_total", "adapted_total"] diff --git a/tests/fortran/modules/end_to_end/test_module_variable_reexport.py b/tests/fortran/modules/end_to_end/test_module_variable_reexport.py new file mode 100644 index 000000000..d5c3b75d8 --- /dev/null +++ b/tests/fortran/modules/end_to_end/test_module_variable_reexport.py @@ -0,0 +1,292 @@ +"""A re-exported module variable is another route to one native variable. + +Publishing a variable in a second namespace adds Python names only. The native +storage, accessors, allocation state, and pointer association stay single, so +every publication observes the same changes whichever one made them. +""" + +from pathlib import Path + +import numpy as np +import pytest + +from prik.pipeline.build import build_fortran_extension, build_pyi_extension +from tests.fortran._support.wrapper_build import _generate_checked_pyi_contract, _import_from_build_dir + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = """\ +module store_mod + implicit none + integer :: counter = 5 + integer, parameter :: limit = 42 + character(len=8) :: label = 'first ' + character(len=4) :: tags(2) = ['ab ', 'cd '] + real(8), allocatable :: values(:) + real(8), pointer :: view(:) => null() + real(8), target :: backing(4) = [1.0d0, 2.0d0, 3.0d0, 4.0d0] +contains + subroutine allocate_values(n) + integer, intent(in) :: n + if (allocated(values)) deallocate(values) + allocate(values(n)) + values = 1.0d0 + end subroutine allocate_values + + subroutine release_values() + if (allocated(values)) deallocate(values) + end subroutine release_values + + subroutine associate_view() + view => backing + end subroutine associate_view + + subroutine clear_view() + view => null() + end subroutine clear_view + +end module store_mod + +module facade_mod + use store_mod, only : counter, limit, label, tags, values, view + implicit none + public :: counter, limit, label, tags, values, view +end module facade_mod + +module renamed_mod + use store_mod, only : tally => counter + implicit none + public :: tally +end module renamed_mod + +module hop_mod + use renamed_mod, only : tally + implicit none + public :: tally +end module hop_mod +""" + + +@pytest.fixture(scope="module") +def built(tmp_path_factory): + """Build the shared source project once for the read-only checks.""" + tmp_path = tmp_path_factory.mktemp("variable_reexport") + source = tmp_path / "store.f90" + source.write_text(SOURCE, encoding="utf-8") + result = build_fortran_extension(source, output_dir=tmp_path / "build", output_name="store_api") + return _import_from_build_dir(result.module_name, result.output_dir) + + +def test_a_scalar_publication_reads_and_writes_one_native_variable(built): + """Both namespaces name the same storage, so either one observes the other.""" + built.store_mod.counter = np.int32(11) + assert built.facade_mod.counter == np.int32(11) + + built.facade_mod.counter = np.int32(23) + assert built.store_mod.counter == np.int32(23) + + +def test_a_renamed_publication_reaches_the_same_variable(built): + """A rename changes the Python name a namespace binds, never the variable.""" + built.store_mod.counter = np.int32(31) + assert built.renamed_mod.tally == np.int32(31) + + built.renamed_mod.tally = np.int32(37) + assert built.store_mod.counter == np.int32(37) + + +def test_a_multi_hop_publication_resolves_to_the_declaring_variable(built): + """A -> B -> C publishes what A declares, not a copy B made.""" + built.store_mod.counter = np.int32(41) + assert built.hop_mod.tally == np.int32(41) + + built.hop_mod.tally = np.int32(43) + assert built.store_mod.counter == np.int32(43) + assert built.renamed_mod.tally == np.int32(43) + + +def test_a_character_scalar_and_array_publish_one_storage(built): + """String storage is shared the same way a scalar is.""" + built.store_mod.label = "second " + assert built.facade_mod.label == "second " + + built.store_mod.tags[0] = b"zz " + assert bytes(built.facade_mod.tags[0]) == b"zz " + + +def test_allocation_state_is_one_state_for_every_publication(built): + """Allocating through the declaring module is visible through the facade.""" + built.store_mod.release_values() + assert built.facade_mod.values.allocated is False + + built.store_mod.allocate_values(np.int32(3)) + assert built.facade_mod.values.allocated is True + assert built.facade_mod.values.shape == (3,) + + # Reallocating to another extent replaces the one descriptor both see. + built.store_mod.allocate_values(np.int32(5)) + assert built.facade_mod.values.shape == (5,) + + built.store_mod.release_values() + assert built.facade_mod.values.allocated is False + + +def test_pointer_association_is_one_association_for_every_publication(built): + """Associating and nullifying reach every namespace publishing the pointer.""" + built.store_mod.clear_view() + assert built.facade_mod.view.associated is False + + built.store_mod.associate_view() + assert built.facade_mod.view.associated is True + assert built.facade_mod.view.shape == (4,) + + built.store_mod.clear_view() + assert built.facade_mod.view.associated is False + + +def test_publishing_a_protected_variable_keeps_the_documented_refusal(tmp_path: Path): + """A second namespace adds names, so it cannot make an unsupported form work. + + PRIK refuses `protected` because a generated accessor cannot define the + variable outside its own module, and re-exporting it changes nothing about + that. + """ + source = tmp_path / "guarded.f90" + source.write_text( + """\ +module guard_mod + implicit none + integer, protected :: guarded = 9 +end module guard_mod + +module guard_facade + use guard_mod, only : guarded + implicit none + public :: guarded +end module guard_facade +""", + encoding="utf-8", + ) + + with pytest.raises(ValueError) as error: + build_fortran_extension( + source, + output_dir=tmp_path / "build", + output_name="guard_api", + generate_sources=True, + ) + + assert "is PROTECTED" in str(error.value) + + +def test_a_parameter_publishes_a_value_rather_than_shared_storage(built): + """A `parameter` has no storage to share, so each namespace holds the value. + + Assignment is not refused, and rebinding one name changes neither the + Fortran parameter nor any other namespace publishing it. + """ + assert built.store_mod.limit == np.int32(42) + assert built.facade_mod.limit == np.int32(42) + + built.facade_mod.limit = np.int32(7) + + assert built.facade_mod.limit == np.int32(7) + assert built.store_mod.limit == np.int32(42) + + +def test_one_native_accessor_serves_every_publication(tmp_path: Path): + """The second namespace adds names, so no second accessor is generated.""" + source = tmp_path / "store.f90" + source.write_text(SOURCE, encoding="utf-8") + + result = build_fortran_extension( + source, + output_dir=tmp_path / "generated", + output_name="accessor_api", + generate_sources=True, + ) + wrapper = (result.output_dir / "accessor_api_wrapper.c").read_text(encoding="utf-8") + + # One getter and one setter definition carry `counter`, however many + # namespaces publish it; the four dispatches all call the same pair. + assert wrapper.count("static PyObject * module_get_counter(void) {") == 1 + assert wrapper.count("static int module_set_counter(PyObject * value_obj) {") == 1 + assert wrapper.count("return module_get_counter();") == 4 + + +def test_a_facade_may_publish_a_variable_its_declaring_namespace_hides(tmp_path: Path): + """Owning the one variable plan must not put the declaring module in Python.""" + source = tmp_path / "store.f90" + source.write_text( + """\ +module home_mod + implicit none + integer :: counter = 5 +end module home_mod +""", + encoding="utf-8", + ) + package = tmp_path / "contracts" + package.mkdir() + (package / "home_mod.pyi").write_text( + "from prik.contracts import Int32\n\ncounter: Int32\n\n__all__ = []\n", + encoding="utf-8", + ) + (package / "facade.pyi").write_text( + 'from .home_mod import counter\n\n__all__ = ["counter"]\n', + encoding="utf-8", + ) + (package / "__init__.pyi").write_text( + 'from . import facade\n\n__all__ = ["facade"]\n', + encoding="utf-8", + ) + + result = build_pyi_extension( + package / "__init__.pyi", + native_fortran_sources=[str(source)], + output_dir=tmp_path / "build", + output_name="hidden_home_api", + ) + module = result.import_module() + + assert module.facade.counter == np.int32(5) + module.facade.counter = np.int32(17) + assert module.facade.counter == np.int32(17) + # The declaring module publishes nothing, so it is not a Python namespace. + assert not hasattr(module, "home_mod") + + +def test_a_generated_contract_publishes_the_same_variables_as_its_source(tmp_path: Path): + """Both routes reach one variable, so the two builds publish the same surface.""" + source = tmp_path / "store.f90" + source.write_text(SOURCE, encoding="utf-8") + + source_result = build_fortran_extension( + source, + output_dir=tmp_path / "source_build", + output_name="parity_source", + ) + contracts = tmp_path / "contracts" + _generate_checked_pyi_contract(source, contracts, None) + contract_result = build_pyi_extension( + contracts / "__init__.pyi", + native_fortran_sources=[str(source)], + output_dir=tmp_path / "contract_build", + output_name="parity_contract", + ) + + from_source = _import_from_build_dir(source_result.module_name, source_result.output_dir) + from_contract = _import_from_build_dir(contract_result.module_name, contract_result.output_dir) + + def surface(module): + return { + namespace: sorted(n for n in dir(getattr(module, namespace)) if not n.startswith("_")) + for namespace in ("store_mod", "facade_mod", "renamed_mod", "hop_mod") + } + + assert surface(from_source) == surface(from_contract) + + # The contract route reaches the same native variable, not a copy of it. + from_contract.facade_mod.counter = np.int32(61) + assert from_contract.store_mod.counter == np.int32(61) + assert from_contract.hop_mod.tally == np.int32(61) diff --git a/tests/fortran/modules/end_to_end/test_module_variables_and_state.py b/tests/fortran/modules/end_to_end/test_module_variables_and_state.py index 04268fe8c..681df1ba5 100644 --- a/tests/fortran/modules/end_to_end/test_module_variables_and_state.py +++ b/tests/fortran/modules/end_to_end/test_module_variables_and_state.py @@ -7,6 +7,8 @@ import numpy as np import pytest from tests.fortran._support.wrapper_build import ( + _build_generated_pyi_and_import, + _build_source_and_import, _build_source_or_generated_pyi_and_import, _build_text_and_import, _sole_native_module, @@ -18,6 +20,47 @@ pytestmark = pytest.mark.fortran_end_to_end +MODULE_VARIABLE_REEXPORT_SOURCE = """ +module reexport_state_home + use iso_fortran_env, only: int32, real64 + implicit none + + type :: item + integer(int32) :: value = 0 + end type item + + integer(int32), parameter :: limit = 7 + integer(int32) :: counter = 3 + integer(int32) :: numbers(3) + real(real64), allocatable :: values(:) + real(real64), target :: backing(3) + real(real64), pointer :: selected(:) => null() + type(item) :: current + type(item), allocatable :: optional_item + +contains + + subroutine setup() + numbers = [1, 2, 3] + if (.not. allocated(values)) allocate(values(3)) + values = [4.0_real64, 5.0_real64, 6.0_real64] + backing = [7.0_real64, 8.0_real64, 9.0_real64] + selected => backing + current%value = 10 + if (.not. allocated(optional_item)) allocate(optional_item) + optional_item%value = 11 + end subroutine setup +end module reexport_state_home + +module reexport_state_facade + use reexport_state_home, only: limit, counter, numbers, values, selected, current, optional_item + implicit none + private + public :: limit, counter, numbers, values, selected, current, optional_item +end module reexport_state_facade +""" + + def _module_variables_build_dir(tmp_path: Path, build_mode: str) -> Path: if build_mode == "source": return tmp_path / "source_build" @@ -52,7 +95,7 @@ def test_scalar_module_variables_use_attributes_and_parameters_have_no_native_se assert "Assignment writes through to native storage." not in module_docstring assert module.nmax == np.int32(12) - assert isinstance(module.black, module.rgb_color) + assert isinstance(module.black, module.Rgb_Color) assert module.black.r == np.int32(0) assert module.black.g == np.int32(0) assert module.black.b == np.int32(0) @@ -534,3 +577,358 @@ def test_declared_length_character_module_arrays_compile_and_expose_their_width( module.deferred_ptr.deallocate() assert module.deferred_ptr.associated is False assert module.deferred_ptr.shape is None + + +REEXPORT_SOURCE = """ +module reexport_home_mod + implicit none +contains + subroutine scale_value(value, scaled) + integer, intent(in) :: value + integer, intent(out) :: scaled + scaled = value * 2 + end subroutine scale_value +end module reexport_home_mod + +module reexport_facade_mod + use reexport_home_mod, only : scale_value + implicit none + private + public :: scale_value +end module reexport_facade_mod + +module reexport_default_mod + use reexport_home_mod + implicit none +end module reexport_default_mod + +module reexport_shout_mod + implicit none +contains + subroutine SCALE_LOUD(value, scaled) + integer, intent(in) :: value + integer, intent(out) :: scaled + scaled = value * 3 + end subroutine SCALE_LOUD +end module reexport_shout_mod + +module reexport_case_mod + use reexport_shout_mod, only : SCALE_LOUD + implicit none + private + public :: SCALE_LOUD +end module reexport_case_mod + +module reexport_renamed_mod + use reexport_home_mod, only : public_scale => scale_value + implicit none + private + public :: public_scale +end module reexport_renamed_mod + +module reexport_wildcard_mod + use reexport_home_mod + implicit none + private + public :: scale_value +end module reexport_wildcard_mod + +module reexport_hop_mod + use reexport_facade_mod, only : scale_value + implicit none + private + public :: scale_value +end module reexport_hop_mod + +module reexport_collide_mod + implicit none +contains + subroutine lambda(x) + integer, intent(inout) :: x + x = x + 1 + end subroutine lambda + subroutine lambda_(x) + integer, intent(inout) :: x + x = x + 100 + end subroutine lambda_ +end module reexport_collide_mod + +module reexport_collide_user_mod + use reexport_collide_mod, only : lambda_ + implicit none + private + public :: lambda_ +end module reexport_collide_user_mod +""" + + +def test_module_variable_reexports_share_one_native_entity_from_source_and_contract( + pyi_parity_build_mode: str, + tmp_path: Path, +): + """Every publication reads one variable plan and its live native state.""" + source = tmp_path / "module_variable_reexports.f90" + source.write_text(MODULE_VARIABLE_REEXPORT_SOURCE, encoding="utf-8") + + if pyi_parity_build_mode == "source": + build_dir = tmp_path / "source_build" + module = _build_source_and_import( + source, + build_dir, + { + "bind_c_module_variable_reexports_wrapper.f90", + "module_variable_reexports_wrapper.c", + "module_variable_reexports_wrapper.h", + }, + ) + else: + workdir = tmp_path / "generated_pyi_build" + module = _build_generated_pyi_and_import(source, workdir) + build_dir = workdir / "pyi_build" + + contracts = workdir / "contracts" / source.stem + home_contract = (contracts / "reexport_state_home.pyi").read_text(encoding="utf-8") + facade_contract = (contracts / "reexport_state_facade.pyi").read_text(encoding="utf-8") + assert "counter: Int32" in home_contract + assert "limit: Final[Int32]" in home_contract + assert "from .reexport_state_home import " in facade_contract + imported_names = facade_contract.partition("import ")[2].partition("\n")[0].split(", ") + assert {"counter", "limit"}.issubset(imported_names) + assert '"counter"' in facade_contract.partition("__all__ = ")[2] + assert '"limit"' in facade_contract.partition("__all__ = ")[2] + + home = module.reexport_state_home + facade = module.reexport_state_facade + home.setup() + + assert home.limit == facade.limit == np.int32(7) + + home.counter = np.int32(10) + assert facade.counter == np.int32(10) + facade.counter = np.int32(25) + assert home.counter == np.int32(25) + + home.numbers[0] = np.int32(21) + assert facade.numbers[0] == np.int32(21) + facade.numbers[1] = np.int32(22) + assert home.numbers[1] == np.int32(22) + + home.values.to_numpy()[0] = np.float64(31.0) + assert facade.values.to_numpy()[0] == np.float64(31.0) + facade.values.to_numpy()[1] = np.float64(32.0) + assert home.values.to_numpy()[1] == np.float64(32.0) + + assert home.selected.associated is True + facade.selected.nullify() + assert home.selected.associated is False + + home.current.value = np.int32(41) + assert facade.current.value == np.int32(41) + facade.current.value = np.int32(42) + assert home.current.value == np.int32(42) + + home.optional_item.value = np.int32(51) + assert facade.optional_item.value == np.int32(51) + facade.optional_item.value = np.int32(52) + assert home.optional_item.value == np.int32(52) + + generated = next(build_dir.glob("*_wrapper.c")).read_text(encoding="utf-8") + assert "module_get_limit" not in generated + assert "module_set_limit" not in generated + for name in ("counter", "numbers", "values", "selected", "current", "optional_item"): + assert generated.count(f"static PyObject * module_get_{name}(void) {{") == 1 + assert generated.count("static int module_set_counter(PyObject * value_obj) {") == 1 + + bridge = next(build_dir.glob("bind_c_*_wrapper.f90")).read_text(encoding="utf-8") + assert "bind_c_get_limit" not in bridge + assert "bind_c_set_limit" not in bridge + for signature in ( + "function bind_c_get_counter(", + "subroutine bind_c_set_counter(", + "function bind_c_get_numbers(", + "subroutine bind_c_values_descriptor(", + "subroutine bind_c_selected_descriptor(", + "function bind_c_prik_module_field_current_value_get(", + "subroutine bind_c_prik_module_field_current_value_set(", + "function bind_c_prik_module_field_optional_item_value_get(", + "subroutine bind_c_prik_module_field_optional_item_value_set(", + ): + assert bridge.count(signature) == 1 + + +def test_explicitly_published_import_is_reachable_without_a_second_wrapper(tmp_path: Path): + """Naming an imported procedure in a `public` statement publishes it here. + + The declaration is not repeated: the published name binds to the one + wrapper its own module exposes, so both namespaces share a single callable. + A default-public module also republishes an accessible imported name. + """ + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_facade_mod.scale_value is module.reexport_home_mod.scale_value + assert module.reexport_facade_mod.scale_value(np.int32(4)) == np.int32(8) + + assert module.reexport_default_mod.scale_value is module.reexport_home_mod.scale_value + + # One wrapper defines the procedure; the facade only names it again. + generated = (tmp_path / "build" / "reexport_wrapper.c").read_text(encoding="utf-8") + assert generated.count("static PyObject * wrap_scale_value") == 1 + + +def test_published_import_resolves_the_python_name_its_declaring_module_bound(tmp_path: Path): + """A re-export binds a Python attribute, which is not a Fortran spelling. + + A Fortran entity written in capitals is exported under its Python name, so + the module publishing it has to reach for that name rather than the source + spelling, which names no attribute at all. + """ + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_case_mod.scale_loud is module.reexport_shout_mod.scale_loud + assert module.reexport_case_mod.scale_loud(np.int32(4)) == np.int32(12) + assert not hasattr(module.reexport_case_mod, "SCALE_LOUD") + + +def test_renamed_published_import_shares_the_wrapper_it_renames(tmp_path: Path): + """A renamed re-export states a new name for one existing callable.""" + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_renamed_mod.public_scale is module.reexport_home_mod.scale_value + assert module.reexport_renamed_mod.public_scale(np.int32(6)) == np.int32(12) + + +def test_publishing_a_name_a_plain_use_brought_in_republishes_that_name(tmp_path: Path): + """A plain `use` carries public names that remain accessible by default. + + An explicit `public` statement also publishes the named import; both routes + bind the one wrapper owned by the declaring module. + """ + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_wildcard_mod.scale_value is module.reexport_home_mod.scale_value + assert module.reexport_wildcard_mod.scale_value(np.int32(5)) == np.int32(10) + assert module.reexport_default_mod.scale_value is module.reexport_home_mod.scale_value + + +def test_publishing_an_already_published_import_follows_it_to_its_declaration(tmp_path: Path): + """A published name may come from a module that published it in turn. + + The module a `use` reads is not always the one declaring the entity, so + each hop is followed until the declaration itself is reached; stopping at + the first module leaves the name looking like nothing at all. + """ + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_hop_mod.scale_value is module.reexport_home_mod.scale_value + assert module.reexport_hop_mod.scale_value(np.int32(7)) == np.int32(14) + + +def test_published_import_binds_the_declaration_a_collision_moved_aside(tmp_path: Path): + """Two source names may want one Python name, and only one may have it. + + A module holding both `lambda` and `lambda_` publishes them as `lambda_` + and `lambda__2`, so a module publishing the second reaches the name the + declaring module settled on rather than the one its source resembles. + """ + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_collide_mod.lambda_(np.int32(0)) == np.int32(1) + assert module.reexport_collide_mod.lambda__2(np.int32(0)) == np.int32(100) + assert module.reexport_collide_user_mod.lambda_ is module.reexport_collide_mod.lambda__2 + assert module.reexport_collide_user_mod.lambda_(np.int32(0)) == np.int32(100) + + +def test_a_reexport_binds_one_callable_from_source_and_from_its_contract(tmp_path: Path): + """A published name is an alias, so both routes bind the same object. + + A re-export names a procedure that is already wrapped, whichever way the + build was described. Wrapping it a second time would give one native + procedure two Python objects, and a renamed re-export is no different: the + name it binds changes, not the callable behind it. + """ + import subprocess + import sys + + from tests.fortran._support.wrapper_build import _compiler, _import_from_build_dir + from prik import build_pyi_extension + + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + + from_source = _build_source_and_import( + source, + tmp_path / "source_build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + assert from_source.reexport_facade_mod.scale_value is from_source.reexport_home_mod.scale_value + assert from_source.reexport_renamed_mod.public_scale is from_source.reexport_home_mod.scale_value + + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + str(source), + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(source)], + output_dir=tmp_path / "contract_build", + output_name="reexport_contract", + ) + from_contract = _import_from_build_dir(result.module_name, result.output_dir) + + assert from_contract.reexport_facade_mod.scale_value is from_contract.reexport_home_mod.scale_value + assert from_contract.reexport_renamed_mod.public_scale is from_contract.reexport_home_mod.scale_value + assert from_contract.reexport_facade_mod.scale_value(np.int32(4)) == np.int32(8) + + # One wrapper defines the procedure on either route. + generated = (result.output_dir / "reexport_contract_wrapper.c").read_text(encoding="utf-8") + assert generated.count("static PyObject * wrap_scale_value") == 1 diff --git a/tests/fortran/modules/parsing/test_project_scope_models.py b/tests/fortran/modules/parsing/test_project_scope_models.py index 8a00c141c..229a962d3 100644 --- a/tests/fortran/modules/parsing/test_project_scope_models.py +++ b/tests/fortran/modules/parsing/test_project_scope_models.py @@ -3,6 +3,7 @@ import pytest from prik.parsers.fortran import FortranParseError, parse_fortran_file, parse_fortran_project +from prik.parsers.fortran.scope import ScopeUses from prik.parsers.fortran.parser import FortranParser @@ -190,7 +191,7 @@ def test_program_contains_and_unnamed_block_data_public_models(): parsed = parse_fortran_file(code, filename="units.f90") - assert parsed.programs[0].uses["callback_mod"] == [] + assert ScopeUses(parsed.programs[0].uses).imports_all("callback_mod") is True assert [var.name for var in parsed.programs[0].variables] == ["ierr"] assert parsed.block_data_units[0].name is None assert [var.name for var in parsed.block_data_units[0].variables] == ["seed"] @@ -320,7 +321,7 @@ def test_directory_project_tracks_renamed_kind_imports_from_other_files(tmp_path assert args["x"].kind == "8" assert args["x"].shape == ["1:stride"] assert args["y"].kind == "16" - assert [(mapping.source, mapping.target) for mapping in proc.uses["precision_mod"]] == [ + assert [(mapping.source, mapping.target) for mapping in ScopeUses(proc.uses).mappings("precision_mod")] == [ ("wp", "local_wp"), ("stride", None), ("wide", "local_wide"), @@ -567,4 +568,7 @@ def test_project_resolution_uses_file_level_use_only_and_local_parameters(tmp_pa assert args["x"].kind == "selected_real_kind(12)" assert args["x"].shape == ["1:n"] assert args["y"].kind == "selected_real_kind(6)" - assert [mapping.local_name for mapping in proc.uses["public_params_mod"]] == ["rk", "n"] + assert [mapping.local_name for mapping in ScopeUses(proc.uses).mappings("public_params_mod")] == [ + "rk", + "n", + ] diff --git a/tests/fortran/modules/parsing/test_scope_handling.py b/tests/fortran/modules/parsing/test_scope_handling.py index 3e5867c6c..e5d15c0f7 100644 --- a/tests/fortran/modules/parsing/test_scope_handling.py +++ b/tests/fortran/modules/parsing/test_scope_handling.py @@ -2,6 +2,7 @@ from prik.parsers.fortran.models import FortranParseError from prik.parsers.fortran import parse_fortran_file +from prik.parsers.fortran.scope import ScopeUses def test_same_argument_name_in_different_procedures_is_allowed(): @@ -188,3 +189,147 @@ def test_fortran_parser_class_entrypoint(): assert len(signatures) == 1 assert signatures[0].name == "touch" + + +def test_repeated_use_of_one_module_accumulates_its_imports(): + """A scope may name the same module in several `use` statements. + + Each statement adds what it lists, so a later one extends the imports + rather than replacing them; real sources split long import lists this way, + and dropping the earlier statements loses the names they carried. + """ + module = parse_fortran_file( + """ +module consumer_mod + use, intrinsic :: iso_fortran_env, only : INT32, SP => REAL32, DP => REAL64 + use, intrinsic :: iso_fortran_env, only : QP => REAL128 + use, intrinsic :: iso_fortran_env, only : STDOUT => OUTPUT_UNIT + implicit none +end module consumer_mod +""" + ).modules[0] + + scope = ScopeUses(module.uses) + assert [(item.source, item.target) for item in scope.mappings("iso_fortran_env")] == [ + ("INT32", None), + ("REAL32", "SP"), + ("REAL64", "DP"), + ("REAL128", "QP"), + ("OUTPUT_UNIT", "STDOUT"), + ] + + +def test_a_bare_use_is_read_beside_the_named_imports_of_the_same_module(): + """Importing everything does not erase what another statement listed. + + Both statements are source facts, and the language reads them together: the + module's public names are accessible, and `rk` is bound as well. + """ + module = parse_fortran_file( + """ +module wide_mod + use kinds_mod, only : rk + use kinds_mod + implicit none +end module wide_mod +""" + ).modules[0] + + scope = ScopeUses(module.uses) + assert scope.imports_all("kinds_mod") is True + assert [(item.source, item.target) for item in scope.mappings("kinds_mod")] == [("rk", None)] + + +def test_an_empty_only_list_imports_nothing(): + """`use m, only :` is valid and narrows to no names at all.""" + module = parse_fortran_file( + """ +module narrow_mod + use kinds_mod, only : + implicit none +end module narrow_mod +""" + ).modules[0] + + scope = ScopeUses(module.uses) + assert (scope.imports_all("kinds_mod"), scope.mappings("kinds_mod")) == (False, ()) + + +def test_a_procedure_local_use_stays_out_of_its_module_imports(): + """A scope inherits its parent's statements; it cannot add to them. + + The procedure sees what the module imported and what it imported itself, + while the module keeps only its own -- otherwise a procedure-local import + would reach module accessibility and re-export analysis. + """ + module = parse_fortran_file( + """ +module owner_mod + use dep_mod, only : x + implicit none +contains + subroutine inner() + use dep_mod, only : y + end subroutine inner +end module owner_mod +""" + ).modules[0] + + assert [item.source for statement in module.uses for item in statement.mappings] == ["x"] + procedure = module.procedures[0] + assert [item.source for statement in procedure.uses for item in statement.mappings] == ["x", "y"] + + +def test_statements_for_one_module_are_read_whatever_their_spelling(): + """Fortran module names are case-insensitive, so both statements are one use.""" + module = parse_fortran_file( + """ +module consumer_mod + use DEP_MOD, only : p => q + use dep_mod + implicit none +end module consumer_mod +""" + ).modules[0] + + scope = ScopeUses(module.uses) + assert scope.modules() == ("DEP_MOD",) + assert scope.imports_all("dep_mod") is True + assert [(item.source, item.target) for item in scope.mappings("dep_mod")] == [("q", "p")] + + +def test_one_local_name_reached_by_two_entities_keeps_both_routes(): + """A rename may collide with a name the same module already publishes. + + `use dep, x => y` binds `y` as `x` while `x` itself still arrives, so the + local name reaches two entities. Reporting both routes is what lets the + stage holding them call that ambiguous rather than picking one. + """ + module = parse_fortran_file( + """ +module consumer_mod + use dep_mod, x => y + implicit none +end module consumer_mod +""" + ).modules[0] + + routes = ScopeUses(module.uses).routes_for("x", lambda name: {"x", "y"}) + assert sorted(route.source_name for route in routes) == ["x", "y"] + + +def test_unread_whole_module_routes_still_apply_rename_semantics(): + """Unknown offered names stay possible except under a renamed-away spelling.""" + module = parse_fortran_file( + """ +module consumer_mod + use dep_mod, x => y + implicit none +end module consumer_mod +""" + ).modules[0] + scope = ScopeUses(module.uses) + + assert [route.source_name for route in scope.unresolved_routes_for("x", lambda name: None)] == ["x"] + assert scope.unresolved_routes_for("y", lambda name: None) == () + assert [route.source_name for route in scope.unresolved_routes_for("z", lambda name: None)] == ["z"] diff --git a/tests/fortran/modules/semantics/test_declaration_publication.py b/tests/fortran/modules/semantics/test_declaration_publication.py new file mode 100644 index 000000000..5bdc35672 --- /dev/null +++ b/tests/fortran/modules/semantics/test_declaration_publication.py @@ -0,0 +1,333 @@ +"""A module publishes the declarations its own accessibility makes reachable. + +A prototype or a generic follows the same rule every other declaration does: +the contract may need to name it for typing or dispatch, but only what the +module makes public becomes part of its Python surface. A block written inside +a contained procedure is reachable in that procedure alone, so it is never a +module publication at all. +""" + +from pathlib import Path + +from prik.parsers.fortran import parse_fortran_file, parse_fortran_project +from prik.printers.pyi import PyiPrinter +from prik.semantics.models import CONTRACT_NAME_METADATA, completed_contract_name +from prik.policy.contract_imports import complete_contract_imports +from prik.policy.exports import complete_python_export_policy +from prik.semantics.fortran2ir import fortran_file_to_semantic_modules, fortran_project_to_semantic_modules + +PRIVATE_SOURCE = """\ +module m + implicit none + private + + abstract interface + subroutine cb() + end subroutine + end interface + + interface hidden_generic + module procedure hidden_one + end interface + + public :: run +contains + subroutine run(f) + procedure(cb) :: f + call f() + end subroutine run + + subroutine hidden_one(a) + integer, intent(in) :: a + print *, a + end subroutine hidden_one +end module m +""" + +LOCAL_INTERFACE_SOURCE = """\ +module m + implicit none +contains + subroutine first(f) + abstract interface + subroutine cb(x) + integer :: x + end subroutine + end interface + procedure(cb) :: f + call f(1) + end subroutine first + + subroutine second(f) + abstract interface + subroutine cb(x) + real :: x + end subroutine + end interface + procedure(cb) :: f + call f(1.0) + end subroutine second +end module m +""" + + +def _module(source: str, tmp_path: Path): + """Convert one source to its policy-complete semantic module.""" + path = tmp_path / "m.f90" + path.write_text(source, encoding="utf-8") + module = fortran_file_to_semantic_modules(parse_fortran_file(source, filename=str(path)))[0] + complete_python_export_policy(module) + return module + + +def test_a_private_prototype_and_generic_state_their_accessibility(tmp_path: Path): + """Semantics records what the module's `private` default says about each.""" + module = _module(PRIVATE_SOURCE, tmp_path) + + assert [(item.name, item.visibility) for item in module.prototypes] == [("cb", "private")] + assert [(item.name, item.visibility) for item in module.overload_sets] == [("hidden_generic", "private")] + + +def test_a_private_prototype_and_generic_are_written_but_not_published(tmp_path: Path): + """The contract names both for typing and dispatch, and publishes neither.""" + module = _module(PRIVATE_SOURCE, tmp_path) + contract = PyiPrinter().emit(module) + + # `run` annotates its callback with the prototype, so the name must exist. + assert "def cb() -> None: ..." in contract + assert "def hidden_generic(" in contract + assert '__all__ = ["run"]' in contract + + +def test_two_procedures_may_name_different_interfaces_the_same_way(tmp_path: Path): + """A block inside a procedure is that procedure's, so each keeps its own.""" + module = _module(LOCAL_INTERFACE_SOURCE, tmp_path) + + assert [(completed_contract_name(item), item.native_name, item.visibility) for item in module.prototypes] == [ + ("first_cb", "cb", "private"), + ("second_cb", "cb", "private"), + ] + # Each procedure's callback keeps the signature its own block declares. + signatures = { + function.name: [argument.semantic_type.metadata["arguments"][0].name for argument in function.arguments] + for function in module.functions + } + assert signatures == {"first": ["Int32"], "second": ["Float32"]} + + +def test_a_procedure_local_interface_is_never_a_module_publication(tmp_path: Path): + """A `use` of the module cannot reach it, so the contract does not publish it.""" + module = _module(LOCAL_INTERFACE_SOURCE, tmp_path) + contract = PyiPrinter(normalize_public_names=True).emit(module) + + assert "def first_cb(" in contract + assert "def second_cb(" in contract + assert "f: first_cb" in contract + assert "f: second_cb" in contract + assert '__all__ = ["first", "second"]' in contract + + +MODULE_AND_LOCAL_SOURCE = """\ +module m + implicit none + abstract interface + subroutine first_cb(x) + integer :: x + end subroutine + end interface +contains + subroutine first(f) + abstract interface + subroutine cb(x) + real :: x + end subroutine + end interface + procedure(cb) :: f + call f(1.0) + end subroutine first + + subroutine uses_module_one(g) + procedure(first_cb) :: g + call g(1) + end subroutine uses_module_one +end module m +""" + +JOINED_COLLISION_SOURCE = """\ +module m + implicit none +contains + subroutine a_b(f) + abstract interface + subroutine c(x) + integer :: x + end subroutine + end interface + procedure(c) :: f + call f(1) + end subroutine a_b + + subroutine a(f) + abstract interface + subroutine b_c(x) + real :: x + end subroutine + end interface + procedure(b_c) :: f + call f(1.0) + end subroutine a +end module m +""" + + +def _callback_annotations(module) -> dict[str, tuple[str, str]]: + """Return each callback argument's contract name and first argument type.""" + return { + f"{function.name}.{argument.name}": ( + argument.semantic_type.metadata[CONTRACT_NAME_METADATA], + argument.semantic_type.metadata["arguments"][0].name, + ) + for function in module.functions + for argument in function.arguments + if argument.semantic_type.storage is not None and argument.semantic_type.storage.kind == "callback" + } + + +def test_a_prototype_is_identified_by_its_scope_rather_than_its_spelling(tmp_path: Path): + """A module block and a procedure block are different declarations. + + Naming the procedure-local one by joining its scope to its name produces + the module block's own spelling, so the two would be one prototype and one + of the callbacks would be given the other's signature. + """ + module = _module(MODULE_AND_LOCAL_SOURCE, tmp_path) + + identities = [(item.native_name, item.declaring_scope, item.visibility) for item in module.prototypes] + assert identities == [("first_cb", (), "public"), ("cb", ("first",), "private")] + + # The module's own block keeps the spelling another module imports it by. + names = [completed_contract_name(item) for item in module.prototypes] + assert names[0] == "first_cb" + assert names[1] != "first_cb" + + annotations = _callback_annotations(module) + assert annotations["uses_module_one.g"] == ("first_cb", "Int32") + assert annotations["first.f"] == (names[1], "Float32") + + +def test_scopes_whose_joined_spellings_collide_keep_distinct_contract_names(tmp_path: Path): + """`a_b` declaring `c` and `a` declaring `b_c` are different prototypes.""" + module = _module(JOINED_COLLISION_SOURCE, tmp_path) + + names = [completed_contract_name(item) for item in module.prototypes] + assert len(set(names)) == 2 + + annotations = _callback_annotations(module) + assert annotations["a_b.f"] == (names[0], "Int32") + assert annotations["a.f"] == (names[1], "Float32") + + +def test_a_contract_writes_one_prototype_for_each_scope(tmp_path: Path): + """Both prototypes are written, and only the module's own is published.""" + module = _module(MODULE_AND_LOCAL_SOURCE, tmp_path) + contract = PyiPrinter(normalize_public_names=True).emit(module) + local_name = completed_contract_name(module.prototypes[1]) + + assert "def first_cb(\n x: Int32[()]\n) -> None: ..." in contract + assert f"def {local_name}(\n x: Float32[()]\n) -> None: ..." in contract + assert f"f: {local_name}" in contract + assert "g: first_cb" in contract + assert '__all__ = ["first_cb", "first", "uses_module_one"]' in contract + + +IMPORT_COLLISION_SOURCE = """\ +module helper_mod + implicit none + integer :: first_cb = 7 +end module helper_mod + +module m_mod + use helper_mod, only : first_cb + implicit none +contains + subroutine first(f) + abstract interface + subroutine cb(x) + real :: x + end subroutine + end interface + procedure(cb) :: f + call f(1.0) + end subroutine first +end module m_mod +""" + + +def test_a_prototype_does_not_take_a_name_the_module_imports(tmp_path: Path): + """A use-associated name binds here too, so a prototype cannot be given it. + + `m_mod` imports `first_cb`, and its contained procedure declares `cb`, + whose suggested spelling is the same. Allocating against the declared names + alone let the prototype shadow the import the contract writes. + """ + (tmp_path / "project.f90").write_text(IMPORT_COLLISION_SOURCE, encoding="utf-8") + modules = { + module.name: module for module in fortran_project_to_semantic_modules(parse_fortran_project(str(tmp_path))) + } + module = modules["m_mod"] + complete_python_export_policy(module) + + assert [(item.native_name, item.declaring_scope) for item in module.prototypes] == [("cb", ("first",))] + spelled = completed_contract_name(module.prototypes[0]) + assert spelled != "first_cb" + + complete_contract_imports([module]) + contract = PyiPrinter(normalize_public_names=True).emit(module) + assert "from .helper_mod import first_cb" in contract + assert f"def {spelled}(" in contract + assert f"f: {spelled}" in contract + + +UNBOUND_USE_SOURCE = """\ +module helper_mod + implicit none + integer :: first_cb = 7 +end module helper_mod + +module m_mod + use helper_mod, only : first_cb + implicit none + private + public :: first +contains + subroutine first(f) + abstract interface + subroutine cb(x) + real :: x + end subroutine + end interface + procedure(cb) :: f + call f(real(first_cb)) + end subroutine first +end module m_mod +""" + + +def test_a_prototype_is_spelled_against_what_the_contract_binds(tmp_path: Path): + """A name the module reaches but its contract never binds does not move a prototype. + + `m_mod` uses `first_cb` only in executable code and publishes nothing but + `first`, so its contract imports no `first_cb`. Spelling prototypes while + converting source held every use-associated name and suffixed this one. + """ + (tmp_path / "project.f90").write_text(UNBOUND_USE_SOURCE, encoding="utf-8") + module = { + module.name: module for module in fortran_project_to_semantic_modules(parse_fortran_project(str(tmp_path))) + }["m_mod"] + complete_python_export_policy(module) + complete_contract_imports([module]) + contract = PyiPrinter(normalize_public_names=True).emit(module) + + assert completed_contract_name(module.prototypes[0]) == "first_cb" + assert "from .helper_mod" not in contract + assert "f: first_cb\n" in contract diff --git a/tests/fortran/modules/semantics/test_modules_and_imports.py b/tests/fortran/modules/semantics/test_modules_and_imports.py index 33601bd02..b1582954a 100644 --- a/tests/fortran/modules/semantics/test_modules_and_imports.py +++ b/tests/fortran/modules/semantics/test_modules_and_imports.py @@ -1,6 +1,7 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" from prik.parsers.fortran.models import ( + FortranUseStatement, FortranArgument, FortranModule, ) @@ -24,14 +25,19 @@ def test_converter_normalizes_wrapped_types_and_resolves_wildcard_imports(): converter = FortranToIRConverter(wrapped_derived_types={("types_mod", "state_t")}) - module = FortranModule(name="consumer", uses={"OTHER_MOD": [], "TYPES_MOD": []}) + module = FortranModule( + name="consumer", + uses=[FortranUseStatement("OTHER_MOD"), FortranUseStatement("TYPES_MOD")], + ) context = converter._module_derived_type_context(module) state = converter.visit( FortranArgument(name="state", base_type="derived", kind="state_t"), derived_type_context=context, ).semantic_type - opaque_context = converter._module_derived_type_context(FortranModule(name="consumer", uses={"OPAQUE_MOD": []})) + opaque_context = converter._module_derived_type_context( + FortranModule(name="consumer", uses=[FortranUseStatement("OPAQUE_MOD")]) + ) opaque = converter.visit( FortranArgument(name="opaque", base_type="derived", kind="opaque_t"), derived_type_context=opaque_context, @@ -300,7 +306,9 @@ def test_fortran_to_ir_preserves_module_semantics_from_inline_source(): assert array_contract(semantic_arg.semantic_type).allocatable is True assert semantic_proc.projection[0].python_position == 0 assert semantic_dtype.base_classes == ["base"] - assert semantic_module.imports == ["iso_c_binding"] + # No declaration is written with a name `use iso_c_binding` supplies, and a + # compiler-supplied module has no contract to read one from. + assert semantic_module.imports == [] assert semantic_dtype.visibility == "private" assert semantic_proc.visibility == "public" assert semantic_file_modules[0].name == "m" diff --git a/tests/fortran/modules/semantics/test_reexport_accessibility.py b/tests/fortran/modules/semantics/test_reexport_accessibility.py new file mode 100644 index 000000000..df950b000 --- /dev/null +++ b/tests/fortran/modules/semantics/test_reexport_accessibility.py @@ -0,0 +1,1010 @@ +"""Fortran accessibility decides which use-associated entities a module publishes. + +Accessibility is settled by precedence: an access statement naming the entity +decides it, otherwise the module's bare `public`/`private` default does, and +that default is itself `public`. Those rules cover a use-associated entity, so +an ordinary module publishes what it imports without naming it anywhere. +""" + +from pathlib import Path + +import pytest + +from prik.parsers.fortran import parse_fortran_project +from prik.semantics.fortran2ir import fortran_project_to_semantic_modules + +DECLARING = """\ +module a_mod + implicit none + integer :: x = 7 + integer :: y = 9 + type :: box + integer :: value + end type box +contains + integer function scale_value(v) + integer, intent(in) :: v + scale_value = v * 2 + end function scale_value +end module a_mod +""" + + +def _reexports( + tmp_path: Path, + importer: str, + *, + module_name: str = "b_mod", +) -> list[tuple[str, str, str]]: + """Return one module's public use associations as (local, source, origin).""" + source = tmp_path / "project.f90" + source.write_text(f"{DECLARING}\n{importer}", encoding="utf-8") + modules = fortran_project_to_semantic_modules(parse_fortran_project([source])) + importing = next(module for module in modules if module.name == module_name) + return [(item.local_name, item.source_name, item.origin_module) for item in importing.reexports] + + +def test_a_default_public_module_publishes_what_it_imports(tmp_path: Path): + """No access statement is needed: the module default is public.""" + assert _reexports( + tmp_path, + """\ +module b_mod + use a_mod, only : x + implicit none +end module b_mod +""", + ) == [("x", "x", "a_mod")] + + +def test_a_declaration_dependency_remains_a_public_use_association(tmp_path: Path): + """Using an import in a declaration does not change its accessibility.""" + assert _reexports( + tmp_path, + """\ +module b_mod + use a_mod, only : crate => box + implicit none +contains + integer function crate_value(item) result(out) + type(crate), intent(in) :: item + out = item%value + end function crate_value +end module b_mod +""", + ) == [("crate", "box", "a_mod")] + + +def test_a_third_module_resolves_a_declaration_dependency_through_its_importer(tmp_path: Path): + """A public use association remains available to another Fortran module.""" + assert _reexports( + tmp_path, + """\ +module b_mod + use a_mod, only : box + implicit none + type(box) :: stored +end module b_mod + +module c_mod + use b_mod, only : box + implicit none + type(box) :: another +end module c_mod +""", + module_name="c_mod", + ) == [("box", "box", "a_mod")] + + +def test_explicit_public_still_publishes_a_declaration_dependency(tmp_path: Path): + """A named public statement is an explicit publication request.""" + assert _reexports( + tmp_path, + """\ +module b_mod + use a_mod, only : crate => box + implicit none + public :: crate +contains + integer function crate_value(item) result(out) + type(crate), intent(in) :: item + out = item%value + end function crate_value +end module b_mod +""", + ) == [("crate", "box", "a_mod")] + + +def test_a_bare_private_default_publishes_nothing_it_imports(tmp_path: Path): + """A bare `private` sets the default, which then covers the import.""" + assert ( + _reexports( + tmp_path, + """\ +module b_mod + use a_mod, only : x + implicit none + private +end module b_mod +""", + ) + == [] + ) + + +def test_an_access_statement_outranks_a_private_default(tmp_path: Path): + """Naming the entity decides it, whichever way the default points.""" + assert _reexports( + tmp_path, + """\ +module b_mod + use a_mod, only : x + implicit none + private + public :: x +end module b_mod +""", + ) == [("x", "x", "a_mod")] + + +def test_an_access_statement_outranks_a_public_default(tmp_path: Path): + """`private :: x` decides it even though the default is public.""" + assert ( + _reexports( + tmp_path, + """\ +module b_mod + use a_mod, only : x + implicit none + private :: x +end module b_mod +""", + ) + == [] + ) + + +def test_a_private_used_module_route_withholds_its_entities(tmp_path: Path): + """Naming the only used-module route private makes its entities private.""" + assert ( + _reexports( + tmp_path, + """\ +module b_mod + use a_mod + implicit none + private :: a_mod +end module b_mod +""", + ) + == [] + ) + + +def test_a_public_used_module_route_outranks_the_private_default(tmp_path: Path): + """A public route exposes its entities despite the module's bare default.""" + published = _reexports( + tmp_path, + """\ +module b_mod + use a_mod + implicit none + private + public :: a_mod +end module b_mod +""", + ) + + assert sorted(local for local, _source, _origin in published) == ["box", "scale_value", "x", "y"] + + +def test_any_public_route_keeps_a_multiply_accessible_entity_public(tmp_path: Path): + """One public route wins when another route to the same entity is private.""" + assert _reexports( + tmp_path, + """\ +module left_mod + use a_mod, only : x +end module left_mod + +module right_mod + use a_mod, only : x +end module right_mod + +module b_mod + use left_mod + use right_mod + implicit none + private :: left_mod + public :: right_mod +end module b_mod +""", + ) == [("x", "x", "a_mod")] + + +def test_a_renamed_default_public_import_publishes_the_local_name(tmp_path: Path): + """A rename changes the name this module publishes, never the declaration.""" + assert _reexports( + tmp_path, + """\ +module b_mod + use a_mod, only : renamed => y + implicit none +end module b_mod +""", + ) == [("renamed", "y", "a_mod")] + + +def test_a_plain_use_carries_the_public_names_of_what_it_reads(tmp_path: Path): + """A `use` naming no list carries every public name, default rules applying.""" + carried = _reexports( + tmp_path, + """\ +module b_mod + use a_mod + implicit none +end module b_mod +""", + ) + + assert sorted(local for local, _source, _origin in carried) == ["box", "scale_value", "x", "y"] + + +def test_a_plain_use_carries_a_named_generic_interface(tmp_path: Path): + """The offered-name inventory includes named interface declarations.""" + carried = _reexports( + tmp_path, + """\ +module generic_home + implicit none + interface convert + module procedure convert_i + module procedure convert_r + end interface convert +contains + integer function convert_i(value) + integer, intent(in) :: value + convert_i = value + end function convert_i + real function convert_r(value) + real, intent(in) :: value + convert_r = value + end function convert_r +end module generic_home + +module b_mod + use generic_home + implicit none +end module b_mod +""", + ) + + assert ("convert", "convert", "generic_home") in carried + + +def test_a_plain_use_under_a_private_default_carries_nothing(tmp_path: Path): + """The importing module's default decides what it publishes in turn.""" + assert ( + _reexports( + tmp_path, + """\ +module b_mod + use a_mod + implicit none + private +end module b_mod +""", + ) + == [] + ) + + +@pytest.mark.parametrize("kind", ["variable", "procedure"]) +def test_accessibility_decides_every_re_exportable_kind(kind: str, tmp_path: Path): + """The rule is about accessibility, so it does not single out one kind.""" + name = "x" if kind == "variable" else "scale_value" + published = _reexports( + tmp_path, + f"""\ +module b_mod + use a_mod, only : {name} + implicit none +end module b_mod +""", + ) + + assert published == [(name, name, "a_mod")] + + +CALLBACK_HOME = """\ +module callback_types + implicit none + abstract interface + integer function unary(x) + integer, intent(in) :: x + end function unary + end interface +end module callback_types +""" + + +def _callback_reexports(tmp_path: Path, importer: str, *, module_name: str) -> list[tuple[str, str, str]]: + """Return one module's public use associations over an abstract-interface home.""" + source = tmp_path / "callbacks.f90" + source.write_text(f"{CALLBACK_HOME}\n{importer}", encoding="utf-8") + modules = fortran_project_to_semantic_modules(parse_fortran_project([source])) + importing = next(module for module in modules if module.name == module_name) + return [(item.local_name, item.source_name, item.origin_module) for item in importing.reexports] + + +def test_a_plain_use_carries_an_abstract_interface_procedure(tmp_path: Path): + """An abstract block names no generic; what it declares are its procedures.""" + assert _callback_reexports( + tmp_path, + """\ +module middle_mod + use callback_types + implicit none +end module middle_mod +""", + module_name="middle_mod", + ) == [("unary", "unary", "callback_types")] + + +def test_an_abstract_interface_procedure_survives_a_further_hop(tmp_path: Path): + """Carrying it once makes it importable by name from the carrying module.""" + assert _callback_reexports( + tmp_path, + """\ +module middle_mod + use callback_types + implicit none +end module middle_mod + +module user_mod + use middle_mod, only : unary + implicit none +end module user_mod +""", + module_name="user_mod", + ) == [("unary", "unary", "callback_types")] + + +def test_a_callback_reached_through_a_public_route_stays_public(tmp_path: Path): + """Callback accessibility is the module's accessibility, routes included. + + A bare `private` would hide the name were the used module not named public, + so judging it by the symbol statements alone reaches the wrong answer. + """ + assert _callback_reexports( + tmp_path, + """\ +module facade_mod + use callback_types + implicit none + private + public :: callback_types +end module facade_mod +""", + module_name="facade_mod", + ) == [("unary", "unary", "callback_types")] + + +def test_a_callback_reached_through_a_private_route_is_withheld(tmp_path: Path): + """Naming the used module private withholds what it carried, default aside.""" + assert ( + _callback_reexports( + tmp_path, + """\ +module facade_mod + use callback_types + implicit none + private :: callback_types +end module facade_mod +""", + module_name="facade_mod", + ) + == [] + ) + + +def test_routes_that_agree_on_one_entity_publish_it(tmp_path: Path): + """Two `use` statements naming the same declaration name one entity.""" + assert _reexports( + tmp_path, + """\ +module middle_mod + use a_mod, only : x + implicit none +end module middle_mod + +module b_mod + use a_mod, only : x + use middle_mod, only : x + implicit none +end module b_mod +""", + ) == [("x", "x", "a_mod")] + + +def test_a_readable_route_beside_an_unreadable_one_is_not_guessed(tmp_path: Path): + """An unparsed module may carry the same entity or another one. + + Choosing the readable route would be a guess about the one this project + cannot read, so the name is left out rather than resolved to either. + """ + assert ( + _reexports( + tmp_path, + """\ +module b_mod + use a_mod, only : x + use external_mod, only : x + implicit none +end module b_mod +""", + ) + == [] + ) + + +def test_a_single_unreadable_route_still_names_what_it_reached(tmp_path: Path): + """One route names one entity, whether or not this project can read it.""" + assert _reexports( + tmp_path, + """\ +module b_mod + use external_mod, only : y + implicit none +end module b_mod +""", + ) == [("y", "y", "external_mod")] + + +def test_a_procedure_local_abstract_interface_stays_inside_its_procedure(tmp_path: Path): + """A block written inside a contained procedure declares a name only there. + + Those blocks are stored beside the module's own, so nothing but the + declaring scope distinguishes them. + """ + assert _reexports( + tmp_path, + """\ +module local_home + implicit none +contains + subroutine work() + abstract interface + subroutine local_callback() + end subroutine local_callback + end interface + end subroutine work +end module local_home + +module b_mod + use local_home + implicit none +end module b_mod +""", + ) == [("work", "work", "local_home")] + + +def test_a_procedure_local_generic_stays_inside_its_procedure(tmp_path: Path): + """A named generic declared inside a procedure is that procedure's, too.""" + carried = _reexports( + tmp_path, + """\ +module local_home + implicit none +contains + subroutine work() + interface local_generic + module procedure work + end interface local_generic + end subroutine work +end module local_home + +module b_mod + use local_home + implicit none +end module b_mod +""", + ) + + assert [local for local, _source, _origin in carried] == ["work"] + + +def test_a_wildcard_route_beside_an_unreadable_one_is_not_guessed(tmp_path: Path): + """A plain `use` compares routes the way a named import does. + + Discarding the unreadable route would leave the readable one standing + alone and answer for a module this project never read. + """ + assert ( + _reexports( + tmp_path, + """\ +module left_mod + use a_mod, only : x + implicit none +end module left_mod + +module right_mod + use external_mod, only : x + implicit none +end module right_mod + +module b_mod + use left_mod + use right_mod + implicit none +end module b_mod +""", + ) + == [] + ) + + +def test_wildcard_routes_that_agree_on_one_entity_publish_it(tmp_path: Path): + """Repeating a route to the same declaration names one entity.""" + assert _reexports( + tmp_path, + """\ +module left_mod + use a_mod, only : x + implicit none +end module left_mod + +module b_mod + use left_mod + use a_mod, only : x + implicit none +end module b_mod +""", + ) == [("x", "x", "a_mod")] + + +def test_a_name_spelled_inside_a_character_literal_is_not_a_dependency(tmp_path: Path): + """A literal's contents are its value, not a reference to what they spell.""" + source = tmp_path / "project.f90" + source.write_text( + f"""{DECLARING} +module b_mod + use a_mod, only : box + implicit none + character(len=3), parameter :: label = "box" +end module b_mod +""", + encoding="utf-8", + ) + modules = fortran_project_to_semantic_modules(parse_fortran_project([source])) + importing = next(module for module in modules if module.name == "b_mod") + + assert [(item.local_name, item.declaration_dependency) for item in importing.reexports] == [("box", False)] + + +def test_a_type_a_declaration_names_is_a_dependency(tmp_path: Path): + """Declaring with an imported type is what makes it a dependency.""" + source = tmp_path / "project.f90" + source.write_text( + f"""{DECLARING} +module b_mod + use a_mod, only : box + implicit none + type(box) :: item +end module b_mod +""", + encoding="utf-8", + ) + modules = fortran_project_to_semantic_modules(parse_fortran_project([source])) + importing = next(module for module in modules if module.name == "b_mod") + + assert [(item.local_name, item.declaration_dependency) for item in importing.reexports] == [("box", True)] + + +def test_a_compile_time_symbol_is_not_substituted_inside_a_character_literal(): + """A literal's contents are data, so a symbol spelled there is not a reference.""" + from prik.semantics.fortran2ir import _resolve_compile_time_text + + values = {"runtime": "4"} + + assert _resolve_compile_time_text('len("runtime")', values) == 'len("runtime")' + # A reference outside the literal is still resolved. + assert _resolve_compile_time_text("runtime + 1", values) == "4 + 1" + assert _resolve_compile_time_text('len("runtime") + runtime', values) == 'len("runtime") + 4' + + +TRANSITIVE_DECLARING = """\ +module a_mod + implicit none + integer :: x = 1 +end module a_mod +""" + +TRANSITIVE_OTHER = """\ +module c_mod + implicit none + real :: x = 2.0 +end module c_mod +""" + + +def _project_modules(tmp_path: Path, *sources: str): + """Parse one throwaway project and return its semantic modules by name.""" + (tmp_path / "project.f90").write_text("\n".join(sources), encoding="utf-8") + modules = fortran_project_to_semantic_modules(parse_fortran_project(str(tmp_path))) + return {module.name: module for module in modules} + + +def test_a_private_name_in_an_intermediate_module_ends_the_chain(tmp_path: Path): + """Each hop applies the accessibility rule, so a `private` stops the walk. + + `middle` imports `x` and makes it private, so `outer` cannot reach the + declaration behind it however `middle` got there. + """ + modules = _project_modules( + tmp_path, + TRANSITIVE_DECLARING, + """\ +module middle_mod + use a_mod, only : x + implicit none + private :: x +end module middle_mod + +module outer_mod + use middle_mod, only : x + implicit none +end module outer_mod +""", + ) + + reexports = {item.local_name: item for item in modules["outer_mod"].reexports} + assert reexports["x"].entity_kind == "unknown" + assert reexports["x"].origin_module == "middle_mod" + + +def test_routes_disagreeing_inside_an_intermediate_module_stay_unresolved(tmp_path: Path): + """`middle` reaches two different `x`, so no hop through it names one.""" + modules = _project_modules( + tmp_path, + TRANSITIVE_DECLARING, + TRANSITIVE_OTHER, + """\ +module middle_mod + use a_mod, only : x + use c_mod, only : x + implicit none +end module middle_mod + +module outer_mod + use middle_mod, only : x + implicit none +end module outer_mod +""", + ) + + reexports = {item.local_name: item for item in modules["outer_mod"].reexports} + assert reexports["x"].entity_kind == "unknown" + assert reexports["x"].origin_module == "middle_mod" + + +def test_an_ordinary_chain_still_reaches_the_declaring_module(tmp_path: Path): + """One accessible, unambiguous route per hop resolves to the declaration.""" + modules = _project_modules( + tmp_path, + TRANSITIVE_DECLARING, + """\ +module middle_mod + use a_mod, only : x + implicit none + public :: x +end module middle_mod + +module outer_mod + use middle_mod, only : x + implicit none +end module outer_mod +""", + ) + + reexports = {item.local_name: item for item in modules["outer_mod"].reexports} + assert (reexports["x"].entity_kind, reexports["x"].origin_module, reexports["x"].source_name) == ( + "variable", + "a_mod", + "x", + ) + + +def test_an_enumerator_is_carried_and_classified_as_the_constant_it_is(tmp_path: Path): + """An enum names constants, which is how every later stage models them.""" + modules = _project_modules( + tmp_path, + """\ +module colors_mod + implicit none + enum, bind(c) + enumerator :: red = 1 + enumerator :: green = 2 + end enum +end module colors_mod + +module facade_mod + use colors_mod + implicit none +end module facade_mod + +module named_facade_mod + use colors_mod, only : red + implicit none +end module named_facade_mod +""", + ) + + # A plain `use` carries every public name, enumerators included. + carried = {item.local_name: item.entity_kind for item in modules["facade_mod"].reexports} + assert carried == {"red": "variable", "green": "variable"} + + named = {item.local_name: item for item in modules["named_facade_mod"].reexports} + assert named["red"].entity_kind == "variable" + assert (named["red"].origin_module, named["red"].source_name) == ("colors_mod", "red") + + +def test_an_enumerator_initializer_is_a_declaration_dependency(tmp_path: Path): + """A name an enum's value reads expresses a declaration, so it is a dependency.""" + modules = _project_modules( + tmp_path, + """\ +module constants_mod + implicit none + integer, parameter :: base = 10 +end module constants_mod + +module colors_mod + use constants_mod, only : base + implicit none + enum, bind(c) + enumerator :: red = base + end enum +end module colors_mod +""", + ) + + reexports = {item.local_name: item for item in modules["colors_mod"].reexports} + assert reexports["base"].declaration_dependency is True + + +def test_a_named_and_a_wildcard_route_to_different_entities_stay_unresolved(tmp_path: Path): + """How a route entered says nothing about what it carries. + + `b_mod` reaches two different `x`, one through an `only` list and one + through a plain `use`. Examining the named route first would publish + `a_mod::x` as the canonical one, and a re-exported module variable + generates native access to that owner directly, so the Fortran compiler + never gets to diagnose the ambiguity. + """ + modules = _project_modules( + tmp_path, + TRANSITIVE_DECLARING, + TRANSITIVE_OTHER, + """\ +module b_mod + use a_mod, only : x + use c_mod + implicit none +end module b_mod +""", + ) + + assert [item.local_name for item in modules["b_mod"].reexports] == [] + + +def test_a_named_and_a_wildcard_route_to_one_entity_resolve_together(tmp_path: Path): + """Two routes naming one declaration are not a disagreement.""" + modules = _project_modules( + tmp_path, + TRANSITIVE_DECLARING, + """\ +module pass_mod + use a_mod + implicit none +end module pass_mod + +module b_mod + use a_mod, only : x + use pass_mod + implicit none +end module b_mod +""", + ) + + reexports = {item.local_name: item for item in modules["b_mod"].reexports} + assert (reexports["x"].entity_kind, reexports["x"].origin_module) == ("variable", "a_mod") + + +def test_an_unparsed_plain_use_carries_no_assumed_name(tmp_path: Path): + """PRIK cannot enumerate an unread module, so it is not a route for a name.""" + modules = _project_modules( + tmp_path, + TRANSITIVE_DECLARING, + """\ +module b_mod + use a_mod, only : x + use external_mod + implicit none +end module b_mod +""", + ) + + reexports = {item.local_name: item for item in modules["b_mod"].reexports} + assert (reexports["x"].entity_kind, reexports["x"].origin_module) == ("variable", "a_mod") + + +def test_mixed_routes_through_an_intermediate_module_stay_unresolved(tmp_path: Path): + """The rule is the same at every hop, whichever way each route entered.""" + modules = _project_modules( + tmp_path, + TRANSITIVE_DECLARING, + TRANSITIVE_OTHER, + """\ +module middle_mod + use a_mod, only : x + use c_mod + implicit none +end module middle_mod + +module outer_mod + use middle_mod, only : x + implicit none +end module outer_mod +""", + ) + + reexports = {item.local_name: item for item in modules["outer_mod"].reexports} + assert reexports["x"].entity_kind == "unknown" + assert reexports["x"].origin_module == "middle_mod" + + +def test_a_rename_without_only_still_carries_the_rest_of_the_module(tmp_path: Path): + """Only an `only` list narrows a `use`; a rename just binds another name. + + `use a_mod, p => q` accesses that entity as `p` and still carries whatever + else `a_mod` offers. Reading a non-empty mapping list as an `only` list + dropped every other name the module publishes. + """ + modules = _project_modules( + tmp_path, + """\ +module a_mod + implicit none + integer :: q = 1 + integer :: other = 2 +end module a_mod + +module b_mod + use a_mod, p => q + implicit none +end module b_mod +""", + ) + + reexports = {item.local_name: (item.origin_module, item.source_name) for item in modules["b_mod"].reexports} + assert reexports == {"p": ("a_mod", "q"), "other": ("a_mod", "other")} + + +def test_an_only_list_still_carries_nothing_else(tmp_path: Path): + """The narrowing form keeps narrowing.""" + modules = _project_modules( + tmp_path, + """\ +module a_mod + implicit none + integer :: q = 1 + integer :: other = 2 +end module a_mod + +module b_mod + use a_mod, only : q + implicit none +end module b_mod +""", + ) + + assert {item.local_name for item in modules["b_mod"].reexports} == {"q"} + + +def test_a_renamed_entity_is_not_also_carried_under_its_own_name(tmp_path: Path): + """`use m, p => q` accesses the entity as `p`, so `q` names nothing here.""" + modules = _project_modules( + tmp_path, + """\ +module a_mod + implicit none + integer :: q = 1 +end module a_mod + +module b_mod + use a_mod, p => q + implicit none +end module b_mod +""", + ) + + assert {item.local_name for item in modules["b_mod"].reexports} == {"p"} + + +def test_an_empty_only_list_carries_no_name(tmp_path: Path): + """`use m, only :` is valid syntax that narrows to nothing. + + It lists no names, exactly as a bare `use` does, so a model that cannot + tell the two apart reads one of them wrongly. + """ + modules = _project_modules( + tmp_path, + TRANSITIVE_DECLARING, + """\ +module b_mod + use a_mod, only : + implicit none +end module b_mod +""", + ) + + assert modules["b_mod"].reexports == [] + + +def test_statements_naming_one_module_are_read_together(tmp_path: Path): + """The language combines them, so neither statement erases the other.""" + modules = _project_modules( + tmp_path, + """\ +module a_mod + implicit none + integer :: q = 1 + integer :: other = 2 +end module a_mod + +module b_mod + use a_mod, only : p => q + use a_mod + implicit none +end module b_mod +""", + ) + + reexports = {item.local_name: (item.origin_module, item.source_name) for item in modules["b_mod"].reexports} + assert reexports == {"p": ("a_mod", "q"), "other": ("a_mod", "other")} + + +def test_a_non_only_rename_still_carries_imported_compile_time_symbols(tmp_path: Path): + """Every consumer reads the same association, not just route resolution. + + `use kinds_mod, wp => rk` binds `wp` and still imports `nmax`, which a + declaration's extent needs resolved. + """ + (tmp_path / "project.f90").write_text( + """\ +module kinds_mod + implicit none + integer, parameter :: rk = 8 + integer, parameter :: nmax = 4 +end module kinds_mod + +module use_mod + use kinds_mod, wp => rk + implicit none + real(wp) :: values(nmax) +end module use_mod +""", + encoding="utf-8", + ) + project = parse_fortran_project(str(tmp_path)) + declared = next( + variable + for parsed in project.files + for module in parsed.modules + if module.name == "use_mod" + for variable in module.variables + ) + + assert (declared.kind, declared.shape) == ("8", ["4"]) diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_f90/__init__.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_f90/__init__.pyi index f7d365f73..8f77c6659 100644 --- a/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_f90/__init__.pyi +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_f90/__init__.pyi @@ -1 +1,3 @@ from . import foptional_f90 + +__all__ = ["foptional_f90"] diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_f90/foptional_f90.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_f90/foptional_f90.pyi index 3cd02cc53..d226df026 100644 --- a/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_f90/foptional_f90.pyi +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_f90/foptional_f90.pyi @@ -1,6 +1,6 @@ from prik.contracts import Addr, Arg, Float64, Int32, Returns, String, native_call -class sample: +class Sample: def __init__( self, *, @@ -15,7 +15,7 @@ def summarize( scale: Int32 = ..., values: Float64[::] = ..., label: String = ..., - item: sample = ... + item: Sample = ... ) -> Int32: ... @native_call([Arg(0), Addr(Arg(1))]) @@ -35,3 +35,5 @@ def optional_status( base: Int32, status: Int32[()] = ... ) -> tuple[Int32, Returns["status", Int32[()]] | None]: ... + +__all__ = ["Sample", "summarize", "mutate_optional", "fill_optional", "optional_status"] diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_fixed/__init__.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_fixed/__init__.pyi index bc769ae74..2387de1fc 100644 --- a/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_fixed/__init__.pyi +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_fixed/__init__.pyi @@ -6,3 +6,5 @@ def optional_scale( base: Int32, factor: Int32 = ... ) -> Int32: ... + +__all__ = ["optional_scale"] diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/__init__.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/__init__.pyi index 9386e4b6a..953bc6910 100644 --- a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import optional_arguments_direct_bind_c_f90 + +__all__ = ["optional_arguments_direct_bind_c_f90"] diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/optional_arguments_direct_bind_c_f90.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/optional_arguments_direct_bind_c_f90.pyi index 7177288c4..dc255e522 100644 --- a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/optional_arguments_direct_bind_c_f90.pyi +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/optional_arguments_direct_bind_c_f90.pyi @@ -11,3 +11,5 @@ def optional_state( def add_optional( value: Float64 = ... ) -> Float64: ... + +__all__ = ["optional_state", "add_optional"] diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/__init__.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/__init__.pyi index 409f07adb..d89d23a32 100644 --- a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import optional_arguments_mixed_bind_c_f90 + +__all__ = ["optional_arguments_mixed_bind_c_f90"] diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/optional_arguments_mixed_bind_c_f90.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/optional_arguments_mixed_bind_c_f90.pyi index 3fdcc4893..a1d6fd393 100644 --- a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/optional_arguments_mixed_bind_c_f90.pyi +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/optional_arguments_mixed_bind_c_f90.pyi @@ -9,3 +9,5 @@ def direct_optional_state( def adapted_optional_value_state( value: Float64 = ... ) -> Int32: ... + +__all__ = ["direct_optional_state", "adapted_optional_value_state"] diff --git a/tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py b/tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py index 1f3d76f76..d1b1b5134 100644 --- a/tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py +++ b/tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py @@ -108,7 +108,7 @@ def test_optional_arguments_drive_fortran_present_behavior( assert "May be omitted or passed as None." in module.summarize.__doc__ values = np.array([1.0, 2.0, 3.0], dtype=np.float64) - item = module.sample() + item = module.Sample() item.value = np.int32(7) assert module.summarize(np.int32(5)) == np.int32(5) diff --git a/tests/fortran/optional_arguments/policy/test_optional_policy.py b/tests/fortran/optional_arguments/policy/test_optional_policy.py index 21b1a08d9..41839ca55 100644 --- a/tests/fortran/optional_arguments/policy/test_optional_policy.py +++ b/tests/fortran/optional_arguments/policy/test_optional_policy.py @@ -115,7 +115,7 @@ def alloc_state(value: Float64 | None) -> Int32: ... assert value.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION -def test_optional_passed_procedure_is_blocked_before_codegen(): +def test_optional_value_callback_dummy_is_blocked_before_codegen(): module = parse_pyi_text( """ @prototype @@ -131,4 +131,7 @@ def apply(callback: callback_shape) -> None: ... policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] assert isinstance(policy, FunctionWrapperPolicy) assert policy.supported is False - assert "callback argument 'value' cannot be optional" in policy.blockers + assert ( + "callback argument 'value' cannot be both optional and passed by value; " + "use a reference dummy so absence has a null-pointer ABI" + ) in policy.blockers diff --git a/tests/fortran/pointers/codegen/test_pointer_lowering.py b/tests/fortran/pointers/codegen/test_pointer_lowering.py index 46dd5862b..be5368ae1 100644 --- a/tests/fortran/pointers/codegen/test_pointer_lowering.py +++ b/tests/fortran/pointers/codegen/test_pointer_lowering.py @@ -77,7 +77,7 @@ def select_pointer(n: Int32) -> Annotated[ def test_pointer_plans_complete_descriptor_ownership_and_operations_before_lowering(): plan = _pointer_plan() namespace = plan.namespaces[0] - module_pointer = namespace.variables[0].native_array_handle + module_pointer = plan.variables[0].native_array_handle functions = {function.binding.python_name: function for function in namespace.functions} pointer_result = functions["make_pointer"].results[0].native_array_handle pointer_output = functions["select_pointer"].results[0] diff --git a/tests/fortran/pointers/end_to_end/fixtures/contracts/fpointers_f90/__init__.pyi b/tests/fortran/pointers/end_to_end/fixtures/contracts/fpointers_f90/__init__.pyi index 87f07ee9f..e991531a3 100644 --- a/tests/fortran/pointers/end_to_end/fixtures/contracts/fpointers_f90/__init__.pyi +++ b/tests/fortran/pointers/end_to_end/fixtures/contracts/fpointers_f90/__init__.pyi @@ -1 +1,3 @@ from . import fpointers_f90 + +__all__ = ["fpointers_f90"] diff --git a/tests/fortran/pointers/end_to_end/fixtures/contracts/fpointers_f90/fpointers_f90.pyi b/tests/fortran/pointers/end_to_end/fixtures/contracts/fpointers_f90/fpointers_f90.pyi index af965acdf..8c36684aa 100644 --- a/tests/fortran/pointers/end_to_end/fixtures/contracts/fpointers_f90/fpointers_f90.pyi +++ b/tests/fortran/pointers/end_to_end/fixtures/contracts/fpointers_f90/fpointers_f90.pyi @@ -20,3 +20,5 @@ def pointer_to_values( values: Annotated[Float64[::], Aliased], use_values: Int32 ) -> Annotated[Pointer[Float64[:]], PointerAssociation("runtime")]: ... + +__all__ = ["read_pointer", "pointer_to_scalar", "sum_pointer", "pointer_to_values"] diff --git a/tests/fortran/pointers/end_to_end/test_pointer_handles.py b/tests/fortran/pointers/end_to_end/test_pointer_handles.py index b33c95872..9996ed027 100644 --- a/tests/fortran/pointers/end_to_end/test_pointer_handles.py +++ b/tests/fortran/pointers/end_to_end/test_pointer_handles.py @@ -321,7 +321,7 @@ def test_module_and_derived_pointer_handles_track_native_association( assert module_handle.associated is False assert module_handle.shape is None - owner = module.pointer_box() + owner = module.Pointer_Box() field_handle = owner.values assert isinstance(field_handle, PointerArray) assert field_handle.owner is owner diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fcharacter_edges_f90/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fcharacter_edges_f90/__init__.pyi index 668cabd26..3c3f03c6a 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fcharacter_edges_f90/__init__.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fcharacter_edges_f90/__init__.pyi @@ -1 +1,3 @@ from . import fcharacter_edges_f90 + +__all__ = ["fcharacter_edges_f90"] diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fcharacter_edges_f90/fcharacter_edges_f90.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fcharacter_edges_f90/fcharacter_edges_f90.pyi index 7ab7d3861..c6f6c5ebb 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fcharacter_edges_f90/fcharacter_edges_f90.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fcharacter_edges_f90/fcharacter_edges_f90.pyi @@ -18,3 +18,5 @@ def make_out() -> String[6]: ... def unicode_echo( label: String ) -> String[5]: ... + +__all__ = ["fixed_inout", "assumed_inout", "optional_inout", "make_out", "unicode_echo"] diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/__init__.pyi index bf2f33a49..426379f01 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/__init__.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/__init__.pyi @@ -1 +1,3 @@ from . import fstring_descriptors_f90 + +__all__ = ["fstring_descriptors_f90"] diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/fstring_descriptors_f90.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/fstring_descriptors_f90.pyi index 0fab1f839..56b0299ec 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/fstring_descriptors_f90.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/fstring_descriptors_f90.pyi @@ -124,3 +124,34 @@ def pointer_result() -> Annotated[String[:], Ownership("python"), Transfer("snap @native_call([], result=Pointer(Return(0))) def fixed_pointer_result() -> Annotated[String[4], Ownership("python"), Transfer("snapshot_copy"), Destruction("python_refcount")] | None: ... + +__all__ = [ + "grow", + "shrink", + "drop", + "optional_grow", + "grow_both", + "grow_and_measure", + "measure", + "make", + "measure_fixed_allocatable", + "make_fixed_allocatable", + "relabel_fixed_allocatable", + "drop_fixed_allocatable", + "measure_pointer", + "point_at_static", + "edit_pointer_in_place", + "reassociate_pointer", + "deallocate_pointer", + "nullify_pointer", + "optional_pointer_measure", + "optional_pointer_edit", + "regrow_pointer", + "measure_fixed_pointer", + "point_at_fixed_static", + "relabel_fixed_pointer", + "allocatable_result", + "fixed_allocatable_result", + "pointer_result", + "fixed_pointer_result", +] diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi index 9248c87c6..d0825325f 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi @@ -1,47 +1,50 @@ -from prik.contracts import Int32, Returns, String, bind, standalone +from prik.contracts import Int32, Returns, String, standalone -@bind("CHAR_CODE_DEFAULT") @standalone def char_code_default( C: String[1] ) -> tuple[Int32, Returns["C", String[1]]]: ... -@bind("CHAR_CODE_STAR1") @standalone def char_code_star1( C: String[1] ) -> tuple[Int32, Returns["C", String[1]]]: ... -@bind("STRING_LEN_STAR8") @standalone def string_len_star8( TEXT: String[8] ) -> tuple[Int32, Returns["TEXT", String[8]]]: ... -@bind("STRING_LEN_ASSUMED") @standalone def string_len_assumed( TEXT: String ) -> tuple[Int32, Returns["TEXT", String]]: ... -@bind("STRING_LEN_ENTITY") @standalone def string_len_entity( TEXT: String[6] ) -> tuple[Int32, Returns["TEXT", String[6]]]: ... -@bind("CHAR_RESULT_DEFAULT") @standalone def char_result_default() -> String[1]: ... -@bind("STRING_RESULT_STAR8") @standalone def string_result_star8() -> String[8]: ... -@bind("STRING_RESULT_PADDED") @standalone def string_result_padded() -> String[8]: ... -@bind("STRING_RESULT_DECLARED") @standalone def string_result_declared() -> String[6]: ... + +__all__ = [ + "char_code_default", + "char_code_star1", + "string_len_star8", + "string_len_assumed", + "string_len_entity", + "char_result_default", + "string_result_star8", + "string_result_padded", + "string_result_declared", +] diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/__init__.pyi index 83c4333c3..c7edd646d 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/__init__.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/__init__.pyi @@ -1 +1,3 @@ from . import fstrings_f90 + +__all__ = ["fstrings_f90"] diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/fstrings_f90.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/fstrings_f90.pyi index b94124e47..7afaa30c9 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/fstrings_f90.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/fstrings_f90.pyi @@ -54,3 +54,22 @@ def replace_names( def rewrite_storage( label: String[8] ) -> Returns["label", String[8]]: ... + +__all__ = [ + "char_code_default", + "char_code_len1", + "char_code_kind1", + "char_code_c_char", + "string_len_fixed", + "string_len_assumed", + "string_len_c_char", + "char_result_default", + "char_result_c_char", + "string_result_fixed", + "string_result_padded", + "string_result_c_char", + "string_result_deferred", + "fixed_array_extent", + "replace_names", + "rewrite_storage", +] diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/__init__.pyi index 776d6c2dd..3e1122b0d 100644 --- a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import strings_direct_bind_c_f90 + +__all__ = ["strings_direct_bind_c_f90"] diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/strings_direct_bind_c_f90.pyi b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/strings_direct_bind_c_f90.pyi index 4c7c740ba..c151912ec 100644 --- a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/strings_direct_bind_c_f90.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/strings_direct_bind_c_f90.pyi @@ -22,3 +22,5 @@ def direct_uppercase_buffer( n: Int32, text: String[1][n] ) -> None: ... + +__all__ = ["direct_char_code", "direct_uppercase", "direct_buffer_sum", "direct_uppercase_buffer"] diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/__init__.pyi index 109ff5434..72bbdb4f6 100644 --- a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import strings_mixed_bind_c_f90 + +__all__ = ["strings_mixed_bind_c_f90"] diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/strings_mixed_bind_c_f90.pyi b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/strings_mixed_bind_c_f90.pyi index a0e88f3f9..9c04f8478 100644 --- a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/strings_mixed_bind_c_f90.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/strings_mixed_bind_c_f90.pyi @@ -9,3 +9,5 @@ def direct_char_code( def adapted_fixed_code( text: String[4] ) -> Int32: ... + +__all__ = ["direct_char_code", "adapted_fixed_code"] diff --git a/tests/fortran/strings/end_to_end/test_character_constant_quoting.py b/tests/fortran/strings/end_to_end/test_character_constant_quoting.py new file mode 100644 index 000000000..d0a14cd2a --- /dev/null +++ b/tests/fortran/strings/end_to_end/test_character_constant_quoting.py @@ -0,0 +1,71 @@ +"""A character constant reaches Python holding the characters it declares. + +Fortran doubles a quote to hold one, so `'don''t'` is five characters. Python +reads that same spelling as two literals written side by side and joins them, +which silently drops the quote. Both the generated contract and the built +extension therefore decode the Fortran literal rather than hand its text to a +Python reader. +""" + +from pathlib import Path + +import pytest + +from prik.pipeline.build import build_fortran_extension +from tests.fortran._support.wrapper_build import _generate_checked_pyi_contract, _import_from_build_dir + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = """\ +module quoting_mod + use iso_c_binding, only : c_char + implicit none + character(len=5), parameter :: word = 'don''t' + character(len=3), parameter :: pair = "a""b" + character(len=4), parameter :: plain = 'abcd' + character(kind=c_char, len=3), parameter :: tagged = c_char_'abc' + character(len=3), parameter :: numbered = 1_'xyz' + character(len=5), parameter :: tagged_quote = c_char_'don''t' +end module quoting_mod +""" + + +@pytest.fixture(scope="module") +def built(tmp_path_factory): + """Build the quoting source once for the read-only checks.""" + tmp_path = tmp_path_factory.mktemp("character_quoting") + source = tmp_path / "quoting.f90" + source.write_text(SOURCE, encoding="utf-8") + result = build_fortran_extension(source, output_dir=tmp_path / "build", output_name="quoting_api") + return _import_from_build_dir(result.module_name, result.output_dir) + + +def test_a_doubled_quote_reaches_python_as_one_quote(built): + """Each constant holds exactly the characters its declared length counts.""" + assert built.quoting_mod.word == "don't" + assert built.quoting_mod.pair == 'a"b' + assert built.quoting_mod.plain == "abcd" + + +def test_a_literal_states_its_kind_without_the_kind_joining_the_value(built): + """A literal's kind is a type fact, so only its characters are the value.""" + assert built.quoting_mod.tagged == "abc" + assert built.quoting_mod.numbered == "xyz" + assert built.quoting_mod.tagged_quote == "don't" + + +def test_a_generated_contract_states_the_declared_characters(tmp_path: Path): + """The contract publishes the same value the extension returns.""" + source = tmp_path / "quoting.f90" + source.write_text(SOURCE, encoding="utf-8") + contracts = tmp_path / "contracts" + + _generate_checked_pyi_contract(source, contracts, None) + contract = (contracts / "quoting_mod.pyi").read_text(encoding="utf-8") + + assert 'word: Final[String[5]] = "don\'t"' in contract + assert "pair: Final[String[3]] = 'a\"b'" in contract + assert "plain: Final[String[4]] = 'abcd'" in contract + assert "tagged: Final[String[3]] = 'abc'" in contract + assert "numbered: Final[String[3]] = 'xyz'" in contract + assert 'tagged_quote: Final[String[5]] = "don\'t"' in contract diff --git a/tests/fortran/strings/parsing/test_character_length_parsing.py b/tests/fortran/strings/parsing/test_character_length_parsing.py index dbef8de95..a9d011bcc 100644 --- a/tests/fortran/strings/parsing/test_character_length_parsing.py +++ b/tests/fortran/strings/parsing/test_character_length_parsing.py @@ -1,6 +1,8 @@ """Declaration parsing, interfaces, and less common scope edges.""" from prik.parsers.fortran import parse_fortran_file +from prik.parsers.fortran.models import FortranFile, FortranVariable +from prik.semantics.fortran2ir import FortranToIRConverter, collect_semantic_compile_time_requirements def test_character_entity_lengths_and_assumed_bounds_are_preserved(): @@ -20,3 +22,74 @@ def test_character_entity_lengths_and_assumed_bounds_are_preserved(): assert args["table"].shape == ["0:"] assert args["table"].lbound == ["0"] assert args["table"].ubound == [None] + + +def test_a_character_selector_separates_a_comma_bearing_kind_from_its_length(): + """Either selector expression may hold commas, so both are read whole.""" + parsed = parse_fortran_file( + """\ +module selector_mod + use iso_c_binding, only : c_char + implicit none + character(len=8, kind=max(c_char, 1)) :: spread_out +end module selector_mod +""", + filename="selector.f90", + ) + declared = parsed.modules[0].variables[0] + + assert declared.character_length_expression == "8" + assert declared.character_kind_expression == "max(c_char, 1)" + assert declared.character_length_syntax is False + + +def test_a_character_model_states_only_the_selector_it_records(): + """The `kind` text alone cannot say whether it spells a length or a kind. + + Every producer of a character model records its selector through one + reader, so a model built without one states neither -- which is what a + bare `character` declaration means -- rather than leaving a second reader + to guess from the joined text. + """ + bare = FortranVariable(name="text", base_type="character", kind="c_char") + + assert bare.character_length_expression is None + assert bare.character_kind_expression is None + assert bare.character_length_syntax is False + + named = FortranVariable(name="text", base_type="character", kind="len=12, kind=c_char") + named.record_character_selector("(len=12, kind=c_char)") + + assert named.character_length_expression == "12" + assert named.character_kind_expression == "c_char" + assert named.character_length_syntax is False + + positional = FortranVariable(name="text", base_type="character", kind="8") + positional.record_character_selector("(8)") + + assert positional.character_length_expression == "8" + assert positional.character_kind_expression is None + assert positional.character_length_syntax is True + + +def test_a_recorded_selector_is_what_semantics_reads_for_a_character_kind(): + """The recorded selector is the authority, not the legacy `kind` field. + + A model built through the recorder alone leaves that field empty, so + consulting it first would report the default character kind while the + model plainly states another one. + """ + converter = FortranToIRConverter() + + recorded = FortranVariable(name="x", base_type="character") + recorded.record_character_selector("(kind=c_char)") + + assert recorded.kind == "" + assert converter._semantic_kind_key(recorded) == "c_char" + assert converter._target_type_key(recorded) == ("character", "c_char") + + unsupported = FortranVariable(name="x", base_type="character") + unsupported.record_character_selector("(kind=bad)") + requirements = collect_semantic_compile_time_requirements(FortranFile(variables=[unsupported])) + + assert [(item["symbol"], item["kind"], item["expression"]) for item in requirements] == [("x", "bad", "bad")] diff --git a/tests/fortran/strings/semantics/test_fortran_string_semantics.py b/tests/fortran/strings/semantics/test_fortran_string_semantics.py index e7d28deaf..d701412b6 100644 --- a/tests/fortran/strings/semantics/test_fortran_string_semantics.py +++ b/tests/fortran/strings/semantics/test_fortran_string_semantics.py @@ -24,3 +24,57 @@ def test_scalar_character_inout_is_projected_as_replacement_return(): assert mapping.python_position == 0 assert mapping.native_position == 0 assert mapping.result_position == 0 + + +def test_a_character_length_keeps_the_commas_its_own_expression_holds(): + """The selector's two expressions are separated, not split back apart. + + `len=` and `kind=` are one parenthesized selector, and either may hold a + comma of its own, so finding the length inside a joined spelling cuts an + expression such as `max(4, n)` short at its first comma. + """ + parsed = parse_fortran_source( + """ +module selector_mod + use iso_c_binding, only : c_char + implicit none +contains + subroutine take(n, name) + integer, intent(in) :: n + character(len=max(4, n), kind=c_char), intent(in) :: name + end subroutine take +end module selector_mod +""", + filename="selector_mod.f90", + ) + + function = get_function(fortran_module_to_semantic_module(parsed.modules[0]), "take") + argument = next(item for item in function.arguments if item.name == "name") + + assert argument.semantic_type.metadata["fortran_character_length"] == "max(4, n)" + + +def test_every_character_declaration_form_states_its_own_length(): + """Separating the length leaves the ordinary spellings reading as before.""" + parsed = parse_fortran_source( + """ +module forms_mod + implicit none + integer, parameter :: fixed = 6 +contains + subroutine forms(a, b, c, d, e) + character(len=16), intent(in) :: a + character(8), intent(in) :: b + character(*), intent(in) :: c + character(len=fixed), intent(in) :: d + character, intent(in) :: e + end subroutine forms +end module forms_mod +""", + filename="forms_mod.f90", + ) + + function = get_function(fortran_module_to_semantic_module(parsed.modules[0]), "forms") + lengths = {item.name: item.semantic_type.metadata.get("fortran_character_length") for item in function.arguments} + + assert lengths == {"a": "16", "b": "8", "c": "*", "d": "6", "e": "1"} diff --git a/tests/fortran/strings/semantics/test_string_pyi_semantics.py b/tests/fortran/strings/semantics/test_string_pyi_semantics.py index 8de38d5c5..7f9ddb587 100644 --- a/tests/fortran/strings/semantics/test_string_pyi_semantics.py +++ b/tests/fortran/strings/semantics/test_string_pyi_semantics.py @@ -79,7 +79,7 @@ def array_assumed_strided(values: String[...][::]) -> None: ... assert assumed_type.metadata["fortran_character_length"] == "*" assert assumed_type.rank == 1 assert assumed_type.shape == [":"] - assert array_assumed_strided.arguments[0].semantic_type.shape == ["::Strided"] + assert array_assumed_strided.arguments[0].semantic_type.shape == ["::"] emitted = emit_module(module) assert "value: String" in emitted diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/__init__.pyi b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/__init__.pyi index d3de141ea..4bf6c13f4 100644 --- a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/__init__.pyi +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import subroutines_direct_bind_c_f90 + +__all__ = ["subroutines_direct_bind_c_f90"] diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/subroutines_direct_bind_c_f90.pyi b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/subroutines_direct_bind_c_f90.pyi index ea200cbff..00321755b 100644 --- a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/subroutines_direct_bind_c_f90.pyi +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/subroutines_direct_bind_c_f90.pyi @@ -11,3 +11,5 @@ def direct_reference( def direct_outputs( value: Int32 ) -> tuple[Returns["value", Int32], Int32, Int32]: ... + +__all__ = ["direct_reference", "direct_outputs"] diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/__init__.pyi b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/__init__.pyi index 8c2a6e70d..9662c7980 100644 --- a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/__init__.pyi +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/__init__.pyi @@ -1 +1,3 @@ from . import subroutines_mixed_bind_c_f90 + +__all__ = ["subroutines_mixed_bind_c_f90"] diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/subroutines_mixed_bind_c_f90.pyi b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/subroutines_mixed_bind_c_f90.pyi index b450f4a73..dd10ad7c8 100644 --- a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/subroutines_mixed_bind_c_f90.pyi +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/subroutines_mixed_bind_c_f90.pyi @@ -10,3 +10,5 @@ def direct_outputs( def adapted_outputs( value: Int32 ) -> tuple[Returns["value", Int32], Int32]: ... + +__all__ = ["direct_outputs", "adapted_outputs"] diff --git a/tests/fortran/subroutines/end_to_end/test_assumed_scalar_intent.py b/tests/fortran/subroutines/end_to_end/test_assumed_scalar_intent.py index d03c44744..8515b18c0 100644 --- a/tests/fortran/subroutines/end_to_end/test_assumed_scalar_intent.py +++ b/tests/fortran/subroutines/end_to_end/test_assumed_scalar_intent.py @@ -48,7 +48,7 @@ def test_assumed_scalar_intent_returns_only_the_function_result(tmp_path: Path): def test_assumed_scalar_intent_keeps_array_and_derived_writeback(tmp_path: Path): module = _module(tmp_path, assume_intent_in_scalars=True) - item = module.sample(x=np.float64(1.0)) + item = module.Sample(x=np.float64(1.0)) values = np.array([1.0, 2.0, 3.0], dtype=np.float64) assert module.touch(np.int32(5), item, values) is None diff --git a/tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py b/tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py index c59090887..191876a31 100644 --- a/tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py +++ b/tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py @@ -47,7 +47,7 @@ def test_subroutine_outputs_and_caller_storage_follow_documented_projection_rule assert module.no_intent_scalar(no_intent) == np.float64(6.0) assert no_intent == np.float64(5.0) - point = module.point() + point = module.Point() assert module.fill_point(point) is None assert point.x == np.float64(9.5)