Conversation
Documents the selected approach for mutable character(len=:) arguments and the two rejected alternatives, so the remaining work can start from a clean session without re-deriving the boundary. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A scalar character dummy carrying `allocatable` or `pointer` needs an adapter local with the same attribute; the generated adapter always built a plain fixed-length temporary, which the Fortran compiler rejects. Most of these forms therefore stopped at a policy diagnostic, and the declared-length ones reached gfortran or plan validation and failed there. Policy now completes the adapter-local storage each dummy needs -- its attribute, its length, and who releases it -- as `CharacterLocalPolicy`, replacing the narrower `deferred_character_length` fact. Every direction is supported at deferred and declared length: `intent(in)`, `intent(out)`, `intent(inout)`, and function results. The C ABI is unchanged; a scalar character argument still crosses as a byte buffer and a length. A `pointer` local is storage the adapter allocated, so its release is a completed decision. A read-only dummy cannot reassociate, so the adapter always frees it. A mutable dummy may be reassociated or deallocated by the native procedure, so the adapter frees its allocation only while the dummy still identifies it -- freeing the seed unconditionally double-frees the ordinary deallocate-then-reallocate idiom, so a reassociating procedure orphans the call-local allocation instead. An `allocatable` character function result is moved out through an allocatable dummy rather than assigned, which makes allocation a testable fact, so an unallocated result becomes `None`. Separately, pointer array handles now expose `deallocate()` without a `PointerPolicy` annotation, matching what allocatable handles already offered. Release stays manual and caller-driven -- prik never frees a native target on its own -- so this is the same responsibility a Fortran caller takes writing `deallocate`. Previously a procedure returning freshly allocated pointer storage leaked with no way to reclaim it from Python. Stages changed: policy (ownership, completion, construction), planning (models, planner), codegen (Fortran bridge), pipeline validation, and docs. Verified with the full Fortran suite (2213 passed), docs/c/tools/workflows (1188 passed), and the static-analysis gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The live-view lane for module arrays required a primitive numeric element type, so a `character(len=8), target :: labels(3)` module array was rejected before generation even though the same declaration works as a wrapped argument and as an allocatable or pointer module array. A character element differs from a numeric one only in carrying its Fortran element length as the dtype width, so the borrowed view now selects `NPY_STRING` with that itemsize instead of a NumPy scalar type macro, and sizes its stride from the same length. Native writes appear in the view Python already holds, and Python writes reach the storage Fortran reads, at any rank and preserving Fortran order. The `target` attribute stays required, exactly as it already was for numeric module arrays: the lane borrows addressable storage rather than copying. Stages changed: policy (module-variable construction), codegen (C binding), docs. Verified with the full Fortran suite (2214 passed) and the docs suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A scalar character module variable was rejected before generation because the scalar accessor lane required a primitive rank-zero type. Only string `parameter` constants had a path, and those materialize at build time without touching native storage, so mutable character module state was unreachable from Python while the identical declaration already worked as a derived-type field. Policy now completes a `character_value` getter action for a declared-length rank-zero character module variable, and grants it the same write-through setter a character field already had. A character value has no by-value C ABI, so the generated accessors copy through the fixed-width byte buffer the field accessors already use: the Fortran side transfers to and from a `character(kind=c_char), dimension(n)` parameter, and the binding decodes UTF-8 on read and requires exactly the declared byte width on write, refusing to truncate or pad. The write-through decision is scoped to a declared width. An assumed-length `character(len=*)` module variable has no width to write into, so it keeps its rejecting setter rather than claiming a lane code generation cannot serve -- which is what three existing diagnostic tests were pinning, and they pass unchanged. `allocatable` and `pointer` scalar character module variables stay blocked; they need the descriptor lane, not this one. Stages changed: policy (ownership, construction, models), planning (models, planner, entrypoints), codegen (Fortran bridge, C binding), docs. Verified with the full Fortran suite (2217 passed), docs/c/tools/workflows (1188 passed), and the static-analysis gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three character module-variable shapes were rejected before generation while the identical declarations already worked elsewhere in the wrapper. An `allocatable` or `pointer` scalar now reads through the same nullable snapshot a descriptor numeric scalar already used. A descriptor character has no width until it is allocated, so the bridge getter reports the length beside the copied bytes and the binding decodes exactly that many; an unallocated or unassociated descriptor is `None`, and a failed allocation is a `MemoryError` rather than a false absence, which only the reported width separates. A `character` `parameter` array now takes the same import-time snapshot a numeric parameter array took: the bridge getter copies the compiler-owned parameter into persistent storage once, and the binding copies that into one read-only Python-owned array. Only the element spelling changes -- the Fortran snapshot declares the character length, and the binding allocates from that itemsize instead of a NumPy scalar type number. Numeric parameter arrays keep their existing `PyArray_EMPTY` lowering unchanged. Assumed-length (`character(len=*)`) module state stays refused, and now with a diagnostic rather than a codegen failure: its width comes from an initializer prik does not evaluate, so a `parameter` array is blocked in policy and a scalar keeps its rejecting setter. Stages changed: policy (construction), planning (entrypoints), codegen (Fortran bridge, C binding), docs. Verified with the full Fortran suite (2220 passed), docs/c/tools/workflows (1188 passed), and the static-analysis gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two module-array shapes never compiled, on this branch or before it: a declared-length character array with `allocatable` or `pointer`. The generated descriptor-consumer interface and the descriptor ABI parameter both spelled `character(kind=c_char, len=:)` whatever the array declared, and Fortran accepts a deferred-length actual for such a dummy only when the dummy declares one too. Both now spell the width the array carries. The same rule is now applied to element widths generally: a character module array reports its own width through its accessor rather than having the binding restate one. The copied-parameter getter and the borrowed-view getter each gain an `itemsize` output beside their extents, set from `len(...)` on the native variable. That removes the last place a width had to be known before generation, so an assumed-length `character(len=*)` parameter array now works and takes the dtype width its initializer implied -- it was refused by a policy diagnostic before, and crashed in code generation before that. A deferred-length `character(len=:), allocatable` module array is still unbuildable, but for a reason outside prik: GNU Fortran 11.4 raises an internal compiler error on that declaration. It is left out of the regression fixture with a note rather than blocked in policy. Stages changed: policy (construction), planning (entrypoints), codegen (Fortran bridge, C binding), docs. Verified with the full Fortran suite (2222 passed), docs/c/tools/workflows (1188 passed), and the static-analysis gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README named "writing to an assumed-length (`character(len=*)`) character module variable" as a limitation. That form does not exist: Fortran rejects an assumed-length entity that is neither a dummy argument nor a PARAMETER, so prik never sees one. The claim was also hiding the real limitation, which is that `allocatable` and `pointer` module scalars are read-only snapshots -- for numeric state exactly as much as for `character` state, so it belongs under storage and ownership rather than in the character row. The feature matrix said nothing about `character` module state at all, and still described character arrays as partially supported with a dtype width the binding restates; both now match what the accessors do. Also recorded that a deferred-length `character(len=:), allocatable` module array does not build under GNU Fortran 11.4, which raises an internal compiler error on that declaration. Verified by building and running each form rather than by completing policy: declared-length scalars read and write, descriptor scalars read as `str` or `None` and reject assignment, parameters read as constants, and `len=*` works in the two places Fortran allows it -- a dummy argument and a PARAMETER. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening a generated wrapper meant reading procedures that ran together with no separation and no indication of what any of them was for, leaving a reader to reconstruct each one's purpose from the wrapper plan. Generated Fortran modules now put a blank line before each procedure, and both backends emit a short leading comment on the procedures they generate: an adapter names the native procedure it wraps, the C symbol it exports, and the conversions it performs; a binding function names the Python callable it serves and the entrypoint it calls; module accessors say in plain terms what they read or write rather than naming a policy enum, which is prik vocabulary rather than anything a reader of generated source can use. Two constraints shaped this. Free-form Fortran caps a line at 132 columns and a generated procedure is indented inside its module, so comment prose is wrapped well short of that; a conversion summary otherwise overran the limit and failed to compile. Each backend also reads only its own plan facet, so the binding never names a Fortran symbol and the bridge never names a Python one -- the first draft violated that in both directions and the facet-boundary test caught it. Stages changed: codegen nodes, both printers, Fortran bridge, C binding, and the developer documentation examples that show generated output. The exact-bytes codegen baseline is updated to the new output. Verified with the full Fortran suite (2222 passed), docs/c/tools/workflows (1188 passed), and the static-analysis gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An audit of this branch against main found six issues, all in code added here. None changed behavior; the full suite passes unchanged at 2222. Two were stage violations. Selecting the move collector for an allocatable character function result re-derived, in code generation, the fact that its storage may be absent -- the equivalent array result reads that as completed policy (`result_allocation is MAYBE_UNALLOCATED`). Policy now states it as `ScalarDescriptorResultPolicy.may_be_unallocated` and both collectors read a completed fact. Separately, both backends chose the module *setter* lowering by reading the *getter* action; the bridge now dispatches on `AssignmentMode.CHARACTER_COPY` and the binding on `setter_converts_characters`, each stating the mechanism on its own facet. The plan validator rejected the new assignment mode until it was taught it, which is that stage working as intended. The rest were minimality and duplication. `FortranComment` was added with a printer visitor but never constructed, since procedure prose travels on the function node's `doc` field; both are removed. `_module_array_element_type` was left as a one-line wrapper with a single caller once its character branch moved, and is inlined. Character-length normalization existed twice, in ownership and construction, with the same accepted spellings maintained separately; construction now uses the one parser that lives beside the other character metadata readers it already imports. Facet separation is intact in both directions: the bridge reads no binding fact or Python name, and the binding reads no bridge or adapter fact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oal3 checklist for handling C
Fortran 2008 derived-type support, taken far enough that BSPLINE-FORTRAN
wraps unmodified.
Abstract types and deferred bindings
A `type, abstract ::` declaration becomes a Python class with no
constructor; instantiating it raises TypeError naming the concrete
extensions. Its extensions stay ordinary Python subclasses. A deferred
binding is declared on the base and resolved by the object's own type
through the polymorphic discriminator the bridge already generated, so
no new emitted-code mechanism was needed. An abstract type publishes no
component accessors of its own and is excluded from the polymorphic
cases a caller can supply.
Generic constructors
`interface <typename>` is that type's constructor: its specifics become
one overloaded `__init__`. A specific that is private in its module is
reached through the public type name. A constructor carries no `@bind`
-- the class name states the generic that reaches it -- and `@private`
on `__init__` is refused.
Accessibility statements
A derived type's `private`/`public` statements are honored for both
components and type-bound procedures. The statement after `contains`
previously failed to parse; the one before it parsed but was discarded,
so private components reached the compiler as accessors that read them.
Parser and probe
Deferred bindings and named `block` constructs parse. A `type, public ::`
declaration is no longer hidden by a module `private` default, which
silently dropped the type and every method. The compiler type probe no
longer emits expressions naming project symbols it cannot resolve.
bind(C)
A module whose only procedures are `bind(C)` now installs the native
support its derived-type accessors call, fixing an undefined-symbol link
failure.
Contracts
Every build writes its semantic `.pyi` beside the extension, under
`contracts/` in the build directory. `@abstract` and `@abstractmethod`
join the contract vocabulary; `@native_type(attributes=('public',))` is
no longer emitted, since `public` is the default.
Example
examples/bspline wraps BSPLINE-FORTRAN 7.4.0 unmodified and validates
both interfaces against analytic values and scipy.interpolate. It is the
first example project in modern Fortran rather than FORTRAN 77.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A generic interface whose specifics carry an `intent(out)` argument could not be reloaded from its own generated contract. The declaration states the public signature, so an output the projection turns into a result is not one of the arguments it accepts -- but the check compared the declaration against the specific's native argument list, which still contained it. Every such generic was rejected, which is the common shape in numerical Fortran: BSPLINE-FORTRAN's `db1ink`, `db1val`, and the type-bound `initialize` all failed. One projection rule now applies to both signatures, and the same rule drives a type-bound generic's receiver search. The projected-result comparison is additive, so a declaration that already matched its target's own return keeps matching. The three bspline contract modules now load; the remaining blocker for rebuilding that project from its contract is a callback argument (`procedure(b1fqad_func) :: fun`), which is separate work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tests/c/` was organized by pipeline stage alone -- `parsing/`,
`semantics/`, `preprocessing/`, `probes/`, `cli/` -- while `tests/`
documents one shape for the whole tree:
tests/<language>/<documented-feature>/<owning-stage>/
That left C with no home for the `policy/`, `codegen/`, and `end_to_end/`
owners its wrapper work needs, and it mixed documented behavior with
internal mechanism in one directory.
Every file moves; none is rewritten. Four were also misfiled rather than
just mis-shaped: the C parser CLI coverage sat under `parsing/`, and the
lexer, public-API, model-serialization, and JSON-shape tests protect
internal mechanisms rather than documented C behavior, so they move to
`infrastructure/` beside their production package.
The shared conversion helpers move to `tests/c/_support/semantic_conversion.py`,
matching the Fortran support module of the same name. `tests/c/fixtures/`
stays where it is: it is read through `_support/fixture_outputs.py`, which
anchors on `tests/c/`, and several features share it.
496 passed, 1 skipped -- the same counts as before the move.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reorganised C tree kept enums inside `records/`, so the directory vocabulary claimed structs and unions but silently held enum coverage too. Move the enum-owned tests to `enumerations/parsing/` and `enumerations/semantics/`, matching the Fortran tree's feature names. Tests that assert on records *and* enums in one parse (duplicate tag diagnostics) stay in `records/`, since their invariant spans both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The C test tree moved to `<feature>/<stage>/`, leaving every test path in the deferred parser reference stale. Update them, and list the new enum parsing owner alongside the records one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moving the C and Fortran suites to `<owner>/<stage>` updated everything inside `tests/`, but several references outside it still named the old paths. The tracked pre-push hook was the worst: it pointed at a wrapper smoke node that no longer collects, so pytest exited 4 and every push from a clone with `core.hooksPath` enabled was blocked. Repoint the hook, the two published feature-matrix evidence links, the golden-regeneration command in the C parser fixture README, and three package-level pointers. Reconcile both owner tables with the tree: drop the `infrastructure/types/` row for a directory that does not exist, add `printers/`, and record the C `execution_examples/` owner. Vulture matches `fnmatch` against the resolved absolute path, so every repo-relative pattern in its exclude list had silently matched nothing since it was written. Rewrite them with a leading `*/`, which also restores coverage of the relocated build fixtures. Finally, replace the depth-coupled `Path(__file__).parents[N]` arithmetic that reached across owners -- three sites had grown to `parents[5]` -- with anchors in `tests/<language>/_support/paths.py`. Each root was previously defined in four places at four different depths; a later move would have resolved them to the wrong directory instead of failing. `_visit_FortranModule` also crossed the staged complexity limit when abstract types landed, which blocks the same pre-push hook; extract `_record_abstract_type_names` to bring it back under. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wrapping abstract types and generic constructors made three published claims false, and nothing caught it because no test reads these files. The generic-interfaces guide still said source generic interfaces are never inferred as constructors; an interface named for a derived type has been that type's constructor since generic constructors landed. The README still listed abstract types and deferred bindings among the forms PRIK rejects, and described the real constructor diagnostics as "ambiguous or incomplete" candidates -- vague enough to be unactionable. The actual rejections are a shared runtime signature between overload candidates, and edited `.pyi` constructors that omit `@bind` or sit alongside the generated field constructor; name those instead. In the coverage table, the generic-interface limitations row claimed Blocked status and cited two tests deleted with the behavior they pinned. Restate it as partially supported, point it at the constructor inference and keyword-field evidence, and repoint the inheritance and semantic `.pyi` rows at live negative evidence. Four node IDs left over from earlier commits stay untouched: their tests were renamed alongside a behavior change from blocked to supported, so substituting the new names would record a claim their owners never made. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Current limitations" documents Fortran forms PRIK will not wrap. A hand-edited `.pyi` whose constructor declarations contradict each other is not such a form -- it is a malformed contract, and the diagnostic naming it is the tool working. Overload candidates that share one runtime signature are likewise already stated as a rule in the generic-interfaces Key Rules, not a boundary on what can be wrapped. Drop both from the README and the generic-interfaces limitations, and narrow the coverage row to the dimensions that remain documented limitations. The contradictory-constructor test keeps its four citations on the `.pyi` contract-format rows, where the diagnostic belongs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.