Skip to content

fix: move generics into a lowerer instead of resolver phase#1820

Open
ghaith wants to merge 7 commits into
masterfrom
fix/generic-monomorph-lowering
Open

fix: move generics into a lowerer instead of resolver phase#1820
ghaith wants to merge 7 commits into
masterfrom
fix/generic-monomorph-lowering

Conversation

@ghaith

@ghaith ghaith commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Move generic function-call resolution out of the type annotator's first pass into a dedicated
GenericLowerer lowering phase that materializes each monomorphization as a real {external} POU
in the AST. This makes scalar and aggregate-return generics behave uniformly and turns a
genuinely-missing monomorphization into a clean link-time error instead of a silent,
byte-reinterpreting mis-dispatch (which could segfault for e.g. TO_STRING/TO_WSTRING on numeric
or unsupported types).

Background

Generic monomorphizations used to be resolved during annotation and registered only as synthetic
index entries — never real declarations that survive a re-index. This caused a scalar-vs-aggregate
divergence:

  • Scalar generic calls kept a generic operator and emitted an external call the linker resolved.
  • Aggregate-return generic calls were re-resolved by the aggregate-return lowerer, which could
    bind e.g. TO_STRING(aDint) to TO_STRING__STRING and reinterpret the argument's bytes → garbage
    or a crash.

The root cause was that the aggregate lowerer was effectively re-doing the resolver's job.

What changed

  • New GenericLowerer participant runs (post_annotate) before the aggregate-return lowerer.
    For each non-builtin generic call it:
    1. resolves the concrete monomorphization,
    2. materializes it as an {external} POU (with concrete-type substitution) when no real POU
      already provides it,
    3. rewrites the call operator to the concrete name,
    4. re-indexes + re-annotates to a fixed point (handles nested generics).
  • Because monomorphizations are now real AST POUs, they survive every re-index; downstream passes
    (aggregate-return lowering, codegen) only ever see concrete calls; and a missing monomorphization
    is a link-time undefined symbol, uniform for scalar and aggregate returns.
  • Builtin generics (MUX/SEL/SHL/MOVE/…) have no monomorphization and are resolved inline
    by codegen, so their resolution stays in the annotator.
  • Generic-nature (E062) violations are now reported by the lowering phase.
  • The aggregate-return lowerer no longer special-cases generics (its operator-rewrite branch and
    in-lowering nature check are removed).

Behavior changes worth noting

  • A generic call to a type with no monomorphization now fails at link time (undefined symbol)
    rather than compile-time E048.
  • The resolved-call annotation names the monomorphization directly (qualified_name: "FN__T")
    instead of the generic plus a call_name redirect.
  • Fixed a latent bug: arguments to a sized ({sized} T...) variadic with a numeric element type are
    now coerced to that element type instead of being C-promoted (a promoted value would overflow the
    narrower array slot).

Testing

  • Full unit suite green; the generic-resolution, validation, and codegen unit tests were migrated to
    exercise the lowering phase (new test_utils helpers run it).
  • cargo xtask lit green; the unsupported-conversion test now expects the link-time undefined symbol.
  • Verified end-to-end that TO_STRING/TO_WSTRING/MAX/ABS/CONCAT/LEN resolve and run
    correctly, and that an unsupported type produces a clean undefined symbol error.

Follow-up (separate PR)

  • A --no-undefined linker gate (default-on for shared objects, with an opt-out flag) so a missing
    monomorphization fails at build-invocation time rather than only at final link.

ghaith and others added 6 commits July 13, 2026 11:06
…tion

When lowering a call to an aggregate-returning generic function, the lowerer
reset the call operator to the bare generic name and relied on re-annotation to
re-resolve it. For a call whose concrete monomorphization did not exist, that
re-resolution ran against the injected aggregate return buffer and bound the
call to `<fn>__STRING`, reinterpreting the argument's bytes as a string. For
values without an early null byte this read out of bounds and aborted at
runtime.

Reset the operator to the resolved monomorphization name instead. A generic
call whose monomorphization is not declared is now an E048 unresolved-reference
error at compile time, consistent with how scalar generics already surface a
missing implementation.

Also add the TO_STRING__<T> / TO_WSTRING__<T> monomorphizations for the scalar,
bit-string, real and date/time types that already ship a typed <T>_TO_STRING /
<T>_TO_WSTRING conversion, so TO_STRING/TO_WSTRING work for them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compare TO_WSTRING(DINT#42) against a WSTRING literal instead of round-tripping
it back through TO_STRING to print it. This is clearer, avoids the odd
double conversion, and checks the TO_WSTRING__DINT binding directly. Also drop
an internal tracker reference from the two conversion lit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract get_generic_candidate, get_specific_function_annotation and
derive_generic_types from TypeAnnotator into module-level free functions
parameterized by the index (and annotations / new_index). The existing methods
now delegate, so first-pass behavior is unchanged. This lets a later generic
monomorphization lowering phase reuse the exact same resolution logic without a
TypeAnnotator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…core

Add src/lowering/generics.rs with the reusable core for the upcoming generic
monomorphization lowering phase: a recursive type-declaration substitutor
(T -> concrete, recursing through array/pointer/{sized} vararg element types)
and a materializer that builds an {external} monomorph POU + empty-body
implementation from a generic template (aggregate returns preserved for the
aggregate lowerer). Unit-tested in isolation; wired into the pipeline as a
participant in the next step (allow(dead_code) until then).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve non-builtin generic function calls in a new GenericLowerer
lowering phase rather than the type-annotator's first pass. The phase
materializes each monomorphization as an {external} POU in the AST,
rewrites the call operator to the concrete name, then re-indexes and
re-annotates to a fixed point.

Materialized monomorphizations are real AST POUs, so they survive every
re-index and downstream passes (aggregate-return lowering, codegen) only
see concrete calls. A missing monomorphization becomes a link-time
undefined symbol, uniform for scalar and aggregate returns, instead of
the aggregate-return lowerer re-resolving generics and risking a
byte-reinterpreting mis-dispatch.

Builtin generics (MUX/SEL/SHL/...) have no monomorphization and stay
resolved by the annotator. Generic-nature (E062) checks move into the
lowering phase. Sized-variadic arguments with a numeric element type are
coerced to that element type instead of being C-promoted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Conflict in src/lowering/calls.rs: reconcile master's #1794 restructure of
visit_call_statement (deferred_reference for oversized C string returns +
the flags tuple) with the rework's removal of generic handling from the
aggregate-return lowerer. Kept master's structure and dropped the generic
bits (generic_name/is_generic_function, the operator-rewrite block, and
collect_generic_nature_violations) — the GenericLowerer phase rewrites
generic calls to concrete monomorphs before this lowerer runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

Build Artifacts

🐧 Linux

Artifact Link Size
deb-x86_64 Download 38.4 MB
schema Download 0.0 MB
stdlib Download 32.4 MB
plc-x86_64 Download 43.4 MB
deb-aarch64 Download 30.8 MB
plc-aarch64 Download 43.3 MB

From workflow run

🪟 Windows

Artifact Link Size
stdlib.lib Download 4.0 MB
stdlib.dll Download 0.1 MB
plc.exe Download 38.3 MB

From workflow run

Monomorphizations are no longer required to be declared in ST. The compiler
emits an {external} declaration for any monomorphization that isn't provided,
so a missing implementation fails uniformly at link time (scalar and aggregate
return types alike) rather than aggregate returns erroring at compile time.

Co-Authored-By: Claude Opus 4.8 <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