Function type support - #8
Merged
Merged
Conversation
This was referenced Jul 27, 2026
milyin
added a commit
that referenced
this pull request
Jul 28, 2026
… extents to C (#210, first step of #211) (#212) * Lower array lengths in one frontend walk, not two adapter walks Array-length handling had two independent walks over the same expression: a whitelist in jnigen deciding what was ACCEPTED, and a rewriter deciding what it could QUALIFY. Nothing tied them together, so they drifted eight times (#210). The eighth was still open — the whitelist accepted every `Expr::Path`, the rewriter skipped paths with a `qself`, so pub qualified_assoc: [u8; <Holder>::N], was accepted and emitted verbatim into a crate where `Holder` is not in scope. Fixing that shape alone leaves the mechanism that produced #1-#8 intact. Both walks are replaced by one fallible lowering in a new `core::frontend`: lower_array_len(&Expr, &NameIndex) -> Result<ArrayLen, UnsupportedArrayLen> with the contract that `Ok` means the length was fully understood AND fully resolved. "Accepted" is now a consequence of lowering rather than a parallel judgement, so accepted-but-unqualified is not representable. `ArrayLen` is the one closed representation every consumer reads. It runs at INGEST, as pass 3 of `Registry::from_items`, where the name index is complete. So the decision is made before any adapter exists — both generators refuse the same input with the same message — and no emit-time length pass remains: `reject_unsupported_array_length`, `QualifyLengthPaths` and the `length_names` map are deleted, along with the comment block recording which defect each guard came from. The grammar narrows to a literal or a plain path. Const arithmetic (`[u8; A + 1]`, `[u8; A as usize]`) and `const fn` calls (`[u8; array_len()]`) leave the language; hoist the value into a named `const`. This is an intentional breaking change with a small blast radius — `[T; N]` crossing arrived in #209 and no released binding depends on it. `zenoh-flat`'s `[u8; ZENOH_ID_MAX_SIZE]`, the one real use, is a free-const path and is unaffected; regenerating zenoh-flat-jni changes converter names and one diagnostic string, nothing on the Kotlin surface. Three variants rather than the two #211 sketches: a path naming nothing the registry indexes (`usize::MAX`) is legal and must be emitted verbatim. That was a silent fallthrough in the old rewriter; `ExternalConst` makes it explicit, so lowering stays total instead of reintroducing a guess. Two shapes change behavior beyond the narrowing. A `qself` is now an explicit rejection naming the offending sub-expression — #210 asked for the decision to be made either way, and silent acceptance was the one option it ruled out. And `crate::MAX` now resolves like the bare `MAX`; previously its leading segment missed the name set and it was emitted as `crate::MAX`, which names the CONSUMER's crate. Stripping the source head is deliberately not the type-path rule, which reduces to the final segment and would collapse `myflat::Holder::N` to `N`, losing the owner of the associated const. Tests replace the eight characterization tests with a table: accepted spelling -> lowered value -> emitted spelling, and refused spelling -> reason. A new shape is a row. The two jnigen tests that only an end-to-end run can prove survive — that qualification reaches emitted code and does NOT touch a converter body's identically-named locals. The covertest fixture sizes `Arrays::bytes` by a `#[prebindgen]` const instead of a literal. That the covertest crate COMPILES is the check: a literal would prove nothing about resolving a path a different crate can name. `docs/source-language.md` writes down the accepted subset — #211 step 1 — and is explicit that only this row has moved into the frontend so far. Closes #210. First step of #211. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Resolve a length path by its item, not by its first segment Review finding on #212. `strip_source_head` dropped a `crate`/`self`/source-crate head and then assumed the next segment was the semantic anchor. That assumption holds only if accepted paths contain no intermediate modules, which was never stated and is not true: mod limits { #[prebindgen] pub const MAX: usize = 4; } pub bytes: [u8; crate::limits::MAX], `limits` is not indexed, so this became `ExternalConst { limits::MAX }` and the generated crate emitted an unqualified `limits::MAX` — unresolvable there, or worse, bound to a consumer-side module. It also contradicted the `ExternalConst` contract outright: the head was stripped BEFORE the path was classified, so a path documented as verbatim was not. The fix is the flat namespace. Prebindgen items are uniquely named and reachable as `<origin crate>::<bare name>`, so a module prefix inside the source crate carries no information — the bare name already identifies the item. Lowering becomes two ordered decisions: 1. Is the path source-relative? Its head is `crate`, `self`, a source module, or an indexed name. Everything else is external and is returned untouched. Classifying FIRST is what makes the verbatim guarantee true; it also keeps `other_crate::Holder::N` alone even though `Holder` names an indexed item, the same rule `normalize_type` applies to foreign type paths. 2. Which segment is the item? The leftmost that names one. Everything before it is module path and is replaced by the origin module; everything after is relative to the item and kept. So `MAX`, `crate::MAX`, `myflat::MAX`, `crate::limits::MAX` all lower to one value — which is the property that was missing, not just a missing branch. Leftmost, not rightmost: in `Holder::N` with a free const `N` also indexed, `Holder` is the anchor and `N` is its associated const. A `Leaf` item — a const or fn, through which nothing is reachable — is skipped when segments follow it, so a const sharing a module's name cannot capture the path. That is the only collision Rust permits, modules and types sharing one namespace but consts sitting in another, and it is a matrix row. A source-relative path naming no indexed item is now a hard error rather than silently verbatim: it claims a source item, so emitting it into a different crate is exactly the misinterpretation this pass exists to prevent. The bare spelling stays `ExternalConst` — indistinguishable from an external namespace — and the docs now say so instead of implying one rule. This makes explicit an invariant that was already load-bearing for TYPES: `normalize_type` reduces `crate::a::Foo` to `Foo` and emission qualifies it to `myflat::Foo`, so a nested item has always had to be re-exported at the source crate root. zenoh-flat already does — `ZENOH_ID_MAX_SIZE` lives in `base::config::zenoh_id` and `lib.rs` re-exports it. Verified the anchor scan is load-bearing by restoring the first-segment rule: the matrix fails. Generated output byte-identical; covertest PASS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * An array length is a number, not a path Rereview of #212 found a two-source provenance hole: one registry-wide name index, no origin for the item being lowered, so `crate::limits::MAX` in source A — with `MAX` unmarked there and marked in source B — anchored to B and silently changed the length from 4 to 8. It also found relative module paths to be irreducibly ambiguous without indexing modules, and the constructor unusable through the public facade. All three dissolve rather than get patched, once the requirement is stated properly: a length must be a KNOWN NUMBER. A generator runs in build.rs and cannot evaluate Rust, and a destination language that groups a small array into scalars needs the count literally — a Kotlin surface cannot reference a Rust const at all. So the grammar is an integer literal, or the BARE NAME of a `#[prebindgen]` const whose own initializer is an integer literal. The frontend reads the value and emits the number. That is a real narrowing, and it is the point: * module paths go, because a marked item is uniquely named and the bare name is its whole address — `crate::limits::MAX` only restated it, and `limits::MAX` was never distinguishable from a foreign crate path; * associated consts go, because prebindgen never captures `impl` blocks, so `Holder::N` could only ever have produced a path, never a number; * external paths go, and `usize::MAX` shows why — it is not even a fixed value; * an UNMARKED const is now a hard error rather than emitted verbatim. The generated crate sees only what the macro exposed, so this was never a qualification problem: the item does not exist downstream. Provenance is then a one-line rule instead of a lowering context: a bare name must be a const marked in the item's own source crate. Uniqueness holds across the marked namespace only, so without it the review's example still binds to the other crate's value; with it, that is unrepresentable. Origin-less streams are one anonymous crate and match trivially. Emitting the number is what removes the defect class rather than fixing it. There is no path left in generated code, so there is nothing to qualify, no namespace to get wrong, and no re-export invariant to rely on. A const length and the same number written literally become one type and one converter — which they always were in Rust. And a changed const now shows in the diff of a committed generated artifact, where echoing the name showed nothing at all. API surface (finding 3, my call): the lowering machinery is crate-private and the public frontend is the decided model plus its diagnostics. `NameIndex`, `ItemRole` and `ArrayLenResolver` are gone from the facade rather than completed; the entry point is `Registry::from_items`, and offering a second route to a partly-built model is what #211 exists to prevent. `Registry::named_item_idents`/`named_items` go with them — they existed only for the machinery this replaces. The restriction is on LENGTHS, not on consts: a `#[prebindgen]` const may still be computed however you like. Generated output is byte-identical to main apart from the new fixture const: `[u8; ARRAY_BYTES]` evaluates to the `[u8; 4]` that was there before. covertest PASS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * A type-keyed length table stores the number, not the spelling Review finding on 4880ffe. `Registry::array_lens` was keyed by `TypeKey` but stored `ArrayLen`, which records WHICH SPELLING produced it — and `TypeKey` has already collapsed the spellings. Given pub a: [u8; A], // A = 4 pub b: [u8; B], // B = 4 pub literal: [u8; 4], all three are one key, so the stored `Const { name }` was whichever occurrence the iteration reached last: order dependent, and false for the other two either way. No consumer reads the name today, so nothing drifted — it was a latent nondeterminism and a claim the table could not honour. Three identities were being conflated, and the fix is to keep them apart rather than to drop the resolution: * const identity — `A` is `4` in crate X — the const index; * type semantics — `[u8; 4]` — the type table; * source-use provenance — field `S::a` was written `[u8; A]` — per occurrence, belonging to a per-use model that does not exist yet (#211's `SourceModel`). So the type-keyed table holds `usize` and `Registry::array_len` returns one. `ArrayLen` keeps the spelling, because at the occurrence level that is true, and becomes crate-private: there is no type-keyed table it could be handed out through honestly, and nothing public produced it any more. `equal_lengths_collapse_to_one_typed_entry` is the regression: three fields, two const-spelled with equal values, one type between them. It asserts both halves — the occurrences stay distinguishable, the type does not — so the layering is pinned rather than the current storage choice. Generated output byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * The spelling of a length is discarded, and says so Review finding on 961be86: the previous test claimed occurrences "stay distinguishable", but that was only true inside the lowering call. After `Registry::from_items` the item types are rewritten to the literal and the occurrence vector is dropped, so no adapter can tell that `S::a` wrote `A`, `S::b` wrote `B`, and `S::literal` wrote `4`. The model was advertising provenance that did not survive the boundary it was documented at. The review offered two coherent ends: build the per-use record now, or declare the spelling semantically irrelevant. Taking the second, explicitly. `ArrayLen` is deleted; lowering returns `usize`. It carried a name that nothing read — a successful lowering raises no diagnostic, and `UnsupportedArrayLen` already renders the offending expression — so the enum existed only to make the overclaim expressible. Why the policy rather than the record: `[u8; A]` and `[u8; B]` with `A == B == 4` are one Rust type, one `TypeKey` and one converter. An adapter rendering them differently would need two destination representations for a single Rust type, which the type-keyed converter table cannot express — so the capability is incoherent at this layer even if the provenance were carried. Building a per-use record now would also mean inventing the "stable use identity" that is precisely `SourceModel`'s design work, one construct at a time, which is the parallel structure #211 exists to remove. The cost is stated rather than hidden: a C header cannot echo `uint8_t x[MAX_SIZE]`, only `uint8_t x[16]`. If that is wanted, provenance arrives on the use site in `SourceModel` — never on a type-keyed table, where three occupants of one key cannot be told apart by the key. The regression now inspects the state AFTER `Registry::from_items`, as asked, and pins both halves: all three fields are `[u8; 4]` sharing one type-keyed row with value 4, and the const names appear nowhere in the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * A C header can spell an array extent by name `uint8_t tag[MARKER_TAG_LEN]`, not `uint8_t tag[4]`. A symbolic extent is part of the C API's meaning: it makes changing the size one edit instead of a hunt through literals. 32370af had discarded the spelling, so that fact could not reach cbindgen at all. The obvious patch — stash the original `syn::Type` and let cbindgen re-read it — is what #211 forbids twice over: adapters do not parse captured source, and emitters do not recover semantic facts by re-reading it. So the fact goes in the IR, and this begins `SourceModel` (#211 step 2) rather than building a side channel that would have to be deleted again. `core::frontend::model` adds a closed, language-neutral `SourceType`. Lowering is TOTAL over the grammar in docs/source-language.md — a form it cannot lower is a frontend error, the same acceptance-is-lowering contract array lengths already had. An array node carries `ArrayExtent { value, source }`: the value is the semantic length that keeps `[u8; A]` and `[u8; 4]` one type and one converter, the source is which const the use site named. Both halves have consumers, and they are different ones. The model is the source of truth and `syn` is a projection: pass 3 lowers every struct field and writes `to_syn()` back into the `syn::ItemStruct`, so there are never two representations to disagree. `TypeKey` stays numeric, which is why all of jnigen and most of cbindgen are untouched — covertest PASS, generated Kotlin and Rust byte-identical. Scope is bounded on purpose. Types: complete. Items: structs only, the one surface an adapter consumes as source; functions and enums keep their `syn` items, which #211 step 4 sanctions while adapters migrate. Adapters: cbindgen's struct-field path only. Its per-field CLASSIFICATION still runs on `to_syn()` — `SourceType` is already the answer to `is_scalar`/`is_string`/`is_vec`, but deleting those duplicates is F5/F6, not this change. Two things blocked the header besides the missing fact, both verified rather than assumed: * cbindgen had no array support at all, so `[T; N]` in a data-struct field panicked. `c_field_wire` now accepts an array of non-`bool` scalars: `[u8; N]` is already `#[repr(C)]`-compatible, so it is its own wire and the field copies with no conversion. `[bool; N]` stays refused — its domain is `0`/`1` and a mirror is reinterpreted wholesale with no per-element hook, the same hazard `restricted_validity_field` documents. * consts never reached the header. `on_const` aliased them to `= perftest_flat::ARRAY_BYTES`, a path cbindgen cannot evaluate, so it emitted no `#define`. A probe pinned this down exactly: with a literal it emits `#define MARKER_TAG_LEN 4` and `uint8_t tag[MARKER_TAG_LEN]`; with the alias it emits the struct referencing an UNDEFINED symbol. So the literal is not cosmetic — without it the feature produces a header that does not compile. Only extent consts change; the rest keep the alias. The exercise is in example-cbindgen because `smoke-asan.sh` is the only place CI compiles a generated header. `Marker { tag: [u8; MARKER_TAG_LEN], weight }` round-trips by value in smoke.c, which uses the macro as a bound — so the test COMPILING is most of the proof, since a missing `#define` is a compile error. The x86_64 goldens are derived: the substitution from the aarch64 pair was first verified to reproduce the committed x86_64 pair exactly at HEAD, then applied. CI on x86_64 is the oracle. `equal_lengths_collapse_to_one_type_after_ingest` now pins both halves for real. The second half — that the model still reports `A`, `B` and a literal per field — was the assertion the previous review correctly called untrue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * The projection preserves type identity Review finding on 21ab04a. Pass 3 writes `field.ty = ty.to_syn()`, which made the projection a semantic rewrite in the live pipeline — while it could not reconstruct five forms the grammar accepts: &'static Foo -> &Foo Foo<'static> -> Foo (Foo,) -> (Foo) <T as Trait>::Assoc -> Trait::Assoc foreign::Option<u8> -> Option<u8> Each changes `TypeKey`, so each changes converter selection and the emitted field type. The lifetime losses contradict this repo's own rule that `Foo<'static>` is not `Foo`, which `ptr_class!(ZKeyExpr<'static>)` relies on. Three of the five turn out to be LANGUAGE questions, so the fix narrows the grammar rather than growing the model: * Tuples are not in the language. Verified: every `Type::Tuple` site in both adapters is the unit case or a generic walk — no adapter has ever lowered a non-empty tuple. Refusing deletes the 1-tuple bug instead of fixing it, and turns a late "unresolved type" into a precise frontend error naming the type. * Associated types are refused. `#[prebindgen]` never captures `impl` blocks, so what `<T as Trait>::Assoc` resolves to is unknowable here; carrying the spelling would only move the failure downstream. `Named` then needs no `qself`, which removes that reconstruction path entirely. * `foreign::Option` was a DETECTION bug, not a fidelity one: builtins matched on the last segment alone. They now require a bare single-segment path, which is exactly what `normalize_type` already guarantees — it reduces the genuine std spellings at ingest and deliberately leaves unknown crate paths alone. A foreign type that merely shares a name stays foreign. The remaining two are fidelity, and lifetimes are kept the way they actually behave: part of a type's NAME, never structure, because they mean nothing to a destination language. `Named` keeps its path as identity with the last segment's arguments split out in SOURCE ORDER, lifetimes included, so `Foo<'a, T>` comes back exactly; nested types stay canonical, so an extent inside a generic still projects numerically and `TypeKey` stays right. `Ref` gains a lifetime — no struct field needs one today, but `&'static Encoding` is a real zenoh-flat return, and the variant has to be right before signatures are modeled. The old `to_syn_round_trips` could not catch any of this: it projected an already-projected type, so it only proved a lossy function idempotent. It is replaced by comparing `TypeKey` of the NORMALIZED ORIGINAL against `TypeKey` of its projection, over every accepted form including all five above. Verified it discriminates by restoring two of the bugs: each fails naming its own symptom. Generated output is byte-identical in-repo, and the sibling zenoh-flat-jni regenerates identically to 21ab04a — none of the five occurs in a real struct field, so a correct fix moves nothing. covertest PASS, smoke.c PASS. Also fixes the contract text the review flagged: `frontend.rs`'s "Scope today" and `docs/source-language.md`'s "Read this first" both still claimed only array lengths had moved. The Types table now states the tuple and qself refusals, and records that they bind only in MODELED positions — struct fields today — which is the honest shape of a partial migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Turbofish and a trailing generic comma are not identity Review finding on 13978e7: `Wrapper::<u8>` and `Wrapper<u8,>` lost their punctuation through `to_syn()`, and `TypeKey` — whose identity is the normalized token string — treated the result as a different type. The damage is worse than a mismatched key. Modeled struct fields are rewritten through `to_syn()` and function signatures are not, so during the migration one Rust type would hold the old key in a signature and the new key in a field, splitting converter lookup by POSITION. Fixed where the reviewer said it belongs: at the declared canonicalization boundary. `normalize_type` now drops the turbofish and a trailing comma, so every position agrees and no consumer has to. Carrying the punctuation in `SourceType` was the alternative, and it is the wrong one — the spellings mean the same type, so preserving the difference only spreads it. That also keeps reconstruction from silently defining canonicalization, which was the reviewer's real point: `to_syn()` already emitted the bare form, so it was deciding by accident what `normalize_type` should decide on purpose. Verified both tests are load-bearing by removing the normalization: the types_util test fails `"Wrapper :: < u8 >" != "Wrapper < u8 >"` and the identity matrix fails `Wrapper::<u8> lost identity`, which is exactly what the review reproduced. Lifetimes stay identity — `Foo::<'static,>` collapses to `Foo<'static>`, never to `Foo`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Close the callback gate, and say which refusals are temporary Review finding on a3cf2af, and this one was LIVE rather than latent. `extract_fn_trait_args` is the acceptance gate for every callback — 14 call sites across core resolution and both adapters — and it read `p.inputs` while never looking at `p.output` or the bound's `lifetimes`. So impl Fn(u8) -> u16 + Send + Sync + 'static was accepted, planned from its arguments alone, and its result dropped: every callback wire is void-shaped, C's `call` having no return and jnigen's `run` returning void. The test that now refuses it reports, without the fix, exactly what was happening — `accepted as Callback { args: [Scalar(U8)] }`. Refusals now come in two kinds, because a bare "no" leaves the author guessing whether to redesign or to wait, and those are opposite responses: * RESERVED — the language intends it, the machinery does not exist yet. A callback returning a value is reserved, and the diagnostic names issue #216, which carries the design that actually blocks it: both adapters have a case where the foreign side yields no value (a null `call` pointer, a throwing Kotlin callback), and with a return type there is nothing to swallow to. A callback returning a callback is reserved too. * UNSUPPORTED — it cannot work here. A higher-ranked binder is the case: no FFI boundary can be generic over a lifetime, so no adapter could ever carry one. The gate becomes `extract_fn_trait_sig -> Result<_, CallbackReject>` with `extract_fn_trait_args` a thin `.ok()` wrapper, so the 13 shape-query callers are untouched and ONE function still decides. A separate "why did it fail" helper beside an Option-returning gate would have been the two-authority drift this PR exists to remove. `ScanError::DisallowedImplTrait` and the model's `UnsupportedTypeReason` both carry that one reason rather than restating it. Three further spellings were one type with different keys — the split-by-position bug fixed for turbofish in a3cf2af, reappearing in `Fn`'s parenthesized argument list and its bound list. They canonicalize at the same boundary, by the rule settled last round: a trailing input comma, an explicitly-spelled `-> ()`, and bound order (traits sorted, lifetime bounds last so the result stays valid Rust). Since every accepted callback is therefore unit-returning with a fixed bound order, `SourceType::Callback` needs no output field and its projection stays exact. Verified both new rules are load-bearing by reverting each: the refusal matrix accepts the non-unit callback again, and the normalization test reports `Sync + Send` against `Send + Sync`. Generated output byte-identical in-repo, and the sibling zenoh-flat-jni regenerates identically to a3cf2af. covertest PASS, smoke.c PASS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix normalization idempotence, a false HRTB claim, and a layering inversion Three findings on ca21e16, all correct. **Idempotence.** `visit_path_arguments_mut` tested whether the `Fn` return was `()` BEFORE calling the recursion that unwraps `Paren`, so `impl Fn(u8) -> (())` needed two passes to reach `impl Fn(u8)`. Items are normalized at ingest and `TypeKey::from_type` normalizes again, while a directly built key normalizes once — so a key could depend on how many passes its input had had. The recursion moves to the top, matching `visit_type_impl_trait_mut`, which already had the right order. The regression is the general property, not the instance: `normalize_is_idempotent` asserts `canon(canon(x)) == canon(x)` over every spelling the suite exercises, because the way this breaks is a rule reading a node the recursion has not reached yet, and that mistake belongs to no particular rule. **The HRTB claim was wrong.** `ca21e16` refused an explicit `for<'a>` binder as definitively unsupported, reasoning that no FFI boundary can be generic over a lifetime. The counter-example is decisive: `impl Fn(&u8)` IS higher-ranked — it desugars to `impl for<'a> Fn(&'a u8)` — and the two are mutually substitutable. The binder constrains the Rust closure; it is not carried on any wire. So the accepted spelling and the refused one are the same type, which makes this the last two-spellings-one-type case rather than an impossibility. Reclassified to RESERVED with the real reason: the canonicalization is not written, and it has to be exact, since elision gives each elided input lifetime its own fresh binder — `for<'a> Fn(&'a u8, &'a u8)` is NOT `Fn(&u8, &u8)`. Issue #222 carries that rule. The diagnostic now points at the elided form, which is accepted and means the same thing. **Core depended on an adapter.** `extract_fn_trait_sig` called `jnigen::util::is_unit` — a language-neutral source rule reaching into one adapter, the inversion #211 exists to prevent. Core's own `is_unit` is ungated and jnigen's is DELETED rather than re-exported: it had exactly one other caller, and two names for one predicate is what let the inversion happen unnoticed. Verified the idempotence fix is load-bearing by moving the recursion back — both the general assertion and the `-> (())` row fail, reproducing the review exactly. Generated output byte-identical in-repo and in the sibling zenoh-flat-jni. Builds clean with and without default features. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * The HRTB fix-it is only exact when elision preserves the binding Follow-up on 2b84e02. That commit corrected the HRTB *classification* but left the advice unconditional: "write the elided form instead (`Fn(&T)`, which means exactly the same thing)". True for `for<'a> Fn(&'a T)`, false for the rest of the refused class — the enum's own doc gives the counterexample two lines above. Elision binds each input separately, so `Fn(&T, &T)` is `for<'a, 'b> Fn(&'a T, &'b T)`; an author whose signature ties two inputs to one lifetime was being told to make a semantic change and told it was exact. The message now states the condition: exact when every bound lifetime is used once, and no accepted spelling yet when one is used twice. `source-language.md` carried the same unconditional sentence and gets the same treatment. The acceptance matrix still labelled the row UNSUPPORTED and repeated the retracted "no FFI boundary can be generic over a lifetime" claim, so the executable document disagreed with both the enum and the guide. Relabelled RESERVED with the actual reason. Checked that no copy of the retracted claim survives anywhere in the tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.