Skip to content

Devel doc - #63

Open
saidctb wants to merge 26 commits into
mainfrom
devel-doc
Open

Devel doc#63
saidctb wants to merge 26 commits into
mainfrom
devel-doc

Conversation

@saidctb

@saidctb saidctb commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

No description provided.

saidctb and others added 22 commits August 18, 2026 18:58
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant