Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
0de7e41
Map the Language integration program
milyin Jul 29, 2026
0901651
Parse the record stream into elements that keep their syntax (#211) (…
milyin Jul 29, 2026
989010e
Flat: a resolved model with direct access, not a stream of elements (…
milyin Jul 30, 2026
d622e99
Merge main into language-integration
milyin Jul 30, 2026
1326810
Merge main into language-integration
milyin Jul 30, 2026
610c1c2
A prelude, Extern instead of Opaque, and no args (#235)
milyin Jul 30, 2026
3d8f292
Cow<'_, T> is transparent, like Box<T> (#236)
milyin Jul 30, 2026
3d47156
Merge remote-tracking branch 'origin/main' into language-integration
milyin Jul 30, 2026
4a10ea0
L1: Registry consumes Flat (#238)
milyin Jul 30, 2026
c9bd116
The type table carries Flat's reading of each type (#239)
milyin Jul 30, 2026
4be973d
Flat is the only index; Registry stops keeping a second one (#243)
milyin Jul 31, 2026
5852c75
Flat owns the type index too (#246)
milyin Jul 31, 2026
0746b8e
Bring the stage map up to what the program actually did
milyin Jul 31, 2026
ddb2dc9
Merge pull request #247 from milyin/docs-stage-map-resync
milyin Jul 31, 2026
d845c8f
Delete the pattern engine; the model already names the one shape it m…
milyin Jul 31, 2026
19a2b77
The ledger started falling, and a merged stack is missing from the ma…
milyin Jul 31, 2026
b3c793c
L1.75 is on this branch; I read the commit count instead of the trees…
milyin Jul 31, 2026
baf0dae
L2a: the scan walks the model's edges, not the syntax (#257)
milyin Aug 1, 2026
66ac799
L2e + L2d: every cell carries a reading, and spelling moves home (#258)
milyin Aug 1, 2026
fbb77b6
JniGen keeps declarations, not the builder that made them (#260)
milyin Aug 1, 2026
a7ce2d5
keep unstable examle autogenerated
milyin Aug 1, 2026
47c4869
Live counts belong in the ledger and the PR, not in prose here (#259)
milyin Aug 1, 2026
8c905f2
L2b: one layer read, and api/core stops classifying types (#261)
milyin Aug 1, 2026
89b612b
Carry the reading; stop re-deriving it from origin (#263)
milyin Aug 1, 2026
4fed457
L2 is done, and it taught something worth keeping (#262)
milyin Aug 1, 2026
0380d59
L4a: the crossing hands over the reading (#264)
milyin Aug 1, 2026
8e1153c
Take the reading from the declaration; `reading` becomes a lookup (#267)
milyin Aug 1, 2026
520c989
Don't assume how Rust spells an optional (#271)
milyin Aug 1, 2026
237ee8c
The selector reads the model instead of guessing types by name (#272)
milyin Aug 1, 2026
aad98fe
Ask the model whether a type is optional, not the spelling (#274)
milyin Aug 1, 2026
040dfa0
The `Prebindgen` trait hands over elements, not syn items (#276)
milyin Aug 2, 2026
01c9e5d
docs: the item methods take elements (L5), and classify.rs is closed …
milyin Aug 2, 2026
88c38ea
Core's plan leaves carry the reading (#278)
milyin Aug 2, 2026
0727b6d
The adapter's reading sources, and the last accessor goes (#279)
milyin Aug 2, 2026
08f854d
Seal `TypeRef`: only the model may mint one (#280)
milyin Aug 2, 2026
31304f0
The registration path carries readings instead of re-deriving them (#…
milyin Aug 2, 2026
b407284
jnigen: the wrapper-shape handlers take the inner type as a reading (…
milyin Aug 2, 2026
995d110
core: registry lookups take the reading, not a spelling (#286)
milyin Aug 2, 2026
83603aa
jnigen: struct_plan peels off the model instead of the tokens (#288)
milyin Aug 2, 2026
e299426
jnigen: readings instead of spellings across the input plan (#290)
milyin Aug 2, 2026
8d79043
core: the model says what it erased, and one rebuild stops guessing (…
milyin Aug 2, 2026
a972bb0
jnigen: rebuild the spelling instead of refusing it (#294)
milyin Aug 2, 2026
59d83b8
cbindgen: one kind presents one C type, however it is spelled (#295)
milyin Aug 2, 2026
53d4f17
jnigen: the wrapped-Vec-element refusal is reserved, not definitive (…
milyin Aug 2, 2026
8cb1a5e
core: the edge walk takes the key it was re-deriving anyway (#291 sta…
milyin Aug 3, 2026
a2cb4f8
A declaration keeps the type it was written with (#291 stage B) (#299)
milyin Aug 3, 2026
a89783f
A key answers what it is called (#291 stage C1) (#300)
milyin Aug 3, 2026
c4fa23c
A key is only an identity (#291 stage D, closes #291) (#301)
milyin Aug 3, 2026
7ec4121
jnigen: the jobject decoder reads the element, and finds a wrong desc…
milyin Aug 3, 2026
15daa1d
jnigen: the sum decoder reads the element too (#289) (#303)
milyin Aug 3, 2026
d0e3eef
jnigen: a sum's match pattern keeps its own delimiters (#304)
milyin Aug 3, 2026
b13b519
core: delete `SumSpec` — the model already describes a sum (#305)
milyin Aug 3, 2026
9047275
core: a `SumTag` selector registers the sum it names (#282) (#306)
milyin Aug 3, 2026
8319db5
jnigen: one reading of the movability rule (#223 item 1) (#307)
milyin Aug 3, 2026
63eeafd
jnigen: the transparent bridge gets its outbound half (closes #309) (…
milyin Aug 3, 2026
f72d9a8
core: a type is its syntax — `TypeKind` stops classifying (#312)
milyin Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
684 changes: 684 additions & 0 deletions docs/language-integration.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/covertest-helpers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//!
//! This crate exists to prove the multi-source model: a binding crate's
//! `build.rs` chains SEVERAL prebindgen source streams into one registry
//! (`Registry::from_items(flat.items_all().chain(helpers.items_all()))`) and
//! (`Flat::builder().items(flat.items_all()).items(helpers.items_all())`) and
//! the generated Rust qualifies each function with its origin crate.
//! covertest-kotlin additionally RENAMES this dependency
//! (`cov_helpers = { package = "covertest-helpers", .. }`) and overrides the
Expand Down
88 changes: 62 additions & 26 deletions examples/covertest-kotlin/build.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
//! Build script generating Kotlin/JNI bindings for `perftest-flat` using
//! prebindgen's [`prebindgen::lang::JniGen`] adapter — exercising **every**
//! JniGen feature so the hand-written `kotlin/.../Test.kt` can assert each one.
//! prebindgen's [`prebindgen::lang::JniGenBuilder`] adapter — exercising **every**
//! JniGenBuilder feature so the hand-written `kotlin/.../Test.kt` can assert each one.
//!
//! Unlike `examples/perftest-kotlin` (which maps only the lean perf surface in
//! the performance-optimal shape), this binding maps the *same* flat library —
//! including the coverage-only items in `perftest_flat::ext` — through the full
//! adapter surface. `JniGen` accepts pre-built declaration objects (the
//! adapter surface. `JniGenBuilder` accepts pre-built declaration objects (the
//! `prebindgen::lang` decl types, built by the root decl macros) rather than a fluent typestate
//! chain — each row below is a `PackageDecl`/`ConvertDecl`/etc. built
//! independently and then handed to `jni.package(...)` / `jni.convert(...)`:
//!
//! | JniGen feature | Exercised by |
//! | JniGenBuilder feature | Exercised by |
//! |--------------------------------------|--------------|
//! | default module (first stream origin) | `perftest_flat` |
//! | `JniGen::set_package_prefix` | `io.prebindgen.covertest` |
//! | `JniGen::package` (subpackages) | `model` / `errors` / `analytics` / `storage` |
//! | `JniGen::set_jni_native_init` | `NativeLibrary.ensureLoaded()` |
//! | `JniGenBuilder::set_package_prefix` | `io.prebindgen.covertest` |
//! | `JniGenBuilder::package` (subpackages) | `model` / `errors` / `analytics` / `storage` |
//! | `JniGenBuilder::set_jni_native_init` | `NativeLibrary.ensureLoaded()` |
//! | contextual name-mangle closures | package-aware class/function hooks + package/class-aware method hook |
//! | `DataClassDecl` | `Payload`; `Annotated` (recursive direct + optional nested fields) |
//! | `DataClassDecl::jobject_input()` | `ObjectBoundary` (127 `Long` leaves plus JNI infrastructure exceed the JVM's 255-slot method limit) |
//! | `PtrClassDecl` | `Storage` / `Summary` / `StorageError` / `Archive` / handlers |
//! | `EnumClassDecl` | `Priority` |
//! | `convert!` + chained source streams | `Millis` ⇄ `Long` via `covertest-helpers` fns |
//! | `Source::builder().crate_name()` | the helpers dep is RENAMED to `cov_helpers` in Cargo.toml |
//! | `.source_named(dir, "cov_helpers")` | the helpers dep is RENAMED to `cov_helpers` in Cargo.toml |
//! | `convert!` `.input(from!)`/`.output(into!)` | `Celsius` ⇄ `Int` via `From`/`Into` impls |
//! | fallible conversion stages under `Option` | `Option<Percent>` ⇄ `Int?`; raw `TryFrom::Error` input and binding-local `String` output errors normalize to `JniErrorHandler` |
//! | `convert!` sources `fun!(crate::…).sig(sig!)` | `Label` ⇄ `String` via binding-local fns (`crate::label_in`/`label_out`); the sig's `Result` = error channel, empty label → `onError` |
Expand All @@ -37,7 +37,7 @@
//! | `expand_return!` `.fields(fields!(…))` (#213) | `Report` — boundary DERIVED from the value form instead of restated; covers every per-field rule (spliced `Summary`, inlined `Stamp`, `Option<data class>`, a sum with a handle payload, a plain leaf) |
//! | `expand_return!` `.fields_self_into(fields!(…))` | `report_into_struct(r: Report)` — the CONSUMING value form: the value is given away and its fields MOVED out, so the clones the borrowing `report_to_struct` pays are not emitted at all |
//! | `PackageDecl::fun` / `FunctionDecl::name`| every free function; `.name` renames `millis_add` → `addMillis` |
//! | `Generation::report()` (C7) | `kotlin/REPORT.md` — the resolved surface, committed next to the regen |
//! | `JniGen::report()` (C7) | `kotlin/REPORT.md` — the resolved surface, committed next to the regen |
//! | contextual method names | method hook strips `storage`/`stamp` class prefixes; `summary_new`→`.name("of")` still overrides |
//! | per-class `.name()` | `Archive` → Kotlin `SummaryVault` (literal, bypasses mangles) |
//! | `.interface()` + `.implements(…)` | `Storage`/`Payload` emit an Api interface; `CovResource`/`Timestamped` extend it (#54) |
Expand Down Expand Up @@ -70,13 +70,13 @@
//! | binding-error channel (`JniErrorHandler`) | wrong-length `[u8; 2]` (fixed-size array length guard) |
//! | callback no-throw contract | a throwing `PayloadCallback` (described + cleared per upcall) |
//! | `data_class` instance member | `Payload.labelLen()` (receiver crosses as `this` field leaves) |
//! | `JniGen::ignore` (exact) | `string_len` / `storage_put_by_read_and_update` (acknowledged-unbound, no skip warnings) |
//! | `JniGen::ignore` + `matching(…)` | the `storage_get_into_*` group (one name predicate, any item kind) |
//! | `JniGenBuilder::ignore` (exact) | `string_len` / `storage_put_by_read_and_update` (acknowledged-unbound, no skip warnings) |
//! | `JniGenBuilder::ignore` + `matching(…)` | the `storage_get_into_*` group (one name predicate, any item kind) |
//!
//! One feature is deliberately left at its default and documented rather than
//! toggled, because it is mutually exclusive with a richer path this example
//! prefers to keep covered:
//! * `JniGen::set_emit_handle_locks` — kept ENABLED (default). Toggling
//! * `JniGenBuilder::set_emit_handle_locks` — kept ENABLED (default). Toggling
//! it OFF would remove the `withSortedHandleLocks` codegen this example
//! asserts against; a single binding can only be in one lock mode, so we
//! keep the locked one. (The toggle is a verification aid, not an
Expand All @@ -98,9 +98,9 @@
//! "skipping undeclared" build warning while emitting nothing.

use prebindgen::{
constant, convert, core::Registry, data_class, enum_class, expand_param, expand_return, expr,
fields, from, fun, into, lang::JniGen, matching, package, path, ptr_class, sealed_class, sig,
try_from, ty, variant,
constant, convert, data_class, enum_class, expand_param, expand_return, expr, fields, from,
fun, into, lang::JniGen, matching, package, path, ptr_class, sealed_class, sig, try_from, ty,
variant,
};

fn strip_flat_class_prefix(class: &str, name: &str) -> String {
Expand All @@ -118,7 +118,9 @@ fn strip_flat_class_prefix(class: &str, name: &str) -> String {
}

fn main() {
let jni = JniGen::new()
let binding = JniGen::builder()
.source(perftest_flat::PREBINDGEN_OUT_DIR)
.source_named(cov_helpers::PREBINDGEN_OUT_DIR, "cov_helpers")
.set_package_prefix("io.prebindgen.covertest")
.set_jni_native_init("io.prebindgen.covertest.NativeLibrary.ensureLoaded()")
// Every naming tier used here is configured. The harness hook is a
Expand Down Expand Up @@ -204,6 +206,17 @@ fn main() {
.method(fun!(payload_label_len)),
),
)
// `Option<Holder>` where `Holder` has a REQUIRED handle field: the
// absent case passes pointer 0 for it, so the field decodes must stay
// inside the presence gate or `null` becomes a binding error instead of
// `None` (PR#294 review).
.package(package!().class(data_class!(Holder)))
// A data class whose FIELDS carry transparent wrappers (#289 + #292):
// `boxed: Box<Option<i64>>` must cross exactly as `plain: Option<i64>`
// does — the decoupled `(present, value)` pair — with the `Box` put back
// on the Rust side. Peeling the field by path segment answered "not
// optional" and boxed it instead.
.package(package!().class(data_class!(WrappedFields)))
// ── Subpackage `model`: enum + value class + nested data class ──────
.package(
package!("model")
Expand Down Expand Up @@ -513,6 +526,34 @@ fn main() {
// crossing covers the borrowed payload, the owned one, and the
// sum each report carries.
.fun(fun!(ledger_each))
// A transparent wrapper (`Box<Option<String>>`) in and out. The
// model erases the `Box`, so this must cross exactly as a
// `String?` — and because this crate compiles its generated
// binding, a converter that named the wrong type or bridged it
// with the wrong number of dereferences fails the build (#270).
.fun(fun!(boxed_note_echo))
.fun(fun!(plain_note_echo))
// Transparent wrappers on the INPUT side (#292 item 3), one per
// specialized lowering. These rebuild their parameter rather
// than decoding it, so the erased wrapper has to go back on
// before the value reaches the signature — and each layer is
// applied at a different point in the construction, which is why
// one shape cannot cover them all. Compiling this crate is the
// check: a missing `Box::new` is an `E0308`, invisible to any
// text assertion.
.fun(fun!(wrapped_fields_sum))
.fun(fun!(holder_tag_or))
.fun(fun!(boxed_payload_id))
.fun(fun!(boxed_opt_payload_id))
.fun(fun!(boxed_opt_priority_weight))
.fun(fun!(boxed_elem_id_sum))
.fun(fun!(boxed_run_id_sum))
// The same wrapper over a DECOMPOSED return (#292). `Summary`
// has an output expansion, so this return takes no converter to
// name the spelling for it — the extern binds the value and
// matches it, and a `Box` match ergonomics cannot see through
// is an `E0308` that only compiling this crate catches.
.fun(fun!(boxed_latest))
.fun(fun!(ledger_new))
.fun(fun!(archive_set_reading))
.fun(fun!(archive_reading))
Expand All @@ -536,6 +577,7 @@ fn main() {
.fun(fun!(blob_value_echo))
.fun(fun!(arrays_echo))
.fun(fun!(duration_optional))
.fun(fun!(boxed_duration_echo))
.fun(fun!(duration_boundary_echo))
// The converted analogue of `unsigned_emit`: a whole-value
// callback argument, which encodes on its own path rather than
Expand Down Expand Up @@ -700,20 +742,14 @@ fn main() {
// so the stamp recorded at capture time (`covertest-helpers`) would not
// resolve from this crate — `source_named` overrides it with the name this
// crate actually uses, per directory.
let registry = Registry::builder()
.source(perftest_flat::PREBINDGEN_OUT_DIR)
.source_named(cov_helpers::PREBINDGEN_OUT_DIR, "cov_helpers")
.build()
.expect("scan prebindgen items");

let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();

// Rust JNI wrappers → src/generated_bindings.rs (committed; included by lib.rs).
let rust_dest = std::path::Path::new(&crate_dir)
.join("src")
.join("generated_bindings.rs");
let gen = registry.resolve(jni).expect("resolve failed");
let rust_path = gen.write_rust(&rust_dest).expect("write_rust failed");
let jni = binding.build().expect("build failed");
let rust_path = jni.write_rust(&rust_dest).expect("write_rust failed");
println!(
"cargo:warning=Generated bindings at: {}",
rust_path.display()
Expand All @@ -725,7 +761,7 @@ fn main() {
.join("generated");
// The root is prebindgen-owned: `write_kotlin` replaces marked output,
// so no consumer-side cleanup is needed.
for path in gen.write_kotlin(&kotlin_root).expect("write_kotlin failed") {
for path in jni.write_kotlin(&kotlin_root).expect("write_kotlin failed") {
println!("cargo:warning=Wrote {}", path.display());
}

Expand All @@ -735,7 +771,7 @@ fn main() {
std::path::Path::new(&crate_dir)
.join("kotlin")
.join("REPORT.md"),
gen.report(),
jni.report(),
)
.expect("write REPORT.md");
}
14 changes: 14 additions & 0 deletions examples/covertest-kotlin/kotlin/REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ Base package: `io.prebindgen.covertest`
- shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery)
- `blob_value_new` — `fun blobValueNew(secs: Long, id: ByteArray, chunks: List<ByteArray>, onError: JniErrorHandler<BlobValue>): BlobValue`
- shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery)
- `boxed_duration_echo` — `fun boxedDurationEcho(value: ULong, onError: JniErrorHandler<ULong>): ULong`
- `boxed_elem_id_sum` — `fun boxedElemIdSum(ps: List<Payload>, onError: JniErrorHandler<Long>): Long`
- `boxed_latest` — `fun <R> boxedLatest(a: SummaryVault, onError: JniErrorHandler<R?>, build: SummaryBuilder<R>): R?`
- shaped by: return `Summary` decomposed → [count, total] (Callback delivery)
- `boxed_note_echo` — `fun boxedNoteEcho(note: String?, onError: JniErrorHandler<String?>): String?`
- `boxed_opt_payload_id` — `fun boxedOptPayloadId(p: Payload?, onError: JniErrorHandler<Long>): Long`
- `boxed_opt_priority_weight` — `fun boxedOptPriorityWeight(p: Priority?, onError: JniErrorHandler<Long>): Long`
- `boxed_payload_id` — `fun boxedPayloadId(p: Payload, onError: JniErrorHandler<Long>): Long`
- `boxed_run_id_sum` — `fun boxedRunIdSum(ps: List<Payload>, onError: JniErrorHandler<Long>): Long`
- `cache_config_weight` — `fun cacheConfigWeight(cache: CacheConfig?, onError: JniErrorHandler<Int>): Int`
- `celsius_double` — `fun celsiusDouble(c: Int, onError: JniErrorHandler<Int>): Int`
- `duration_boundary_echo` — `fun durationBoundaryEcho(value: DurationBoundary, onError: JniErrorHandler<DurationBoundary>): DurationBoundary`
Expand All @@ -73,6 +82,7 @@ Base package: `io.prebindgen.covertest`
- `hold_echo` — `fun holdEcho(h: Hold, onError: JniErrorHandler<Hold>): Hold`
- shaped by: return `Hold` decomposed → [tag, for_v0] (Callback delivery)
- `hold_policy_echo` — `fun holdPolicyEcho(p: HoldPolicy, onError: JniErrorHandler<HoldPolicy>): HoldPolicy`
- `holder_tag_or` — `fun holderTagOr(h: Holder?, fallback: Long, onError: JniErrorHandler<Long>): Long`
- `label_reverse` — `fun labelReverse(l: String, onError: JniErrorHandler<String>): String`
- `label_series_echo` — `fun labelSeriesEcho(labels: List<String>, onError: JniErrorHandler<List<String>>): List<String>`
- `ledger_each` — `fun ledgerEach(n: Long, sink: LedgerCallback, onError: JniErrorHandler<Unit>)`
Expand All @@ -90,6 +100,7 @@ Base package: `io.prebindgen.covertest`
- `percent_invalid_output` — `fun percentInvalidOutput(onError: JniErrorHandler<Int?>): Int?`
- `percent_optional` — `fun percentOptional(p: Int?, onError: JniErrorHandler<Int?>): Int?`
- `percent_scale` — `fun percentScale(p: Int, factor: Int, onError: JniErrorHandler<Int>): Int`
- `plain_note_echo` — `fun plainNoteEcho(note: String?, onError: JniErrorHandler<String?>): String?`
- `priority_or` — `fun priorityOr(p: Priority?, fallback: Priority, onError: JniErrorHandler<Priority>): Priority`
- `priority_weight` — `fun priorityWeight(p: Priority, onError: JniErrorHandler<Int>): Int`
- `reading_each` — `fun readingEach(n: Int, sink: ReadingCallback, onError: JniErrorHandler<Unit>)`
Expand All @@ -113,6 +124,7 @@ Base package: `io.prebindgen.covertest`
- shaped by: return `Unsigned` decomposed → [byte, short, int, long, maybeLong] (Callback delivery)
- `unsigned_series` — `fun unsignedSeries(onError: JniErrorHandler<List<ULong>>): List<ULong>`
- shaped by: return `u64` decomposed → [] (Callback delivery)
- `wrapped_fields_sum` — `fun wrappedFieldsSum(w: WrappedFields, onError: JniErrorHandler<Long>): Long`

## package `io.prebindgen.covertest.storage`

Expand Down Expand Up @@ -188,6 +200,7 @@ Base package: `io.prebindgen.covertest`
- `EscapeProbe`: ptr_class → `io.prebindgen.covertest.esc_pkg.Esc_Probe` (wire `jni :: sys :: jlong`)
- `Hold`: sealed_class → `io.prebindgen.covertest.model.Hold` (wire `?`)
- `HoldPolicy`: data_class → `io.prebindgen.covertest.model.HoldPolicy` (wire `jni :: objects :: JObject`)
- `Holder`: data_class → `io.prebindgen.covertest.Holder` (wire `jni :: objects :: JObject`)
- `Lookup`: sealed_class → `io.prebindgen.covertest.model.Lookup` (wire `?`)
- `Marker`: sealed_class → `io.prebindgen.covertest.model.Marker` (wire `?`)
- `ObjectBoundary`: data_class → `io.prebindgen.covertest.model.ObjectBoundary` (wire `jni :: objects :: JObject`, input `JObject` opt-in)
Expand All @@ -214,6 +227,7 @@ Base package: `io.prebindgen.covertest`
- `Summary`: ptr_class → `io.prebindgen.covertest.analytics.Summary` (wire `jni :: sys :: jlong`)
- `Tagged`: data_class → `io.prebindgen.covertest.model.Tagged` (wire `jni :: objects :: JObject`)
- `Unsigned`: data_class → `io.prebindgen.covertest.model.Unsigned` (wire `jni :: objects :: JObject`)
- `WrappedFields`: data_class → `io.prebindgen.covertest.WrappedFields` (wire `jni :: objects :: JObject`)

## conversions

Expand Down
Loading
Loading