From 5f3beb2adf4863378ccbf94a1858cbe92ebcf3c2 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Thu, 6 Aug 2026 16:12:43 +0300 Subject: [PATCH 01/43] feat: add note storage schema and codec sections to note packages A note's storage is a bare felt vector, so every off-chain consumer had to hand-mirror the `#[note]` struct's felt layout, and nothing caught drift between the two. This implements the schema design from discussion #1294 (issues #814, #1204, #1307): the type definition travels inside the package, and string-facing behavior binds to it by WIT type identity. The `#[note]` macro now renders the storage struct and its nested `#[export_type]` types as a self-contained WIT document in the same expansion that derives the on-chain decoder, so the schema cannot drift from the code. The compiler carries it into the `.masp` as the digest-exempt `note_storage_schema` section. Named-field structs get schemas; unit structs emit none; tuple structs and `Vec` fields are rejected for now. Off-chain consumption comes in three layers, all `publish = false`: - `miden-note-schema`: loads the schema from a package, interprets the structural felt layout, and builds or decodes `NoteStorage` from named string values through a codec registry with standard leaf codecs. - `miden-note-bindings` (with the shared `miden-note-schema-codegen`): `from_project!`/`from_package!` generate typed host structs with native felt-repr conversions from the embedded schema. - `miden-note-codec` (+ macros): authors implement `AuthorTypeCodec` in a sibling host crate marked with `#[note_codec]`; `export_codecs!` lowers it to the `miden:note-codec` component world. `cargo miden build` compiles that crate to a zero-import Wasm component and attaches it as the `note_codec` section when `[package.metadata.note-codec-crate]` points at it. Consumers load bundled codecs behind the non-default `codec-component` feature, keeping wasmtime out of default builds. The new `dex-note`/`dex-note-codec` examples exercise a custom storage type end to end, mockchain tests consume both the codec and no-codec paths from string inputs, and the shared p2id test encoder now goes through the schema builder instead of a hand-written felt layout. --- Cargo.lock | 685 +++- Cargo.toml | 15 + examples/dex-note-codec/.gitignore | 1 + examples/dex-note-codec/Cargo.lock | 3098 +++++++++++++++++ examples/dex-note-codec/Cargo.toml | 11 + examples/dex-note-codec/src/lib.rs | 140 + examples/dex-note/.cargo/config.toml | 8 + examples/dex-note/.gitignore | 1 + examples/dex-note/Cargo.lock | 3082 ++++++++++++++++ examples/dex-note/Cargo.toml | 19 + examples/dex-note/cargo-generate.toml | 2 + examples/dex-note/miden-project.toml | 20 + examples/dex-note/rust-toolchain.toml | 5 + examples/dex-note/src/lib.rs | 45 + frontend/wasm/src/module/module_env.rs | 39 +- midenc-compile/src/cargo.rs | 4 +- midenc-compile/src/pipeline/assembly.rs | 17 + midenc-compile/src/pipeline/backend.rs | 3 +- midenc-compile/src/pipeline/testing.rs | 1 + sdk/CHANGELOG.md | 13 + sdk/base-macros/Cargo.toml | 1 + sdk/base-macros/src/component_macro/mod.rs | 1 + sdk/base-macros/src/lib.rs | 1 + sdk/base-macros/src/note.rs | 83 +- sdk/base-macros/src/note_schema.rs | 841 +++++ sdk/base-macros/src/types.rs | 37 +- sdk/base-macros/src/types/tests.rs | 32 + sdk/base-macros/src/wit_builder.rs | 26 + sdk/note-bindings/Cargo.toml | 37 + sdk/note-bindings/src/expected/custom.rs | 479 +++ sdk/note-bindings/src/expected/p2id.rs | 359 ++ sdk/note-bindings/src/lib.rs | 356 ++ sdk/note-bindings/src/tests.rs | 111 + sdk/note-bindings/tests/generated_custom.rs | 96 + sdk/note-bindings/tests/p2id_consumer.rs | 151 + sdk/note-codec/Cargo.toml | 30 + sdk/note-codec/macros/Cargo.toml | 32 + sdk/note-codec/macros/src/artifact.rs | 117 + sdk/note-codec/macros/src/expand.rs | 310 ++ sdk/note-codec/macros/src/lib.rs | 65 + sdk/note-codec/macros/src/registry.rs | 154 + sdk/note-codec/macros/src/tests.rs | 59 + sdk/note-codec/src/lib.rs | 165 + sdk/note-codec/tests/component_export.rs | 224 ++ sdk/note-codec/tests/dispatch.rs | 72 + sdk/note-codec/wit/note-codec.wit | 27 + sdk/note-schema/Cargo.toml | 38 + sdk/note-schema/codegen/Cargo.toml | 29 + sdk/note-schema/codegen/src/lib.rs | 641 ++++ sdk/note-schema/codegen/src/tests.rs | 95 + sdk/note-schema/src/builder.rs | 257 ++ sdk/note-schema/src/codec.rs | 295 ++ sdk/note-schema/src/codec_component.rs | 397 +++ sdk/note-schema/src/error.rs | 34 + sdk/note-schema/src/lib.rs | 39 + sdk/note-schema/src/schema.rs | 568 +++ sdk/note-schema/src/tests.rs | 264 ++ sdk/note-schema/src/value.rs | 285 ++ sdk/note-schema/tests/p2id_package.rs | 42 + sdk/wasm-metadata/src/lib.rs | 11 + tests/integration-network/Cargo.toml | 4 + .../src/mockchain/notes/mod.rs | 1 + .../src/mockchain/notes/schema.rs | 203 ++ .../src/mockchain/support/helpers.rs | 11 +- tests/integration/Cargo.toml | 2 + .../src/end_to_end/examples/mod.rs | 1 + .../examples/note_schema_metadata.rs | 394 +++ tools/cargo-miden/Cargo.toml | 5 + .../cargo-miden/tests/dex_note_codec_build.rs | 92 + tools/cargo-miden/tests/mod.rs | 1 + 70 files changed, 14744 insertions(+), 40 deletions(-) create mode 100644 examples/dex-note-codec/.gitignore create mode 100644 examples/dex-note-codec/Cargo.lock create mode 100644 examples/dex-note-codec/Cargo.toml create mode 100644 examples/dex-note-codec/src/lib.rs create mode 100644 examples/dex-note/.cargo/config.toml create mode 100644 examples/dex-note/.gitignore create mode 100644 examples/dex-note/Cargo.lock create mode 100644 examples/dex-note/Cargo.toml create mode 100644 examples/dex-note/cargo-generate.toml create mode 100644 examples/dex-note/miden-project.toml create mode 100644 examples/dex-note/rust-toolchain.toml create mode 100644 examples/dex-note/src/lib.rs create mode 100644 sdk/base-macros/src/note_schema.rs create mode 100644 sdk/note-bindings/Cargo.toml create mode 100644 sdk/note-bindings/src/expected/custom.rs create mode 100644 sdk/note-bindings/src/expected/p2id.rs create mode 100644 sdk/note-bindings/src/lib.rs create mode 100644 sdk/note-bindings/src/tests.rs create mode 100644 sdk/note-bindings/tests/generated_custom.rs create mode 100644 sdk/note-bindings/tests/p2id_consumer.rs create mode 100644 sdk/note-codec/Cargo.toml create mode 100644 sdk/note-codec/macros/Cargo.toml create mode 100644 sdk/note-codec/macros/src/artifact.rs create mode 100644 sdk/note-codec/macros/src/expand.rs create mode 100644 sdk/note-codec/macros/src/lib.rs create mode 100644 sdk/note-codec/macros/src/registry.rs create mode 100644 sdk/note-codec/macros/src/tests.rs create mode 100644 sdk/note-codec/src/lib.rs create mode 100644 sdk/note-codec/tests/component_export.rs create mode 100644 sdk/note-codec/tests/dispatch.rs create mode 100644 sdk/note-codec/wit/note-codec.wit create mode 100644 sdk/note-schema/Cargo.toml create mode 100644 sdk/note-schema/codegen/Cargo.toml create mode 100644 sdk/note-schema/codegen/src/lib.rs create mode 100644 sdk/note-schema/codegen/src/tests.rs create mode 100644 sdk/note-schema/src/builder.rs create mode 100644 sdk/note-schema/src/codec.rs create mode 100644 sdk/note-schema/src/codec_component.rs create mode 100644 sdk/note-schema/src/error.rs create mode 100644 sdk/note-schema/src/lib.rs create mode 100644 sdk/note-schema/src/schema.rs create mode 100644 sdk/note-schema/src/tests.rs create mode 100644 sdk/note-schema/src/value.rs create mode 100644 sdk/note-schema/tests/p2id_package.rs create mode 100644 tests/integration-network/src/mockchain/notes/schema.rs create mode 100644 tests/integration/src/end_to_end/examples/note_schema_metadata.rs create mode 100644 tools/cargo-miden/tests/dex_note_codec_build.rs diff --git a/Cargo.lock b/Cargo.lock index d48b4a042b..0e6205b05b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,15 @@ dependencies = [ "regex", ] +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli 0.31.1", +] + [[package]] name = "addr2line" version = "0.25.1" @@ -242,6 +251,21 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object 0.39.1", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "ark-ff" version = "0.3.0" @@ -761,6 +785,9 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2 0.2.21", +] [[package]] name = "byte-slice-cast" @@ -806,7 +833,10 @@ dependencies = [ "liquid", "liquid-core", "log", + "miden-mast-package", + "miden-project", "midenc-compile", + "midenc-frontend-wasm-metadata", "midenc-hir", "midenc-log", "midenc-session", @@ -816,6 +846,8 @@ dependencies = [ "tempfile", "toml_edit", "walkdir", + "wit-component", + "wit-parser 0.247.0", ] [[package]] @@ -1012,6 +1044,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + [[package]] name = "codegen" version = "0.3.0" @@ -1154,6 +1195,43 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce81edaca6167d1f78da026afa92d7ff957a80aa82a79076e11cd34cde20165" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d0d51e12f958551165969c6e8767e1e461729f6c1ccae923b0ba1d5cbcbbbf8" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41294c755094d2c8a514cea903039742474423f2e91601332eab5f4094f76333" +dependencies = [ + "cranelift-entity 0.121.2", +] + +[[package]] +name = "cranelift-bitset" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb6f5d0df5bd0d02c63ec48e8f2e38a176b123f59e084f22caf89a0d0593e7e" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "cranelift-bitset" version = "0.135.1" @@ -1163,16 +1241,116 @@ dependencies = [ "wasmtime-internal-core", ] +[[package]] +name = "cranelift-codegen" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e543cdb278b7c15f739021cf880ee1808c68fa2402febb87edb9307f552c8fec" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset 0.121.2", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity 0.121.2", + "cranelift-isle", + "gimli 0.31.1", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f979c75cfd712dbc754799dfe4a4d0db7a51defc2e36d006b27a8a63e018eece" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f36e74ba4033490587a47952f74390cb7d4f1fc1fa28ace50564e491f1e38f" + +[[package]] +name = "cranelift-control" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6671962c7d65b9a7ad038cd92da6784744d8a9ecf8ded8bb9a1f7046dbe2ccf" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee832f8329fa87c5df6c1d64a8506a58031e6f8a190d9b21b1900272a4dbb47d" +dependencies = [ + "cranelift-bitset 0.121.2", + "serde", + "serde_derive", +] + [[package]] name = "cranelift-entity" version = "0.135.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0ec225d6b79c5d61358cb5ea081a1384ec541dbf8c1a8c618d187875af326ad" dependencies = [ - "cranelift-bitset", + "cranelift-bitset 0.135.1", "wasmtime-internal-core", ] +[[package]] +name = "cranelift-frontend" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7bc17aa3277214eab4b63a03544b1b46962154012b751c9f14c2a5419c6471" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff02dcecae2e7e9c61b713f1fb46eabecdca9f55b49f99859ceb1a3e7f4a9cb" + +[[package]] +name = "cranelift-native" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f76fd681f35bdf17be9c3e516b9acc0c7bd61b81faf95496decd8e0000979c" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3d9071bc5ee5573e723d9d84a45b7025a29e8f2c5ad81b3b9d0293129541d9" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1543,6 +1721,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "ena" version = "0.14.4" @@ -1552,6 +1742,15 @@ dependencies = [ "log", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-ordinalize" version = "4.4.2" @@ -1910,6 +2109,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap 2.14.0", + "stable_deref_trait", +] + [[package]] name = "gimli" version = "0.32.3" @@ -2001,6 +2211,7 @@ dependencies = [ "allocator-api2 0.2.21", "equivalent", "foldhash 0.1.5", + "serde", ] [[package]] @@ -2783,6 +2994,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "macro-string" version = "0.2.0" @@ -2809,6 +3029,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + [[package]] name = "memmap2" version = "0.9.11" @@ -2959,6 +3188,7 @@ dependencies = [ "miden-mast-package", "miden-project", "miden-protocol", + "midenc-expect-test", "midenc-frontend-wasm-metadata", "proc-macro2", "quote", @@ -3378,6 +3608,86 @@ dependencies = [ "tonic-prost-build", ] +[[package]] +name = "miden-note-bindings" +version = "0.14.0" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-mast-package", + "miden-note-schema", + "miden-note-schema-codegen", + "miden-protocol", + "midenc-expect-test", + "midenc-frontend-wasm", + "midenc-integration-test-support", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "miden-note-codec" +version = "0.14.0" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-note-codec-macros", + "miden-protocol", + "tempfile", + "wit-bindgen", + "wit-component", + "wit-parser 0.247.0", +] + +[[package]] +name = "miden-note-codec-macros" +version = "0.14.0" +dependencies = [ + "heck", + "miden-mast-package", + "miden-note-schema", + "miden-note-schema-codegen", + "prettyplease", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-note-schema" +version = "0.14.0" +dependencies = [ + "miden-core", + "miden-field", + "miden-field-repr", + "miden-mast-package", + "miden-protocol", + "midenc-frontend-wasm", + "midenc-frontend-wasm-metadata", + "midenc-integration-test-support", + "tempfile", + "wasmtime", + "wit-component", + "wit-parser 0.247.0", +] + +[[package]] +name = "miden-note-schema-codegen" +version = "0.14.0" +dependencies = [ + "heck", + "miden-note-schema", + "midenc-expect-test", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "miden-note-transport-proto-build" version = "0.5.0-alpha.1" @@ -3962,7 +4272,7 @@ version = "0.10.0" dependencies = [ "addr2line 0.26.1", "anyhow", - "cranelift-entity", + "cranelift-entity 0.135.1", "gimli 0.33.0", "indexmap 2.14.0", "log", @@ -3979,7 +4289,7 @@ dependencies = [ "midenc-hir-symbol", "midenc-session", "wasmparser 0.248.0", - "wasmprinter", + "wasmprinter 0.248.0", "wat", ] @@ -4137,12 +4447,14 @@ dependencies = [ "miden-field", "miden-field-repr", "miden-mast-package", + "miden-note-schema", "miden-protocol", "miden-standards", "miden-testing", "miden-tx-script-args", "midenc-expect-test", "midenc-frontend-wasm", + "midenc-frontend-wasm-metadata", "midenc-integration-test-support", "rand 0.10.2", "tokio", @@ -4210,6 +4522,7 @@ dependencies = [ "syn 2.0.119", "wasmi", "wat", + "wit-bindgen-core", ] [[package]] @@ -4474,6 +4787,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "object" version = "0.37.3" @@ -4949,6 +5274,18 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -5210,6 +5547,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "pubgrub" version = "0.3.0" @@ -5244,6 +5591,29 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "pulley-interpreter" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14280b69a9cbb6ada02a7aa5f7b3f1b72d1043b5bc9336990b700525dea6e3" +dependencies = [ + "cranelift-bitset 0.121.2", + "log", + "pulley-macros", + "wasmtime-math", +] + +[[package]] +name = "pulley-macros" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076f1be746801280af4c96c4407b5fd1d09cfa53ab27ba0ac7dd8f207e7bbf83" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pyo3-build-config" version = "0.28.3" @@ -5457,6 +5827,20 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "regalloc2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +dependencies = [ + "allocator-api2 0.2.21", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + [[package]] name = "regex" version = "1.13.1" @@ -7068,6 +7452,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.233.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9679ae3cf7cfa2ca3a327f7fab97f27f3294d402fd1a76ca8ab514e17973e4d3" +dependencies = [ + "leb128fmt", + "wasmparser 0.233.0", +] + [[package]] name = "wasm-encoder" version = "0.247.0" @@ -7154,6 +7548,19 @@ dependencies = [ "wasmi_core", ] +[[package]] +name = "wasmparser" +version = "0.233.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51cb03afce7964bbfce46602d6cb358726f36430b6ba084ac6020d8ce5bc102" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver 1.0.28", + "serde", +] + [[package]] name = "wasmparser" version = "0.239.0" @@ -7198,6 +7605,17 @@ dependencies = [ "semver 1.0.28", ] +[[package]] +name = "wasmprinter" +version = "0.233.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf8e5b732895c99b21aa615f1b73352e51bbe2b2cb6c87eae7f990d07c1ac18" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.233.0", +] + [[package]] name = "wasmprinter" version = "0.248.0" @@ -7209,6 +7627,149 @@ dependencies = [ "wasmparser 0.248.0", ] +[[package]] +name = "wasmtime" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec10e50038f22ab407fdd8708120b8feed3450a02618efcf26ca47e82122927d" +dependencies = [ + "addr2line 0.24.2", + "anyhow", + "bitflags 2.13.1", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "libc", + "log", + "mach2", + "memfd", + "object 0.36.7", + "once_cell", + "postcard", + "psm", + "pulley-interpreter", + "rustix", + "semver 1.0.28", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser 0.233.0", + "wasmtime-asm-macros", + "wasmtime-component-macro", + "wasmtime-component-util", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-icache-coherence", + "wasmtime-math", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "wasmtime-winch", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d379cda46d6fd18619e282a75fbb09b70b3d0f166b605f45b4059dfaf9dc6ce" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-component-macro" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b08be093e0a876da45f79070c2ada4656f2785eb77c01b86ce60be3153920a5" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser 0.233.0", +] + +[[package]] +name = "wasmtime-component-util" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0451ce0dd94a33d0dbd57934ce666a04c2753a5262ca2bc84cf6a67cf5303dc" + +[[package]] +name = "wasmtime-cranelift" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15aa836683d7398f13f2f26bbe74c404ceaba66b6bbb96700d6b7f91bec90e03" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity 0.121.2", + "cranelift-frontend", + "cranelift-native", + "gimli 0.31.1", + "itertools 0.14.0", + "log", + "object 0.36.7", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser 0.233.0", + "wasmtime-environ", + "wasmtime-math", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317081a0cbbb1f749d348b262575608fc082d47ab11b6247bbe9163eeb955777" +dependencies = [ + "anyhow", + "cranelift-bitset 0.121.2", + "cranelift-entity 0.121.2", + "gimli 0.31.1", + "indexmap 2.14.0", + "log", + "object 0.36.7", + "postcard", + "semver 1.0.28", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.233.0", + "wasmparser 0.233.0", + "wasmprinter 0.233.0", + "wasmtime-component-util", +] + +[[package]] +name = "wasmtime-fiber" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6763b33eceefc443f6477d84dc8751df5f23d280d7e01f28339fa3ec4b00ff13" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.59.0", +] + [[package]] name = "wasmtime-internal-core" version = "48.0.1" @@ -7219,6 +7780,73 @@ dependencies = [ "libm", ] +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea6b740d1a35f2cebfe88e013ac8a4a84ff8dabc3a392df920abf554e871cf2" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-math" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62fa317691aedc64aae3a86b3d786e4b2b0007bc0b56e0b6098b8b5a85ab2134" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-slab" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a06819d24370273021054b50589e3078e7f5cfac15515e58b3fbbebf5e5b39" + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca100ed168ffc9b37aefc07a5be440645eab612a2ff6e2ff884e8cc3740e666" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-winch" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595f51430606a7b5578f34e0d7c73dca52a22ed24756f2ba9d4d0c1bde8631af" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli 0.31.1", + "object 0.36.7", + "target-lexicon", + "wasmparser 0.233.0", + "wasmtime-cranelift", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233fdcb96f9097be697319ba647ef42bdbdb40e89f04c8ae3713103813b5b793" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "wit-parser 0.233.0", +] + [[package]] name = "wast" version = "256.0.0" @@ -7282,6 +7910,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf007d7940f62127ce4f33a8aa92dadedfdc78c3860a057e06c8c24e26e180d" +dependencies = [ + "anyhow", + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli 0.31.1", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser 0.233.0", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-math", +] + [[package]] name = "wincode" version = "0.6.1" @@ -7404,6 +8052,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -7518,7 +8175,7 @@ checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.247.0", ] [[package]] @@ -7568,7 +8225,25 @@ dependencies = [ "wasm-encoder 0.247.0", "wasm-metadata", "wasmparser 0.247.0", - "wit-parser", + "wit-parser 0.247.0", +] + +[[package]] +name = "wit-parser" +version = "0.233.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22f1cd55247a2e616870b619766e9522df36b7abafbb29bbeb34b7a9da7e9f0" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver 1.0.28", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.233.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 48e765c5ec..f1f86e87f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,11 @@ members = [ "sdk/base-macros", "sdk/base-sys", "sdk/build-script-support", + "sdk/note-bindings", + "sdk/note-codec", + "sdk/note-codec/macros", + "sdk/note-schema", + "sdk/note-schema/codegen", "sdk/sdk", "sdk/stdlib-sys", "sdk/tx-script-args", @@ -160,6 +165,12 @@ wasmparser = { version = "^0.248", default-features = false, features = [ "simd", ] } wit-component = "0.247" +wit-parser = "0.247" +wasmtime = { version = "34.0.0", default-features = false, features = [ + "component-model", + "cranelift", + "runtime", +] } # Workspace crates midenc-codegen-masm = { version = "0.10.0", path = "codegen/masm" } @@ -186,6 +197,10 @@ midenc-integration-test-support = { path = "tests/support" } midenc-expect-test = { path = "tools/expect-test" } miden-base-sys = { version = "0.14.0", path = "sdk/base-sys" } miden-field-repr = { version = "0.14.0", path = "sdk/field-repr/repr" } +miden-note-codec = { version = "0.14.0", path = "sdk/note-codec" } +miden-note-codec-macros = { version = "0.14.0", path = "sdk/note-codec/macros" } +miden-note-schema = { version = "0.14.0", path = "sdk/note-schema" } +miden-note-schema-codegen = { version = "0.14.0", path = "sdk/note-schema/codegen" } miden-stdlib-sys = { version = "0.14.0", path = "sdk/stdlib-sys" } miden-tx-script-args = { version = "0.14.0", path = "sdk/tx-script-args" } diff --git a/examples/dex-note-codec/.gitignore b/examples/dex-note-codec/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/examples/dex-note-codec/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/dex-note-codec/Cargo.lock b/examples/dex-note-codec/Cargo.lock new file mode 100644 index 0000000000..24f42869c8 --- /dev/null +++ b/examples/dex-note-codec/Cargo.lock @@ -0,0 +1,3098 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer", + "crypto-common", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.119", +] + +[[package]] +name = "dex-note-codec" +version = "0.1.0" +dependencies = [ + "miden-note-codec", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "ctutils", +] + +[[package]] +name = "dissimilar" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common", + "digest", + "ff", + "group", + "hkdf", + "hybrid-array", + "pkcs8", + "rand_core 0.10.1", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" +dependencies = [ + "cpubits", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", + "wnaf", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miden-ace-codegen" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1831e4b40ba86d848581824b7089da20fb039dd8161f44e02b1ae2e3da6f30" +dependencies = [ + "miden-core", + "miden-crypto", + "thiserror", +] + +[[package]] +name = "miden-air" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1cb4a9efe57aa970a7506b07729abd32702bdc6482a2a0364ecb045866c1d5b" +dependencies = [ + "miden-ace-codegen", + "miden-core", + "miden-crypto", + "miden-utils-indexing", + "proptest", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-assembly" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31a8dbf11a81ae5f563ef5140a33bff2ec413ff0d34ca48404a8d11a2a43280" +dependencies = [ + "log", + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "miden-project", + "proptest", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-assembly-syntax" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97be191cb4063a22312d88c5f6debf020d1f7b7d4eb2b1563172c4fda1f0b5de" +dependencies = [ + "log", + "miden-assembly-syntax-cst", + "miden-core", + "miden-debug-types", + "miden-utils-diagnostics", + "midenc-hir-type", + "proptest", + "regex", + "rustc_version 0.4.1", + "semver 1.0.28", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-assembly-syntax-cst" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb812985ff68aa2d17ea8a87b48e769c5f9f69974ee723323c4739ef00a54f0" +dependencies = [ + "miden-debug-types", + "miden-rowan", + "miden-utils-diagnostics", + "thiserror", +] + +[[package]] +name = "miden-core" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e26dcf78743d4abbca1bbc6799712e4ea674bad8e86dc4fd8732bf237450b1" +dependencies = [ + "derive_more", + "log", + "miden-crypto", + "miden-debug-types", + "miden-formatting", + "miden-utils-core-derive", + "miden-utils-indexing", + "miden-utils-sync", + "serde", + "thiserror", +] + +[[package]] +name = "miden-core-lib" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcb1063ac5272a02037a5fb36355b267024aa09de81bb46f54bb68ced0eebb1" +dependencies = [ + "env_logger", + "fs-err", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-crypto", + "miden-mast-package", + "miden-package-registry", + "miden-processor", + "miden-utils-sync", + "thiserror", +] + +[[package]] +name = "miden-crypto" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10eaaf3e927c3c6a720a3ed782123be25e38d34c5a7be2ca1bfbbef10a940396" +dependencies = [ + "blake3", + "cc", + "chacha20poly1305", + "curve25519-dalek", + "der", + "ed25519-dalek", + "flume", + "hkdf", + "k256", + "miden-crypto-derive", + "miden-field", + "miden-lifted-stark", + "miden-serde-utils", + "num", + "num-complex", + "once_cell", + "p3-blake3", + "p3-challenger", + "p3-dft", + "p3-goldilocks", + "p3-keccak", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.2", + "rand_chacha 0.10.0", + "rayon", + "serde", + "sha2", + "sha3", + "subtle", + "thiserror", + "x25519-dalek", +] + +[[package]] +name = "miden-crypto-derive" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3958eeade8b938895d3f170fd82f43e9d4141a799443b796e6496ecd9bc107d0" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-debug-types" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b9ba4ccb10ca8f719dc3c96a418aa2ade8251f108fba6034facb3e3eabae70" +dependencies = [ + "memchr", + "miden-crypto", + "miden-formatting", + "miden-miette", + "miden-utils-indexing", + "miden-utils-sync", + "paste", + "proptest", + "serde", + "serde_spanned", + "thiserror", + "zerocopy", +] + +[[package]] +name = "miden-field" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25fbce3dc2399704c7094d1d4e500ffa1fe6dabb11bf9e512fc3764498b2e1" +dependencies = [ + "miden-serde-utils", + "num-bigint 0.5.1", + "p3-challenger", + "p3-field", + "p3-goldilocks", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "subtle", + "thiserror", +] + +[[package]] +name = "miden-field-repr" +version = "0.13.1" +dependencies = [ + "miden-field", + "miden-field-repr-derive", +] + +[[package]] +name = "miden-field-repr-derive" +version = "0.13.1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-formatting" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e392e0a8c34b32671012b439de35fa8987bf14f0f8aac279b97f8b8cc6e263b" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "miden-lifted-air" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a183cb8209eb2e80bafb33fec0e841b290877d83267dfa888607ffb671ab684" +dependencies = [ + "p3-air", + "p3-challenger", + "p3-field", + "p3-matrix", + "p3-util", + "thiserror", +] + +[[package]] +name = "miden-lifted-stark" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a24dd2d8fd4978c56dd7c3d3c76c1a6fb17fef1edfe470ed4e517e38a556bdb" +dependencies = [ + "miden-lifted-air", + "miden-stark-transcript", + "miden-stateful-hasher", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-goldilocks", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.2", + "serde", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-mast-package" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee79180c3d317ab6239d7c2c488760fe3590b9f639e549d9d13f3e0f314a381" +dependencies = [ + "hashbrown", + "log", + "miden-assembly-syntax", + "miden-core", + "miden-debug-types", + "miden-utils-indexing", + "rustc-hash", + "serde", + "thiserror", + "zerocopy", +] + +[[package]] +name = "miden-miette" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef536978f24a179d94fa2a41e4f92b28e7d8aab14b8d23df28ad2a3d7098b20" +dependencies = [ + "cfg-if", + "futures", + "indenter", + "lazy_static", + "miden-miette-derive", + "owo-colors", + "regex", + "rustc_version 0.2.3", + "rustversion", + "serde_json", + "spin 0.9.9", + "strip-ansi-escapes", + "syn 2.0.119", + "textwrap", + "thiserror", + "trybuild", + "unicode-width 0.1.14", +] + +[[package]] +name = "miden-miette-derive" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a905f3ea65634dd4d1041a4f0fd0a3e77aa4118341d265af1a94339182222f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-note-codec" +version = "0.13.1" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-note-codec-macros", + "miden-protocol", + "wit-bindgen", +] + +[[package]] +name = "miden-note-codec-macros" +version = "0.13.1" +dependencies = [ + "heck", + "miden-mast-package", + "miden-note-schema", + "miden-note-schema-codegen", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-note-schema" +version = "0.13.1" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-mast-package", + "miden-protocol", + "midenc-frontend-wasm-metadata", + "wit-parser", +] + +[[package]] +name = "miden-note-schema-codegen" +version = "0.13.1" +dependencies = [ + "heck", + "miden-note-schema", + "proc-macro2", + "quote", +] + +[[package]] +name = "miden-package-registry" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bf8ca360321a414771807cd3b95c732e99e30bf7187a9a80a0e63db36bbcc2" +dependencies = [ + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "proptest", + "pubgrub", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-processor" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6853b729e245f8c310bfba8e47cdbd303b1d11763499d3b7efd90bc821239aa9" +dependencies = [ + "itertools 0.14.0", + "miden-air", + "miden-core", + "miden-debug-types", + "miden-mast-package", + "miden-utils-diagnostics", + "miden-utils-indexing", + "paste", + "rayon", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-project" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d12f4281e563c01305d989461574ccab41b749ac5d6e987ede1e9941ed18f5e" +dependencies = [ + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "proptest", + "serde", + "serde-untagged", + "thiserror", + "toml", +] + +[[package]] +name = "miden-protocol" +version = "0.16.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f185d7a1e0c6c05ae760281956de3f6057e6d7889daeade4c71c158d6a252748" +dependencies = [ + "bech32", + "fs-err", + "getrandom 0.4.3", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-core-lib", + "miden-crypto", + "miden-crypto-derive", + "miden-mast-package", + "miden-package-registry", + "miden-processor", + "miden-utils-sync", + "miden-verifier", + "rand 0.10.2", + "regex", + "semver 1.0.28", + "serde", + "thiserror", + "toml", + "walkdir", +] + +[[package]] +name = "miden-rowan" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13695bf99aabaa21d6572b807c66bb26251aa3d9b75e828b3c99b97a3b1ce7e" +dependencies = [ + "hashbrown", + "rustc-hash", +] + +[[package]] +name = "miden-serde-utils" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63ed62fe47d4e6255502618761a8f27d9c33f22759fbace97a6fa36a03252659" +dependencies = [ + "p3-field", + "p3-goldilocks", +] + +[[package]] +name = "miden-stark-transcript" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc79432ed9d7cf1264217ca7af4c7c1768bded83bad2429ee1ab2fb2d394d774" +dependencies = [ + "p3-challenger", + "p3-field", + "serde", + "thiserror", +] + +[[package]] +name = "miden-stateful-hasher" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08359f6cabcc418a76ac9317d6e1ccb33800a1724e08c5b4b1ced7d8aeb26e1b" +dependencies = [ + "p3-field", + "p3-symmetric", +] + +[[package]] +name = "miden-utils-core-derive" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb2baba2f71907ab82be0d064410030196f8e06f19687d7bb33970e02dd9cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "miden-utils-diagnostics" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485627595e49b2d83d163511ddc7200194a07ee80bbeab67713ebcfeab57e2e4" +dependencies = [ + "miden-debug-types", + "miden-miette", + "tracing", +] + +[[package]] +name = "miden-utils-indexing" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6be5944699579d8babf57d0d6dd625cb59b7a10bdf85e5fc96ae568991df9f" +dependencies = [ + "miden-serde-utils", + "proptest", + "serde", + "thiserror", +] + +[[package]] +name = "miden-utils-sync" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4980ed8c1f02727ef294ec78ec2e73c367bc53ca784f4de62c1c3e5b6cbfe3" +dependencies = [ + "lock_api", + "loom", + "once_cell", + "parking_lot", +] + +[[package]] +name = "miden-verifier" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afef2b344a7c0a5a90c2b6335f4ab50e74cbaa255009ec718eb6c9598b7d9486" +dependencies = [ + "miden-air", + "miden-core", + "miden-crypto", + "serde", + "serde-wincode", + "thiserror", + "tracing", +] + +[[package]] +name = "midenc-frontend-wasm-metadata" +version = "0.13.1" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "midenc-hir-type" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b879cc9e04ad1b98ccd2fe53b1ed4ed4aa00d3231506a1e1703b17b31b3389" +dependencies = [ + "miden-formatting", + "miden-serde-utils", + "serde", + "serde_repr", + "smallvec", + "thiserror", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint 0.4.8", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint 0.4.8", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "p3-air" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb1be05c0d6f691afe0c9f468018a9a37cfa904dee78a8081ec96eb3cdd88e8" +dependencies = [ + "p3-field", + "p3-matrix", + "tracing", +] + +[[package]] +name = "p3-blake3" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f202f5fbcceb6f56f783d98efb5de27e5a171470e3364de97b0923b39c87ab5" +dependencies = [ + "blake3", + "p3-symmetric", + "p3-util", +] + +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin 0.12.2", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-goldilocks" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b3f31080df31be723b876709246f8f1e532e1c5b82efb5281d705c8304c63" +dependencies = [ + "num-bigint 0.5.1", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "spin 0.12.2", +] + +[[package]] +name = "p3-keccak" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae50c8c37eb847c660298fb275e53c025c49b2623a8cfabf67f5322258b2b4db" +dependencies = [ + "p3-symmetric", + "p3-util", + "tiny-keccak", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" +dependencies = [ + "rayon", +] + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "spin 0.12.2", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.2", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "rayon", + "serde", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "primefield", + "serdect", + "wnaf", +] + +[[package]] +name = "priority-queue" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" +dependencies = [ + "equivalent", + "indexmap", + "serde", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + +[[package]] +name = "pubgrub" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5df7e552bc7edd075f5783a87fbfc21d6a546e32c16985679c488c18192d83" +dependencies = [ + "indexmap", + "log", + "priority-queue", + "rustc-hash", + "thiserror", + "version-ranges", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.28", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-wincode" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa9d3a86c66cf10ce79df36f555a5a4c8d72a82515d9ea8ca420e02c925c30f" +dependencies = [ + "serde", + "thiserror", + "wincode", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest", + "keccak", + "sponge-cursor", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest", + "rand_core 0.10.1", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8abadc99fd9c7bbb7d0ca2b31d72a067d0c0dcd7aad25ab8cac71ba91417694b" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-triple" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trybuild" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" +dependencies = [ + "dissimilar", + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common", + "ctutils", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8" +dependencies = [ + "smallvec", +] + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +dependencies = [ + "bitflags 2.13.1", + "hashbrown", + "indexmap", + "semver 1.0.28", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "wincode" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d39d1a984eb7ae37afa348f058216d62a6d5f71640f4113a8114386c2a812a" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.119", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +dependencies = [ + "anyhow", + "bitflags 2.13.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver 1.0.28", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek", + "rand_core 0.10.1", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/dex-note-codec/Cargo.toml b/examples/dex-note-codec/Cargo.toml new file mode 100644 index 0000000000..875476c045 --- /dev/null +++ b/examples/dex-note-codec/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "dex-note-codec" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden-note-codec = { path = "../../sdk/note-codec" } diff --git a/examples/dex-note-codec/src/lib.rs b/examples/dex-note-codec/src/lib.rs new file mode 100644 index 0000000000..f43ee44dd9 --- /dev/null +++ b/examples/dex-note-codec/src/lib.rs @@ -0,0 +1,140 @@ +//! Author-side codecs for the DEX note storage schema. + +use miden_note_codec::AuthorTypeCodec; + +miden_note_codec::from_project!("../dex-note"); + +#[miden_note_codec::note_codec] +impl AuthorTypeCodec for LimitPrice { + fn parse(value: &str) -> Result { + parse_limit_price(value) + } + + fn display(&self) -> String { + display_limit_price(self) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the limit-price denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } +} + +miden_note_codec::export_codecs!(); + +/// Parses a fraction or finite decimal limit price. +fn parse_limit_price(value: &str) -> Result { + let (numerator, denominator) = if let Some((numerator, denominator)) = value.split_once('/') { + (parse_part(numerator, "numerator")?, parse_part(denominator, "denominator")?) + } else if let Some((whole, fraction)) = value.split_once('.') { + if whole.is_empty() || fraction.is_empty() || fraction.contains('.') { + return Err(format!("invalid decimal limit price `{value}`")); + } + let whole = parse_part(whole, "whole part")?; + let fraction_value = parse_part(fraction, "fractional part")?; + let scale = u32::try_from(fraction.len()) + .map_err(|_| format!("decimal limit price `{value}` has too many digits"))?; + let denominator = 10_u64 + .checked_pow(scale) + .ok_or_else(|| format!("decimal limit price `{value}` has too many digits"))?; + let numerator = whole + .checked_mul(denominator) + .and_then(|whole| whole.checked_add(fraction_value)) + .ok_or_else(|| format!("decimal limit price `{value}` is too large"))?; + (numerator, denominator) + } else { + (parse_part(value, "value")?, 1) + }; + let divisor = greatest_common_divisor(numerator, denominator); + let (numerator, denominator) = if divisor > 1 { + (numerator / divisor, denominator / divisor) + } else { + (numerator, denominator) + }; + Ok(LimitPrice { + numerator, + denominator, + }) +} + +/// Parses one unsigned integer part. +fn parse_part(value: &str, name: &str) -> Result { + if value.is_empty() { + return Err(format!("the limit-price {name} is empty")); + } + value + .parse::() + .map_err(|error| format!("invalid limit-price {name} `{value}`: {error}")) +} + +/// Displays finite fractions as decimals and other fractions as ratios. +fn display_limit_price(value: &LimitPrice) -> String { + if value.denominator == 0 { + return format!("{}/{}", value.numerator, value.denominator); + } + let divisor = greatest_common_divisor(value.numerator, value.denominator); + let numerator = value.numerator / divisor; + let denominator = value.denominator / divisor; + let mut remainder = denominator; + let mut twos = 0_u32; + let mut fives = 0_u32; + while remainder.is_multiple_of(2) { + remainder /= 2; + twos += 1; + } + while remainder.is_multiple_of(5) { + remainder /= 5; + fives += 1; + } + if remainder != 1 { + return format!("{numerator}/{denominator}"); + } + + let scale = twos.max(fives); + let Some(power) = 10_u64.checked_pow(scale) else { + return format!("{numerator}/{denominator}"); + }; + let Some(scaled) = numerator.checked_mul(power / denominator) else { + return format!("{numerator}/{denominator}"); + }; + if scale == 0 { + return scaled.to_string(); + } + let whole = scaled / power; + let mut fraction = format!("{:0width$}", scaled % power, width = scale as usize); + while fraction.ends_with('0') { + fraction.pop(); + } + format!("{whole}.{fraction}") +} + +/// Returns the greatest common divisor of two integers. +fn greatest_common_divisor(mut left: u64, mut right: u64) -> u64 { + while right != 0 { + (left, right) = (right, left % right); + } + left.max(1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_fraction_and_decimal_forms() { + let fraction = LimitPrice::parse("3/2").unwrap(); + let decimal = LimitPrice::parse("1.5").unwrap(); + assert_eq!(fraction, decimal); + assert_eq!(fraction.display(), "1.5"); + fraction.validate().unwrap(); + } + + #[test] + fn rejects_zero_denominator_during_validation() { + let value = LimitPrice::parse("1/0").unwrap(); + assert!(value.validate().unwrap_err().contains("denominator")); + } +} diff --git a/examples/dex-note/.cargo/config.toml b/examples/dex-note/.cargo/config.toml new file mode 100644 index 0000000000..82112cc9a5 --- /dev/null +++ b/examples/dex-note/.cargo/config.toml @@ -0,0 +1,8 @@ +# This example is intended to be built as Wasm for the Miden VM. + +[build] +target = "wasm32-wasip2" + +[target.wasm32-wasip2] +# Force-enable `cfg(miden)` for Miden-VM-targeted builds (including editor/LSP workflows). +rustflags = ["--cfg", "miden"] diff --git a/examples/dex-note/.gitignore b/examples/dex-note/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/examples/dex-note/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/dex-note/Cargo.lock b/examples/dex-note/Cargo.lock new file mode 100644 index 0000000000..ddcce11e11 --- /dev/null +++ b/examples/dex-note/Cargo.lock @@ -0,0 +1,3082 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer", + "crypto-common", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.119", +] + +[[package]] +name = "dex-note" +version = "0.1.0" +dependencies = [ + "miden", + "miden-field-repr", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "ctutils", +] + +[[package]] +name = "dissimilar" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common", + "digest", + "ff", + "group", + "hkdf", + "hybrid-array", + "pkcs8", + "rand_core 0.10.1", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" +dependencies = [ + "cpubits", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", + "wnaf", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miden" +version = "0.13.1" +dependencies = [ + "miden-base", + "miden-base-macros", + "miden-base-sys", + "miden-field", + "miden-field-repr", + "miden-sdk-alloc", + "miden-stdlib-sys", + "wit-bindgen", +] + +[[package]] +name = "miden-ace-codegen" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1831e4b40ba86d848581824b7089da20fb039dd8161f44e02b1ae2e3da6f30" +dependencies = [ + "miden-core", + "miden-crypto", + "thiserror", +] + +[[package]] +name = "miden-air" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1cb4a9efe57aa970a7506b07729abd32702bdc6482a2a0364ecb045866c1d5b" +dependencies = [ + "miden-ace-codegen", + "miden-core", + "miden-crypto", + "miden-utils-indexing", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-assembly" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31a8dbf11a81ae5f563ef5140a33bff2ec413ff0d34ca48404a8d11a2a43280" +dependencies = [ + "log", + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "miden-project", + "proptest", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-assembly-syntax" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97be191cb4063a22312d88c5f6debf020d1f7b7d4eb2b1563172c4fda1f0b5de" +dependencies = [ + "log", + "miden-assembly-syntax-cst", + "miden-core", + "miden-debug-types", + "miden-utils-diagnostics", + "midenc-hir-type", + "proptest", + "regex", + "rustc_version 0.4.1", + "semver 1.0.28", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-assembly-syntax-cst" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb812985ff68aa2d17ea8a87b48e769c5f9f69974ee723323c4739ef00a54f0" +dependencies = [ + "miden-debug-types", + "miden-rowan", + "miden-utils-diagnostics", + "thiserror", +] + +[[package]] +name = "miden-base" +version = "0.13.1" +dependencies = [ + "miden-base-sys", + "miden-stdlib-sys", +] + +[[package]] +name = "miden-base-macros" +version = "0.13.1" +dependencies = [ + "heck", + "miden-assembly-syntax", + "miden-debug-types", + "miden-formatting", + "miden-mast-package", + "miden-project", + "miden-protocol", + "midenc-frontend-wasm-metadata", + "proc-macro2", + "quote", + "semver 1.0.28", + "syn 2.0.119", + "toml", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "miden-base-sys" +version = "0.13.1" +dependencies = [ + "miden-field-repr", + "miden-stdlib-sys", +] + +[[package]] +name = "miden-core" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e26dcf78743d4abbca1bbc6799712e4ea674bad8e86dc4fd8732bf237450b1" +dependencies = [ + "derive_more", + "log", + "miden-crypto", + "miden-debug-types", + "miden-formatting", + "miden-utils-core-derive", + "miden-utils-indexing", + "miden-utils-sync", + "serde", + "thiserror", +] + +[[package]] +name = "miden-core-lib" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcb1063ac5272a02037a5fb36355b267024aa09de81bb46f54bb68ced0eebb1" +dependencies = [ + "env_logger", + "fs-err", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-crypto", + "miden-mast-package", + "miden-package-registry", + "miden-processor", + "miden-utils-sync", + "thiserror", +] + +[[package]] +name = "miden-crypto" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10eaaf3e927c3c6a720a3ed782123be25e38d34c5a7be2ca1bfbbef10a940396" +dependencies = [ + "blake3", + "cc", + "chacha20poly1305", + "curve25519-dalek", + "der", + "ed25519-dalek", + "flume", + "hkdf", + "k256", + "miden-crypto-derive", + "miden-field", + "miden-lifted-stark", + "miden-serde-utils", + "num", + "num-complex", + "once_cell", + "p3-blake3", + "p3-challenger", + "p3-dft", + "p3-goldilocks", + "p3-keccak", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.2", + "rand_chacha 0.10.0", + "serde", + "sha2", + "sha3", + "subtle", + "thiserror", + "x25519-dalek", +] + +[[package]] +name = "miden-crypto-derive" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3958eeade8b938895d3f170fd82f43e9d4141a799443b796e6496ecd9bc107d0" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-debug-types" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b9ba4ccb10ca8f719dc3c96a418aa2ade8251f108fba6034facb3e3eabae70" +dependencies = [ + "memchr", + "miden-crypto", + "miden-formatting", + "miden-miette", + "miden-utils-indexing", + "miden-utils-sync", + "paste", + "proptest", + "serde", + "serde_spanned", + "thiserror", + "zerocopy", +] + +[[package]] +name = "miden-field" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25fbce3dc2399704c7094d1d4e500ffa1fe6dabb11bf9e512fc3764498b2e1" +dependencies = [ + "miden-serde-utils", + "num-bigint 0.5.1", + "p3-challenger", + "p3-field", + "p3-goldilocks", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "subtle", + "thiserror", +] + +[[package]] +name = "miden-field-repr" +version = "0.13.1" +dependencies = [ + "miden-field", + "miden-field-repr-derive", +] + +[[package]] +name = "miden-field-repr-derive" +version = "0.13.1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-formatting" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e392e0a8c34b32671012b439de35fa8987bf14f0f8aac279b97f8b8cc6e263b" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "miden-lifted-air" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a183cb8209eb2e80bafb33fec0e841b290877d83267dfa888607ffb671ab684" +dependencies = [ + "p3-air", + "p3-challenger", + "p3-field", + "p3-matrix", + "p3-util", + "thiserror", +] + +[[package]] +name = "miden-lifted-stark" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a24dd2d8fd4978c56dd7c3d3c76c1a6fb17fef1edfe470ed4e517e38a556bdb" +dependencies = [ + "miden-lifted-air", + "miden-stark-transcript", + "miden-stateful-hasher", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-goldilocks", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.2", + "serde", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-mast-package" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee79180c3d317ab6239d7c2c488760fe3590b9f639e549d9d13f3e0f314a381" +dependencies = [ + "hashbrown", + "log", + "miden-assembly-syntax", + "miden-core", + "miden-debug-types", + "miden-utils-indexing", + "rustc-hash", + "serde", + "thiserror", + "zerocopy", +] + +[[package]] +name = "miden-miette" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef536978f24a179d94fa2a41e4f92b28e7d8aab14b8d23df28ad2a3d7098b20" +dependencies = [ + "cfg-if", + "futures", + "indenter", + "lazy_static", + "miden-miette-derive", + "owo-colors", + "regex", + "rustc_version 0.2.3", + "rustversion", + "serde_json", + "spin 0.9.9", + "strip-ansi-escapes", + "syn 2.0.119", + "textwrap", + "thiserror", + "trybuild", + "unicode-width 0.1.14", +] + +[[package]] +name = "miden-miette-derive" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a905f3ea65634dd4d1041a4f0fd0a3e77aa4118341d265af1a94339182222f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-package-registry" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bf8ca360321a414771807cd3b95c732e99e30bf7187a9a80a0e63db36bbcc2" +dependencies = [ + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "proptest", + "pubgrub", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-processor" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6853b729e245f8c310bfba8e47cdbd303b1d11763499d3b7efd90bc821239aa9" +dependencies = [ + "itertools 0.14.0", + "miden-air", + "miden-core", + "miden-debug-types", + "miden-mast-package", + "miden-utils-diagnostics", + "miden-utils-indexing", + "paste", + "rayon", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-project" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d12f4281e563c01305d989461574ccab41b749ac5d6e987ede1e9941ed18f5e" +dependencies = [ + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "proptest", + "serde", + "serde-untagged", + "thiserror", + "toml", +] + +[[package]] +name = "miden-protocol" +version = "0.16.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f185d7a1e0c6c05ae760281956de3f6057e6d7889daeade4c71c158d6a252748" +dependencies = [ + "bech32", + "fs-err", + "getrandom 0.4.3", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-core-lib", + "miden-crypto", + "miden-crypto-derive", + "miden-mast-package", + "miden-package-registry", + "miden-processor", + "miden-utils-sync", + "miden-verifier", + "rand 0.10.2", + "regex", + "semver 1.0.28", + "thiserror", + "walkdir", +] + +[[package]] +name = "miden-rowan" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13695bf99aabaa21d6572b807c66bb26251aa3d9b75e828b3c99b97a3b1ce7e" +dependencies = [ + "hashbrown", + "rustc-hash", +] + +[[package]] +name = "miden-sdk-alloc" +version = "0.13.1" + +[[package]] +name = "miden-serde-utils" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63ed62fe47d4e6255502618761a8f27d9c33f22759fbace97a6fa36a03252659" +dependencies = [ + "p3-field", + "p3-goldilocks", +] + +[[package]] +name = "miden-stark-transcript" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc79432ed9d7cf1264217ca7af4c7c1768bded83bad2429ee1ab2fb2d394d774" +dependencies = [ + "p3-challenger", + "p3-field", + "serde", + "thiserror", +] + +[[package]] +name = "miden-stateful-hasher" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08359f6cabcc418a76ac9317d6e1ccb33800a1724e08c5b4b1ced7d8aeb26e1b" +dependencies = [ + "p3-field", + "p3-symmetric", +] + +[[package]] +name = "miden-stdlib-sys" +version = "0.13.1" +dependencies = [ + "miden-field", +] + +[[package]] +name = "miden-utils-core-derive" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb2baba2f71907ab82be0d064410030196f8e06f19687d7bb33970e02dd9cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "miden-utils-diagnostics" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485627595e49b2d83d163511ddc7200194a07ee80bbeab67713ebcfeab57e2e4" +dependencies = [ + "miden-debug-types", + "miden-miette", + "tracing", +] + +[[package]] +name = "miden-utils-indexing" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6be5944699579d8babf57d0d6dd625cb59b7a10bdf85e5fc96ae568991df9f" +dependencies = [ + "miden-serde-utils", + "proptest", + "serde", + "thiserror", +] + +[[package]] +name = "miden-utils-sync" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4980ed8c1f02727ef294ec78ec2e73c367bc53ca784f4de62c1c3e5b6cbfe3" +dependencies = [ + "lock_api", + "loom", + "once_cell", + "parking_lot", +] + +[[package]] +name = "miden-verifier" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afef2b344a7c0a5a90c2b6335f4ab50e74cbaa255009ec718eb6c9598b7d9486" +dependencies = [ + "miden-air", + "miden-core", + "miden-crypto", + "serde", + "serde-wincode", + "thiserror", + "tracing", +] + +[[package]] +name = "midenc-frontend-wasm-metadata" +version = "0.13.1" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "midenc-hir-type" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b879cc9e04ad1b98ccd2fe53b1ed4ed4aa00d3231506a1e1703b17b31b3389" +dependencies = [ + "miden-formatting", + "miden-serde-utils", + "serde", + "serde_repr", + "smallvec", + "thiserror", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint 0.4.8", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint 0.4.8", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "p3-air" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb1be05c0d6f691afe0c9f468018a9a37cfa904dee78a8081ec96eb3cdd88e8" +dependencies = [ + "p3-field", + "p3-matrix", + "tracing", +] + +[[package]] +name = "p3-blake3" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f202f5fbcceb6f56f783d98efb5de27e5a171470e3364de97b0923b39c87ab5" +dependencies = [ + "blake3", + "p3-symmetric", + "p3-util", +] + +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin 0.12.2", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-goldilocks" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b3f31080df31be723b876709246f8f1e532e1c5b82efb5281d705c8304c63" +dependencies = [ + "num-bigint 0.5.1", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "spin 0.12.2", +] + +[[package]] +name = "p3-keccak" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae50c8c37eb847c660298fb275e53c025c49b2623a8cfabf67f5322258b2b4db" +dependencies = [ + "p3-symmetric", + "p3-util", + "tiny-keccak", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "spin 0.12.2", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.2", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "serde", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "primefield", + "serdect", + "wnaf", +] + +[[package]] +name = "priority-queue" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" +dependencies = [ + "equivalent", + "indexmap", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + +[[package]] +name = "pubgrub" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5df7e552bc7edd075f5783a87fbfc21d6a546e32c16985679c488c18192d83" +dependencies = [ + "indexmap", + "log", + "priority-queue", + "rustc-hash", + "thiserror", + "version-ranges", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.28", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-wincode" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa9d3a86c66cf10ce79df36f555a5a4c8d72a82515d9ea8ca420e02c925c30f" +dependencies = [ + "serde", + "thiserror", + "wincode", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest", + "keccak", + "sponge-cursor", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest", + "rand_core 0.10.1", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8abadc99fd9c7bbb7d0ca2b31d72a067d0c0dcd7aad25ab8cac71ba91417694b" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-triple" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trybuild" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" +dependencies = [ + "dissimilar", + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common", + "ctutils", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8" +dependencies = [ + "smallvec", +] + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +dependencies = [ + "bitflags 2.13.1", + "hashbrown", + "indexmap", + "semver 1.0.28", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "wincode" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d39d1a984eb7ae37afa348f058216d62a6d5f71640f4113a8114386c2a812a" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.119", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +dependencies = [ + "anyhow", + "bitflags 2.13.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver 1.0.28", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek", + "rand_core 0.10.1", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/dex-note/Cargo.toml b/examples/dex-note/Cargo.toml new file mode 100644 index 0000000000..96d1027c78 --- /dev/null +++ b/examples/dex-note/Cargo.toml @@ -0,0 +1,19 @@ +cargo-features = ["trim-paths"] + +[package] +name = "dex-note" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = { path = "../../sdk/sdk" } +miden-field-repr = { path = "../../sdk/field-repr/repr" } + +[profile.release] +trim-paths = ["diagnostics", "object"] + +[profile.dev] +trim-paths = ["diagnostics", "object"] diff --git a/examples/dex-note/cargo-generate.toml b/examples/dex-note/cargo-generate.toml new file mode 100644 index 0000000000..26029f3e76 --- /dev/null +++ b/examples/dex-note/cargo-generate.toml @@ -0,0 +1,2 @@ +[template] +ignore = ["target"] diff --git a/examples/dex-note/miden-project.toml b/examples/dex-note/miden-project.toml new file mode 100644 index 0000000000..08fd3e0cd0 --- /dev/null +++ b/examples/dex-note/miden-project.toml @@ -0,0 +1,20 @@ +[package] +name = "dex-note" +version = "0.1.0" + +[lib] +kind = "note" +namespace = "miden:dex-note/miden-dex-note@0.1.0" +path = "src/lib.rs" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +basic-wallet = { path = "../basic-wallet" } + +[package.metadata.miden.dependencies] +basic-wallet = { wit = "../basic-wallet/target/generated-wit/" } + +# MetadataSet represents each named metadata entry as a table. +[package.metadata.note-codec-crate] +path = "../dex-note-codec" diff --git a/examples/dex-note/rust-toolchain.toml b/examples/dex-note/rust-toolchain.toml new file mode 100644 index 0000000000..9148f62f16 --- /dev/null +++ b/examples/dex-note/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "nightly-2026-09-01" +components = ["rustfmt", "rust-src", "clippy"] +targets = ["wasm32-wasip2"] +profile = "minimal" diff --git a/examples/dex-note/src/lib.rs b/examples/dex-note/src/lib.rs new file mode 100644 index 0000000000..8456e726d2 --- /dev/null +++ b/examples/dex-note/src/lib.rs @@ -0,0 +1,45 @@ +//! DEX note with a custom limit-price storage type. + +#![no_std] +#![feature(alloc_error_handler)] + +use miden::{AccountId, Word, account, active_note, export_type, note}; +use miden_field_repr::{FromFeltRepr, ToFeltRepr}; + +/// A rational limit price. +#[export_type] +#[derive(FromFeltRepr, ToFeltRepr)] +pub struct LimitPrice { + /// The price numerator. + pub numerator: u64, + /// The price denominator. + pub denominator: u64, +} + +/// Storage for one DEX note. +#[note] +struct DexNote { + /// The account that can consume this note. + target: AccountId, + /// The exchange limit price. + price: LimitPrice, +} + +/// Native account interface used by the note script. +#[account(basic_wallet::BasicWallet)] +pub struct Wallet; + +#[note] +impl DexNote { + /// Sends every note asset to the target account. + #[note_script] + pub fn script(self, _arg: Word, account: &mut Wallet) { + assert_eq!(account.get_id(), self.target); + let _limit_price = self.price; + + let assets = active_note::get_initial_assets(); + for asset in assets { + account.receive_asset(asset); + } + } +} diff --git a/frontend/wasm/src/module/module_env.rs b/frontend/wasm/src/module/module_env.rs index ad9feb054a..980b4377dc 100644 --- a/frontend/wasm/src/module/module_env.rs +++ b/frontend/wasm/src/module/module_env.rs @@ -6,7 +6,7 @@ use cranelift_entity::{PrimaryMap, packed_option::ReservedValue}; use midenc_frontend_wasm_metadata::{ FrontendMetadata, PackageSections, WASM_ACCOUNT_COMPONENT_METADATA_CUSTOM_SECTION_NAME, WASM_COMPONENT_WIT_CUSTOM_SECTION_NAME, WASM_FRONTEND_METADATA_CUSTOM_SECTION_NAME, - count_top_level_wit_packages, decode_section, + WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME, count_top_level_wit_packages, decode_section, }; use midenc_hir::{FxHashMap, FxHashSet, Ident, interner::Symbol}; use midenc_session::diagnostics::{DiagnosticsHandler, IntoDiagnostic, Report, Severity}; @@ -96,6 +96,8 @@ pub struct ParsedModule<'data> { pub account_component_metadata_bytes: Option<&'data [u8]>, /// The component's public WIT source emitted by the `#[component]` macro. pub component_wit_bytes: Option<&'data [u8]>, + /// The note storage schema emitted by the `#[note]` macro. + pub note_storage_schema_bytes: Option<&'data [u8]>, /// Frontend-only component metadata entries emitted by SDK macros (empty when none present). pub component_frontend_metadata: Vec, } @@ -149,6 +151,7 @@ pub(crate) fn collect_package_sections<'a, 'data: 'a>( ) -> WasmResult { let mut account_component_metadata = None; let mut component_wit = None; + let mut note_storage_schema = None; for module in modules { merge_section_payload( &mut account_component_metadata, @@ -162,10 +165,17 @@ pub(crate) fn collect_package_sections<'a, 'data: 'a>( WASM_COMPONENT_WIT_CUSTOM_SECTION_NAME, diagnostics, )?; + merge_section_payload( + &mut note_storage_schema, + module.note_storage_schema_bytes, + WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME, + diagnostics, + )?; } Ok(PackageSections { account_component_metadata: account_component_metadata.map(<[u8]>::to_vec), component_wit: component_wit.map(<[u8]>::to_vec), + note_storage_schema: note_storage_schema.map(<[u8]>::to_vec), }) } @@ -451,6 +461,27 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> { .into_report()); } } + Payload::CustomSection(s) + if s.name() == WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME => + { + core::str::from_utf8(trim_trailing_nuls(s.data())).map_err(|err| { + diagnostics + .diagnostic(Severity::Error) + .with_message(format!( + "failed to parse note storage schema section as UTF-8: {err}" + )) + .into_report() + })?; + if self.result.note_storage_schema_bytes.replace(s.data()).is_some() { + return Err(diagnostics + .diagnostic(Severity::Error) + .with_message(format!( + "wasm error: multiple '{WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME}' \ + custom sections were found; only one is allowed per core Wasm module" + )) + .into_report()); + } + } Payload::CustomSection(s) if s.name() == WASM_FRONTEND_METADATA_CUSTOM_SECTION_NAME => { let metadata = decode_section(s.data()).map_err(|err| { diagnostics @@ -1093,3 +1124,9 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> { Ok(()) } } + +/// Removes the zero padding from a metadata section payload. +fn trim_trailing_nuls(bytes: &[u8]) -> &[u8] { + let len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); + &bytes[..len] +} diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 0cf4543221..909ce0bdbc 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -253,7 +253,7 @@ pub(crate) fn cargo_build( /* let CodegenOutput { component, - account_component_metadata_bytes, + sections, } = crate::pipeline::frontends::rust::compile_manifest(&manifest_path, None, context.clone())? else { panic!( @@ -264,7 +264,7 @@ pub(crate) fn cargo_build( Ok(CodegenOutput { component, - account_component_metadata_bytes, + sections, }) */ diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index 2771df6c6d..489bf1df70 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -18,6 +18,7 @@ use alloc::vec::Vec; use miden_mast_package::Package; use midenc_codegen_masm::{MasmComponent, intrinsics}; +use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; use midenc_session::{Session, diagnostics::Report}; /// Apply the session's link inputs to `assembler` before a project is assembled with it. @@ -72,6 +73,7 @@ pub(crate) fn post_process_package( attach_account_component_metadata(package, sections.account_component_metadata.as_deref()); attach_component_wit(package, sections.component_wit.as_deref()); + attach_note_storage_schema(package, sections.note_storage_schema.as_deref())?; extend_rodata_advice_map(package, &component.rodata); // Embed the kernel in note/transaction script packages, if not already embedded @@ -92,6 +94,21 @@ pub(crate) fn post_process_package( Ok(()) } +/// Attach the note storage schema to the assembled package. +fn attach_note_storage_schema( + package: &mut Package, + note_storage_schema: Option<&[u8]>, +) -> Result<(), Report> { + use miden_mast_package::{Section, SectionId}; + + if let Some(bytes) = note_storage_schema { + let section_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID) + .map_err(|err| Report::msg(format!("invalid note storage schema section id: {err}")))?; + package.sections.push(Section::new(section_id, bytes.to_vec())); + } + Ok(()) +} + /// Attach serialized account component metadata to the assembled package. fn attach_account_component_metadata( package: &mut Package, diff --git a/midenc-compile/src/pipeline/backend.rs b/midenc-compile/src/pipeline/backend.rs index 1db71fdc83..f76640a925 100644 --- a/midenc-compile/src/pipeline/backend.rs +++ b/midenc-compile/src/pipeline/backend.rs @@ -51,7 +51,8 @@ use crate::{CodegenOutput, CompilerResult, MidenComponent}; /// asks the advice provider for the data behind it, so a package assembled without the /// advice map fails at run time, in the VM, with nothing in the build to point at. /// - [`sections`](LoweredTarget::sections) carries the out-of-band payloads — the serialized -/// account-component metadata and the component's public WIT — that become package sections. +/// account-component metadata, the component's public WIT, and the note storage schema — that +/// become package sections. /// - [`source_provenance`](LoweredTarget::source_provenance) is what the assembler hashes to /// decide whether a cached build of this target is still current. /// diff --git a/midenc-compile/src/pipeline/testing.rs b/midenc-compile/src/pipeline/testing.rs index bad2f5c6da..4f1734577d 100644 --- a/midenc-compile/src/pipeline/testing.rs +++ b/midenc-compile/src/pipeline/testing.rs @@ -215,6 +215,7 @@ pub(crate) fn component_in_namespace( sections: midenc_frontend_wasm_metadata::PackageSections { account_component_metadata: metadata, component_wit: None, + note_storage_schema: None, }, source_provenance: ProjectSourceProvenanceInputs { root: SourceFileProvenance { diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 38828f847a..77a8eddd49 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added optional `codec-component` support to the internal `miden-note-schema` host crate. It can + load author-defined note codecs from a package without adding Wasmtime to the default feature + set or the guest SDK dependency graph. +- Added typed host note-storage bindings through the internal `miden-note-bindings` macros. Bindings + can load a built note project or an exact `.masp`, generate native Rust storage types, and convert + typed values to and from note storage. +- `#[note]` now embeds a WIT storage schema for named-field note structs in the + `note_storage_schema` section of the compiled `.masp`. Schema records preserve Rust doc comments + and can include nested types declared with `#[export_type]` before the note struct. Unit structs + emit no schema; tuple structs and `Vec` fields are not supported yet. + ## [0.14.0] ### Added diff --git a/sdk/base-macros/Cargo.toml b/sdk/base-macros/Cargo.toml index 423d53d689..79690f6747 100644 --- a/sdk/base-macros/Cargo.toml +++ b/sdk/base-macros/Cargo.toml @@ -43,6 +43,7 @@ miden-assembly = { workspace = true, features = ["std"] } miden-protocol = { workspace = true, features = ["std"] } miden-field.workspace = true miden-field-repr.workspace = true +midenc-expect-test.workspace = true wit-component.workspace = true [package.metadata.docs.rs] diff --git a/sdk/base-macros/src/component_macro/mod.rs b/sdk/base-macros/src/component_macro/mod.rs index 6039e40a7e..0ca8f7bdbc 100644 --- a/sdk/base-macros/src/component_macro/mod.rs +++ b/sdk/base-macros/src/component_macro/mod.rs @@ -1527,6 +1527,7 @@ mod tests { #[test] fn build_custom_with_entries_prefers_custom_paths() { let exported_types = vec![ExportedTypeDef { + docs: Vec::new(), rust_name: "StructA".into(), wit_name: "struct-a".into(), kind: ExportedTypeKind::Record { fields: Vec::new() }, diff --git a/sdk/base-macros/src/lib.rs b/sdk/base-macros/src/lib.rs index 29cce44958..336a08d74b 100644 --- a/sdk/base-macros/src/lib.rs +++ b/sdk/base-macros/src/lib.rs @@ -75,6 +75,7 @@ mod fpi; mod generate; mod manifest_paths; mod note; +mod note_schema; mod script; #[cfg(test)] mod test_support; diff --git a/sdk/base-macros/src/note.rs b/sdk/base-macros/src/note.rs index ab9ec8749f..644db2d7d3 100644 --- a/sdk/base-macros/src/note.rs +++ b/sdk/base-macros/src/note.rs @@ -11,6 +11,7 @@ use syn::{ use crate::{ boilerplate::runtime_boilerplate, + note_schema::expand_note_storage_schema, types::{TypeRef, map_type_to_type_ref, registered_export_type_map}, util::{ generate_frontend_link_section, generate_wit_link_section, is_type_named, @@ -135,9 +136,9 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { } let to_felt_repr_impl = note_storage_encoding(&item_struct); - let from_impl = match &item_struct.fields { + let (from_impl, schema_static) = match &item_struct.fields { syn::Fields::Unit => { - quote! { + let from_impl = quote! { impl ::core::convert::TryFrom<&[::miden::Felt]> for #struct_ident { type Error = ::miden::felt_repr::FeltReprError; @@ -148,9 +149,14 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { Ok(Self) } } - } + }; + (from_impl, quote! {}) } syn::Fields::Named(fields) => { + let schema_static = match expand_note_storage_schema(&item_struct) { + Ok(schema_static) => schema_static, + Err(err) => return err.into_compile_error(), + }; let field_inits = fields.named.iter().map(|field| { let ident = field.ident.as_ref().expect("named fields must have identifiers"); let ty = &field.ty; @@ -159,7 +165,7 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { } }); - quote! { + let from_impl = quote! { impl ::core::convert::TryFrom<&[::miden::Felt]> for #struct_ident { type Error = ::miden::felt_repr::FeltReprError; @@ -171,29 +177,12 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { Ok(value) } } - } + }; + (from_impl, schema_static) } syn::Fields::Unnamed(fields) => { - let field_inits = fields.unnamed.iter().map(|field| { - let ty = &field.ty; - quote! { - <#ty as ::miden::felt_repr::FromFeltRepr>::from_felt_repr(&mut reader)? - } - }); - - quote! { - impl ::core::convert::TryFrom<&[::miden::Felt]> for #struct_ident { - type Error = ::miden::felt_repr::FeltReprError; - - #[inline(always)] - fn try_from(felts: &[::miden::Felt]) -> Result { - let mut reader = ::miden::felt_repr::FeltReader::new(felts); - let value = Self(#(#field_inits),*); - reader.ensure_eof()?; - Ok(value) - } - } - } + return syn::Error::new(fields.span(), "note storage schema needs named fields") + .into_compile_error(); } }; @@ -203,6 +192,7 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { #to_felt_repr_impl impl ::miden::active_note::ActiveNote for #struct_ident {} + #schema_static } } @@ -355,8 +345,7 @@ fn expand_note_impl(item_impl: ItemImpl) -> TokenStream2 { Ok(metadata) => metadata, Err(err) => return err.to_compile_error(), }; - let component_package = - format!("miden:{}", metadata.package.name().into_inner().to_kebab_case()); + let component_package = metadata.component_package(); let interface_name = component_package.to_kebab_case(); let world_name = format!("{interface_name}-world"); let interface_module = interface_name.to_snake_case(); @@ -371,7 +360,7 @@ fn expand_note_impl(item_impl: ItemImpl) -> TokenStream2 { let inline_wit = build_note_script_wit( &component_package, - metadata.package.version().inner(), + metadata.component_version(), &interface_name, &world_name, &export_name, @@ -1196,6 +1185,44 @@ mod tests { use syn::parse_quote; use super::*; + use crate::types::reset_export_type_registry_for_tests; + + #[test] + fn named_note_struct_emits_storage_schema_static() { + reset_export_type_registry_for_tests(); + let item_struct: ItemStruct = parse_quote! { + struct PaymentNote { + target: AccountId, + } + }; + + let tokens = expand_note_struct(item_struct).to_string(); + + assert!(tokens.contains("__MIDEN_NOTE_STORAGE_SCHEMA_BYTES")); + assert!(tokens.contains("miden_note_schema")); + } + + #[test] + fn unit_note_struct_does_not_emit_storage_schema() { + let item_struct: ItemStruct = parse_quote!( + struct EmptyNote; + ); + + let tokens = expand_note_struct(item_struct).to_string(); + + assert!(!tokens.contains("__MIDEN_NOTE_STORAGE_SCHEMA_BYTES")); + } + + #[test] + fn tuple_note_struct_requires_named_fields() { + let item_struct: ItemStruct = parse_quote!( + struct TupleNote(Felt); + ); + + let tokens = expand_note_struct(item_struct).to_string(); + + assert!(tokens.contains("note storage schema needs named fields")); + } #[test] fn entrypoint_signature_allows_non_run_name() { diff --git a/sdk/base-macros/src/note_schema.rs b/sdk/base-macros/src/note_schema.rs new file mode 100644 index 0000000000..b0de7627df --- /dev/null +++ b/sdk/base-macros/src/note_schema.rs @@ -0,0 +1,841 @@ +//! Note storage schema generation for `#[note]` structs. + +use std::collections::{BTreeSet, HashMap, HashSet}; + +use heck::ToKebabCase; +use midenc_frontend_wasm_metadata::WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME; +use proc_macro2::{Literal, TokenStream as TokenStream2}; +use quote::quote; +use semver::Version; +use syn::{ItemStruct, Type, spanned::Spanned}; + +use crate::{ + manifest_paths::SDK_WIT_SOURCE, + types::{ + ExportedField, ExportedTypeDef, ExportedTypeKind, TypeRef, doc_comments, + map_type_to_type_ref, registered_export_types, + }, + wit_builder::{WitBody, WitBuilder}, + wit_world::ManifestPackage, +}; + +const CORE_TYPES_PACKAGE: &str = "miden:base/core-types@1.0.0"; +const CORE_TYPES_PACKAGE_NAME: &str = "miden:base"; +const CORE_TYPES_INTERFACE: &str = "core-types"; + +/// Generates the note storage schema custom-section static for a named-field note struct. +pub(crate) fn expand_note_storage_schema( + item_struct: &ItemStruct, +) -> Result { + let package = ManifestPackage::load_or_default(item_struct.ident.span())?; + let document = render_note_storage_schema( + item_struct, + &package.component_package(), + package.component_version(), + )?; + let mut bytes = document.into_bytes(); + let padded_len = bytes.len().div_ceil(16) * 16; + bytes.resize(padded_len, 0); + + let bytes_len = bytes.len(); + let encoded_bytes = Literal::byte_string(&bytes); + + Ok(quote! { + // Mach-O limits section names to 16 bytes. Wasm uses the canonical section name below. + #[cfg_attr(target_os = "macos", unsafe(link_section = "rodata,miden_note_schem"))] + #[cfg_attr( + not(target_os = "macos"), + unsafe(link_section = #WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME) + )] + #[doc(hidden)] + #[allow(clippy::octal_escapes)] + pub static __MIDEN_NOTE_STORAGE_SCHEMA_BYTES: [u8; #bytes_len] = *#encoded_bytes; + }) +} + +/// Renders the note storage schema WIT document for a named-field note struct. +fn render_note_storage_schema( + item_struct: &ItemStruct, + component_package: &str, + component_version: &Version, +) -> Result { + let registry = registered_export_types(); + render_note_storage_schema_with_registry( + item_struct, + component_package, + component_version, + ®istry, + ) +} + +/// Renders a note storage schema against one export-type registry snapshot. +fn render_note_storage_schema_with_registry( + item_struct: &ItemStruct, + component_package: &str, + component_version: &Version, + registry: &[ExportedTypeDef], +) -> Result { + let registry_by_rust_name = registry + .iter() + .cloned() + .map(|definition| (definition.rust_name.clone(), definition)) + .collect(); + let root = note_root_type(item_struct, ®istry_by_rust_name)?; + let custom_types = referenced_custom_types(&root, registry, item_struct.ident.span())?; + let core_imports = required_core_type_imports(&root, &custom_types); + let schema_package = schema_package_name(component_package); + + let mut wit = WitBuilder::new("#[note]", &schema_package, component_version); + if !core_imports.is_empty() { + wit.use_path(CORE_TYPES_PACKAGE); + wit.blank_line(); + } + wit.interface("note-storage", |interface| { + if !core_imports.is_empty() { + interface.line(&format!( + "use core-types.{{{}}};", + core_imports.iter().cloned().collect::>().join(", ") + )); + interface.blank_line(); + } + + for custom_type in &custom_types { + render_type_definition(interface, custom_type); + interface.blank_line(); + } + render_type_definition(interface, &root); + interface.blank_line(); + interface.line(&format!("type storage = {};", root.wit_name)); + }); + wit.blank_line(); + render_core_types_package(&mut wit)?; + + Ok(wit.finish()) +} + +/// Builds the exported type definition for the note storage root. +fn note_root_type( + item_struct: &ItemStruct, + registry: &HashMap, +) -> Result { + let syn::Fields::Named(named) = &item_struct.fields else { + return Err(syn::Error::new( + item_struct.fields.span(), + "note storage schema needs named fields", + )); + }; + + let mut fields = Vec::with_capacity(named.named.len()); + for field in &named.named { + let ident = field.ident.as_ref().expect("named fields must have identifiers"); + let ty = map_note_field_type(&field.ty, registry)?; + fields.push(ExportedField { + docs: doc_comments(&field.attrs), + name: ident.to_string(), + ty, + }); + } + + Ok(ExportedTypeDef { + docs: doc_comments(&item_struct.attrs), + rust_name: item_struct.ident.to_string(), + wit_name: item_struct.ident.to_string().to_kebab_case(), + kind: ExportedTypeKind::Record { fields }, + }) +} + +/// Maps a Rust field type to WIT syntax with note-specific diagnostics. +fn map_note_field_type( + ty: &Type, + registry: &HashMap, +) -> Result { + if contains_vec(ty) { + return Err(syn::Error::new( + ty.span(), + "`Vec` is not supported in note storage schemas yet", + )); + } + + map_type_to_type_ref(ty, registry).map_err(|err| { + syn::Error::new(ty.span(), format!("type is not supported in note storage schemas: {err}")) + }) +} + +/// Returns true when a type contains `Vec` at any nesting depth. +fn contains_vec(ty: &Type) -> bool { + match ty { + Type::Group(group) => contains_vec(&group.elem), + Type::Paren(paren) => contains_vec(&paren.elem), + Type::Path(path) => path.path.segments.iter().any(|segment| { + if segment.ident == "Vec" { + return true; + } + let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else { + return false; + }; + arguments.args.iter().any( + |argument| matches!(argument, syn::GenericArgument::Type(ty) if contains_vec(ty)), + ) + }), + _ => false, + } +} + +/// Returns the registry definitions reachable from the root, in registry order. +fn referenced_custom_types( + root: &ExportedTypeDef, + registry: &[ExportedTypeDef], + span: proc_macro2::Span, +) -> Result, syn::Error> { + let by_wit_name = registry + .iter() + .map(|definition| (definition.wit_name.as_str(), definition)) + .collect::>(); + let mut referenced = HashSet::new(); + visit_definition_types(root, &by_wit_name, &mut referenced, span)?; + + Ok(registry + .iter() + .filter(|definition| referenced.contains(definition.wit_name.as_str())) + .cloned() + .collect()) +} + +/// Visits every type reference in an exported definition. +fn visit_definition_types( + definition: &ExportedTypeDef, + registry: &HashMap<&str, &ExportedTypeDef>, + referenced: &mut HashSet, + span: proc_macro2::Span, +) -> Result<(), syn::Error> { + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + visit_type_ref(&field.ty, registry, referenced, span)?; + } + } + ExportedTypeKind::Variant { variants } => { + for payload in variants.iter().filter_map(|variant| variant.payload.as_ref()) { + visit_type_ref(payload, registry, referenced, span)?; + } + } + } + Ok(()) +} + +/// Visits a type reference and its custom-type dependencies. +fn visit_type_ref( + type_ref: &TypeRef, + registry: &HashMap<&str, &ExportedTypeDef>, + referenced: &mut HashSet, + span: proc_macro2::Span, +) -> Result<(), syn::Error> { + for dependency in &type_ref.dependencies { + visit_type_ref(dependency, registry, referenced, span)?; + } + if !type_ref.is_custom || referenced.contains(&type_ref.wit_name) { + return Ok(()); + } + + let definition = registry.get(type_ref.wit_name.as_str()).ok_or_else(|| { + let rust_name = type_ref.path.last().map(String::as_str).unwrap_or(&type_ref.wit_name); + syn::Error::new( + span, + format!( + "custom type `{rust_name}` in a note storage schema needs #[export_type] on its \ + definition before the #[note] struct" + ), + ) + })?; + referenced.insert(type_ref.wit_name.clone()); + visit_definition_types(definition, registry, referenced, span) +} + +/// Returns the sorted SDK core types used by the rendered schema definitions. +fn required_core_type_imports( + root: &ExportedTypeDef, + custom_types: &[ExportedTypeDef], +) -> BTreeSet { + let mut imports = BTreeSet::new(); + add_definition_core_type_imports(root, &mut imports); + for definition in custom_types { + add_definition_core_type_imports(definition, &mut imports); + } + imports +} + +/// Adds the SDK core types used by one definition to `imports`. +fn add_definition_core_type_imports(definition: &ExportedTypeDef, imports: &mut BTreeSet) { + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + field.ty.add_required_core_type_imports(imports); + } + } + ExportedTypeKind::Variant { variants } => { + for payload in variants.iter().filter_map(|variant| variant.payload.as_ref()) { + payload.add_required_core_type_imports(imports); + } + } + } +} + +/// Writes one record or variant definition to an interface body. +fn render_type_definition(interface: &mut WitBody, definition: &ExportedTypeDef) { + render_docs(interface, &definition.docs); + match &definition.kind { + ExportedTypeKind::Record { fields } => { + interface.block(&format!("record {} {{", definition.wit_name), |record| { + for field in fields { + render_docs(record, &field.docs); + record.line(&format!("{}: {},", field.name.to_kebab_case(), field.ty.wit_name)); + } + }); + } + ExportedTypeKind::Variant { variants } => { + interface.block(&format!("variant {} {{", definition.wit_name), |variant_body| { + for variant in variants { + render_docs(variant_body, &variant.docs); + match &variant.payload { + Some(payload) => variant_body + .line(&format!("{}({}),", variant.wit_name, payload.wit_name)), + None => variant_body.line(&format!("{},", variant.wit_name)), + } + } + }); + } + } +} + +/// Writes Rust doc attribute text as WIT doc comments. +fn render_docs(body: &mut WitBody, docs: &[String]) { + for doc in docs { + let doc = doc.strip_prefix(' ').unwrap_or(doc); + if doc.is_empty() { + body.line("///"); + } else { + body.line(&format!("/// {doc}")); + } + } +} + +/// Appends `-schema` to a component package name before any version suffix. +fn schema_package_name(component_package: &str) -> String { + match component_package.split_once('@') { + Some((name, version)) => format!("{name}-schema@{version}"), + None => format!("{component_package}-schema"), + } +} + +/// Writes the embedded SDK `core-types` interface as a braced dependency package. +fn render_core_types_package(wit: &mut WitBuilder) -> Result<(), syn::Error> { + let body = extract_interface_body(SDK_WIT_SOURCE, CORE_TYPES_INTERFACE).ok_or_else(|| { + syn::Error::new( + proc_macro2::Span::call_site(), + "failed to find the core-types interface in the embedded SDK WIT", + ) + })?; + let body = body.strip_prefix('\n').unwrap_or(body); + let body = body.strip_suffix('\n').unwrap_or(body); + + wit.package_block(CORE_TYPES_PACKAGE_NAME, &Version::new(1, 0, 0), |package| { + package.line("interface core-types {"); + for line in body.split('\n') { + package.line(line); + } + package.line("}"); + }); + Ok(()) +} + +/// Extracts a brace-balanced interface body without changing its text. +fn extract_interface_body<'a>(source: &'a str, interface_name: &str) -> Option<&'a str> { + let header = format!("interface {interface_name} {{"); + let body_start = source.find(&header)? + header.len(); + let mut depth = 1usize; + for (offset, ch) in source[body_start..].char_indices() { + match ch { + '{' => depth += 1, + '}' => { + depth -= 1; + if depth == 0 { + return Some(&source[body_start..body_start + offset]); + } + } + _ => {} + } + } + None +} + +#[cfg(test)] +mod tests { + use midenc_expect_test::expect; + use syn::parse_quote; + use wit_bindgen_core::wit_parser::{Resolve, Type as WitType, TypeDefKind}; + + use super::*; + use crate::types::{ + ExportedVariant, exported_type_from_struct, map_type_to_type_ref, + reset_export_type_registry_for_tests, + }; + + /// Checks that the schema package contains the expected root alias. + fn assert_schema_root(source: &str, expected_root: &str) { + let mut resolve = Resolve::default(); + let package_id = + resolve.push_str("note-schema.wit", source).expect("note schema must parse"); + let package = &resolve.packages[package_id]; + let interface_id = package.interfaces["note-storage"]; + let interface = &resolve.interfaces[interface_id]; + let storage_id = interface.types["storage"]; + let TypeDefKind::Type(WitType::Id(root_id)) = resolve.types[storage_id].kind else { + panic!("storage must be a named type alias"); + }; + assert_eq!(resolve.types[root_id].name.as_deref(), Some(expected_root)); + } + + #[test] + fn extracts_nested_interface_body_verbatim() { + let source = "package test:a;\ninterface target {\n record nested {\n value: \ + u32,\n }\n}\n"; + assert_eq!( + extract_interface_body(source, "target"), + Some("\n record nested {\n value: u32,\n }\n") + ); + assert!(extract_interface_body(SDK_WIT_SOURCE, CORE_TYPES_INTERFACE).is_some()); + } + + #[test] + fn renders_p2id_shaped_schema() { + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct P2idNote { + target_account_id: AccountId, + } + }; + let source = render_note_storage_schema(¬e, "miden:p2id", &Version::new(0, 1, 0)) + .expect("schema must render"); + + expect![[r#" + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. + + package miden:p2id-schema@0.1.0; + + use miden:base/core-types@1.0.0; + + interface note-storage { + use core-types.{account-id}; + + record p2id-note { + target-account-id: account-id, + } + + type storage = p2id-note; + } + + package miden:base@1.0.0 { + interface core-types { + /// Represents an on-chain felt. + /// + /// Field modulus M = 2^64 - 2^32 + 1. + record felt { + /// The backing type is `f32` which will be treated as a felt by the compiler. + /// We're basically hijacking the Wasm `f32` type and treat as felt. + inner: f32, + } + + + /// A group of four field elements in the Miden base field. + record word { + a: felt, + b: felt, + c: felt, + d: felt, + } + + /// A cryptographic digest representing a 256-bit hash value. + /// This is a wrapper around `word` which contains 4 field elements. + record digest { + inner: word + } + + /// Unique identifier of an account. + /// + /// # Layout + /// + /// An `AccountId` consists of two field elements, where the first is called the prefix and the + /// second is called the suffix. It is laid out as follows: + /// + /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] + /// suffix: [zero bit | hash (55 bits) | 8 zero bits] + record account-id { + prefix: felt, + suffix: felt + } + + /// Creates a new account ID from a field element. + //account-id-from-felt: func(felt: felt) -> account-id; + + /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) + record recipient { + inner: word + } + + record tag { + inner: felt + } + + /// A fungible or a non-fungible asset. + /// + /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. + /// + /// The methodology for constructing fungible and non-fungible assets is described below. + /// + /// # Fungible assets + /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `[amount, 0, 0, 0]` + /// + /// # Non-fungible assets + /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `DATA_HASH` + record asset { + key: word, + value: word, + } + + /// A validated fungible asset amount, at most 2^63 - 2^31. + record asset-amount { + inner: felt + } + + /// Account nonce + record nonce { + inner: felt + } + + /// A block height in the chain + record block-number { + inner: felt + } + + /// Account hash + record account-hash { + inner: word + } + + /// Block hash + record block-hash { + inner: word + } + + /// Storage value + record storage-value { + inner: word + } + + /// Account storage root + record storage-root { + inner: word + } + + /// Account code root + record account-code-root { + inner: word + } + + /// Commitment to the account vault + record vault-commitment { + inner: word + } + + /// An index of the created note + record note-idx { + inner: felt + } + + record note-type { + inner: felt + } + + record note-execution-hint { + inner: felt + } + + } + } + "#]].assert_eq(&source); + assert_schema_root(&source, "p2id-note"); + } + + #[test] + fn renders_nested_record_and_enum_schema() { + reset_export_type_registry_for_tests(); + let destination: syn::ItemStruct = parse_quote! { + /// Destination details. + struct Destination { + /// Destination account. + account_id: AccountId, + } + }; + let route: syn::ItemEnum = parse_quote! { + /// Route selection. + enum Route { + /// Send directly. + Direct, + /// Send through a destination. + Via(Destination), + } + }; + let destination = exported_type_from_struct(&destination).expect("record must map"); + let destination_type: Type = parse_quote!(Destination); + let route_payload = map_type_to_type_ref( + &destination_type, + &HashMap::from([(destination.rust_name.clone(), destination.clone())]), + ) + .expect("enum payload must map"); + let route = ExportedTypeDef { + docs: doc_comments(&route.attrs), + rust_name: route.ident.to_string(), + wit_name: route.ident.to_string().to_kebab_case(), + kind: ExportedTypeKind::Variant { + variants: vec![ + ExportedVariant { + docs: doc_comments(&route.variants[0].attrs), + wit_name: "direct".into(), + payload: None, + }, + ExportedVariant { + docs: doc_comments(&route.variants[1].attrs), + wit_name: "via".into(), + payload: Some(route_payload), + }, + ], + }, + }; + let note: ItemStruct = parse_quote! { + /// A routed note. + struct RoutedNote { + /// Selected route. + route: Route, + } + }; + let source = render_note_storage_schema_with_registry( + ¬e, + "miden:routed-note", + &Version::new(2, 3, 4), + &[destination, route], + ) + .expect("schema must render"); + + expect![[r#" + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. + + package miden:routed-note-schema@2.3.4; + + use miden:base/core-types@1.0.0; + + interface note-storage { + use core-types.{account-id}; + + /// Destination details. + record destination { + /// Destination account. + account-id: account-id, + } + + /// Route selection. + variant route { + /// Send directly. + direct, + /// Send through a destination. + via(destination), + } + + /// A routed note. + record routed-note { + /// Selected route. + route: route, + } + + type storage = routed-note; + } + + package miden:base@1.0.0 { + interface core-types { + /// Represents an on-chain felt. + /// + /// Field modulus M = 2^64 - 2^32 + 1. + record felt { + /// The backing type is `f32` which will be treated as a felt by the compiler. + /// We're basically hijacking the Wasm `f32` type and treat as felt. + inner: f32, + } + + + /// A group of four field elements in the Miden base field. + record word { + a: felt, + b: felt, + c: felt, + d: felt, + } + + /// A cryptographic digest representing a 256-bit hash value. + /// This is a wrapper around `word` which contains 4 field elements. + record digest { + inner: word + } + + /// Unique identifier of an account. + /// + /// # Layout + /// + /// An `AccountId` consists of two field elements, where the first is called the prefix and the + /// second is called the suffix. It is laid out as follows: + /// + /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] + /// suffix: [zero bit | hash (55 bits) | 8 zero bits] + record account-id { + prefix: felt, + suffix: felt + } + + /// Creates a new account ID from a field element. + //account-id-from-felt: func(felt: felt) -> account-id; + + /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) + record recipient { + inner: word + } + + record tag { + inner: felt + } + + /// A fungible or a non-fungible asset. + /// + /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. + /// + /// The methodology for constructing fungible and non-fungible assets is described below. + /// + /// # Fungible assets + /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `[amount, 0, 0, 0]` + /// + /// # Non-fungible assets + /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `DATA_HASH` + record asset { + key: word, + value: word, + } + + /// A validated fungible asset amount, at most 2^63 - 2^31. + record asset-amount { + inner: felt + } + + /// Account nonce + record nonce { + inner: felt + } + + /// A block height in the chain + record block-number { + inner: felt + } + + /// Account hash + record account-hash { + inner: word + } + + /// Block hash + record block-hash { + inner: word + } + + /// Storage value + record storage-value { + inner: word + } + + /// Account storage root + record storage-root { + inner: word + } + + /// Account code root + record account-code-root { + inner: word + } + + /// Commitment to the account vault + record vault-commitment { + inner: word + } + + /// An index of the created note + record note-idx { + inner: felt + } + + record note-type { + inner: felt + } + + record note-execution-hint { + inner: felt + } + + } + } + "#]].assert_eq(&source); + assert_schema_root(&source, "routed-note"); + } + + #[test] + fn rejects_tuple_note_structs() { + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote!( + struct TupleNote(Felt); + ); + let err = render_note_storage_schema(¬e, "miden:tuple-note", &Version::new(1, 0, 0)) + .expect_err("tuple notes must fail"); + + assert_eq!(err.to_string(), "note storage schema needs named fields"); + } + + #[test] + fn rejects_vec_fields() { + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct VecNote { + values: Vec, + } + }; + let err = render_note_storage_schema(¬e, "miden:vec-note", &Version::new(1, 0, 0)) + .expect_err("Vec fields must fail"); + + assert_eq!(err.to_string(), "`Vec` is not supported in note storage schemas yet"); + } + + #[test] + fn rejects_custom_types_registered_after_the_note() { + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct CustomNote { + value: MissingType, + } + }; + let err = render_note_storage_schema(¬e, "miden:custom-note", &Version::new(1, 0, 0)) + .expect_err("unregistered custom types must fail"); + + let message = err.to_string(); + assert!(message.contains("#[export_type]")); + assert!(message.contains("before the #[note] struct")); + } +} diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index 3739f6f669..a116513c47 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -7,7 +7,7 @@ static EXPORTED_TYPES: OnceLock>> = OnceLock::new(); use heck::ToKebabCase; use proc_macro2::Span; -use syn::{ItemStruct, Type, spanned::Spanned}; +use syn::{Attribute, ItemStruct, Type, spanned::Spanned}; use wit_bindgen_core::wit_parser::Type as WitType; use crate::manifest_paths::SDK_WIT_SOURCE; @@ -39,12 +39,14 @@ impl TypeRef { #[derive(Clone, Debug)] pub(crate) struct ExportedField { + pub(crate) docs: Vec, pub(crate) name: String, pub(crate) ty: TypeRef, } #[derive(Clone, Debug)] pub(crate) struct ExportedVariant { + pub(crate) docs: Vec, pub(crate) wit_name: String, pub(crate) payload: Option, } @@ -57,11 +59,34 @@ pub(crate) enum ExportedTypeKind { #[derive(Clone, Debug)] pub(crate) struct ExportedTypeDef { + pub(crate) docs: Vec, pub(crate) rust_name: String, pub(crate) wit_name: String, pub(crate) kind: ExportedTypeKind, } +/// Returns the text stored in `#[doc = "..."]` attributes. +pub(crate) fn doc_comments(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter_map(|attr| { + if !attr.path().is_ident("doc") { + return None; + } + let syn::Meta::NameValue(meta) = &attr.meta else { + return None; + }; + let syn::Expr::Lit(expr) = &meta.value else { + return None; + }; + let syn::Lit::Str(value) = &expr.lit else { + return None; + }; + Some(value.value()) + }) + .collect() +} + /// Represents the types that can be used as storage fields. /// /// During macro expansion struct field types correspond to strings, as types haven't been @@ -367,18 +392,21 @@ pub(crate) fn exported_type_from_struct( })?; let field_ty = map_type_to_type_ref(&field.ty, &known_exported)?; fields.push(ExportedField { + docs: doc_comments(&field.attrs), name: field_ident.to_string(), ty: field_ty, }); } Ok(ExportedTypeDef { + docs: doc_comments(&item_struct.attrs), rust_name: item_struct.ident.to_string(), wit_name: item_struct.ident.to_string().to_kebab_case(), kind: ExportedTypeKind::Record { fields }, }) } syn::Fields::Unit => Ok(ExportedTypeDef { + docs: doc_comments(&item_struct.attrs), rust_name: item_struct.ident.to_string(), wit_name: item_struct.ident.to_string().to_kebab_case(), kind: ExportedTypeKind::Record { fields: Vec::new() }, @@ -421,10 +449,15 @@ pub(crate) fn exported_type_from_enum( } }; - variants.push(ExportedVariant { wit_name, payload }); + variants.push(ExportedVariant { + docs: doc_comments(&variant.attrs), + wit_name, + payload, + }); } Ok(ExportedTypeDef { + docs: doc_comments(&item_enum.attrs), rust_name: item_enum.ident.to_string(), wit_name: item_enum.ident.to_string().to_kebab_case(), kind: ExportedTypeKind::Variant { variants }, diff --git a/sdk/base-macros/src/types/tests.rs b/sdk/base-macros/src/types/tests.rs index e0759c727a..ac43fc0c36 100644 --- a/sdk/base-macros/src/types/tests.rs +++ b/sdk/base-macros/src/types/tests.rs @@ -113,6 +113,38 @@ fn struct_fields_allow_wit_primitive_types() { } } +#[test] +fn exported_types_capture_doc_attributes() { + reset_export_type_registry_for_tests(); + let item_struct: syn::ItemStruct = parse_quote! { + /// Record documentation. + struct DocumentedRecord { + /// Field documentation. + value: Felt, + } + }; + let record = exported_type_from_struct(&item_struct).expect("record definition should parse"); + assert_eq!(record.docs, vec![" Record documentation."]); + let ExportedTypeKind::Record { fields } = record.kind else { + panic!("expected record kind"); + }; + assert_eq!(fields[0].docs, vec![" Field documentation."]); + + let item_enum: syn::ItemEnum = parse_quote! { + /// Variant documentation. + enum DocumentedVariant { + /// Case documentation. + Case, + } + }; + let variant = exported_type_from_enum(&item_enum).expect("variant definition should parse"); + assert_eq!(variant.docs, vec![" Variant documentation."]); + let ExportedTypeKind::Variant { variants } = variant.kind else { + panic!("expected variant kind"); + }; + assert_eq!(variants[0].docs, vec![" Case documentation."]); +} + #[test] fn maps_rust_primitive_types_to_wit_types() { reset_export_type_registry_for_tests(); diff --git a/sdk/base-macros/src/wit_builder.rs b/sdk/base-macros/src/wit_builder.rs index 0f9ee25cc3..9feb76414e 100644 --- a/sdk/base-macros/src/wit_builder.rs +++ b/sdk/base-macros/src/wit_builder.rs @@ -49,6 +49,20 @@ impl WitBuilder { result } + /// Writes a braced `package` block and returns the closure result. + pub(crate) fn package_block( + &mut self, + name: &str, + version: &Version, + build: impl FnOnce(&mut WitBody) -> T, + ) -> T { + let mut body = WitBody::new(); + let result = build(&mut body); + let package = package_with_version(name, version); + self.push_block(format!("package {package} {{"), body.finish()); + result + } + /// Finishes rendering and returns the final WIT source. pub(crate) fn finish(self) -> String { self.source.to_string() + "\n" @@ -154,6 +168,12 @@ mod tests { wit.world("foo-world", |world| { world.line("export foo;"); }); + wit.blank_line(); + wit.package_block("miden:dependency", &Version::new(2, 0, 0), |package| { + package.block("interface dependency {", |interface| { + interface.line("type value = u32;"); + }); + }); let expected = r#"// This file is auto-generated by the `#[test]` macro. // Do not edit this file manually. @@ -173,6 +193,12 @@ interface foo { world foo-world { export foo; } + +package miden:dependency@2.0.0 { + interface dependency { + type value = u32; + } +} "#; assert_eq!(wit.finish(), expected); diff --git a/sdk/note-bindings/Cargo.toml b/sdk/note-bindings/Cargo.toml new file mode 100644 index 0000000000..ac183d7394 --- /dev/null +++ b/sdk/note-bindings/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "miden-note-bindings" +description = "Typed host bindings for Miden note storage schemas" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true +publish = false + +[lib] +proc-macro = true +doctest = false + +[dependencies] +miden-mast-package = { workspace = true, features = ["std"] } +miden-note-schema.workspace = true +miden-note-schema-codegen.workspace = true +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true + +[dev-dependencies] +miden-field.workspace = true +miden-field-repr.workspace = true +miden-protocol = { workspace = true, features = ["std"] } +midenc-expect-test.workspace = true +midenc-frontend-wasm.workspace = true +midenc-integration-test-support.workspace = true +prettyplease = "0.2" +tempfile.workspace = true diff --git a/sdk/note-bindings/src/expected/custom.rs b/sdk/note-bindings/src/expected/custom.rs new file mode 100644 index 0000000000..5e9dbe1752 --- /dev/null +++ b/sdk/note-bindings/src/expected/custom.rs @@ -0,0 +1,479 @@ +#[doc(hidden)] +trait __MidenNoteEncode { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()>; +} +#[doc(hidden)] +trait __MidenNoteDecode: Sized { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result; +} +impl __MidenNoteEncode for u64 { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for u64 { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u64), + ), + ) + }) + } +} +impl __MidenNoteEncode for u32 { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for u32 { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u32), + ), + ) + }) + } +} +impl __MidenNoteEncode for u8 { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for u8 { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", stringify!(u8), + ), + ) + }) + } +} +impl __MidenNoteEncode for bool { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for bool { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(bool), + ), + ) + }) + } +} +impl __MidenNoteEncode for ::miden_field::Felt { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for ::miden_field::Felt { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_field::Felt), + ), + ) + }) + } +} +impl __MidenNoteEncode for ::miden_field::Word { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for ::miden_field::Word { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_field::Word), + ), + ) + }) + } +} +impl __MidenNoteEncode for ::miden_protocol::account::AccountId { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + writer.write(self.prefix().as_felt()); + writer.write(self.suffix()); + Ok(()) + } +} +impl __MidenNoteDecode for ::miden_protocol::account::AccountId { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let prefix = reader + .read() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to decode account-id prefix: {error}"), + ) + })?; + let suffix = reader + .read() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to decode account-id suffix: {error}"), + ) + })?; + ::miden_protocol::account::AccountId::try_from_elements(suffix, prefix) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("invalid account-id in note storage: {error}"), + ) + }) + } +} +impl __MidenNoteEncode for ::miden_protocol::asset::AssetAmount { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + writer.write(::miden_field::Felt::from(*self)); + Ok(()) + } +} +impl __MidenNoteDecode for ::miden_protocol::asset::AssetAmount { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let value = reader + .read() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to decode asset-amount: {error}"), + ) + })?; + ::miden_protocol::asset::AssetAmount::try_from(value) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("invalid asset-amount in note storage: {error}"), + ) + }) + } +} +impl __MidenNoteEncode for Option +where + T: __MidenNoteEncode, +{ + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + match self { + None => writer.write(::miden_field::Felt::ZERO), + Some(value) => { + writer.write(::miden_field::Felt::ONE); + value.__write_note_felts(writer)?; + } + } + Ok(()) + } +} +impl __MidenNoteDecode for Option +where + T: __MidenNoteDecode, +{ + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let tag = reader + .read() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to decode option tag: {error}"), + ) + })?; + match tag.as_canonical_u64() { + 0 => Ok(None), + 1 => Ok(Some(T::__read_note_felts(reader)?)), + tag => { + Err( + ::miden_note_schema::Error::new( + format!("invalid option tag {tag}; expected 0 or 1"), + ), + ) + } + } + } +} +///Rust binding for WIT type `example:dex-schema/note-storage@1.0.0.dex-note`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DexNote { + ///Value of the WIT `target` field. + pub target: ::miden_protocol::account::AccountId, + ///Value of the WIT `kind` field. + pub kind: OrderKind, +} +impl DexNote { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.dex-note"; +} +impl __MidenNoteEncode for DexNote { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + self.target.__write_note_felts(writer)?; + self.kind.__write_note_felts(writer)?; + Ok(()) + } +} +impl __MidenNoteDecode for DexNote { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + Ok(Self { + target: <::miden_protocol::account::AccountId as __MidenNoteDecode>::__read_note_felts( + reader, + )?, + kind: ::__read_note_felts(reader)?, + }) + } +} +///Selects order execution. +#[derive( + Clone, + Debug, + PartialEq, + Eq, + ::miden_field_repr::ToFeltRepr, + ::miden_field_repr::FromFeltRepr, +)] +pub enum OrderKind { + ///WIT `market` case. + Market, + ///WIT `limit` case. + Limit(LimitPrice), +} +impl OrderKind { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.order-kind"; +} +impl __MidenNoteEncode for OrderKind { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + match self { + Self::Market => { + writer.write(::miden_field::Felt::from_u32(0u32)); + } + Self::Limit(value) => { + writer.write(::miden_field::Felt::from_u32(1u32)); + value.__write_note_felts(writer)?; + } + } + Ok(()) + } +} +impl __MidenNoteDecode for OrderKind { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let tag = reader + .read_u32() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to decode {} tag: {error}", stringify!(OrderKind),), + ) + })?; + match tag { + 0u32 => Ok(Self::Market), + 1u32 => { + Ok( + Self::Limit( + ::__read_note_felts(reader)?, + ), + ) + } + tag => { + Err( + ::miden_note_schema::Error::new( + format!( + "invalid {} tag {tag}; expected a declaration ordinal below {}", + stringify!(OrderKind), 2usize, + ), + ), + ) + } + } + } +} +///A ratio used as an order limit. +#[derive( + Clone, + Debug, + PartialEq, + Eq, + ::miden_field_repr::ToFeltRepr, + ::miden_field_repr::FromFeltRepr, +)] +pub struct LimitPrice { + ///Value of the WIT `numerator` field. + pub numerator: u64, + ///Value of the WIT `denominator` field. + pub denominator: u64, +} +impl LimitPrice { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.limit-price"; +} +impl __MidenNoteEncode for LimitPrice { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + self.numerator.__write_note_felts(writer)?; + self.denominator.__write_note_felts(writer)?; + Ok(()) + } +} +impl __MidenNoteDecode for LimitPrice { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + Ok(Self { + numerator: ::__read_note_felts(reader)?, + denominator: ::__read_note_felts(reader)?, + }) + } +} +#[doc(hidden)] +const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:dex-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n /// A ratio used as an order limit.\n record limit-price {\n numerator: u64,\n denominator: u64,\n }\n\n /// Selects order execution.\n variant order-kind {\n market,\n limit(limit-price),\n }\n\n record dex-note {\n target: account-id,\n kind: order-kind,\n }\n\n type storage = dex-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; +#[doc(hidden)] +fn __miden_note_storage_schema() -> ::miden_note_schema::Result< + ::miden_note_schema::NoteStorageSchema, +> { + ::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) +} +impl DexNote { + /// Encodes this typed value as note storage in WIT declaration order. + pub fn to_note_storage( + &self, + ) -> ::miden_note_schema::Result<::miden_note_schema::NoteStorage> { + let mut felts = Vec::new(); + self.__write_note_felts(&mut ::miden_field_repr::FeltWriter::new(&mut felts))?; + ::miden_note_schema::NoteStorage::new(felts) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to create note storage: {error}"), + ) + }) + } + /// Decodes this typed value from complete note storage. + pub fn from_note_storage( + storage: &::miden_note_schema::NoteStorage, + ) -> ::miden_note_schema::Result { + let mut reader = ::miden_field_repr::FeltReader::new(storage.items()); + let value = Self::__read_note_felts(&mut reader)?; + reader + .ensure_eof() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("note storage has trailing data: {error}"), + ) + })?; + Ok(value) + } + /// Builds a typed value from normalized string paths and a codec registry. + pub fn from_str_values( + values: &::std::collections::BTreeMap, + codecs: &::miden_note_schema::CodecRegistry, + ) -> ::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder_with_registry(codecs); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + /// Validates this value with structural rules and the supplied codecs. + pub fn validate_with( + &self, + codecs: &::miden_note_schema::CodecRegistry, + ) -> ::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(()) + } + /// Displays this value with the supplied codecs and structural fallbacks. + pub fn display_with( + &self, + codecs: &::miden_note_schema::CodecRegistry, + ) -> ::miden_note_schema::Result { + let storage = self.to_note_storage()?; + let decoded = __miden_note_storage_schema()? + .decode_with_registry(&storage, codecs)?; + Ok(decoded.to_string()) + } +} diff --git a/sdk/note-bindings/src/expected/p2id.rs b/sdk/note-bindings/src/expected/p2id.rs new file mode 100644 index 0000000000..a27ecd4b83 --- /dev/null +++ b/sdk/note-bindings/src/expected/p2id.rs @@ -0,0 +1,359 @@ +#[doc(hidden)] +trait __MidenNoteEncode { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()>; +} +#[doc(hidden)] +trait __MidenNoteDecode: Sized { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result; +} +impl __MidenNoteEncode for u64 { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for u64 { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u64), + ), + ) + }) + } +} +impl __MidenNoteEncode for u32 { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for u32 { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u32), + ), + ) + }) + } +} +impl __MidenNoteEncode for u8 { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for u8 { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", stringify!(u8), + ), + ) + }) + } +} +impl __MidenNoteEncode for bool { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for bool { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(bool), + ), + ) + }) + } +} +impl __MidenNoteEncode for ::miden_field::Felt { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for ::miden_field::Felt { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_field::Felt), + ), + ) + }) + } +} +impl __MidenNoteEncode for ::miden_field::Word { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } +} +impl __MidenNoteDecode for ::miden_field::Word { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_field::Word), + ), + ) + }) + } +} +impl __MidenNoteEncode for ::miden_protocol::account::AccountId { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + writer.write(self.prefix().as_felt()); + writer.write(self.suffix()); + Ok(()) + } +} +impl __MidenNoteDecode for ::miden_protocol::account::AccountId { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let prefix = reader + .read() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to decode account-id prefix: {error}"), + ) + })?; + let suffix = reader + .read() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to decode account-id suffix: {error}"), + ) + })?; + ::miden_protocol::account::AccountId::try_from_elements(suffix, prefix) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("invalid account-id in note storage: {error}"), + ) + }) + } +} +impl __MidenNoteEncode for ::miden_protocol::asset::AssetAmount { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + writer.write(::miden_field::Felt::from(*self)); + Ok(()) + } +} +impl __MidenNoteDecode for ::miden_protocol::asset::AssetAmount { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let value = reader + .read() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to decode asset-amount: {error}"), + ) + })?; + ::miden_protocol::asset::AssetAmount::try_from(value) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("invalid asset-amount in note storage: {error}"), + ) + }) + } +} +impl __MidenNoteEncode for Option +where + T: __MidenNoteEncode, +{ + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + match self { + None => writer.write(::miden_field::Felt::ZERO), + Some(value) => { + writer.write(::miden_field::Felt::ONE); + value.__write_note_felts(writer)?; + } + } + Ok(()) + } +} +impl __MidenNoteDecode for Option +where + T: __MidenNoteDecode, +{ + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let tag = reader + .read() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to decode option tag: {error}"), + ) + })?; + match tag.as_canonical_u64() { + 0 => Ok(None), + 1 => Ok(Some(T::__read_note_felts(reader)?)), + tag => { + Err( + ::miden_note_schema::Error::new( + format!("invalid option tag {tag}; expected 0 or 1"), + ), + ) + } + } + } +} +///Rust binding for WIT type `example:p2id-schema/note-storage@1.0.0.p2id-note`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct P2idNote { + ///Value of the WIT `target-account-id` field. + pub target_account_id: ::miden_protocol::account::AccountId, +} +impl P2idNote { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:p2id-schema/note-storage@1.0.0.p2id-note"; +} +impl __MidenNoteEncode for P2idNote { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + self.target_account_id.__write_note_felts(writer)?; + Ok(()) + } +} +impl __MidenNoteDecode for P2idNote { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + Ok(Self { + target_account_id: <::miden_protocol::account::AccountId as __MidenNoteDecode>::__read_note_felts( + reader, + )?, + }) + } +} +#[doc(hidden)] +const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:p2id-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n record p2id-note {\n target-account-id: account-id,\n }\n\n type storage = p2id-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; +#[doc(hidden)] +fn __miden_note_storage_schema() -> ::miden_note_schema::Result< + ::miden_note_schema::NoteStorageSchema, +> { + ::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) +} +impl P2idNote { + /// Encodes this typed value as note storage in WIT declaration order. + pub fn to_note_storage( + &self, + ) -> ::miden_note_schema::Result<::miden_note_schema::NoteStorage> { + let mut felts = Vec::new(); + self.__write_note_felts(&mut ::miden_field_repr::FeltWriter::new(&mut felts))?; + ::miden_note_schema::NoteStorage::new(felts) + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("failed to create note storage: {error}"), + ) + }) + } + /// Decodes this typed value from complete note storage. + pub fn from_note_storage( + storage: &::miden_note_schema::NoteStorage, + ) -> ::miden_note_schema::Result { + let mut reader = ::miden_field_repr::FeltReader::new(storage.items()); + let value = Self::__read_note_felts(&mut reader)?; + reader + .ensure_eof() + .map_err(|error| { + ::miden_note_schema::Error::new( + format!("note storage has trailing data: {error}"), + ) + })?; + Ok(value) + } + /// Builds a typed value from normalized string paths. + pub fn from_str_values( + values: &::std::collections::BTreeMap, + ) -> ::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder(); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + /// Validates this value with structural rules and standard codecs. + pub fn validate_with(&self) -> ::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode(&storage)?; + Ok(()) + } + /// Displays this value with standard codecs and structural fallbacks. + pub fn display_with(&self) -> ::miden_note_schema::Result { + let storage = self.to_note_storage()?; + let decoded = __miden_note_storage_schema()?.decode(&storage)?; + Ok(decoded.to_string()) + } +} diff --git a/sdk/note-bindings/src/lib.rs b/sdk/note-bindings/src/lib.rs new file mode 100644 index 0000000000..cac72bba82 --- /dev/null +++ b/sdk/note-bindings/src/lib.rs @@ -0,0 +1,356 @@ +//! Procedural macros that generate typed host bindings for Miden note storage. + +#![deny(missing_docs)] + +extern crate proc_macro; + +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use miden_mast_package::Package; +use miden_note_schema::NoteStorageSchema; +use miden_note_schema_codegen::generate_host_types; +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{LitStr, parse_macro_input}; + +/// Generates typed bindings from the freshest package built by a Miden project. +/// +/// The project path is relative to `CARGO_MANIFEST_DIR`. Build the note project with +/// `cargo miden build` before compiling the consumer. +#[proc_macro] +pub fn from_project(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand_from_project(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates typed bindings from one exact Miden package path. +/// +/// A relative path is resolved against `CARGO_MANIFEST_DIR`. +#[proc_macro] +pub fn from_package(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand_from_package(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates typed bindings from WIT text for internal tests. +#[doc(hidden)] +#[proc_macro] +pub fn from_wit_text(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand_from_wit_text(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Expands a project-relative note binding request. +fn expand_from_project(input: &LitStr) -> syn::Result { + let project_dir = resolve_manifest_path(&input.value(), input.span())?; + if !project_dir.is_dir() { + return Err(syn::Error::new( + input.span(), + format!("note project directory '{}' does not exist", project_dir.display()), + )); + } + let package_path = freshest_project_package(&project_dir, input.span())?.ok_or_else(|| { + syn::Error::new(input.span(), missing_project_package_message(&project_dir)) + })?; + expand_package_path(&package_path, input.span()) +} + +/// Expands an exact package binding request. +fn expand_from_package(input: &LitStr) -> syn::Result { + let package_path = resolve_manifest_path(&input.value(), input.span())?; + if !package_path.is_file() { + return Err(syn::Error::new( + input.span(), + format!("Miden package '{}' does not exist", package_path.display()), + )); + } + expand_package_path(&package_path, input.span()) +} + +/// Expands bindings from a loaded package and tracks the artifact as a macro input. +fn expand_package_path(package_path: &Path, span: Span) -> syn::Result { + let package = Package::deserialize_from_file(package_path).map_err(|error| { + syn::Error::new( + span, + format!("failed to read Miden package '{}': {error}", package_path.display()), + ) + })?; + let schema = NoteStorageSchema::from_package(&package).map_err(|error| { + syn::Error::new( + span, + format!( + "failed to read note storage schema from '{}': {error}", + package_path.display() + ), + ) + })?; + let bindings = expand_schema(&schema, span)?; + let tracked_path = package_path.to_string_lossy(); + Ok(quote! { + #[doc(hidden)] + const _: &[u8] = include_bytes!(#tracked_path); + #bindings + }) +} + +/// Expands bindings from a WIT string literal. +fn expand_from_wit_text(input: &LitStr) -> syn::Result { + let schema = NoteStorageSchema::from_wit_text(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_schema(&schema, input.span()) +} + +/// Adds the typed consumer API to shared generated host types. +fn expand_schema(schema: &NoteStorageSchema, span: Span) -> syn::Result { + let generated = + generate_host_types(schema).map_err(|error| syn::Error::new(span, error.to_string()))?; + let type_tokens = generated.tokens(); + let root_ident = generated.root_ident(); + let wit_text = schema.wit_text(); + + let (from_str_values, validate_with, display_with) = if generated.has_custom_types() { + ( + quote! { + /// Builds a typed value from normalized string paths and a codec registry. + pub fn from_str_values( + values: &::std::collections::BTreeMap, + codecs: &::miden_note_schema::CodecRegistry, + ) -> ::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder_with_registry(codecs); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + }, + quote! { + /// Validates this value with structural rules and the supplied codecs. + pub fn validate_with( + &self, + codecs: &::miden_note_schema::CodecRegistry, + ) -> ::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(()) + } + }, + quote! { + /// Displays this value with the supplied codecs and structural fallbacks. + pub fn display_with( + &self, + codecs: &::miden_note_schema::CodecRegistry, + ) -> ::miden_note_schema::Result { + let storage = self.to_note_storage()?; + let decoded = + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(decoded.to_string()) + } + }, + ) + } else { + ( + quote! { + /// Builds a typed value from normalized string paths. + pub fn from_str_values( + values: &::std::collections::BTreeMap, + ) -> ::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder(); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + }, + quote! { + /// Validates this value with structural rules and standard codecs. + pub fn validate_with(&self) -> ::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode(&storage)?; + Ok(()) + } + }, + quote! { + /// Displays this value with standard codecs and structural fallbacks. + pub fn display_with(&self) -> ::miden_note_schema::Result { + let storage = self.to_note_storage()?; + let decoded = __miden_note_storage_schema()?.decode(&storage)?; + Ok(decoded.to_string()) + } + }, + ) + }; + + Ok(quote! { + #type_tokens + + #[doc(hidden)] + const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = #wit_text; + + #[doc(hidden)] + fn __miden_note_storage_schema( + ) -> ::miden_note_schema::Result<::miden_note_schema::NoteStorageSchema> { + ::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) + } + + impl #root_ident { + /// Encodes this typed value as note storage in WIT declaration order. + pub fn to_note_storage( + &self, + ) -> ::miden_note_schema::Result<::miden_note_schema::NoteStorage> { + let mut felts = Vec::new(); + self.__write_note_felts( + &mut ::miden_field_repr::FeltWriter::new(&mut felts), + )?; + ::miden_note_schema::NoteStorage::new(felts).map_err(|error| { + ::miden_note_schema::Error::new(format!( + "failed to create note storage: {error}" + )) + }) + } + + /// Decodes this typed value from complete note storage. + pub fn from_note_storage( + storage: &::miden_note_schema::NoteStorage, + ) -> ::miden_note_schema::Result { + let mut reader = ::miden_field_repr::FeltReader::new(storage.items()); + let value = Self::__read_note_felts(&mut reader)?; + reader.ensure_eof().map_err(|error| { + ::miden_note_schema::Error::new(format!( + "note storage has trailing data: {error}" + )) + })?; + Ok(value) + } + + #from_str_values + #validate_with + #display_with + } + }) +} + +/// Resolves a macro path relative to the consuming crate manifest. +fn resolve_manifest_path(value: &str, span: Span) -> syn::Result { + let path = PathBuf::from(value); + if path.is_absolute() { + return Ok(path); + } + let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| { + syn::Error::new(span, "CARGO_MANIFEST_DIR is not set during note binding generation") + })?; + Ok(PathBuf::from(manifest_dir).join(path)) +} + +/// Returns the newest package directly inside any project Miden profile directory. +fn freshest_project_package(project_dir: &Path, span: Span) -> syn::Result> { + let target_dir = project_dir.join("target/miden"); + if !target_dir.is_dir() { + return Ok(None); + } + + let profile_dirs = candidate_profile_dirs(&target_dir, span)?; + let mut candidates = Vec::new(); + for profile_dir in profile_dirs { + let entries = fs::read_dir(&profile_dir).map_err(|error| { + syn::Error::new(span, format!("failed to read '{}': {error}", profile_dir.display())) + })?; + for entry in entries { + let entry = entry.map_err(|error| { + syn::Error::new( + span, + format!("failed to read an entry in '{}': {error}", profile_dir.display()), + ) + })?; + let path = entry.path(); + if !path.is_file() || path.extension().is_none_or(|extension| extension != "masp") { + continue; + } + let modified = + entry.metadata().and_then(|metadata| metadata.modified()).map_err(|error| { + syn::Error::new( + span, + format!( + "failed to read modification time for '{}': {error}", + path.display() + ), + ) + })?; + candidates.push((modified, path)); + } + } + candidates.sort_by(|(left_time, left_path), (right_time, right_path)| { + left_time.cmp(right_time).then_with(|| left_path.cmp(right_path)) + }); + Ok(candidates.pop().map(|(_, path)| path)) +} + +/// Returns project profile directories in candidate order without duplicates. +fn candidate_profile_dirs(target_dir: &Path, span: Span) -> syn::Result> { + let mut profiles = Vec::new(); + if let Ok(profile) = env::var("PROFILE") { + push_profile(&mut profiles, profile); + } + push_profile(&mut profiles, "release".to_owned()); + push_profile(&mut profiles, "debug".to_owned()); + + let entries = fs::read_dir(target_dir).map_err(|error| { + syn::Error::new(span, format!("failed to read '{}': {error}", target_dir.display())) + })?; + let mut discovered = entries + .filter_map(Result::ok) + .filter(|entry| entry.path().is_dir()) + .filter_map(|entry| entry.file_name().into_string().ok()) + .filter(|name| name != "packages" && name != "generated-wit") + .collect::>(); + discovered.sort(); + for profile in discovered { + push_profile(&mut profiles, profile); + } + + Ok(profiles + .into_iter() + .map(|profile| target_dir.join(profile)) + .filter(|path| path.is_dir()) + .collect()) +} + +/// Adds a profile name once. +fn push_profile(profiles: &mut Vec, profile: String) { + if !profile.is_empty() && !profiles.contains(&profile) { + profiles.push(profile); + } +} + +/// Formats the missing-project-package diagnostic. +fn missing_project_package_message(project_dir: &Path) -> String { + let manifest = project_dir.join("Cargo.toml"); + let build = if manifest.is_file() { + format!("cargo miden build --manifest-path {} --release", manifest.display()) + } else { + "cargo miden build --release".to_owned() + }; + format!( + "miden-note-bindings could not find a built `.masp` package under '{}'. Build the note \ + project first with `{build}`.", + project_dir.join("target/miden/").display() + ) +} + +#[cfg(test)] +mod tests; diff --git a/sdk/note-bindings/src/tests.rs b/sdk/note-bindings/src/tests.rs new file mode 100644 index 0000000000..fb8143c8ac --- /dev/null +++ b/sdk/note-bindings/src/tests.rs @@ -0,0 +1,111 @@ +//! Tests for macro expansion and package discovery. + +use std::{fs, thread, time::Duration}; + +use midenc_expect_test::expect_file; +use syn::LitStr; + +use crate::{expand_from_wit_text, freshest_project_package, missing_project_package_message}; + +const P2ID_SCHEMA: &str = r#" +package example:p2id-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + record p2id-note { + target-account-id: account-id, + } + + type storage = p2id-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"#; + +const CUSTOM_SCHEMA: &str = r#" +package example:dex-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + /// A ratio used as an order limit. + record limit-price { + numerator: u64, + denominator: u64, + } + + /// Selects order execution. + variant order-kind { + market, + limit(limit-price), + } + + record dex-note { + target: account-id, + kind: order-kind, + } + + type storage = dex-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"#; + +/// Formats one WIT-text macro expansion as Rust source. +fn expand(wit: &str) -> String { + let literal = LitStr::new(wit, proc_macro2::Span::call_site()); + let tokens = expand_from_wit_text(&literal).unwrap(); + let file: syn::File = syn::parse2(tokens).unwrap(); + prettyplease::unparse(&file) +} + +#[test] +fn expands_p2id_schema_golden() { + expect_file!["expected/p2id.rs"].assert_eq(&expand(P2ID_SCHEMA)); +} + +#[test] +fn expands_custom_schema_golden() { + expect_file!["expected/custom.rs"].assert_eq(&expand(CUSTOM_SCHEMA)); +} + +#[test] +fn selects_the_freshest_package_across_profiles() { + let temp = tempfile::tempdir().unwrap(); + let debug = temp.path().join("target/miden/debug"); + let release = temp.path().join("target/miden/release"); + fs::create_dir_all(&debug).unwrap(); + fs::create_dir_all(&release).unwrap(); + fs::write(debug.join("note.masp"), b"old").unwrap(); + thread::sleep(Duration::from_millis(20)); + fs::write(release.join("note.masp"), b"new").unwrap(); + + let selected = freshest_project_package(temp.path(), proc_macro2::Span::call_site()) + .unwrap() + .unwrap(); + assert_eq!(selected, release.join("note.masp")); +} + +#[test] +fn missing_package_diagnostic_names_build_command() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='note'\nversion='0.1.0'").unwrap(); + let message = missing_project_package_message(temp.path()); + assert!(message.contains("cargo miden build --manifest-path")); + assert!(message.contains("--release")); +} diff --git a/sdk/note-bindings/tests/generated_custom.rs b/sdk/note-bindings/tests/generated_custom.rs new file mode 100644 index 0000000000..96769e015d --- /dev/null +++ b/sdk/note-bindings/tests/generated_custom.rs @@ -0,0 +1,96 @@ +//! Type-checks and exercises bindings generated from custom WIT types. + +use std::collections::BTreeMap; + +use miden_field::Felt; +use miden_field_repr::{FromFeltRepr, ToFeltRepr}; +use miden_note_schema::CodecRegistry; + +miden_note_bindings::from_wit_text!( + r#" +package example:custom-schema@1.0.0; + +interface note-storage { + record limit-price { + numerator: u64, + denominator: u64, + } + + variant order-kind { + market, + limit(limit-price), + } + + record custom-note { + price: limit-price, + kind: order-kind, + } + + type storage = custom-note; +} +"# +); + +/// Requires a type to carry both native felt-repr traits. +fn assert_native_felt_repr() {} + +#[test] +fn custom_types_have_native_repr_and_typed_round_trips() { + assert_native_felt_repr::(); + assert_native_felt_repr::(); + assert_native_felt_repr::(); + + let value = CustomNote { + price: LimitPrice { + numerator: 3, + denominator: 2, + }, + kind: OrderKind::Limit(LimitPrice { + numerator: 5, + denominator: 4, + }), + }; + let storage = value.to_note_storage().unwrap(); + assert_eq!( + storage.items(), + &[ + Felt::from_u32(3), + Felt::ZERO, + Felt::from_u32(2), + Felt::ZERO, + Felt::ONE, + Felt::from_u32(5), + Felt::ZERO, + Felt::from_u32(4), + Felt::ZERO, + ] + ); + assert_eq!(CustomNote::from_note_storage(&storage).unwrap(), value); + + let codecs = CodecRegistry::default(); + value.validate_with(&codecs).unwrap(); + assert_eq!( + value.display_with(&codecs).unwrap(), + "{price: {numerator: 3, denominator: 2}, kind: limit({numerator: 5, denominator: 4})}" + ); +} + +#[test] +fn custom_record_string_paths_use_the_registry_parameter() { + let mut values = BTreeMap::new(); + values.insert("price.numerator".to_owned(), "7".to_owned()); + values.insert("price.denominator".to_owned(), "6".to_owned()); + values.insert("kind".to_owned(), "market".to_owned()); + + let value = CustomNote::from_str_values(&values, &CodecRegistry::default()).unwrap(); + assert_eq!( + value, + CustomNote { + price: LimitPrice { + numerator: 7, + denominator: 6, + }, + kind: OrderKind::Market, + } + ); +} diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs new file mode 100644 index 0000000000..80e549cdfd --- /dev/null +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -0,0 +1,151 @@ +//! End-to-end test for package discovery and generated p2id consumer bindings. + +use std::{ + env, fs, + path::{Path, PathBuf}, + process::Command, + sync::Arc, +}; + +use miden_mast_package::Package; +use midenc_frontend_wasm::WasmTranslationConfig; +use midenc_integration_test_support::CompilerTest; + +/// Compiles one Cargo Miden project without debug output. +fn compile_project(project_path: &Path) -> Arc { + let mut test = CompilerTest::rust_source_cargo_miden( + project_path, + WasmTranslationConfig::default(), + ["--debug".to_owned(), "none".to_owned()], + ); + test.compile_package() +} + +/// Returns the compiler workspace root. +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() +} + +/// Returns the native rustc host target. +fn host_target() -> String { + let output = Command::new(env::var_os("RUSTC").unwrap_or_else(|| "rustc".into())) + .arg("-vV") + .output() + .expect("failed to query the rustc host target"); + assert!(output.status.success(), "rustc -vV failed"); + String::from_utf8(output.stdout) + .unwrap() + .lines() + .find_map(|line| line.strip_prefix("host: ")) + .expect("rustc -vV did not report a host target") + .to_owned() +} + +#[test] +fn generated_p2id_bindings_compile_and_run_in_a_consumer_crate() { + let workspace = workspace_root(); + let examples = workspace.join("examples"); + let wallet_dir = examples.join("basic-wallet"); + let wallet = compile_project(&wallet_dir); + wallet + .write_masp_file(wallet_dir.join("target/miden/release")) + .expect("failed to persist the basic-wallet dependency package"); + + let p2id_dir = examples.join("p2id-note"); + let p2id = compile_project(&p2id_dir); + let package_dir = p2id_dir.join("target/miden/release"); + p2id.write_masp_file(&package_dir).expect("failed to persist the p2id package"); + let package_path = package_dir.join("p2id.masp"); + + let temp = tempfile::tempdir().unwrap(); + fs::create_dir_all(temp.path().join("src")).unwrap(); + let bindings_dir = workspace.join("sdk/note-bindings"); + let schema_dir = workspace.join("sdk/note-schema"); + let field_repr_dir = workspace.join("sdk/field-repr/repr"); + fs::write( + temp.path().join("Cargo.toml"), + format!( + r#"[package] +name = "note-bindings-consumer" +version = "0.1.0" +edition = "2024" + +[dependencies] +miden-field = "0.28" +miden-field-repr = {{ path = {field_repr_dir:?} }} +miden-note-bindings = {{ path = {bindings_dir:?} }} +miden-note-schema = {{ path = {schema_dir:?} }} +miden-protocol = {{ version = "=0.16.0-alpha.4", features = ["std"] }} +"#, + field_repr_dir = field_repr_dir.to_string_lossy(), + bindings_dir = bindings_dir.to_string_lossy(), + schema_dir = schema_dir.to_string_lossy(), + ), + ) + .unwrap(); + + let source = format!( + r#"use std::collections::BTreeMap; + +use miden_protocol::{{account::AccountId, address::NetworkId}}; + +mod project_bindings {{ + miden_note_bindings::from_project!({project_dir:?}); +}} + +mod package_bindings {{ + miden_note_bindings::from_package!({package_path:?}); +}} + +fn main() {{ + let account_id = + AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let bech32 = account_id.to_bech32(NetworkId::Mainnet); + let mut values = BTreeMap::new(); + values.insert("target_account_id".to_owned(), bech32.clone()); + + let typed = project_bindings::P2idNote::from_str_values(&values).unwrap(); + assert_eq!(typed.target_account_id, account_id); + let storage = typed.to_note_storage().unwrap(); + assert_eq!( + storage.items(), + &[account_id.prefix().as_felt(), account_id.suffix()], + ); + typed.validate_with().unwrap(); + assert_eq!( + typed.display_with().unwrap(), + format!("{{{{target-account-id: {{bech32}}}}}}"), + ); + + let decoded = project_bindings::P2idNote::from_note_storage(&storage).unwrap(); + assert_eq!(decoded, typed); + + let exact = package_bindings::P2idNote::from_note_storage(&storage).unwrap(); + assert_eq!(exact.target_account_id, account_id); +}} +"#, + project_dir = p2id_dir.to_string_lossy(), + package_path = package_path.to_string_lossy(), + ); + fs::write(temp.path().join("src/main.rs"), source).unwrap(); + + let output = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .args(["run", "--quiet", "--target"]) + .arg(host_target()) + .arg("--manifest-path") + .arg(temp.path().join("Cargo.toml")) + .current_dir(temp.path()) + .env("CARGO_TARGET_DIR", workspace.join("target/note-bindings-consumer")) + .env("CARGO_NET_OFFLINE", "true") + .env_remove("CARGO_BUILD_TARGET") + .env_remove("CARGO_ENCODED_RUSTFLAGS") + .env_remove("RUSTFLAGS") + .output() + .expect("failed to spawn Cargo for the generated bindings consumer"); + assert!( + output.status.success(), + "generated bindings consumer failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} diff --git a/sdk/note-codec/Cargo.toml b/sdk/note-codec/Cargo.toml new file mode 100644 index 0000000000..92f8e0ed32 --- /dev/null +++ b/sdk/note-codec/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "miden-note-codec" +description = "Author-side codecs for typed Miden note storage" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true +publish = false + +[lib] +doctest = false + +[dependencies] +miden-field.workspace = true +miden-field-repr.workspace = true +miden-note-codec-macros.workspace = true +miden-protocol.workspace = true +wit-bindgen = { version = "0.57", default-features = false, features = ["macros"] } + +[dev-dependencies] +tempfile.workspace = true +wit-component.workspace = true +wit-parser.workspace = true diff --git a/sdk/note-codec/macros/Cargo.toml b/sdk/note-codec/macros/Cargo.toml new file mode 100644 index 0000000000..a16a70bffa --- /dev/null +++ b/sdk/note-codec/macros/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "miden-note-codec-macros" +description = "Procedural macros for author-side Miden note codecs" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true +publish = false + +[lib] +proc-macro = true +doctest = false + +[dependencies] +heck.workspace = true +miden-mast-package = { workspace = true, features = ["std"] } +miden-note-schema.workspace = true +miden-note-schema-codegen.workspace = true +proc-macro2.workspace = true +proc-macro-crate = "3.5" +quote.workspace = true +syn = { workspace = true, features = ["visit-mut"] } + +[dev-dependencies] +prettyplease = "0.2" diff --git a/sdk/note-codec/macros/src/artifact.rs b/sdk/note-codec/macros/src/artifact.rs new file mode 100644 index 0000000000..9b617aa062 --- /dev/null +++ b/sdk/note-codec/macros/src/artifact.rs @@ -0,0 +1,117 @@ +//! Miden package artifact resolution for codec macros. + +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use proc_macro2::Span; + +/// Resolves a macro path relative to the consuming crate manifest. +pub(crate) fn resolve_manifest_path(value: &str, span: Span) -> syn::Result { + let path = PathBuf::from(value); + if path.is_absolute() { + return Ok(path); + } + let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| { + syn::Error::new(span, "CARGO_MANIFEST_DIR is not set during note codec generation") + })?; + Ok(PathBuf::from(manifest_dir).join(path)) +} + +/// Returns the newest package directly inside any project Miden profile directory. +pub(crate) fn freshest_project_package( + project_dir: &Path, + span: Span, +) -> syn::Result> { + let target_dir = project_dir.join("target/miden"); + if !target_dir.is_dir() { + return Ok(None); + } + + let mut candidates = Vec::new(); + for profile_dir in candidate_profile_dirs(&target_dir, span)? { + let entries = fs::read_dir(&profile_dir).map_err(|error| { + syn::Error::new(span, format!("failed to read '{}': {error}", profile_dir.display())) + })?; + for entry in entries { + let entry = entry.map_err(|error| { + syn::Error::new( + span, + format!("failed to read an entry in '{}': {error}", profile_dir.display()), + ) + })?; + let path = entry.path(); + if !path.is_file() || !path.extension().is_some_and(|extension| extension == "masp") { + continue; + } + let modified = + entry.metadata().and_then(|metadata| metadata.modified()).map_err(|error| { + syn::Error::new( + span, + format!( + "failed to read modification time for '{}': {error}", + path.display() + ), + ) + })?; + candidates.push((modified, path)); + } + } + candidates.sort_by(|(left_time, left_path), (right_time, right_path)| { + left_time.cmp(right_time).then_with(|| left_path.cmp(right_path)) + }); + Ok(candidates.pop().map(|(_, path)| path)) +} + +/// Returns project profile directories without duplicates. +fn candidate_profile_dirs(target_dir: &Path, span: Span) -> syn::Result> { + let mut profiles = Vec::new(); + if let Ok(profile) = env::var("PROFILE") { + push_profile(&mut profiles, profile); + } + push_profile(&mut profiles, "release".to_owned()); + push_profile(&mut profiles, "debug".to_owned()); + + let entries = fs::read_dir(target_dir).map_err(|error| { + syn::Error::new(span, format!("failed to read '{}': {error}", target_dir.display())) + })?; + let mut discovered = entries + .filter_map(Result::ok) + .filter(|entry| entry.path().is_dir()) + .filter_map(|entry| entry.file_name().into_string().ok()) + .filter(|name| name != "packages" && name != "generated-wit") + .collect::>(); + discovered.sort(); + for profile in discovered { + push_profile(&mut profiles, profile); + } + + Ok(profiles + .into_iter() + .map(|profile| target_dir.join(profile)) + .filter(|path| path.is_dir()) + .collect()) +} + +/// Adds a profile name once. +fn push_profile(profiles: &mut Vec, profile: String) { + if !profile.is_empty() && !profiles.contains(&profile) { + profiles.push(profile); + } +} + +/// Formats the missing-project-package diagnostic. +pub(crate) fn missing_project_package_message(project_dir: &Path) -> String { + let manifest = project_dir.join("Cargo.toml"); + let build = if manifest.is_file() { + format!("cargo miden build --manifest-path {} --release", manifest.display()) + } else { + "cargo miden build --release".to_owned() + }; + format!( + "miden-note-codec could not find a built `.masp` package under '{}'. Build the note \ + project first with `{build}`.", + project_dir.join("target/miden/").display() + ) +} diff --git a/sdk/note-codec/macros/src/expand.rs b/sdk/note-codec/macros/src/expand.rs new file mode 100644 index 0000000000..25e6640357 --- /dev/null +++ b/sdk/note-codec/macros/src/expand.rs @@ -0,0 +1,310 @@ +//! Macro expansion for generated author types and component dispatch. + +use std::path::Path; + +use miden_mast_package::Package; +use miden_note_schema::NoteStorageSchema; +use miden_note_schema_codegen::generate_host_types; +use proc_macro_crate::{FoundCrate, crate_name}; +use proc_macro2::{Span, TokenStream}; +use quote::{ToTokens, quote}; +use syn::{ItemImpl, LitStr, Type, visit_mut::VisitMut}; + +use crate::{ + artifact::{freshest_project_package, missing_project_package_message, resolve_manifest_path}, + registry::{register_codec, register_schema, registered_codecs}, +}; + +/// The component world embedded in generated export glue. +const NOTE_CODEC_WIT: &str = include_str!("../../wit/note-codec.wit"); + +/// Expands a project-relative type generation request. +pub(crate) fn from_project(input: &LitStr) -> syn::Result { + let project_dir = resolve_manifest_path(&input.value(), input.span())?; + if !project_dir.is_dir() { + return Err(syn::Error::new( + input.span(), + format!("note project directory '{}' does not exist", project_dir.display()), + )); + } + let package_path = freshest_project_package(&project_dir, input.span())?.ok_or_else(|| { + syn::Error::new(input.span(), missing_project_package_message(&project_dir)) + })?; + expand_package_path(&package_path, input.span()) +} + +/// Expands an exact package type generation request. +pub(crate) fn from_package(input: &LitStr) -> syn::Result { + let package_path = resolve_manifest_path(&input.value(), input.span())?; + if !package_path.is_file() { + return Err(syn::Error::new( + input.span(), + format!("Miden package '{}' does not exist", package_path.display()), + )); + } + expand_package_path(&package_path, input.span()) +} + +/// Expands a WIT string literal for internal tests. +pub(crate) fn from_wit_text(input: &LitStr) -> syn::Result { + let schema = NoteStorageSchema::from_wit_text(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_schema(&schema, input.span()) +} + +/// Loads one package, tracks it as an input, and expands its schema types. +fn expand_package_path(package_path: &Path, span: Span) -> syn::Result { + let package = Package::deserialize_from_file(package_path).map_err(|error| { + syn::Error::new( + span, + format!("failed to read Miden package '{}': {error}", package_path.display()), + ) + })?; + let schema = NoteStorageSchema::from_package(&package).map_err(|error| { + syn::Error::new( + span, + format!( + "failed to read note storage schema from '{}': {error}", + package_path.display() + ), + ) + })?; + let types = expand_schema(&schema, span)?; + let tracked_path = package_path.to_string_lossy(); + Ok(quote! { + #[doc(hidden)] + const _: &[u8] = include_bytes!(#tracked_path); + #types + }) +} + +/// Generates host-profile types and records their WIT identities. +fn expand_schema(schema: &NoteStorageSchema, span: Span) -> syn::Result { + let generated = + generate_host_types(schema).map_err(|error| syn::Error::new(span, error.to_string()))?; + register_schema(schema, span)?; + let generated = rewrite_runtime_paths(generated.tokens().clone())?; + let felt_repr_alias = felt_repr_alias(); + Ok(quote! { + #felt_repr_alias + + #generated + }) +} + +/// Provides the fixed crate name used by the felt representation derives. +fn felt_repr_alias() -> TokenStream { + match crate_name("miden-field-repr") { + Ok(FoundCrate::Itself) => TokenStream::new(), + Ok(FoundCrate::Name(name)) if name == "miden_field_repr" => TokenStream::new(), + Ok(FoundCrate::Name(name)) => { + let name = syn::Ident::new(&name, Span::call_site()); + quote! { + #[doc(hidden)] + extern crate #name as miden_field_repr; + } + } + Err(_) => quote! { + #[doc(hidden)] + extern crate miden_note_codec as miden_field_repr; + }, + } +} + +/// Validates and records one marked author codec implementation. +pub(crate) fn note_codec(args: TokenStream, item: ItemImpl) -> syn::Result { + if !args.is_empty() { + return Err(syn::Error::new_spanned(args, "#[note_codec] does not accept arguments")); + } + let (_, trait_path, _) = item.trait_.as_ref().ok_or_else(|| { + syn::Error::new_spanned(&item.self_ty, "#[note_codec] needs an AuthorTypeCodec impl") + })?; + if !trait_path + .segments + .last() + .is_some_and(|segment| segment.ident == "AuthorTypeCodec") + { + return Err(syn::Error::new_spanned( + trait_path, + "#[note_codec] can only mark an AuthorTypeCodec impl", + )); + } + if !item.generics.params.is_empty() { + return Err(syn::Error::new_spanned( + &item.generics, + "#[note_codec] does not support generic implementations", + )); + } + let rust_name = rust_type_name(&item.self_ty)?; + register_codec(&rust_name, item.self_ty.to_token_stream().to_string(), item.self_ty.span())?; + Ok(quote!(#item)) +} + +/// Generates native dispatch and Wasm-only component export glue. +pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { + if !input.is_empty() { + return Err(syn::Error::new_spanned(input, "export_codecs! does not accept arguments")); + } + let codecs = registered_codecs(Span::call_site())?; + let registrations = codecs + .iter() + .map(|codec| { + let fqn = &codec.fqn; + let ty = syn::parse_str::(&codec.rust_type).map_err(|error| { + syn::Error::new( + Span::call_site(), + format!("failed to restore codec type `{}`: {error}", codec.rust_type), + ) + })?; + Ok((fqn, ty)) + }) + .collect::>>()?; + let fqns = registrations.iter().map(|(fqn, _)| fqn).collect::>(); + let parse_arms = registrations.iter().map(|(fqn, ty)| { + quote! { + #fqn => { + let value = <#ty as ::miden_note_codec::AuthorTypeCodec>::parse(value)?; + Ok(::miden_note_codec::encode_felt_repr(&value)) + } + } + }); + let display_arms = registrations.iter().map(|(fqn, ty)| { + quote! { + #fqn => { + let value = ::miden_note_codec::decode_felt_repr::<#ty>(value)?; + Ok(<#ty as ::miden_note_codec::AuthorTypeCodec>::display(&value)) + } + } + }); + let validate_arms = registrations.iter().map(|(fqn, ty)| { + quote! { + #fqn => { + let value = ::miden_note_codec::decode_felt_repr::<#ty>(value)?; + <#ty as ::miden_note_codec::AuthorTypeCodec>::validate(&value) + } + } + }); + let wit = NOTE_CODEC_WIT; + + Ok(quote! { + /// Native dispatch used by the note codec component adapter. + #[doc(hidden)] + pub mod __miden_note_codec_dispatch { + use super::*; + + /// Returns all supported canonical WIT FQNs. + pub fn supported_types() -> Vec { + vec![#(#fqns.to_owned()),*] + } + + /// Parses one value through its marked author codec. + pub fn parse(type_fqn: &str, value: &str) -> Result, String> { + match type_fqn { + #(#parse_arms,)* + _ => Err(format!( + "no note codec is registered for WIT type `{type_fqn}`" + )), + } + } + + /// Displays one value through its marked author codec. + pub fn display(type_fqn: &str, value: &[u64]) -> Result { + match type_fqn { + #(#display_arms,)* + _ => Err(format!( + "no note codec is registered for WIT type `{type_fqn}`" + )), + } + } + + /// Validates one value through its marked author codec. + pub fn validate(type_fqn: &str, value: &[u64]) -> Result<(), String> { + match type_fqn { + #(#validate_arms,)* + _ => Err(format!( + "no note codec is registered for WIT type `{type_fqn}`" + )), + } + } + } + + #[cfg(target_family = "wasm")] + mod __miden_note_codec_component { + ::miden_note_codec::__private::wit_bindgen::generate!({ + inline: #wit, + world: "note-codec", + runtime_path: "::miden_note_codec::__private::wit_bindgen::rt", + }); + + struct Component; + + impl exports::miden::note_codec::codec::Guest for Component { + fn supported_types() -> Vec { + super::__miden_note_codec_dispatch::supported_types() + } + + fn parse(type_fqn: String, value: String) -> Result, String> { + super::__miden_note_codec_dispatch::parse(&type_fqn, &value) + } + + fn display(type_fqn: String, value: Vec) -> Result { + super::__miden_note_codec_dispatch::display(&type_fqn, &value) + } + + fn validate(type_fqn: String, value: Vec) -> Result<(), String> { + super::__miden_note_codec_dispatch::validate(&type_fqn, &value) + } + } + + export!(Component); + } + }) +} + +/// Returns the final identifier of one concrete impl self type. +fn rust_type_name(ty: &Type) -> syn::Result { + let Type::Path(path) = ty else { + return Err(syn::Error::new_spanned(ty, "#[note_codec] needs a concrete generated type")); + }; + path.path + .segments + .last() + .map(|segment| segment.ident.to_string()) + .ok_or_else(|| syn::Error::new_spanned(ty, "#[note_codec] type path is empty")) +} + +/// Rewrites generated runtime paths through `miden-note-codec` re-exports. +fn rewrite_runtime_paths(tokens: TokenStream) -> syn::Result { + let mut file = syn::parse2::(tokens)?; + RuntimePathRewriter.visit_file_mut(&mut file); + Ok(quote!(#file)) +} + +/// Rewrites the four runtime crates referenced by shared host-profile codegen. +struct RuntimePathRewriter; + +impl VisitMut for RuntimePathRewriter { + fn visit_path_mut(&mut self, path: &mut syn::Path) { + let Some(first) = path.segments.first() else { + return; + }; + if path.leading_colon.is_none() + || !matches!( + first.ident.to_string().as_str(), + "miden_field" | "miden_field_repr" | "miden_note_schema" | "miden_protocol" + ) + { + syn::visit_mut::visit_path_mut(self, path); + return; + } + + let crate_name = first.ident.clone(); + let tail = path.segments.iter().skip(1).cloned().collect::>(); + let mut rewritten: syn::Path = + syn::parse_quote!(::miden_note_codec::__private::#crate_name); + rewritten.segments.extend(tail); + *path = rewritten; + } +} + +use syn::spanned::Spanned; diff --git a/sdk/note-codec/macros/src/lib.rs b/sdk/note-codec/macros/src/lib.rs new file mode 100644 index 0000000000..ec4983c03e --- /dev/null +++ b/sdk/note-codec/macros/src/lib.rs @@ -0,0 +1,65 @@ +//! Procedural macros for author-side note codec components. + +#![deny(missing_docs)] + +extern crate proc_macro; + +mod artifact; +mod expand; +mod registry; + +use proc_macro::TokenStream; +use syn::{ItemImpl, LitStr, parse_macro_input}; + +/// Generates host-profile note types from the freshest package built by a Miden project. +/// +/// The project path is relative to `CARGO_MANIFEST_DIR`. Build the note project with +/// `cargo miden build` before compiling the codec crate. +#[proc_macro] +pub fn from_project(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand::from_project(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates host-profile note types from one exact Miden package path. +/// +/// A relative path is resolved against `CARGO_MANIFEST_DIR`. +#[proc_macro] +pub fn from_package(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand::from_package(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates host-profile note types from WIT text for internal tests. +#[doc(hidden)] +#[proc_macro] +pub fn from_wit_text(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand::from_wit_text(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Marks an [`miden_note_codec::AuthorTypeCodec`] implementation for component export. +#[proc_macro_attribute] +pub fn note_codec(args: TokenStream, input: TokenStream) -> TokenStream { + let item = parse_macro_input!(input as ItemImpl); + expand::note_codec(args.into(), item) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Exports all marked note codecs through the `miden:note-codec` component world. +#[proc_macro] +pub fn export_codecs(input: TokenStream) -> TokenStream { + expand::export_codecs(input.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[cfg(test)] +mod tests; diff --git a/sdk/note-codec/macros/src/registry.rs b/sdk/note-codec/macros/src/registry.rs new file mode 100644 index 0000000000..29eb2359a7 --- /dev/null +++ b/sdk/note-codec/macros/src/registry.rs @@ -0,0 +1,154 @@ +//! Process-global schema and codec registration for one macro expansion process. + +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::{Mutex, OnceLock}, +}; + +use heck::ToUpperCamelCase; +use miden_note_schema::{ + ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, FELT_FQN, NoteStorageSchema, SchemaCase, SchemaType, + SchemaTypeKind, WORD_FQN, +}; +use proc_macro2::Span; + +/// One marked author codec. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct CodecRegistration { + pub(crate) fqn: String, + pub(crate) rust_type: String, +} + +/// Schema types and marked codecs registered by earlier macro expansions. +#[derive(Default)] +struct Registry { + schemas: BTreeSet, + types: BTreeMap>, + codecs: BTreeMap, +} + +static REGISTRY: OnceLock> = OnceLock::new(); + +/// Returns the shared registry. +fn registry() -> &'static Mutex { + REGISTRY.get_or_init(|| Mutex::new(Registry::default())) +} + +/// Records every generated named type, including the storage root. +pub(crate) fn register_schema(schema: &NoteStorageSchema, span: Span) -> syn::Result<()> { + let mut bindings = BTreeMap::new(); + collect_type_bindings(schema.root(), &mut BTreeSet::new(), &mut bindings)?; + + let mut registry = registry() + .lock() + .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; + if !registry.schemas.insert(schema.wit_text().to_owned()) { + return Ok(()); + } + for (rust_name, fqn) in bindings { + registry.types.entry(rust_name).or_default().insert(fqn); + } + Ok(()) +} + +/// Resolves and records a marked codec implementation by generated Rust type name. +pub(crate) fn register_codec(rust_name: &str, rust_type: String, span: Span) -> syn::Result<()> { + let mut registry = registry() + .lock() + .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; + let fqns = registry.types.get(rust_name).ok_or_else(|| { + syn::Error::new( + span, + format!( + "type `{rust_name}` is not part of a registered note schema; invoke \ + miden_note_codec::from_project! or from_package! before #[note_codec]" + ), + ) + })?; + if fqns.len() != 1 { + return Err(syn::Error::new( + span, + format!( + "generated type `{rust_name}` is ambiguous across note schemas: {}", + fqns.iter().cloned().collect::>().join(", ") + ), + )); + } + let fqn = fqns.first().expect("one FQN was checked above").clone(); + let registration = CodecRegistration { + fqn: fqn.clone(), + rust_type, + }; + if let Some(existing) = registry.codecs.get(&fqn) + && existing != ®istration + { + return Err(syn::Error::new( + span, + format!("WIT type `{fqn}` already has a different #[note_codec] implementation"), + )); + } + registry.codecs.insert(fqn, registration); + Ok(()) +} + +/// Returns marked codecs in canonical FQN order. +pub(crate) fn registered_codecs(span: Span) -> syn::Result> { + let registry = registry() + .lock() + .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; + Ok(registry.codecs.values().cloned().collect()) +} + +/// Collects reachable generated record and variant bindings. +fn collect_type_bindings( + ty: &SchemaType, + seen: &mut BTreeSet, + bindings: &mut BTreeMap, +) -> syn::Result<()> { + if is_protocol_leaf(ty) { + return Ok(()); + } + if matches!(ty.kind(), SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_)) { + let fqn = ty.fqn().ok_or_else(|| { + syn::Error::new(Span::call_site(), "a generated note codec type has no WIT FQN") + })?; + let name = ty.name().ok_or_else(|| { + syn::Error::new( + Span::call_site(), + format!("generated WIT type `{fqn}` has no local name"), + ) + })?; + if !seen.insert(fqn.to_owned()) { + return Ok(()); + } + bindings.insert(name.to_upper_camel_case(), fqn.to_owned()); + } + + match ty.kind() { + SchemaTypeKind::Record(fields) => { + for field in fields { + collect_type_bindings(field.ty(), seen, bindings)?; + } + } + SchemaTypeKind::Option(payload) => collect_type_bindings(payload, seen, bindings)?, + SchemaTypeKind::Variant(cases) => { + for payload in cases.iter().filter_map(SchemaCase::payload) { + collect_type_bindings(payload, seen, bindings)?; + } + } + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {} + } + Ok(()) +} + +/// Returns true for protocol leaves mapped to existing host types. +fn is_protocol_leaf(ty: &SchemaType) -> bool { + matches!(ty.fqn(), Some(FELT_FQN | WORD_FQN | ACCOUNT_ID_FQN | ASSET_AMOUNT_FQN)) +} + +#[cfg(test)] +pub(crate) fn reset_for_tests() { + if let Some(registry) = REGISTRY.get() { + *registry.lock().expect("mutex poisoned") = Registry::default(); + } +} diff --git a/sdk/note-codec/macros/src/tests.rs b/sdk/note-codec/macros/src/tests.rs new file mode 100644 index 0000000000..40415066c8 --- /dev/null +++ b/sdk/note-codec/macros/src/tests.rs @@ -0,0 +1,59 @@ +//! Tests for codec macro registration and expansion. + +use proc_macro2::Span; +use quote::quote; +use syn::{ItemImpl, LitStr}; + +use crate::{expand, registry::reset_for_tests}; + +const SCHEMA: &str = r#" +package example:codec-schema@1.0.0; + +interface note-storage { + record ratio { + numerator: u64, + denominator: u64, + } + + record codec-note { + ratio: ratio, + } + + type storage = codec-note; +} +"#; + +#[test] +fn schema_and_codec_registration_generate_native_and_wasm_dispatch() { + reset_for_tests(); + let schema = LitStr::new(SCHEMA, Span::call_site()); + let generated = expand::from_wit_text(&schema).unwrap(); + let item: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for Ratio { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + expand::note_codec(quote!(), item).unwrap(); + let root_item: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for CodecNote { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + expand::note_codec(quote!(), root_item).unwrap(); + let exported = expand::export_codecs(quote!()).unwrap(); + let source = prettyplease::unparse(&syn::parse2(quote!(#generated #exported)).unwrap()); + + assert!(source.contains("pub struct CodecNote")); + assert!(source.contains("pub struct Ratio")); + assert!(source.contains("::miden_note_codec::__private::miden_field_repr")); + assert!(source.contains("example:codec-schema/note-storage@1.0.0.codec-note")); + assert!(source.contains("example:codec-schema/note-storage@1.0.0.ratio")); + assert!(source.contains("cfg(target_family = \"wasm\")")); + assert!(source.contains("exports::miden::note_codec::codec::Guest")); +} diff --git a/sdk/note-codec/src/lib.rs b/sdk/note-codec/src/lib.rs new file mode 100644 index 0000000000..a3197ccd8a --- /dev/null +++ b/sdk/note-codec/src/lib.rs @@ -0,0 +1,165 @@ +//! Author-side support for typed note storage codecs. +//! +//! Codec components exchange field elements as canonical `u64` values. This crate checks every +//! integer before it enters [`Felt`], so a component cannot introduce a reduced or ambiguous field +//! representation. + +#![deny(missing_docs)] + +pub use miden_field_repr::*; +#[doc(hidden)] +pub use miden_note_codec_macros::from_wit_text; +pub use miden_note_codec_macros::{export_codecs, from_package, from_project, note_codec}; +pub use miden_protocol::{account, asset}; + +/// The WIT document implemented by generated note codec components. +pub const NOTE_CODEC_WIT: &str = include_str!("../wit/note-codec.wit"); + +/// Parses, displays, and validates one author-defined note storage type. +pub trait AuthorTypeCodec: Sized { + /// Parses text into a typed value. + fn parse(value: &str) -> Result; + + /// Displays a typed value. + fn display(&self) -> String; + + /// Validates semantic constraints that are not part of the felt layout. + fn validate(&self) -> Result<(), String>; +} + +/// Converts a field element to its canonical component-boundary integer. +pub fn felt_to_u64(value: Felt) -> u64 { + value.as_canonical_u64() +} + +/// Converts a component-boundary integer to a canonical field element. +pub fn felt_from_u64(value: u64) -> Result { + Felt::new(value).map_err(|error| format!("invalid component felt `{value}`: {error}")) +} + +/// Converts field elements to canonical component-boundary integers. +pub fn felts_to_u64(values: &[Felt]) -> Vec { + values.iter().copied().map(felt_to_u64).collect() +} + +/// Converts component-boundary integers to canonical field elements. +pub fn felts_from_u64(values: &[u64]) -> Result, String> { + values + .iter() + .copied() + .enumerate() + .map(|(index, value)| { + felt_from_u64(value) + .map_err(|error| format!("component felt at index {index} is invalid: {error}")) + }) + .collect() +} + +/// Encodes a native felt-repr value for the component boundary. +pub fn encode_felt_repr(value: &impl ToFeltRepr) -> Vec { + felts_to_u64(&value.to_felt_repr()) +} + +/// Decodes one complete native felt-repr value from the component boundary. +pub fn decode_felt_repr(values: &[u64]) -> Result { + let felts = felts_from_u64(values)?; + let mut reader = FeltReader::new(&felts); + let value = T::from_felt_repr(&mut reader) + .map_err(|error| format!("invalid felt representation: {error}"))?; + reader + .ensure_eof() + .map_err(|error| format!("invalid felt representation: {error}"))?; + Ok(value) +} + +/// Support used by generated host-profile types. +#[doc(hidden)] +pub mod __private { + pub use miden_field; + pub use miden_field_repr; + pub use miden_protocol; + pub use wit_bindgen; + + /// Lightweight error support required by shared generated type helpers. + pub mod miden_note_schema { + use core::fmt; + + /// A generated type conversion error. + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Error(String); + + impl Error { + /// Creates a generated type conversion error. + pub fn new(message: impl Into) -> Self { + Self(message.into()) + } + } + + impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } + } + + impl std::error::Error for Error {} + + /// A generated type conversion result. + pub type Result = core::result::Result; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A sample author type used to test the native boundary glue. + #[derive(Clone, Debug, Eq, PartialEq, FromFeltRepr, ToFeltRepr)] + struct Ratio { + numerator: u64, + denominator: u64, + } + + impl AuthorTypeCodec for Ratio { + fn parse(value: &str) -> Result { + let (numerator, denominator) = value + .split_once('/') + .ok_or_else(|| "a ratio must use `numerator/denominator`".to_owned())?; + Ok(Self { + numerator: numerator.parse::().map_err(|error| error.to_string())?, + denominator: denominator.parse::().map_err(|error| error.to_string())?, + }) + } + + fn display(&self) -> String { + format!("{}/{}", self.numerator, self.denominator) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } + } + + #[test] + fn author_trait_values_round_trip_through_boundary_glue() { + let ratio = Ratio::parse("3/2").unwrap(); + ratio.validate().unwrap(); + let encoded = encode_felt_repr(&ratio); + assert_eq!(encoded, [3, 0, 2, 0]); + let decoded = decode_felt_repr::(&encoded).unwrap(); + assert_eq!(decoded, ratio); + assert_eq!(decoded.display(), "3/2"); + } + + #[test] + fn boundary_rejects_noncanonical_field_integers() { + let maximum = Felt::ORDER - 1; + assert_eq!(felt_to_u64(felt_from_u64(maximum).unwrap()), maximum); + assert!(felt_from_u64(Felt::ORDER).unwrap_err().contains("exceeds the felt modulus")); + assert!(felt_from_u64(u64::MAX).unwrap_err().contains("exceeds the felt modulus")); + assert!(felts_from_u64(&[0, Felt::ORDER]).unwrap_err().contains("index 1")); + } +} diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs new file mode 100644 index 0000000000..ab28d7f92b --- /dev/null +++ b/sdk/note-codec/tests/component_export.rs @@ -0,0 +1,224 @@ +//! Component export test for the author codec world. + +use std::{ + env, fs, + path::{Path, PathBuf}, + process::{Command, Output}, +}; + +use tempfile::TempDir; +use wit_component::{ComponentEncoder, DecodedWasm}; +use wit_parser::WorldItem; + +const WASM_TARGET: &str = "wasm32-unknown-unknown"; + +#[test] +fn minimal_codec_crate_encodes_to_zero_import_component() { + if !ensure_wasm_target() { + eprintln!( + "skipping component export test: rustup could not install {WASM_TARGET} in this \ + environment" + ); + return; + } + + let fixture = TempDir::new().expect("failed to create temporary codec crate"); + write_fixture(fixture.path()); + let target_dir = workspace_root().join("target/note-codec-component-test"); + let output = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .args([ + "build", + "--manifest-path", + fixture.path().join("Cargo.toml").to_str().unwrap(), + "--release", + "--target", + WASM_TARGET, + "--offline", + ]) + .env("CARGO_TARGET_DIR", &target_dir) + .env_remove("CARGO_BUILD_TARGET") + .env_remove("CARGO_ENCODED_RUSTFLAGS") + .env_remove("RUSTFLAGS") + .output() + .expect("failed to run cargo for the component fixture"); + assert_command_succeeded("building the component fixture", &output); + + let module = fs::read( + target_dir.join(format!("{WASM_TARGET}/release/note_codec_component_fixture.wasm")), + ) + .expect("component fixture did not produce a Wasm module"); + let component = ComponentEncoder::default() + .module(&module) + .expect("the fixture is not a component-ready core module") + .validate(true) + .encode() + .expect("failed to encode the codec component"); + let DecodedWasm::Component(resolve, world_id) = + wit_component::decode(&component).expect("failed to decode the encoded component") + else { + panic!("ComponentEncoder did not produce a component"); + }; + let world = &resolve.worlds[world_id]; + assert!(world.imports.is_empty(), "codec component imports: {:#?}", world.imports); + assert_eq!(world.exports.len(), 1); + + let interface_id = world + .exports + .values() + .find_map(|item| match item { + WorldItem::Interface { id, .. } => Some(*id), + _ => None, + }) + .expect("the component does not export the note codec interface"); + let interface = &resolve.interfaces[interface_id]; + assert_eq!(interface.name.as_deref(), Some("codec")); + let package_id = interface.package.expect("codec interface has no package"); + let package = &resolve.packages[package_id].name; + assert_eq!(package.namespace, "miden"); + assert_eq!(package.name, "note-codec"); + assert_eq!(package.version.as_ref().map(ToString::to_string).as_deref(), Some("1.0.0")); + assert_eq!( + interface.functions.keys().map(String::as_str).collect::>(), + ["supported-types", "parse", "display", "validate"] + ); +} + +/// Checks the target and tries to install it before a graceful skip. +fn ensure_wasm_target() -> bool { + let listed = match Command::new("rustup").args(["target", "list"]).output() { + Ok(output) if output.status.success() => output, + Ok(output) => { + eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); + return false; + } + Err(error) => { + eprintln!("could not run `rustup target list`: {error}"); + return false; + } + }; + if target_is_installed(&listed.stdout) { + return true; + } + + let install = Command::new("rustup").args(["target", "add", WASM_TARGET]).output(); + match install { + Ok(output) if output.status.success() => {} + Ok(output) => { + eprintln!( + "`rustup target add {WASM_TARGET}` failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + return false; + } + Err(error) => { + eprintln!("could not run `rustup target add {WASM_TARGET}`: {error}"); + return false; + } + } + + match Command::new("rustup").args(["target", "list"]).output() { + Ok(output) => output.status.success() && target_is_installed(&output.stdout), + Err(error) => { + eprintln!("could not re-run `rustup target list`: {error}"); + false + } + } +} + +/// Returns true when rustup reports the primary component target as installed. +fn target_is_installed(output: &[u8]) -> bool { + String::from_utf8_lossy(output) + .lines() + .any(|line| line.starts_with(WASM_TARGET) && line.contains("(installed)")) +} + +/// Writes the minimal author codec crate used by the componentization test. +fn write_fixture(root: &Path) { + fs::create_dir(root.join("src")).expect("failed to create fixture source directory"); + let codec_path = Path::new(env!("CARGO_MANIFEST_DIR")); + let manifest = format!( + r#"[package] +name = "note-codec-component-fixture" +version = "0.0.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden-note-codec = {{ path = {:?} }} + +[workspace] +"#, + codec_path + ); + fs::write(root.join("Cargo.toml"), manifest).expect("failed to write fixture manifest"); + fs::write(root.join("src/lib.rs"), FIXTURE_SOURCE).expect("failed to write fixture source"); +} + +/// Returns the compiler workspace root. +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("note-codec must be inside sdk/") + .to_owned() +} + +/// Reports all command output when one fixture command fails. +fn assert_command_succeeded(action: &str, output: &Output) { + assert!( + output.status.success(), + "failed while {action}\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} + +const FIXTURE_SOURCE: &str = r##" +use miden_note_codec::AuthorTypeCodec; + +miden_note_codec::from_wit_text!(r#" +package example:codec-schema@1.0.0; + +interface note-storage { + record ratio { + numerator: u64, + denominator: u64, + } + + record codec-note { + ratio: ratio, + } + + type storage = codec-note; +} +"#); + +#[miden_note_codec::note_codec] +impl AuthorTypeCodec for Ratio { + fn parse(value: &str) -> Result { + let (numerator, denominator) = value + .split_once('/') + .ok_or_else(|| "a ratio must use `numerator/denominator`".to_owned())?; + Ok(Self { + numerator: numerator.parse::().map_err(|error| error.to_string())?, + denominator: denominator.parse::().map_err(|error| error.to_string())?, + }) + } + + fn display(&self) -> String { + format!("{}/{}", self.numerator, self.denominator) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } +} + +miden_note_codec::export_codecs!(); +"##; diff --git a/sdk/note-codec/tests/dispatch.rs b/sdk/note-codec/tests/dispatch.rs new file mode 100644 index 0000000000..11fc943609 --- /dev/null +++ b/sdk/note-codec/tests/dispatch.rs @@ -0,0 +1,72 @@ +//! Native tests for generated author codec dispatch. + +use miden_note_codec::AuthorTypeCodec; + +miden_note_codec::from_wit_text!( + r#" +package example:codec-schema@1.0.0; + +interface note-storage { + record ratio { + numerator: u64, + denominator: u64, + } + + record codec-note { + ratio: ratio, + } + + type storage = codec-note; +} +"# +); + +#[miden_note_codec::note_codec] +impl AuthorTypeCodec for Ratio { + fn parse(value: &str) -> Result { + let (numerator, denominator) = value + .split_once('/') + .ok_or_else(|| "a ratio must use `numerator/denominator`".to_owned())?; + Ok(Self { + numerator: numerator.parse::().map_err(|error| error.to_string())?, + denominator: denominator.parse::().map_err(|error| error.to_string())?, + }) + } + + fn display(&self) -> String { + format!("{}/{}", self.numerator, self.denominator) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } +} + +miden_note_codec::export_codecs!(); + +#[test] +fn marked_codec_dispatches_by_canonical_wit_fqn() { + const RATIO_FQN: &str = "example:codec-schema/note-storage@1.0.0.ratio"; + + assert_eq!(__miden_note_codec_dispatch::supported_types(), [RATIO_FQN]); + let encoded = __miden_note_codec_dispatch::parse(RATIO_FQN, "3/2").unwrap(); + assert_eq!(encoded, [3, 0, 2, 0]); + assert_eq!(__miden_note_codec_dispatch::display(RATIO_FQN, &encoded).unwrap(), "3/2"); + __miden_note_codec_dispatch::validate(RATIO_FQN, &encoded).unwrap(); + + let invalid = __miden_note_codec_dispatch::parse(RATIO_FQN, "3/0").unwrap(); + assert!( + __miden_note_codec_dispatch::validate(RATIO_FQN, &invalid) + .unwrap_err() + .contains("denominator") + ); + assert!( + __miden_note_codec_dispatch::parse("example:unknown/type", "3/2") + .unwrap_err() + .contains("no note codec is registered") + ); +} diff --git a/sdk/note-codec/wit/note-codec.wit b/sdk/note-codec/wit/note-codec.wit new file mode 100644 index 0000000000..3cde3d4442 --- /dev/null +++ b/sdk/note-codec/wit/note-codec.wit @@ -0,0 +1,27 @@ +package miden:note-codec@1.0.0; + +/// Parses, displays, and validates custom note storage types. +interface codec { + /// A canonical Miden field element at the component boundary. + /// + /// This is a `u64`, not the core-types `felt` record, because that record models the + /// compiler's guest felt value and does not expose its canonical integer representation. + type felt = u64; + + /// Returns the fully-qualified WIT names handled by this component. + supported-types: func() -> list; + + /// Parses text into a type's structural felt representation. + parse: func(type-fqn: string, value: string) -> result, string>; + + /// Displays a type's structural felt representation. + display: func(type-fqn: string, value: list) -> result; + + /// Validates a type's structural felt representation. + validate: func(type-fqn: string, value: list) -> result<_, string>; +} + +world note-codec { + export codec; +} + diff --git a/sdk/note-schema/Cargo.toml b/sdk/note-schema/Cargo.toml new file mode 100644 index 0000000000..ab19fd5c1c --- /dev/null +++ b/sdk/note-schema/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "miden-note-schema" +description = "Host-side reader and codec registry for Miden note storage schemas" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true +publish = false + +[features] +default = [] +codec-component = ["dep:wasmtime"] + +[lib] +doctest = false + +[dependencies] +miden-field.workspace = true +miden-field-repr.workspace = true +miden-mast-package = { workspace = true, features = ["std"] } +miden-protocol = { workspace = true, features = ["std"] } +midenc-frontend-wasm-metadata.workspace = true +wit-parser.workspace = true +wasmtime = { workspace = true, optional = true } + +[dev-dependencies] +miden-core.workspace = true +midenc-frontend-wasm.workspace = true +midenc-integration-test-support.workspace = true +tempfile.workspace = true +wit-component.workspace = true diff --git a/sdk/note-schema/codegen/Cargo.toml b/sdk/note-schema/codegen/Cargo.toml new file mode 100644 index 0000000000..c97f187b16 --- /dev/null +++ b/sdk/note-schema/codegen/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "miden-note-schema-codegen" +description = "Internal Rust code generator for Miden note storage schemas" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true +publish = false + +[lib] +doctest = false + +[dependencies] +heck.workspace = true +miden-note-schema.workspace = true +proc-macro2.workspace = true +quote.workspace = true + +[dev-dependencies] +midenc-expect-test.workspace = true +prettyplease = "0.2" +syn.workspace = true diff --git a/sdk/note-schema/codegen/src/lib.rs b/sdk/note-schema/codegen/src/lib.rs new file mode 100644 index 0000000000..1a28c583cf --- /dev/null +++ b/sdk/note-schema/codegen/src/lib.rs @@ -0,0 +1,641 @@ +//! Shared Rust code generation for resolved note storage schemas. + +#![deny(missing_docs)] + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt, +}; + +use heck::{ToSnakeCase, ToUpperCamelCase}; +use miden_note_schema::{ + ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, FELT_FQN, NoteStorageSchema, PrimitiveType, SchemaCase, + SchemaField, SchemaType, SchemaTypeKind, WORD_FQN, +}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote}; + +/// An error reported while generating Rust bindings. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CodegenError { + message: String, +} + +impl CodegenError { + /// Creates a code-generation error. + fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +impl fmt::Display for CodegenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for CodegenError {} + +/// Rust host types and metadata produced from one note storage schema. +pub struct GeneratedTypes { + tokens: TokenStream, + root_ident: Ident, + has_custom_types: bool, +} + +impl GeneratedTypes { + /// Returns the generated Rust items. + pub const fn tokens(&self) -> &TokenStream { + &self.tokens + } + + /// Returns the Rust identifier for the root storage type. + pub const fn root_ident(&self) -> &Ident { + &self.root_ident + } + + /// Returns true when the schema has named types other than its storage root and standard + /// leaves. + pub const fn has_custom_types(&self) -> bool { + self.has_custom_types + } +} + +/// Generates Rust host-profile types and structural felt conversion helpers. +pub fn generate_host_types(schema: &NoteStorageSchema) -> Result { + let root = schema.root(); + let root_fqn = root + .fqn() + .ok_or_else(|| CodegenError::new("the note storage root does not have a WIT FQN"))?; + let root_name = root + .name() + .ok_or_else(|| CodegenError::new("the note storage root does not have a WIT name"))?; + + let mut definitions = Vec::new(); + let mut seen = BTreeSet::new(); + collect_named_types(root, &mut seen, &mut definitions)?; + + let mut rust_names = BTreeMap::new(); + let mut used_names = BTreeMap::::new(); + for definition in &definitions { + let fqn = definition.fqn().expect("collected named types always have a FQN"); + let name = definition.name().expect("collected named types always have a name"); + let ident = type_ident(name); + if let Some(existing) = used_names.insert(ident.to_string(), fqn.to_owned()) + && existing != fqn + { + return Err(CodegenError::new(format!( + "WIT types `{existing}` and `{fqn}` both map to Rust type `{ident}`" + ))); + } + rust_names.insert(fqn.to_owned(), ident); + } + + let root_ident = rust_names.get(root_fqn).cloned().unwrap_or_else(|| type_ident(root_name)); + let helper_traits = generate_helper_traits(); + let items = definitions + .iter() + .map(|definition| generate_type(definition, &rust_names)) + .collect::, _>>()?; + let has_custom_types = definitions + .iter() + .any(|definition| definition.fqn().is_some_and(|fqn| fqn != root_fqn)); + + Ok(GeneratedTypes { + tokens: quote! { + #helper_traits + #(#items)* + }, + root_ident, + has_custom_types, + }) +} + +/// Collects reachable named records and variants, excluding mapped protocol leaves. +fn collect_named_types<'a>( + ty: &'a SchemaType, + seen: &mut BTreeSet, + definitions: &mut Vec<&'a SchemaType>, +) -> Result<(), CodegenError> { + if mapped_leaf(ty).is_some() { + return Ok(()); + } + + let is_definition = matches!(ty.kind(), SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_)); + if is_definition { + let fqn = ty.fqn().ok_or_else(|| { + CodegenError::new("anonymous WIT records and variants cannot be emitted as Rust types") + })?; + ty.name().ok_or_else(|| { + CodegenError::new(format!("WIT type `{fqn}` does not have a local name")) + })?; + if !seen.insert(fqn.to_owned()) { + return Ok(()); + } + definitions.push(ty); + } + + match ty.kind() { + SchemaTypeKind::Record(fields) => { + for field in fields { + collect_named_types(field.ty(), seen, definitions)?; + } + } + SchemaTypeKind::Option(payload) => collect_named_types(payload, seen, definitions)?, + SchemaTypeKind::Variant(cases) => { + for payload in cases.iter().filter_map(SchemaCase::payload) { + collect_named_types(payload, seen, definitions)?; + } + } + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {} + } + Ok(()) +} + +/// Generates the private traits that keep protocol-leaf order separate from foreign trait impls. +fn generate_helper_traits() -> TokenStream { + let primitive_impls = [ + quote!(u64), + quote!(u32), + quote!(u8), + quote!(bool), + quote!(::miden_field::Felt), + quote!(::miden_field::Word), + ] + .into_iter() + .map(|ty| { + quote! { + impl __MidenNoteEncode for #ty { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } + } + + impl __MidenNoteDecode for #ty { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + ::miden_field_repr::FromFeltRepr::from_felt_repr(reader).map_err(|error| { + ::miden_note_schema::Error::new(format!( + "failed to decode {} from note storage: {error}", + stringify!(#ty), + )) + }) + } + } + } + }); + + quote! { + #[doc(hidden)] + trait __MidenNoteEncode { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()>; + } + + #[doc(hidden)] + trait __MidenNoteDecode: Sized { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result; + } + + #(#primitive_impls)* + + impl __MidenNoteEncode for ::miden_protocol::account::AccountId { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + // The WIT record declares prefix before suffix. + writer.write(self.prefix().as_felt()); + writer.write(self.suffix()); + Ok(()) + } + } + + impl __MidenNoteDecode for ::miden_protocol::account::AccountId { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let prefix = reader.read().map_err(|error| { + ::miden_note_schema::Error::new(format!( + "failed to decode account-id prefix: {error}" + )) + })?; + let suffix = reader.read().map_err(|error| { + ::miden_note_schema::Error::new(format!( + "failed to decode account-id suffix: {error}" + )) + })?; + ::miden_protocol::account::AccountId::try_from_elements(suffix, prefix).map_err( + |error| { + ::miden_note_schema::Error::new(format!( + "invalid account-id in note storage: {error}" + )) + }, + ) + } + } + + impl __MidenNoteEncode for ::miden_protocol::asset::AssetAmount { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + writer.write(::miden_field::Felt::from(*self)); + Ok(()) + } + } + + impl __MidenNoteDecode for ::miden_protocol::asset::AssetAmount { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let value = reader.read().map_err(|error| { + ::miden_note_schema::Error::new(format!( + "failed to decode asset-amount: {error}" + )) + })?; + ::miden_protocol::asset::AssetAmount::try_from(value).map_err(|error| { + ::miden_note_schema::Error::new(format!( + "invalid asset-amount in note storage: {error}" + )) + }) + } + } + + impl __MidenNoteEncode for Option + where + T: __MidenNoteEncode, + { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + match self { + None => writer.write(::miden_field::Felt::ZERO), + Some(value) => { + writer.write(::miden_field::Felt::ONE); + value.__write_note_felts(writer)?; + } + } + Ok(()) + } + } + + impl __MidenNoteDecode for Option + where + T: __MidenNoteDecode, + { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let tag = reader.read().map_err(|error| { + ::miden_note_schema::Error::new(format!( + "failed to decode option tag: {error}" + )) + })?; + match tag.as_canonical_u64() { + 0 => Ok(None), + 1 => Ok(Some(T::__read_note_felts(reader)?)), + tag => Err(::miden_note_schema::Error::new(format!( + "invalid option tag {tag}; expected 0 or 1" + ))), + } + } + } + } +} + +/// Generates one Rust record or variant and its structural conversion helpers. +fn generate_type( + definition: &SchemaType, + rust_names: &BTreeMap, +) -> Result { + let fqn = definition.fqn().expect("generated type definitions always have a FQN"); + let ident = rust_names.get(fqn).expect("every generated type has a Rust identifier"); + let docs = type_docs(definition, fqn); + let derives = if supports_native_felt_repr(definition) { + quote! { + #[derive( + Clone, + Debug, + PartialEq, + Eq, + ::miden_field_repr::ToFeltRepr, + ::miden_field_repr::FromFeltRepr, + )] + } + } else { + quote! { #[derive(Clone, Debug, PartialEq, Eq)] } + }; + + let (item, encode_impl, decode_impl) = match definition.kind() { + SchemaTypeKind::Record(fields) => { + generate_record(ident, fields, rust_names, &docs, &derives)? + } + SchemaTypeKind::Variant(cases) => { + generate_variant(ident, cases, rust_names, &docs, &derives)? + } + _ => { + return Err(CodegenError::new(format!( + "named WIT type `{fqn}` is not a record or variant" + ))); + } + }; + + Ok(quote! { + #item + + impl #ident { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = #fqn; + } + + #encode_impl + #decode_impl + }) +} + +/// Generates a Rust struct in WIT declaration order. +fn generate_record( + ident: &Ident, + fields: &[SchemaField], + rust_names: &BTreeMap, + docs: &TokenStream, + derives: &TokenStream, +) -> Result<(TokenStream, TokenStream, TokenStream), CodegenError> { + let rust_fields = fields + .iter() + .map(|field| { + let ident = value_ident(field.name()); + let ty = rust_type(field.ty(), rust_names)?; + let docs = field_docs(field); + Ok((ident, ty, docs)) + }) + .collect::, CodegenError>>()?; + let field_idents = rust_fields.iter().map(|(ident, ..)| ident).collect::>(); + let field_types = rust_fields.iter().map(|(_, ty, _)| ty).collect::>(); + let field_docs = rust_fields.iter().map(|(_, _, docs)| docs).collect::>(); + + let item = quote! { + #docs + #derives + pub struct #ident { + #( + #field_docs + pub #field_idents: #field_types, + )* + } + }; + let encode_impl = quote! { + impl __MidenNoteEncode for #ident { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + #(self.#field_idents.__write_note_felts(writer)?;)* + Ok(()) + } + } + }; + let decode_impl = quote! { + impl __MidenNoteDecode for #ident { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + Ok(Self { + #(#field_idents: <#field_types as __MidenNoteDecode>::__read_note_felts(reader)?,)* + }) + } + } + }; + Ok((item, encode_impl, decode_impl)) +} + +/// Generates a Rust enum with declaration-ordinal tags. +fn generate_variant( + ident: &Ident, + cases: &[SchemaCase], + rust_names: &BTreeMap, + docs: &TokenStream, + derives: &TokenStream, +) -> Result<(TokenStream, TokenStream, TokenStream), CodegenError> { + let rust_cases = cases + .iter() + .map(|case| { + let ident = type_ident(case.name()); + let payload = case.payload().map(|ty| rust_type(ty, rust_names)).transpose()?; + let docs = case_docs(case); + Ok((ident, payload, docs)) + }) + .collect::, CodegenError>>()?; + + let declarations = rust_cases.iter().map(|(case, payload, docs)| match payload { + Some(payload) => quote! { + #docs + #case(#payload), + }, + None => quote! { + #docs + #case, + }, + }); + let encode_arms = rust_cases.iter().enumerate().map(|(ordinal, (case, payload, _))| { + let ordinal = ordinal as u32; + match payload { + Some(_) => quote! { + Self::#case(value) => { + writer.write(::miden_field::Felt::from_u32(#ordinal)); + value.__write_note_felts(writer)?; + } + }, + None => quote! { + Self::#case => { + writer.write(::miden_field::Felt::from_u32(#ordinal)); + } + }, + } + }); + let decode_arms = rust_cases.iter().enumerate().map(|(ordinal, (case, payload, _))| { + let ordinal = ordinal as u32; + match payload { + Some(payload) => quote! { + #ordinal => Ok(Self::#case( + <#payload as __MidenNoteDecode>::__read_note_felts(reader)?, + )), + }, + None => quote! { #ordinal => Ok(Self::#case), }, + } + }); + let case_count = cases.len(); + + let item = quote! { + #docs + #derives + pub enum #ident { + #(#declarations)* + } + }; + let encode_impl = quote! { + impl __MidenNoteEncode for #ident { + fn __write_note_felts( + &self, + writer: &mut ::miden_field_repr::FeltWriter<'_>, + ) -> ::miden_note_schema::Result<()> { + match self { + #(#encode_arms)* + } + Ok(()) + } + } + }; + let decode_impl = quote! { + impl __MidenNoteDecode for #ident { + fn __read_note_felts( + reader: &mut ::miden_field_repr::FeltReader<'_>, + ) -> ::miden_note_schema::Result { + let tag = reader.read_u32().map_err(|error| { + ::miden_note_schema::Error::new(format!( + "failed to decode {} tag: {error}", + stringify!(#ident), + )) + })?; + match tag { + #(#decode_arms)* + tag => Err(::miden_note_schema::Error::new(format!( + "invalid {} tag {tag}; expected a declaration ordinal below {}", + stringify!(#ident), + #case_count, + ))), + } + } + } + }; + Ok((item, encode_impl, decode_impl)) +} + +/// Maps one schema type to its host-profile Rust type. +fn rust_type( + ty: &SchemaType, + rust_names: &BTreeMap, +) -> Result { + if let Some(mapped) = mapped_leaf(ty) { + return Ok(mapped); + } + if let Some(fqn) = ty.fqn() + && let Some(ident) = rust_names.get(fqn) + { + return Ok(quote!(#ident)); + } + + match ty.kind() { + SchemaTypeKind::Primitive(PrimitiveType::U64) => Ok(quote!(u64)), + SchemaTypeKind::Primitive(PrimitiveType::U32) => Ok(quote!(u32)), + SchemaTypeKind::Primitive(PrimitiveType::U8) => Ok(quote!(u8)), + SchemaTypeKind::Primitive(PrimitiveType::Bool) => Ok(quote!(bool)), + SchemaTypeKind::Option(payload) => { + let payload = rust_type(payload, rust_names)?; + Ok(quote!(Option<#payload>)) + } + SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_) => Err(CodegenError::new(format!( + "named WIT type `{}` was not collected for Rust generation", + ty.fqn().or(ty.name()).unwrap_or("") + ))), + SchemaTypeKind::Felt => Ok(quote!(::miden_field::Felt)), + } +} + +/// Returns the native Rust type for one standard WIT leaf. +fn mapped_leaf(ty: &SchemaType) -> Option { + match ty.fqn()? { + FELT_FQN => Some(quote!(::miden_field::Felt)), + WORD_FQN => Some(quote!(::miden_field::Word)), + ACCOUNT_ID_FQN => Some(quote!(::miden_protocol::account::AccountId)), + ASSET_AMOUNT_FQN => Some(quote!(::miden_protocol::asset::AssetAmount)), + _ => None, + } +} + +/// Returns true when all fields implement the native felt-repr traits without protocol adapters. +fn supports_native_felt_repr(ty: &SchemaType) -> bool { + if matches!(ty.fqn(), Some(ACCOUNT_ID_FQN | ASSET_AMOUNT_FQN)) { + return false; + } + match ty.kind() { + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => true, + SchemaTypeKind::Record(fields) => { + fields.iter().all(|field| supports_native_felt_repr(field.ty())) + } + SchemaTypeKind::Option(payload) => supports_native_felt_repr(payload), + SchemaTypeKind::Variant(cases) => { + cases.iter().all(|case| case.payload().is_none_or(supports_native_felt_repr)) + } + } +} + +/// Produces documentation for a generated WIT type. +fn type_docs(ty: &SchemaType, fqn: &str) -> TokenStream { + let docs = ty + .docs() + .map(str::to_owned) + .unwrap_or_else(|| format!("Rust binding for WIT type `{fqn}`.")); + quote!(#[doc = #docs]) +} + +/// Produces documentation for a generated record field. +fn field_docs(field: &SchemaField) -> TokenStream { + let docs = field + .docs() + .map(str::to_owned) + .unwrap_or_else(|| format!("Value of the WIT `{}` field.", field.name())); + quote!(#[doc = #docs]) +} + +/// Produces documentation for a generated variant case. +fn case_docs(case: &SchemaCase) -> TokenStream { + let docs = case + .docs() + .map(str::to_owned) + .unwrap_or_else(|| format!("WIT `{}` case.", case.name())); + quote!(#[doc = #docs]) +} + +/// Converts a WIT type or case name to a Rust type identifier. +fn type_ident(name: &str) -> Ident { + rust_ident(&name.to_upper_camel_case()) +} + +/// Converts a WIT field name to a Rust value identifier. +fn value_ident(name: &str) -> Ident { + rust_ident(&name.replace('-', "_").to_snake_case()) +} + +/// Creates an identifier and avoids Rust reserved words. +fn rust_ident(name: &str) -> Ident { + const RESERVED: &[&str] = &[ + "Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue", + "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if", + "impl", "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", + "pub", "ref", "return", "self", "static", "struct", "super", "trait", "true", "try", + "type", "typeof", "union", "unsafe", "unsized", "use", "virtual", "where", "while", + "yield", + ]; + if RESERVED.contains(&name) { + format_ident!("{name}_") + } else { + Ident::new(name, Span::call_site()) + } +} + +#[cfg(test)] +mod tests; diff --git a/sdk/note-schema/codegen/src/tests.rs b/sdk/note-schema/codegen/src/tests.rs new file mode 100644 index 0000000000..f65c13bb97 --- /dev/null +++ b/sdk/note-schema/codegen/src/tests.rs @@ -0,0 +1,95 @@ +//! Tests for host-profile type generation. + +use miden_note_schema::NoteStorageSchema; + +use crate::generate_host_types; + +const P2ID_SCHEMA: &str = r#" +package example:p2id-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + record p2id-note { + target-account-id: account-id, + } + + type storage = p2id-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"#; + +const CUSTOM_SCHEMA: &str = r#" +package example:dex-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + record limit-price { + numerator: u64, + denominator: u64, + } + + variant order-kind { + market, + limit(limit-price), + } + + record dex-note { + target: account-id, + kind: order-kind, + } + + type storage = dex-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"#; + +/// Formats generated tokens as Rust source. +fn generate(wit: &str) -> (String, bool, String) { + let schema = NoteStorageSchema::from_wit_text(wit).unwrap(); + let generated = generate_host_types(&schema).unwrap(); + let file: syn::File = syn::parse2(generated.tokens().clone()).unwrap(); + ( + prettyplease::unparse(&file), + generated.has_custom_types(), + generated.root_ident().to_string(), + ) +} + +#[test] +fn maps_protocol_leaf_and_root_type() { + let (source, has_custom_types, root) = generate(P2ID_SCHEMA); + assert_eq!(root, "P2idNote"); + assert!(!has_custom_types); + assert!(source.contains("pub target_account_id: ::miden_protocol::account::AccountId")); + assert!(source.contains("pub const WIT_FQN")); + assert!(source.contains("self.prefix().as_felt()")); +} + +#[test] +fn derives_native_repr_for_custom_records_and_variants() { + let (source, has_custom_types, root) = generate(CUSTOM_SCHEMA); + assert_eq!(root, "DexNote"); + assert!(has_custom_types); + assert!(source.contains("pub struct LimitPrice")); + assert!(source.contains("pub enum OrderKind")); + assert!(source.matches("::miden_field_repr::ToFeltRepr").count() >= 2); + assert!(source.contains("example:dex-schema/note-storage@1.0.0.limit-price")); +} diff --git a/sdk/note-schema/src/builder.rs b/sdk/note-schema/src/builder.rs new file mode 100644 index 0000000000..79d70d09cd --- /dev/null +++ b/sdk/note-schema/src/builder.rs @@ -0,0 +1,257 @@ +//! String-path note storage builder. + +use std::collections::BTreeMap; + +use miden_field_repr::FeltWriter; + +use crate::{ + CodecRegistry, Error, Felt, NoteStorage, NoteStorageSchema, PrimitiveType, Result, SchemaType, + SchemaTypeKind, + codec::{parse_felt, parse_unsigned, write_repr}, + schema::normalize_name, + value::validate_encoding, +}; + +/// Builds note storage from normalized dotted string paths. +pub struct NoteStorageBuilder<'a> { + schema: &'a NoteStorageSchema, + registry: &'a CodecRegistry, + values: BTreeMap, +} + +impl<'a> NoteStorageBuilder<'a> { + /// Creates an empty builder for a schema and codec registry. + pub(crate) fn new(schema: &'a NoteStorageSchema, registry: &'a CodecRegistry) -> Self { + Self { + schema, + registry, + values: BTreeMap::new(), + } + } + + /// Sets a leaf value by kebab-case or snake_case dotted path. + /// + /// Options accept `none` or `some()`. Variants accept a case name or + /// `case-name()` when the selected case has a payload. + pub fn set(mut self, path: &str, value: impl AsRef) -> Result { + let segments = normalize_path(path)?; + resolve_path(self.schema.root(), &segments)?; + let normalized = segments.join("."); + + if let Some(conflict) = self.values.keys().find(|existing| { + *existing == &normalized + || existing.starts_with(&format!("{normalized}.")) + || normalized.starts_with(&format!("{existing}.")) + }) { + return Err(Error::new(format!( + "path `{normalized}` conflicts with the existing value at `{conflict}`" + ))); + } + self.values.insert(normalized, value.as_ref().to_owned()); + Ok(self) + } + + /// Checks completeness and returns note storage in declaration order. + pub fn build(self) -> Result { + let mut felts = Vec::new(); + encode_type( + self.schema.root(), + "", + &self.values, + self.registry, + &mut FeltWriter::new(&mut felts), + )?; + validate_encoding(self.schema.root(), &felts) + .map_err(|err| err.context("built note storage does not match its schema"))?; + NoteStorage::new(felts) + .map_err(|err| Error::new(format!("failed to create note storage: {err}"))) + } +} + +/// Normalizes and validates a dotted field path. +fn normalize_path(path: &str) -> Result> { + let segments = path.split('.').map(normalize_name).collect::>(); + if segments.is_empty() || segments.iter().any(String::is_empty) { + return Err(Error::new(format!("invalid empty note storage path `{path}`"))); + } + Ok(segments) +} + +/// Resolves a dotted path through structural record fields. +fn resolve_path<'a>(mut ty: &'a SchemaType, segments: &[String]) -> Result<&'a SchemaType> { + let mut resolved = Vec::with_capacity(segments.len()); + for segment in segments { + let SchemaTypeKind::Record(fields) = ty.kind() else { + return Err(Error::new(format!( + "path `{}` continues through non-record type `{}`", + resolved.join("."), + ty.fqn().or(ty.name()).unwrap_or("") + ))); + }; + let field = fields.iter().find(|field| field.name() == segment).ok_or_else(|| { + let prefix = if resolved.is_empty() { + "".to_owned() + } else { + resolved.join(".") + }; + Error::new(format!("record `{prefix}` has no field named `{segment}`")) + })?; + resolved.push(segment.clone()); + ty = field.ty(); + } + Ok(ty) +} + +/// Encodes a schema type from complete path assignments. +fn encode_type( + ty: &SchemaType, + path: &str, + values: &BTreeMap, + registry: &CodecRegistry, + writer: &mut FeltWriter<'_>, +) -> Result<()> { + if let Some(value) = values.get(path) { + let felts = encode_text_value(ty, value, registry) + .map_err(|err| err.context(format!("value at `{path}`")))?; + for felt in felts { + writer.write(felt); + } + return Ok(()); + } + + if let SchemaTypeKind::Record(fields) = ty.kind() { + for field in fields { + let field_path = if path.is_empty() { + field.name().to_owned() + } else { + format!("{path}.{}", field.name()) + }; + encode_type(field.ty(), &field_path, values, registry, writer)?; + } + return Ok(()); + } + + Err(Error::new(format!( + "missing note storage value for `{}`", + if path.is_empty() { "" } else { path } + ))) +} + +/// Encodes one direct string value. +fn encode_text_value(ty: &SchemaType, value: &str, registry: &CodecRegistry) -> Result> { + if let Some(fqn) = ty.fqn() + && let Some(codec) = registry.codec(fqn) + { + let felts = codec.parse(value)?; + codec + .validate(&felts) + .map_err(|err| err.context(format!("codec `{fqn}` validation failed")))?; + validate_encoding(ty, &felts) + .map_err(|err| err.context(format!("codec `{fqn}` changed the structural layout")))?; + return Ok(felts); + } + + match ty.kind() { + SchemaTypeKind::Felt => Ok(write_repr(&parse_felt(value)?)), + SchemaTypeKind::Primitive(primitive) => encode_primitive(*primitive, value), + SchemaTypeKind::Option(payload) => encode_option(payload, value, registry), + SchemaTypeKind::Variant(cases) => { + let (case_name, payload_text) = parse_constructor(value)?; + let case_name = normalize_name(case_name); + let (ordinal, case) = + cases.iter().enumerate().find(|(_, case)| case.name() == case_name).ok_or_else( + || { + Error::new(format!( + "variant `{}` has no case named `{case_name}`", + ty.fqn().or(ty.name()).unwrap_or("") + )) + }, + )?; + let ordinal = u32::try_from(ordinal) + .map_err(|_| Error::new("variant has more than u32::MAX cases"))?; + let mut felts = write_repr(&ordinal); + match (case.payload(), payload_text) { + (None, None) => {} + (None, Some(_)) => { + return Err(Error::new(format!( + "variant case `{case_name}` does not accept a payload" + ))); + } + (Some(_), None) => { + return Err(Error::new(format!("variant case `{case_name}` needs a payload"))); + } + (Some(payload), Some(payload_text)) => { + felts.extend(encode_text_value(payload, payload_text, registry)?); + } + } + Ok(felts) + } + SchemaTypeKind::Record(_) => Err(Error::new(format!( + "type `{}` has no codec; set its leaf fields with dotted paths", + ty.fqn().or(ty.name()).unwrap_or("") + ))), + } +} + +/// Encodes an option tag and optional direct payload. +fn encode_option(payload: &SchemaType, value: &str, registry: &CodecRegistry) -> Result> { + let value = value.trim(); + if value == "none" { + return Ok(write_repr(&0u32)); + } + let (constructor, payload_text) = parse_constructor(value)?; + if normalize_name(constructor) != "some" { + return Err(Error::new("an option value must be `none` or `some()`")); + } + let payload_text = + payload_text.ok_or_else(|| Error::new("an option `some` value needs a payload"))?; + let mut felts = write_repr(&1u32); + felts.extend(encode_text_value(payload, payload_text, registry)?); + Ok(felts) +} + +/// Encodes one supported primitive through `miden-field-repr`. +fn encode_primitive(primitive: PrimitiveType, value: &str) -> Result> { + match primitive { + PrimitiveType::U64 => Ok(write_repr(&parse_unsigned(value, "u64")?)), + PrimitiveType::U32 => { + let value = u32::try_from(parse_unsigned(value, "u32")?) + .map_err(|_| Error::new(format!("u32 value `{value}` is out of range")))?; + Ok(write_repr(&value)) + } + PrimitiveType::U8 => { + let value = u8::try_from(parse_unsigned(value, "u8")?) + .map_err(|_| Error::new(format!("u8 value `{value}` is out of range")))?; + Ok(write_repr(&value)) + } + PrimitiveType::Bool => { + let value = match value.trim() { + "true" | "1" => true, + "false" | "0" => false, + value => { + return Err(Error::new(format!( + "invalid bool `{value}`; expected true, false, 1, or 0" + ))); + } + }; + Ok(write_repr(&value)) + } + } +} + +/// Splits `name` or `name(payload)` text without interpreting the payload. +fn parse_constructor(value: &str) -> Result<(&str, Option<&str>)> { + let value = value.trim(); + let Some(open) = value.find('(') else { + return Ok((value, None)); + }; + if !value.ends_with(')') { + return Err(Error::new(format!("value `{value}` has an unclosed payload"))); + } + let name = value[..open].trim(); + let payload = value[open + 1..value.len() - 1].trim(); + if name.is_empty() || payload.is_empty() { + return Err(Error::new(format!("value `{value}` has an empty constructor or payload"))); + } + Ok((name, Some(payload))) +} diff --git a/sdk/note-schema/src/codec.rs b/sdk/note-schema/src/codec.rs new file mode 100644 index 0000000000..d53e0bc24f --- /dev/null +++ b/sdk/note-schema/src/codec.rs @@ -0,0 +1,295 @@ +//! String codecs for named WIT leaf types. + +use std::{collections::BTreeMap, sync::Arc}; + +use miden_field::{Felt, Word}; +use miden_field_repr::{FeltReader, FeltWriter, FromFeltRepr, ToFeltRepr}; +use miden_protocol::{account::AccountId, address::NetworkId, asset::AssetAmount}; + +use crate::{Error, Result}; + +/// The canonical WIT FQN for `felt`. +pub const FELT_FQN: &str = "miden:base/core-types@1.0.0.felt"; +/// The canonical WIT FQN for `word`. +pub const WORD_FQN: &str = "miden:base/core-types@1.0.0.word"; +/// The canonical WIT FQN for `account-id`. +pub const ACCOUNT_ID_FQN: &str = "miden:base/core-types@1.0.0.account-id"; +/// The canonical WIT FQN for `asset-amount`. +pub const ASSET_AMOUNT_FQN: &str = "miden:base/core-types@1.0.0.asset-amount"; + +/// Parses, displays, and validates one fully-qualified WIT leaf type. +pub trait ConsumerTypeCodec: Send + Sync { + /// Parses a string into its structural felt representation. + fn parse(&self, value: &str) -> Result>; + + /// Displays a structurally valid felt representation. + fn display(&self, felts: &[Felt]) -> Result; + + /// Validates the felt representation and any semantic type constraints. + fn validate(&self, felts: &[Felt]) -> Result<()>; +} + +/// A registry of codecs keyed by canonical WIT fully-qualified type name. +/// +/// The canonical form is `:/@.`. The version follows +/// the interface, matching WIT interface identifiers. When a package has no version, the +/// `@` part is omitted, for example `miden:base/core-types.account-id`. +/// The standard account ID codec displays the canonical mainnet bech32 form because the network +/// identifier is not part of an account ID's felt representation. +#[derive(Clone)] +pub struct CodecRegistry { + codecs: BTreeMap>, +} + +impl CodecRegistry { + /// Creates an empty codec registry. + pub fn new() -> Self { + Self { + codecs: BTreeMap::new(), + } + } + + /// Registers or replaces a codec under its canonical WIT FQN. + pub fn register(&mut self, fqn: impl Into, codec: impl ConsumerTypeCodec + 'static) { + self.register_shared(fqn, Arc::new(codec)); + } + + /// Registers or replaces a shared codec under its canonical WIT FQN. + pub fn register_shared(&mut self, fqn: impl Into, codec: Arc) { + self.codecs.insert(fqn.into(), codec); + } + + /// Returns the codec registered for a canonical WIT FQN. + pub fn codec(&self, fqn: &str) -> Option<&dyn ConsumerTypeCodec> { + self.codecs.get(fqn).map(Arc::as_ref) + } + + /// Returns true when the canonical WIT FQN has a codec. + pub fn contains(&self, fqn: &str) -> bool { + self.codecs.contains_key(fqn) + } +} + +impl Default for CodecRegistry { + fn default() -> Self { + let mut registry = Self::new(); + registry.register(FELT_FQN, FeltCodec); + registry.register(WORD_FQN, WordCodec); + registry.register(ACCOUNT_ID_FQN, AccountIdCodec); + registry.register(ASSET_AMOUNT_FQN, AssetAmountCodec); + registry + } +} + +/// Parses and displays one field element. +struct FeltCodec; + +impl ConsumerTypeCodec for FeltCodec { + fn parse(&self, value: &str) -> Result> { + let felt = parse_felt(value)?; + Ok(write_repr(&felt)) + } + + fn display(&self, felts: &[Felt]) -> Result { + read_repr::(felts).map(|felt| felt.as_canonical_u64().to_string()) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + read_repr::(felts).map(|_| ()) + } +} + +/// Parses and displays a four-felt word. +struct WordCodec; + +impl ConsumerTypeCodec for WordCodec { + fn parse(&self, value: &str) -> Result> { + let value = value.trim(); + let word = if value.starts_with("0x") || value.starts_with("0X") { + Word::parse(value).map_err(|err| Error::new(format!("invalid word hex: {err}")))? + } else { + let values = value.trim_matches(['[', ']']); + let felts = values.split(',').map(parse_felt).collect::>>()?; + let elements: [Felt; 4] = felts.try_into().map_err(|felts: Vec| { + Error::new(format!( + "a word needs four comma-separated felts, found {}", + felts.len() + )) + })?; + Word::new(elements) + }; + Ok(write_repr(&word)) + } + + fn display(&self, felts: &[Felt]) -> Result { + read_repr::(felts).map(|word| word.to_hex()) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + read_repr::(felts).map(|_| ()) + } +} + +/// Parses and displays a protocol account ID. +struct AccountIdCodec; + +impl ConsumerTypeCodec for AccountIdCodec { + fn parse(&self, value: &str) -> Result> { + let (account_id, _) = AccountId::parse(value) + .map_err(|err| Error::new(format!("invalid account-id: {err}")))?; + let mut felts = Vec::with_capacity(2); + let mut writer = FeltWriter::new(&mut felts); + writer.write(account_id.prefix().as_felt()); + writer.write(account_id.suffix()); + Ok(felts) + } + + fn display(&self, felts: &[Felt]) -> Result { + read_account_id(felts).map(|account_id| account_id.to_bech32(NetworkId::Mainnet)) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + read_account_id(felts).map(|_| ()) + } +} + +/// Parses and displays a validated asset amount. +struct AssetAmountCodec; + +impl ConsumerTypeCodec for AssetAmountCodec { + fn parse(&self, value: &str) -> Result> { + let amount = value + .trim() + .parse::() + .map_err(|err| Error::new(format!("invalid asset amount: {err}")))?; + let amount = AssetAmount::new(amount) + .map_err(|err| Error::new(format!("invalid asset amount: {err}")))?; + let felt = Felt::from(amount); + Ok(write_repr(&felt)) + } + + fn display(&self, felts: &[Felt]) -> Result { + read_asset_amount(felts).map(|amount| amount.to_string()) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + read_asset_amount(felts).map(|_| ()) + } +} + +/// Parses a canonical felt from decimal or hexadecimal text. +pub(crate) fn parse_felt(value: &str) -> Result { + let value = parse_unsigned(value, "felt")?; + Felt::new(value).map_err(|err| Error::new(format!("invalid felt: {err}"))) +} + +/// Parses a decimal or hexadecimal unsigned integer. +pub(crate) fn parse_unsigned(value: &str, ty: &str) -> Result { + let value = value.trim(); + let parsed = match value.strip_prefix("0x").or_else(|| value.strip_prefix("0X")) { + Some(digits) => u64::from_str_radix(digits, 16), + None => value.parse::(), + }; + parsed.map_err(|err| Error::new(format!("invalid {ty}: {err}"))) +} + +/// Encodes a felt-repr value through the shared writer. +pub(crate) fn write_repr(value: &impl ToFeltRepr) -> Vec { + let mut felts = Vec::new(); + value.write_felt_repr(&mut FeltWriter::new(&mut felts)); + felts +} + +/// Decodes one felt-repr value and rejects trailing elements. +fn read_repr(felts: &[Felt]) -> Result { + let mut reader = FeltReader::new(felts); + let value = T::from_felt_repr(&mut reader) + .map_err(|err| Error::new(format!("invalid felt representation: {err}")))?; + reader + .ensure_eof() + .map_err(|err| Error::new(format!("invalid felt representation: {err}")))?; + Ok(value) +} + +/// Decodes and validates an account ID in WIT record field order. +fn read_account_id(felts: &[Felt]) -> Result { + let mut reader = FeltReader::new(felts); + let prefix = reader + .read() + .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?; + let suffix = reader + .read() + .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?; + reader + .ensure_eof() + .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?; + AccountId::try_from_elements(suffix, prefix) + .map_err(|err| Error::new(format!("invalid account-id representation: {err}"))) +} + +/// Decodes and validates an asset amount. +fn read_asset_amount(felts: &[Felt]) -> Result { + let felt = read_repr::(felts)?; + AssetAmount::try_from(felt) + .map_err(|err| Error::new(format!("invalid asset amount representation: {err}"))) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Returns a valid account ID and its mainnet bech32 form. + fn account_id() -> (AccountId, String) { + let account_id = + AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let bech32 = account_id.to_bech32(NetworkId::Mainnet); + (account_id, bech32) + } + + #[test] + fn standard_registry_contains_canonical_versioned_fqns() { + let registry = CodecRegistry::default(); + + assert!(registry.contains(FELT_FQN)); + assert!(registry.contains(WORD_FQN)); + assert!(registry.contains(ACCOUNT_ID_FQN)); + assert!(registry.contains(ASSET_AMOUNT_FQN)); + } + + #[test] + fn felt_codec_accepts_decimal_and_hex() { + let codec = FeltCodec; + + assert_eq!(codec.parse("42").unwrap(), codec.parse("0x2a").unwrap()); + assert_eq!(codec.display(&codec.parse("42").unwrap()).unwrap(), "42"); + } + + #[test] + fn word_codec_accepts_hex_and_four_felts() { + let codec = WordCodec; + let from_felts = codec.parse("[1, 2, 3, 4]").unwrap(); + let from_hex = codec.parse(&codec.display(&from_felts).unwrap()).unwrap(); + + assert_eq!(from_hex, from_felts); + } + + #[test] + fn account_id_codec_uses_prefix_suffix_field_order() { + let codec = AccountIdCodec; + let (account_id, bech32) = account_id(); + let felts = codec.parse(&bech32).unwrap(); + let hex_felts = codec.parse(&account_id.to_hex()).unwrap(); + + assert_eq!(felts, [account_id.prefix().as_felt(), account_id.suffix()]); + assert_eq!(hex_felts, felts); + assert_eq!(codec.display(&felts).unwrap(), bech32); + } + + #[test] + fn asset_amount_codec_enforces_protocol_limit() { + let codec = AssetAmountCodec; + + assert!(codec.parse(&AssetAmount::MAX.as_u64().to_string()).is_ok()); + assert!(codec.parse(&(AssetAmount::MAX.as_u64() + 1).to_string()).is_err()); + } +} diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs new file mode 100644 index 0000000000..1c3682c5b3 --- /dev/null +++ b/sdk/note-schema/src/codec_component.rs @@ -0,0 +1,397 @@ +//! Consumer adapters for author codec components. + +use std::sync::{Arc, Mutex}; + +use miden_field::Felt; +use miden_mast_package::{Package, SectionId}; +use midenc_frontend_wasm_metadata::PACKAGE_NOTE_CODEC_SECTION_ID; +use wasmtime::{ + Config, Engine, Store, + component::{Component, Linker}, +}; + +use crate::{CodecRegistry, ConsumerTypeCodec, Error, Result}; + +wasmtime::component::bindgen!({ + path: "../note-codec/wit", + world: "note-codec", +}); + +impl CodecRegistry { + /// Loads the note codec component from a package and registers all reported types. + pub fn load_from_package(package: &Package) -> Result { + let section_id = SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID).map_err(|error| { + Error::new(format!( + "invalid note codec section id `{PACKAGE_NOTE_CODEC_SECTION_ID}`: {error}" + )) + })?; + let bytes = package + .sections + .iter() + .find(|section| section.id == section_id) + .ok_or_else(|| { + Error::new(format!( + "package does not contain the `{PACKAGE_NOTE_CODEC_SECTION_ID}` section" + )) + })? + .data + .as_ref(); + + Self::load_from_component(bytes) + } + + /// Loads a zero-import note codec component. + fn load_from_component(bytes: &[u8]) -> Result { + let mut runtime = ComponentRuntime::instantiate(bytes)?; + let supported_types = runtime.supported_types()?; + let runtime = Arc::new(Mutex::new(runtime)); + let mut registry = Self::default(); + + for fqn in supported_types { + if fqn.trim().is_empty() { + return Err(Error::new("note codec component reported an empty type FQN")); + } + registry.register_shared( + fqn.clone(), + Arc::new(ComponentCodec { + fqn, + runtime: Arc::clone(&runtime), + }), + ); + } + + Ok(registry) + } +} + +/// One instantiated note codec component and its mutable store. +struct ComponentRuntime { + store: Store<()>, + bindings: NoteCodec, +} + +impl ComponentRuntime { + /// Instantiates a component without defining any host imports. + fn instantiate(bytes: &[u8]) -> Result { + let mut config = Config::new(); + config.wasm_component_model(true); + let engine = Engine::new(&config) + .map_err(|error| component_error("create the Wasmtime engine", error))?; + let component = Component::new(&engine, bytes) + .map_err(|error| component_error("compile the note codec component", error))?; + let linker = Linker::new(&engine); + let mut store = Store::new(&engine, ()); + let bindings = NoteCodec::instantiate(&mut store, &component, &linker) + .map_err(|error| component_error("instantiate the zero-import note codec", error))?; + Ok(Self { store, bindings }) + } + + /// Queries the component's supported FQNs once during registry construction. + fn supported_types(&mut self) -> Result> { + self.bindings + .miden_note_codec_codec() + .call_supported_types(&mut self.store) + .map_err(|error| component_error("call `supported-types`", error)) + } +} + +/// A registry entry that dispatches one FQN into a shared component instance. +struct ComponentCodec { + fqn: String, + runtime: Arc>, +} + +impl ComponentCodec { + /// Calls one component operation while holding exclusive access to its store. + fn with_runtime( + &self, + operation: &str, + call: impl FnOnce(&NoteCodec, &mut Store<()>) -> wasmtime::Result, + ) -> Result { + let mut runtime = self.runtime.lock().map_err(|_| { + Error::new(format!( + "note codec component state is unavailable while calling `{operation}` for `{}`", + self.fqn + )) + })?; + let ComponentRuntime { bindings, store } = &mut *runtime; + call(bindings, store).map_err(|error| { + component_error(&format!("call `{operation}` for codec `{}`", self.fqn), error) + }) + } +} + +impl ConsumerTypeCodec for ComponentCodec { + fn parse(&self, value: &str) -> Result> { + let result = self.with_runtime("parse", |bindings, store| { + bindings.miden_note_codec_codec().call_parse(store, &self.fqn, value) + })?; + let values = result.map_err(|message| codec_rejection("parse", &self.fqn, message))?; + component_values_to_felts(&self.fqn, &values) + } + + fn display(&self, felts: &[Felt]) -> Result { + let values = felts.iter().map(|felt| felt.as_canonical_u64()).collect::>(); + self.with_runtime("display", |bindings, store| { + bindings.miden_note_codec_codec().call_display(store, &self.fqn, &values) + })? + .map_err(|message| codec_rejection("display", &self.fqn, message)) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + let values = felts.iter().map(|felt| felt.as_canonical_u64()).collect::>(); + self.with_runtime("validate", |bindings, store| { + bindings.miden_note_codec_codec().call_validate(store, &self.fqn, &values) + })? + .map_err(|message| codec_rejection("validate", &self.fqn, message)) + } +} + +/// Converts component integers into canonical field elements. +fn component_values_to_felts(fqn: &str, values: &[u64]) -> Result> { + values + .iter() + .copied() + .enumerate() + .map(|(index, value)| { + Felt::new(value).map_err(|error| { + Error::new(format!( + "codec `{fqn}` returned a noncanonical felt at index {index}: {error}" + )) + }) + }) + .collect() +} + +/// Creates a host error for a component runtime failure. +fn component_error(action: &str, error: impl core::fmt::Display) -> Error { + Error::new(format!("failed to {action}: {error}")) +} + +/// Creates a host error for an author codec rejection. +fn codec_rejection(operation: &str, fqn: &str, message: String) -> Error { + Error::new(format!("codec `{fqn}` rejected `{operation}`: {message}")) +} + +#[cfg(test)] +mod tests { + use std::{ + env, fs, + path::{Path, PathBuf}, + process::{Command, Output}, + sync::Arc, + }; + + use miden_core::{ + mast::{BasicBlockNodeBuilder, DenseMastForestBuilder, MastNodeExt}, + operations::Operation, + }; + use miden_mast_package::{ + PackageExport, PackageId, PathBuf as MastPathBuf, ProcedureExport, Section, TargetType, + Version, + }; + use tempfile::TempDir; + use wit_component::ComponentEncoder; + + use super::*; + + const WASM_TARGET: &str = "wasm32-unknown-unknown"; + + #[test] + fn component_boundary_rejects_noncanonical_felts() { + let error = + component_values_to_felts("example:test/codec.value", &[Felt::ORDER]).unwrap_err(); + + assert!(error.to_string().contains("noncanonical felt at index 0")); + } + + #[test] + fn package_component_registers_and_dispatches_author_codec() { + if !wasm_target_is_installed() { + eprintln!("skipping component adapter test: {WASM_TARGET} is not installed"); + return; + } + + let component = build_fixture_component(); + let mut package = test_package(); + package.sections.push(Section::new( + SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID).unwrap(), + component, + )); + + let registry = CodecRegistry::load_from_package(&package).unwrap(); + let fqn = "example:codec-schema/note-storage@1.0.0.ratio"; + let codec = registry.codec(fqn).expect("fixture ratio codec was not registered"); + let encoded = codec.parse("3/2").unwrap(); + assert_eq!(encoded, [Felt::new(3).unwrap(), Felt::ZERO, Felt::new(2).unwrap(), Felt::ZERO]); + codec.validate(&encoded).unwrap(); + assert_eq!(codec.display(&encoded).unwrap(), "3/2"); + + let invalid = codec.parse("3/0").unwrap(); + assert!(codec.validate(&invalid).unwrap_err().to_string().contains("denominator")); + } + + /// Builds a valid package with one procedure export. + fn test_package() -> Package { + let mut builder = DenseMastForestBuilder::new(); + let node_id = builder + .push_node(BasicBlockNodeBuilder::new(vec![Operation::Add])) + .expect("failed to build package procedure"); + builder.mark_root(node_id); + let (forest, remapping) = builder.finish_with_id_map().expect("failed to build package"); + let node_id = remapping.get(node_id).expect("package root was removed"); + let export = ProcedureExport::new( + MastPathBuf::absolute("component-codec-test::run").into(), + Some(node_id), + forest[node_id].digest(), + None, + ); + + Package::create( + PackageId::from("component-codec-test"), + Version::new(0, 0, 0), + TargetType::Library, + Arc::new(forest), + [PackageExport::Procedure(export)], + [], + ) + .expect("failed to create test package") + } + + /// Builds the minimal author codec used by the Phase 4a component spike. + fn build_fixture_component() -> Vec { + let fixture = TempDir::new().expect("failed to create component fixture directory"); + write_fixture(fixture.path()); + let target_dir = workspace_root().join("target/note-schema-component-test"); + let output = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .args([ + "build", + "--manifest-path", + fixture.path().join("Cargo.toml").to_str().unwrap(), + "--release", + "--target", + WASM_TARGET, + "--offline", + ]) + .env("CARGO_TARGET_DIR", &target_dir) + .env_remove("CARGO_BUILD_TARGET") + .env_remove("CARGO_ENCODED_RUSTFLAGS") + .env_remove("RUSTFLAGS") + .output() + .expect("failed to start fixture build"); + assert_command_succeeded("building the component adapter fixture", &output); + + let module = fs::read( + target_dir.join(format!("{WASM_TARGET}/release/note_schema_component_fixture.wasm")), + ) + .expect("component fixture did not produce its Wasm module"); + ComponentEncoder::default() + .module(&module) + .expect("fixture module is not component-ready") + .validate(true) + .encode() + .expect("failed to encode fixture component") + } + + /// Writes a standalone codec crate for the component adapter test. + fn write_fixture(root: &Path) { + fs::create_dir(root.join("src")).unwrap(); + let codec_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../note-codec"); + let manifest = format!( + r#"[package] +name = "note-schema-component-fixture" +version = "0.0.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden-note-codec = {{ path = {:?} }} + +[workspace] +"#, + codec_path + ); + fs::write(root.join("Cargo.toml"), manifest).unwrap(); + fs::write(root.join("src/lib.rs"), FIXTURE_SOURCE).unwrap(); + } + + /// Returns true when rustup reports the component target as installed. + fn wasm_target_is_installed() -> bool { + let Ok(output) = Command::new("rustup").args(["target", "list"]).output() else { + return false; + }; + output.status.success() + && String::from_utf8_lossy(&output.stdout) + .lines() + .any(|line| line.starts_with(WASM_TARGET) && line.contains("(installed)")) + } + + /// Returns the compiler workspace root. + fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .unwrap() + .to_owned() + } + + /// Includes both output streams when one fixture command fails. + fn assert_command_succeeded(action: &str, output: &Output) { + assert!( + output.status.success(), + "failed while {action}\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + const FIXTURE_SOURCE: &str = r##" +use miden_note_codec::AuthorTypeCodec; + +miden_note_codec::from_wit_text!(r#" +package example:codec-schema@1.0.0; + +interface note-storage { + record ratio { + numerator: u64, + denominator: u64, + } + + record codec-note { + ratio: ratio, + } + + type storage = codec-note; +} +"#); + +#[miden_note_codec::note_codec] +impl AuthorTypeCodec for Ratio { + fn parse(value: &str) -> Result { + let (numerator, denominator) = value + .split_once('/') + .ok_or_else(|| "a ratio must use `numerator/denominator`".to_owned())?; + Ok(Self { + numerator: numerator.parse::().map_err(|error| error.to_string())?, + denominator: denominator.parse::().map_err(|error| error.to_string())?, + }) + } + + fn display(&self) -> String { + format!("{}/{}", self.numerator, self.denominator) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } +} + +miden_note_codec::export_codecs!(); +"##; +} diff --git a/sdk/note-schema/src/error.rs b/sdk/note-schema/src/error.rs new file mode 100644 index 0000000000..eb11106f6f --- /dev/null +++ b/sdk/note-schema/src/error.rs @@ -0,0 +1,34 @@ +//! Error types for note storage schemas. + +use core::fmt; + +/// An error reported while reading, encoding, or decoding a note storage schema. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Error { + message: String, +} + +impl Error { + /// Creates an error with an actionable message. + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } + + /// Adds context before the current error message. + pub(crate) fn context(self, context: impl fmt::Display) -> Self { + Self::new(format!("{context}: {}", self.message)) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for Error {} + +/// A result returned by note storage schema operations. +pub type Result = core::result::Result; diff --git a/sdk/note-schema/src/lib.rs b/sdk/note-schema/src/lib.rs new file mode 100644 index 0000000000..3feaa4c327 --- /dev/null +++ b/sdk/note-schema/src/lib.rs @@ -0,0 +1,39 @@ +//! Host-side access to note storage schemas embedded in Miden packages. +//! +//! # Felt layout +//! +//! The layout is structural over the resolved WIT type tree. The record +//! `miden:base/core-types@1.0.0.felt` is one felt. A `u64` uses two felts in low-then-high +//! `u32` limb order. A `u32`, `u8`, or `bool` uses one range-checked felt. An `option` uses one +//! tag felt followed by its payload when present. A variant uses one declaration-ordinal tag felt +//! followed by the selected case payload. Record fields concatenate in declaration order. +//! +//! Encoding and decoding use [`miden_field_repr::FeltReader`] and +//! [`miden_field_repr::FeltWriter`]. Codecs only parse, display, and validate values. They do not +//! change this layout. + +#![deny(missing_docs)] + +mod builder; +mod codec; +#[cfg(feature = "codec-component")] +mod codec_component; +mod error; +mod schema; +mod value; + +#[cfg(test)] +mod tests; + +pub use builder::NoteStorageBuilder; +pub use codec::{ + ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, CodecRegistry, ConsumerTypeCodec, FELT_FQN, WORD_FQN, +}; +pub use error::{Error, Result}; +pub use miden_field::Felt; +pub use miden_protocol::note::NoteStorage; +pub use schema::{ + FeltLayout, NoteStorageSchema, PrimitiveType, SchemaCase, SchemaField, SchemaType, + SchemaTypeKind, +}; +pub use value::{DecodedValue, DecodedValueKind}; diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs new file mode 100644 index 0000000000..1b76ad46f6 --- /dev/null +++ b/sdk/note-schema/src/schema.rs @@ -0,0 +1,568 @@ +//! Resolved note storage schema model. + +use std::collections::HashSet; + +use miden_mast_package::{Package, SectionId}; +use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; +use wit_parser::{Resolve, Type, TypeDefKind, TypeId, TypeOwner}; + +use crate::{ + CodecRegistry, DecodedValue, Error, NoteStorage, NoteStorageBuilder, Result, codec::FELT_FQN, +}; + +/// The minimum and maximum felt count for a schema type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FeltLayout { + minimum: usize, + maximum: usize, +} + +impl FeltLayout { + /// Returns the minimum number of felts accepted by this layout. + pub const fn minimum(self) -> usize { + self.minimum + } + + /// Returns the maximum number of felts accepted by this layout. + pub const fn maximum(self) -> usize { + self.maximum + } + + /// Returns the fixed width, or `None` for a variable-width layout. + pub const fn fixed_width(self) -> Option { + if self.minimum == self.maximum { + Some(self.minimum) + } else { + None + } + } + + /// Creates a fixed-width layout. + const fn fixed(width: usize) -> Self { + Self { + minimum: width, + maximum: width, + } + } + + /// Adds two layouts in declaration order. + fn concatenate(self, other: Self) -> Result { + let minimum = self + .minimum + .checked_add(other.minimum) + .ok_or_else(|| Error::new("note storage layout minimum width is too large"))?; + let maximum = self + .maximum + .checked_add(other.maximum) + .ok_or_else(|| Error::new("note storage layout maximum width is too large"))?; + Ok(Self { minimum, maximum }) + } +} + +/// A supported primitive WIT type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PrimitiveType { + /// An unsigned 64-bit integer stored as low and high `u32` limbs. + U64, + /// An unsigned 32-bit integer stored in one felt. + U32, + /// An unsigned 8-bit integer stored in one felt. + U8, + /// A boolean stored as zero or one. + Bool, +} + +/// The structural kind of a resolved schema type. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum SchemaTypeKind { + /// The one-felt `miden:base/core-types.felt` bedrock type. + Felt, + /// A supported WIT primitive. + Primitive(PrimitiveType), + /// A record with fields in declaration order. + Record(Vec), + /// An optional payload stored after a tag felt. + Option(Box), + /// A variant with declaration-ordinal cases. + Variant(Vec), +} + +/// A resolved WIT type used by note storage. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SchemaType { + name: Option, + fqn: Option, + docs: Option, + kind: SchemaTypeKind, + layout: FeltLayout, +} + +impl SchemaType { + /// Returns the WIT type name when this is a named type. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Returns the canonical fully-qualified WIT type name. + pub fn fqn(&self) -> Option<&str> { + self.fqn.as_deref() + } + + /// Returns the resolved WIT documentation. + pub fn docs(&self) -> Option<&str> { + self.docs.as_deref() + } + + /// Returns the structural type kind. + pub const fn kind(&self) -> &SchemaTypeKind { + &self.kind + } + + /// Returns the felt layout for this type. + pub const fn layout(&self) -> FeltLayout { + self.layout + } +} + +/// A named record field in declaration order. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SchemaField { + name: String, + docs: Option, + ty: SchemaType, +} + +impl SchemaField { + /// Returns the field's kebab-case WIT name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the field-level WIT documentation. + pub fn docs(&self) -> Option<&str> { + self.docs.as_deref() + } + + /// Returns the field type. + pub const fn ty(&self) -> &SchemaType { + &self.ty + } +} + +/// A WIT variant case in declaration order. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SchemaCase { + name: String, + docs: Option, + payload: Option, +} + +impl SchemaCase { + /// Returns the case's kebab-case WIT name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the case documentation. + pub fn docs(&self) -> Option<&str> { + self.docs.as_deref() + } + + /// Returns the optional case payload. + pub const fn payload(&self) -> Option<&SchemaType> { + self.payload.as_ref() + } +} + +/// A resolved note storage schema with the standard codec registry. +#[derive(Clone)] +pub struct NoteStorageSchema { + wit_text: String, + root: SchemaType, + codecs: CodecRegistry, +} + +impl NoteStorageSchema { + /// Reads and resolves the note storage schema section from a Miden package. + pub fn from_package(package: &Package) -> Result { + let section_id = + SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).map_err(|err| { + Error::new(format!( + "invalid note storage schema section id \ + `{PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID}`: {err}" + )) + })?; + let bytes = package + .sections + .iter() + .find(|section| section.id == section_id) + .ok_or_else(|| { + Error::new(format!( + "package does not contain the `{PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID}` \ + section" + )) + })? + .data + .as_ref(); + let unpadded_len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); + let text = core::str::from_utf8(&bytes[..unpadded_len]).map_err(|err| { + Error::new(format!("note storage schema section is not valid UTF-8: {err}")) + })?; + Self::from_wit_text(text) + } + + /// Resolves a note storage schema from a WIT document. + pub fn from_wit_text(wit_text: &str) -> Result { + let wit_text = wit_text.trim_end_matches('\0'); + let mut resolve = Resolve::default(); + let package_id = resolve.push_str("note-storage-schema.wit", wit_text).map_err(|err| { + Error::new(format!("failed to resolve note storage schema WIT: {err:#}")) + })?; + let package = &resolve.packages[package_id]; + let interface_id = package.interfaces.get("note-storage").copied().ok_or_else(|| { + Error::new(format!( + "schema package `{}` does not define the `note-storage` interface", + package.name + )) + })?; + let interface = &resolve.interfaces[interface_id]; + let storage_id = interface.types.get("storage").copied().ok_or_else(|| { + Error::new("the `note-storage` interface does not define the `storage` type alias") + })?; + let root = ModelBuilder::new(&resolve).build(Type::Id(storage_id))?; + if !matches!(root.kind, SchemaTypeKind::Record(_)) { + return Err(Error::new(format!( + "the `note-storage.storage` alias must resolve to a record, found {}", + kind_name(&root.kind) + ))); + } + + Ok(Self { + wit_text: wit_text.to_owned(), + root, + codecs: CodecRegistry::default(), + }) + } + + /// Returns the unpadded WIT document. + pub fn wit_text(&self) -> &str { + &self.wit_text + } + + /// Returns the root storage record. + pub const fn root(&self) -> &SchemaType { + &self.root + } + + /// Returns the root felt layout. + pub const fn layout(&self) -> FeltLayout { + self.root.layout + } + + /// Returns the schema's standard codec registry. + pub const fn codecs(&self) -> &CodecRegistry { + &self.codecs + } + + /// Replaces the codec registry used by `builder` and `decode`. + pub fn with_codec_registry(mut self, codecs: CodecRegistry) -> Self { + self.codecs = codecs; + self + } + + /// Creates a string-value builder with the schema's codec registry. + pub fn builder(&self) -> NoteStorageBuilder<'_> { + self.builder_with_registry(&self.codecs) + } + + /// Creates a string-value builder with a caller-provided codec registry. + pub fn builder_with_registry<'a>( + &'a self, + registry: &'a CodecRegistry, + ) -> NoteStorageBuilder<'a> { + NoteStorageBuilder::new(self, registry) + } + + /// Decodes note storage with the schema's codec registry. + pub fn decode(&self, storage: &NoteStorage) -> Result { + self.decode_with_registry(storage, &self.codecs) + } + + /// Decodes note storage with a caller-provided codec registry. + pub fn decode_with_registry( + &self, + storage: &NoteStorage, + registry: &CodecRegistry, + ) -> Result { + crate::value::decode(&self.root, storage, registry) + } +} + +/// Builds an owned schema type tree from a resolved WIT graph. +struct ModelBuilder<'a> { + resolve: &'a Resolve, + active: HashSet, +} + +impl<'a> ModelBuilder<'a> { + /// Creates a model builder for one schema package. + fn new(resolve: &'a Resolve) -> Self { + Self { + resolve, + active: HashSet::new(), + } + } + + /// Resolves one WIT type. + fn build(mut self, ty: Type) -> Result { + self.build_type(ty) + } + + /// Resolves a primitive or named type. + fn build_type(&mut self, ty: Type) -> Result { + match ty { + Type::Id(id) => self.build_type_id(id), + Type::U64 => self.primitive(PrimitiveType::U64, None, None, None), + Type::U32 => self.primitive(PrimitiveType::U32, None, None, None), + Type::U8 => self.primitive(PrimitiveType::U8, None, None, None), + Type::Bool => self.primitive(PrimitiveType::Bool, None, None, None), + unsupported => Err(Error::new(format!( + "WIT primitive `{unsupported:?}` is not supported in note storage schemas" + ))), + } + } + + /// Resolves aliases to the type definition that owns the structural type. + fn build_type_id(&mut self, id: TypeId) -> Result { + let id = self.follow_aliases(id)?; + if !self.active.insert(id) { + return Err(Error::new( + "recursive WIT types are not supported in note storage schemas", + )); + } + + let definition = self.resolve.types[id].clone(); + let name = definition.name.clone(); + let docs = definition.docs.contents.clone(); + let fqn = self.type_fqn(id)?; + let result = if fqn.as_deref() == Some(FELT_FQN) { + Ok(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Felt, + layout: FeltLayout::fixed(1), + }) + } else { + match definition.kind { + TypeDefKind::Type(ty) => self.build_named_alias(ty, name, fqn, docs), + TypeDefKind::Record(record) => { + let mut fields = Vec::with_capacity(record.fields.len()); + let mut layout = FeltLayout::fixed(0); + for field in record.fields { + let ty = self.build_type(field.ty)?; + layout = layout.concatenate(ty.layout)?; + fields.push(SchemaField { + name: field.name, + docs: field.docs.contents, + ty, + }); + } + Ok(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Record(fields), + layout, + }) + } + TypeDefKind::Option(payload) => { + let payload = Box::new(self.build_type(payload)?); + let layout = FeltLayout { + minimum: 1, + maximum: 1usize.checked_add(payload.layout.maximum).ok_or_else(|| { + Error::new("option layout maximum width is too large") + })?, + }; + Ok(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Option(payload), + layout, + }) + } + TypeDefKind::Variant(variant) => { + let mut cases = Vec::with_capacity(variant.cases.len()); + for case in variant.cases { + cases.push(SchemaCase { + name: case.name, + docs: case.docs.contents, + payload: case.ty.map(|ty| self.build_type(ty)).transpose()?, + }); + } + let layout = variant_layout(&cases)?; + Ok(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Variant(cases), + layout, + }) + } + TypeDefKind::Enum(enum_) => { + let cases = enum_ + .cases + .into_iter() + .map(|case| SchemaCase { + name: case.name, + docs: case.docs.contents, + payload: None, + }) + .collect::>(); + let layout = variant_layout(&cases)?; + Ok(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Variant(cases), + layout, + }) + } + unsupported => Err(Error::new(format!( + "WIT {} `{}` is not supported in note storage schemas", + unsupported.as_str(), + fqn.as_deref().or(name.as_deref()).unwrap_or("") + ))), + } + }; + self.active.remove(&id); + result + } + + /// Resolves a named alias whose target is a primitive. + fn build_named_alias( + &mut self, + ty: Type, + name: Option, + fqn: Option, + docs: Option, + ) -> Result { + match ty { + Type::Id(id) => self.build_type_id(id), + Type::U64 => self.primitive(PrimitiveType::U64, name, fqn, docs), + Type::U32 => self.primitive(PrimitiveType::U32, name, fqn, docs), + Type::U8 => self.primitive(PrimitiveType::U8, name, fqn, docs), + Type::Bool => self.primitive(PrimitiveType::Bool, name, fqn, docs), + unsupported => Err(Error::new(format!( + "WIT primitive alias `{unsupported:?}` is not supported in note storage schemas" + ))), + } + } + + /// Creates a supported primitive type. + fn primitive( + &self, + primitive: PrimitiveType, + name: Option, + fqn: Option, + docs: Option, + ) -> Result { + let width = match primitive { + PrimitiveType::U64 => 2, + PrimitiveType::U32 | PrimitiveType::U8 | PrimitiveType::Bool => 1, + }; + Ok(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Primitive(primitive), + layout: FeltLayout::fixed(width), + }) + } + + /// Follows `type = id` aliases to their defining type. + fn follow_aliases(&self, mut id: TypeId) -> Result { + let mut visited = HashSet::new(); + loop { + if !visited.insert(id) { + return Err(Error::new("cyclic WIT type aliases are not supported")); + } + match self.resolve.types[id].kind { + TypeDefKind::Type(Type::Id(next)) => id = next, + _ => return Ok(id), + } + } + } + + /// Reconstructs the canonical FQN for a named interface type. + fn type_fqn(&self, id: TypeId) -> Result> { + let definition = &self.resolve.types[id]; + let Some(type_name) = definition.name.as_deref() else { + return Ok(None); + }; + let TypeOwner::Interface(interface_id) = definition.owner else { + return Err(Error::new(format!( + "named WIT type `{type_name}` is not owned by an interface" + ))); + }; + let interface = &self.resolve.interfaces[interface_id]; + let interface_name = interface.name.as_deref().ok_or_else(|| { + Error::new(format!("type `{type_name}` belongs to an unnamed interface")) + })?; + let package_id = interface.package.ok_or_else(|| { + Error::new(format!("interface `{interface_name}` does not belong to a package")) + })?; + let package_name = &self.resolve.packages[package_id].name; + let mut fqn = + format!("{}:{}/{}", package_name.namespace, package_name.name, interface_name); + if let Some(version) = &package_name.version { + fqn.push('@'); + fqn.push_str(&version.to_string()); + } + fqn.push('.'); + fqn.push_str(type_name); + Ok(Some(fqn)) + } +} + +/// Returns a variable layout for declaration-ordinal cases. +fn variant_layout(cases: &[SchemaCase]) -> Result { + if cases.is_empty() { + return Err(Error::new("a note storage variant must define at least one case")); + } + let minimum_payload = cases + .iter() + .map(|case| case.payload.as_ref().map_or(0, |ty| ty.layout.minimum)) + .min() + .unwrap_or(0); + let maximum_payload = cases + .iter() + .map(|case| case.payload.as_ref().map_or(0, |ty| ty.layout.maximum)) + .max() + .unwrap_or(0); + Ok(FeltLayout { + minimum: 1usize + .checked_add(minimum_payload) + .ok_or_else(|| Error::new("variant layout minimum width is too large"))?, + maximum: 1usize + .checked_add(maximum_payload) + .ok_or_else(|| Error::new("variant layout maximum width is too large"))?, + }) +} + +/// Returns a stable name for a model kind. +fn kind_name(kind: &SchemaTypeKind) -> &'static str { + match kind { + SchemaTypeKind::Felt => "felt", + SchemaTypeKind::Primitive(_) => "primitive", + SchemaTypeKind::Record(_) => "record", + SchemaTypeKind::Option(_) => "option", + SchemaTypeKind::Variant(_) => "variant", + } +} + +/// Normalizes one WIT path segment from snake case to kebab case. +pub(crate) fn normalize_name(name: &str) -> String { + name.trim().replace('_', "-") +} diff --git a/sdk/note-schema/src/tests.rs b/sdk/note-schema/src/tests.rs new file mode 100644 index 0000000000..d905eeb40e --- /dev/null +++ b/sdk/note-schema/src/tests.rs @@ -0,0 +1,264 @@ +//! Unit tests for schema interpretation, building, and decoding. + +use miden_field_repr::ToFeltRepr; +use miden_protocol::{account::AccountId, address::NetworkId}; + +use crate::{ + ACCOUNT_ID_FQN, CodecRegistry, DecodedValueKind, Felt, NoteStorage, NoteStorageSchema, + SchemaTypeKind, +}; + +const LAYOUT_SCHEMA: &str = r#" +package example:layout-schema@1.2.3; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id, felt}; + + record nested-values { + /// A wide counter. + wide-count: u64, + small-count: u8, + } + + variant selection { + empty, + count(u32), + } + + record layout-note { + bedrock: felt, + nested: nested-values, + maybe-enabled: option, + selected: selection, + target-account-id: account-id, + } + + type storage = layout-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { + inner: f32, + } + + record account-id { + prefix: felt, + suffix: felt, + } + } +} +"#; + +/// Returns a valid account ID and its mainnet bech32 form. +fn account_id() -> (AccountId, String) { + let account_id = AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let bech32 = account_id.to_bech32(NetworkId::Mainnet); + (account_id, bech32) +} + +#[test] +fn resolves_alias_fqns_docs_and_structural_widths() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + + assert_eq!( + schema.root().fqn(), + Some("example:layout-schema/note-storage@1.2.3.layout-note") + ); + assert_eq!(schema.layout().minimum(), 8); + assert_eq!(schema.layout().maximum(), 10); + assert_eq!(schema.layout().fixed_width(), None); + + let SchemaTypeKind::Record(root_fields) = schema.root().kind() else { + panic!("storage root must be a record"); + }; + let nested = root_fields.iter().find(|field| field.name() == "nested").unwrap(); + let SchemaTypeKind::Record(nested_fields) = nested.ty().kind() else { + panic!("nested must be a record"); + }; + assert_eq!(nested_fields[0].docs(), Some("A wide counter.")); + + let account_id = root_fields.iter().find(|field| field.name() == "target-account-id").unwrap(); + assert_eq!(account_id.ty().fqn(), Some(ACCOUNT_ID_FQN)); + assert_eq!(account_id.ty().layout().fixed_width(), Some(2)); +} + +#[test] +fn wit_reader_reports_missing_schema_surface() { + let error = NoteStorageSchema::from_wit_text( + "package example:missing@1.0.0; interface other { record value {} }", + ) + .err() + .unwrap() + .to_string(); + + assert!(error.contains("does not define the `note-storage` interface")); +} + +#[test] +fn builder_normalizes_paths_and_uses_declaration_order() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (account_id, bech32) = account_id(); + let storage = schema + .builder() + .set("bedrock", "5") + .unwrap() + .set("nested.wide_count", "4294967298") + .unwrap() + .set("nested.small-count", "7") + .unwrap() + .set("maybe_enabled", "some(true)") + .unwrap() + .set("selected", "count(9)") + .unwrap() + .set("target_account_id", &bech32) + .unwrap() + .build() + .unwrap(); + + let expected = NoteStorage::new(vec![ + Felt::from_u32(5), + Felt::from_u32(2), + Felt::from_u32(1), + Felt::from_u32(7), + Felt::ONE, + Felt::ONE, + Felt::ONE, + Felt::from_u32(9), + account_id.prefix().as_felt(), + account_id.suffix(), + ]) + .unwrap(); + assert_eq!(storage, expected); + + let decoded = schema.decode(&storage).unwrap(); + assert_eq!( + decoded.field("nested").unwrap().field("wide_count").unwrap().to_string(), + "4294967298" + ); + assert_eq!(decoded.field("maybe_enabled").unwrap().to_string(), "some(true)"); + assert_eq!(decoded.field("selected").unwrap().to_string(), "count(9)"); + assert_eq!(decoded.field("target_account_id").unwrap().to_string(), bech32); +} + +#[test] +fn decoder_uses_structural_fallback_without_a_codec() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (account_id, bech32) = account_id(); + let storage = schema + .builder() + .set("bedrock", "5") + .unwrap() + .set("nested.wide-count", "0") + .unwrap() + .set("nested.small-count", "0") + .unwrap() + .set("maybe-enabled", "none") + .unwrap() + .set("selected", "empty") + .unwrap() + .set("target-account-id", &bech32) + .unwrap() + .build() + .unwrap(); + + let decoded = schema.decode_with_registry(&storage, &CodecRegistry::new()).unwrap(); + let account = decoded.field("target-account-id").unwrap(); + let DecodedValueKind::Record(fields) = account.kind() else { + panic!("account-id must use its structural record fallback"); + }; + assert_eq!(fields[0].name(), Some("prefix")); + assert_eq!(fields[0].to_string(), account_id.prefix().as_u64().to_string()); + assert_eq!(fields[1].name(), Some("suffix")); + assert_eq!(fields[1].to_string(), account_id.suffix().as_canonical_u64().to_string()); +} + +#[test] +fn builder_reports_missing_unknown_conflicting_and_range_errors() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (_, bech32) = account_id(); + + let missing = schema.builder().build().unwrap_err().to_string(); + assert!(missing.contains("missing note storage value for `bedrock`")); + + let unknown = schema.builder().set("nested.unknown", "1").err().unwrap().to_string(); + assert!(unknown.contains("has no field named `unknown`")); + + let conflict = schema + .builder() + .set("nested", "value") + .unwrap() + .set("nested.wide-count", "1") + .err() + .unwrap() + .to_string(); + assert!(conflict.contains("conflicts")); + + let range = schema + .builder() + .set("bedrock", "0") + .unwrap() + .set("nested.wide-count", "0") + .unwrap() + .set("nested.small-count", "256") + .unwrap() + .set("maybe-enabled", "none") + .unwrap() + .set("selected", "empty") + .unwrap() + .set("target-account-id", &bech32) + .unwrap() + .build() + .unwrap_err() + .to_string(); + assert!(range.contains("u8 value `256` is out of range")); +} + +#[test] +fn decoder_rejects_invalid_option_and_variant_tags() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (account_id, _) = account_id(); + let suffix = [account_id.prefix().as_felt(), account_id.suffix()]; + + let invalid_option = NoteStorage::new( + [ + vec![Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from_u32(2)], + vec![Felt::ZERO], + suffix.to_vec(), + ] + .concat(), + ) + .unwrap(); + assert!( + schema + .decode(&invalid_option) + .unwrap_err() + .to_string() + .contains("invalid option tag") + ); + + let invalid_variant = NoteStorage::new( + [ + vec![Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO], + vec![Felt::from_u32(2)], + suffix.to_vec(), + ] + .concat(), + ) + .unwrap(); + assert!( + schema + .decode(&invalid_variant) + .unwrap_err() + .to_string() + .contains("invalid variant tag 2") + ); +} + +#[test] +fn u64_layout_uses_shared_low_then_high_limb_encoding() { + let value = 0x1234_5678_90ab_cdefu64; + assert_eq!(value.to_felt_repr(), [Felt::from_u32(0x90ab_cdef), Felt::from_u32(0x1234_5678)]); +} diff --git a/sdk/note-schema/src/value.rs b/sdk/note-schema/src/value.rs new file mode 100644 index 0000000000..5b8b084b2c --- /dev/null +++ b/sdk/note-schema/src/value.rs @@ -0,0 +1,285 @@ +//! Named values decoded from note storage. + +use core::fmt; + +use miden_field_repr::{FeltReader, FromFeltRepr}; + +use crate::{ + CodecRegistry, Error, Felt, NoteStorage, PrimitiveType, Result, SchemaType, SchemaTypeKind, + schema::normalize_name, +}; + +/// A named value decoded from note storage. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DecodedValue { + name: Option, + docs: Option, + fqn: Option, + kind: DecodedValueKind, +} + +impl DecodedValue { + /// Returns the field or root type name. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Returns the field-level or root type documentation. + pub fn docs(&self) -> Option<&str> { + self.docs.as_deref() + } + + /// Returns the canonical WIT FQN when this value has one. + pub fn fqn(&self) -> Option<&str> { + self.fqn.as_deref() + } + + /// Returns the decoded value kind. + pub const fn kind(&self) -> &DecodedValueKind { + &self.kind + } + + /// Finds a direct record field with kebab-case or snake_case spelling. + pub fn field(&self, name: &str) -> Option<&Self> { + let DecodedValueKind::Record(fields) = &self.kind else { + return None; + }; + let name = normalize_name(name); + fields.iter().find(|field| field.name.as_deref() == Some(name.as_str())) + } +} + +/// The structural value stored in a decoded node. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DecodedValueKind { + /// A codec-rendered or primitive leaf with its encoded felts. + Leaf { + /// The structural felt representation. + felts: Vec, + /// The registry-backed or primitive display text. + display: String, + }, + /// Record fields in declaration order. + Record(Vec), + /// An optional value. + Option(Option>), + /// A selected variant case and its optional payload. + Variant { + /// The selected case name. + case: String, + /// The decoded case payload. + value: Option>, + }, +} + +impl fmt::Display for DecodedValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.kind { + DecodedValueKind::Leaf { display, .. } => f.write_str(display), + DecodedValueKind::Record(fields) => { + f.write_str("{")?; + for (index, field) in fields.iter().enumerate() { + if index != 0 { + f.write_str(", ")?; + } + f.write_str(field.name.as_deref().unwrap_or(""))?; + f.write_str(": ")?; + field.fmt(f)?; + } + f.write_str("}") + } + DecodedValueKind::Option(None) => f.write_str("none"), + DecodedValueKind::Option(Some(value)) => write!(f, "some({value})"), + DecodedValueKind::Variant { case, value: None } => f.write_str(case), + DecodedValueKind::Variant { + case, + value: Some(value), + } => write!(f, "{case}({value})"), + } + } +} + +/// Decodes a root storage value and rejects trailing felts. +pub(crate) fn decode( + root: &SchemaType, + storage: &NoteStorage, + registry: &CodecRegistry, +) -> Result { + let (value, consumed) = decode_type( + root, + storage.items(), + Some(registry), + root.name().map(str::to_owned), + root.docs().map(str::to_owned), + )?; + if consumed != storage.items().len() { + return Err(Error::new(format!( + "note storage has {} trailing felt(s) after the schema root", + storage.items().len() - consumed + ))); + } + Ok(value) +} + +/// Validates one complete structural encoding without applying codecs. +pub(crate) fn validate_encoding(ty: &SchemaType, felts: &[Felt]) -> Result<()> { + let (_, consumed) = decode_type(ty, felts, None, None, None)?; + if consumed != felts.len() { + return Err(Error::new(format!( + "value has {} trailing felt(s) after its structural encoding", + felts.len() - consumed + ))); + } + Ok(()) +} + +/// Decodes one value prefix and returns the consumed felt count. +fn decode_type( + ty: &SchemaType, + input: &[Felt], + registry: Option<&CodecRegistry>, + name: Option, + docs: Option, +) -> Result<(DecodedValue, usize)> { + if let (Some(registry), Some(fqn)) = (registry, ty.fqn()) + && let Some(codec) = registry.codec(fqn) + { + let (_, consumed) = decode_type(ty, input, None, None, None)?; + let felts = input[..consumed].to_vec(); + codec + .validate(&felts) + .map_err(|err| err.context(format!("codec `{fqn}` rejected decoded value")))?; + let display = codec + .display(&felts) + .map_err(|err| err.context(format!("codec `{fqn}` failed to display decoded value")))?; + return Ok(( + DecodedValue { + name, + docs, + fqn: Some(fqn.to_owned()), + kind: DecodedValueKind::Leaf { display, felts }, + }, + consumed, + )); + } + + let fqn = ty.fqn().map(str::to_owned); + let (kind, consumed) = match ty.kind() { + SchemaTypeKind::Felt => { + let mut reader = FeltReader::new(input); + let felt = reader + .read() + .map_err(|err| Error::new(format!("invalid felt representation: {err}")))?; + ( + DecodedValueKind::Leaf { + felts: vec![felt], + display: felt.as_canonical_u64().to_string(), + }, + reader.pos(), + ) + } + SchemaTypeKind::Primitive(primitive) => decode_primitive(*primitive, input)?, + SchemaTypeKind::Record(fields) => { + let mut decoded = Vec::with_capacity(fields.len()); + let mut offset = 0usize; + for field in fields { + let (value, consumed) = decode_type( + field.ty(), + &input[offset..], + registry, + Some(field.name().to_owned()), + field.docs().map(str::to_owned), + ) + .map_err(|err| err.context(format!("field `{}`", field.name())))?; + offset = offset + .checked_add(consumed) + .ok_or_else(|| Error::new("decoded record width is too large"))?; + decoded.push(value); + } + (DecodedValueKind::Record(decoded), offset) + } + SchemaTypeKind::Option(payload) => { + let mut reader = FeltReader::new(input); + let tag = reader + .read() + .map_err(|err| Error::new(format!("invalid option representation: {err}")))? + .as_canonical_u64(); + match tag { + 0 => (DecodedValueKind::Option(None), reader.pos()), + 1 => { + let (value, consumed) = + decode_type(payload, &input[reader.pos()..], registry, None, None)?; + (DecodedValueKind::Option(Some(Box::new(value))), reader.pos() + consumed) + } + tag => { + return Err(Error::new(format!("invalid option tag {tag}; expected 0 or 1"))); + } + } + } + SchemaTypeKind::Variant(cases) => { + let mut reader = FeltReader::new(input); + let tag = reader + .read_u32() + .map_err(|err| Error::new(format!("invalid variant representation: {err}")))? + as usize; + let case = cases.get(tag).ok_or_else(|| { + Error::new(format!( + "invalid variant tag {tag}; expected a declaration ordinal below {}", + cases.len() + )) + })?; + let (value, consumed) = match case.payload() { + Some(payload) => { + let (value, consumed) = + decode_type(payload, &input[reader.pos()..], registry, None, None)?; + (Some(Box::new(value)), reader.pos() + consumed) + } + None => (None, reader.pos()), + }; + ( + DecodedValueKind::Variant { + case: case.name().to_owned(), + value, + }, + consumed, + ) + } + }; + + Ok(( + DecodedValue { + name, + docs, + fqn, + kind, + }, + consumed, + )) +} + +/// Decodes a supported primitive through `miden-field-repr`. +fn decode_primitive(primitive: PrimitiveType, input: &[Felt]) -> Result<(DecodedValueKind, usize)> { + match primitive { + PrimitiveType::U64 => read_primitive::(input), + PrimitiveType::U32 => read_primitive::(input), + PrimitiveType::U8 => read_primitive::(input), + PrimitiveType::Bool => read_primitive::(input), + } +} + +/// Decodes and displays one primitive value. +fn read_primitive(input: &[Felt]) -> Result<(DecodedValueKind, usize)> +where + T: FromFeltRepr + fmt::Display, +{ + let mut reader = FeltReader::new(input); + let value = T::from_felt_repr(&mut reader) + .map_err(|err| Error::new(format!("invalid primitive representation: {err}")))?; + Ok(( + DecodedValueKind::Leaf { + felts: input[..reader.pos()].to_vec(), + display: value.to_string(), + }, + reader.pos(), + )) +} diff --git a/sdk/note-schema/tests/p2id_package.rs b/sdk/note-schema/tests/p2id_package.rs new file mode 100644 index 0000000000..917ced2114 --- /dev/null +++ b/sdk/note-schema/tests/p2id_package.rs @@ -0,0 +1,42 @@ +//! End-to-end test for a schema embedded in the p2id note package. + +use std::{path::Path, sync::Arc}; + +use miden_mast_package::Package; +use miden_note_schema::{NoteStorage, NoteStorageSchema}; +use miden_protocol::{account::AccountId, address::NetworkId}; +use midenc_frontend_wasm::WasmTranslationConfig; +use midenc_integration_test_support::CompilerTest; + +/// Compiles one Cargo Miden project without debug output. +fn compile_project(project_path: &Path) -> Arc { + let mut test = CompilerTest::rust_source_cargo_miden( + project_path, + WasmTranslationConfig::default(), + ["--debug".to_owned(), "none".to_owned()], + ); + test.compile_package() +} + +#[test] +fn p2id_schema_builds_and_decodes_account_id_storage() { + let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples"); + let wallet_dir = examples.join("basic-wallet"); + let wallet = compile_project(&wallet_dir); + wallet + .write_masp_file(wallet_dir.join("target/miden/release")) + .expect("failed to persist the basic-wallet dependency package"); + + let p2id = compile_project(&examples.join("p2id-note")); + let schema = NoteStorageSchema::from_package(&p2id).expect("p2id schema must resolve"); + let account_id = AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let bech32 = account_id.to_bech32(NetworkId::Mainnet); + + let built = schema.builder().set("target-account-id", &bech32).unwrap().build().unwrap(); + let expected = + NoteStorage::new(vec![account_id.prefix().as_felt(), account_id.suffix()]).unwrap(); + assert_eq!(built, expected); + + let decoded = schema.decode(&built).unwrap(); + assert_eq!(decoded.field("target_account_id").unwrap().to_string(), bech32); +} diff --git a/sdk/wasm-metadata/src/lib.rs b/sdk/wasm-metadata/src/lib.rs index d002ac66e1..f5afd5649b 100644 --- a/sdk/wasm-metadata/src/lib.rs +++ b/sdk/wasm-metadata/src/lib.rs @@ -35,6 +35,15 @@ pub fn package_wit_section_id() -> miden_mast_package::SectionId { .expect("the WIT section id must be a valid custom section id") } +/// Name of the Wasm custom section that stores a note storage schema. +pub const WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME: &str = "rodata,miden_note_schema"; + +/// Name of the Miden package section that stores a note storage schema. +pub const PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID: &str = "note_storage_schema"; + +/// Name of the Miden package section that stores a note codec. +pub const PACKAGE_NOTE_CODEC_SECTION_ID: &str = "note_codec"; + /// The filesystem package-cache exchange contract. /// /// The compiler publishes compiled dependency packages — and its recorded dependency @@ -250,6 +259,8 @@ pub struct PackageSections { pub account_component_metadata: Option>, /// The component's public WIT source emitted by the `#[component]` macro. pub component_wit: Option>, + /// The note storage schema. + pub note_storage_schema: Option>, } /// Frontend-only metadata emitted by the SDK macros into a dedicated Wasm custom section. diff --git a/tests/integration-network/Cargo.toml b/tests/integration-network/Cargo.toml index 3bec4aff8d..01745cc759 100644 --- a/tests/integration-network/Cargo.toml +++ b/tests/integration-network/Cargo.toml @@ -33,3 +33,7 @@ tokio.workspace = true # For accessing shared compiler test builders and helpers midenc-integration-test-support.workspace = true + +[dev-dependencies] +miden-note-schema = { workspace = true, features = ["codec-component"] } +midenc-frontend-wasm-metadata.workspace = true diff --git a/tests/integration-network/src/mockchain/notes/mod.rs b/tests/integration-network/src/mockchain/notes/mod.rs index 6ef1b20e3b..4580940069 100644 --- a/tests/integration-network/src/mockchain/notes/mod.rs +++ b/tests/integration-network/src/mockchain/notes/mod.rs @@ -1,2 +1,3 @@ mod basic_wallet; mod note_constructor; +mod schema; diff --git a/tests/integration-network/src/mockchain/notes/schema.rs b/tests/integration-network/src/mockchain/notes/schema.rs new file mode 100644 index 0000000000..cd2d1c8257 --- /dev/null +++ b/tests/integration-network/src/mockchain/notes/schema.rs @@ -0,0 +1,203 @@ +//! Schema-driven note storage tests on the mock chain. + +use std::{ + env, + path::{Path, PathBuf}, + process::{Command, Output}, + sync::Arc, +}; + +use miden_client::{ + account::{AccountComponent, component::InitStorageData}, + asset::{Asset, FungibleAsset}, + transaction::RawOutputNote, +}; +use miden_mast_package::{Package, SectionId}; +use miden_note_schema::{CodecRegistry, NoteStorage, NoteStorageSchema}; +use miden_protocol::{ + account::{AccountId, auth::AuthScheme}, + address::NetworkId, + crypto::rand::RandomCoin, + note::Note, +}; +use miden_standards::testing::note::NoteBuilder; +use miden_testing::{Auth, MockChain}; +use midenc_frontend_wasm_metadata::{ + PACKAGE_NOTE_CODEC_SECTION_ID, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, +}; + +use super::super::support::{ + assert_account_has_fungible_asset, build_send_notes_script, compile_rust_package, execute_tx, + note_script_root, +}; + +/// Builds and consumes a note, then returns the consumed note and account ID. +fn transfer_with_storage( + note_package: Arc, + build_storage: impl FnOnce(AccountId) -> NoteStorage, +) -> (Note, AccountId) { + let wallet_package = compile_rust_package("../../examples/basic-wallet", true); + let wallet_component = + AccountComponent::from_package(&wallet_package, &InitStorageData::default()).unwrap(); + + let mut builder = MockChain::builder(); + let faucet = builder + .add_existing_basic_faucet( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + "TEST", + 1_000_000, + None, + ) + .unwrap(); + let faucet_id = faucet.id(); + let recipient = builder + .add_existing_account_from_components( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + [wallet_component], + ) + .unwrap(); + let recipient_id = recipient.id(); + + let mut chain = builder.build().unwrap(); + chain.prove_next_block().unwrap(); + chain.prove_next_block().unwrap(); + + let transfer_amount = 100_000; + let asset = FungibleAsset::new(faucet_id, transfer_amount).unwrap(); + let mut note_rng = RandomCoin::new(note_script_root(¬e_package)); + let note = NoteBuilder::new(faucet_id, &mut note_rng) + .package((*note_package).clone()) + .add_assets([Asset::from(asset)]) + .note_storage(build_storage(recipient_id).to_elements()) + .unwrap() + .build() + .unwrap(); + + let faucet = chain.committed_account(faucet_id).unwrap().clone(); + let send_script = build_send_notes_script(&faucet, std::slice::from_ref(¬e)); + let send = chain + .build_tx_context(faucet_id, &[], &[]) + .unwrap() + .tx_script(send_script.into()) + .extend_expected_output_notes(vec![RawOutputNote::Full(note.clone())]); + execute_tx(&mut chain, send); + + let consume = chain + .build_tx_context(recipient_id, &[note.id()], &[]) + .unwrap() + .foreign_accounts(vec![chain.get_foreign_account_inputs(faucet_id).unwrap()]); + execute_tx(&mut chain, consume); + + assert_account_has_fungible_asset( + chain.committed_account(recipient_id).unwrap(), + faucet_id, + transfer_amount, + ); + (note, recipient_id) +} + +#[test] +fn dex_note_uses_embedded_schema_and_component_codec() { + let note_package = build_dex_note_package(); + assert_package_section(¬e_package, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID); + assert_package_section(¬e_package, PACKAGE_NOTE_CODEC_SECTION_ID); + let schema = NoteStorageSchema::from_package(¬e_package).unwrap(); + let codecs = CodecRegistry::load_from_package(¬e_package).unwrap(); + + let (note, recipient_id) = transfer_with_storage(Arc::clone(¬e_package), |recipient_id| { + schema + .builder_with_registry(&codecs) + .set("target", recipient_id.to_bech32(NetworkId::Mainnet)) + .unwrap() + .set("price", "1.5") + .unwrap() + .build() + .unwrap() + }); + + let decoded = schema.decode_with_registry(note.storage(), &codecs).unwrap(); + assert_eq!( + decoded.field("target").unwrap().to_string(), + recipient_id.to_bech32(NetworkId::Mainnet) + ); + assert_eq!(decoded.field("price").unwrap().to_string(), "1.5"); +} + +#[test] +fn p2id_note_builds_storage_without_a_component_codec() { + let note_package = compile_rust_package("../../examples/p2id-note", true); + let schema = NoteStorageSchema::from_package(¬e_package).unwrap(); + + let (note, recipient_id) = transfer_with_storage(note_package, |recipient_id| { + schema + .builder() + .set("target-account-id", recipient_id.to_bech32(NetworkId::Mainnet)) + .unwrap() + .build() + .unwrap() + }); + + let decoded = schema.decode(note.storage()).unwrap(); + assert_eq!( + decoded.field("target_account_id").unwrap().to_string(), + recipient_id.to_bech32(NetworkId::Mainnet) + ); +} + +/// Runs cargo-miden so the DEX package receives its codec section. +fn build_dex_note_package() -> Arc { + let root = workspace_root(); + let project = root.join("examples/dex-note"); + let binary = cargo_miden_binary(&root); + let output = Command::new(binary) + .args(["miden", "build", "--release"]) + .current_dir(&project) + .output() + .expect("failed to start cargo miden for dex-note"); + assert_command_succeeded("cargo miden build for dex-note", &output); + + Arc::new( + Package::deserialize_from_file(project.join("target/miden/release/dex-note.masp")) + .expect("failed to read the cargo-miden DEX package"), + ) +} + +/// Returns the cargo-miden binary built by the workspace test workflow. +fn cargo_miden_binary(root: &Path) -> PathBuf { + let candidates = [root.join("target/debug/cargo-miden"), root.join("bin/cargo-miden")]; + candidates.into_iter().find(|candidate| candidate.is_file()).unwrap_or_else(|| { + panic!("cargo-miden is not built; run `cargo build -p cargo-miden` before this test") + }) +} + +/// Returns the compiler workspace root. +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("integration-network must be under tests/") + .to_owned() +} + +/// Asserts that a package carries one named custom section. +fn assert_package_section(package: &Package, name: &str) { + let id = SectionId::custom(name).unwrap(); + assert!( + package.sections.iter().any(|section| section.id == id), + "package does not contain the `{name}` section" + ); +} + +/// Includes both output streams when a child process fails. +fn assert_command_succeeded(action: &str, output: &Output) { + assert!( + output.status.success(), + "{action} failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} diff --git a/tests/integration-network/src/mockchain/support/helpers.rs b/tests/integration-network/src/mockchain/support/helpers.rs index 9d058d6354..7e6b70a0ca 100644 --- a/tests/integration-network/src/mockchain/support/helpers.rs +++ b/tests/integration-network/src/mockchain/support/helpers.rs @@ -17,6 +17,7 @@ use miden_client::{ use miden_core::Felt; use miden_field_repr::{FromFeltRepr, ToFeltRepr}; use miden_mast_package::{Package, TargetType}; +use miden_note_schema::NoteStorageSchema; use miden_protocol::{ account::{ Account, AccountBuilder, AccountComponent, AccountId, AccountStorage, AccountType, @@ -288,10 +289,18 @@ pub(crate) fn build_asset_transfer_tx( let faucet_id = asset.faucet_id(); let asset: Asset = asset.into(); + let schema = NoteStorageSchema::from_package(&p2id_note_package) + .expect("p2id note package should contain a storage schema"); + let note_storage = schema + .builder() + .set("target-account-id", recipient_id.to_hex()) + .expect("recipient account ID should match the p2id schema") + .build() + .expect("p2id schema should produce valid note storage"); let output_note = NoteBuilder::new(sender_id, rng) .serial_number(serial_num) .package((*p2id_note_package).clone()) - .note_storage(to_core_felts(&recipient_id)) + .note_storage(note_storage.to_elements()) .unwrap() .add_assets([asset]) .tag(0) diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index a3944ce901..b8be07d7ed 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -50,3 +50,5 @@ concat-idents = "1.1" libloading = "0.8" wasmi = "1.1.0" wat.workspace = true +wit-bindgen-core = "0.57" +midenc-frontend-wasm-metadata.workspace = true diff --git a/tests/integration/src/end_to_end/examples/mod.rs b/tests/integration/src/end_to_end/examples/mod.rs index 2ff4a3adce..a0ea501de6 100644 --- a/tests/integration/src/end_to_end/examples/mod.rs +++ b/tests/integration/src/end_to_end/examples/mod.rs @@ -7,4 +7,5 @@ mod counter_metadata; mod counter_note; mod fibonacci; mod is_prime; +mod note_schema_metadata; mod storage_metadata; diff --git a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs new file mode 100644 index 0000000000..4af7f99544 --- /dev/null +++ b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs @@ -0,0 +1,394 @@ +//! Integration tests for note storage schemas stored in Miden packages. + +use std::sync::Arc; + +use miden_mast_package::{Package, SectionId}; +use midenc_expect_test::{Expect, expect}; +use midenc_frontend_wasm::WasmTranslationConfig; +use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; +use wit_bindgen_core::wit_parser::{Resolve, Type as WitType, TypeDefKind}; + +use super::persist_cargo_miden_dependency; +use crate::CompilerTest; + +/// Disables debug output so compiled package content is stable. +fn no_debug_flags() -> [String; 2] { + ["--debug".to_string(), "none".to_string()] +} + +/// Compiles one project with the Cargo Miden frontend. +fn compile_project(project_path: &str) -> Arc { + let mut test = CompilerTest::rust_source_cargo_miden( + project_path, + WasmTranslationConfig::default(), + no_debug_flags(), + ); + test.compile_package() +} + +/// Returns the unpadded note storage schema text from a package. +fn note_storage_schema(package: &Package) -> &str { + let section_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID) + .expect("schema section id must be valid"); + let bytes = package + .sections + .iter() + .find(|section| section.id == section_id) + .expect("package must contain a note storage schema") + .data + .as_ref(); + assert_eq!(bytes.len() % 16, 0, "schema payload must use 16-byte padding"); + let len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); + str::from_utf8(&bytes[..len]).expect("note storage schema must be UTF-8") +} + +/// Checks a schema golden and resolves its root storage alias with wit-parser. +fn assert_note_storage_schema(package: &Package, expected_root: &str, expected: Expect) { + let source = note_storage_schema(package); + expected.assert_eq(source); + + let mut resolve = Resolve::default(); + let package_id = resolve + .push_str("note-schema.wit", source) + .expect("note storage schema must resolve"); + let schema_package = &resolve.packages[package_id]; + let interface_id = schema_package.interfaces["note-storage"]; + let interface = &resolve.interfaces[interface_id]; + let storage_id = interface.types["storage"]; + let TypeDefKind::Type(WitType::Id(root_id)) = resolve.types[storage_id].kind else { + panic!("storage must be a named type alias"); + }; + assert_eq!(resolve.types[root_id].name.as_deref(), Some(expected_root)); +} + +#[test] +fn note_packages_carry_resolvable_storage_schema_metadata() { + let wallet = compile_project("../../examples/basic-wallet"); + persist_cargo_miden_dependency("../../examples/basic-wallet", wallet.as_ref()); + + let p2id = compile_project("../../examples/p2id-note"); + assert_note_storage_schema( + &p2id, + "p2id-note", + expect![[r#" + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. + + package miden:p2id-schema@0.1.0; + + use miden:base/core-types@1.0.0; + + interface note-storage { + use core-types.{account-id}; + + record p2id-note { + target-account-id: account-id, + } + + type storage = p2id-note; + } + + package miden:base@1.0.0 { + interface core-types { + /// Represents an on-chain felt. + /// + /// Field modulus M = 2^64 - 2^32 + 1. + record felt { + /// The backing type is `f32` which will be treated as a felt by the compiler. + /// We're basically hijacking the Wasm `f32` type and treat as felt. + inner: f32, + } + + + /// A group of four field elements in the Miden base field. + record word { + a: felt, + b: felt, + c: felt, + d: felt, + } + + /// A cryptographic digest representing a 256-bit hash value. + /// This is a wrapper around `word` which contains 4 field elements. + record digest { + inner: word + } + + /// Unique identifier of an account. + /// + /// # Layout + /// + /// An `AccountId` consists of two field elements, where the first is called the prefix and the + /// second is called the suffix. It is laid out as follows: + /// + /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] + /// suffix: [zero bit | hash (55 bits) | 8 zero bits] + record account-id { + prefix: felt, + suffix: felt + } + + /// Creates a new account ID from a field element. + //account-id-from-felt: func(felt: felt) -> account-id; + + /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) + record recipient { + inner: word + } + + record tag { + inner: felt + } + + /// A fungible or a non-fungible asset. + /// + /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. + /// + /// The methodology for constructing fungible and non-fungible assets is described below. + /// + /// # Fungible assets + /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `[amount, 0, 0, 0]` + /// + /// # Non-fungible assets + /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `DATA_HASH` + record asset { + key: word, + value: word, + } + + /// A validated fungible asset amount, at most 2^63 - 2^31. + record asset-amount { + inner: felt + } + + /// Account nonce + record nonce { + inner: felt + } + + /// A block height in the chain + record block-number { + inner: felt + } + + /// Account hash + record account-hash { + inner: word + } + + /// Block hash + record block-hash { + inner: word + } + + /// Storage value + record storage-value { + inner: word + } + + /// Account storage root + record storage-root { + inner: word + } + + /// Account code root + record account-code-root { + inner: word + } + + /// Commitment to the account vault + record vault-commitment { + inner: word + } + + /// An index of the created note + record note-idx { + inner: felt + } + + record note-type { + inner: felt + } + + record note-execution-hint { + inner: felt + } + + } + } + "#]], + ); + + let swapp = compile_project("../fixtures/components/swapp-note"); + assert_note_storage_schema( + &swapp, + "swapp-note", + expect![[r#" + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. + + package miden:swapp-note-schema@0.1.0; + + use miden:base/core-types@1.0.0; + + interface note-storage { + use core-types.{account-id, felt, word}; + + /// SWAPP note storage. + /// + /// The note creator stores the swap terms in the note storage; the fields below are decoded + /// from the storage elements in declaration order. + record swapp-note { + /// Vault key identifying the requested asset (faucet id, composition, callback flags). + requested-asset-key: word, + /// Total requested asset amount for the full offer. + requested-total: felt, + /// The account that created the swap offer and receives the requested asset. + creator: account-id, + /// Note type used for the notes created by this script (P2ID routing note and remainder + /// SWAPP note). + output-note-type: felt, + /// Tag routing the P2ID note to the creator. + p2id-tag: felt, + /// Script root of the P2ID note script used for the routing note. + p2id-script-root: word, + } + + type storage = swapp-note; + } + + package miden:base@1.0.0 { + interface core-types { + /// Represents an on-chain felt. + /// + /// Field modulus M = 2^64 - 2^32 + 1. + record felt { + /// The backing type is `f32` which will be treated as a felt by the compiler. + /// We're basically hijacking the Wasm `f32` type and treat as felt. + inner: f32, + } + + + /// A group of four field elements in the Miden base field. + record word { + a: felt, + b: felt, + c: felt, + d: felt, + } + + /// A cryptographic digest representing a 256-bit hash value. + /// This is a wrapper around `word` which contains 4 field elements. + record digest { + inner: word + } + + /// Unique identifier of an account. + /// + /// # Layout + /// + /// An `AccountId` consists of two field elements, where the first is called the prefix and the + /// second is called the suffix. It is laid out as follows: + /// + /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] + /// suffix: [zero bit | hash (55 bits) | 8 zero bits] + record account-id { + prefix: felt, + suffix: felt + } + + /// Creates a new account ID from a field element. + //account-id-from-felt: func(felt: felt) -> account-id; + + /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) + record recipient { + inner: word + } + + record tag { + inner: felt + } + + /// A fungible or a non-fungible asset. + /// + /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. + /// + /// The methodology for constructing fungible and non-fungible assets is described below. + /// + /// # Fungible assets + /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `[amount, 0, 0, 0]` + /// + /// # Non-fungible assets + /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `DATA_HASH` + record asset { + key: word, + value: word, + } + + /// A validated fungible asset amount, at most 2^63 - 2^31. + record asset-amount { + inner: felt + } + + /// Account nonce + record nonce { + inner: felt + } + + /// A block height in the chain + record block-number { + inner: felt + } + + /// Account hash + record account-hash { + inner: word + } + + /// Block hash + record block-hash { + inner: word + } + + /// Storage value + record storage-value { + inner: word + } + + /// Account storage root + record storage-root { + inner: word + } + + /// Account code root + record account-code-root { + inner: word + } + + /// Commitment to the account vault + record vault-commitment { + inner: word + } + + /// An index of the created note + record note-idx { + inner: felt + } + + record note-type { + inner: felt + } + + record note-execution-hint { + inner: felt + } + + } + } + "#]], + ); +} diff --git a/tools/cargo-miden/Cargo.toml b/tools/cargo-miden/Cargo.toml index da30d8303b..6b097e5594 100644 --- a/tools/cargo-miden/Cargo.toml +++ b/tools/cargo-miden/Cargo.toml @@ -40,7 +40,10 @@ path = "tests/mod.rs" flate2.workspace = true [dependencies] +miden-mast-package = { workspace = true, features = ["std"] } +miden-project = { workspace = true, features = ["std", "serde"] } midenc-compile = { workspace = true, features = ["std"] } +midenc-frontend-wasm-metadata.workspace = true midenc-hir = { workspace = true, features = ["std"] } midenc-session.workspace = true midenc-log.workspace = true @@ -57,3 +60,5 @@ walkdir = "2.5" serde_json.workspace = true # Verifies a downloaded template bundle against the digest GitHub reports. sha2.workspace = true +wit-component.workspace = true +wit-parser.workspace = true diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs new file mode 100644 index 0000000000..dcf1c6252c --- /dev/null +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -0,0 +1,92 @@ +//! Integration test for DEX note schema and codec package sections. + +use std::env; + +use cargo_miden::run; +use miden_mast_package::{Package, SectionId}; +use midenc_frontend_wasm_metadata::{ + PACKAGE_NOTE_CODEC_SECTION_ID, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, +}; +use wit_component::DecodedWasm; +use wit_parser::WorldItem; + +use crate::utils::{current_dir_lock, workspace_root}; + +#[test] +fn dex_note_build_embeds_schema_and_zero_import_codec_component() { + let _cwd_lock = current_dir_lock(); + let _ = midenc_log::Builder::from_env("MIDENC_TRACE") + .is_test(true) + .format_timestamp(None) + .try_init(); + + let restore_target_dir = env::var_os("CARGO_TARGET_DIR"); + unsafe { + env::remove_var("CARGO_TARGET_DIR"); + } + + let note_dir = workspace_root().join("examples/dex-note"); + env::set_current_dir(¬e_dir).unwrap(); + let result = run(["cargo", "miden", "build", "--release"].into_iter().map(str::to_owned)); + + match restore_target_dir { + Some(value) => unsafe { env::set_var("CARGO_TARGET_DIR", value) }, + None => unsafe { env::remove_var("CARGO_TARGET_DIR") }, + } + + let output = result + .expect("cargo miden build for dex-note failed") + .expect("expected BuildCommandOutput") + .unwrap_build_output(); + assert_eq!(output.len(), 1, "expected one dex-note package artifact, got {output:?}"); + let package = Package::deserialize_from_file(&output[0]) + .expect("failed to read the built dex-note package"); + + let schema_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(); + assert!( + package.sections.iter().any(|section| section.id == schema_id), + "dex-note package has no note storage schema section" + ); + + let codec_id = SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID).unwrap(); + let codec = package + .sections + .iter() + .find(|section| section.id == codec_id) + .expect("dex-note package has no note codec section"); + assert_note_codec_component(codec.data.as_ref()); +} + +/// Verifies the sandbox and versioned interface exported by a note codec component. +fn assert_note_codec_component(component: &[u8]) { + let DecodedWasm::Component(resolve, world_id) = + wit_component::decode(component).expect("note codec section is not valid component bytes") + else { + panic!("note codec section is not a component"); + }; + let world = &resolve.worlds[world_id]; + assert!(world.imports.is_empty(), "note codec imports: {:#?}", world.imports); + assert_eq!(world.exports.len(), 1, "unexpected note codec exports: {:#?}", world.exports); + + let interface = world + .exports + .values() + .find_map(|item| { + let WorldItem::Interface { id, .. } = item else { + return None; + }; + let interface = &resolve.interfaces[*id]; + let package_id = interface.package?; + let package = &resolve.packages[package_id].name; + (interface.name.as_deref() == Some("codec") + && package.namespace == "miden" + && package.name == "note-codec" + && package.version.as_ref().is_some_and(|version| version.to_string() == "1.0.0")) + .then_some(interface) + }) + .expect("component does not export `miden:note-codec/codec@1.0.0`"); + assert_eq!( + interface.functions.keys().map(String::as_str).collect::>(), + ["supported-types", "parse", "display", "validate"] + ); +} diff --git a/tools/cargo-miden/tests/mod.rs b/tools/cargo-miden/tests/mod.rs index 7f555af08a..fa5adc5e1d 100755 --- a/tools/cargo-miden/tests/mod.rs +++ b/tools/cargo-miden/tests/mod.rs @@ -1,3 +1,4 @@ +mod dex_note_codec_build; mod masm_dependency; mod p2id_cargo_miden_build; mod target_dir; From 6e8a034704596b7a3dee698f604ebc5597d2f7f8 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Thu, 6 Aug 2026 17:47:47 +0300 Subject: [PATCH 02/43] refactor: move note codec orchestration from cargo-miden into midenc-compile cargo-miden is a thin wrapper around the compiler; the note codec orchestration added with the schema work belongs to the compile pipeline, where package post-processing already lives. Owning it in midenc-compile also means every compilation path that builds a note project with a codec pointer produces the `note_codec` section, not only `cargo miden build`. Move the whole orchestration (manifest pointer lookup, package staging for `from_project!`, sibling codec crate build, component encoding and zero-import validation, section bytes) into `build_project_note_codec` in midenc-compile's cargo module, attach the section from `post_process_package`, and restore `BuildCommand::exec` to its wrapper shape. Runtime dependencies move to midenc-compile; the component-validation dependencies remaining in cargo-miden are dev-only, used by its end-to-end test. The produced codec section is byte-identical to the previous implementation. --- Cargo.lock | 3 +- midenc-compile/Cargo.toml | 4 + midenc-compile/src/cargo.rs | 308 ++++++++++++++++++ midenc-compile/src/pipeline/assembly.rs | 37 ++- midenc-compile/src/pipeline/backend.rs | 3 +- midenc-compile/src/pipeline/frontends/hir.rs | 3 +- midenc-compile/src/pipeline/frontends/rust.rs | 3 +- midenc-compile/src/pipeline/frontends/wasm.rs | 3 +- midenc-compile/src/pipeline/seed.rs | 6 +- tools/cargo-miden/Cargo.toml | 5 +- 10 files changed, 355 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e6205b05b..2a87972e7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,7 +834,6 @@ dependencies = [ "liquid-core", "log", "miden-mast-package", - "miden-project", "midenc-compile", "midenc-frontend-wasm-metadata", "midenc-hir", @@ -4157,6 +4156,8 @@ dependencies = [ "midenc-session", "toml_edit", "wat", + "wit-component", + "wit-parser 0.247.0", ] [[package]] diff --git a/midenc-compile/Cargo.toml b/midenc-compile/Cargo.toml index 95c03398ff..77ff3ec8c7 100644 --- a/midenc-compile/Cargo.toml +++ b/midenc-compile/Cargo.toml @@ -27,6 +27,8 @@ std = [ "dep:clap", "dep:toml_edit", "dep:wat", + "dep:wit-component", + "dep:wit-parser", ] [dependencies] @@ -49,3 +51,5 @@ midenc-session.workspace = true toml_edit = { workspace = true, optional = true, features = ["parse", "display"] } thiserror.workspace = true wat = { workspace = true, optional = true } +wit-component = { workspace = true, optional = true } +wit-parser = { workspace = true, optional = true } diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 909ce0bdbc..8bc37bf6ff 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -1,7 +1,9 @@ use core::str::FromStr; use std::{ boxed::Box, + env, fs, path::{Path, PathBuf}, + process::Command, rc::Rc, string::{String, ToString}, sync::Arc, @@ -9,11 +11,23 @@ use std::{ }; use miden_assembly::SourceManager; +use miden_mast_package::Package as MastPackage; use midenc_hir::Report; use midenc_session::{InputFile, LinkLibrary, Session, miden_project}; +use wit_component::{ComponentEncoder, DecodedWasm}; +use wit_parser::WorldItem; use crate::{CodegenOutput, CompilerResult}; +/// Metadata table that points to an author-side note codec crate. +const NOTE_CODEC_CRATE_METADATA: &str = "note-codec-crate"; + +/// Metadata field that contains the codec crate directory. +const NOTE_CODEC_CRATE_PATH: &str = "path"; + +/// Rust target used for zero-import note codec components. +const NOTE_CODEC_TARGET: &str = "wasm32-unknown-unknown"; + /// Cargo-specific options extracted from the `Compiler` struct. /// /// These options are recognized by `cargo miden build` and forwarded to the underlying @@ -311,6 +325,300 @@ pub fn write_package_atomic( }) } +/// Builds the optional note codec component declared by a project package. +pub(crate) fn build_project_note_codec( + project_package: &miden_project::Package, + project_manifest_path: &Path, + note_project_dir: &Path, + note_package: &MastPackage, +) -> CompilerResult>> { + let Some(codec_crate_dir) = + note_codec_crate_dir(project_package.metadata(), project_manifest_path, note_project_dir)? + else { + return Ok(None); + }; + + build_note_codec_component(&codec_crate_dir, note_project_dir, note_package).map(Some) +} + +/// Reads the optional codec crate path from Miden project metadata. +fn note_codec_crate_dir( + metadata: &miden_project::MetadataSet, + project_manifest_path: &Path, + project_dir: &Path, +) -> CompilerResult> { + let Some(codec_metadata) = metadata.get(NOTE_CODEC_CRATE_METADATA) else { + return Ok(None); + }; + let path = codec_metadata + .get(NOTE_CODEC_CRATE_PATH) + .ok_or_else(|| { + Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_CRATE_METADATA}]` in '{}' must define a string \ + `{NOTE_CODEC_CRATE_PATH}`", + project_manifest_path.display() + )) + })? + .inner() + .as_str() + .ok_or_else(|| { + Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_CRATE_METADATA}].{NOTE_CODEC_CRATE_PATH}` in '{}' \ + must be a string", + project_manifest_path.display() + )) + })?; + if path.is_empty() { + return Err(Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_CRATE_METADATA}].{NOTE_CODEC_CRATE_PATH}` in '{}' \ + must not be empty", + project_manifest_path.display() + ))); + } + + let codec_crate_dir = project_dir.join(path); + let codec_crate_dir = codec_crate_dir.canonicalize().map_err(|error| { + Report::msg(format!( + "note codec crate '{}' does not exist; update \ + `[package.metadata.{NOTE_CODEC_CRATE_METADATA}].{NOTE_CODEC_CRATE_PATH}` in '{}': \ + {error}", + codec_crate_dir.display(), + project_manifest_path.display() + )) + })?; + if !codec_crate_dir.is_dir() { + return Err(Report::msg(format!( + "note codec crate path '{}' is not a directory", + codec_crate_dir.display() + ))); + } + Ok(Some(codec_crate_dir)) +} + +/// Builds and componentizes one author-side note codec crate. +fn build_note_codec_component( + codec_crate_dir: &Path, + note_project_dir: &Path, + note_package: &MastPackage, +) -> CompilerResult> { + let _staged_package = stage_note_package(note_project_dir, note_package)?; + let manifest_path = codec_crate_dir.join("Cargo.toml"); + let artifact_name = codec_artifact_name(&manifest_path)?; + let target_dir = codec_crate_dir.join("target"); + let wasm_path = target_dir + .join(NOTE_CODEC_TARGET) + .join("release") + .join(artifact_name) + .with_extension("wasm"); + // Cargo does not track a package file that a procedural macro reads. Remove the final output + // so the codec crate expands against the package staged above. + match fs::remove_file(&wasm_path) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => { + return Err(Report::msg(format!( + "failed to prepare note codec output '{}': {error}", + wasm_path.display() + ))); + } + } + let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + let output = Command::new(cargo) + .current_dir(codec_crate_dir) + .args([ + "build", + "--manifest-path", + manifest_path.to_str().ok_or_else(|| { + Report::msg(format!( + "codec manifest path '{}' is not UTF-8", + manifest_path.display() + )) + })?, + "--lib", + "--release", + "--target", + NOTE_CODEC_TARGET, + "--target-dir", + target_dir.to_str().ok_or_else(|| { + Report::msg(format!("codec target path '{}' is not UTF-8", target_dir.display())) + })?, + ]) + .env_remove("CARGO_BUILD_TARGET") + .env_remove("CARGO_ENCODED_RUSTFLAGS") + .env_remove("RUSTFLAGS") + .output() + .map_err(|error| { + Report::msg(format!( + "failed to start `cargo build` for note codec crate '{}': {error}", + codec_crate_dir.display() + )) + })?; + if !output.status.success() { + return Err(Report::msg(format!( + "failed to build note codec crate '{}' for {NOTE_CODEC_TARGET} in release mode \ + (install the target with `rustup target add {NOTE_CODEC_TARGET}` if \ + needed)\nstdout:\n{}\nstderr:\n{}", + codec_crate_dir.display(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ))); + } + + let module = fs::read(&wasm_path).map_err(|error| { + Report::msg(format!( + "note codec build succeeded but did not produce the expected cdylib '{}': {error}", + wasm_path.display() + )) + })?; + let component = ComponentEncoder::default() + .module(&module) + .map_err(|error| { + Report::msg(format!( + "note codec module '{}' is not component-ready: {error}", + wasm_path.display() + )) + })? + .validate(true) + .encode() + .map_err(|error| { + Report::msg(format!( + "failed to encode note codec module '{}' as a component: {error}", + wasm_path.display() + )) + })?; + validate_note_codec_component(&component).map_err(|error| { + Report::msg(format!( + "note codec crate '{}' produced an invalid component: {error}", + codec_crate_dir.display() + )) + })?; + Ok(component) +} + +/// Stages the current in-memory note package for `from_project!` during the codec build. +fn stage_note_package( + note_project_dir: &Path, + note_package: &MastPackage, +) -> CompilerResult { + let profiles_dir = note_project_dir.join("target/miden"); + fs::create_dir_all(&profiles_dir).map_err(|error| { + Report::msg(format!( + "failed to create note package staging root '{}': {error}", + profiles_dir.display() + )) + })?; + let staging_dir = tempfile::Builder::new() + .prefix("zz-note-codec-input-") + .tempdir_in(&profiles_dir) + .map_err(|error| { + Report::msg(format!( + "failed to create note package staging directory in '{}': {error}", + profiles_dir.display() + )) + })?; + note_package.write_masp_file(staging_dir.path()).map_err(|error| { + Report::msg(format!( + "failed to stage note package {}@{} for codec generation: {error}", + note_package.name, note_package.version + )) + })?; + Ok(staging_dir) +} + +/// Returns the expected Wasm artifact name and checks the cdylib configuration. +fn codec_artifact_name(manifest_path: &Path) -> CompilerResult { + let source = fs::read_to_string(manifest_path).map_err(|error| { + Report::msg(format!( + "note codec crate has no readable manifest at '{}': {error}", + manifest_path.display() + )) + })?; + let manifest = source.parse::().map_err(|error| { + Report::msg(format!( + "failed to parse note codec manifest '{}': {error}", + manifest_path.display() + )) + })?; + let package = + manifest + .get("package") + .and_then(toml_edit::Item::as_table_like) + .ok_or_else(|| { + Report::msg(format!( + "codec manifest '{}' has no `[package]` table", + manifest_path.display() + )) + })?; + let package_name = package.get("name").and_then(toml_edit::Item::as_str).ok_or_else(|| { + Report::msg(format!("codec manifest '{}' has no package name", manifest_path.display())) + })?; + let lib = manifest.get("lib").and_then(toml_edit::Item::as_table_like).ok_or_else(|| { + Report::msg(format!("codec manifest '{}' has no `[lib]` table", manifest_path.display())) + })?; + let is_cdylib = + lib.get("crate-type") + .and_then(toml_edit::Item::as_array) + .is_some_and(|crate_types| { + crate_types.iter().any(|crate_type| crate_type.as_str() == Some("cdylib")) + }); + if !is_cdylib { + return Err(Report::msg(format!( + "note codec manifest '{}' must set `[lib] crate-type = [\"cdylib\"]`", + manifest_path.display() + ))); + } + let lib_name = lib.get("name").and_then(toml_edit::Item::as_str).unwrap_or(package_name); + Ok(lib_name.replace('-', "_")) +} + +/// Verifies the component sandbox and the versioned codec interface export. +fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { + let DecodedWasm::Component(resolve, world_id) = + wit_component::decode(component).map_err(|error| { + Report::msg(format!("failed to decode the encoded note codec component: {error}")) + })? + else { + return Err(Report::msg("note codec output is not a component")); + }; + let world = &resolve.worlds[world_id]; + if !world.imports.is_empty() { + return Err(Report::msg(format!( + "note codec component must have zero imports, found: {:#?}", + world.imports + ))); + } + if world.exports.len() != 1 { + return Err(Report::msg(format!( + "note codec component must export only `miden:note-codec/codec@1.0.0`, found: {:#?}", + world.exports + ))); + } + + let interface = world.exports.values().find_map(|item| { + let WorldItem::Interface { id, .. } = item else { + return None; + }; + let interface = &resolve.interfaces[*id]; + let package_id = interface.package?; + let package = &resolve.packages[package_id].name; + (interface.name.as_deref() == Some("codec") + && package.namespace == "miden" + && package.name == "note-codec" + && package.version.as_ref().is_some_and(|version| version.to_string() == "1.0.0")) + .then_some(interface) + }); + let interface = interface + .ok_or_else(|| Report::msg("component does not export `miden:note-codec/codec@1.0.0`"))?; + for function in ["supported-types", "parse", "display", "validate"] { + if !interface.functions.contains_key(function) { + return Err(Report::msg(format!( + "`miden:note-codec/codec@1.0.0` is missing `{function}`" + ))); + } + } + Ok(()) +} + /// Parse `cargo -Zscript`-style frontmatter from a given input string, if present. /// /// Returns `Ok(None)` if the input does not define Cargo frontmatter. diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index 489bf1df70..750f995b25 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -2,7 +2,8 @@ //! //! [`prepare_assembler`] runs before assembly, applying the session's link inputs; //! [`post_process_package`] runs after it, attaching to the assembled package the sections -//! and advice-map entries that codegen produced but the assembler knows nothing about. +//! and advice-map entries that codegen produced but the assembler knows nothing about. It also +//! builds and attaches an author codec declared by project metadata. //! //! Both are shared: the [`Pipeline`](super::Pipeline) driver prepares its own assembler //! through the first, and every frontend that lowers HIR post-processes through the second — @@ -16,6 +17,7 @@ use alloc::vec::Vec; +use miden_assembly::TargetAssemblyContext; use miden_mast_package::Package; use midenc_codegen_masm::{MasmComponent, intrinsics}; use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; @@ -64,8 +66,7 @@ pub(crate) fn post_process_package( package: &mut Package, component: &MasmComponent, sections: &midenc_frontend_wasm_metadata::PackageSections, - target: &midenc_session::miden_project::Target, - registry: &dyn miden_package_registry::PackageRegistryAndProvider, + context: &TargetAssemblyContext<'_>, ) -> Result<(), Report> { use miden_assembly::serde::Serializable; use miden_mast_package::{Section, SectionId}; @@ -77,7 +78,7 @@ pub(crate) fn post_process_package( extend_rodata_advice_map(package, &component.rodata); // Embed the kernel in note/transaction script packages, if not already embedded - if matches!(target.ty, TargetType::Note | TargetType::TransactionScript) + if matches!(context.target.ty, TargetType::Note | TargetType::TransactionScript) && !package.sections.iter().any(|section| section.id == SectionId::KERNEL) && let Ok(Some(kernel_dep)) = package.kernel_runtime_dependency() { @@ -85,12 +86,38 @@ pub(crate) fn post_process_package( kernel_dep.version().clone(), kernel_dep.digest, ); - let kernel_package = registry.load_package(kernel_dep.id(), &version)?; + let kernel_package = context.package_registry.load_package(kernel_dep.id(), &version)?; package .sections .push(Section::new(SectionId::KERNEL, kernel_package.to_bytes())); } + attach_note_codec(package, context)?; + + Ok(()) +} + +/// Build and attach the note codec declared by the current project package. +fn attach_note_codec( + package: &mut Package, + context: &TargetAssemblyContext<'_>, +) -> Result<(), Report> { + let Some(component) = crate::cargo::build_project_note_codec( + context.package.as_ref(), + context.manifest_path, + context.project_root.as_ref(), + package, + )? + else { + return Ok(()); + }; + + use miden_mast_package::{Section, SectionId}; + let section_id = + SectionId::custom(midenc_frontend_wasm_metadata::PACKAGE_NOTE_CODEC_SECTION_ID).map_err( + |error| Report::msg(format!("the note codec package section id is invalid: {error}")), + )?; + package.sections.push(Section::new(section_id, component)); Ok(()) } diff --git a/midenc-compile/src/pipeline/backend.rs b/midenc-compile/src/pipeline/backend.rs index f76640a925..e22a5c123e 100644 --- a/midenc-compile/src/pipeline/backend.rs +++ b/midenc-compile/src/pipeline/backend.rs @@ -831,8 +831,7 @@ mod tests { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), ) } diff --git a/midenc-compile/src/pipeline/frontends/hir.rs b/midenc-compile/src/pipeline/frontends/hir.rs index 0942260190..0b62e5a1ad 100644 --- a/midenc-compile/src/pipeline/frontends/hir.rs +++ b/midenc-compile/src/pipeline/frontends/hir.rs @@ -393,8 +393,7 @@ impl Frontend for HirFrontend { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), ) } diff --git a/midenc-compile/src/pipeline/frontends/rust.rs b/midenc-compile/src/pipeline/frontends/rust.rs index efceda88c8..2c6fa6af94 100644 --- a/midenc-compile/src/pipeline/frontends/rust.rs +++ b/midenc-compile/src/pipeline/frontends/rust.rs @@ -1632,8 +1632,7 @@ impl Frontend for RustProjectFrontend { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), ) } } diff --git a/midenc-compile/src/pipeline/frontends/wasm.rs b/midenc-compile/src/pipeline/frontends/wasm.rs index af3d2bd7a6..9a5fc48fd7 100644 --- a/midenc-compile/src/pipeline/frontends/wasm.rs +++ b/midenc-compile/src/pipeline/frontends/wasm.rs @@ -591,8 +591,7 @@ impl Frontend for WasmFrontend { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), ) } diff --git a/midenc-compile/src/pipeline/seed.rs b/midenc-compile/src/pipeline/seed.rs index afed7bedb0..f653af7cd7 100644 --- a/midenc-compile/src/pipeline/seed.rs +++ b/midenc-compile/src/pipeline/seed.rs @@ -438,8 +438,7 @@ impl Frontend for SeedFrontend { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), ) } @@ -605,8 +604,7 @@ mod tests { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), ) } diff --git a/tools/cargo-miden/Cargo.toml b/tools/cargo-miden/Cargo.toml index 6b097e5594..454cf8c1e3 100644 --- a/tools/cargo-miden/Cargo.toml +++ b/tools/cargo-miden/Cargo.toml @@ -41,9 +41,7 @@ flate2.workspace = true [dependencies] miden-mast-package = { workspace = true, features = ["std"] } -miden-project = { workspace = true, features = ["std", "serde"] } midenc-compile = { workspace = true, features = ["std"] } -midenc-frontend-wasm-metadata.workspace = true midenc-hir = { workspace = true, features = ["std"] } midenc-session.workspace = true midenc-log.workspace = true @@ -60,5 +58,8 @@ walkdir = "2.5" serde_json.workspace = true # Verifies a downloaded template bundle against the digest GitHub reports. sha2.workspace = true + +[dev-dependencies] +midenc-frontend-wasm-metadata.workspace = true wit-component.workspace = true wit-parser.workspace = true From a1684b23567aa17dda59b1b01f657725d2c9a4d7 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Mon, 10 Aug 2026 11:32:06 +0300 Subject: [PATCH 03/43] fix: harden note schema emission, codec builds, and consumer APIs after review A multi-pass review of the schema work surfaced correctness holes, trust-boundary gaps, and API footguns. This addresses the branch-scope findings. Schema emission: a fixed export-name guard symbol enforces the one-`#[note]`-struct-per-crate invariant at link time (two structs previously concatenated into one corrupt section); rendered schemas are resolved with wit-parser during expansion so unsupported shapes fail at the field that causes them; the accepted type surface now matches what the host reader implements; multiline doc comments render as separate WIT doc lines. The tuple-struct and `Vec` rejections are recorded as breaking changes with migration guidance. Codec builds: the nested build is gated to note targets with focused errors elsewhere, follows the session's profile, toolchain, and target directory, uses the compiler's cargo helpers and JSON artifact paths, stages the in-flight package at a stable path handed to the codec macros through `MIDENC_NOTE_CODEC_PACKAGE_PATH`, and validates the produced component against the pinned world by signatures. Schema and codec sections are attached and read as exactly-one. Codec execution: each codec call runs in a fresh instance with a fuel budget, a memory limiter, and output-size caps; reported type names can no longer shadow standard codecs or reach outside the note's own schema. Consumer surface: artifact discovery lives once in miden-note-schema; codec dispatch works for types containing protocol leaves such as `AccountId`; generated string APIs keep a stable shape as schemas gain nested types; `CodecRegistry::empty`/`with_standard_codecs` replace the ambiguous constructor pair; `miden-note-bindings` is a facade that supplies generated-code dependencies and stays hygienic across multiple expansions, backed by an internal `crate_path` override in the felt-repr derives; embedded core-type definitions are structurally verified before native mappings apply; end-to-end tests exercise the in-process pipeline and inherit workspace patches. --- .github/workflows/ci.yml | 2 +- Cargo.lock | 17 +- Cargo.toml | 3 + midenc-compile/src/cargo.rs | 498 +++++--- midenc-compile/src/pipeline/assembly.rs | 111 +- midenc-compile/src/pipeline/backend.rs | 1 + midenc-compile/src/pipeline/frontends/hir.rs | 1 + midenc-compile/src/pipeline/frontends/rust.rs | 1 + midenc-compile/src/pipeline/frontends/wasm.rs | 1 + midenc-compile/src/pipeline/seed.rs | 2 + sdk/CHANGELOG.md | 15 +- sdk/base-macros/src/note.rs | 15 +- sdk/base-macros/src/note_schema.rs | 417 ++++++- sdk/base-macros/src/types.rs | 16 + sdk/base-macros/src/util.rs | 3 + sdk/base-macros/tests/note_trailing_data.rs | 12 - .../tests/unit_note_trailing_data.rs | 25 + sdk/field-repr/derive/src/lib.rs | 40 +- sdk/note-bindings/Cargo.toml | 17 +- sdk/note-bindings/macros/Cargo.toml | 31 + sdk/note-bindings/macros/src/lib.rs | 319 ++++++ sdk/note-bindings/{ => macros}/src/tests.rs | 36 +- sdk/note-bindings/src/expected/custom.rs | 1008 ++++++++++------- sdk/note-bindings/src/expected/p2id.rs | 770 +++++++------ sdk/note-bindings/src/lib.rs | 363 +----- sdk/note-bindings/tests/generated_custom.rs | 8 +- sdk/note-bindings/tests/p2id_consumer.rs | 71 +- sdk/note-codec/macros/Cargo.toml | 1 - sdk/note-codec/macros/src/artifact.rs | 117 -- sdk/note-codec/macros/src/expand.rs | 97 +- sdk/note-codec/macros/src/lib.rs | 1 - sdk/note-codec/tests/account_id_dispatch.rs | 68 ++ sdk/note-codec/tests/component_export.rs | 46 +- sdk/note-schema/codegen/src/lib.rs | 32 +- sdk/note-schema/codegen/src/tests.rs | 10 +- sdk/note-schema/src/artifact.rs | 249 ++++ sdk/note-schema/src/builder.rs | 51 +- sdk/note-schema/src/codec.rs | 20 +- sdk/note-schema/src/codec_component.rs | 384 +++++-- sdk/note-schema/src/lib.rs | 3 + sdk/note-schema/src/schema.rs | 242 +++- sdk/note-schema/src/section.rs | 23 + sdk/note-schema/src/tests.rs | 78 +- sdk/sdk/MIGRATION.md | 42 + .../src/mockchain/notes/schema.rs | 72 +- .../cargo-miden/tests/dex_note_codec_build.rs | 22 + 46 files changed, 3639 insertions(+), 1722 deletions(-) create mode 100644 sdk/base-macros/tests/unit_note_trailing_data.rs create mode 100644 sdk/note-bindings/macros/Cargo.toml create mode 100644 sdk/note-bindings/macros/src/lib.rs rename sdk/note-bindings/{ => macros}/src/tests.rs (53%) delete mode 100644 sdk/note-codec/macros/src/artifact.rs create mode 100644 sdk/note-codec/tests/account_id_dispatch.rs create mode 100644 sdk/note-schema/src/artifact.rs create mode 100644 sdk/note-schema/src/section.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f68e55fe10..175cf737df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,7 +148,7 @@ jobs: cargo make check --tests - name: Test run: | - cargo make test -E 'not (package(midenc-integration-tests) or package(midenc-integration-network-tests) or package(cargo-miden) or package(midenc-template-tests))' + cargo make test -E 'not (package(midenc-integration-tests) or package(midenc-integration-network-tests) or package(cargo-miden) or package(midenc-template-tests) or test(~p2id_schema_builds_and_decodes_account_id_storage) or test(~generated_p2id_bindings_compile_and_run_in_a_consumer_crate))' - name: Upload test timings and failures if: ${{ always() }} diff --git a/Cargo.lock b/Cargo.lock index 2a87972e7f..9b27b83a27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3614,17 +3614,27 @@ dependencies = [ "miden-field", "miden-field-repr", "miden-mast-package", + "miden-note-bindings-macros", "miden-note-schema", - "miden-note-schema-codegen", "miden-protocol", - "midenc-expect-test", "midenc-frontend-wasm", + "midenc-frontend-wasm-metadata", "midenc-integration-test-support", + "tempfile", +] + +[[package]] +name = "miden-note-bindings-macros" +version = "0.14.0" +dependencies = [ + "miden-note-schema", + "miden-note-schema-codegen", + "midenc-expect-test", "prettyplease", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.119", - "tempfile", ] [[package]] @@ -3646,7 +3656,6 @@ name = "miden-note-codec-macros" version = "0.14.0" dependencies = [ "heck", - "miden-mast-package", "miden-note-schema", "miden-note-schema-codegen", "prettyplease", diff --git a/Cargo.toml b/Cargo.toml index f1f86e87f9..6ccbb35df3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "sdk/base-sys", "sdk/build-script-support", "sdk/note-bindings", + "sdk/note-bindings/macros", "sdk/note-codec", "sdk/note-codec/macros", "sdk/note-schema", @@ -197,6 +198,8 @@ midenc-integration-test-support = { path = "tests/support" } midenc-expect-test = { path = "tools/expect-test" } miden-base-sys = { version = "0.14.0", path = "sdk/base-sys" } miden-field-repr = { version = "0.14.0", path = "sdk/field-repr/repr" } +miden-note-bindings = { version = "0.14.0", path = "sdk/note-bindings" } +miden-note-bindings-macros = { version = "0.14.0", path = "sdk/note-bindings/macros" } miden-note-codec = { version = "0.14.0", path = "sdk/note-codec" } miden-note-codec-macros = { version = "0.14.0", path = "sdk/note-codec/macros" } miden-note-schema = { version = "0.14.0", path = "sdk/note-schema" } diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 8bc37bf6ff..7034ac286c 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -3,7 +3,7 @@ use std::{ boxed::Box, env, fs, path::{Path, PathBuf}, - process::Command, + process::{Command, Stdio}, rc::Rc, string::{String, ToString}, sync::Arc, @@ -15,7 +15,7 @@ use miden_mast_package::Package as MastPackage; use midenc_hir::Report; use midenc_session::{InputFile, LinkLibrary, Session, miden_project}; use wit_component::{ComponentEncoder, DecodedWasm}; -use wit_parser::WorldItem; +use wit_parser::{Function, FunctionKind, Resolve, Type, TypeDefKind, TypeId, WorldId, WorldItem}; use crate::{CodegenOutput, CompilerResult}; @@ -28,6 +28,12 @@ const NOTE_CODEC_CRATE_PATH: &str = "path"; /// Rust target used for zero-import note codec components. const NOTE_CODEC_TARGET: &str = "wasm32-unknown-unknown"; +/// Compiler-provided path to the exact package consumed by `from_project!`. +const NOTE_CODEC_PACKAGE_PATH_ENV: &str = "MIDENC_NOTE_CODEC_PACKAGE_PATH"; + +/// Pinned author codec interface used to validate component signatures. +const NOTE_CODEC_WIT: &str = include_str!("../../sdk/note-codec/wit/note-codec.wit"); + /// Cargo-specific options extracted from the `Compiler` struct. /// /// These options are recognized by `cargo miden build` and forwarded to the underlying @@ -325,12 +331,19 @@ pub fn write_package_atomic( }) } + +/// Returns true when project metadata declares an author-side note codec crate. +pub(crate) fn has_project_note_codec(metadata: &miden_project::MetadataSet) -> bool { + metadata.get(NOTE_CODEC_CRATE_METADATA).is_some() +} + /// Builds the optional note codec component declared by a project package. pub(crate) fn build_project_note_codec( project_package: &miden_project::Package, project_manifest_path: &Path, note_project_dir: &Path, note_package: &MastPackage, + session: &Session, ) -> CompilerResult>> { let Some(codec_crate_dir) = note_codec_crate_dir(project_package.metadata(), project_manifest_path, note_project_dir)? @@ -338,7 +351,7 @@ pub(crate) fn build_project_note_codec( return Ok(None); }; - build_note_codec_component(&codec_crate_dir, note_project_dir, note_package).map(Some) + build_note_codec_component(&codec_crate_dir, note_project_dir, note_package, session).map(Some) } /// Reads the optional codec crate path from Miden project metadata. @@ -400,73 +413,91 @@ fn build_note_codec_component( codec_crate_dir: &Path, note_project_dir: &Path, note_package: &MastPackage, + session: &Session, ) -> CompilerResult> { - let _staged_package = stage_note_package(note_project_dir, note_package)?; + sweep_legacy_note_codec_inputs(note_project_dir)?; + let session_target_dir = if session.options.target_dir.is_absolute() { + session.options.target_dir.clone() + } else { + session.options.current_dir.join(&session.options.target_dir) + }; + let work_dir = session_target_dir.join(&session.options.profile).join("note-codec"); + let staged_package = stage_note_package(&work_dir, note_package)?; let manifest_path = codec_crate_dir.join("Cargo.toml"); - let artifact_name = codec_artifact_name(&manifest_path)?; - let target_dir = codec_crate_dir.join("target"); - let wasm_path = target_dir - .join(NOTE_CODEC_TARGET) - .join("release") - .join(artifact_name) - .with_extension("wasm"); - // Cargo does not track a package file that a procedural macro reads. Remove the final output - // so the codec crate expands against the package staged above. - match fs::remove_file(&wasm_path) { - Ok(()) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => { - return Err(Report::msg(format!( - "failed to prepare note codec output '{}': {error}", - wasm_path.display() - ))); - } + if !manifest_path.is_file() { + return Err(Report::msg(format!( + "note codec crate has no manifest at '{}'", + manifest_path.display() + ))); } - let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let output = Command::new(cargo) + + let cargo_env = env::var_os("CARGO").map(PathBuf::from); + let cargo_path = cargo_env.as_deref().unwrap_or_else(|| Path::new("cargo")); + let toolchain = if cargo_env.is_none() { + crate::rust::rustup_toolchain() + } else { + None + }; + crate::rust::install_wasm32_target("unknown-unknown", toolchain.as_deref())?; + + let cargo_target_dir = work_dir.join("cargo-target"); + let mut cargo = Command::new(cargo_path); + if let Some(toolchain) = toolchain.as_deref() { + cargo.arg(format!("+{toolchain}")); + } + cargo .current_dir(codec_crate_dir) - .args([ - "build", - "--manifest-path", - manifest_path.to_str().ok_or_else(|| { - Report::msg(format!( - "codec manifest path '{}' is not UTF-8", - manifest_path.display() - )) - })?, - "--lib", - "--release", - "--target", - NOTE_CODEC_TARGET, - "--target-dir", - target_dir.to_str().ok_or_else(|| { - Report::msg(format!("codec target path '{}' is not UTF-8", target_dir.display())) - })?, - ]) + .arg("build") + .arg("--manifest-path") + .arg(&manifest_path) + .arg("--lib") + .arg("--profile") + .arg(&session.options.profile) + .arg("--target") + .arg(NOTE_CODEC_TARGET) + .arg("--target-dir") + .arg(&cargo_target_dir) + .arg("--message-format") + .arg("json-render-diagnostics") + .env(NOTE_CODEC_PACKAGE_PATH_ENV, &staged_package) .env_remove("CARGO_BUILD_TARGET") .env_remove("CARGO_ENCODED_RUSTFLAGS") .env_remove("RUSTFLAGS") - .output() - .map_err(|error| { - Report::msg(format!( - "failed to start `cargo build` for note codec crate '{}': {error}", - codec_crate_dir.display() - )) - })?; - if !output.status.success() { + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()); + + let manifest_path = manifest_path.canonicalize().map_err(|error| { + Report::msg(format!( + "failed to resolve note codec manifest '{}': {error}", + manifest_path.display() + )) + })?; + let artifacts = crate::rust::spawn_cargo(cargo, cargo_path)?; + let mut wasm_paths = artifacts + .into_iter() + .filter(|artifact| { + artifact.manifest_path.as_std_path() == manifest_path + && artifact.target.crate_types.contains(&cargo_metadata::CrateType::CDyLib) + }) + .flat_map(|artifact| artifact.filenames) + .filter(|path| path.extension() == Some("wasm")) + .map(|path| path.into_std_path_buf()) + .collect::>(); + wasm_paths.sort(); + wasm_paths.dedup(); + if wasm_paths.len() != 1 { return Err(Report::msg(format!( - "failed to build note codec crate '{}' for {NOTE_CODEC_TARGET} in release mode \ - (install the target with `rustup target add {NOTE_CODEC_TARGET}` if \ - needed)\nstdout:\n{}\nstderr:\n{}", + "note codec build for '{}' must produce exactly one `{NOTE_CODEC_TARGET}` cdylib; set \ + `[lib] crate-type = [\"cdylib\"]` in '{}', found: {wasm_paths:#?}", codec_crate_dir.display(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), + manifest_path.display() ))); } + let wasm_path = wasm_paths.pop().expect("one codec artifact was checked above"); let module = fs::read(&wasm_path).map_err(|error| { Report::msg(format!( - "note codec build succeeded but did not produce the expected cdylib '{}': {error}", + "note codec build produced an unreadable cdylib '{}': {error}", wasm_path.display() )) })?; @@ -495,80 +526,71 @@ fn build_note_codec_component( Ok(component) } -/// Stages the current in-memory note package for `from_project!` during the codec build. -fn stage_note_package( - note_project_dir: &Path, - note_package: &MastPackage, -) -> CompilerResult { - let profiles_dir = note_project_dir.join("target/miden"); - fs::create_dir_all(&profiles_dir).map_err(|error| { +/// Stages the current in-memory note package at a stable compiler-owned path. +fn stage_note_package(work_dir: &Path, note_package: &MastPackage) -> CompilerResult { + let staging_dir = work_dir.join("input"); + fs::create_dir_all(&staging_dir).map_err(|error| { Report::msg(format!( - "failed to create note package staging root '{}': {error}", - profiles_dir.display() + "failed to create note package staging directory '{}': {error}", + staging_dir.display() )) })?; - let staging_dir = tempfile::Builder::new() - .prefix("zz-note-codec-input-") - .tempdir_in(&profiles_dir) - .map_err(|error| { - Report::msg(format!( - "failed to create note package staging directory in '{}': {error}", - profiles_dir.display() - )) - })?; - note_package.write_masp_file(staging_dir.path()).map_err(|error| { + note_package.write_masp_file(&staging_dir).map_err(|error| { Report::msg(format!( "failed to stage note package {}@{} for codec generation: {error}", note_package.name, note_package.version )) })?; - Ok(staging_dir) + let package_name: &str = ¬e_package.name; + staging_dir + .join(package_name) + .with_extension(MastPackage::EXTENSION) + .canonicalize() + .map_err(|error| { + Report::msg(format!( + "failed to resolve the staged note package in '{}': {error}", + staging_dir.display() + )) + }) } -/// Returns the expected Wasm artifact name and checks the cdylib configuration. -fn codec_artifact_name(manifest_path: &Path) -> CompilerResult { - let source = fs::read_to_string(manifest_path).map_err(|error| { - Report::msg(format!( - "note codec crate has no readable manifest at '{}': {error}", - manifest_path.display() - )) - })?; - let manifest = source.parse::().map_err(|error| { - Report::msg(format!( - "failed to parse note codec manifest '{}': {error}", - manifest_path.display() - )) - })?; - let package = - manifest - .get("package") - .and_then(toml_edit::Item::as_table_like) - .ok_or_else(|| { +/// Removes staging directories created by the temporary-directory implementation. +fn sweep_legacy_note_codec_inputs(note_project_dir: &Path) -> CompilerResult<()> { + let legacy_root = note_project_dir.join("target/miden"); + let entries = match fs::read_dir(&legacy_root) { + Ok(entries) => entries, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) => { + return Err(Report::msg(format!( + "failed to inspect legacy note codec staging root '{}': {error}", + legacy_root.display() + ))); + } + }; + for entry in entries { + let entry = entry.map_err(|error| { + Report::msg(format!( + "failed to inspect legacy note codec staging root '{}': {error}", + legacy_root.display() + )) + })?; + let name = entry.file_name(); + let file_type = entry.file_type().map_err(|error| { + Report::msg(format!( + "failed to inspect legacy note codec staging entry '{}': {error}", + entry.path().display() + )) + })?; + if name.to_string_lossy().starts_with("zz-note-codec-input-") && file_type.is_dir() { + fs::remove_dir_all(entry.path()).map_err(|error| { Report::msg(format!( - "codec manifest '{}' has no `[package]` table", - manifest_path.display() + "failed to remove legacy note codec staging directory '{}': {error}", + entry.path().display() )) })?; - let package_name = package.get("name").and_then(toml_edit::Item::as_str).ok_or_else(|| { - Report::msg(format!("codec manifest '{}' has no package name", manifest_path.display())) - })?; - let lib = manifest.get("lib").and_then(toml_edit::Item::as_table_like).ok_or_else(|| { - Report::msg(format!("codec manifest '{}' has no `[lib]` table", manifest_path.display())) - })?; - let is_cdylib = - lib.get("crate-type") - .and_then(toml_edit::Item::as_array) - .is_some_and(|crate_types| { - crate_types.iter().any(|crate_type| crate_type.as_str() == Some("cdylib")) - }); - if !is_cdylib { - return Err(Report::msg(format!( - "note codec manifest '{}' must set `[lib] crate-type = [\"cdylib\"]`", - manifest_path.display() - ))); + } } - let lib_name = lib.get("name").and_then(toml_edit::Item::as_str).unwrap_or(package_name); - Ok(lib_name.replace('-', "_")) + Ok(()) } /// Verifies the component sandbox and the versioned codec interface export. @@ -594,31 +616,179 @@ fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { ))); } - let interface = world.exports.values().find_map(|item| { - let WorldItem::Interface { id, .. } = item else { - return None; - }; - let interface = &resolve.interfaces[*id]; - let package_id = interface.package?; - let package = &resolve.packages[package_id].name; - (interface.name.as_deref() == Some("codec") - && package.namespace == "miden" - && package.name == "note-codec" - && package.version.as_ref().is_some_and(|version| version.to_string() == "1.0.0")) - .then_some(interface) - }); - let interface = interface - .ok_or_else(|| Report::msg("component does not export `miden:note-codec/codec@1.0.0`"))?; - for function in ["supported-types", "parse", "display", "validate"] { - if !interface.functions.contains_key(function) { + let (actual_key, actual_interface_id) = codec_world_export(&resolve, world_id)?; + + let mut expected = Resolve::default(); + let package_id = expected.push_str("note-codec.wit", NOTE_CODEC_WIT).map_err(|error| { + Report::msg(format!("failed to resolve the pinned note codec WIT: {error:#}")) + })?; + let expected_world_id = expected.packages[package_id] + .worlds + .get("note-codec") + .copied() + .ok_or_else(|| Report::msg("pinned note codec WIT has no `note-codec` world"))?; + let (expected_key, expected_interface_id) = codec_world_export(&expected, expected_world_id)?; + + if actual_key != expected_key { + return Err(Report::msg(format!( + "note codec world exports `{actual_key}`, expected `{expected_key}`" + ))); + } + compare_codec_interface_signatures( + &resolve, + actual_interface_id, + &expected, + expected_interface_id, + ) +} + +/// Returns the canonical key and interface ID for the sole codec world export. +fn codec_world_export( + resolve: &Resolve, + world_id: WorldId, +) -> CompilerResult<(String, wit_parser::InterfaceId)> { + let world = &resolve.worlds[world_id]; + let (key, item) = world.exports.iter().next().ok_or_else(|| { + Report::msg("note codec component does not export `miden:note-codec/codec@1.0.0`") + })?; + let WorldItem::Interface { id, .. } = item else { + return Err(Report::msg(format!( + "note codec world export `{}` is not an interface", + resolve.name_world_key(key) + ))); + }; + let interface = &resolve.interfaces[*id]; + let package_id = interface.package.ok_or_else(|| { + Report::msg("note codec world exports an interface without a package identity") + })?; + let package = &resolve.packages[package_id].name; + if interface.name.as_deref() != Some("codec") + || package.namespace != "miden" + || package.name != "note-codec" + || package.version.as_ref().is_none_or(|version| version.to_string() != "1.0.0") + { + return Err(Report::msg("component does not export `miden:note-codec/codec@1.0.0`")); + } + Ok((resolve.name_canonicalized_world_key(key), *id)) +} + +/// Compares all exported codec function names and structural signatures. +fn compare_codec_interface_signatures( + actual_resolve: &Resolve, + actual_id: wit_parser::InterfaceId, + expected_resolve: &Resolve, + expected_id: wit_parser::InterfaceId, +) -> CompilerResult<()> { + let actual = &actual_resolve.interfaces[actual_id]; + let expected = &expected_resolve.interfaces[expected_id]; + if actual.functions.len() != expected.functions.len() { + return Err(Report::msg(format!( + "`miden:note-codec/codec@1.0.0` exports {} functions, expected {}", + actual.functions.len(), + expected.functions.len() + ))); + } + for (name, expected_function) in &expected.functions { + let actual_function = actual.functions.get(name).ok_or_else(|| { + Report::msg(format!("`miden:note-codec/codec@1.0.0` is missing `{name}`")) + })?; + let actual_signature = function_signature(actual_resolve, actual_function)?; + let expected_signature = function_signature(expected_resolve, expected_function)?; + if actual_signature != expected_signature { return Err(Report::msg(format!( - "`miden:note-codec/codec@1.0.0` is missing `{function}`" + "`miden:note-codec/codec@1.0.0.{name}` has signature `{actual_signature}`, \ + expected `{expected_signature}`" ))); } } Ok(()) } +/// Returns a structural function signature with aliases resolved. +fn function_signature(resolve: &Resolve, function: &Function) -> CompilerResult { + if function.kind != FunctionKind::Freestanding { + return Err(Report::msg(format!( + "note codec function `{}` must be freestanding", + function.name + ))); + } + let params = function + .params + .iter() + .map(|param| { + canonical_wit_type(resolve, param.ty, &mut Vec::new()) + .map(|ty| format!("{}: {ty}", param.name)) + }) + .collect::>>()? + .join(", "); + let result = function + .result + .map(|ty| canonical_wit_type(resolve, ty, &mut Vec::new())) + .transpose()? + .unwrap_or_else(|| "_".to_string()); + Ok(format!("func({params}) -> {result}")) +} + +/// Returns a structural WIT type signature with aliases resolved. +fn canonical_wit_type( + resolve: &Resolve, + ty: Type, + active: &mut Vec, +) -> CompilerResult { + let primitive = match ty { + Type::Bool => Some("bool"), + Type::U8 => Some("u8"), + Type::U16 => Some("u16"), + Type::U32 => Some("u32"), + Type::U64 => Some("u64"), + Type::S8 => Some("s8"), + Type::S16 => Some("s16"), + Type::S32 => Some("s32"), + Type::S64 => Some("s64"), + Type::F32 => Some("f32"), + Type::F64 => Some("f64"), + Type::Char => Some("char"), + Type::String => Some("string"), + Type::ErrorContext => Some("error-context"), + Type::Id(_) => None, + }; + if let Some(primitive) = primitive { + return Ok(primitive.to_string()); + } + let Type::Id(id) = ty else { + unreachable!("all primitive WIT types returned above") + }; + if active.contains(&id) { + return Err(Report::msg("recursive types are not valid in the note codec interface")); + } + active.push(id); + let result = match &resolve.types[id].kind { + TypeDefKind::Type(ty) => canonical_wit_type(resolve, *ty, active), + TypeDefKind::List(ty) => { + canonical_wit_type(resolve, *ty, active).map(|ty| format!("list<{ty}>")) + } + TypeDefKind::Result(result) => { + let ok = result + .ok + .map(|ty| canonical_wit_type(resolve, ty, active)) + .transpose()? + .unwrap_or_else(|| "_".to_string()); + let err = result + .err + .map(|ty| canonical_wit_type(resolve, ty, active)) + .transpose()? + .unwrap_or_else(|| "_".to_string()); + Ok(format!("result<{ok}, {err}>")) + } + kind => Err(Report::msg(format!( + "unsupported `{}` type in the note codec interface signature", + kind.as_str() + ))), + }; + active.pop(); + result +} + /// Parse `cargo -Zscript`-style frontmatter from a given input string, if present. /// /// Returns `Ok(None)` if the input does not define Cargo frontmatter. @@ -698,3 +868,63 @@ pub fn parse_cargo_frontmatter( Ok(Some(dependencies)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn codec_interface_validation_compares_function_signatures() { + let (expected_resolve, expected_id) = resolve_codec_interface(NOTE_CODEC_WIT); + compare_codec_interface_signatures( + &expected_resolve, + expected_id, + &expected_resolve, + expected_id, + ) + .unwrap(); + + let changed = NOTE_CODEC_WIT.replace( + "parse: func(type-fqn: string, value: string)", + "parse: func(type-fqn: string, value: u64)", + ); + let (actual_resolve, actual_id) = resolve_codec_interface(&changed); + let error = compare_codec_interface_signatures( + &actual_resolve, + actual_id, + &expected_resolve, + expected_id, + ) + .unwrap_err() + .to_string(); + + assert!(error.contains(".parse` has signature")); + assert!(error.contains("value: u64")); + assert!(error.contains("value: string")); + } + + #[test] + fn note_codec_staging_is_stable_and_sweeps_legacy_directories() { + let root = tempfile::TempDir::new().unwrap(); + let package = midenc_codegen_masm::intrinsics::load(); + + let first = stage_note_package(root.path(), &package).unwrap(); + let second = stage_note_package(root.path(), &package).unwrap(); + assert_eq!(first, second); + assert_eq!(first.parent().and_then(Path::file_name), Some("input".as_ref())); + + let legacy = root.path().join("project/target/miden/zz-note-codec-input-old"); + fs::create_dir_all(&legacy).unwrap(); + sweep_legacy_note_codec_inputs(&root.path().join("project")).unwrap(); + assert!(!legacy.exists()); + } + + /// Resolves the codec interface from one complete WIT document. + fn resolve_codec_interface(wit: &str) -> (Resolve, wit_parser::InterfaceId) { + let mut resolve = Resolve::default(); + let package_id = resolve.push_str("note-codec-test.wit", wit).unwrap(); + let world_id = resolve.packages[package_id].worlds["note-codec"]; + let (_, interface_id) = codec_world_export(&resolve, world_id).unwrap(); + (resolve, interface_id) + } +} diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index 750f995b25..fcb3d1f2dc 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -2,8 +2,8 @@ //! //! [`prepare_assembler`] runs before assembly, applying the session's link inputs; //! [`post_process_package`] runs after it, attaching to the assembled package the sections -//! and advice-map entries that codegen produced but the assembler knows nothing about. It also -//! builds and attaches an author codec declared by project metadata. +//! and advice-map entries that codegen produced but the assembler knows nothing about. For note +//! targets, it also builds, validates, and attaches an author codec declared by project metadata. //! //! Both are shared: the [`Pipeline`](super::Pipeline) driver prepares its own assembler //! through the first, and every frontend that lowers HIR post-processes through the second — @@ -67,11 +67,20 @@ pub(crate) fn post_process_package( component: &MasmComponent, sections: &midenc_frontend_wasm_metadata::PackageSections, context: &TargetAssemblyContext<'_>, + session: &Session, ) -> Result<(), Report> { use miden_assembly::serde::Serializable; use miden_mast_package::{Section, SectionId}; use midenc_session::miden_project::TargetType; + let has_note_codec = crate::cargo::has_project_note_codec(context.package.metadata()); + validate_note_codec_declaration( + has_note_codec, + context.target.ty, + sections.note_storage_schema.is_some(), + context.target.name.inner(), + )?; + attach_account_component_metadata(package, sections.account_component_metadata.as_deref()); attach_component_wit(package, sections.component_wit.as_deref()); attach_note_storage_schema(package, sections.note_storage_schema.as_deref())?; @@ -92,8 +101,34 @@ pub(crate) fn post_process_package( .push(Section::new(SectionId::KERNEL, kernel_package.to_bytes())); } - attach_note_codec(package, context)?; + if has_note_codec { + attach_note_codec(package, context, session)?; + } + + Ok(()) +} + +/// Validates the target and schema required by an author codec declaration. +fn validate_note_codec_declaration( + has_note_codec: bool, + target_type: midenc_session::miden_project::TargetType, + has_note_storage_schema: bool, + target_name: &str, +) -> Result<(), Report> { + use midenc_session::miden_project::TargetType; + if has_note_codec && target_type != TargetType::Note { + return Err(Report::msg(format!( + "`[package.metadata.note-codec-crate]` is only valid for note targets, but target \ + '{target_name}' has type `{target_type}`" + ))); + } + if has_note_codec && !has_note_storage_schema { + return Err(Report::msg(format!( + "note target '{target_name}' declares `[package.metadata.note-codec-crate]` but \ + emitted no note storage schema; add one named-field `#[note]` struct" + ))); + } Ok(()) } @@ -101,24 +136,25 @@ pub(crate) fn post_process_package( fn attach_note_codec( package: &mut Package, context: &TargetAssemblyContext<'_>, + session: &Session, ) -> Result<(), Report> { let Some(component) = crate::cargo::build_project_note_codec( context.package.as_ref(), context.manifest_path, context.project_root.as_ref(), package, + session, )? else { return Ok(()); }; - use miden_mast_package::{Section, SectionId}; + use miden_mast_package::SectionId; let section_id = SectionId::custom(midenc_frontend_wasm_metadata::PACKAGE_NOTE_CODEC_SECTION_ID).map_err( |error| Report::msg(format!("the note codec package section id is invalid: {error}")), )?; - package.sections.push(Section::new(section_id, component)); - Ok(()) + set_unique_section(package, section_id, component, "note codec") } /// Attach the note storage schema to the assembled package. @@ -126,13 +162,31 @@ fn attach_note_storage_schema( package: &mut Package, note_storage_schema: Option<&[u8]>, ) -> Result<(), Report> { - use miden_mast_package::{Section, SectionId}; + use miden_mast_package::SectionId; if let Some(bytes) = note_storage_schema { let section_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID) .map_err(|err| Report::msg(format!("invalid note storage schema section id: {err}")))?; - package.sections.push(Section::new(section_id, bytes.to_vec())); + set_unique_section(package, section_id, bytes.to_vec(), "note storage schema")?; + } + Ok(()) +} + +/// Adds one package section and rejects an existing section with the same identifier. +fn set_unique_section( + package: &mut Package, + section_id: miden_mast_package::SectionId, + bytes: Vec, + description: &str, +) -> Result<(), Report> { + use miden_mast_package::Section; + + if package.sections.iter().any(|section| section.id == section_id) { + return Err(Report::msg(format!( + "cannot attach {description}: package already contains section `{section_id}`" + ))); } + package.sections.push(Section::new(section_id, bytes)); Ok(()) } @@ -167,3 +221,44 @@ fn extend_rodata_advice_map(package: &mut Package, rodata: &[midenc_codegen_masm let advice_map = rodata.iter().map(|segment| (segment.digest, segment.to_elements())).collect(); package.extend_advice_map(advice_map); } + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use miden_mast_package::SectionId; + use midenc_session::miden_project::TargetType; + + use super::*; + + #[test] + fn unique_sections_reject_an_existing_identifier() { + let mut package = (*midenc_codegen_masm::intrinsics::load()).clone(); + let id = SectionId::custom("test_unique_section").unwrap(); + + set_unique_section(&mut package, id.clone(), vec![1], "test section").unwrap(); + let error = set_unique_section(&mut package, id, vec![2], "test section") + .unwrap_err() + .to_string(); + + assert!(error.contains("already contains section `test_unique_section`")); + } + + #[test] + fn codec_metadata_requires_a_note_target_with_a_schema() { + let wrong_target = + validate_note_codec_declaration(true, TargetType::Library, true, "library") + .unwrap_err() + .to_string(); + assert!(wrong_target.contains("only valid for note targets")); + + let missing_schema = + validate_note_codec_declaration(true, TargetType::Note, false, "schema-less") + .unwrap_err() + .to_string(); + assert!(missing_schema.contains("emitted no note storage schema")); + + validate_note_codec_declaration(true, TargetType::Note, true, "note").unwrap(); + validate_note_codec_declaration(false, TargetType::Library, false, "library").unwrap(); + } +} diff --git a/midenc-compile/src/pipeline/backend.rs b/midenc-compile/src/pipeline/backend.rs index e22a5c123e..5e238cc6dc 100644 --- a/midenc-compile/src/pipeline/backend.rs +++ b/midenc-compile/src/pipeline/backend.rs @@ -832,6 +832,7 @@ mod tests { &found.component, &found.sections, cx.assembly(), + &cx.session(), ) } diff --git a/midenc-compile/src/pipeline/frontends/hir.rs b/midenc-compile/src/pipeline/frontends/hir.rs index 0b62e5a1ad..cf55d39721 100644 --- a/midenc-compile/src/pipeline/frontends/hir.rs +++ b/midenc-compile/src/pipeline/frontends/hir.rs @@ -394,6 +394,7 @@ impl Frontend for HirFrontend { &found.component, &found.sections, cx.assembly(), + &cx.session(), ) } diff --git a/midenc-compile/src/pipeline/frontends/rust.rs b/midenc-compile/src/pipeline/frontends/rust.rs index 2c6fa6af94..7696c83a0b 100644 --- a/midenc-compile/src/pipeline/frontends/rust.rs +++ b/midenc-compile/src/pipeline/frontends/rust.rs @@ -1633,6 +1633,7 @@ impl Frontend for RustProjectFrontend { &found.component, &found.sections, cx.assembly(), + &cx.session(), ) } } diff --git a/midenc-compile/src/pipeline/frontends/wasm.rs b/midenc-compile/src/pipeline/frontends/wasm.rs index 9a5fc48fd7..b2e22b22e2 100644 --- a/midenc-compile/src/pipeline/frontends/wasm.rs +++ b/midenc-compile/src/pipeline/frontends/wasm.rs @@ -592,6 +592,7 @@ impl Frontend for WasmFrontend { &found.component, &found.sections, cx.assembly(), + &cx.session(), ) } diff --git a/midenc-compile/src/pipeline/seed.rs b/midenc-compile/src/pipeline/seed.rs index f653af7cd7..b7d43455e1 100644 --- a/midenc-compile/src/pipeline/seed.rs +++ b/midenc-compile/src/pipeline/seed.rs @@ -439,6 +439,7 @@ impl Frontend for SeedFrontend { &found.component, &found.sections, cx.assembly(), + &cx.session(), ) } @@ -605,6 +606,7 @@ mod tests { &found.component, &found.sections, cx.assembly(), + &cx.session(), ) } diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 77a8eddd49..db4512ef55 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -14,11 +14,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 set or the guest SDK dependency graph. - Added typed host note-storage bindings through the internal `miden-note-bindings` macros. Bindings can load a built note project or an exact `.masp`, generate native Rust storage types, and convert - typed values to and from note storage. + typed values to and from note storage. Its facade supplies all generated runtime dependencies, + and generated string, validation, and display APIs keep stable standard-registry and + caller-provided-registry forms as schemas gain nested types. +- The `FromFeltRepr`/`ToFeltRepr` derives accept an internal `#[felt_repr(crate_path = "...")]` + attribute so macro-generated code can reference the runtime crate through a facade re-export. - `#[note]` now embeds a WIT storage schema for named-field note structs in the `note_storage_schema` section of the compiled `.masp`. Schema records preserve Rust doc comments and can include nested types declared with `#[export_type]` before the note struct. Unit structs - emit no schema; tuple structs and `Vec` fields are not supported yet. + emit no schema. + +### Migration and breaking changes + +- `#[note]` storage types now require named-field or unit structs. Tuple structs no longer compile, + and note storage fields no longer accept `Vec`. Follow the + [migration guidance](./sdk/MIGRATION.md#rewrite-tuple-note-and-vec-storage-layouts) to preserve + field order with named fields and replace dynamic vectors with a fixed schema. ## [0.14.0] diff --git a/sdk/base-macros/src/note.rs b/sdk/base-macros/src/note.rs index 644db2d7d3..ab2271d8e4 100644 --- a/sdk/base-macros/src/note.rs +++ b/sdk/base-macros/src/note.rs @@ -11,11 +11,11 @@ use syn::{ use crate::{ boilerplate::runtime_boilerplate, - note_schema::expand_note_storage_schema, + note_schema::{expand_note_storage_schema, note_storage_schema_uniqueness_guard}, types::{TypeRef, map_type_to_type_ref, registered_export_type_map}, util::{ - generate_frontend_link_section, generate_wit_link_section, is_type_named, - is_unit_return_type, + NOTE_NAMED_FIELDS_ERROR, generate_frontend_link_section, generate_wit_link_section, + is_type_named, is_unit_return_type, }, wit_builder::WitBuilder, wit_world::{ManifestPackage, write_world_block}, @@ -126,6 +126,7 @@ fn expand_method_marker_attr( fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { let struct_ident = &item_struct.ident; + let uniqueness_guard = note_storage_schema_uniqueness_guard(); if !item_struct.generics.params.is_empty() { return syn::Error::new( @@ -181,8 +182,7 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { (from_impl, schema_static) } syn::Fields::Unnamed(fields) => { - return syn::Error::new(fields.span(), "note storage schema needs named fields") - .into_compile_error(); + return syn::Error::new(fields.span(), NOTE_NAMED_FIELDS_ERROR).into_compile_error(); } }; @@ -193,6 +193,7 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { impl ::miden::active_note::ActiveNote for #struct_ident {} #schema_static + #uniqueness_guard } } @@ -1199,6 +1200,7 @@ mod tests { let tokens = expand_note_struct(item_struct).to_string(); assert!(tokens.contains("__MIDEN_NOTE_STORAGE_SCHEMA_BYTES")); + assert!(tokens.contains(crate::note_schema::NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD_SYMBOL)); assert!(tokens.contains("miden_note_schema")); } @@ -1211,6 +1213,7 @@ mod tests { let tokens = expand_note_struct(item_struct).to_string(); assert!(!tokens.contains("__MIDEN_NOTE_STORAGE_SCHEMA_BYTES")); + assert!(tokens.contains(crate::note_schema::NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD_SYMBOL)); } #[test] @@ -1221,7 +1224,7 @@ mod tests { let tokens = expand_note_struct(item_struct).to_string(); - assert!(tokens.contains("note storage schema needs named fields")); + assert!(tokens.contains(NOTE_NAMED_FIELDS_ERROR)); } #[test] diff --git a/sdk/base-macros/src/note_schema.rs b/sdk/base-macros/src/note_schema.rs index b0de7627df..858e6114d2 100644 --- a/sdk/base-macros/src/note_schema.rs +++ b/sdk/base-macros/src/note_schema.rs @@ -4,10 +4,11 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use heck::ToKebabCase; use midenc_frontend_wasm_metadata::WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME; -use proc_macro2::{Literal, TokenStream as TokenStream2}; +use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; use quote::quote; use semver::Version; use syn::{ItemStruct, Type, spanned::Spanned}; +use wit_bindgen_core::wit_parser::Resolve; use crate::{ manifest_paths::SDK_WIT_SOURCE, @@ -15,6 +16,7 @@ use crate::{ ExportedField, ExportedTypeDef, ExportedTypeKind, TypeRef, doc_comments, map_type_to_type_ref, registered_export_types, }, + util::NOTE_NAMED_FIELDS_ERROR, wit_builder::{WitBody, WitBuilder}, wit_world::ManifestPackage, }; @@ -22,18 +24,36 @@ use crate::{ const CORE_TYPES_PACKAGE: &str = "miden:base/core-types@1.0.0"; const CORE_TYPES_PACKAGE_NAME: &str = "miden:base"; const CORE_TYPES_INTERFACE: &str = "core-types"; +const NOTE_STORAGE_SCHEMA_SOURCE_NAME: &str = "note-storage-schema.wit"; +const NOTE_STORAGE_SUPPORTED_TYPES: &str = "`u64`, `u32`, `u8`, `bool`, SDK core-type records, \ + `#[export_type]` records or enums, and `Option` \ + over a supported type"; +/// Linker symbol used to reject multiple note storage schemas in one crate. +pub(crate) const NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD_SYMBOL: &str = + "__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD"; + +/// A rendered note storage schema and the Rust definitions that produced it. +struct RenderedNoteStorageSchema { + span: Span, + source: String, + definitions: Vec, +} /// Generates the note storage schema custom-section static for a named-field note struct. pub(crate) fn expand_note_storage_schema( item_struct: &ItemStruct, ) -> Result { let package = ManifestPackage::load_or_default(item_struct.ident.span())?; - let document = render_note_storage_schema( + let registry = registered_export_types(); + let rendered = render_note_storage_schema_with_registry_model( item_struct, &package.component_package(), package.component_version(), + ®istry, )?; - let mut bytes = document.into_bytes(); + validate_rendered_note_storage_schema(&rendered)?; + + let mut bytes = rendered.source.into_bytes(); let padded_len = bytes.len().div_ceil(16) * 16; bytes.resize(padded_len, 0); @@ -53,28 +73,61 @@ pub(crate) fn expand_note_storage_schema( }) } +/// Emits a fixed linker symbol that permits one note struct per crate. +pub(crate) fn note_storage_schema_uniqueness_guard() -> TokenStream2 { + quote! { + const _: () = { + // A crate may contain exactly one `#[note]` struct. Reusing a fixed symbol name lets + // the linker reject duplicates across modules. + #[doc(hidden)] + #[used] + #[unsafe(export_name = #NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD_SYMBOL)] + static __miden_note_storage_schema_uniqueness_guard: u8 = 0; + }; + } +} + /// Renders the note storage schema WIT document for a named-field note struct. +#[cfg(test)] fn render_note_storage_schema( item_struct: &ItemStruct, component_package: &str, component_version: &Version, ) -> Result { let registry = registered_export_types(); - render_note_storage_schema_with_registry( + Ok(render_note_storage_schema_with_registry_model( item_struct, component_package, component_version, ®istry, - ) + )? + .source) } /// Renders a note storage schema against one export-type registry snapshot. +#[cfg(test)] fn render_note_storage_schema_with_registry( item_struct: &ItemStruct, component_package: &str, component_version: &Version, registry: &[ExportedTypeDef], ) -> Result { + Ok(render_note_storage_schema_with_registry_model( + item_struct, + component_package, + component_version, + registry, + )? + .source) +} + +/// Renders a note storage schema and retains its source-definition context. +fn render_note_storage_schema_with_registry_model( + item_struct: &ItemStruct, + component_package: &str, + component_version: &Version, + registry: &[ExportedTypeDef], +) -> Result { let registry_by_rust_name = registry .iter() .cloned() @@ -82,6 +135,9 @@ fn render_note_storage_schema_with_registry( .collect(); let root = note_root_type(item_struct, ®istry_by_rust_name)?; let custom_types = referenced_custom_types(&root, registry, item_struct.ident.span())?; + for definition in custom_types.iter().chain([&root]) { + validate_note_storage_definition(definition, item_struct.ident.span())?; + } let core_imports = required_core_type_imports(&root, &custom_types); let schema_package = schema_package_name(component_package); @@ -110,7 +166,13 @@ fn render_note_storage_schema_with_registry( wit.blank_line(); render_core_types_package(&mut wit)?; - Ok(wit.finish()) + let mut definitions = custom_types; + definitions.push(root); + Ok(RenderedNoteStorageSchema { + span: item_struct.ident.span(), + source: wit.finish(), + definitions, + }) } /// Builds the exported type definition for the note storage root. @@ -119,16 +181,15 @@ fn note_root_type( registry: &HashMap, ) -> Result { let syn::Fields::Named(named) = &item_struct.fields else { - return Err(syn::Error::new( - item_struct.fields.span(), - "note storage schema needs named fields", - )); + return Err(syn::Error::new(item_struct.fields.span(), NOTE_NAMED_FIELDS_ERROR)); }; let mut fields = Vec::with_capacity(named.named.len()); for field in &named.named { let ident = field.ident.as_ref().expect("named fields must have identifiers"); - let ty = map_note_field_type(&field.ty, registry)?; + let context = format!("field `{ident}` in type `{}`", item_struct.ident); + let ty = map_note_field_type(&field.ty, registry, &context)?; + validate_note_storage_type_ref(&ty, field.ty.span(), &context)?; fields.push(ExportedField { docs: doc_comments(&field.attrs), name: ident.to_string(), @@ -144,10 +205,171 @@ fn note_root_type( }) } +/// Checks one record or variant against the supported note storage type surface. +fn validate_note_storage_definition( + definition: &ExportedTypeDef, + span: Span, +) -> Result<(), syn::Error> { + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + validate_note_storage_type_ref( + &field.ty, + span, + &format!("field `{}` in type `{}`", field.name, definition.rust_name), + )?; + } + } + ExportedTypeKind::Variant { variants } => { + for variant in variants { + if let Some(payload) = &variant.payload { + validate_note_storage_type_ref( + payload, + span, + &format!( + "variant `{}` in type `{}`", + variant.wit_name, definition.rust_name + ), + )?; + } + } + } + } + Ok(()) +} + +/// Checks one type reference against the supported note storage type surface. +fn validate_note_storage_type_ref( + type_ref: &TypeRef, + span: Span, + context: &str, +) -> Result<(), syn::Error> { + if type_ref.is_custom + || type_ref.is_sdk_core_record() + || matches!(type_ref.wit_name.as_str(), "u64" | "u32" | "u8" | "bool") + { + return Ok(()); + } + + if type_ref.path.last().is_some_and(|segment| segment == "Option") { + let [inner] = type_ref.dependencies.as_slice() else { + return Err(unsupported_note_storage_type(type_ref, span, context)); + }; + return validate_note_storage_type_ref(inner, span, context); + } + + Err(unsupported_note_storage_type(type_ref, span, context)) +} + +/// Builds an actionable error for a type outside the note storage surface. +fn unsupported_note_storage_type(type_ref: &TypeRef, span: Span, context: &str) -> syn::Error { + syn::Error::new( + span, + format!( + "`#[note]` storage {context} uses unsupported WIT type `{}`; supported types are \ + {NOTE_STORAGE_SUPPORTED_TYPES}", + type_ref.wit_name, + ), + ) +} + +/// Resolves the generated WIT before it is embedded in the guest binary. +fn validate_rendered_note_storage_schema( + rendered: &RenderedNoteStorageSchema, +) -> Result<(), syn::Error> { + let mut resolve = Resolve::default(); + resolve + .push_str(NOTE_STORAGE_SCHEMA_SOURCE_NAME, &rendered.source) + .map(|_| ()) + .map_err(|error| { + let message = format!("{error:#}"); + let (span, context) = rendered_schema_error_context(rendered, &message); + syn::Error::new( + span, + format!("failed to resolve note storage schema for {context}: {message}"), + ) + }) +} + +/// Finds the Rust definition associated with a WIT parser diagnostic. +fn rendered_schema_error_context( + rendered: &RenderedNoteStorageSchema, + error: &str, +) -> (Span, String) { + let fallback = rendered + .definitions + .last() + .map(|definition| (rendered.span, format!("type `{}`", definition.rust_name))) + .unwrap_or_else(|| (Span::call_site(), "the `#[note]` storage type".to_string())); + let Some(error_line) = wit_error_line(error).and_then(|line| line.checked_sub(1)) else { + return fallback; + }; + let Some(error_line) = rendered.source.lines().nth(error_line) else { + return fallback; + }; + let error_line = error_line.trim(); + for definition in &rendered.definitions { + let keyword = match &definition.kind { + ExportedTypeKind::Record { .. } => "record", + ExportedTypeKind::Variant { .. } => "variant", + }; + let header = format!("{keyword} {} {{", definition.wit_name); + if error_line == header { + return (rendered.span, format!("type `{}`", definition.rust_name)); + } + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + let field_prefix = format!("{}:", field.name.to_kebab_case()); + if error_line.starts_with(&field_prefix) { + return ( + rendered.span, + format!( + "field `{}` of type `{}` in type `{}`", + field.name, field.ty.wit_name, definition.rust_name + ), + ); + } + } + } + ExportedTypeKind::Variant { variants } => { + for variant in variants { + if error_line.starts_with(&variant.wit_name) { + let payload = variant + .payload + .as_ref() + .map(|payload| format!(" with payload type `{}`", payload.wit_name)) + .unwrap_or_default(); + return ( + rendered.span, + format!( + "variant `{}`{payload} in type `{}`", + variant.wit_name, definition.rust_name + ), + ); + } + } + } + } + } + + fallback +} + +/// Reads the one-based source line from a highlighted WIT parser diagnostic. +fn wit_error_line(error: &str) -> Option { + let marker = format!("{NOTE_STORAGE_SCHEMA_SOURCE_NAME}:"); + error.lines().find_map(|line| { + let (_, location) = line.split_once(&marker)?; + location.split(':').next()?.parse().ok() + }) +} + /// Maps a Rust field type to WIT syntax with note-specific diagnostics. fn map_note_field_type( ty: &Type, registry: &HashMap, + context: &str, ) -> Result { if contains_vec(ty) { return Err(syn::Error::new( @@ -157,7 +379,13 @@ fn map_note_field_type( } map_type_to_type_ref(ty, registry).map_err(|err| { - syn::Error::new(ty.span(), format!("type is not supported in note storage schemas: {err}")) + syn::Error::new( + ty.span(), + format!( + "`#[note]` storage {context} is not supported: {err}; supported types are \ + {NOTE_STORAGE_SUPPORTED_TYPES}" + ), + ) }) } @@ -310,11 +538,14 @@ fn render_type_definition(interface: &mut WitBody, definition: &ExportedTypeDef) /// Writes Rust doc attribute text as WIT doc comments. fn render_docs(body: &mut WitBody, docs: &[String]) { for doc in docs { - let doc = doc.strip_prefix(' ').unwrap_or(doc); - if doc.is_empty() { - body.line("///"); - } else { - body.line(&format!("/// {doc}")); + for line in doc.split('\n') { + let line = line.strip_suffix('\r').unwrap_or(line); + let line = line.strip_prefix(' ').unwrap_or(line); + if line.is_empty() { + body.line("///"); + } else { + body.line(&format!("/// {line}")); + } } } } @@ -406,6 +637,13 @@ mod tests { assert!(extract_interface_body(SDK_WIT_SOURCE, CORE_TYPES_INTERFACE).is_some()); } + #[test] + fn uniqueness_guard_expansion_matches_golden() { + let tokens = note_storage_schema_uniqueness_guard().to_string(); + + expect![[r#"const _ : () = { # [doc (hidden)] # [used] # [unsafe (export_name = "__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD")] static __miden_note_storage_schema_uniqueness_guard : u8 = 0 ; } ;"#]].assert_eq(&tokens); + } + #[test] fn renders_p2id_shaped_schema() { reset_export_type_registry_for_tests(); @@ -797,6 +1035,129 @@ mod tests { assert_schema_root(&source, "routed-note"); } + #[test] + fn renders_multiline_doc_attributes_as_separate_wit_comments() { + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + #[doc = " First note line.\n Second note line."] + struct DocumentedNote { + #[doc = " First field line.\n Second field line."] + value: u64, + } + }; + let source = + render_note_storage_schema(¬e, "miden:documented-note", &Version::new(1, 0, 0)) + .expect("multiline docs must render"); + + assert!(source.contains("/// First note line.\n /// Second note line.")); + assert!(source.contains("/// First field line.\n /// Second field line.")); + assert_schema_root(&source, "documented-note"); + } + + #[test] + fn rejects_signed_integer_storage_fields() { + for (rust_type, wit_type) in [("i8", "s8"), ("i16", "s16"), ("i32", "s32"), ("i64", "s64")] + { + assert_unsupported_note_field_type(rust_type, wit_type); + } + } + + #[test] + fn rejects_u16_storage_fields() { + assert_unsupported_note_field_type("u16", "u16"); + } + + #[test] + fn rejects_result_storage_fields() { + assert_unsupported_note_field_type("Result", "result"); + } + + #[test] + fn rejects_other_types_outside_the_storage_allow_list() { + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct FloatNote { + value: f32, + } + }; + let err = render_note_storage_schema_with_registry( + ¬e, + "miden:float-note", + &Version::new(1, 0, 0), + &[], + ) + .expect_err("types outside the note storage surface must fail"); + + let message = err.to_string(); + assert!(message.contains("field `value` in type `FloatNote`")); + assert!(message.contains("`f32` is not supported")); + assert!(message.contains("supported types are")); + } + + #[test] + fn rejects_unsupported_types_nested_in_options() { + assert_unsupported_note_field_type("Option", "s16"); + } + + #[test] + fn rejects_unsupported_fields_in_nested_records() { + reset_export_type_registry_for_tests(); + let nested: syn::ItemStruct = parse_quote! { + struct Nested { + count: u16, + } + }; + let nested = exported_type_from_struct(&nested).expect("record must map"); + let note: ItemStruct = parse_quote! { + struct NestedNote { + nested: Nested, + } + }; + let err = render_note_storage_schema_with_registry( + ¬e, + "miden:nested-note", + &Version::new(1, 0, 0), + &[nested], + ) + .expect_err("unsupported nested fields must fail"); + + let message = err.to_string(); + assert!(message.contains("field `count` in type `Nested`")); + assert!(message.contains("unsupported WIT type `u16`")); + } + + #[test] + fn expansion_surfaces_wit_parser_errors_with_type_context() { + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct Type { + value: u64, + } + }; + let err = expand_note_storage_schema(¬e) + .expect_err("a WIT keyword cannot be used as a record name"); + + let message = err.to_string(); + assert!(message.contains("failed to resolve note storage schema")); + assert!(message.contains("type `Type`")); + } + + #[test] + fn expansion_surfaces_wit_parser_errors_with_field_context() { + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct InvalidFieldNote { + type_: u64, + } + }; + let err = expand_note_storage_schema(¬e) + .expect_err("a WIT keyword cannot be used as a field name"); + + let message = err.to_string(); + assert!(message.contains("failed to resolve note storage schema")); + assert!(message.contains("field `type_` of type `u64`")); + } + #[test] fn rejects_tuple_note_structs() { reset_export_type_registry_for_tests(); @@ -806,7 +1167,7 @@ mod tests { let err = render_note_storage_schema(¬e, "miden:tuple-note", &Version::new(1, 0, 0)) .expect_err("tuple notes must fail"); - assert_eq!(err.to_string(), "note storage schema needs named fields"); + assert_eq!(err.to_string(), NOTE_NAMED_FIELDS_ERROR); } #[test] @@ -838,4 +1199,24 @@ mod tests { assert!(message.contains("#[export_type]")); assert!(message.contains("before the #[note] struct")); } + + /// Checks one Rust field type against the note-specific allow-list diagnostic. + fn assert_unsupported_note_field_type(rust_type: &str, expected_wit_type: &str) { + reset_export_type_registry_for_tests(); + let note: ItemStruct = + syn::parse_str(&format!("struct UnsupportedNote {{ value: {rust_type}, }}")) + .expect("test note must parse"); + let err = render_note_storage_schema_with_registry( + ¬e, + "miden:unsupported-note", + &Version::new(1, 0, 0), + &[], + ) + .expect_err("unsupported note field must fail"); + + let message = err.to_string(); + assert!(message.contains("field `value` in type `UnsupportedNote`")); + assert!(message.contains(&format!("unsupported WIT type `{expected_wit_type}`"))); + assert!(message.contains("supported types are")); + } } diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index a116513c47..5542397a6a 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -35,6 +35,11 @@ impl TypeRef { dependency.add_required_core_type_imports(imports); } } + + /// Returns true when this type is an SDK core-type record. + pub(crate) fn is_sdk_core_record(&self) -> bool { + !self.is_custom && sdk_core_record_names().contains(&self.wit_name) + } } #[derive(Clone, Debug)] @@ -333,6 +338,17 @@ fn sdk_core_type_names() -> &'static HashSet { NAMES.get_or_init(|| parse_wit_type_names(SDK_WIT_SOURCE)) } +/// Returns the record names declared by the SDK core-types WIT document. +fn sdk_core_record_names() -> &'static HashSet { + static NAMES: OnceLock> = OnceLock::new(); + NAMES.get_or_init(|| { + SDK_WIT_SOURCE + .lines() + .filter_map(|line| extract_wit_type_name(line.trim_start(), "record")) + .collect() + }) +} + fn parse_wit_type_names(source: &str) -> HashSet { let mut names = HashSet::new(); for line in source.lines() { diff --git a/sdk/base-macros/src/util.rs b/sdk/base-macros/src/util.rs index 625aec103d..0596cd9d5a 100644 --- a/sdk/base-macros/src/util.rs +++ b/sdk/base-macros/src/util.rs @@ -19,6 +19,9 @@ const FRONTEND_METADATA_BYTES_STATIC_IDENT: &str = "__miden_frontend_metadata_by /// Linker symbol used to reject multiple frontend-marked procedures in one project. pub(crate) const FRONTEND_METADATA_UNIQUENESS_GUARD_SYMBOL: &str = "__MIDEN_FRONTEND_METADATA_UNIQUENESS_GUARD"; +/// Diagnostic emitted when `#[note]` is applied to a tuple struct. +pub(crate) const NOTE_NAMED_FIELDS_ERROR: &str = + "#[note] requires named fields; tuple structs are no longer supported"; /// Returns true if a function's return type is unit. pub(crate) fn is_unit_return_type(output: &syn::ReturnType) -> bool { diff --git a/sdk/base-macros/tests/note_trailing_data.rs b/sdk/base-macros/tests/note_trailing_data.rs index 310e54100f..f44e798a9f 100644 --- a/sdk/base-macros/tests/note_trailing_data.rs +++ b/sdk/base-macros/tests/note_trailing_data.rs @@ -16,10 +16,6 @@ pub mod active_note { pub trait ActiveNote {} } -#[derive(Debug)] -#[note] -struct UnitNote; - #[derive(Debug)] #[note] struct OneFeltNote { @@ -27,14 +23,6 @@ struct OneFeltNote { a: miden::Felt, } -#[test] -fn unit_note_rejects_trailing_data() { - let felts = [miden::Felt::new(0).unwrap()]; - - let err = UnitNote::try_from(felts.as_slice()).unwrap_err(); - assert_eq!(err, miden::felt_repr::FeltReprError::TrailingData { pos: 0, len: 1 }); -} - #[test] fn note_struct_rejects_trailing_data() { let felts = [miden::Felt::new(1).unwrap(), miden::Felt::new(2).unwrap()]; diff --git a/sdk/base-macros/tests/unit_note_trailing_data.rs b/sdk/base-macros/tests/unit_note_trailing_data.rs new file mode 100644 index 0000000000..d1238da1e0 --- /dev/null +++ b/sdk/base-macros/tests/unit_note_trailing_data.rs @@ -0,0 +1,25 @@ +//! Tests trailing-data rejection for unit note structs. + +use core::convert::TryFrom; + +use miden_base_macros::note; + +extern crate self as miden; + +pub use miden_field::Felt; + +pub mod felt_repr { + pub use miden_field_repr::{FeltReader, FeltReprError, FeltWriter, FromFeltRepr, ToFeltRepr}; +} + +#[derive(Debug)] +#[note] +struct UnitNote; + +#[test] +fn unit_note_rejects_trailing_data() { + let felts = [miden::Felt::new(0).unwrap()]; + + let err = UnitNote::try_from(felts.as_slice()).unwrap_err(); + assert_eq!(err, miden::felt_repr::FeltReprError::TrailingData { pos: 0, len: 1 }); +} diff --git a/sdk/field-repr/derive/src/lib.rs b/sdk/field-repr/derive/src/lib.rs index 236c5e9074..a2b5e69f37 100644 --- a/sdk/field-repr/derive/src/lib.rs +++ b/sdk/field-repr/derive/src/lib.rs @@ -130,7 +130,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{ - Data, DeriveInput, Error, Field, Fields, Index, Variant, parse_macro_input, + Data, DeriveInput, Error, Field, Fields, Index, LitStr, Variant, parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma, }; @@ -260,15 +260,13 @@ fn ensure_no_explicit_discriminants( /// pub suffix: Felt, /// } /// ``` -#[proc_macro_derive(DeriveFromFeltRepr)] +#[proc_macro_derive(DeriveFromFeltRepr, attributes(felt_repr))] pub fn derive_from_felt_repr(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - - let expanded = derive_from_felt_repr_impl( - &input, - quote!(miden_field_repr), - quote!(miden_field_repr::Felt), - ); + let expanded = felt_repr_crate_path(&input).and_then(|felt_repr_crate| { + let felt_ty = quote!(#felt_repr_crate::Felt); + derive_from_felt_repr_impl(&input, felt_repr_crate, felt_ty) + }); match expanded { Ok(ts) => ts, Err(err) => err.into_compile_error().into(), @@ -422,16 +420,38 @@ fn derive_from_felt_repr_impl( /// pub suffix: Felt, /// } /// ``` -#[proc_macro_derive(DeriveToFeltRepr)] +#[proc_macro_derive(DeriveToFeltRepr, attributes(felt_repr))] pub fn derive_to_felt_repr(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - match derive_to_felt_repr_impl(&input, quote!(miden_field_repr)) { + match felt_repr_crate_path(&input) + .and_then(|felt_repr_crate| derive_to_felt_repr_impl(&input, felt_repr_crate)) + { Ok(ts) => ts, Err(err) => err.into_compile_error().into(), } } +/// Returns the felt representation crate path selected by a generated facade. +fn felt_repr_crate_path(input: &DeriveInput) -> Result { + let mut selected = None; + for attr in input.attrs.iter().filter(|attr| attr.path().is_ident("felt_repr")) { + attr.parse_nested_meta(|meta| { + if !meta.path.is_ident("crate_path") { + return Err(meta.error("expected `crate_path = \"path\"`")); + } + if selected.is_some() { + return Err(meta.error("`crate_path` is already set")); + } + let literal: LitStr = meta.value()?.parse()?; + let path: syn::Path = literal.parse()?; + selected = Some(quote!(#path)); + Ok(()) + })?; + } + Ok(selected.unwrap_or_else(|| quote!(miden_field_repr))) +} + fn derive_to_felt_repr_impl( input: &DeriveInput, felt_repr_crate: TokenStream2, diff --git a/sdk/note-bindings/Cargo.toml b/sdk/note-bindings/Cargo.toml index ac183d7394..16ca5db6aa 100644 --- a/sdk/note-bindings/Cargo.toml +++ b/sdk/note-bindings/Cargo.toml @@ -15,23 +15,18 @@ edition.workspace = true publish = false [lib] -proc-macro = true doctest = false [dependencies] -miden-mast-package = { workspace = true, features = ["std"] } -miden-note-schema.workspace = true -miden-note-schema-codegen.workspace = true -proc-macro2.workspace = true -quote.workspace = true -syn.workspace = true - -[dev-dependencies] miden-field.workspace = true miden-field-repr.workspace = true +miden-note-bindings-macros.workspace = true +miden-note-schema.workspace = true miden-protocol = { workspace = true, features = ["std"] } -midenc-expect-test.workspace = true + +[dev-dependencies] +miden-mast-package = { workspace = true, features = ["std"] } +midenc-frontend-wasm-metadata.workspace = true midenc-frontend-wasm.workspace = true midenc-integration-test-support.workspace = true -prettyplease = "0.2" tempfile.workspace = true diff --git a/sdk/note-bindings/macros/Cargo.toml b/sdk/note-bindings/macros/Cargo.toml new file mode 100644 index 0000000000..89703963ae --- /dev/null +++ b/sdk/note-bindings/macros/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "miden-note-bindings-macros" +description = "Procedural macros for typed Miden note storage bindings" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true +publish = false + +[lib] +proc-macro = true +doctest = false + +[dependencies] +miden-note-schema.workspace = true +miden-note-schema-codegen.workspace = true +proc-macro2.workspace = true +proc-macro-crate = "3.5" +quote.workspace = true +syn = { workspace = true, features = ["visit-mut"] } + +[dev-dependencies] +midenc-expect-test.workspace = true +prettyplease = "0.2" diff --git a/sdk/note-bindings/macros/src/lib.rs b/sdk/note-bindings/macros/src/lib.rs new file mode 100644 index 0000000000..5375b237f6 --- /dev/null +++ b/sdk/note-bindings/macros/src/lib.rs @@ -0,0 +1,319 @@ +//! Procedural macros that generate typed host bindings for Miden note storage. + +#![deny(missing_docs)] + +extern crate proc_macro; + +use miden_note_schema::{NotePackageArtifact, NotePackageResolver, NoteStorageSchema}; +use miden_note_schema_codegen::generate_host_types; +use proc_macro::TokenStream; +use proc_macro_crate::{FoundCrate, crate_name}; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::{ + LitStr, Token, parse::Parser, parse_macro_input, punctuated::Punctuated, visit_mut::VisitMut, +}; + +/// Generates typed bindings from the freshest package built by a Miden project. +/// +/// The project path is relative to `CARGO_MANIFEST_DIR`. Build the note project with +/// `cargo miden build` before compiling the consumer. +#[proc_macro] +pub fn from_project(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand_from_project(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates typed bindings from one exact Miden package path. +/// +/// A relative path is resolved against `CARGO_MANIFEST_DIR`. +#[proc_macro] +pub fn from_package(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand_from_package(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates typed bindings from WIT text for internal tests. +#[doc(hidden)] +#[proc_macro] +pub fn from_wit_text(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand_from_wit_text(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Expands a project-relative note binding request. +fn expand_from_project(input: &LitStr) -> syn::Result { + let artifact = NotePackageResolver::new("miden-note-bindings") + .from_project(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_package_artifact(&artifact, input.span()) +} + +/// Expands an exact package binding request. +fn expand_from_package(input: &LitStr) -> syn::Result { + let artifact = NotePackageResolver::new("miden-note-bindings") + .from_package(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_package_artifact(&artifact, input.span()) +} + +/// Expands bindings from a loaded package and tracks the artifact as a macro input. +fn expand_package_artifact( + artifact: &NotePackageArtifact, + span: Span, +) -> syn::Result { + let scope_key = artifact.path().to_string_lossy(); + let bindings = expand_schema(artifact.schema(), span, &scope_key)?; + let tracked_path = artifact.path().to_string_lossy(); + Ok(quote! { + #[doc(hidden)] + const _: &[u8] = include_bytes!(#tracked_path); + #bindings + }) +} + +/// Expands bindings from a WIT string literal. +fn expand_from_wit_text(input: &LitStr) -> syn::Result { + let schema = NoteStorageSchema::from_wit_text(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_schema(&schema, input.span(), &input.value()) +} + +/// Adds the typed consumer API to shared generated host types. +fn expand_schema( + schema: &NoteStorageSchema, + span: Span, + scope_key: &str, +) -> syn::Result { + let generated = + generate_host_types(schema).map_err(|error| syn::Error::new(span, error.to_string()))?; + let facade_path = binding_facade(); + let type_tokens = rewrite_runtime_paths(generated.tokens().clone(), &facade_path)?; + let root_ident = generated.root_ident(); + let type_idents = generated.type_idents(); + let wit_text = schema.wit_text(); + let scope_ident = format_ident!("__miden_note_bindings_{:016x}", stable_hash(scope_key)); + let runtime = quote!(#facade_path::__private); + + Ok(quote! { + #[doc(hidden)] + mod #scope_ident { + #type_tokens + + #[doc(hidden)] + const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = #wit_text; + + #[doc(hidden)] + fn __miden_note_storage_schema( + ) -> #runtime::miden_note_schema::Result<#runtime::miden_note_schema::NoteStorageSchema> { + #runtime::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) + } + + impl #root_ident { + /// Encodes this typed value as note storage in WIT declaration order. + pub fn to_note_storage( + &self, + ) -> #runtime::miden_note_schema::Result<#runtime::miden_note_schema::NoteStorage> { + let mut felts = Vec::new(); + self.__write_note_felts( + &mut #runtime::miden_field_repr::FeltWriter::new(&mut felts), + )?; + #runtime::miden_note_schema::NoteStorage::new(felts).map_err(|error| { + #runtime::miden_note_schema::Error::new(format!( + "failed to create note storage: {error}" + )) + }) + } + + /// Decodes this typed value from complete note storage. + pub fn from_note_storage( + storage: &#runtime::miden_note_schema::NoteStorage, + ) -> #runtime::miden_note_schema::Result { + let mut reader = #runtime::miden_field_repr::FeltReader::new(storage.items()); + let value = Self::__read_note_felts(&mut reader)?; + reader.ensure_eof().map_err(|error| { + #runtime::miden_note_schema::Error::new(format!( + "note storage has trailing data: {error}" + )) + })?; + Ok(value) + } + + /// Builds a typed value with a caller-provided codec registry. + pub fn from_str_values_with( + values: &::std::collections::BTreeMap, + codecs: &#runtime::miden_note_schema::CodecRegistry, + ) -> #runtime::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder_with_registry(codecs); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + + /// Builds a typed value with the standard codec registry. + pub fn from_str_values( + values: &::std::collections::BTreeMap, + ) -> #runtime::miden_note_schema::Result { + let codecs = + #runtime::miden_note_schema::CodecRegistry::with_standard_codecs(); + Self::from_str_values_with(values, &codecs) + } + + /// Validates this value with structural rules and caller-provided codecs. + pub fn validate_with( + &self, + codecs: &#runtime::miden_note_schema::CodecRegistry, + ) -> #runtime::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(()) + } + + /// Validates this value with the standard codec registry. + pub fn validate(&self) -> #runtime::miden_note_schema::Result<()> { + let codecs = + #runtime::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.validate_with(&codecs) + } + + /// Displays this value with caller-provided codecs and structural fallbacks. + pub fn display_with( + &self, + codecs: &#runtime::miden_note_schema::CodecRegistry, + ) -> #runtime::miden_note_schema::Result { + let storage = self.to_note_storage()?; + let decoded = + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(decoded.to_string()) + } + + /// Displays this value with standard codecs and structural fallbacks. + pub fn display(&self) -> #runtime::miden_note_schema::Result { + let codecs = + #runtime::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.display_with(&codecs) + } + } + } + + pub use #scope_ident::{#(#type_idents),*}; + }) +} + +/// Resolves the bindings facade path in the consuming crate. +fn binding_facade() -> syn::Path { + match crate_name("miden-note-bindings") { + Ok(FoundCrate::Itself) => syn::parse_quote!(crate), + Ok(FoundCrate::Name(name)) => { + let ident = syn::Ident::new(&name, Span::call_site()); + syn::parse_quote!(::#ident) + } + Err(_) => syn::parse_quote!(::miden_note_bindings), + } +} + +/// Rewrites shared generated paths through the bindings facade. +fn rewrite_runtime_paths(tokens: TokenStream2, facade: &syn::Path) -> syn::Result { + let mut file = syn::parse2::(tokens)?; + RuntimePathRewriter { facade }.visit_file_mut(&mut file); + Ok(quote!(#file)) +} + +/// Rewrites runtime crate paths in shared host-profile code generation. +struct RuntimePathRewriter<'a> { + facade: &'a syn::Path, +} + +impl VisitMut for RuntimePathRewriter<'_> { + fn visit_item_struct_mut(&mut self, item: &mut syn::ItemStruct) { + add_felt_repr_crate_path(&mut item.attrs, self.facade); + syn::visit_mut::visit_item_struct_mut(self, item); + } + + fn visit_item_enum_mut(&mut self, item: &mut syn::ItemEnum) { + add_felt_repr_crate_path(&mut item.attrs, self.facade); + syn::visit_mut::visit_item_enum_mut(self, item); + } + + fn visit_attribute_mut(&mut self, attribute: &mut syn::Attribute) { + if attribute.path().is_ident("derive") { + let parser = Punctuated::::parse_terminated; + let mut paths = parser + .parse2(attribute.meta.require_list().expect("derive is a list").tokens.clone()) + .expect("generated derive paths must parse"); + for path in &mut paths { + self.visit_path_mut(path); + } + attribute.meta = syn::parse_quote!(derive(#paths)); + return; + } + syn::visit_mut::visit_attribute_mut(self, attribute); + } + + fn visit_path_mut(&mut self, path: &mut syn::Path) { + let Some(first) = path.segments.first() else { + return; + }; + if path.leading_colon.is_none() + || !matches!( + first.ident.to_string().as_str(), + "miden_field" | "miden_field_repr" | "miden_note_schema" | "miden_protocol" + ) + { + syn::visit_mut::visit_path_mut(self, path); + return; + } + + let crate_name = first.ident.clone(); + let tail = path.segments.iter().skip(1).cloned().collect::>(); + let facade = self.facade; + let mut rewritten: syn::Path = syn::parse_quote!(#facade::__private::#crate_name); + rewritten.segments.extend(tail); + *path = rewritten; + } +} + +/// Selects the facade's felt representation runtime for generated derives. +fn add_felt_repr_crate_path(attributes: &mut Vec, facade: &syn::Path) { + let has_felt_repr_derive = attributes.iter().any(|attribute| { + if !attribute.path().is_ident("derive") { + return false; + } + let parser = Punctuated::::parse_terminated; + parser + .parse2(attribute.meta.require_list().expect("derive is a list").tokens.clone()) + .expect("generated derive paths must parse") + .iter() + .any(|path| { + path.segments.last().is_some_and(|segment| { + matches!(segment.ident.to_string().as_str(), "ToFeltRepr" | "FromFeltRepr") + }) + }) + }); + if has_felt_repr_derive { + let path = format!("{}::__private::miden_field_repr", quote!(#facade)).replace(' ', ""); + let path = LitStr::new(&path, Span::call_site()); + attributes.push(syn::parse_quote!(#[felt_repr(crate_path = #path)])); + } +} + +/// Returns a deterministic scope suffix for one macro input. +fn stable_hash(value: &str) -> u64 { + value.bytes().fold(0xcbf2_9ce4_8422_2325, |hash, byte| { + (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3) + }) +} + +#[cfg(test)] +mod tests; diff --git a/sdk/note-bindings/src/tests.rs b/sdk/note-bindings/macros/src/tests.rs similarity index 53% rename from sdk/note-bindings/src/tests.rs rename to sdk/note-bindings/macros/src/tests.rs index fb8143c8ac..b6a6ba6fe3 100644 --- a/sdk/note-bindings/src/tests.rs +++ b/sdk/note-bindings/macros/src/tests.rs @@ -1,11 +1,9 @@ -//! Tests for macro expansion and package discovery. - -use std::{fs, thread, time::Duration}; +//! Tests for macro expansion. use midenc_expect_test::expect_file; use syn::LitStr; -use crate::{expand_from_wit_text, freshest_project_package, missing_project_package_message}; +use crate::expand_from_wit_text; const P2ID_SCHEMA: &str = r#" package example:p2id-schema@1.0.0; @@ -76,36 +74,10 @@ fn expand(wit: &str) -> String { #[test] fn expands_p2id_schema_golden() { - expect_file!["expected/p2id.rs"].assert_eq(&expand(P2ID_SCHEMA)); + expect_file!["../../src/expected/p2id.rs"].assert_eq(&expand(P2ID_SCHEMA)); } #[test] fn expands_custom_schema_golden() { - expect_file!["expected/custom.rs"].assert_eq(&expand(CUSTOM_SCHEMA)); -} - -#[test] -fn selects_the_freshest_package_across_profiles() { - let temp = tempfile::tempdir().unwrap(); - let debug = temp.path().join("target/miden/debug"); - let release = temp.path().join("target/miden/release"); - fs::create_dir_all(&debug).unwrap(); - fs::create_dir_all(&release).unwrap(); - fs::write(debug.join("note.masp"), b"old").unwrap(); - thread::sleep(Duration::from_millis(20)); - fs::write(release.join("note.masp"), b"new").unwrap(); - - let selected = freshest_project_package(temp.path(), proc_macro2::Span::call_site()) - .unwrap() - .unwrap(); - assert_eq!(selected, release.join("note.masp")); -} - -#[test] -fn missing_package_diagnostic_names_build_command() { - let temp = tempfile::tempdir().unwrap(); - fs::write(temp.path().join("Cargo.toml"), "[package]\nname='note'\nversion='0.1.0'").unwrap(); - let message = missing_project_package_message(temp.path()); - assert!(message.contains("cargo miden build --manifest-path")); - assert!(message.contains("--release")); + expect_file!["../../src/expected/custom.rs"].assert_eq(&expand(CUSTOM_SCHEMA)); } diff --git a/sdk/note-bindings/src/expected/custom.rs b/sdk/note-bindings/src/expected/custom.rs index 5e9dbe1752..7b76ed456a 100644 --- a/sdk/note-bindings/src/expected/custom.rs +++ b/sdk/note-bindings/src/expected/custom.rs @@ -1,479 +1,623 @@ #[doc(hidden)] -trait __MidenNoteEncode { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()>; -} -#[doc(hidden)] -trait __MidenNoteDecode: Sized { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result; -} -impl __MidenNoteEncode for u64 { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) +mod __miden_note_bindings_a3280bdaca3ec21e { + #[doc(hidden)] + trait __MidenNoteEncode { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()>; } -} -impl __MidenNoteDecode for u64 { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(u64), - ), - ) - }) + #[doc(hidden)] + trait __MidenNoteDecode: Sized { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result; } -} -impl __MidenNoteEncode for u32 { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for u64 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for u32 { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(u32), - ), + impl __MidenNoteDecode for u64 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u64), + ), + ) + }) + } } -} -impl __MidenNoteEncode for u8 { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for u32 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for u8 { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", stringify!(u8), - ), + impl __MidenNoteDecode for u32 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u32), + ), + ) + }) + } } -} -impl __MidenNoteEncode for bool { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for u8 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for bool { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(bool), - ), + impl __MidenNoteDecode for u8 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u8), + ), + ) + }) + } } -} -impl __MidenNoteEncode for ::miden_field::Felt { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for bool { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for ::miden_field::Felt { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(::miden_field::Felt), - ), + impl __MidenNoteDecode for bool { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(bool), + ), + ) + }) + } } -} -impl __MidenNoteEncode for ::miden_field::Word { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for ::miden_note_bindings::__private::miden_field::Felt { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for ::miden_field::Word { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(::miden_field::Word), - ), + impl __MidenNoteDecode for ::miden_note_bindings::__private::miden_field::Felt { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_field::Felt), + ), + ) + }) + } } -} -impl __MidenNoteEncode for ::miden_protocol::account::AccountId { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - writer.write(self.prefix().as_felt()); - writer.write(self.suffix()); - Ok(()) + impl __MidenNoteEncode for ::miden_note_bindings::__private::miden_field::Word { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for ::miden_protocol::account::AccountId { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - let prefix = reader - .read() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to decode account-id prefix: {error}"), - ) - })?; - let suffix = reader - .read() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to decode account-id suffix: {error}"), - ) - })?; - ::miden_protocol::account::AccountId::try_from_elements(suffix, prefix) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("invalid account-id in note storage: {error}"), + impl __MidenNoteDecode for ::miden_note_bindings::__private::miden_field::Word { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_field::Word), + ), + ) + }) + } } -} -impl __MidenNoteEncode for ::miden_protocol::asset::AssetAmount { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - writer.write(::miden_field::Felt::from(*self)); - Ok(()) + impl __MidenNoteEncode + for ::miden_note_bindings::__private::miden_protocol::account::AccountId { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + writer.write(self.prefix().as_felt()); + writer.write(self.suffix()); + Ok(()) + } } -} -impl __MidenNoteDecode for ::miden_protocol::asset::AssetAmount { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - let value = reader - .read() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to decode asset-amount: {error}"), + impl __MidenNoteDecode + for ::miden_note_bindings::__private::miden_protocol::account::AccountId { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let prefix = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode account-id prefix: {error}"), + ) + })?; + let suffix = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode account-id suffix: {error}"), + ) + })?; + ::miden_note_bindings::__private::miden_protocol::account::AccountId::try_from_elements( + suffix, + prefix, ) - })?; - ::miden_protocol::asset::AssetAmount::try_from(value) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("invalid asset-amount in note storage: {error}"), + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid account-id in note storage: {error}"), + ) + }) + } + } + impl __MidenNoteEncode + for ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::from(*self)); + Ok(()) + } + } + impl __MidenNoteDecode + for ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let value = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode asset-amount: {error}"), + ) + })?; + ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount::try_from( + value, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid asset-amount in note storage: {error}"), + ) + }) + } } -} -impl __MidenNoteEncode for Option -where - T: __MidenNoteEncode, -{ - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - match self { - None => writer.write(::miden_field::Felt::ZERO), - Some(value) => { - writer.write(::miden_field::Felt::ONE); - value.__write_note_felts(writer)?; + impl __MidenNoteEncode for Option + where + T: __MidenNoteEncode, + { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + match self { + None => { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::ZERO) + } + Some(value) => { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::ONE); + value.__write_note_felts(writer)?; + } } + Ok(()) } - Ok(()) } -} -impl __MidenNoteDecode for Option -where - T: __MidenNoteDecode, -{ - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - let tag = reader - .read() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to decode option tag: {error}"), - ) - })?; - match tag.as_canonical_u64() { - 0 => Ok(None), - 1 => Ok(Some(T::__read_note_felts(reader)?)), - tag => { - Err( - ::miden_note_schema::Error::new( - format!("invalid option tag {tag}; expected 0 or 1"), - ), - ) + impl __MidenNoteDecode for Option + where + T: __MidenNoteDecode, + { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let tag = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode option tag: {error}"), + ) + })?; + match tag.as_canonical_u64() { + 0 => Ok(None), + 1 => Ok(Some(T::__read_note_felts(reader)?)), + tag => { + Err( + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid option tag {tag}; expected 0 or 1"), + ), + ) + } } } } -} -///Rust binding for WIT type `example:dex-schema/note-storage@1.0.0.dex-note`. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct DexNote { - ///Value of the WIT `target` field. - pub target: ::miden_protocol::account::AccountId, - ///Value of the WIT `kind` field. - pub kind: OrderKind, -} -impl DexNote { - /// The canonical fully-qualified WIT name for this type. - pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.dex-note"; -} -impl __MidenNoteEncode for DexNote { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - self.target.__write_note_felts(writer)?; - self.kind.__write_note_felts(writer)?; - Ok(()) + ///Rust binding for WIT type `example:dex-schema/note-storage@1.0.0.dex-note`. + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct DexNote { + ///Value of the WIT `target` field. + pub target: ::miden_note_bindings::__private::miden_protocol::account::AccountId, + ///Value of the WIT `kind` field. + pub kind: OrderKind, } -} -impl __MidenNoteDecode for DexNote { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - Ok(Self { - target: <::miden_protocol::account::AccountId as __MidenNoteDecode>::__read_note_felts( - reader, - )?, - kind: ::__read_note_felts(reader)?, - }) + impl DexNote { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.dex-note"; } -} -///Selects order execution. -#[derive( - Clone, - Debug, - PartialEq, - Eq, - ::miden_field_repr::ToFeltRepr, - ::miden_field_repr::FromFeltRepr, -)] -pub enum OrderKind { - ///WIT `market` case. - Market, - ///WIT `limit` case. - Limit(LimitPrice), -} -impl OrderKind { - /// The canonical fully-qualified WIT name for this type. - pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.order-kind"; -} -impl __MidenNoteEncode for OrderKind { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - match self { - Self::Market => { - writer.write(::miden_field::Felt::from_u32(0u32)); - } - Self::Limit(value) => { - writer.write(::miden_field::Felt::from_u32(1u32)); - value.__write_note_felts(writer)?; - } + impl __MidenNoteEncode for DexNote { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + self.target.__write_note_felts(writer)?; + self.kind.__write_note_felts(writer)?; + Ok(()) } - Ok(()) } -} -impl __MidenNoteDecode for OrderKind { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - let tag = reader - .read_u32() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to decode {} tag: {error}", stringify!(OrderKind),), - ) - })?; - match tag { - 0u32 => Ok(Self::Market), - 1u32 => { - Ok( - Self::Limit( - ::__read_note_felts(reader)?, - ), - ) + impl __MidenNoteDecode for DexNote { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + Ok(Self { + target: <::miden_note_bindings::__private::miden_protocol::account::AccountId as __MidenNoteDecode>::__read_note_felts( + reader, + )?, + kind: ::__read_note_felts(reader)?, + }) + } + } + ///Selects order execution. + #[derive( + Clone, + Debug, + PartialEq, + Eq, + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr, + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr, + )] + #[felt_repr(crate_path = "::miden_note_bindings::__private::miden_field_repr")] + pub enum OrderKind { + ///WIT `market` case. + Market, + ///WIT `limit` case. + Limit(LimitPrice), + } + impl OrderKind { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.order-kind"; + } + impl __MidenNoteEncode for OrderKind { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + match self { + Self::Market => { + writer + .write( + ::miden_note_bindings::__private::miden_field::Felt::from_u32( + 0u32, + ), + ); + } + Self::Limit(value) => { + writer + .write( + ::miden_note_bindings::__private::miden_field::Felt::from_u32( + 1u32, + ), + ); + value.__write_note_felts(writer)?; + } } - tag => { - Err( - ::miden_note_schema::Error::new( + Ok(()) + } + } + impl __MidenNoteDecode for OrderKind { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let tag = reader + .read_u32() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( format!( - "invalid {} tag {tag}; expected a declaration ordinal below {}", - stringify!(OrderKind), 2usize, + "failed to decode {} tag: {error}", stringify!(OrderKind), ), - ), - ) + ) + })?; + match tag { + 0u32 => Ok(Self::Market), + 1u32 => { + Ok( + Self::Limit( + ::__read_note_felts(reader)?, + ), + ) + } + tag => { + Err( + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "invalid {} tag {tag}; expected a declaration ordinal below {}", + stringify!(OrderKind), 2usize, + ), + ), + ) + } } } } -} -///A ratio used as an order limit. -#[derive( - Clone, - Debug, - PartialEq, - Eq, - ::miden_field_repr::ToFeltRepr, - ::miden_field_repr::FromFeltRepr, -)] -pub struct LimitPrice { - ///Value of the WIT `numerator` field. - pub numerator: u64, - ///Value of the WIT `denominator` field. - pub denominator: u64, -} -impl LimitPrice { - /// The canonical fully-qualified WIT name for this type. - pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.limit-price"; -} -impl __MidenNoteEncode for LimitPrice { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - self.numerator.__write_note_felts(writer)?; - self.denominator.__write_note_felts(writer)?; - Ok(()) + ///A ratio used as an order limit. + #[derive( + Clone, + Debug, + PartialEq, + Eq, + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr, + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr, + )] + #[felt_repr(crate_path = "::miden_note_bindings::__private::miden_field_repr")] + pub struct LimitPrice { + ///Value of the WIT `numerator` field. + pub numerator: u64, + ///Value of the WIT `denominator` field. + pub denominator: u64, } -} -impl __MidenNoteDecode for LimitPrice { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - Ok(Self { - numerator: ::__read_note_felts(reader)?, - denominator: ::__read_note_felts(reader)?, - }) + impl LimitPrice { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.limit-price"; } -} -#[doc(hidden)] -const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:dex-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n /// A ratio used as an order limit.\n record limit-price {\n numerator: u64,\n denominator: u64,\n }\n\n /// Selects order execution.\n variant order-kind {\n market,\n limit(limit-price),\n }\n\n record dex-note {\n target: account-id,\n kind: order-kind,\n }\n\n type storage = dex-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; -#[doc(hidden)] -fn __miden_note_storage_schema() -> ::miden_note_schema::Result< - ::miden_note_schema::NoteStorageSchema, -> { - ::miden_note_schema::NoteStorageSchema::from_wit_text( - __MIDEN_NOTE_STORAGE_SCHEMA_WIT, - ) -} -impl DexNote { - /// Encodes this typed value as note storage in WIT declaration order. - pub fn to_note_storage( - &self, - ) -> ::miden_note_schema::Result<::miden_note_schema::NoteStorage> { - let mut felts = Vec::new(); - self.__write_note_felts(&mut ::miden_field_repr::FeltWriter::new(&mut felts))?; - ::miden_note_schema::NoteStorage::new(felts) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to create note storage: {error}"), - ) + impl __MidenNoteEncode for LimitPrice { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + self.numerator.__write_note_felts(writer)?; + self.denominator.__write_note_felts(writer)?; + Ok(()) + } + } + impl __MidenNoteDecode for LimitPrice { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + Ok(Self { + numerator: ::__read_note_felts(reader)?, + denominator: ::__read_note_felts(reader)?, }) + } } - /// Decodes this typed value from complete note storage. - pub fn from_note_storage( - storage: &::miden_note_schema::NoteStorage, - ) -> ::miden_note_schema::Result { - let mut reader = ::miden_field_repr::FeltReader::new(storage.items()); - let value = Self::__read_note_felts(&mut reader)?; - reader - .ensure_eof() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("note storage has trailing data: {error}"), - ) - })?; - Ok(value) - } - /// Builds a typed value from normalized string paths and a codec registry. - pub fn from_str_values( - values: &::std::collections::BTreeMap, - codecs: &::miden_note_schema::CodecRegistry, - ) -> ::miden_note_schema::Result { - let schema = __miden_note_storage_schema()?; - let mut builder = schema.builder_with_registry(codecs); - for (path, value) in values { - builder = builder.set(path, value)?; - } - let storage = builder.build()?; - Self::from_note_storage(&storage) - } - /// Validates this value with structural rules and the supplied codecs. - pub fn validate_with( - &self, - codecs: &::miden_note_schema::CodecRegistry, - ) -> ::miden_note_schema::Result<()> { - let storage = self.to_note_storage()?; - __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; - Ok(()) - } - /// Displays this value with the supplied codecs and structural fallbacks. - pub fn display_with( - &self, - codecs: &::miden_note_schema::CodecRegistry, - ) -> ::miden_note_schema::Result { - let storage = self.to_note_storage()?; - let decoded = __miden_note_storage_schema()? - .decode_with_registry(&storage, codecs)?; - Ok(decoded.to_string()) + #[doc(hidden)] + const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:dex-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n /// A ratio used as an order limit.\n record limit-price {\n numerator: u64,\n denominator: u64,\n }\n\n /// Selects order execution.\n variant order-kind {\n market,\n limit(limit-price),\n }\n\n record dex-note {\n target: account-id,\n kind: order-kind,\n }\n\n type storage = dex-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; + #[doc(hidden)] + fn __miden_note_storage_schema() -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + > { + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) + } + impl DexNote { + /// Encodes this typed value as note storage in WIT declaration order. + pub fn to_note_storage( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorage, + > { + let mut felts = Vec::new(); + self.__write_note_felts( + &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter::new( + &mut felts, + ), + )?; + ::miden_note_bindings::__private::miden_note_schema::NoteStorage::new(felts) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to create note storage: {error}"), + ) + }) + } + /// Decodes this typed value from complete note storage. + pub fn from_note_storage( + storage: &::miden_note_bindings::__private::miden_note_schema::NoteStorage, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let mut reader = ::miden_note_bindings::__private::miden_field_repr::FeltReader::new( + storage.items(), + ); + let value = Self::__read_note_felts(&mut reader)?; + reader + .ensure_eof() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("note storage has trailing data: {error}"), + ) + })?; + Ok(value) + } + /// Builds a typed value with a caller-provided codec registry. + pub fn from_str_values_with( + values: &::std::collections::BTreeMap, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder_with_registry(codecs); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + /// Builds a typed value with the standard codec registry. + pub fn from_str_values( + values: &::std::collections::BTreeMap, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + Self::from_str_values_with(values, &codecs) + } + /// Validates this value with structural rules and caller-provided codecs. + pub fn validate_with( + &self, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(()) + } + /// Validates this value with the standard codec registry. + pub fn validate( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.validate_with(&codecs) + } + /// Displays this value with caller-provided codecs and structural fallbacks. + pub fn display_with( + &self, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let storage = self.to_note_storage()?; + let decoded = __miden_note_storage_schema()? + .decode_with_registry(&storage, codecs)?; + Ok(decoded.to_string()) + } + /// Displays this value with standard codecs and structural fallbacks. + pub fn display( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.display_with(&codecs) + } } } +pub use __miden_note_bindings_a3280bdaca3ec21e::{DexNote, OrderKind, LimitPrice}; diff --git a/sdk/note-bindings/src/expected/p2id.rs b/sdk/note-bindings/src/expected/p2id.rs index a27ecd4b83..bdb4fcac7f 100644 --- a/sdk/note-bindings/src/expected/p2id.rs +++ b/sdk/note-bindings/src/expected/p2id.rs @@ -1,359 +1,489 @@ #[doc(hidden)] -trait __MidenNoteEncode { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()>; -} -#[doc(hidden)] -trait __MidenNoteDecode: Sized { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result; -} -impl __MidenNoteEncode for u64 { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) +mod __miden_note_bindings_f74ea5e7a6e77b2d { + #[doc(hidden)] + trait __MidenNoteEncode { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()>; } -} -impl __MidenNoteDecode for u64 { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(u64), - ), - ) - }) + #[doc(hidden)] + trait __MidenNoteDecode: Sized { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result; } -} -impl __MidenNoteEncode for u32 { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for u64 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for u32 { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(u32), - ), + impl __MidenNoteDecode for u64 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u64), + ), + ) + }) + } } -} -impl __MidenNoteEncode for u8 { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for u32 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for u8 { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", stringify!(u8), - ), + impl __MidenNoteDecode for u32 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u32), + ), + ) + }) + } } -} -impl __MidenNoteEncode for bool { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for u8 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for bool { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(bool), - ), + impl __MidenNoteDecode for u8 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u8), + ), + ) + }) + } } -} -impl __MidenNoteEncode for ::miden_field::Felt { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for bool { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for ::miden_field::Felt { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(::miden_field::Felt), - ), + impl __MidenNoteDecode for bool { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(bool), + ), + ) + }) + } } -} -impl __MidenNoteEncode for ::miden_field::Word { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); - Ok(()) + impl __MidenNoteEncode for ::miden_note_bindings::__private::miden_field::Felt { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for ::miden_field::Word { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!( - "failed to decode {} from note storage: {error}", - stringify!(::miden_field::Word), - ), + impl __MidenNoteDecode for ::miden_note_bindings::__private::miden_field::Felt { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_field::Felt), + ), + ) + }) + } } -} -impl __MidenNoteEncode for ::miden_protocol::account::AccountId { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - writer.write(self.prefix().as_felt()); - writer.write(self.suffix()); - Ok(()) + impl __MidenNoteEncode for ::miden_note_bindings::__private::miden_field::Word { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } } -} -impl __MidenNoteDecode for ::miden_protocol::account::AccountId { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - let prefix = reader - .read() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to decode account-id prefix: {error}"), - ) - })?; - let suffix = reader - .read() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to decode account-id suffix: {error}"), + impl __MidenNoteDecode for ::miden_note_bindings::__private::miden_field::Word { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, ) - })?; - ::miden_protocol::account::AccountId::try_from_elements(suffix, prefix) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("invalid account-id in note storage: {error}"), - ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_field::Word), + ), + ) + }) + } } -} -impl __MidenNoteEncode for ::miden_protocol::asset::AssetAmount { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - writer.write(::miden_field::Felt::from(*self)); - Ok(()) + impl __MidenNoteEncode + for ::miden_note_bindings::__private::miden_protocol::account::AccountId { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + writer.write(self.prefix().as_felt()); + writer.write(self.suffix()); + Ok(()) + } } -} -impl __MidenNoteDecode for ::miden_protocol::asset::AssetAmount { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - let value = reader - .read() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to decode asset-amount: {error}"), + impl __MidenNoteDecode + for ::miden_note_bindings::__private::miden_protocol::account::AccountId { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let prefix = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode account-id prefix: {error}"), + ) + })?; + let suffix = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode account-id suffix: {error}"), + ) + })?; + ::miden_note_bindings::__private::miden_protocol::account::AccountId::try_from_elements( + suffix, + prefix, ) - })?; - ::miden_protocol::asset::AssetAmount::try_from(value) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("invalid asset-amount in note storage: {error}"), + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid account-id in note storage: {error}"), + ) + }) + } + } + impl __MidenNoteEncode + for ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::from(*self)); + Ok(()) + } + } + impl __MidenNoteDecode + for ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let value = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode asset-amount: {error}"), + ) + })?; + ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount::try_from( + value, ) - }) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid asset-amount in note storage: {error}"), + ) + }) + } } -} -impl __MidenNoteEncode for Option -where - T: __MidenNoteEncode, -{ - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - match self { - None => writer.write(::miden_field::Felt::ZERO), - Some(value) => { - writer.write(::miden_field::Felt::ONE); - value.__write_note_felts(writer)?; + impl __MidenNoteEncode for Option + where + T: __MidenNoteEncode, + { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + match self { + None => { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::ZERO) + } + Some(value) => { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::ONE); + value.__write_note_felts(writer)?; + } } + Ok(()) } - Ok(()) } -} -impl __MidenNoteDecode for Option -where - T: __MidenNoteDecode, -{ - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - let tag = reader - .read() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to decode option tag: {error}"), - ) - })?; - match tag.as_canonical_u64() { - 0 => Ok(None), - 1 => Ok(Some(T::__read_note_felts(reader)?)), - tag => { - Err( - ::miden_note_schema::Error::new( - format!("invalid option tag {tag}; expected 0 or 1"), - ), - ) + impl __MidenNoteDecode for Option + where + T: __MidenNoteDecode, + { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let tag = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode option tag: {error}"), + ) + })?; + match tag.as_canonical_u64() { + 0 => Ok(None), + 1 => Ok(Some(T::__read_note_felts(reader)?)), + tag => { + Err( + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid option tag {tag}; expected 0 or 1"), + ), + ) + } } } } -} -///Rust binding for WIT type `example:p2id-schema/note-storage@1.0.0.p2id-note`. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct P2idNote { - ///Value of the WIT `target-account-id` field. - pub target_account_id: ::miden_protocol::account::AccountId, -} -impl P2idNote { - /// The canonical fully-qualified WIT name for this type. - pub const WIT_FQN: &'static str = "example:p2id-schema/note-storage@1.0.0.p2id-note"; -} -impl __MidenNoteEncode for P2idNote { - fn __write_note_felts( - &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - self.target_account_id.__write_note_felts(writer)?; - Ok(()) + ///Rust binding for WIT type `example:p2id-schema/note-storage@1.0.0.p2id-note`. + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct P2idNote { + ///Value of the WIT `target-account-id` field. + pub target_account_id: ::miden_note_bindings::__private::miden_protocol::account::AccountId, } -} -impl __MidenNoteDecode for P2idNote { - fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - Ok(Self { - target_account_id: <::miden_protocol::account::AccountId as __MidenNoteDecode>::__read_note_felts( - reader, - )?, - }) + impl P2idNote { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:p2id-schema/note-storage@1.0.0.p2id-note"; } -} -#[doc(hidden)] -const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:p2id-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n record p2id-note {\n target-account-id: account-id,\n }\n\n type storage = p2id-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; -#[doc(hidden)] -fn __miden_note_storage_schema() -> ::miden_note_schema::Result< - ::miden_note_schema::NoteStorageSchema, -> { - ::miden_note_schema::NoteStorageSchema::from_wit_text( - __MIDEN_NOTE_STORAGE_SCHEMA_WIT, - ) -} -impl P2idNote { - /// Encodes this typed value as note storage in WIT declaration order. - pub fn to_note_storage( - &self, - ) -> ::miden_note_schema::Result<::miden_note_schema::NoteStorage> { - let mut felts = Vec::new(); - self.__write_note_felts(&mut ::miden_field_repr::FeltWriter::new(&mut felts))?; - ::miden_note_schema::NoteStorage::new(felts) - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("failed to create note storage: {error}"), - ) - }) - } - /// Decodes this typed value from complete note storage. - pub fn from_note_storage( - storage: &::miden_note_schema::NoteStorage, - ) -> ::miden_note_schema::Result { - let mut reader = ::miden_field_repr::FeltReader::new(storage.items()); - let value = Self::__read_note_felts(&mut reader)?; - reader - .ensure_eof() - .map_err(|error| { - ::miden_note_schema::Error::new( - format!("note storage has trailing data: {error}"), - ) - })?; - Ok(value) + impl __MidenNoteEncode for P2idNote { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + self.target_account_id.__write_note_felts(writer)?; + Ok(()) + } } - /// Builds a typed value from normalized string paths. - pub fn from_str_values( - values: &::std::collections::BTreeMap, - ) -> ::miden_note_schema::Result { - let schema = __miden_note_storage_schema()?; - let mut builder = schema.builder(); - for (path, value) in values { - builder = builder.set(path, value)?; + impl __MidenNoteDecode for P2idNote { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + Ok(Self { + target_account_id: <::miden_note_bindings::__private::miden_protocol::account::AccountId as __MidenNoteDecode>::__read_note_felts( + reader, + )?, + }) } - let storage = builder.build()?; - Self::from_note_storage(&storage) } - /// Validates this value with structural rules and standard codecs. - pub fn validate_with(&self) -> ::miden_note_schema::Result<()> { - let storage = self.to_note_storage()?; - __miden_note_storage_schema()?.decode(&storage)?; - Ok(()) + #[doc(hidden)] + const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:p2id-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n record p2id-note {\n target-account-id: account-id,\n }\n\n type storage = p2id-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; + #[doc(hidden)] + fn __miden_note_storage_schema() -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + > { + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) } - /// Displays this value with standard codecs and structural fallbacks. - pub fn display_with(&self) -> ::miden_note_schema::Result { - let storage = self.to_note_storage()?; - let decoded = __miden_note_storage_schema()?.decode(&storage)?; - Ok(decoded.to_string()) + impl P2idNote { + /// Encodes this typed value as note storage in WIT declaration order. + pub fn to_note_storage( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorage, + > { + let mut felts = Vec::new(); + self.__write_note_felts( + &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter::new( + &mut felts, + ), + )?; + ::miden_note_bindings::__private::miden_note_schema::NoteStorage::new(felts) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to create note storage: {error}"), + ) + }) + } + /// Decodes this typed value from complete note storage. + pub fn from_note_storage( + storage: &::miden_note_bindings::__private::miden_note_schema::NoteStorage, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let mut reader = ::miden_note_bindings::__private::miden_field_repr::FeltReader::new( + storage.items(), + ); + let value = Self::__read_note_felts(&mut reader)?; + reader + .ensure_eof() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("note storage has trailing data: {error}"), + ) + })?; + Ok(value) + } + /// Builds a typed value with a caller-provided codec registry. + pub fn from_str_values_with( + values: &::std::collections::BTreeMap, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder_with_registry(codecs); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + /// Builds a typed value with the standard codec registry. + pub fn from_str_values( + values: &::std::collections::BTreeMap, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + Self::from_str_values_with(values, &codecs) + } + /// Validates this value with structural rules and caller-provided codecs. + pub fn validate_with( + &self, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(()) + } + /// Validates this value with the standard codec registry. + pub fn validate( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.validate_with(&codecs) + } + /// Displays this value with caller-provided codecs and structural fallbacks. + pub fn display_with( + &self, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let storage = self.to_note_storage()?; + let decoded = __miden_note_storage_schema()? + .decode_with_registry(&storage, codecs)?; + Ok(decoded.to_string()) + } + /// Displays this value with standard codecs and structural fallbacks. + pub fn display( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.display_with(&codecs) + } } } +pub use __miden_note_bindings_f74ea5e7a6e77b2d::P2idNote; diff --git a/sdk/note-bindings/src/lib.rs b/sdk/note-bindings/src/lib.rs index cac72bba82..082f8b20fc 100644 --- a/sdk/note-bindings/src/lib.rs +++ b/sdk/note-bindings/src/lib.rs @@ -1,356 +1,19 @@ -//! Procedural macros that generate typed host bindings for Miden note storage. +//! Typed host bindings for Miden note storage schemas. #![deny(missing_docs)] -extern crate proc_macro; - -use std::{ - env, fs, - path::{Path, PathBuf}, -}; - -use miden_mast_package::Package; -use miden_note_schema::NoteStorageSchema; -use miden_note_schema_codegen::generate_host_types; -use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::quote; -use syn::{LitStr, parse_macro_input}; - -/// Generates typed bindings from the freshest package built by a Miden project. -/// -/// The project path is relative to `CARGO_MANIFEST_DIR`. Build the note project with -/// `cargo miden build` before compiling the consumer. -#[proc_macro] -pub fn from_project(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as LitStr); - expand_from_project(&input) - .unwrap_or_else(syn::Error::into_compile_error) - .into() -} - -/// Generates typed bindings from one exact Miden package path. -/// -/// A relative path is resolved against `CARGO_MANIFEST_DIR`. -#[proc_macro] -pub fn from_package(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as LitStr); - expand_from_package(&input) - .unwrap_or_else(syn::Error::into_compile_error) - .into() -} - -/// Generates typed bindings from WIT text for internal tests. +pub use miden_field_repr::*; #[doc(hidden)] -#[proc_macro] -pub fn from_wit_text(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as LitStr); - expand_from_wit_text(&input) - .unwrap_or_else(syn::Error::into_compile_error) - .into() -} - -/// Expands a project-relative note binding request. -fn expand_from_project(input: &LitStr) -> syn::Result { - let project_dir = resolve_manifest_path(&input.value(), input.span())?; - if !project_dir.is_dir() { - return Err(syn::Error::new( - input.span(), - format!("note project directory '{}' does not exist", project_dir.display()), - )); - } - let package_path = freshest_project_package(&project_dir, input.span())?.ok_or_else(|| { - syn::Error::new(input.span(), missing_project_package_message(&project_dir)) - })?; - expand_package_path(&package_path, input.span()) -} - -/// Expands an exact package binding request. -fn expand_from_package(input: &LitStr) -> syn::Result { - let package_path = resolve_manifest_path(&input.value(), input.span())?; - if !package_path.is_file() { - return Err(syn::Error::new( - input.span(), - format!("Miden package '{}' does not exist", package_path.display()), - )); - } - expand_package_path(&package_path, input.span()) -} - -/// Expands bindings from a loaded package and tracks the artifact as a macro input. -fn expand_package_path(package_path: &Path, span: Span) -> syn::Result { - let package = Package::deserialize_from_file(package_path).map_err(|error| { - syn::Error::new( - span, - format!("failed to read Miden package '{}': {error}", package_path.display()), - ) - })?; - let schema = NoteStorageSchema::from_package(&package).map_err(|error| { - syn::Error::new( - span, - format!( - "failed to read note storage schema from '{}': {error}", - package_path.display() - ), - ) - })?; - let bindings = expand_schema(&schema, span)?; - let tracked_path = package_path.to_string_lossy(); - Ok(quote! { - #[doc(hidden)] - const _: &[u8] = include_bytes!(#tracked_path); - #bindings - }) -} - -/// Expands bindings from a WIT string literal. -fn expand_from_wit_text(input: &LitStr) -> syn::Result { - let schema = NoteStorageSchema::from_wit_text(&input.value()) - .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; - expand_schema(&schema, input.span()) -} - -/// Adds the typed consumer API to shared generated host types. -fn expand_schema(schema: &NoteStorageSchema, span: Span) -> syn::Result { - let generated = - generate_host_types(schema).map_err(|error| syn::Error::new(span, error.to_string()))?; - let type_tokens = generated.tokens(); - let root_ident = generated.root_ident(); - let wit_text = schema.wit_text(); - - let (from_str_values, validate_with, display_with) = if generated.has_custom_types() { - ( - quote! { - /// Builds a typed value from normalized string paths and a codec registry. - pub fn from_str_values( - values: &::std::collections::BTreeMap, - codecs: &::miden_note_schema::CodecRegistry, - ) -> ::miden_note_schema::Result { - let schema = __miden_note_storage_schema()?; - let mut builder = schema.builder_with_registry(codecs); - for (path, value) in values { - builder = builder.set(path, value)?; - } - let storage = builder.build()?; - Self::from_note_storage(&storage) - } - }, - quote! { - /// Validates this value with structural rules and the supplied codecs. - pub fn validate_with( - &self, - codecs: &::miden_note_schema::CodecRegistry, - ) -> ::miden_note_schema::Result<()> { - let storage = self.to_note_storage()?; - __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; - Ok(()) - } - }, - quote! { - /// Displays this value with the supplied codecs and structural fallbacks. - pub fn display_with( - &self, - codecs: &::miden_note_schema::CodecRegistry, - ) -> ::miden_note_schema::Result { - let storage = self.to_note_storage()?; - let decoded = - __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; - Ok(decoded.to_string()) - } - }, - ) - } else { - ( - quote! { - /// Builds a typed value from normalized string paths. - pub fn from_str_values( - values: &::std::collections::BTreeMap, - ) -> ::miden_note_schema::Result { - let schema = __miden_note_storage_schema()?; - let mut builder = schema.builder(); - for (path, value) in values { - builder = builder.set(path, value)?; - } - let storage = builder.build()?; - Self::from_note_storage(&storage) - } - }, - quote! { - /// Validates this value with structural rules and standard codecs. - pub fn validate_with(&self) -> ::miden_note_schema::Result<()> { - let storage = self.to_note_storage()?; - __miden_note_storage_schema()?.decode(&storage)?; - Ok(()) - } - }, - quote! { - /// Displays this value with standard codecs and structural fallbacks. - pub fn display_with(&self) -> ::miden_note_schema::Result { - let storage = self.to_note_storage()?; - let decoded = __miden_note_storage_schema()?.decode(&storage)?; - Ok(decoded.to_string()) - } - }, - ) - }; - - Ok(quote! { - #type_tokens +pub use miden_note_bindings_macros::from_wit_text; +pub use miden_note_bindings_macros::{from_package, from_project}; +pub use miden_note_schema::{CodecRegistry, Error, NoteStorage, Result}; +pub use miden_protocol::{account, address, asset}; - #[doc(hidden)] - const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = #wit_text; - - #[doc(hidden)] - fn __miden_note_storage_schema( - ) -> ::miden_note_schema::Result<::miden_note_schema::NoteStorageSchema> { - ::miden_note_schema::NoteStorageSchema::from_wit_text( - __MIDEN_NOTE_STORAGE_SCHEMA_WIT, - ) - } - - impl #root_ident { - /// Encodes this typed value as note storage in WIT declaration order. - pub fn to_note_storage( - &self, - ) -> ::miden_note_schema::Result<::miden_note_schema::NoteStorage> { - let mut felts = Vec::new(); - self.__write_note_felts( - &mut ::miden_field_repr::FeltWriter::new(&mut felts), - )?; - ::miden_note_schema::NoteStorage::new(felts).map_err(|error| { - ::miden_note_schema::Error::new(format!( - "failed to create note storage: {error}" - )) - }) - } - - /// Decodes this typed value from complete note storage. - pub fn from_note_storage( - storage: &::miden_note_schema::NoteStorage, - ) -> ::miden_note_schema::Result { - let mut reader = ::miden_field_repr::FeltReader::new(storage.items()); - let value = Self::__read_note_felts(&mut reader)?; - reader.ensure_eof().map_err(|error| { - ::miden_note_schema::Error::new(format!( - "note storage has trailing data: {error}" - )) - })?; - Ok(value) - } - - #from_str_values - #validate_with - #display_with - } - }) -} - -/// Resolves a macro path relative to the consuming crate manifest. -fn resolve_manifest_path(value: &str, span: Span) -> syn::Result { - let path = PathBuf::from(value); - if path.is_absolute() { - return Ok(path); - } - let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| { - syn::Error::new(span, "CARGO_MANIFEST_DIR is not set during note binding generation") - })?; - Ok(PathBuf::from(manifest_dir).join(path)) -} - -/// Returns the newest package directly inside any project Miden profile directory. -fn freshest_project_package(project_dir: &Path, span: Span) -> syn::Result> { - let target_dir = project_dir.join("target/miden"); - if !target_dir.is_dir() { - return Ok(None); - } - - let profile_dirs = candidate_profile_dirs(&target_dir, span)?; - let mut candidates = Vec::new(); - for profile_dir in profile_dirs { - let entries = fs::read_dir(&profile_dir).map_err(|error| { - syn::Error::new(span, format!("failed to read '{}': {error}", profile_dir.display())) - })?; - for entry in entries { - let entry = entry.map_err(|error| { - syn::Error::new( - span, - format!("failed to read an entry in '{}': {error}", profile_dir.display()), - ) - })?; - let path = entry.path(); - if !path.is_file() || path.extension().is_none_or(|extension| extension != "masp") { - continue; - } - let modified = - entry.metadata().and_then(|metadata| metadata.modified()).map_err(|error| { - syn::Error::new( - span, - format!( - "failed to read modification time for '{}': {error}", - path.display() - ), - ) - })?; - candidates.push((modified, path)); - } - } - candidates.sort_by(|(left_time, left_path), (right_time, right_path)| { - left_time.cmp(right_time).then_with(|| left_path.cmp(right_path)) - }); - Ok(candidates.pop().map(|(_, path)| path)) -} - -/// Returns project profile directories in candidate order without duplicates. -fn candidate_profile_dirs(target_dir: &Path, span: Span) -> syn::Result> { - let mut profiles = Vec::new(); - if let Ok(profile) = env::var("PROFILE") { - push_profile(&mut profiles, profile); - } - push_profile(&mut profiles, "release".to_owned()); - push_profile(&mut profiles, "debug".to_owned()); - - let entries = fs::read_dir(target_dir).map_err(|error| { - syn::Error::new(span, format!("failed to read '{}': {error}", target_dir.display())) - })?; - let mut discovered = entries - .filter_map(Result::ok) - .filter(|entry| entry.path().is_dir()) - .filter_map(|entry| entry.file_name().into_string().ok()) - .filter(|name| name != "packages" && name != "generated-wit") - .collect::>(); - discovered.sort(); - for profile in discovered { - push_profile(&mut profiles, profile); - } - - Ok(profiles - .into_iter() - .map(|profile| target_dir.join(profile)) - .filter(|path| path.is_dir()) - .collect()) -} - -/// Adds a profile name once. -fn push_profile(profiles: &mut Vec, profile: String) { - if !profile.is_empty() && !profiles.contains(&profile) { - profiles.push(profile); - } -} - -/// Formats the missing-project-package diagnostic. -fn missing_project_package_message(project_dir: &Path) -> String { - let manifest = project_dir.join("Cargo.toml"); - let build = if manifest.is_file() { - format!("cargo miden build --manifest-path {} --release", manifest.display()) - } else { - "cargo miden build --release".to_owned() - }; - format!( - "miden-note-bindings could not find a built `.masp` package under '{}'. Build the note \ - project first with `{build}`.", - project_dir.join("target/miden/").display() - ) +/// Support used by generated typed bindings. +#[doc(hidden)] +pub mod __private { + pub use miden_field; + pub use miden_field_repr; + pub use miden_note_schema; + pub use miden_protocol; } - -#[cfg(test)] -mod tests; diff --git a/sdk/note-bindings/tests/generated_custom.rs b/sdk/note-bindings/tests/generated_custom.rs index 96769e015d..5391481b73 100644 --- a/sdk/note-bindings/tests/generated_custom.rs +++ b/sdk/note-bindings/tests/generated_custom.rs @@ -2,9 +2,7 @@ use std::collections::BTreeMap; -use miden_field::Felt; -use miden_field_repr::{FromFeltRepr, ToFeltRepr}; -use miden_note_schema::CodecRegistry; +use miden_note_bindings::{CodecRegistry, Felt, FromFeltRepr, ToFeltRepr}; miden_note_bindings::from_wit_text!( r#" @@ -73,6 +71,8 @@ fn custom_types_have_native_repr_and_typed_round_trips() { value.display_with(&codecs).unwrap(), "{price: {numerator: 3, denominator: 2}, kind: limit({numerator: 5, denominator: 4})}" ); + value.validate().unwrap(); + assert_eq!(value.display().unwrap(), value.display_with(&codecs).unwrap()); } #[test] @@ -82,7 +82,7 @@ fn custom_record_string_paths_use_the_registry_parameter() { values.insert("price.denominator".to_owned(), "6".to_owned()); values.insert("kind".to_owned(), "market".to_owned()); - let value = CustomNote::from_str_values(&values, &CodecRegistry::default()).unwrap(); + let value = CustomNote::from_str_values_with(&values, &CodecRegistry::default()).unwrap(); assert_eq!( value, CustomNote { diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs index 80e549cdfd..95ee900438 100644 --- a/sdk/note-bindings/tests/p2id_consumer.rs +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -7,8 +7,9 @@ use std::{ sync::Arc, }; -use miden_mast_package::Package; +use miden_mast_package::{Package, Section, SectionId}; use midenc_frontend_wasm::WasmTranslationConfig; +use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; use midenc_integration_test_support::CompilerTest; /// Compiles one Cargo Miden project without debug output. @@ -41,6 +42,27 @@ fn host_target() -> String { .to_owned() } +/// Copies the workspace patch table into an isolated consumer manifest. +fn workspace_patch_section(workspace: &Path) -> String { + let manifest = fs::read_to_string(workspace.join("Cargo.toml")).unwrap(); + let mut section = String::new(); + let mut copying = false; + for line in manifest.lines() { + let trimmed = line.trim(); + if trimmed == "[patch.crates-io]" { + copying = true; + } else if copying && trimmed.starts_with('[') { + break; + } + if copying { + section.push_str(line); + section.push('\n'); + } + } + assert!(!section.is_empty(), "workspace manifest has no [patch.crates-io] section"); + section +} + #[test] fn generated_p2id_bindings_compile_and_run_in_a_consumer_crate() { let workspace = workspace_root(); @@ -59,9 +81,29 @@ fn generated_p2id_bindings_compile_and_run_in_a_consumer_crate() { let temp = tempfile::tempdir().unwrap(); fs::create_dir_all(temp.path().join("src")).unwrap(); + let second_package_dir = temp.path().join("packages/counter"); + fs::create_dir_all(&second_package_dir).unwrap(); + let mut second_package = (*p2id).clone(); + let schema_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(); + second_package.sections.retain(|section| section.id != schema_id); + second_package.sections.push(Section::new( + schema_id, + br#"package example:counter-schema@1.0.0; + +interface note-storage { + record counter-note { value: u64 } + type storage = counter-note; +} +"# + .to_vec(), + )); + second_package + .write_masp_file(&second_package_dir) + .expect("failed to persist the second schema package"); + let second_package_path = second_package_dir.join("p2id.masp"); + let bindings_dir = workspace.join("sdk/note-bindings"); - let schema_dir = workspace.join("sdk/note-schema"); - let field_repr_dir = workspace.join("sdk/field-repr/repr"); + let patches = workspace_patch_section(&workspace); fs::write( temp.path().join("Cargo.toml"), format!( @@ -70,16 +112,14 @@ name = "note-bindings-consumer" version = "0.1.0" edition = "2024" +[workspace] + [dependencies] -miden-field = "0.28" -miden-field-repr = {{ path = {field_repr_dir:?} }} miden-note-bindings = {{ path = {bindings_dir:?} }} -miden-note-schema = {{ path = {schema_dir:?} }} -miden-protocol = {{ version = "=0.16.0-alpha.4", features = ["std"] }} + +{patches} "#, - field_repr_dir = field_repr_dir.to_string_lossy(), bindings_dir = bindings_dir.to_string_lossy(), - schema_dir = schema_dir.to_string_lossy(), ), ) .unwrap(); @@ -87,7 +127,7 @@ miden-protocol = {{ version = "=0.16.0-alpha.4", features = ["std"] }} let source = format!( r#"use std::collections::BTreeMap; -use miden_protocol::{{account::AccountId, address::NetworkId}}; +use miden_note_bindings::{{account::AccountId, address::NetworkId}}; mod project_bindings {{ miden_note_bindings::from_project!({project_dir:?}); @@ -95,6 +135,7 @@ mod project_bindings {{ mod package_bindings {{ miden_note_bindings::from_package!({package_path:?}); + miden_note_bindings::from_package!({second_package_path:?}); }} fn main() {{ @@ -111,9 +152,9 @@ fn main() {{ storage.items(), &[account_id.prefix().as_felt(), account_id.suffix()], ); - typed.validate_with().unwrap(); + typed.validate().unwrap(); assert_eq!( - typed.display_with().unwrap(), + typed.display().unwrap(), format!("{{{{target-account-id: {{bech32}}}}}}"), ); @@ -122,10 +163,16 @@ fn main() {{ let exact = package_bindings::P2idNote::from_note_storage(&storage).unwrap(); assert_eq!(exact.target_account_id, account_id); + + let counter = package_bindings::CounterNote {{ value: 9 }}; + let counter_storage = counter.to_note_storage().unwrap(); + assert_eq!(counter_storage.items()[0].as_canonical_u64(), 9); + assert_eq!(counter_storage.items()[1].as_canonical_u64(), 0); }} "#, project_dir = p2id_dir.to_string_lossy(), package_path = package_path.to_string_lossy(), + second_package_path = second_package_path.to_string_lossy(), ); fs::write(temp.path().join("src/main.rs"), source).unwrap(); diff --git a/sdk/note-codec/macros/Cargo.toml b/sdk/note-codec/macros/Cargo.toml index a16a70bffa..695c036f2a 100644 --- a/sdk/note-codec/macros/Cargo.toml +++ b/sdk/note-codec/macros/Cargo.toml @@ -20,7 +20,6 @@ doctest = false [dependencies] heck.workspace = true -miden-mast-package = { workspace = true, features = ["std"] } miden-note-schema.workspace = true miden-note-schema-codegen.workspace = true proc-macro2.workspace = true diff --git a/sdk/note-codec/macros/src/artifact.rs b/sdk/note-codec/macros/src/artifact.rs deleted file mode 100644 index 9b617aa062..0000000000 --- a/sdk/note-codec/macros/src/artifact.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Miden package artifact resolution for codec macros. - -use std::{ - env, fs, - path::{Path, PathBuf}, -}; - -use proc_macro2::Span; - -/// Resolves a macro path relative to the consuming crate manifest. -pub(crate) fn resolve_manifest_path(value: &str, span: Span) -> syn::Result { - let path = PathBuf::from(value); - if path.is_absolute() { - return Ok(path); - } - let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| { - syn::Error::new(span, "CARGO_MANIFEST_DIR is not set during note codec generation") - })?; - Ok(PathBuf::from(manifest_dir).join(path)) -} - -/// Returns the newest package directly inside any project Miden profile directory. -pub(crate) fn freshest_project_package( - project_dir: &Path, - span: Span, -) -> syn::Result> { - let target_dir = project_dir.join("target/miden"); - if !target_dir.is_dir() { - return Ok(None); - } - - let mut candidates = Vec::new(); - for profile_dir in candidate_profile_dirs(&target_dir, span)? { - let entries = fs::read_dir(&profile_dir).map_err(|error| { - syn::Error::new(span, format!("failed to read '{}': {error}", profile_dir.display())) - })?; - for entry in entries { - let entry = entry.map_err(|error| { - syn::Error::new( - span, - format!("failed to read an entry in '{}': {error}", profile_dir.display()), - ) - })?; - let path = entry.path(); - if !path.is_file() || !path.extension().is_some_and(|extension| extension == "masp") { - continue; - } - let modified = - entry.metadata().and_then(|metadata| metadata.modified()).map_err(|error| { - syn::Error::new( - span, - format!( - "failed to read modification time for '{}': {error}", - path.display() - ), - ) - })?; - candidates.push((modified, path)); - } - } - candidates.sort_by(|(left_time, left_path), (right_time, right_path)| { - left_time.cmp(right_time).then_with(|| left_path.cmp(right_path)) - }); - Ok(candidates.pop().map(|(_, path)| path)) -} - -/// Returns project profile directories without duplicates. -fn candidate_profile_dirs(target_dir: &Path, span: Span) -> syn::Result> { - let mut profiles = Vec::new(); - if let Ok(profile) = env::var("PROFILE") { - push_profile(&mut profiles, profile); - } - push_profile(&mut profiles, "release".to_owned()); - push_profile(&mut profiles, "debug".to_owned()); - - let entries = fs::read_dir(target_dir).map_err(|error| { - syn::Error::new(span, format!("failed to read '{}': {error}", target_dir.display())) - })?; - let mut discovered = entries - .filter_map(Result::ok) - .filter(|entry| entry.path().is_dir()) - .filter_map(|entry| entry.file_name().into_string().ok()) - .filter(|name| name != "packages" && name != "generated-wit") - .collect::>(); - discovered.sort(); - for profile in discovered { - push_profile(&mut profiles, profile); - } - - Ok(profiles - .into_iter() - .map(|profile| target_dir.join(profile)) - .filter(|path| path.is_dir()) - .collect()) -} - -/// Adds a profile name once. -fn push_profile(profiles: &mut Vec, profile: String) { - if !profile.is_empty() && !profiles.contains(&profile) { - profiles.push(profile); - } -} - -/// Formats the missing-project-package diagnostic. -pub(crate) fn missing_project_package_message(project_dir: &Path) -> String { - let manifest = project_dir.join("Cargo.toml"); - let build = if manifest.is_file() { - format!("cargo miden build --manifest-path {} --release", manifest.display()) - } else { - "cargo miden build --release".to_owned() - }; - format!( - "miden-note-codec could not find a built `.masp` package under '{}'. Build the note \ - project first with `{build}`.", - project_dir.join("target/miden/").display() - ) -} diff --git a/sdk/note-codec/macros/src/expand.rs b/sdk/note-codec/macros/src/expand.rs index 25e6640357..3078e45f5a 100644 --- a/sdk/note-codec/macros/src/expand.rs +++ b/sdk/note-codec/macros/src/expand.rs @@ -1,48 +1,31 @@ //! Macro expansion for generated author types and component dispatch. -use std::path::Path; - -use miden_mast_package::Package; -use miden_note_schema::NoteStorageSchema; +use miden_note_schema::{NotePackageArtifact, NotePackageResolver, NoteStorageSchema}; use miden_note_schema_codegen::generate_host_types; use proc_macro_crate::{FoundCrate, crate_name}; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote}; use syn::{ItemImpl, LitStr, Type, visit_mut::VisitMut}; -use crate::{ - artifact::{freshest_project_package, missing_project_package_message, resolve_manifest_path}, - registry::{register_codec, register_schema, registered_codecs}, -}; +use crate::registry::{register_codec, register_schema, registered_codecs}; /// The component world embedded in generated export glue. const NOTE_CODEC_WIT: &str = include_str!("../../wit/note-codec.wit"); /// Expands a project-relative type generation request. pub(crate) fn from_project(input: &LitStr) -> syn::Result { - let project_dir = resolve_manifest_path(&input.value(), input.span())?; - if !project_dir.is_dir() { - return Err(syn::Error::new( - input.span(), - format!("note project directory '{}' does not exist", project_dir.display()), - )); - } - let package_path = freshest_project_package(&project_dir, input.span())?.ok_or_else(|| { - syn::Error::new(input.span(), missing_project_package_message(&project_dir)) - })?; - expand_package_path(&package_path, input.span()) + let artifact = NotePackageResolver::new("miden-note-codec") + .from_project(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_package_artifact(&artifact, input.span()) } /// Expands an exact package type generation request. pub(crate) fn from_package(input: &LitStr) -> syn::Result { - let package_path = resolve_manifest_path(&input.value(), input.span())?; - if !package_path.is_file() { - return Err(syn::Error::new( - input.span(), - format!("Miden package '{}' does not exist", package_path.display()), - )); - } - expand_package_path(&package_path, input.span()) + let artifact = NotePackageResolver::new("miden-note-codec") + .from_package(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_package_artifact(&artifact, input.span()) } /// Expands a WIT string literal for internal tests. @@ -53,24 +36,9 @@ pub(crate) fn from_wit_text(input: &LitStr) -> syn::Result { } /// Loads one package, tracks it as an input, and expands its schema types. -fn expand_package_path(package_path: &Path, span: Span) -> syn::Result { - let package = Package::deserialize_from_file(package_path).map_err(|error| { - syn::Error::new( - span, - format!("failed to read Miden package '{}': {error}", package_path.display()), - ) - })?; - let schema = NoteStorageSchema::from_package(&package).map_err(|error| { - syn::Error::new( - span, - format!( - "failed to read note storage schema from '{}': {error}", - package_path.display() - ), - ) - })?; - let types = expand_schema(&schema, span)?; - let tracked_path = package_path.to_string_lossy(); +fn expand_package_artifact(artifact: &NotePackageArtifact, span: Span) -> syn::Result { + let types = expand_schema(artifact.schema(), span)?; + let tracked_path = artifact.path().to_string_lossy(); Ok(quote! { #[doc(hidden)] const _: &[u8] = include_bytes!(#tracked_path); @@ -164,14 +132,36 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { quote! { #fqn => { let value = <#ty as ::miden_note_codec::AuthorTypeCodec>::parse(value)?; - Ok(::miden_note_codec::encode_felt_repr(&value)) + let mut felts = Vec::new(); + <#ty as __MidenNoteEncode>::__write_note_felts( + &value, + &mut ::miden_note_codec::__private::miden_field_repr::FeltWriter::new( + &mut felts, + ), + ) + .map_err(|error| format!( + "failed to encode codec type `{}`: {error}", + #fqn, + ))?; + Ok(::miden_note_codec::felts_to_u64(&felts)) } } }); let display_arms = registrations.iter().map(|(fqn, ty)| { quote! { #fqn => { - let value = ::miden_note_codec::decode_felt_repr::<#ty>(value)?; + let felts = ::miden_note_codec::felts_from_u64(value)?; + let mut reader = + ::miden_note_codec::__private::miden_field_repr::FeltReader::new(&felts); + let value = <#ty as __MidenNoteDecode>::__read_note_felts(&mut reader) + .map_err(|error| format!( + "failed to decode codec type `{}`: {error}", + #fqn, + ))?; + reader.ensure_eof().map_err(|error| format!( + "codec type `{}` has trailing felt data: {error}", + #fqn, + ))?; Ok(<#ty as ::miden_note_codec::AuthorTypeCodec>::display(&value)) } } @@ -179,7 +169,18 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { let validate_arms = registrations.iter().map(|(fqn, ty)| { quote! { #fqn => { - let value = ::miden_note_codec::decode_felt_repr::<#ty>(value)?; + let felts = ::miden_note_codec::felts_from_u64(value)?; + let mut reader = + ::miden_note_codec::__private::miden_field_repr::FeltReader::new(&felts); + let value = <#ty as __MidenNoteDecode>::__read_note_felts(&mut reader) + .map_err(|error| format!( + "failed to decode codec type `{}`: {error}", + #fqn, + ))?; + reader.ensure_eof().map_err(|error| format!( + "codec type `{}` has trailing felt data: {error}", + #fqn, + ))?; <#ty as ::miden_note_codec::AuthorTypeCodec>::validate(&value) } } diff --git a/sdk/note-codec/macros/src/lib.rs b/sdk/note-codec/macros/src/lib.rs index ec4983c03e..5aeb17cc05 100644 --- a/sdk/note-codec/macros/src/lib.rs +++ b/sdk/note-codec/macros/src/lib.rs @@ -4,7 +4,6 @@ extern crate proc_macro; -mod artifact; mod expand; mod registry; diff --git a/sdk/note-codec/tests/account_id_dispatch.rs b/sdk/note-codec/tests/account_id_dispatch.rs new file mode 100644 index 0000000000..d86454ac72 --- /dev/null +++ b/sdk/note-codec/tests/account_id_dispatch.rs @@ -0,0 +1,68 @@ +//! Verifies structural codec dispatch for a type that contains a protocol leaf. + +use miden_note_codec::{AuthorTypeCodec, export_codecs, from_wit_text, note_codec}; +use miden_protocol::account::AccountId; + +from_wit_text!( + r#" +package example:account-codec@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + record account-label { + account: account-id, + serial: u64, + } + + record account-note { + label: account-label, + } + + type storage = account-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"# +); + +#[note_codec] +impl AuthorTypeCodec for AccountLabel { + fn parse(value: &str) -> Result { + let (account, serial) = value + .split_once(',') + .ok_or_else(|| "an account label must use `account-id,serial`".to_owned())?; + let (account, _) = AccountId::parse(account).map_err(|error| error.to_string())?; + let serial = serial.parse::().map_err(|error| error.to_string())?; + Ok(Self { account, serial }) + } + + fn display(&self) -> String { + format!("{},{}", self.account.to_hex(), self.serial) + } + + fn validate(&self) -> Result<(), String> { + Ok(()) + } +} + +export_codecs!(); + +#[test] +fn account_id_codec_uses_generated_structural_traits() { + let account = AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let input = format!("{},7", account.to_hex()); + let fqn = AccountLabel::WIT_FQN; + + let encoded = __miden_note_codec_dispatch::parse(fqn, &input).unwrap(); + assert_eq!(encoded, [account.prefix().as_u64(), account.suffix().as_canonical_u64(), 7, 0]); + __miden_note_codec_dispatch::validate(fqn, &encoded).unwrap(); + assert_eq!(__miden_note_codec_dispatch::display(fqn, &encoded).unwrap(), input); +} diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index ab28d7f92b..a1b2022b7e 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -14,11 +14,8 @@ const WASM_TARGET: &str = "wasm32-unknown-unknown"; #[test] fn minimal_codec_crate_encodes_to_zero_import_component() { - if !ensure_wasm_target() { - eprintln!( - "skipping component export test: rustup could not install {WASM_TARGET} in this \ - environment" - ); + if !wasm_target_is_installed() { + eprintln!("skipping component export test: {WASM_TARGET} is not installed"); return; } @@ -83,9 +80,9 @@ fn minimal_codec_crate_encodes_to_zero_import_component() { ); } -/// Checks the target and tries to install it before a graceful skip. -fn ensure_wasm_target() -> bool { - let listed = match Command::new("rustup").args(["target", "list"]).output() { +/// Returns true when rustup reports the component target as installed. +fn wasm_target_is_installed() -> bool { + let output = match Command::new("rustup").args(["target", "list"]).output() { Ok(output) if output.status.success() => output, Ok(output) => { eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); @@ -96,38 +93,7 @@ fn ensure_wasm_target() -> bool { return false; } }; - if target_is_installed(&listed.stdout) { - return true; - } - - let install = Command::new("rustup").args(["target", "add", WASM_TARGET]).output(); - match install { - Ok(output) if output.status.success() => {} - Ok(output) => { - eprintln!( - "`rustup target add {WASM_TARGET}` failed:\n{}", - String::from_utf8_lossy(&output.stderr) - ); - return false; - } - Err(error) => { - eprintln!("could not run `rustup target add {WASM_TARGET}`: {error}"); - return false; - } - } - - match Command::new("rustup").args(["target", "list"]).output() { - Ok(output) => output.status.success() && target_is_installed(&output.stdout), - Err(error) => { - eprintln!("could not re-run `rustup target list`: {error}"); - false - } - } -} - -/// Returns true when rustup reports the primary component target as installed. -fn target_is_installed(output: &[u8]) -> bool { - String::from_utf8_lossy(output) + String::from_utf8_lossy(&output.stdout) .lines() .any(|line| line.starts_with(WASM_TARGET) && line.contains("(installed)")) } diff --git a/sdk/note-schema/codegen/src/lib.rs b/sdk/note-schema/codegen/src/lib.rs index 1a28c583cf..29f73e0ec1 100644 --- a/sdk/note-schema/codegen/src/lib.rs +++ b/sdk/note-schema/codegen/src/lib.rs @@ -42,7 +42,8 @@ impl std::error::Error for CodegenError {} pub struct GeneratedTypes { tokens: TokenStream, root_ident: Ident, - has_custom_types: bool, + type_idents: Vec, + has_nested_named_types: bool, } impl GeneratedTypes { @@ -56,15 +57,22 @@ impl GeneratedTypes { &self.root_ident } - /// Returns true when the schema has named types other than its storage root and standard - /// leaves. - pub const fn has_custom_types(&self) -> bool { - self.has_custom_types + /// Returns every generated Rust type identifier in emission order. + pub fn type_idents(&self) -> &[Ident] { + &self.type_idents + } + + /// Returns true when the schema has a generated named type below its storage root. + pub const fn has_nested_named_types(&self) -> bool { + self.has_nested_named_types } } /// Generates Rust host-profile types and structural felt conversion helpers. pub fn generate_host_types(schema: &NoteStorageSchema) -> Result { + schema + .validate_native_leaf_shapes() + .map_err(|error| CodegenError::new(error.to_string()))?; let root = schema.root(); let root_fqn = root .fqn() @@ -99,7 +107,16 @@ pub fn generate_host_types(schema: &NoteStorageSchema) -> Result, _>>()?; - let has_custom_types = definitions + let type_idents = definitions + .iter() + .map(|definition| { + rust_names + .get(definition.fqn().expect("generated types have an FQN")) + .expect("generated types have a Rust name") + .clone() + }) + .collect(); + let has_nested_named_types = definitions .iter() .any(|definition| definition.fqn().is_some_and(|fqn| fqn != root_fqn)); @@ -109,7 +126,8 @@ pub fn generate_host_types(schema: &NoteStorageSchema) -> Result (String, bool, String) { let file: syn::File = syn::parse2(generated.tokens().clone()).unwrap(); ( prettyplease::unparse(&file), - generated.has_custom_types(), + generated.has_nested_named_types(), generated.root_ident().to_string(), ) } #[test] fn maps_protocol_leaf_and_root_type() { - let (source, has_custom_types, root) = generate(P2ID_SCHEMA); + let (source, has_nested_named_types, root) = generate(P2ID_SCHEMA); assert_eq!(root, "P2idNote"); - assert!(!has_custom_types); + assert!(!has_nested_named_types); assert!(source.contains("pub target_account_id: ::miden_protocol::account::AccountId")); assert!(source.contains("pub const WIT_FQN")); assert!(source.contains("self.prefix().as_felt()")); @@ -85,9 +85,9 @@ fn maps_protocol_leaf_and_root_type() { #[test] fn derives_native_repr_for_custom_records_and_variants() { - let (source, has_custom_types, root) = generate(CUSTOM_SCHEMA); + let (source, has_nested_named_types, root) = generate(CUSTOM_SCHEMA); assert_eq!(root, "DexNote"); - assert!(has_custom_types); + assert!(has_nested_named_types); assert!(source.contains("pub struct LimitPrice")); assert!(source.contains("pub enum OrderKind")); assert!(source.matches("::miden_field_repr::ToFeltRepr").count() >= 2); diff --git a/sdk/note-schema/src/artifact.rs b/sdk/note-schema/src/artifact.rs new file mode 100644 index 0000000000..51f3eacfc5 --- /dev/null +++ b/sdk/note-schema/src/artifact.rs @@ -0,0 +1,249 @@ +//! Miden package discovery for note schema macros. + +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use miden_mast_package::Package; + +use crate::{Error, NoteStorageSchema, Result}; + +/// Compiler-provided path to the package staged for note codec generation. +const NOTE_CODEC_PACKAGE_PATH_ENV: &str = "MIDENC_NOTE_CODEC_PACKAGE_PATH"; + +/// A loaded package artifact and its note storage schema. +pub struct NotePackageArtifact { + path: PathBuf, + schema: NoteStorageSchema, +} + +impl NotePackageArtifact { + /// Returns the exact package path used to load the schema. + pub fn path(&self) -> &Path { + &self.path + } + + /// Returns the schema loaded from the package. + pub const fn schema(&self) -> &NoteStorageSchema { + &self.schema + } +} + +/// Resolves package artifacts for one note macro crate. +pub struct NotePackageResolver<'a> { + macro_crate: &'a str, +} + +impl<'a> NotePackageResolver<'a> { + /// Creates a resolver whose diagnostics name `macro_crate`. + pub const fn new(macro_crate: &'a str) -> Self { + Self { macro_crate } + } + + /// Loads the freshest package built by a Miden project. + pub fn from_project(&self, project: &str) -> Result { + if let Some(path) = self.compiler_staged_package()? { + return self.load_package(path); + } + + let project_dir = self.resolve_manifest_path(project)?; + if !project_dir.is_dir() { + return Err(Error::new(format!( + "{}: note project directory '{}' does not exist", + self.macro_crate, + project_dir.display() + ))); + } + let package_path = freshest_project_package(&project_dir) + .map_err(|error| Error::new(format!("{}: {error}", self.macro_crate)))? + .ok_or_else(|| { + Error::new(missing_project_package_message(self.macro_crate, &project_dir)) + })?; + self.load_package(package_path) + } + + /// Loads one exact Miden package path. + pub fn from_package(&self, package: &str) -> Result { + let package_path = self.resolve_manifest_path(package)?; + if !package_path.is_file() { + return Err(Error::new(format!( + "{}: Miden package '{}' does not exist", + self.macro_crate, + package_path.display() + ))); + } + self.load_package(package_path) + } + + /// Resolves one path relative to the consuming crate manifest. + fn resolve_manifest_path(&self, value: &str) -> Result { + let path = PathBuf::from(value); + if path.is_absolute() { + return Ok(path); + } + let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| { + Error::new(format!( + "{}: CARGO_MANIFEST_DIR is not set during macro expansion", + self.macro_crate + )) + })?; + Ok(PathBuf::from(manifest_dir).join(path)) + } + + /// Returns the compiler-staged package when one is available. + fn compiler_staged_package(&self) -> Result> { + let Some(path) = env::var_os(NOTE_CODEC_PACKAGE_PATH_ENV) else { + return Ok(None); + }; + let path = PathBuf::from(path); + if !path.is_file() { + return Err(Error::new(format!( + "{}: {NOTE_CODEC_PACKAGE_PATH_ENV} points to missing Miden package '{}'", + self.macro_crate, + path.display() + ))); + } + Ok(Some(path)) + } + + /// Loads the package and its unique schema section. + fn load_package(&self, path: PathBuf) -> Result { + let package = Package::deserialize_from_file(&path).map_err(|error| { + Error::new(format!( + "{}: failed to read Miden package '{}': {error}", + self.macro_crate, + path.display() + )) + })?; + let schema = NoteStorageSchema::from_package(&package).map_err(|error| { + Error::new(format!( + "{}: failed to read note storage schema from '{}': {error}", + self.macro_crate, + path.display() + )) + })?; + Ok(NotePackageArtifact { path, schema }) + } +} + +/// Returns the newest package directly inside a Miden project profile directory. +fn freshest_project_package(project_dir: &Path) -> Result> { + let target_dir = project_dir.join("target/miden"); + if !target_dir.is_dir() { + return Ok(None); + } + + let mut candidates = Vec::new(); + for profile_dir in candidate_profile_dirs(&target_dir)? { + let entries = fs::read_dir(&profile_dir).map_err(|error| { + Error::new(format!("failed to read '{}': {error}", profile_dir.display())) + })?; + for entry in entries { + let entry = entry.map_err(|error| { + Error::new(format!( + "failed to read an entry in '{}': {error}", + profile_dir.display() + )) + })?; + let path = entry.path(); + if !path.is_file() || !path.extension().is_some_and(|extension| extension == "masp") { + continue; + } + let modified = + entry.metadata().and_then(|metadata| metadata.modified()).map_err(|error| { + Error::new(format!( + "failed to read modification time for '{}': {error}", + path.display() + )) + })?; + candidates.push((modified, path)); + } + } + candidates.sort_by(|(left_time, left_path), (right_time, right_path)| { + left_time.cmp(right_time).then_with(|| left_path.cmp(right_path)) + }); + Ok(candidates.pop().map(|(_, path)| path)) +} + +/// Returns project profile directories in deterministic candidate order. +fn candidate_profile_dirs(target_dir: &Path) -> Result> { + let mut profiles = Vec::new(); + push_profile(&mut profiles, "release".to_owned()); + push_profile(&mut profiles, "debug".to_owned()); + + let entries = fs::read_dir(target_dir).map_err(|error| { + Error::new(format!("failed to read '{}': {error}", target_dir.display())) + })?; + let mut discovered = entries + .filter_map(core::result::Result::ok) + .filter(|entry| entry.path().is_dir()) + .filter_map(|entry| entry.file_name().into_string().ok()) + .filter(|name| name != "packages" && name != "generated-wit") + .collect::>(); + discovered.sort(); + for profile in discovered { + push_profile(&mut profiles, profile); + } + + Ok(profiles + .into_iter() + .map(|profile| target_dir.join(profile)) + .filter(|path| path.is_dir()) + .collect()) +} + +/// Adds one profile name once. +fn push_profile(profiles: &mut Vec, profile: String) { + if !profile.is_empty() && !profiles.contains(&profile) { + profiles.push(profile); + } +} + +/// Formats the diagnostic for a project without a built package. +fn missing_project_package_message(macro_crate: &str, project_dir: &Path) -> String { + let manifest = project_dir.join("Cargo.toml"); + let build = if manifest.is_file() { + format!("cargo miden build --manifest-path {} --release", manifest.display()) + } else { + "cargo miden build --release".to_owned() + }; + format!( + "{macro_crate} could not find a built `.masp` package under '{}'. Build the note project \ + first with `{build}`.", + project_dir.join("target/miden/").display() + ) +} + +#[cfg(test)] +mod tests { + use std::{fs, thread, time::Duration}; + + use super::{freshest_project_package, missing_project_package_message}; + + #[test] + fn selects_the_freshest_package_across_profiles() { + let temp = tempfile::tempdir().unwrap(); + let debug = temp.path().join("target/miden/debug"); + let release = temp.path().join("target/miden/release"); + fs::create_dir_all(&debug).unwrap(); + fs::create_dir_all(&release).unwrap(); + fs::write(debug.join("note.masp"), b"old").unwrap(); + thread::sleep(Duration::from_millis(20)); + fs::write(release.join("note.masp"), b"new").unwrap(); + + let selected = freshest_project_package(temp.path()).unwrap().unwrap(); + assert_eq!(selected, release.join("note.masp")); + } + + #[test] + fn missing_package_diagnostic_names_macro_and_build_command() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='note'\nversion='0.1.0'") + .unwrap(); + let message = missing_project_package_message("test-note-macro", temp.path()); + assert!(message.contains("test-note-macro")); + assert!(message.contains("cargo miden build --manifest-path")); + assert!(message.contains("--release")); + } +} diff --git a/sdk/note-schema/src/builder.rs b/sdk/note-schema/src/builder.rs index 79d70d09cd..8bade27351 100644 --- a/sdk/note-schema/src/builder.rs +++ b/sdk/note-schema/src/builder.rs @@ -35,8 +35,10 @@ impl<'a> NoteStorageBuilder<'a> { /// `case-name()` when the selected case has a payload. pub fn set(mut self, path: &str, value: impl AsRef) -> Result { let segments = normalize_path(path)?; - resolve_path(self.schema.root(), &segments)?; let normalized = segments.join("."); + let ty = resolve_path(self.schema.root(), &segments)?; + let value = value.as_ref(); + reject_unsupported_constructor_path(ty, &normalized, value, self.registry)?; if let Some(conflict) = self.values.keys().find(|existing| { *existing == &normalized @@ -47,7 +49,7 @@ impl<'a> NoteStorageBuilder<'a> { "path `{normalized}` conflicts with the existing value at `{conflict}`" ))); } - self.values.insert(normalized, value.as_ref().to_owned()); + self.values.insert(normalized, value.to_owned()); Ok(self) } @@ -68,6 +70,51 @@ impl<'a> NoteStorageBuilder<'a> { } } +/// Rejects structural constructor shapes that need nested record text parsing. +fn reject_unsupported_constructor_path( + ty: &SchemaType, + path: &str, + value: &str, + registry: &CodecRegistry, +) -> Result<()> { + if ty.fqn().is_some_and(|fqn| registry.contains(fqn)) { + return Ok(()); + } + match ty.kind() { + SchemaTypeKind::Option(payload) + if matches!(payload.kind(), SchemaTypeKind::Record(_)) + && value.trim() != "none" + && !payload.fqn().is_some_and(|fqn| registry.contains(fqn)) => + { + Err(Error::new(format!( + "path `{path}` selects an option with record payload `{}`; the string builder \ + does not support nested record constructors without a codec for that payload", + payload.fqn().or(payload.name()).unwrap_or("") + ))) + } + SchemaTypeKind::Variant(cases) => { + let (case_name, _) = parse_constructor(value)?; + let case_name = normalize_name(case_name); + let unsupported = cases.iter().find(|case| case.name() == case_name).and_then(|case| { + let payload = case.payload()?; + (matches!(payload.kind(), SchemaTypeKind::Record(_)) + && !payload.fqn().is_some_and(|fqn| registry.contains(fqn))) + .then_some(case.name()) + }); + if let Some(case_name) = unsupported { + Err(Error::new(format!( + "path `{path}` selects variant case `{case_name}` with a record payload; the \ + string builder does not support nested record constructors without a codec \ + for that payload" + ))) + } else { + Ok(()) + } + } + _ => Ok(()), + } +} + /// Normalizes and validates a dotted field path. fn normalize_path(path: &str) -> Result> { let segments = path.split('.').map(normalize_name).collect::>(); diff --git a/sdk/note-schema/src/codec.rs b/sdk/note-schema/src/codec.rs index d53e0bc24f..46cdf35d41 100644 --- a/sdk/note-schema/src/codec.rs +++ b/sdk/note-schema/src/codec.rs @@ -43,12 +43,22 @@ pub struct CodecRegistry { impl CodecRegistry { /// Creates an empty codec registry. - pub fn new() -> Self { + pub fn empty() -> Self { Self { codecs: BTreeMap::new(), } } + /// Creates a registry containing all standard note storage codecs. + pub fn with_standard_codecs() -> Self { + let mut registry = Self::empty(); + registry.register(FELT_FQN, FeltCodec); + registry.register(WORD_FQN, WordCodec); + registry.register(ACCOUNT_ID_FQN, AccountIdCodec); + registry.register(ASSET_AMOUNT_FQN, AssetAmountCodec); + registry + } + /// Registers or replaces a codec under its canonical WIT FQN. pub fn register(&mut self, fqn: impl Into, codec: impl ConsumerTypeCodec + 'static) { self.register_shared(fqn, Arc::new(codec)); @@ -72,12 +82,7 @@ impl CodecRegistry { impl Default for CodecRegistry { fn default() -> Self { - let mut registry = Self::new(); - registry.register(FELT_FQN, FeltCodec); - registry.register(WORD_FQN, WordCodec); - registry.register(ACCOUNT_ID_FQN, AccountIdCodec); - registry.register(ASSET_AMOUNT_FQN, AssetAmountCodec); - registry + Self::with_standard_codecs() } } @@ -254,6 +259,7 @@ mod tests { assert!(registry.contains(WORD_FQN)); assert!(registry.contains(ACCOUNT_ID_FQN)); assert!(registry.contains(ASSET_AMOUNT_FQN)); + assert!(!CodecRegistry::empty().contains(FELT_FQN)); } #[test] diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 1c3682c5b3..55d1fec626 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -1,16 +1,34 @@ //! Consumer adapters for author codec components. -use std::sync::{Arc, Mutex}; +use std::{collections::HashSet, sync::Arc}; use miden_field::Felt; -use miden_mast_package::{Package, SectionId}; +use miden_mast_package::Package; use midenc_frontend_wasm_metadata::PACKAGE_NOTE_CODEC_SECTION_ID; use wasmtime::{ - Config, Engine, Store, + Config, Engine, Store, StoreLimits, StoreLimitsBuilder, component::{Component, Linker}, }; -use crate::{CodecRegistry, ConsumerTypeCodec, Error, Result}; +use crate::{CodecRegistry, ConsumerTypeCodec, Error, NoteStorageSchema, Result}; + +/// Maximum Wasm instructions available to one codec operation. +const CALL_FUEL: u64 = 10_000_000; + +/// Maximum bytes available to one codec component linear memory. +const MAX_COMPONENT_MEMORY_BYTES: usize = 16 * 1024 * 1024; + +/// Maximum FQNs accepted from `supported-types`. +const MAX_SUPPORTED_TYPES: usize = 128; + +/// Maximum bytes accepted in one reported FQN. +const MAX_FQN_BYTES: usize = 512; + +/// Maximum felts accepted from `parse`. +const MAX_RETURNED_FELTS: usize = 4_096; + +/// Maximum bytes accepted in one component-returned string. +const MAX_RETURNED_STRING_BYTES: usize = 16 * 1024; wasmtime::component::bindgen!({ path: "../note-codec/wit", @@ -20,37 +38,19 @@ wasmtime::component::bindgen!({ impl CodecRegistry { /// Loads the note codec component from a package and registers all reported types. pub fn load_from_package(package: &Package) -> Result { - let section_id = SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID).map_err(|error| { - Error::new(format!( - "invalid note codec section id `{PACKAGE_NOTE_CODEC_SECTION_ID}`: {error}" - )) - })?; - let bytes = package - .sections - .iter() - .find(|section| section.id == section_id) - .ok_or_else(|| { - Error::new(format!( - "package does not contain the `{PACKAGE_NOTE_CODEC_SECTION_ID}` section" - )) - })? - .data - .as_ref(); - - Self::load_from_component(bytes) + let schema = NoteStorageSchema::from_package(package)?; + let bytes = crate::section::unique_package_section(package, PACKAGE_NOTE_CODEC_SECTION_ID)?; + Self::load_from_component(bytes, &schema.custom_type_fqns()) } /// Loads a zero-import note codec component. - fn load_from_component(bytes: &[u8]) -> Result { - let mut runtime = ComponentRuntime::instantiate(bytes)?; + fn load_from_component(bytes: &[u8], custom_type_fqns: &HashSet) -> Result { + let runtime = Arc::new(ComponentRuntime::new(bytes)?); let supported_types = runtime.supported_types()?; - let runtime = Arc::new(Mutex::new(runtime)); let mut registry = Self::default(); + validate_reported_fqns(&supported_types, custom_type_fqns, ®istry)?; for fqn in supported_types { - if fqn.trim().is_empty() { - return Err(Error::new("note codec component reported an empty type FQN")); - } registry.register_shared( fqn.clone(), Arc::new(ComponentCodec { @@ -64,58 +64,92 @@ impl CodecRegistry { } } -/// One instantiated note codec component and its mutable store. +/// A compiled component used to create an isolated instance for each operation. struct ComponentRuntime { - store: Store<()>, + engine: Engine, + component: Component, +} + +/// Store state that owns the component resource limits. +struct ComponentStore { + limits: StoreLimits, +} + +/// One isolated codec component call context. +struct ComponentInstance { + store: Store, bindings: NoteCodec, } impl ComponentRuntime { - /// Instantiates a component without defining any host imports. - fn instantiate(bytes: &[u8]) -> Result { + /// Compiles a component with fuel accounting enabled. + fn new(bytes: &[u8]) -> Result { let mut config = Config::new(); config.wasm_component_model(true); + config.consume_fuel(true); let engine = Engine::new(&config) .map_err(|error| component_error("create the Wasmtime engine", error))?; let component = Component::new(&engine, bytes) .map_err(|error| component_error("compile the note codec component", error))?; - let linker = Linker::new(&engine); - let mut store = Store::new(&engine, ()); - let bindings = NoteCodec::instantiate(&mut store, &component, &linker) + Ok(Self { engine, component }) + } + + /// Instantiates a zero-import component with fresh per-call limits. + fn instantiate(&self) -> Result { + let linker = Linker::new(&self.engine); + let limits = StoreLimitsBuilder::new() + .memory_size(MAX_COMPONENT_MEMORY_BYTES) + .instances(32) + .tables(32) + .memories(1) + .trap_on_grow_failure(true) + .build(); + let mut store = Store::new(&self.engine, ComponentStore { limits }); + store.limiter(|state| &mut state.limits); + store + .set_fuel(CALL_FUEL) + .map_err(|error| component_error("set the note codec fuel budget", error))?; + let bindings = NoteCodec::instantiate(&mut store, &self.component, &linker) .map_err(|error| component_error("instantiate the zero-import note codec", error))?; - Ok(Self { store, bindings }) + Ok(ComponentInstance { store, bindings }) } /// Queries the component's supported FQNs once during registry construction. - fn supported_types(&mut self) -> Result> { - self.bindings + fn supported_types(&self) -> Result> { + let mut instance = self.instantiate()?; + let fqns = instance + .bindings .miden_note_codec_codec() - .call_supported_types(&mut self.store) - .map_err(|error| component_error("call `supported-types`", error)) + .call_supported_types(&mut instance.store) + .map_err(|error| component_error("call `supported-types`", error))?; + if fqns.len() > MAX_SUPPORTED_TYPES { + return Err(Error::new(format!( + "note codec component reported {} types; the limit is {MAX_SUPPORTED_TYPES}", + fqns.len() + ))); + } + for fqn in &fqns { + ensure_returned_string_limit("type FQN", fqn, MAX_FQN_BYTES)?; + } + Ok(fqns) } } -/// A registry entry that dispatches one FQN into a shared component instance. +/// A registry entry that dispatches one FQN through isolated component instances. struct ComponentCodec { fqn: String, - runtime: Arc>, + runtime: Arc, } impl ComponentCodec { - /// Calls one component operation while holding exclusive access to its store. + /// Calls one component operation in a fresh fuel- and memory-limited store. fn with_runtime( &self, operation: &str, - call: impl FnOnce(&NoteCodec, &mut Store<()>) -> wasmtime::Result, + call: impl FnOnce(&NoteCodec, &mut Store) -> wasmtime::Result, ) -> Result { - let mut runtime = self.runtime.lock().map_err(|_| { - Error::new(format!( - "note codec component state is unavailable while calling `{operation}` for `{}`", - self.fqn - )) - })?; - let ComponentRuntime { bindings, store } = &mut *runtime; - call(bindings, store).map_err(|error| { + let mut instance = self.runtime.instantiate()?; + call(&instance.bindings, &mut instance.store).map_err(|error| { component_error(&format!("call `{operation}` for codec `{}`", self.fqn), error) }) } @@ -126,27 +160,100 @@ impl ConsumerTypeCodec for ComponentCodec { let result = self.with_runtime("parse", |bindings, store| { bindings.miden_note_codec_codec().call_parse(store, &self.fqn, value) })?; - let values = result.map_err(|message| codec_rejection("parse", &self.fqn, message))?; + let values = match result { + Ok(values) => values, + Err(message) => { + ensure_returned_string_limit("codec error", &message, MAX_RETURNED_STRING_BYTES)?; + return Err(codec_rejection("parse", &self.fqn, message)); + } + }; + ensure_returned_felt_limit(&self.fqn, values.len())?; component_values_to_felts(&self.fqn, &values) } fn display(&self, felts: &[Felt]) -> Result { let values = felts.iter().map(|felt| felt.as_canonical_u64()).collect::>(); - self.with_runtime("display", |bindings, store| { + let result = self.with_runtime("display", |bindings, store| { bindings.miden_note_codec_codec().call_display(store, &self.fqn, &values) - })? - .map_err(|message| codec_rejection("display", &self.fqn, message)) + })?; + let display = match result { + Ok(display) => display, + Err(message) => { + ensure_returned_string_limit("codec error", &message, MAX_RETURNED_STRING_BYTES)?; + return Err(codec_rejection("display", &self.fqn, message)); + } + }; + ensure_returned_string_limit("display value", &display, MAX_RETURNED_STRING_BYTES)?; + Ok(display) } fn validate(&self, felts: &[Felt]) -> Result<()> { let values = felts.iter().map(|felt| felt.as_canonical_u64()).collect::>(); - self.with_runtime("validate", |bindings, store| { + let result = self.with_runtime("validate", |bindings, store| { bindings.miden_note_codec_codec().call_validate(store, &self.fqn, &values) - })? - .map_err(|message| codec_rejection("validate", &self.fqn, message)) + })?; + match result { + Ok(()) => Ok(()), + Err(message) => { + ensure_returned_string_limit("codec error", &message, MAX_RETURNED_STRING_BYTES)?; + Err(codec_rejection("validate", &self.fqn, message)) + } + } } } +/// Validates the component's claimed type authority before registration. +fn validate_reported_fqns( + reported: &[String], + custom_type_fqns: &HashSet, + standard_registry: &CodecRegistry, +) -> Result<()> { + let mut seen = HashSet::new(); + for fqn in reported { + if fqn.trim().is_empty() { + return Err(Error::new("note codec component reported an empty type FQN")); + } + if !seen.insert(fqn) { + return Err(Error::new(format!( + "note codec component reported type FQN `{fqn}` more than once" + ))); + } + if standard_registry.contains(fqn) { + return Err(Error::new(format!( + "note codec component cannot replace the standard codec for `{fqn}`" + ))); + } + if !custom_type_fqns.contains(fqn) { + return Err(Error::new(format!( + "note codec component reported `{fqn}`, but that custom type does not appear in \ + the package note storage schema" + ))); + } + } + Ok(()) +} + +/// Enforces a byte-size cap on one component-returned string. +fn ensure_returned_string_limit(kind: &str, value: &str, limit: usize) -> Result<()> { + if value.len() > limit { + return Err(Error::new(format!( + "note codec component returned a {kind} of {} bytes; the limit is {limit}", + value.len() + ))); + } + Ok(()) +} + +/// Enforces the structural felt count cap on one `parse` result. +fn ensure_returned_felt_limit(fqn: &str, count: usize) -> Result<()> { + if count > MAX_RETURNED_FELTS { + return Err(Error::new(format!( + "codec `{fqn}` returned {count} felts from `parse`; the limit is {MAX_RETURNED_FELTS}" + ))); + } + Ok(()) +} + /// Converts component integers into canonical field elements. fn component_values_to_felts(fqn: &str, values: &[u64]) -> Result> { values @@ -165,7 +272,7 @@ fn component_values_to_felts(fqn: &str, values: &[u64]) -> Result> { /// Creates a host error for a component runtime failure. fn component_error(action: &str, error: impl core::fmt::Display) -> Error { - Error::new(format!("failed to {action}: {error}")) + Error::new(format!("failed to {action}: {error:#}")) } /// Creates a host error for an author codec rejection. @@ -179,7 +286,7 @@ mod tests { env, fs, path::{Path, PathBuf}, process::{Command, Output}, - sync::Arc, + sync::{Arc, OnceLock}, }; use miden_core::{ @@ -187,15 +294,33 @@ mod tests { operations::Operation, }; use miden_mast_package::{ - PackageExport, PackageId, PathBuf as MastPathBuf, ProcedureExport, Section, TargetType, - Version, + PackageExport, PackageId, PathBuf as MastPathBuf, ProcedureExport, Section, SectionId, + TargetType, Version, }; + use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; use tempfile::TempDir; use wit_component::ComponentEncoder; use super::*; const WASM_TARGET: &str = "wasm32-unknown-unknown"; + const FIXTURE_FQN: &str = "example:codec-schema/note-storage@1.0.0.ratio"; + const FIXTURE_SCHEMA: &str = r#" +package example:codec-schema@1.0.0; + +interface note-storage { + record ratio { + numerator: u64, + denominator: u64, + } + + record codec-note { + ratio: ratio, + } + + type storage = codec-note; +} +"#; #[test] fn component_boundary_rejects_noncanonical_felts() { @@ -214,14 +339,17 @@ mod tests { let component = build_fixture_component(); let mut package = test_package(); + package.sections.push(Section::new( + SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(), + FIXTURE_SCHEMA.as_bytes().to_vec(), + )); package.sections.push(Section::new( SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID).unwrap(), component, )); let registry = CodecRegistry::load_from_package(&package).unwrap(); - let fqn = "example:codec-schema/note-storage@1.0.0.ratio"; - let codec = registry.codec(fqn).expect("fixture ratio codec was not registered"); + let codec = registry.codec(FIXTURE_FQN).expect("fixture ratio codec was not registered"); let encoded = codec.parse("3/2").unwrap(); assert_eq!(encoded, [Felt::new(3).unwrap(), Felt::ZERO, Felt::new(2).unwrap(), Felt::ZERO]); codec.validate(&encoded).unwrap(); @@ -231,6 +359,118 @@ mod tests { assert!(codec.validate(&invalid).unwrap_err().to_string().contains("denominator")); } + #[test] + fn component_calls_recover_after_traps_and_enforce_fuel() { + if !wasm_target_is_installed() { + eprintln!("skipping component adapter test: {WASM_TARGET} is not installed"); + return; + } + + let schema = NoteStorageSchema::from_wit_text(FIXTURE_SCHEMA).unwrap(); + let registry = CodecRegistry::load_from_component( + &build_fixture_component(), + &schema.custom_type_fqns(), + ) + .unwrap(); + let codec = registry.codec(FIXTURE_FQN).unwrap(); + + assert!(codec.parse("trap").unwrap_err().to_string().contains("call `parse`")); + assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); + + let oversized_input = "x".repeat(MAX_COMPONENT_MEMORY_BYTES + 1); + assert!(codec.parse(&oversized_input).is_err()); + assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); + + let fuel_error = codec.parse("loop").unwrap_err().to_string(); + assert!( + fuel_error.contains("fuel") || fuel_error.contains("interrupt"), + "unexpected budget error: {fuel_error}" + ); + assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); + } + + #[test] + fn reported_fqns_cannot_replace_standard_or_unrelated_types() { + let standard = CodecRegistry::default(); + let allowed = HashSet::from([FIXTURE_FQN.to_owned()]); + + let collision = + validate_reported_fqns(&[crate::ACCOUNT_ID_FQN.to_owned()], &allowed, &standard) + .unwrap_err() + .to_string(); + assert!(collision.contains("cannot replace the standard codec")); + + let unrelated = validate_reported_fqns( + &["example:other/schema@1.0.0.value".to_owned()], + &allowed, + &standard, + ) + .unwrap_err() + .to_string(); + assert!(unrelated.contains("does not appear in the package note storage schema")); + + let duplicate = validate_reported_fqns( + &[FIXTURE_FQN.to_owned(), FIXTURE_FQN.to_owned()], + &allowed, + &standard, + ) + .unwrap_err() + .to_string(); + assert!(duplicate.contains("more than once")); + } + + #[test] + fn returned_values_are_size_limited() { + let long = "x".repeat(MAX_RETURNED_STRING_BYTES + 1); + assert!( + ensure_returned_string_limit("display value", &long, MAX_RETURNED_STRING_BYTES) + .unwrap_err() + .to_string() + .contains("the limit is") + ); + assert!( + ensure_returned_felt_limit(FIXTURE_FQN, MAX_RETURNED_FELTS + 1) + .unwrap_err() + .to_string() + .contains("the limit is") + ); + } + + #[test] + fn package_readers_reject_duplicate_schema_and_codec_sections() { + let schema_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(); + let codec_id = SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID).unwrap(); + let mut duplicate_schema = test_package(); + duplicate_schema + .sections + .push(Section::new(schema_id.clone(), FIXTURE_SCHEMA.as_bytes().to_vec())); + duplicate_schema + .sections + .push(Section::new(schema_id, FIXTURE_SCHEMA.as_bytes().to_vec())); + assert!( + NoteStorageSchema::from_package(&duplicate_schema) + .err() + .expect("duplicate schema sections must fail") + .to_string() + .contains("more than one `note_storage_schema` section") + ); + + let mut duplicate_codec = test_package(); + duplicate_codec.sections.push(Section::new( + SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(), + FIXTURE_SCHEMA.as_bytes().to_vec(), + )); + duplicate_codec.sections.push(Section::new(codec_id.clone(), Vec::new())); + duplicate_codec.sections.push(Section::new(codec_id, Vec::new())); + assert!( + CodecRegistry::load_from_package(&duplicate_codec) + .err() + .expect("duplicate codec sections must fail") + .to_string() + .contains("more than one `note_codec` section") + ); + } + /// Builds a valid package with one procedure export. fn test_package() -> Package { let mut builder = DenseMastForestBuilder::new(); @@ -260,6 +500,12 @@ mod tests { /// Builds the minimal author codec used by the Phase 4a component spike. fn build_fixture_component() -> Vec { + static COMPONENT: OnceLock> = OnceLock::new(); + COMPONENT.get_or_init(build_fixture_component_uncached).clone() + } + + /// Builds the component fixture once for this test process. + fn build_fixture_component_uncached() -> Vec { let fixture = TempDir::new().expect("failed to create component fixture directory"); write_fixture(fixture.path()); let target_dir = workspace_root().join("target/note-schema-component-test"); @@ -370,6 +616,14 @@ interface note-storage { #[miden_note_codec::note_codec] impl AuthorTypeCodec for Ratio { fn parse(value: &str) -> Result { + if value == "trap" { + panic!("fixture trap"); + } + if value == "loop" { + loop { + std::hint::black_box(()); + } + } let (numerator, denominator) = value .split_once('/') .ok_or_else(|| "a ratio must use `numerator/denominator`".to_owned())?; diff --git a/sdk/note-schema/src/lib.rs b/sdk/note-schema/src/lib.rs index 3feaa4c327..03d86c3969 100644 --- a/sdk/note-schema/src/lib.rs +++ b/sdk/note-schema/src/lib.rs @@ -14,17 +14,20 @@ #![deny(missing_docs)] +mod artifact; mod builder; mod codec; #[cfg(feature = "codec-component")] mod codec_component; mod error; mod schema; +mod section; mod value; #[cfg(test)] mod tests; +pub use artifact::{NotePackageArtifact, NotePackageResolver}; pub use builder::NoteStorageBuilder; pub use codec::{ ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, CodecRegistry, ConsumerTypeCodec, FELT_FQN, WORD_FQN, diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs index 1b76ad46f6..b98afaf006 100644 --- a/sdk/note-schema/src/schema.rs +++ b/sdk/note-schema/src/schema.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; -use miden_mast_package::{Package, SectionId}; +use miden_mast_package::Package; use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; use wit_parser::{Resolve, Type, TypeDefKind, TypeId, TypeOwner}; @@ -185,25 +185,10 @@ pub struct NoteStorageSchema { impl NoteStorageSchema { /// Reads and resolves the note storage schema section from a Miden package. pub fn from_package(package: &Package) -> Result { - let section_id = - SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).map_err(|err| { - Error::new(format!( - "invalid note storage schema section id \ - `{PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID}`: {err}" - )) - })?; - let bytes = package - .sections - .iter() - .find(|section| section.id == section_id) - .ok_or_else(|| { - Error::new(format!( - "package does not contain the `{PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID}` \ - section" - )) - })? - .data - .as_ref(); + let bytes = crate::section::unique_package_section( + package, + PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, + )?; let unpadded_len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); let text = core::str::from_utf8(&bytes[..unpadded_len]).map_err(|err| { Error::new(format!("note storage schema section is not valid UTF-8: {err}")) @@ -229,6 +214,7 @@ impl NoteStorageSchema { let storage_id = interface.types.get("storage").copied().ok_or_else(|| { Error::new("the `note-storage` interface does not define the `storage` type alias") })?; + validate_resolved_core_types(&resolve)?; let root = ModelBuilder::new(&resolve).build(Type::Id(storage_id))?; if !matches!(root.kind, SchemaTypeKind::Record(_)) { return Err(Error::new(format!( @@ -237,11 +223,13 @@ impl NoteStorageSchema { ))); } - Ok(Self { + let schema = Self { wit_text: wit_text.to_owned(), root, codecs: CodecRegistry::default(), - }) + }; + schema.validate_native_leaf_shapes()?; + Ok(schema) } /// Returns the unpadded WIT document. @@ -254,6 +242,19 @@ impl NoteStorageSchema { &self.root } + /// Verifies native host mappings against the pinned standard type shapes. + pub fn validate_native_leaf_shapes(&self) -> Result<()> { + validate_model_type_shapes(&self.root, &mut HashSet::new()) + } + + /// Returns all custom named types reachable from the storage root. + #[cfg(feature = "codec-component")] + pub(crate) fn custom_type_fqns(&self) -> HashSet { + let mut fqns = HashSet::new(); + collect_custom_type_fqns(&self.root, &mut fqns); + fqns + } + /// Returns the root felt layout. pub const fn layout(&self) -> FeltLayout { self.root.layout @@ -298,6 +299,203 @@ impl NoteStorageSchema { } } +/// Verifies the raw embedded core-types definitions before the model applies native mappings. +fn validate_resolved_core_types(resolve: &Resolve) -> Result<()> { + let Some((_, package_id)) = resolve.package_names.iter().find(|(name, _)| { + name.namespace == "miden" + && name.name == "base" + && name.version.as_ref().is_some_and(|version| version.to_string() == "1.0.0") + }) else { + return Ok(()); + }; + let package = &resolve.packages[*package_id]; + let Some(interface_id) = package.interfaces.get("core-types").copied() else { + return Ok(()); + }; + let interface = &resolve.interfaces[interface_id]; + + for (name, fields) in [ + ("felt", &["inner"][..]), + ("word", &["a", "b", "c", "d"][..]), + ("account-id", &["prefix", "suffix"][..]), + ("asset-amount", &["inner"][..]), + ] { + let Some(type_id) = interface.types.get(name).copied() else { + continue; + }; + let type_id = follow_resolved_aliases(resolve, type_id)?; + let TypeDefKind::Record(record) = &resolve.types[type_id].kind else { + return Err(core_shape_error(name, fields)); + }; + if record.fields.len() != fields.len() + || record + .fields + .iter() + .zip(fields) + .any(|(field, expected)| field.name != *expected) + { + return Err(core_shape_error(name, fields)); + } + if name == "felt" { + if !resolves_to_primitive(resolve, record.fields[0].ty, Type::F32)? { + return Err(core_shape_error(name, fields)); + } + } else { + for field in &record.fields { + if !resolves_to_fqn(resolve, field.ty, crate::FELT_FQN)? { + return Err(core_shape_error(name, fields)); + } + } + } + } + Ok(()) +} + +/// Follows raw WIT aliases to their structural definition. +fn follow_resolved_aliases(resolve: &Resolve, mut id: TypeId) -> Result { + let mut visited = HashSet::new(); + loop { + if !visited.insert(id) { + return Err(Error::new("cyclic WIT type aliases are not supported")); + } + match resolve.types[id].kind { + TypeDefKind::Type(Type::Id(next)) => id = next, + _ => return Ok(id), + } + } +} + +/// Returns true when a raw WIT type resolves to one primitive. +fn resolves_to_primitive(resolve: &Resolve, mut ty: Type, expected: Type) -> Result { + let mut visited = HashSet::new(); + loop { + match ty { + Type::Id(id) => { + if !visited.insert(id) { + return Err(Error::new("cyclic WIT type aliases are not supported")); + } + let TypeDefKind::Type(next) = resolve.types[id].kind else { + return Ok(false); + }; + ty = next; + } + primitive => return Ok(primitive == expected), + } + } +} + +/// Returns true when a raw WIT type resolves to one canonical FQN. +fn resolves_to_fqn(resolve: &Resolve, ty: Type, expected: &str) -> Result { + let Type::Id(id) = ty else { + return Ok(false); + }; + let id = follow_resolved_aliases(resolve, id)?; + Ok(ModelBuilder::new(resolve).type_fqn(id)?.as_deref() == Some(expected)) +} + +/// Verifies mapped type shapes in the owned schema model. +fn validate_model_type_shapes(ty: &SchemaType, seen: &mut HashSet) -> Result<()> { + if let Some(fqn) = ty.fqn() + && !seen.insert(fqn.to_owned()) + { + return Ok(()); + } + + match ty.fqn() { + Some(crate::FELT_FQN) if !matches!(ty.kind(), SchemaTypeKind::Felt) => { + return Err(core_shape_error("felt", &["inner"])); + } + Some(crate::WORD_FQN) => { + validate_model_record(ty, "word", &["a", "b", "c", "d"], crate::FELT_FQN)? + } + Some(crate::ACCOUNT_ID_FQN) => { + validate_model_record(ty, "account-id", &["prefix", "suffix"], crate::FELT_FQN)? + } + Some(crate::ASSET_AMOUNT_FQN) => { + validate_model_record(ty, "asset-amount", &["inner"], crate::FELT_FQN)? + } + _ => {} + } + + match ty.kind() { + SchemaTypeKind::Record(fields) => { + for field in fields { + validate_model_type_shapes(field.ty(), seen)?; + } + } + SchemaTypeKind::Option(payload) => validate_model_type_shapes(payload, seen)?, + SchemaTypeKind::Variant(cases) => { + for payload in cases.iter().filter_map(SchemaCase::payload) { + validate_model_type_shapes(payload, seen)?; + } + } + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {} + } + Ok(()) +} + +/// Verifies one mapped record in the owned schema model. +fn validate_model_record( + ty: &SchemaType, + name: &str, + expected_fields: &[&str], + expected_field_fqn: &str, +) -> Result<()> { + let SchemaTypeKind::Record(fields) = ty.kind() else { + return Err(core_shape_error(name, expected_fields)); + }; + if fields.len() != expected_fields.len() + || fields.iter().zip(expected_fields).any(|(field, expected)| { + field.name() != *expected || field.ty().fqn() != Some(expected_field_fqn) + }) + { + return Err(core_shape_error(name, expected_fields)); + } + Ok(()) +} + +/// Creates the canonical core-type shape diagnostic. +fn core_shape_error(name: &str, fields: &[&str]) -> Error { + let field_shape = if name == "felt" { + "inner: f32".to_owned() + } else { + fields + .iter() + .map(|field| format!("{field}: felt")) + .collect::>() + .join(", ") + }; + Error::new(format!( + "embedded WIT type `miden:base/core-types@1.0.0.{name}` does not match the pinned \ + canonical shape `record {name} {{ {field_shape} }}`" + )) +} + +/// Collects schema-owned types and excludes the pinned SDK core-types package. +#[cfg(feature = "codec-component")] +fn collect_custom_type_fqns(ty: &SchemaType, fqns: &mut HashSet) { + if let Some(fqn) = ty.fqn() + && !fqn.starts_with("miden:base/core-types@") + && !fqn.starts_with("miden:base/core-types.") + { + fqns.insert(fqn.to_owned()); + } + match ty.kind() { + SchemaTypeKind::Record(fields) => { + for field in fields { + collect_custom_type_fqns(field.ty(), fqns); + } + } + SchemaTypeKind::Option(payload) => collect_custom_type_fqns(payload, fqns), + SchemaTypeKind::Variant(cases) => { + for payload in cases.iter().filter_map(SchemaCase::payload) { + collect_custom_type_fqns(payload, fqns); + } + } + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {} + } +} + /// Builds an owned schema type tree from a resolved WIT graph. struct ModelBuilder<'a> { resolve: &'a Resolve, diff --git a/sdk/note-schema/src/section.rs b/sdk/note-schema/src/section.rs new file mode 100644 index 0000000000..0b4e2b2653 --- /dev/null +++ b/sdk/note-schema/src/section.rs @@ -0,0 +1,23 @@ +//! Package custom-section access. + +use miden_mast_package::{Package, SectionId}; + +use crate::{Error, Result}; + +/// Returns the only package section with `section_name`. +pub(crate) fn unique_package_section<'a>( + package: &'a Package, + section_name: &str, +) -> Result<&'a [u8]> { + let section_id = SectionId::custom(section_name).map_err(|error| { + Error::new(format!("invalid package section id `{section_name}`: {error}")) + })?; + let mut matches = package.sections.iter().filter(|section| section.id == section_id); + let section = matches.next().ok_or_else(|| { + Error::new(format!("package does not contain the `{section_name}` section")) + })?; + if matches.next().is_some() { + return Err(Error::new(format!("package contains more than one `{section_name}` section"))); + } + Ok(section.data.as_ref()) +} diff --git a/sdk/note-schema/src/tests.rs b/sdk/note-schema/src/tests.rs index d905eeb40e..ed97d2db19 100644 --- a/sdk/note-schema/src/tests.rs +++ b/sdk/note-schema/src/tests.rs @@ -52,6 +52,28 @@ package miden:base@1.0.0 { } "#; +const NESTED_CONSTRUCTOR_SCHEMA: &str = r#" +package example:nested-constructors@1.0.0; + +interface note-storage { + record payload { + value: u64, + } + + variant selection { + empty, + nested(payload), + } + + record constructor-note { + maybe-payload: option, + selected: selection, + } + + type storage = constructor-note; +} +"#; + /// Returns a valid account ID and its mainnet bech32 form. fn account_id() -> (AccountId, String) { let account_id = AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); @@ -97,6 +119,43 @@ fn wit_reader_reports_missing_schema_surface() { assert!(error.contains("does not define the `note-storage` interface")); } +#[test] +fn reader_rejects_noncanonical_native_core_type_shapes() { + for (type_name, definitions) in [ + ("felt", "record felt { inner: u32 }"), + ("word", "record felt { inner: f32 } record word { a: felt, b: felt, c: felt }"), + ( + "account-id", + "record felt { inner: f32 } record account-id { suffix: felt, prefix: felt }", + ), + ("asset-amount", "record felt { inner: f32 } record asset-amount { inner: u64 }"), + ] { + let wit = format!( + r#" +package example:bad-core-shape@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage {{ + use core-types.{{{type_name}}}; + record bad-note {{ value: {type_name} }} + type storage = bad-note; +}} + +package miden:base@1.0.0 {{ + interface core-types {{ {definitions} }} +}} +"# + ); + let error = NoteStorageSchema::from_wit_text(&wit).err().unwrap().to_string(); + assert!( + error.contains(&format!("miden:base/core-types@1.0.0.{type_name}")), + "unexpected error for {type_name}: {error}" + ); + assert!(error.contains("pinned canonical shape")); + } +} + #[test] fn builder_normalizes_paths_and_uses_declaration_order() { let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); @@ -164,7 +223,7 @@ fn decoder_uses_structural_fallback_without_a_codec() { .build() .unwrap(); - let decoded = schema.decode_with_registry(&storage, &CodecRegistry::new()).unwrap(); + let decoded = schema.decode_with_registry(&storage, &CodecRegistry::empty()).unwrap(); let account = decoded.field("target-account-id").unwrap(); let DecodedValueKind::Record(fields) = account.kind() else { panic!("account-id must use its structural record fallback"); @@ -216,6 +275,23 @@ fn builder_reports_missing_unknown_conflicting_and_range_errors() { assert!(range.contains("u8 value `256` is out of range")); } +#[test] +fn builder_rejects_nested_record_constructor_paths() { + let schema = NoteStorageSchema::from_wit_text(NESTED_CONSTRUCTOR_SCHEMA).unwrap(); + + let option_error = + schema.builder().set("maybe_payload", "some(value)").err().unwrap().to_string(); + assert!(option_error.contains("path `maybe-payload`")); + assert!(option_error.contains("option with record payload")); + assert!(option_error.contains("does not support nested record constructors")); + + let variant_error = + schema.builder().set("selected", "nested(value)").err().unwrap().to_string(); + assert!(variant_error.contains("path `selected`")); + assert!(variant_error.contains("variant case `nested` with a record payload")); + assert!(variant_error.contains("does not support nested record constructors")); +} + #[test] fn decoder_rejects_invalid_option_and_variant_tags() { let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); diff --git a/sdk/sdk/MIGRATION.md b/sdk/sdk/MIGRATION.md index b0709599a3..3617b74c94 100644 --- a/sdk/sdk/MIGRATION.md +++ b/sdk/sdk/MIGRATION.md @@ -146,6 +146,48 @@ path. Two different projects that contain a contract crate with the same package version and share one `CARGO_TARGET_DIR` reuse each other's build-script output — including the staged package cache. Use per-checkout target directories for such layouts. +### Rewrite tuple-note and `Vec` storage layouts + +`#[note]` now emits a WIT storage schema and therefore requires each stored value to have a stable, +named position. Unit structs remain valid, but tuple structs must become named-field structs. Keep +the fields in the same order to preserve the existing felt layout: + +```rust +// before +#[note] +struct PaymentNote(AccountId, u64); + +// after +#[note] +struct PaymentNote { + target: AccountId, + amount: u64, +} +``` + +Dynamic `Vec` fields have no fixed note-storage layout and are no longer accepted. Replace a vector +with explicit fields. Use `Option` for fixed optional positions, or use a named `#[export_type]` +record when the same fixed group is nested or reused: + +```rust +// before +#[note] +struct ValuesNote { + values: Vec, +} + +// after +#[note] +struct ValuesNote { + first: Felt, + second: Option, +} +``` + +There is no direct replacement for an unbounded vector. Choose a fixed maximum represented by +named and optional fields, or redesign the note so variable-sized data is committed outside its +storage payload. + ### Kernel scalars are typed instead of `Felt` (counts, block heights, nonces, attachments) Binding surfaces whose values are counts now return `u32`: `tx::get_num_input_notes`, diff --git a/tests/integration-network/src/mockchain/notes/schema.rs b/tests/integration-network/src/mockchain/notes/schema.rs index cd2d1c8257..8b442cdc2c 100644 --- a/tests/integration-network/src/mockchain/notes/schema.rs +++ b/tests/integration-network/src/mockchain/notes/schema.rs @@ -1,11 +1,6 @@ //! Schema-driven note storage tests on the mock chain. -use std::{ - env, - path::{Path, PathBuf}, - process::{Command, Output}, - sync::Arc, -}; +use std::{process::Command, sync::Arc}; use miden_client::{ account::{AccountComponent, component::InitStorageData}, @@ -102,7 +97,11 @@ fn transfer_with_storage( #[test] fn dex_note_uses_embedded_schema_and_component_codec() { - let note_package = build_dex_note_package(); + if !wasm_target_is_installed() { + eprintln!("skipping DEX note schema test: wasm32-unknown-unknown is not installed"); + return; + } + let note_package = compile_rust_package("../../examples/dex-note", true); assert_package_section(¬e_package, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID); assert_package_section(¬e_package, PACKAGE_NOTE_CODEC_SECTION_ID); let schema = NoteStorageSchema::from_package(¬e_package).unwrap(); @@ -148,39 +147,22 @@ fn p2id_note_builds_storage_without_a_component_codec() { ); } -/// Runs cargo-miden so the DEX package receives its codec section. -fn build_dex_note_package() -> Arc { - let root = workspace_root(); - let project = root.join("examples/dex-note"); - let binary = cargo_miden_binary(&root); - let output = Command::new(binary) - .args(["miden", "build", "--release"]) - .current_dir(&project) - .output() - .expect("failed to start cargo miden for dex-note"); - assert_command_succeeded("cargo miden build for dex-note", &output); - - Arc::new( - Package::deserialize_from_file(project.join("target/miden/release/dex-note.masp")) - .expect("failed to read the cargo-miden DEX package"), - ) -} - -/// Returns the cargo-miden binary built by the workspace test workflow. -fn cargo_miden_binary(root: &Path) -> PathBuf { - let candidates = [root.join("target/debug/cargo-miden"), root.join("bin/cargo-miden")]; - candidates.into_iter().find(|candidate| candidate.is_file()).unwrap_or_else(|| { - panic!("cargo-miden is not built; run `cargo build -p cargo-miden` before this test") - }) -} - -/// Returns the compiler workspace root. -fn workspace_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(Path::parent) - .expect("integration-network must be under tests/") - .to_owned() +/// Returns true when rustup reports the codec component target as installed. +fn wasm_target_is_installed() -> bool { + let output = match Command::new("rustup").args(["target", "list"]).output() { + Ok(output) if output.status.success() => output, + Ok(output) => { + eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); + return false; + } + Err(error) => { + eprintln!("could not run `rustup target list`: {error}"); + return false; + } + }; + String::from_utf8_lossy(&output.stdout) + .lines() + .any(|line| line.starts_with("wasm32-unknown-unknown") && line.contains("(installed)")) } /// Asserts that a package carries one named custom section. @@ -191,13 +173,3 @@ fn assert_package_section(package: &Package, name: &str) { "package does not contain the `{name}` section" ); } - -/// Includes both output streams when a child process fails. -fn assert_command_succeeded(action: &str, output: &Output) { - assert!( - output.status.success(), - "{action} failed\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); -} diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs index dcf1c6252c..a5305e8596 100644 --- a/tools/cargo-miden/tests/dex_note_codec_build.rs +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -14,6 +14,10 @@ use crate::utils::{current_dir_lock, workspace_root}; #[test] fn dex_note_build_embeds_schema_and_zero_import_codec_component() { + if !wasm_target_is_installed() { + eprintln!("skipping DEX note codec build test: wasm32-unknown-unknown is not installed"); + return; + } let _cwd_lock = current_dir_lock(); let _ = midenc_log::Builder::from_env("MIDENC_TRACE") .is_test(true) @@ -57,6 +61,24 @@ fn dex_note_build_embeds_schema_and_zero_import_codec_component() { assert_note_codec_component(codec.data.as_ref()); } +/// Returns true when rustup reports the codec component target as installed. +fn wasm_target_is_installed() -> bool { + let output = match std::process::Command::new("rustup").args(["target", "list"]).output() { + Ok(output) if output.status.success() => output, + Ok(output) => { + eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); + return false; + } + Err(error) => { + eprintln!("could not run `rustup target list`: {error}"); + return false; + } + }; + String::from_utf8_lossy(&output.stdout) + .lines() + .any(|line| line.starts_with("wasm32-unknown-unknown") && line.contains("(installed)")) +} + /// Verifies the sandbox and versioned interface exported by a note codec component. fn assert_note_codec_component(component: &[u8]) { let DecodedWasm::Component(resolve, world_id) = From 1c03d684c4b06ffd3d90301dee8882cb685eb33d Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Mon, 10 Aug 2026 12:02:37 +0300 Subject: [PATCH 04/43] build: satisfy CI hygiene checks and classify note crates for sdk publishing The branch's first CI run flagged four problems: dependencies declared inline in several crates instead of the workspace table, a wasmtime feature set that does not compile under the release job's feature unification, unused dependency declarations, and workspace packages missing from the release classification. Separately, the note crates' long-term home changed: they stay in this repository instead of moving to the protocol repo, so they publish with the sdk release unit. Promote `prettyplease`, `wit-bindgen`, and `proc-macro-crate` to workspace dependencies. Add wasmtime's `std` feature, which its mmap-backed runtime needs on Linux. Remove workspace entries nothing inherits and a leftover dev-dependency, and move the bindings expansion goldens next to the tests that read them. Classify all six note crates as published members of the sdk unit (`version-source = "sdk"`) at the sdk train version, and update the changelog wording accordingly. --- .release/config.toml | 24 +++++++++++++++++++ Cargo.lock | 1 - Cargo.toml | 6 +++-- examples/dex-note-codec/Cargo.lock | 15 ++++++------ examples/dex-note/Cargo.lock | 18 +++++++------- sdk/CHANGELOG.md | 4 ++-- sdk/note-bindings/Cargo.toml | 1 - sdk/note-bindings/macros/Cargo.toml | 5 ++-- .../{src => macros}/expected/custom.rs | 0 .../{src => macros}/expected/p2id.rs | 0 sdk/note-bindings/macros/src/tests.rs | 4 ++-- sdk/note-codec/Cargo.toml | 3 +-- sdk/note-codec/macros/Cargo.toml | 5 ++-- sdk/note-schema/Cargo.toml | 1 - sdk/note-schema/codegen/Cargo.toml | 6 ++--- sdk/sdk/Cargo.toml | 2 +- tools/cargo-miden/Cargo.toml | 2 +- 17 files changed, 57 insertions(+), 40 deletions(-) rename sdk/note-bindings/{src => macros}/expected/custom.rs (100%) rename sdk/note-bindings/{src => macros}/expected/p2id.rs (100%) diff --git a/.release/config.toml b/.release/config.toml index 55f7ce7667..8dc8e7884a 100644 --- a/.release/config.toml +++ b/.release/config.toml @@ -223,6 +223,30 @@ unit = "sdk" name = "miden-field-repr-tests" unit = "private" +[[packages]] +name = "miden-note-schema" +unit = "sdk" + +[[packages]] +name = "miden-note-schema-codegen" +unit = "sdk" + +[[packages]] +name = "miden-note-bindings" +unit = "sdk" + +[[packages]] +name = "miden-note-bindings-macros" +unit = "sdk" + +[[packages]] +name = "miden-note-codec" +unit = "sdk" + +[[packages]] +name = "miden-note-codec-macros" +unit = "sdk" + [[packages]] name = "midenc-benchmark-runner" unit = "private" diff --git a/Cargo.lock b/Cargo.lock index 9b27b83a27..bf2bb27134 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3689,7 +3689,6 @@ version = "0.14.0" dependencies = [ "heck", "miden-note-schema", - "midenc-expect-test", "prettyplease", "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 6ccbb35df3..7fdd3a525d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,6 +145,9 @@ serde = { version = "1.0", default-features = false, features = [ ] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } heck = "0.5" +prettyplease = "0.2" +proc-macro-crate = "3.5" +wit-bindgen = { version = "0.57", default-features = false, features = ["macros"] } smallvec = { version = "1.15", default-features = false, features = [ "union", "const_generics", @@ -171,6 +174,7 @@ wasmtime = { version = "34.0.0", default-features = false, features = [ "component-model", "cranelift", "runtime", + "std", ] } # Workspace crates @@ -198,9 +202,7 @@ midenc-integration-test-support = { path = "tests/support" } midenc-expect-test = { path = "tools/expect-test" } miden-base-sys = { version = "0.14.0", path = "sdk/base-sys" } miden-field-repr = { version = "0.14.0", path = "sdk/field-repr/repr" } -miden-note-bindings = { version = "0.14.0", path = "sdk/note-bindings" } miden-note-bindings-macros = { version = "0.14.0", path = "sdk/note-bindings/macros" } -miden-note-codec = { version = "0.14.0", path = "sdk/note-codec" } miden-note-codec-macros = { version = "0.14.0", path = "sdk/note-codec/macros" } miden-note-schema = { version = "0.14.0", path = "sdk/note-schema" } miden-note-schema-codegen = { version = "0.14.0", path = "sdk/note-schema/codegen" } diff --git a/examples/dex-note-codec/Cargo.lock b/examples/dex-note-codec/Cargo.lock index 24f42869c8..6d184e8001 100644 --- a/examples/dex-note-codec/Cargo.lock +++ b/examples/dex-note-codec/Cargo.lock @@ -1171,7 +1171,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1179,7 +1179,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "proc-macro2", "quote", @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "miden-note-codec" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "miden-field", "miden-field-repr", @@ -1299,10 +1299,9 @@ dependencies = [ [[package]] name = "miden-note-codec-macros" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "heck", - "miden-mast-package", "miden-note-schema", "miden-note-schema-codegen", "proc-macro-crate", @@ -1313,7 +1312,7 @@ dependencies = [ [[package]] name = "miden-note-schema" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "miden-field", "miden-field-repr", @@ -1325,7 +1324,7 @@ dependencies = [ [[package]] name = "miden-note-schema-codegen" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "heck", "miden-note-schema", @@ -1519,7 +1518,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "serde", "serde_json", diff --git a/examples/dex-note/Cargo.lock b/examples/dex-note/Cargo.lock index ddcce11e11..56f56f169a 100644 --- a/examples/dex-note/Cargo.lock +++ b/examples/dex-note/Cargo.lock @@ -968,7 +968,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "miden-base", "miden-base-macros", @@ -1057,7 +1057,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1065,7 +1065,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "heck", "miden-assembly-syntax", @@ -1086,7 +1086,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "proc-macro2", "quote", @@ -1427,7 +1427,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.13.1" +version = "0.14.0-rc.1" [[package]] name = "miden-serde-utils" @@ -1463,7 +1463,7 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "miden-field", ] @@ -1531,7 +1531,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.13.1" +version = "0.14.0-rc.1" dependencies = [ "serde", "serde_json", diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index db4512ef55..e6ad4b3dc5 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -9,10 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added optional `codec-component` support to the internal `miden-note-schema` host crate. It can +- Added optional `codec-component` support to the new `miden-note-schema` host crate. It can load author-defined note codecs from a package without adding Wasmtime to the default feature set or the guest SDK dependency graph. -- Added typed host note-storage bindings through the internal `miden-note-bindings` macros. Bindings +- Added typed host note-storage bindings through the new `miden-note-bindings` macros. Bindings can load a built note project or an exact `.masp`, generate native Rust storage types, and convert typed values to and from note storage. Its facade supplies all generated runtime dependencies, and generated string, validation, and display APIs keep stable standard-registry and diff --git a/sdk/note-bindings/Cargo.toml b/sdk/note-bindings/Cargo.toml index 16ca5db6aa..c7d241b458 100644 --- a/sdk/note-bindings/Cargo.toml +++ b/sdk/note-bindings/Cargo.toml @@ -12,7 +12,6 @@ keywords.workspace = true license.workspace = true readme.workspace = true edition.workspace = true -publish = false [lib] doctest = false diff --git a/sdk/note-bindings/macros/Cargo.toml b/sdk/note-bindings/macros/Cargo.toml index 89703963ae..cb5bca123e 100644 --- a/sdk/note-bindings/macros/Cargo.toml +++ b/sdk/note-bindings/macros/Cargo.toml @@ -12,7 +12,6 @@ keywords.workspace = true license.workspace = true readme.workspace = true edition.workspace = true -publish = false [lib] proc-macro = true @@ -22,10 +21,10 @@ doctest = false miden-note-schema.workspace = true miden-note-schema-codegen.workspace = true proc-macro2.workspace = true -proc-macro-crate = "3.5" +proc-macro-crate = { workspace = true } quote.workspace = true syn = { workspace = true, features = ["visit-mut"] } [dev-dependencies] midenc-expect-test.workspace = true -prettyplease = "0.2" +prettyplease = { workspace = true } diff --git a/sdk/note-bindings/src/expected/custom.rs b/sdk/note-bindings/macros/expected/custom.rs similarity index 100% rename from sdk/note-bindings/src/expected/custom.rs rename to sdk/note-bindings/macros/expected/custom.rs diff --git a/sdk/note-bindings/src/expected/p2id.rs b/sdk/note-bindings/macros/expected/p2id.rs similarity index 100% rename from sdk/note-bindings/src/expected/p2id.rs rename to sdk/note-bindings/macros/expected/p2id.rs diff --git a/sdk/note-bindings/macros/src/tests.rs b/sdk/note-bindings/macros/src/tests.rs index b6a6ba6fe3..af56307522 100644 --- a/sdk/note-bindings/macros/src/tests.rs +++ b/sdk/note-bindings/macros/src/tests.rs @@ -74,10 +74,10 @@ fn expand(wit: &str) -> String { #[test] fn expands_p2id_schema_golden() { - expect_file!["../../src/expected/p2id.rs"].assert_eq(&expand(P2ID_SCHEMA)); + expect_file!["../expected/p2id.rs"].assert_eq(&expand(P2ID_SCHEMA)); } #[test] fn expands_custom_schema_golden() { - expect_file!["../../src/expected/custom.rs"].assert_eq(&expand(CUSTOM_SCHEMA)); + expect_file!["../expected/custom.rs"].assert_eq(&expand(CUSTOM_SCHEMA)); } diff --git a/sdk/note-codec/Cargo.toml b/sdk/note-codec/Cargo.toml index 92f8e0ed32..520cef1a01 100644 --- a/sdk/note-codec/Cargo.toml +++ b/sdk/note-codec/Cargo.toml @@ -12,7 +12,6 @@ keywords.workspace = true license.workspace = true readme.workspace = true edition.workspace = true -publish = false [lib] doctest = false @@ -22,7 +21,7 @@ miden-field.workspace = true miden-field-repr.workspace = true miden-note-codec-macros.workspace = true miden-protocol.workspace = true -wit-bindgen = { version = "0.57", default-features = false, features = ["macros"] } +wit-bindgen = { workspace = true } [dev-dependencies] tempfile.workspace = true diff --git a/sdk/note-codec/macros/Cargo.toml b/sdk/note-codec/macros/Cargo.toml index 695c036f2a..bd6829f4df 100644 --- a/sdk/note-codec/macros/Cargo.toml +++ b/sdk/note-codec/macros/Cargo.toml @@ -12,7 +12,6 @@ keywords.workspace = true license.workspace = true readme.workspace = true edition.workspace = true -publish = false [lib] proc-macro = true @@ -23,9 +22,9 @@ heck.workspace = true miden-note-schema.workspace = true miden-note-schema-codegen.workspace = true proc-macro2.workspace = true -proc-macro-crate = "3.5" +proc-macro-crate = { workspace = true } quote.workspace = true syn = { workspace = true, features = ["visit-mut"] } [dev-dependencies] -prettyplease = "0.2" +prettyplease = { workspace = true } diff --git a/sdk/note-schema/Cargo.toml b/sdk/note-schema/Cargo.toml index ab19fd5c1c..cc0f6115a7 100644 --- a/sdk/note-schema/Cargo.toml +++ b/sdk/note-schema/Cargo.toml @@ -12,7 +12,6 @@ keywords.workspace = true license.workspace = true readme.workspace = true edition.workspace = true -publish = false [features] default = [] diff --git a/sdk/note-schema/codegen/Cargo.toml b/sdk/note-schema/codegen/Cargo.toml index c97f187b16..fc681dfe46 100644 --- a/sdk/note-schema/codegen/Cargo.toml +++ b/sdk/note-schema/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miden-note-schema-codegen" -description = "Internal Rust code generator for Miden note storage schemas" +description = "Rust code generator for Miden note storage schemas" version = "0.14.0" rust-version.workspace = true authors.workspace = true @@ -12,7 +12,6 @@ keywords.workspace = true license.workspace = true readme.workspace = true edition.workspace = true -publish = false [lib] doctest = false @@ -24,6 +23,5 @@ proc-macro2.workspace = true quote.workspace = true [dev-dependencies] -midenc-expect-test.workspace = true -prettyplease = "0.2" +prettyplease = { workspace = true } syn.workspace = true diff --git a/sdk/sdk/Cargo.toml b/sdk/sdk/Cargo.toml index 507c22e273..ad674f3779 100644 --- a/sdk/sdk/Cargo.toml +++ b/sdk/sdk/Cargo.toml @@ -25,7 +25,7 @@ miden-base-sys.workspace = true miden-field-repr.workspace = true miden-field = { workspace = true, default-features = false } miden-tx-script-args = { workspace = true, features = ["miden-vm-guest"] } -wit-bindgen = { version = "0.57", default-features = false, features = ["macros"] } +wit-bindgen = { workspace = true } [features] default = [] diff --git a/tools/cargo-miden/Cargo.toml b/tools/cargo-miden/Cargo.toml index 454cf8c1e3..332ec23c79 100644 --- a/tools/cargo-miden/Cargo.toml +++ b/tools/cargo-miden/Cargo.toml @@ -40,7 +40,6 @@ path = "tests/mod.rs" flate2.workspace = true [dependencies] -miden-mast-package = { workspace = true, features = ["std"] } midenc-compile = { workspace = true, features = ["std"] } midenc-hir = { workspace = true, features = ["std"] } midenc-session.workspace = true @@ -60,6 +59,7 @@ serde_json.workspace = true sha2.workspace = true [dev-dependencies] +miden-mast-package = { workspace = true, features = ["std"] } midenc-frontend-wasm-metadata.workspace = true wit-component.workspace = true wit-parser.workspace = true From 0551a95247a2a2a08d2de3ed43dc693ef20e3938 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Mon, 10 Aug 2026 14:12:42 +0300 Subject: [PATCH 05/43] build: fix per-member target selection and package-escaping includes Two release-workflow jobs stayed red after the previous hygiene pass, each with its own root cause. The per-member check job compiled the host-side note crates for wasm: `sdk/.cargo/config.toml` applied `target = "wasm32-wasip1"` to every cargo invocation inside an sdk crate directory, which sent wasmtime onto its no-std custom platform and broke the build. The directory-wide config is replaced by verbatim per-crate configs in the ten guest-side sdk crates, so guest builds behave exactly as before while host crates build for the host. The package-closure job could not build `midenc-compile` from a registry: its pinned copy of the note-codec world WIT was an `include_str!` that escaped the package root, and the codec macros crate carried the same latent escape. Each embedding package now ships its own copy of the file, and a test in the unpublished integration crate locks all three copies together. This avoids the alternative of making the compiler depend on `miden-note-codec`, which would pull the author-side codec stack into the compiler's dependency tree for the sake of one string. --- midenc-compile/src/cargo.rs | 2 +- midenc-compile/wit/note-codec.wit | 27 +++++++++++++++++++ sdk/{ => alloc}/.cargo/config.toml | 0 sdk/base-macros/.cargo/config.toml | 2 ++ sdk/base-sys/.cargo/config.toml | 2 ++ sdk/base/.cargo/config.toml | 2 ++ sdk/field-repr/derive/.cargo/config.toml | 2 ++ sdk/field-repr/repr/.cargo/config.toml | 2 ++ sdk/field-repr/tests/.cargo/config.toml | 2 ++ sdk/note-codec/macros/src/expand.rs | 2 +- sdk/note-codec/macros/wit/note-codec.wit | 27 +++++++++++++++++++ sdk/sdk/.cargo/config.toml | 2 ++ sdk/stdlib-sys/.cargo/config.toml | 2 ++ sdk/wasm-metadata/.cargo/config.toml | 2 ++ .../examples/note_schema_metadata.rs | 12 +++++++++ 15 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 midenc-compile/wit/note-codec.wit rename sdk/{ => alloc}/.cargo/config.toml (100%) create mode 100644 sdk/base-macros/.cargo/config.toml create mode 100644 sdk/base-sys/.cargo/config.toml create mode 100644 sdk/base/.cargo/config.toml create mode 100644 sdk/field-repr/derive/.cargo/config.toml create mode 100644 sdk/field-repr/repr/.cargo/config.toml create mode 100644 sdk/field-repr/tests/.cargo/config.toml create mode 100644 sdk/note-codec/macros/wit/note-codec.wit create mode 100644 sdk/sdk/.cargo/config.toml create mode 100644 sdk/stdlib-sys/.cargo/config.toml create mode 100644 sdk/wasm-metadata/.cargo/config.toml diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 7034ac286c..bf6090a790 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -32,7 +32,7 @@ const NOTE_CODEC_TARGET: &str = "wasm32-unknown-unknown"; const NOTE_CODEC_PACKAGE_PATH_ENV: &str = "MIDENC_NOTE_CODEC_PACKAGE_PATH"; /// Pinned author codec interface used to validate component signatures. -const NOTE_CODEC_WIT: &str = include_str!("../../sdk/note-codec/wit/note-codec.wit"); +const NOTE_CODEC_WIT: &str = include_str!("../wit/note-codec.wit"); /// Cargo-specific options extracted from the `Compiler` struct. /// diff --git a/midenc-compile/wit/note-codec.wit b/midenc-compile/wit/note-codec.wit new file mode 100644 index 0000000000..3cde3d4442 --- /dev/null +++ b/midenc-compile/wit/note-codec.wit @@ -0,0 +1,27 @@ +package miden:note-codec@1.0.0; + +/// Parses, displays, and validates custom note storage types. +interface codec { + /// A canonical Miden field element at the component boundary. + /// + /// This is a `u64`, not the core-types `felt` record, because that record models the + /// compiler's guest felt value and does not expose its canonical integer representation. + type felt = u64; + + /// Returns the fully-qualified WIT names handled by this component. + supported-types: func() -> list; + + /// Parses text into a type's structural felt representation. + parse: func(type-fqn: string, value: string) -> result, string>; + + /// Displays a type's structural felt representation. + display: func(type-fqn: string, value: list) -> result; + + /// Validates a type's structural felt representation. + validate: func(type-fqn: string, value: list) -> result<_, string>; +} + +world note-codec { + export codec; +} + diff --git a/sdk/.cargo/config.toml b/sdk/alloc/.cargo/config.toml similarity index 100% rename from sdk/.cargo/config.toml rename to sdk/alloc/.cargo/config.toml diff --git a/sdk/base-macros/.cargo/config.toml b/sdk/base-macros/.cargo/config.toml new file mode 100644 index 0000000000..6b509f5b70 --- /dev/null +++ b/sdk/base-macros/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/sdk/base-sys/.cargo/config.toml b/sdk/base-sys/.cargo/config.toml new file mode 100644 index 0000000000..6b509f5b70 --- /dev/null +++ b/sdk/base-sys/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/sdk/base/.cargo/config.toml b/sdk/base/.cargo/config.toml new file mode 100644 index 0000000000..6b509f5b70 --- /dev/null +++ b/sdk/base/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/sdk/field-repr/derive/.cargo/config.toml b/sdk/field-repr/derive/.cargo/config.toml new file mode 100644 index 0000000000..6b509f5b70 --- /dev/null +++ b/sdk/field-repr/derive/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/sdk/field-repr/repr/.cargo/config.toml b/sdk/field-repr/repr/.cargo/config.toml new file mode 100644 index 0000000000..6b509f5b70 --- /dev/null +++ b/sdk/field-repr/repr/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/sdk/field-repr/tests/.cargo/config.toml b/sdk/field-repr/tests/.cargo/config.toml new file mode 100644 index 0000000000..6b509f5b70 --- /dev/null +++ b/sdk/field-repr/tests/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/sdk/note-codec/macros/src/expand.rs b/sdk/note-codec/macros/src/expand.rs index 3078e45f5a..eaa5e79c4b 100644 --- a/sdk/note-codec/macros/src/expand.rs +++ b/sdk/note-codec/macros/src/expand.rs @@ -10,7 +10,7 @@ use syn::{ItemImpl, LitStr, Type, visit_mut::VisitMut}; use crate::registry::{register_codec, register_schema, registered_codecs}; /// The component world embedded in generated export glue. -const NOTE_CODEC_WIT: &str = include_str!("../../wit/note-codec.wit"); +const NOTE_CODEC_WIT: &str = include_str!("../wit/note-codec.wit"); /// Expands a project-relative type generation request. pub(crate) fn from_project(input: &LitStr) -> syn::Result { diff --git a/sdk/note-codec/macros/wit/note-codec.wit b/sdk/note-codec/macros/wit/note-codec.wit new file mode 100644 index 0000000000..3cde3d4442 --- /dev/null +++ b/sdk/note-codec/macros/wit/note-codec.wit @@ -0,0 +1,27 @@ +package miden:note-codec@1.0.0; + +/// Parses, displays, and validates custom note storage types. +interface codec { + /// A canonical Miden field element at the component boundary. + /// + /// This is a `u64`, not the core-types `felt` record, because that record models the + /// compiler's guest felt value and does not expose its canonical integer representation. + type felt = u64; + + /// Returns the fully-qualified WIT names handled by this component. + supported-types: func() -> list; + + /// Parses text into a type's structural felt representation. + parse: func(type-fqn: string, value: string) -> result, string>; + + /// Displays a type's structural felt representation. + display: func(type-fqn: string, value: list) -> result; + + /// Validates a type's structural felt representation. + validate: func(type-fqn: string, value: list) -> result<_, string>; +} + +world note-codec { + export codec; +} + diff --git a/sdk/sdk/.cargo/config.toml b/sdk/sdk/.cargo/config.toml new file mode 100644 index 0000000000..6b509f5b70 --- /dev/null +++ b/sdk/sdk/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/sdk/stdlib-sys/.cargo/config.toml b/sdk/stdlib-sys/.cargo/config.toml new file mode 100644 index 0000000000..6b509f5b70 --- /dev/null +++ b/sdk/stdlib-sys/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/sdk/wasm-metadata/.cargo/config.toml b/sdk/wasm-metadata/.cargo/config.toml new file mode 100644 index 0000000000..6b509f5b70 --- /dev/null +++ b/sdk/wasm-metadata/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs index 4af7f99544..bee6215ea4 100644 --- a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs +++ b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs @@ -392,3 +392,15 @@ fn note_packages_carry_resolvable_storage_schema_metadata() { "#]], ); } + +/// The pinned note-codec world WIT is duplicated into each package that embeds it, because +/// `include_str!` paths must not escape a published package's root. This test locks the copies +/// together; update all three files when the world changes. +#[test] +fn note_codec_wit_copies_are_identical() { + let canonical = include_str!("../../../../../sdk/note-codec/wit/note-codec.wit"); + let macros_copy = include_str!("../../../../../sdk/note-codec/macros/wit/note-codec.wit"); + let compiler_copy = include_str!("../../../../../midenc-compile/wit/note-codec.wit"); + assert_eq!(canonical, macros_copy, "sdk/note-codec/macros/wit/note-codec.wit drifted"); + assert_eq!(canonical, compiler_copy, "midenc-compile/wit/note-codec.wit drifted"); +} From a08eb0745b641778932748ea64ca84fd151139a4 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 12 Aug 2026 15:19:22 +0300 Subject: [PATCH 06/43] test: restore formatting and refresh constructor expectations after rebase The rebase onto the note-constructor and typed-script-args work left two loose ends: a rustfmt reflow of the merged import block in the note macro was lost while relocating fixes into their commits, and the new upstream constructor test pins cycle counts that shift by the schema uniqueness guard's fixed advice-map cost. Restore the formatting and refresh the constructor test's cycle expectations; the +10-cycle delta matches the guard cost already reflected in the other mockchain expectations. --- examples/dex-note-codec/Cargo.lock | 1293 +++++++++++++++-- examples/dex-note/Cargo.lock | 246 +++- examples/dex-note/Cargo.toml | 3 + examples/dex-note/build.rs | 3 + examples/dex-note/miden-project.toml | 3 - followup-issues-draft.md | 72 + frontend/wasm/src/module/module_env.rs | 5 +- i1307-implementation-plan.md | 256 ++++ midenc-compile/src/cargo.rs | 1 - sdk/note-schema/src/codec_component.rs | 2 +- .../src/mockchain/notes/schema.rs | 17 +- .../examples/note_schema_metadata.rs | 4 - 12 files changed, 1714 insertions(+), 191 deletions(-) create mode 100644 examples/dex-note/build.rs create mode 100644 followup-issues-draft.md create mode 100644 i1307-implementation-plan.md diff --git a/examples/dex-note-codec/Cargo.lock b/examples/dex-note-codec/Cargo.lock index 6d184e8001..f411bed0fd 100644 --- a/examples/dex-note-codec/Cargo.lock +++ b/examples/dex-note-codec/Cargo.lock @@ -8,7 +8,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common", + "crypto-common 0.2.2", "inout", ] @@ -22,10 +22,23 @@ dependencies = [ ] [[package]] -name = "allocator-api2" -version = "0.2.21" +name = "alloy-rlp" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +checksum = "24671b1f62edcf0f9b62994c7bf72cd621a04a4b99f5020ece1a647b40e2f103" +dependencies = [ + "arrayvec", + "bytes", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] [[package]] name = "anstream" @@ -83,6 +96,269 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint 0.4.8", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint 0.4.8", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint 0.4.8", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" +dependencies = [ + "ark-ff-asm 0.6.0", + "ark-ff-macros 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "educe", + "num-bigint 0.4.8", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-asm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint 0.4.8", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint 0.4.8", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint 0.4.8", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" +dependencies = [ + "num-bigint 0.4.8", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint 0.4.8", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint 0.4.8", +] + +[[package]] +name = "ark-serialize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.6.0", + "digest 0.10.7", + "num-bigint 0.4.8", + "serde_with", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "ark-std" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -95,6 +371,17 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -107,6 +394,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.8.3" @@ -131,6 +424,18 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.8.5" @@ -154,12 +459,39 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "cc" version = "1.4.0" @@ -202,6 +534,18 @@ dependencies = [ "poly1305", ] +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "cipher" version = "0.5.2" @@ -209,7 +553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ "block-buffer", - "crypto-common", + "crypto-common 0.2.2", "inout", ] @@ -231,12 +575,39 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpubits" version = "0.1.1" @@ -304,6 +675,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.2" @@ -333,7 +714,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest", + "digest 0.11.3", "fiat-crypto", "rustc_version 0.4.1", "subtle", @@ -392,6 +773,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -420,6 +821,24 @@ dependencies = [ "miden-note-codec", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "crypto-common 0.1.7", +] + [[package]] name = "digest" version = "0.11.3" @@ -428,7 +847,7 @@ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer", "const-oid", - "crypto-common", + "crypto-common 0.2.2", "ctutils", ] @@ -438,6 +857,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.17.0" @@ -445,7 +870,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ "der", - "digest", + "digest 0.11.3", "elliptic-curve", "rfc6979", "signature", @@ -478,6 +903,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "either" version = "1.17.0" @@ -492,8 +929,8 @@ checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", - "crypto-common", - "digest", + "crypto-common 0.2.2", + "digest 0.11.3", "ff", "group", "hkdf", @@ -505,6 +942,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "env_filter" version = "2.0.0" @@ -545,6 +1002,28 @@ dependencies = [ "typeid", ] +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + [[package]] name = "ff" version = "0.14.0" @@ -567,6 +1046,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.7", + "rustc-hex", + "static_assertions", +] + [[package]] name = "flume" version = "0.12.0" @@ -591,6 +1082,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.33" @@ -649,6 +1146,7 @@ dependencies = [ "futures-sink", "futures-task", "pin-project-lite", + "slab", ] [[package]] @@ -666,6 +1164,27 @@ dependencies = [ "windows-result", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -709,14 +1228,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", ] @@ -726,6 +1249,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.13.0" @@ -741,7 +1270,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest", + "digest 0.11.3", ] [[package]] @@ -755,18 +1284,73 @@ dependencies = [ "zeroize", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "indenter" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -774,7 +1358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -796,9 +1380,18 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.14.0" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -827,10 +1420,12 @@ dependencies = [ "defmt", "jiff-core", "jiff-static", + "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", + "windows-link", ] [[package]] @@ -854,6 +1449,21 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jobserver" version = "0.1.35" @@ -871,6 +1481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -898,6 +1509,21 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy_static" version = "1.5.0" @@ -967,10 +1593,11 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden-ace-codegen" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1831e4b40ba86d848581824b7089da20fb039dd8161f44e02b1ae2e3da6f30" +checksum = "93a217e3f1fec32105bca7dc421d85ab39199b78d2e43081fc0fa92411de9a84" dependencies = [ + "miden-constraint-compiler", "miden-core", "miden-crypto", "thiserror", @@ -978,14 +1605,15 @@ dependencies = [ [[package]] name = "miden-air" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1cb4a9efe57aa970a7506b07729abd32702bdc6482a2a0364ecb045866c1d5b" +checksum = "90b7b3a23756f2bfdba2b37c8d1551b3b531fce0de42e3a9a7f840bb69018106" dependencies = [ "miden-ace-codegen", "miden-core", "miden-crypto", "miden-utils-indexing", + "p3-field", "proptest", "thiserror", "tracing", @@ -993,9 +1621,9 @@ dependencies = [ [[package]] name = "miden-assembly" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31a8dbf11a81ae5f563ef5140a33bff2ec413ff0d34ca48404a8d11a2a43280" +checksum = "727de4350d9ba263be17d9f93dc4b8d0deebabab29c3bed34f32c4af7b1cf20c" dependencies = [ "log", "miden-assembly-syntax", @@ -1010,9 +1638,9 @@ dependencies = [ [[package]] name = "miden-assembly-syntax" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97be191cb4063a22312d88c5f6debf020d1f7b7d4eb2b1563172c4fda1f0b5de" +checksum = "3874c53f40656a56f74afe8e957d75667218808894ab8f5dbd91e58a8a0c3393" dependencies = [ "log", "miden-assembly-syntax-cst", @@ -1031,9 +1659,9 @@ dependencies = [ [[package]] name = "miden-assembly-syntax-cst" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb812985ff68aa2d17ea8a87b48e769c5f9f69974ee723323c4739ef00a54f0" +checksum = "151b657a50aee75a0591dbd66d57774e2f9ec1f00fa17d773820e736bcca0cb6" dependencies = [ "miden-debug-types", "miden-rowan", @@ -1041,11 +1669,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "miden-constraint-compiler" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f126188b824423edbbc5eebf9b61c872905c21e3f1e9e9b69d1afbba12288" +dependencies = [ + "miden-core", + "miden-crypto", +] + [[package]] name = "miden-core" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e26dcf78743d4abbca1bbc6799712e4ea674bad8e86dc4fd8732bf237450b1" +checksum = "55df0c9b5fddcfc2c03c6e476bec0523328fb85090e716d1ab9a4db1d2267c67" dependencies = [ "derive_more", "log", @@ -1061,28 +1699,40 @@ dependencies = [ [[package]] name = "miden-core-lib" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcb1063ac5272a02037a5fb36355b267024aa09de81bb46f54bb68ced0eebb1" +checksum = "f1f5b4dca8d29c99859e3470ec1fadfa89506e6b349ade7fa475574e3104db85" dependencies = [ "env_logger", "fs-err", "miden-assembly", "miden-assembly-syntax", "miden-core", + "miden-core-lib-codegen", "miden-crypto", "miden-mast-package", "miden-package-registry", + "miden-precompiles", "miden-processor", "miden-utils-sync", "thiserror", ] +[[package]] +name = "miden-core-lib-codegen" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d926e5e8a288a2a55c42541059d8e52c43c10d373643b309add169a3a0eb914" +dependencies = [ + "miden-core", + "miden-precompiles", +] + [[package]] name = "miden-crypto" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10eaaf3e927c3c6a720a3ed782123be25e38d34c5a7be2ca1bfbbef10a940396" +checksum = "de00899e7045ee3bea78d4c4c690d8e2c14fff1f3a1ede1fb2922ed699fdda14" dependencies = [ "blake3", "cc", @@ -1122,9 +1772,9 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3958eeade8b938895d3f170fd82f43e9d4141a799443b796e6496ecd9bc107d0" +checksum = "a5b5a5c8b0fd14982e60ebd010f452b06f693b9efa116054487aff1f2657d2f4" dependencies = [ "quote", "syn 2.0.119", @@ -1132,9 +1782,9 @@ dependencies = [ [[package]] name = "miden-debug-types" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b9ba4ccb10ca8f719dc3c96a418aa2ade8251f108fba6034facb3e3eabae70" +checksum = "793b37eaafbac33a4c8bf8f57d4c1d0ce8b8a7d25698ddfd2e1a2a6552e94f6c" dependencies = [ "memchr", "miden-crypto", @@ -1152,9 +1802,9 @@ dependencies = [ [[package]] name = "miden-field" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25fbce3dc2399704c7094d1d4e500ffa1fe6dabb11bf9e512fc3764498b2e1" +checksum = "41be9f7f5c0ef020bcedf526afe54b3a97244e207a5385e4453ca47799fe2afe" dependencies = [ "miden-serde-utils", "num-bigint 0.5.1", @@ -1197,9 +1847,9 @@ dependencies = [ [[package]] name = "miden-lifted-air" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a183cb8209eb2e80bafb33fec0e841b290877d83267dfa888607ffb671ab684" +checksum = "b0ad52f76750f8b9dc8a7c4bed32644628198fab48daf31167c2f750c939378a" dependencies = [ "p3-air", "p3-challenger", @@ -1211,9 +1861,9 @@ dependencies = [ [[package]] name = "miden-lifted-stark" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a24dd2d8fd4978c56dd7c3d3c76c1a6fb17fef1edfe470ed4e517e38a556bdb" +checksum = "212a017744ef2aaca36f89bad2c880949311ec778586b8405a5e47f9e6ca59de" dependencies = [ "miden-lifted-air", "miden-stark-transcript", @@ -1234,11 +1884,11 @@ dependencies = [ [[package]] name = "miden-mast-package" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aee79180c3d317ab6239d7c2c488760fe3590b9f639e549d9d13f3e0f314a381" +checksum = "26062f2e4cb18e7fa9244f8b18fe2adea2bfe3f016431f75f80b5b98249c34f6" dependencies = [ - "hashbrown", + "hashbrown 0.17.1", "log", "miden-assembly-syntax", "miden-core", @@ -1334,9 +1984,9 @@ dependencies = [ [[package]] name = "miden-package-registry" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bf8ca360321a414771807cd3b95c732e99e30bf7187a9a80a0e63db36bbcc2" +checksum = "8653a8792d81ec1807815e5735de98d9d928f89b7bb5280ffadb848e87eb3ed3" dependencies = [ "miden-assembly-syntax", "miden-core", @@ -1348,17 +1998,51 @@ dependencies = [ "thiserror", ] +[[package]] +name = "miden-precompiles" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d01b98147f622a733c9c206db9e78c7e1a353b2309e34e569e2757a435a77e0" +dependencies = [ + "miden-core", + "miden-crypto", +] + +[[package]] +name = "miden-precompiles-prover" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed0d94d331324d6b378f19ee925a6be6e17731e108d43f034c23653048a8ca0" +dependencies = [ + "miden-ace-codegen", + "miden-air", + "miden-core", + "miden-crypto", + "miden-lifted-air", + "miden-lifted-stark", + "miden-precompiles", + "miden-serde-utils", + "ruint", + "serde", + "serde-wincode", + "thiserror", + "tracing", + "wincode", +] + [[package]] name = "miden-processor" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6853b729e245f8c310bfba8e47cdbd303b1d11763499d3b7efd90bc821239aa9" +checksum = "57b3f6dbcea246715c7c528bbc2fbee46464493c11ac417c12c9562a229b136f" dependencies = [ - "itertools 0.14.0", + "hashbrown 0.17.1", + "itertools 0.15.0", "miden-air", "miden-core", "miden-debug-types", "miden-mast-package", + "miden-precompiles", "miden-utils-diagnostics", "miden-utils-indexing", "paste", @@ -1369,9 +2053,9 @@ dependencies = [ [[package]] name = "miden-project" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d12f4281e563c01305d989461574ccab41b749ac5d6e987ede1e9941ed18f5e" +checksum = "e02ac82735cd86ea17fcf8614496ce7e462f3e80dbc1113bf821e8197dcda49d" dependencies = [ "miden-assembly-syntax", "miden-core", @@ -1386,9 +2070,9 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.16.0-alpha.4" +version = "0.16.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f185d7a1e0c6c05ae760281956de3f6057e6d7889daeade4c71c158d6a252748" +checksum = "e275feebbe9c2458c5877c5f2a03b8e2152831ce956376f015fc4c50acfffb50" dependencies = [ "bech32", "fs-err", @@ -1402,14 +2086,30 @@ dependencies = [ "miden-mast-package", "miden-package-registry", "miden-processor", + "miden-protocol-build-utils", "miden-utils-sync", "miden-verifier", "rand 0.10.2", "regex", - "semver 1.0.28", - "serde", - "thiserror", - "toml", + "semver 1.0.28", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "miden-protocol-build-utils" +version = "0.16.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1145d0f5cb0efde2484af06587225b2a222fbf4811c4606d0d7f1bd3ff40dc" +dependencies = [ + "fs-err", + "miden-assembly", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "miden-project", + "regex", "walkdir", ] @@ -1419,25 +2119,26 @@ version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c13695bf99aabaa21d6572b807c66bb26251aa3d9b75e828b3c99b97a3b1ce7e" dependencies = [ - "hashbrown", + "hashbrown 0.17.1", "rustc-hash", ] [[package]] name = "miden-serde-utils" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ed62fe47d4e6255502618761a8f27d9c33f22759fbace97a6fa36a03252659" +checksum = "aa5e67d63441ddaec820a7cf0cabefa8312c15db7a1f2b108ce1b3f589eeeb23" dependencies = [ "p3-field", "p3-goldilocks", + "wincode", ] [[package]] name = "miden-stark-transcript" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc79432ed9d7cf1264217ca7af4c7c1768bded83bad2429ee1ab2fb2d394d774" +checksum = "a5ebdd546c7583ba045b20afdde9986e42e7070d1f5fb8841e13f3c921110873" dependencies = [ "p3-challenger", "p3-field", @@ -1447,9 +2148,9 @@ dependencies = [ [[package]] name = "miden-stateful-hasher" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08359f6cabcc418a76ac9317d6e1ccb33800a1724e08c5b4b1ced7d8aeb26e1b" +checksum = "b47d09c4dbfa32918847a4d6426a69d3d527adbf4e01cf1decf714c95b1bf894" dependencies = [ "p3-field", "p3-symmetric", @@ -1457,9 +2158,9 @@ dependencies = [ [[package]] name = "miden-utils-core-derive" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb2baba2f71907ab82be0d064410030196f8e06f19687d7bb33970e02dd9cec" +checksum = "5764abe966b8c0e7cf377e38de4cbcbbc83a3483f111043c461b7208619bdb96" dependencies = [ "proc-macro2", "quote", @@ -1468,9 +2169,9 @@ dependencies = [ [[package]] name = "miden-utils-diagnostics" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "485627595e49b2d83d163511ddc7200194a07ee80bbeab67713ebcfeab57e2e4" +checksum = "10561ffd67ee21baca489b96fad11e11579573e1f07f1fc5fb8a111d196fea59" dependencies = [ "miden-debug-types", "miden-miette", @@ -1479,9 +2180,9 @@ dependencies = [ [[package]] name = "miden-utils-indexing" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6be5944699579d8babf57d0d6dd625cb59b7a10bdf85e5fc96ae568991df9f" +checksum = "4c18e4f69d5ebe556a72cc9da474acc53eceb4a75c1247b1f542f20f73145deb" dependencies = [ "miden-serde-utils", "proptest", @@ -1491,9 +2192,9 @@ dependencies = [ [[package]] name = "miden-utils-sync" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4980ed8c1f02727ef294ec78ec2e73c367bc53ca784f4de62c1c3e5b6cbfe3" +checksum = "0fa9c0af7dab7842819c02ef386ed1640884db5c2efa32d30da7d4739548f70e" dependencies = [ "lock_api", "loom", @@ -1503,32 +2204,35 @@ dependencies = [ [[package]] name = "miden-verifier" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afef2b344a7c0a5a90c2b6335f4ab50e74cbaa255009ec718eb6c9598b7d9486" +checksum = "900c0473191ecbe2328e3d6550571f0b1ab42d1417f43c1d98a50cafc3ae4ff1" dependencies = [ "miden-air", "miden-core", "miden-crypto", + "miden-precompiles", + "miden-precompiles-prover", + "miden-serde-utils", "serde", "serde-wincode", "thiserror", - "tracing", ] [[package]] name = "midenc-frontend-wasm-metadata" version = "0.14.0-rc.1" dependencies = [ + "miden-mast-package", "serde", "serde_json", ] [[package]] name = "midenc-hir-type" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b879cc9e04ad1b98ccd2fe53b1ed4ed4aa00d3231506a1e1703b17b31b3389" +checksum = "e03aa1e30a8eec3e08eba9a1fd17c7c4462f0d49dfbd64cb65193b68ee14cdcc" dependencies = [ "miden-formatting", "miden-serde-utils", @@ -1590,6 +2294,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" version = "0.1.46" @@ -1859,6 +2569,34 @@ dependencies = [ "serde", ] +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1894,6 +2632,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" +[[package]] +name = "pest" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" +dependencies = [ + "memchr", + "ucd-trie", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1935,6 +2683,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1961,7 +2715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" dependencies = [ "crypto-bigint", - "crypto-common", + "crypto-common 0.2.2", "ff", "rand_core 0.10.1", "subtle", @@ -1980,6 +2734,17 @@ dependencies = [ "wnaf", ] +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + [[package]] name = "priority-queue" version = "2.7.0" @@ -1987,7 +2752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" dependencies = [ "equivalent", - "indexmap", + "indexmap 2.14.0", "serde", ] @@ -2030,7 +2795,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f5df7e552bc7edd075f5783a87fbfc21d6a546e32c16985679c488c18192d83" dependencies = [ - "indexmap", + "indexmap 2.14.0", "log", "priority-queue", "rustc-hash", @@ -2059,6 +2824,23 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.5" @@ -2080,6 +2862,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -2100,6 +2892,15 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "rand_core" version = "0.9.5" @@ -2153,6 +2954,26 @@ dependencies = [ "bitflags 2.13.1", ] +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "regex" version = "1.13.1" @@ -2192,12 +3013,63 @@ dependencies = [ "hmac", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "ruint" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "ark-ff 0.6.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint 0.4.8", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.7", + "rand 0.9.5", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rustc-hash" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "rustc_version" version = "0.2.3" @@ -2207,6 +3079,15 @@ dependencies = [ "semver 0.9.0", ] +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -2231,6 +3112,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2263,7 +3168,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser", + "semver-parser 0.7.0", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser 0.10.3", ] [[package]] @@ -2282,6 +3196,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.229" @@ -2368,6 +3291,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "time", +] + [[package]] name = "serdect" version = "0.4.3" @@ -2386,7 +3329,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.11.3", ] [[package]] @@ -2395,7 +3338,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ - "digest", + "digest 0.11.3", "keccak", "sponge-cursor", ] @@ -2421,10 +3364,16 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "digest", + "digest 0.11.3", "rand_core 0.10.1", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.2" @@ -2474,6 +3423,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strip-ansi-escapes" version = "0.2.1" @@ -2522,6 +3477,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-triple" version = "1.0.1" @@ -2577,6 +3538,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -2586,13 +3577,28 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "toml" version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned", "toml_datetime", @@ -2616,7 +3622,7 @@ version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime", "toml_parser", "winnow", @@ -2726,6 +3732,24 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unarray" version = "0.1.4" @@ -2768,7 +3792,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common", + "crypto-common 0.2.2", "ctutils", ] @@ -2793,6 +3817,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "vte" version = "0.14.1" @@ -2812,6 +3842,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -2883,7 +3919,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -2895,8 +3931,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" dependencies = [ "bitflags 2.13.1", - "hashbrown", - "indexmap", + "hashbrown 0.17.1", + "indexmap 2.14.0", "semver 1.0.28", ] @@ -2921,6 +3957,41 @@ dependencies = [ "thiserror", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -2936,6 +4007,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2982,7 +4062,7 @@ checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.14.0", "prettyplease", "syn 2.0.119", "wasm-metadata", @@ -3013,7 +4093,7 @@ checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" dependencies = [ "anyhow", "bitflags 2.13.1", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -3031,9 +4111,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" dependencies = [ "anyhow", - "hashbrown", + "hashbrown 0.17.1", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver 1.0.28", "serde", @@ -3054,6 +4134,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x25519-dalek" version = "3.0.0" @@ -3089,6 +4178,20 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "zmij" diff --git a/examples/dex-note/Cargo.lock b/examples/dex-note/Cargo.lock index 56f56f169a..11c5a0bead 100644 --- a/examples/dex-note/Cargo.lock +++ b/examples/dex-note/Cargo.lock @@ -21,12 +21,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "anstream" version = "1.0.0" @@ -419,6 +413,7 @@ version = "0.1.0" dependencies = [ "miden", "miden-field-repr", + "miden-sdk-build-script-support", ] [[package]] @@ -716,8 +711,6 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", ] @@ -795,15 +788,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.15.0" @@ -977,15 +961,17 @@ dependencies = [ "miden-field-repr", "miden-sdk-alloc", "miden-stdlib-sys", + "miden-tx-script-args", "wit-bindgen", ] [[package]] name = "miden-ace-codegen" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1831e4b40ba86d848581824b7089da20fb039dd8161f44e02b1ae2e3da6f30" +checksum = "93a217e3f1fec32105bca7dc421d85ab39199b78d2e43081fc0fa92411de9a84" dependencies = [ + "miden-constraint-compiler", "miden-core", "miden-crypto", "thiserror", @@ -993,23 +979,24 @@ dependencies = [ [[package]] name = "miden-air" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1cb4a9efe57aa970a7506b07729abd32702bdc6482a2a0364ecb045866c1d5b" +checksum = "90b7b3a23756f2bfdba2b37c8d1551b3b531fce0de42e3a9a7f840bb69018106" dependencies = [ "miden-ace-codegen", "miden-core", "miden-crypto", "miden-utils-indexing", + "p3-field", "thiserror", "tracing", ] [[package]] name = "miden-assembly" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31a8dbf11a81ae5f563ef5140a33bff2ec413ff0d34ca48404a8d11a2a43280" +checksum = "727de4350d9ba263be17d9f93dc4b8d0deebabab29c3bed34f32c4af7b1cf20c" dependencies = [ "log", "miden-assembly-syntax", @@ -1024,9 +1011,9 @@ dependencies = [ [[package]] name = "miden-assembly-syntax" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97be191cb4063a22312d88c5f6debf020d1f7b7d4eb2b1563172c4fda1f0b5de" +checksum = "3874c53f40656a56f74afe8e957d75667218808894ab8f5dbd91e58a8a0c3393" dependencies = [ "log", "miden-assembly-syntax-cst", @@ -1045,9 +1032,9 @@ dependencies = [ [[package]] name = "miden-assembly-syntax-cst" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb812985ff68aa2d17ea8a87b48e769c5f9f69974ee723323c4739ef00a54f0" +checksum = "151b657a50aee75a0591dbd66d57774e2f9ec1f00fa17d773820e736bcca0cb6" dependencies = [ "miden-debug-types", "miden-rowan", @@ -1092,11 +1079,21 @@ dependencies = [ "miden-stdlib-sys", ] +[[package]] +name = "miden-constraint-compiler" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f126188b824423edbbc5eebf9b61c872905c21e3f1e9e9b69d1afbba12288" +dependencies = [ + "miden-core", + "miden-crypto", +] + [[package]] name = "miden-core" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e26dcf78743d4abbca1bbc6799712e4ea674bad8e86dc4fd8732bf237450b1" +checksum = "55df0c9b5fddcfc2c03c6e476bec0523328fb85090e716d1ab9a4db1d2267c67" dependencies = [ "derive_more", "log", @@ -1112,28 +1109,40 @@ dependencies = [ [[package]] name = "miden-core-lib" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcb1063ac5272a02037a5fb36355b267024aa09de81bb46f54bb68ced0eebb1" +checksum = "f1f5b4dca8d29c99859e3470ec1fadfa89506e6b349ade7fa475574e3104db85" dependencies = [ "env_logger", "fs-err", "miden-assembly", "miden-assembly-syntax", "miden-core", + "miden-core-lib-codegen", "miden-crypto", "miden-mast-package", "miden-package-registry", + "miden-precompiles", "miden-processor", "miden-utils-sync", "thiserror", ] +[[package]] +name = "miden-core-lib-codegen" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d926e5e8a288a2a55c42541059d8e52c43c10d373643b309add169a3a0eb914" +dependencies = [ + "miden-core", + "miden-precompiles", +] + [[package]] name = "miden-crypto" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10eaaf3e927c3c6a720a3ed782123be25e38d34c5a7be2ca1bfbbef10a940396" +checksum = "de00899e7045ee3bea78d4c4c690d8e2c14fff1f3a1ede1fb2922ed699fdda14" dependencies = [ "blake3", "cc", @@ -1172,9 +1181,9 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3958eeade8b938895d3f170fd82f43e9d4141a799443b796e6496ecd9bc107d0" +checksum = "a5b5a5c8b0fd14982e60ebd010f452b06f693b9efa116054487aff1f2657d2f4" dependencies = [ "quote", "syn 2.0.119", @@ -1182,9 +1191,9 @@ dependencies = [ [[package]] name = "miden-debug-types" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b9ba4ccb10ca8f719dc3c96a418aa2ade8251f108fba6034facb3e3eabae70" +checksum = "793b37eaafbac33a4c8bf8f57d4c1d0ce8b8a7d25698ddfd2e1a2a6552e94f6c" dependencies = [ "memchr", "miden-crypto", @@ -1202,9 +1211,9 @@ dependencies = [ [[package]] name = "miden-field" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25fbce3dc2399704c7094d1d4e500ffa1fe6dabb11bf9e512fc3764498b2e1" +checksum = "41be9f7f5c0ef020bcedf526afe54b3a97244e207a5385e4453ca47799fe2afe" dependencies = [ "miden-serde-utils", "num-bigint 0.5.1", @@ -1247,9 +1256,9 @@ dependencies = [ [[package]] name = "miden-lifted-air" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a183cb8209eb2e80bafb33fec0e841b290877d83267dfa888607ffb671ab684" +checksum = "b0ad52f76750f8b9dc8a7c4bed32644628198fab48daf31167c2f750c939378a" dependencies = [ "p3-air", "p3-challenger", @@ -1261,9 +1270,9 @@ dependencies = [ [[package]] name = "miden-lifted-stark" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a24dd2d8fd4978c56dd7c3d3c76c1a6fb17fef1edfe470ed4e517e38a556bdb" +checksum = "212a017744ef2aaca36f89bad2c880949311ec778586b8405a5e47f9e6ca59de" dependencies = [ "miden-lifted-air", "miden-stark-transcript", @@ -1284,9 +1293,9 @@ dependencies = [ [[package]] name = "miden-mast-package" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aee79180c3d317ab6239d7c2c488760fe3590b9f639e549d9d13f3e0f314a381" +checksum = "26062f2e4cb18e7fa9244f8b18fe2adea2bfe3f016431f75f80b5b98249c34f6" dependencies = [ "hashbrown", "log", @@ -1338,9 +1347,9 @@ dependencies = [ [[package]] name = "miden-package-registry" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bf8ca360321a414771807cd3b95c732e99e30bf7187a9a80a0e63db36bbcc2" +checksum = "8653a8792d81ec1807815e5735de98d9d928f89b7bb5280ffadb848e87eb3ed3" dependencies = [ "miden-assembly-syntax", "miden-core", @@ -1352,17 +1361,50 @@ dependencies = [ "thiserror", ] +[[package]] +name = "miden-precompiles" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d01b98147f622a733c9c206db9e78c7e1a353b2309e34e569e2757a435a77e0" +dependencies = [ + "miden-core", + "miden-crypto", +] + +[[package]] +name = "miden-precompiles-prover" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed0d94d331324d6b378f19ee925a6be6e17731e108d43f034c23653048a8ca0" +dependencies = [ + "miden-air", + "miden-core", + "miden-crypto", + "miden-lifted-air", + "miden-lifted-stark", + "miden-precompiles", + "miden-serde-utils", + "ruint", + "serde", + "serde-wincode", + "thiserror", + "tracing", + "wincode", +] + [[package]] name = "miden-processor" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6853b729e245f8c310bfba8e47cdbd303b1d11763499d3b7efd90bc821239aa9" +checksum = "57b3f6dbcea246715c7c528bbc2fbee46464493c11ac417c12c9562a229b136f" dependencies = [ - "itertools 0.14.0", + "hashbrown", + "itertools", "miden-air", "miden-core", "miden-debug-types", "miden-mast-package", + "miden-precompiles", "miden-utils-diagnostics", "miden-utils-indexing", "paste", @@ -1373,9 +1415,9 @@ dependencies = [ [[package]] name = "miden-project" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d12f4281e563c01305d989461574ccab41b749ac5d6e987ede1e9941ed18f5e" +checksum = "e02ac82735cd86ea17fcf8614496ce7e462f3e80dbc1113bf821e8197dcda49d" dependencies = [ "miden-assembly-syntax", "miden-core", @@ -1390,9 +1432,9 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.16.0-alpha.4" +version = "0.16.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f185d7a1e0c6c05ae760281956de3f6057e6d7889daeade4c71c158d6a252748" +checksum = "e275feebbe9c2458c5877c5f2a03b8e2152831ce956376f015fc4c50acfffb50" dependencies = [ "bech32", "fs-err", @@ -1406,12 +1448,28 @@ dependencies = [ "miden-mast-package", "miden-package-registry", "miden-processor", + "miden-protocol-build-utils", "miden-utils-sync", "miden-verifier", "rand 0.10.2", "regex", "semver 1.0.28", "thiserror", +] + +[[package]] +name = "miden-protocol-build-utils" +version = "0.16.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1145d0f5cb0efde2484af06587225b2a222fbf4811c4606d0d7f1bd3ff40dc" +dependencies = [ + "fs-err", + "miden-assembly", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "miden-project", + "regex", "walkdir", ] @@ -1429,21 +1487,26 @@ dependencies = [ name = "miden-sdk-alloc" version = "0.14.0-rc.1" +[[package]] +name = "miden-sdk-build-script-support" +version = "0.14.0-rc.1" + [[package]] name = "miden-serde-utils" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ed62fe47d4e6255502618761a8f27d9c33f22759fbace97a6fa36a03252659" +checksum = "aa5e67d63441ddaec820a7cf0cabefa8312c15db7a1f2b108ce1b3f589eeeb23" dependencies = [ "p3-field", "p3-goldilocks", + "wincode", ] [[package]] name = "miden-stark-transcript" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc79432ed9d7cf1264217ca7af4c7c1768bded83bad2429ee1ab2fb2d394d774" +checksum = "a5ebdd546c7583ba045b20afdde9986e42e7070d1f5fb8841e13f3c921110873" dependencies = [ "p3-challenger", "p3-field", @@ -1453,9 +1516,9 @@ dependencies = [ [[package]] name = "miden-stateful-hasher" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08359f6cabcc418a76ac9317d6e1ccb33800a1724e08c5b4b1ced7d8aeb26e1b" +checksum = "b47d09c4dbfa32918847a4d6426a69d3d527adbf4e01cf1decf714c95b1bf894" dependencies = [ "p3-field", "p3-symmetric", @@ -1468,11 +1531,20 @@ dependencies = [ "miden-field", ] +[[package]] +name = "miden-tx-script-args" +version = "0.14.0-rc.1" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-stdlib-sys", +] + [[package]] name = "miden-utils-core-derive" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb2baba2f71907ab82be0d064410030196f8e06f19687d7bb33970e02dd9cec" +checksum = "5764abe966b8c0e7cf377e38de4cbcbbc83a3483f111043c461b7208619bdb96" dependencies = [ "proc-macro2", "quote", @@ -1481,9 +1553,9 @@ dependencies = [ [[package]] name = "miden-utils-diagnostics" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "485627595e49b2d83d163511ddc7200194a07ee80bbeab67713ebcfeab57e2e4" +checksum = "10561ffd67ee21baca489b96fad11e11579573e1f07f1fc5fb8a111d196fea59" dependencies = [ "miden-debug-types", "miden-miette", @@ -1492,9 +1564,9 @@ dependencies = [ [[package]] name = "miden-utils-indexing" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6be5944699579d8babf57d0d6dd625cb59b7a10bdf85e5fc96ae568991df9f" +checksum = "4c18e4f69d5ebe556a72cc9da474acc53eceb4a75c1247b1f542f20f73145deb" dependencies = [ "miden-serde-utils", "proptest", @@ -1504,9 +1576,9 @@ dependencies = [ [[package]] name = "miden-utils-sync" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4980ed8c1f02727ef294ec78ec2e73c367bc53ca784f4de62c1c3e5b6cbfe3" +checksum = "0fa9c0af7dab7842819c02ef386ed1640884db5c2efa32d30da7d4739548f70e" dependencies = [ "lock_api", "loom", @@ -1516,32 +1588,35 @@ dependencies = [ [[package]] name = "miden-verifier" -version = "0.25.8" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afef2b344a7c0a5a90c2b6335f4ab50e74cbaa255009ec718eb6c9598b7d9486" +checksum = "900c0473191ecbe2328e3d6550571f0b1ab42d1417f43c1d98a50cafc3ae4ff1" dependencies = [ "miden-air", "miden-core", "miden-crypto", + "miden-precompiles", + "miden-precompiles-prover", + "miden-serde-utils", "serde", "serde-wincode", "thiserror", - "tracing", ] [[package]] name = "midenc-frontend-wasm-metadata" version = "0.14.0-rc.1" dependencies = [ + "miden-mast-package", "serde", "serde_json", ] [[package]] name = "midenc-hir-type" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b879cc9e04ad1b98ccd2fe53b1ed4ed4aa00d3231506a1e1703b17b31b3389" +checksum = "e03aa1e30a8eec3e08eba9a1fd17c7c4462f0d49dfbd64cb65193b68ee14cdcc" dependencies = [ "miden-formatting", "miden-serde-utils", @@ -1707,7 +1782,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" dependencies = [ - "itertools 0.15.0", + "itertools", "p3-field", "p3-matrix", "p3-maybe-rayon", @@ -1722,7 +1797,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" dependencies = [ - "itertools 0.15.0", + "itertools", "num-bigint 0.5.1", "p3-maybe-rayon", "p3-util", @@ -1770,7 +1845,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" dependencies = [ - "itertools 0.15.0", + "itertools", "p3-field", "p3-maybe-rayon", "p3-util", @@ -1804,7 +1879,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" dependencies = [ - "itertools 0.15.0", + "itertools", "num-bigint 0.5.1", "p3-dft", "p3-field", @@ -1853,7 +1928,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" dependencies = [ - "itertools 0.15.0", + "itertools", "p3-field", "p3-util", "serde", @@ -2192,6 +2267,21 @@ dependencies = [ "hmac", ] +[[package]] +name = "ruint" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970" +dependencies = [ + "ruint-macro", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rustc-hash" version = "2.1.3" diff --git a/examples/dex-note/Cargo.toml b/examples/dex-note/Cargo.toml index 96d1027c78..e3f9cc6ab3 100644 --- a/examples/dex-note/Cargo.toml +++ b/examples/dex-note/Cargo.toml @@ -12,6 +12,9 @@ crate-type = ["cdylib"] miden = { path = "../../sdk/sdk" } miden-field-repr = { path = "../../sdk/field-repr/repr" } +[build-dependencies] +miden-sdk-build-script-support = { path = "../../sdk/build-script-support" } + [profile.release] trim-paths = ["diagnostics", "object"] diff --git a/examples/dex-note/build.rs b/examples/dex-note/build.rs new file mode 100644 index 0000000000..1db40a34c2 --- /dev/null +++ b/examples/dex-note/build.rs @@ -0,0 +1,3 @@ +fn main() { + miden_sdk_build_script_support::prepare_package_cache(); +} diff --git a/examples/dex-note/miden-project.toml b/examples/dex-note/miden-project.toml index 08fd3e0cd0..b0e899ce0e 100644 --- a/examples/dex-note/miden-project.toml +++ b/examples/dex-note/miden-project.toml @@ -12,9 +12,6 @@ miden-core = "*" miden-protocol = "*" basic-wallet = { path = "../basic-wallet" } -[package.metadata.miden.dependencies] -basic-wallet = { wit = "../basic-wallet/target/generated-wit/" } - # MetadataSet represents each named metadata entry as a table. [package.metadata.note-codec-crate] path = "../dex-note-codec" diff --git a/followup-issues-draft.md b/followup-issues-draft.md new file mode 100644 index 0000000000..9310866589 --- /dev/null +++ b/followup-issues-draft.md @@ -0,0 +1,72 @@ +# i1307 follow-up issues (draft — not filed) + +Source: discussion #1294 design, `review_claude.md` findings deferred from the 2026-08-07 fix +wave, and PoC restrictions. Each section is one proposed issue. + +## 1. Schema/codec sections and package identity (digest coverage, envelope, attestation) + +Custom `.masp` sections are digest-exempt by construction in `miden-mast-package`, so two +packages with identical identity can carry different `note_storage_schema` layouts and different +executable `note_codec` bytes (review finding 2d). Decide how these sections participate in +package identity: upstream digest coverage (a real `SectionId` + content-digest inclusion), or an +attested hash carried inside digest-covered data. In the same change, give the sections a +versioned envelope (storage-ABI version + owning package identity) — weigh against the #1294 +convention that the schema section is plain inspectable WIT text. Belongs with the #1290 +multi-component package redesign conversation. Duplicate-section rejection and exact-one readers +already landed on the branch. + +## 2. Codec build as a first-class pipeline phase + +The nested codec build now runs inside `post_process_package` (gated, session-threaded), but the +cleaner shape is a pipeline phase that produces a validated artifact before assembly, keeping +post-processing deterministic and side-effect-free (review finding 3, oracle suggestion). + +## 3. Sealed storage-ABI trait: one source for schema, encode, and decode + +A custom type with a manual (non-derived) `FromFeltRepr` impl can decode fields in a different +order than the emitted schema declares — a silent on-chain field swap (review finding 6). Derive +the schema node and the felt encode/decode from one source (sealed trait or equivalent) so the +"schema cannot drift from the code" guarantee covers manual impls too. Interim state on the +branch: expansion-time WIT resolution + the derive-based path; the manual-impl hole is +documented. + +## 4. Artifact index for note-project discovery + codec provenance + +`from_project!` discovery now prefers the compiler-staged path via env var, but profile +discovery still selects by newest mtime without package identity, and consumer bindings track +only the chosen file (review finding 4 residue). Have the build write a small artifact index +(package id/version/target/profile) that `from_project!` resolves through and tracks. Decide +whether codec crate sources/manifest/lockfile join the package-cache fingerprint legs (relates +to the i1302 fingerprint design). + +## 5. String-builder support for nested constructor payloads + +The builder rejects `option` and variant-with-record-payload leaves with a clear error +(landed); actually supporting them needs a constructor syntax or structured input (review +finding 8, deferred half). + +## 6. Migrate the remaining hand-encoded mockchain storage sites + +`support/helpers.rs`'s shared p2id path uses the schema builder; ~19 sites still hand-encode +felts (`to_core_felts`, p2ide 4-felt layout, swapp 13-felt `to_storage_felts`, FPI fixtures). +Migrate them to schema-driven construction; delete `to_core_felts` when the last user goes. + +## 7. `list` (Vec) support in note storage schemas + +`Vec` fields are rejected today (breaking change, documented in MIGRATION). Design the `list` +schema mapping (felt-repr already defines the len-prefixed layout) end to end: emitter, reader +layout interpreter (variable width), builder/decode UX, bindings codegen. + +## 8. Schema polish: subset pruning and JS-side validation + +- Embed only the transitively referenced `core-types` subset instead of the whole interface + (#1294 allows both; whole-interface embed was the PoC call). +- Validate the schema document and codec world against JS tooling (jco) — the wit-parser + header-form + braced-package layout is unverified there. + +## 9. Uniqueness-guard cycle cost (optional micro-optimization) + +The `#[note]` schema uniqueness guard is an exported `u8` static and costs +10 cycles per note +execution (+34 bytes MAST) via rodata/advice-map layout shift. Try a zero-sized guard static +(same `export_name` collision semantics, no rodata byte); if it works, apply to the frontend +metadata guard too. diff --git a/frontend/wasm/src/module/module_env.rs b/frontend/wasm/src/module/module_env.rs index 980b4377dc..561b0cf2eb 100644 --- a/frontend/wasm/src/module/module_env.rs +++ b/frontend/wasm/src/module/module_env.rs @@ -476,8 +476,9 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> { return Err(diagnostics .diagnostic(Severity::Error) .with_message(format!( - "wasm error: multiple '{WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME}' \ - custom sections were found; only one is allowed per core Wasm module" + "wasm error: multiple \ + '{WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME}' custom sections \ + were found; only one is allowed per core Wasm module" )) .into_report()); } diff --git a/i1307-implementation-plan.md b/i1307-implementation-plan.md new file mode 100644 index 0000000000..2a3aa3f003 --- /dev/null +++ b/i1307-implementation-plan.md @@ -0,0 +1,256 @@ +# i1307 — PoC: schema for user-defined note types (WIT + Wasm) + +Implements the design in discussion [#1294](https://github.com/0xMiden/compiler/discussions/1294) (issue #1307). +Branch: `i1307-note-type-schema` (on `next`). All new crates are `publish = false` (PoC grade). + +## Settled decisions + +- Full design in scope: schema emission, runtime API, bindings macro, codec component. Codec last. +- `wasmtime` enters the workspace (feature-gated, consumer side only). +- Replicate the `PackageSections` refactor from the WIP `wit-in-package` branch on this branch (do not base on it). +- Examples: p2id = no-codec demo; new `dex-note` + `dex-note-codec` pair = custom-type + codec demo. +- Migration: only the shared p2id path in `tests/integration-network/src/mockchain/support/helpers.rs`. Other hand-encoded sites stay for a follow-up. +- Type surface (PoC): named-field structs → schema; unit structs → no schema section; tuple structs → compile error; `Vec` fields → "not supported yet" error; nested custom types need `#[export_type]` defined before the `#[note]` struct; doc comments are carried into the WIT; embed the whole `core-types` interface (no subset pruning). + +## Name registry (fixed up front) + +| Thing | Name | +|---|---| +| Wasm custom section (schema) | `rodata,miden_note_schema` | +| Wasm static (schema) | `__MIDEN_NOTE_STORAGE_SCHEMA_BYTES` | +| `.masp` section id (schema) | `note_storage_schema` (via `SectionId::custom`) | +| `.masp` section id (codec) | `note_codec` (via `SectionId::custom`) | +| Schema WIT package | `:-schema@`, interface `note-storage`, root alias `type storage = ;` | +| Codec world | `package miden:note-codec@1.0.0`, `world note-codec`, `type felt = u64` | +| Reader crate | `sdk/note-schema` → `miden-note-schema` | +| Shared codegen (internal) | `sdk/note-schema/codegen` → `miden-note-schema-codegen` | +| Bindings macro crate | `sdk/note-bindings` → `miden-note-bindings` (pure proc-macro) | +| Author codec crates | `sdk/note-codec` → `miden-note-codec` (lib) + `sdk/note-codec/macros` → `miden-note-codec-macros` | +| Codec-crate pointer | `[package.metadata] note-codec-crate = "…"` in the note's `miden-project.toml` (`MetadataSet` is free-form; no upstream `miden-project` change) | + +The normative felt layout rule (document it in `miden-note-schema` rustdoc): layout is structural +over the WIT type tree; the record `miden:base/core-types.felt` is the 1-felt bedrock; `u64` = 2 +felts (lo u32, hi u32); `u32`/`u8`/`bool` = 1 range-checked felt; `option` = 1 tag felt + +payload; `variant` = 1 tag felt (declaration ordinal) + case payload; records concatenate fields in +declaration order. This is exactly `miden-field-repr`'s documented layout, so `word` (4), +`account-id` (2), etc. need no special cases — they bottom out at `felt` structurally. Codecs never +change layout; they only bind string parse/display/validate to a WIT fqn. + +--- + +## Phase 0 — `PackageSections` plumbing refactor + +Goal: one struct carried through the pipeline instead of a per-payload `Option>` field, so +Phase 1 (and the wit-in-package rebase later) only add a field. + +1. `sdk/wasm-metadata/src/lib.rs`: + - Add consts `WASM_ACCOUNT_COMPONENT_METADATA_CUSTOM_SECTION_NAME = "rodata,miden_account"` + (replace the two hardcoded uses), `WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME`, + `PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID`, `PACKAGE_NOTE_CODEC_SECTION_ID`. + - Add `pub struct PackageSections { pub account_component_metadata: Option>, pub note_storage_schema: Option> }` + (mirror the shape on `origin/wit-in-package`, without its `component_wit` field). +2. Thread `PackageSections` through, replacing `account_component_metadata_bytes`: + - `frontend/wasm/src/module/module_env.rs` (`ParsedModule`), `frontend/wasm/src/component/translator.rs:176-192`, + `frontend/wasm/src/lib.rs` (`FrontendOutput`), `midenc-compile/src/pipeline/frontends/wasm.rs:470-487`, + `midenc-compile/src/pipeline/artifacts.rs` (`MidenComponent`, `CodegenOutput`), + `midenc-compile/src/pipeline/backend.rs` (`LoweredTarget`), + `midenc-compile/src/pipeline/assembly.rs::post_process_package` and its callers + (`backend.rs:828`, `frontends/hir.rs:392`, `frontends/wasm.rs:590`, `seed.rs:437`, `seed.rs:604`). +3. No behavior change. `cargo make test` must stay green with no expectation updates. + +## Phase 1 — Schema emission into the `.masp` + +### 1a. Macro side (`sdk/base-macros`) + +1. Registry doc support (`src/types.rs`, `src/export_type.rs`): add `docs: Vec` to + `ExportedTypeDef`, `ExportedField`, `ExportedVariant`; capture `#[doc]` attrs at registration. + The component-macro consumer ignores the new fields. +2. New module `src/note_schema.rs`: + - Input: the `#[note]` struct item (+ doc attrs), the export-type registry, package identity. + - Package identity: reuse the same source `build_note_script_wit` uses for the main WIT package + name (crate-name/manifest based) and append `-schema`; version from the same source. Must not + fail on fixtures without `miden-project.toml`. + - Map field types with the existing `map_type_to_type_ref`; wrap its rejections in a + note-specific diagnostic ("`Vec` is not supported in note storage schemas yet", etc.). + Resolve custom types through the registry (transitive closure; unregistered → error that names + `#[export_type]` and the ordering rule, like `ensure_custom_type_defined`). + - Render the multi-package WIT document. Settled layout (spike-verified, 2026-08-05 — the + all-braced form in the discussion sketch does NOT parse in wit-parser 0.247; the main package + must be first and in header form): + `package :-schema@;` header, then `interface note-storage { use …; records…; type storage = ; }` + at top level, then `package miden:base@1.0.0 { interface core-types { … } }` as a braced + block at the end — body extracted verbatim from `SDK_WIT_SOURCE` (textual block extraction; + unit-test it). `WitBuilder` needs a `package_block` (braced package) helper; doc comments + emitted as `///` lines. Field/record names kebab-cased with the existing helpers. + - Emit bytes: UTF-8 text, padded to a 16-byte multiple with NULs (mirror ACM padding; readers + trim trailing NULs). + - Emit the static: `#[unsafe(link_section = "rodata,miden_note_schema")] pub static __MIDEN_NOTE_STORAGE_SCHEMA_BYTES: [u8; N]` + (fixed name → duplicate-symbol link error enforces one storage schema per crate). +3. Hook into `expand_note_struct` (`src/note.rs:100`): named-field arm emits the schema static; + unit arm emits nothing; unnamed/tuple arm becomes a compile error ("note storage schema needs + named fields"). +4. Unit tests (`sdk/base-macros/tests/`, plus module tests using + `reset_export_type_registry_for_tests`): + - Golden expansion for a p2id-shaped struct and for a struct with a nested `#[export_type]` + record and an enum. + - Parse the emitted document with `wit_parser` (`wit-bindgen-core` re-exports it; wit-component + 0.247 is already a dev-dep) and assert it resolves; assert interface/alias/root are found. + - Error cases: tuple struct, `Vec` field, unregistered custom type. + +### 1b. Compiler side + +1. `frontend/wasm/src/module/module_env.rs`: new `Payload::CustomSection` arm for + `WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME` → store raw bytes (validate UTF-8 after NUL trim; + full WIT validation stays in tests/consumers for the PoC). +2. `translator.rs`: collect across nested modules, error on >1 (mirror the ACM logic) → the new + `PackageSections.note_storage_schema` field. +3. `midenc-compile/src/pipeline/assembly.rs`: `attach_note_storage_schema` — push + `Section::new(SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID)?, bytes)` whenever + present. Custom sections are digest-exempt in `miden-mast-package` 0.25.8 by construction — + nothing to do for digests. + +### 1c. Tests + +- New `tests/integration/src/end_to_end/examples/note_schema_metadata.rs` (mirror + `counter_metadata.rs`): build `examples/p2id-note`, read the section, expect-test the WIT text, + and round-trip it through `wit_parser::Resolve`. +- Same golden for the `swapp-note` fixture (6 fields: `Word`, `Felt`, `AccountId`, …) — the richest + existing storage struct. +- Update package-size expectations (`basic_wallet_package_sizes.rs` and friends) with + `UPDATE_EXPECT=1` — every named-field note package now carries the section. + +## Phase 2 — Runtime API: `miden-note-schema` + +New std host crate `sdk/note-schema` (workspace member, `publish = false`). +Deps: `miden-mast-package`, `wit-parser` (promote 0.247 to a workspace dep), `miden-protocol`, +`miden-field`, `miden-field-repr` (native mode — reuse `FeltReader`/`FeltWriter` so the layout +rules live in one place). + +1. Schema model + reader: + - `NoteStorageSchema::from_package(&Package)` — find section, trim NULs, parse UTF-8 WIT, + resolve, locate interface `note-storage` and alias `storage`, build an internal model + (ordered fields; type tree of records/variants/options/primitives/leaf fqns). + `from_wit_text(&str)` for tests. Surface validation errors are actionable. + - Layout interpreter over the model per the normative rule above (widths, walk order). +2. Codecs: + - `trait ConsumerTypeCodec { parse(&self,&str)->Result,_>; display(&self,&[Felt])->String; validate(&self,&[Felt])->Result<(),_> }`. + - `CodecRegistry` keyed by WIT fqn. `Default` = standard leaf codecs over protocol parsers: + `felt` (decimal/hex), `word` (hex / 4-felt), `account-id` (`AccountId::parse` bech32|hex → + `[prefix, suffix]` per the WIT record order), `asset-amount` (decimal u64). Keep the set small. +3. Build direction: `schema.builder()` — `set(name, &str)` (kebab accepted, snake normalized; + dotted paths reach nested-record leaves — the structural UX), leaf routing: codec fqn if + registered, else primitive parse, else error naming the fqn; `build()` does completeness + + range checks → `NoteStorage`. +4. Decode direction: `schema.decode(&NoteStorage)` → named value tree; `Display` uses the registry + when an fqn is registered, else structural rendering. +5. Tests: unit tests on `from_wit_text` (layout widths, builder round-trips, error paths); + integration test: build p2id `.masp`, `builder().set("target-account-id", bech32).build()` + equals the hand-built `[prefix, suffix]` storage; decode round-trip displays the bech32 back. + +## Phase 3 — Typed bindings: `miden-note-bindings` + +1. `sdk/note-schema/codegen` (`miden-note-schema-codegen`, internal lib): schema model → Rust + tokens for the host-profile types — one generator shared by Phases 3 and 4: + - standard leaves → `miden_protocol::account::AccountId`, `miden_field::Word`, + `miden_field::Felt`; primitives verbatim; custom records/variants → generated + structs/enums with `#[derive(ToFeltRepr, FromFeltRepr)]` (native) + a WIT-fqn const per type. + - protocol-leaf encode/decode helpers follow the WIT record order (`account-id` → + `[prefix, suffix]`). +2. `sdk/note-bindings` (pure proc-macro crate): + - `from_project!("../dex-note")` — resolve against `CARGO_MANIFEST_DIR`; find the freshest + `.masp` across `/target/miden//` (mirror the `fpi.rs:1671`/`:1704` candidate + logic); missing artifact → compile error naming `cargo miden build`. + - `from_package!("path/to.masp")` — exact path. Both read the section and delegate to the shared + codegen, then add the consumer surface per the design: `to_note_storage`, + `from_note_storage`, `from_str_values(&BTreeMap<_,_>, &CodecRegistry)`, `validate_with`, + `display_with`; when the schema has no custom types, drop the `codecs` parameters. + - Hidden `from_wit_text!` entry for golden expansion tests. +3. Tests: expansion goldens over schema WIT strings (p2id-shaped, custom-type-shaped); one + end-to-end test that builds `examples/p2id-note` and then compiles + runs a small temp consumer + crate (process-spawned `cargo`, pattern like `tools/cargo-miden/tests`). + +## Phase 4 — Codec component + +### 4a. World + author-side crates + +1. `sdk/note-codec` (`miden-note-codec`, lib): ships `wit/note-codec.wit` (the world exactly as in + the discussion, `type felt = u64` with the doc comment about why it is not the core-types + record); `trait AuthorTypeCodec { fn parse(&str)->Result; fn display(&self)->String; fn validate(&self)->Result<(),String> }`; + boundary glue: u64↔Felt via canonical u64 with canonicality checks (reject ≥ p at the boundary). +2. `sdk/note-codec/macros` (`miden-note-codec-macros`, re-exported from the lib): + - `from_project!` / `from_package!` — same artifact resolution as Phase 3, but generate only the + host-profile types + record the schema (incl. root) in a process-global registry for + `export_codecs!`. + - `#[note_codec]` — marks an `AuthorTypeCodec` impl; registers the type's fqn. + - `export_codecs!()` — wit-bindgen `generate!` for the `note-codec` world plus the `export!` + glue: `supported-types` from the marked set; `parse`/`display`/`validate` dispatch by fqn + through the marked impls and their felt-repr impls. Component glue gated + `cfg(target_family = "wasm")` so the crate still builds and unit-tests natively. +3. Componentization target: primary = `wasm32-unknown-unknown` cdylib + + `wit_component::ComponentEncoder` (no WASI imports — clean sandbox, jco-friendly); fallback if + friction = `wasm32-wasip2` (rustc emits a component directly, consumer then needs a WASI + context). Spike this first inside Phase 4. + +### 4b. `cargo-miden` orchestration + +In `tools/cargo-miden/src/commands/build.rs` after `compile_to_memory`, before `write_masp_file`: +read the note's `miden-project.toml` `[package.metadata] note-codec-crate` (via the +`miden-project` crate's `MetadataSet`); when set: `cargo build --release` for the codec crate at +the componentization target, componentize, and `package.sections.push(Section::new(SectionId::custom("note_codec")?, bytes))`. +Omitted key → no section (structural fallback). + +### 4c. Consumer adapter + +`miden-note-schema`, feature `codec-component` (off by default): `CodecRegistry::load_from_package` +— find the `note_codec` section, instantiate with `wasmtime` (component model), wrap in an adapter +implementing `ConsumerTypeCodec` per fqn reported by `supported-types`. `wasmtime` is a +feature-gated dependency of this crate only. + +### 4d. Example pair + end-to-end + +1. `examples/dex-note` (guest, standalone project like p2id): `#[export_type] #[derive(FromFeltRepr)] struct LimitPrice { numerator: u64, denominator: u64 }`, + `#[note] struct DexNote { target: AccountId, price: LimitPrice }`, trivial `#[note_script]` + against `basic-wallet` (p2id-like), doc comments on everything (they surface in the WIT). + `miden-project.toml` gets the `note-codec-crate` pointer. +2. `examples/dex-note-codec` (host, plain cargo, no wasm target config): depends only on + `miden-note-codec`; `from_project!("../dex-note")`; `#[note_codec] impl AuthorTypeCodec for LimitPrice` + (parse "3/2" and "1.5" forms, display, validate denominator ≠ 0); `export_codecs!()`. +3. Tests: + - `tools/cargo-miden/tests/dex_note_codec_build.rs`: `cargo miden build` on dex-note → package + has both `note_storage_schema` and `note_codec` sections. + - `tests/integration-network` mockchain test: consume a dex note whose storage was built from + strings (`"target"` = bech32 natively, `"price"` = `"1.5"` through the component), and decode + an incoming note back to `"1.5"` via `display`. + - p2id no-codec demo + migration: mockchain test building p2id storage via the schema string + builder; switch the shared p2id path in `support/helpers.rs` (`to_core_felts` call in + `build_asset_transfer_tx`) to the schema builder. + +## Cross-cutting finish work + +- `sdk/sdk/CHANGELOG.md`: entry for the `#[note]` schema emission + new `.masp` section (the + published macro crates change even though the new crates are `publish = false`); new PoC + restrictions (tuple structs, `Vec` fields) called out. No MIGRATION entry (additive; the + restrictions break no released code — verify no external breakage claim beyond the repo). +- Full sweep per repo rules: `cargo make test-all`, `cargo make clippy`, `cargo make format-rust`; + `UPDATE_EXPECT=1` only for the intended package-size/golden updates. + +## Risks / early spikes + +1. ~~**Multi-package WIT text** in wit-parser 0.247~~ — RESOLVED by spike: header-form main + package first + braced dependency package after works; all-braced does not. JS-tooling (jco) + parse compatibility is untested and out of PoC scope (Rust consumers only). +2. **Componentization target** (4a.3) — spike at Phase 4 start. +3. **Package identity in `expand_note_struct`** — must degrade gracefully for fixtures without + `miden-project.toml` (fall back to crate name/version, same as the note-script WIT). +4. **Link-section padding** — ACM pads to 16 bytes; keep the padding, trim NULs on every reader. +5. **`from_project!` freshness** — newest-mtime across profiles is the settled design rule + (schema is profile-invariant); reuse the fpi.rs candidate-dir logic rather than reinventing. +6. **Process-global registries in proc macros** — one schema registry per crate build is the same + trade the `#[export_type]` registry already makes; keep the reset-for-tests hook pattern. + +## Suggested execution order + +Phase 0 → 1 → 2 → 3 → 4, each landable and testable on its own. Phases 0–1 touch the compiler +pipeline and macros; 2–3 are pure new host crates; 4 touches `cargo-miden` + examples. Codex-sized +work packets: each phase is one packet, with Phase 4 split into (world+author crates), (cargo-miden ++ example), (consumer + e2e). diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index bf6090a790..00028824e7 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -331,7 +331,6 @@ pub fn write_package_atomic( }) } - /// Returns true when project metadata declares an author-side note codec crate. pub(crate) fn has_project_note_codec(metadata: &miden_project::MetadataSet) -> bool { metadata.get(NOTE_CODEC_CRATE_METADATA).is_some() diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 55d1fec626..41728cc29a 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -478,7 +478,7 @@ interface note-storage { .push_node(BasicBlockNodeBuilder::new(vec![Operation::Add])) .expect("failed to build package procedure"); builder.mark_root(node_id); - let (forest, remapping) = builder.finish_with_id_map().expect("failed to build package"); + let (forest, remapping) = builder.build_with_id_map().expect("failed to build package"); let node_id = remapping.get(node_id).expect("package root was removed"); let export = ProcedureExport::new( MastPathBuf::absolute("component-codec-test::run").into(), diff --git a/tests/integration-network/src/mockchain/notes/schema.rs b/tests/integration-network/src/mockchain/notes/schema.rs index 8b442cdc2c..76b2a20249 100644 --- a/tests/integration-network/src/mockchain/notes/schema.rs +++ b/tests/integration-network/src/mockchain/notes/schema.rs @@ -75,16 +75,19 @@ fn transfer_with_storage( let faucet = chain.committed_account(faucet_id).unwrap().clone(); let send_script = build_send_notes_script(&faucet, std::slice::from_ref(¬e)); let send = chain - .build_tx_context(faucet_id, &[], &[]) - .unwrap() - .tx_script(send_script.into()) - .extend_expected_output_notes(vec![RawOutputNote::Full(note.clone())]); + .build_transaction(faucet_id) + .send_notes_script(&send_script) + .expected_output_notes(vec![RawOutputNote::Full(note.clone())]) + .build() + .unwrap(); execute_tx(&mut chain, send); let consume = chain - .build_tx_context(recipient_id, &[note.id()], &[]) - .unwrap() - .foreign_accounts(vec![chain.get_foreign_account_inputs(faucet_id).unwrap()]); + .build_transaction(recipient_id) + .authenticated_input_note(note.id()) + .foreign_accounts(vec![chain.get_foreign_account_inputs(faucet_id).unwrap()]) + .build() + .unwrap(); execute_tx(&mut chain, consume); assert_account_has_fungible_asset( diff --git a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs index bee6215ea4..6551848df1 100644 --- a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs +++ b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs @@ -8,7 +8,6 @@ use midenc_frontend_wasm::WasmTranslationConfig; use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; use wit_bindgen_core::wit_parser::{Resolve, Type as WitType, TypeDefKind}; -use super::persist_cargo_miden_dependency; use crate::CompilerTest; /// Disables debug output so compiled package content is stable. @@ -63,9 +62,6 @@ fn assert_note_storage_schema(package: &Package, expected_root: &str, expected: #[test] fn note_packages_carry_resolvable_storage_schema_metadata() { - let wallet = compile_project("../../examples/basic-wallet"); - persist_cargo_miden_dependency("../../examples/basic-wallet", wallet.as_ref()); - let p2id = compile_project("../../examples/p2id-note"); assert_note_storage_schema( &p2id, From 831e2eea4c386febd9be7c863d79d02a4b66b123 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Mon, 17 Aug 2026 08:47:55 +0300 Subject: [PATCH 07/43] fix: close identity, resource, and packaging gaps from the second schema review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second multi-pass review of the schema work surfaced silent type-identity holes, resource-exhaustion paths on untrusted input, a publishability gap, and infrastructure drift between the macro crates. Type identity: registering two exported types with the same name but different shapes is now an error, as is a note field whose unregistered type name merely collides with an SDK core type — both previously produced schemas that disagreed with the guest encoding silently. Untrusted input: the schema model is a memoized graph with explicit limits on section size, type count, nesting depth, and root width, so a small adversarial WIT document can no longer explode into exponential allocation; codec components are size-capped before compilation and their tables are bounded at instantiation. Packaging: the canonical note-codec world WIT moves into the new dependency-free `miden-note-codec-wit` crate, replacing three held-together copies; `miden-note-schema` keeps the one package-local copy its wasmtime bindings require, equality-tested against the crate. The `codec-component` feature now builds from a registry. The changelog documents the author-codec surface and the `note-codec-crate` manifest key, and a codec declaration applies per note target instead of failing sibling targets; core-module builds reject schema sections with guidance instead of dropping them. Infrastructure: note artifact discovery follows the same identity-ordered policy as FPI dependency resolution and the in-flight package stages through the package cache, retiring the bespoke env-var handoff; the nested codec build stages immutably and honors the outer `--locked`/`--offline` policy; generated runtime paths are code-generation inputs, deleting both post-hoc AST rewriters, with one felt-repr redirection mechanism, dependency-rename-safe paths, and a one-schema-per-crate contract with actionable errors. The three walkers share one standard-leaf definition. --- .release/config.toml | 4 + Cargo.lock | 12 + Cargo.toml | 2 + examples/dex-note-codec/Cargo.lock | 7 + frontend/wasm/src/lib.rs | 49 ++- midenc-compile/Cargo.toml | 3 + midenc-compile/src/cargo.rs | 208 ++++++++----- midenc-compile/src/compiler.rs | 18 ++ midenc-compile/src/pipeline/assembly.rs | 48 ++- midenc-compile/src/pipeline/frontends/rust.rs | 28 +- midenc-session/src/options/mod.rs | 6 + sdk/CHANGELOG.md | 26 ++ sdk/base-macros/Cargo.toml | 1 + sdk/base-macros/src/export_type.rs | 17 +- sdk/base-macros/src/note_schema.rs | 9 +- sdk/base-macros/src/types.rs | 206 ++++++++++++- sdk/base-macros/src/types/tests.rs | 157 +++++++++- sdk/note-bindings/macros/Cargo.toml | 2 +- sdk/note-bindings/macros/expected/custom.rs | 4 +- sdk/note-bindings/macros/expected/p2id.rs | 4 +- sdk/note-bindings/macros/src/lib.rs | 102 +------ sdk/note-codec/Cargo.toml | 1 + sdk/note-codec/macros/Cargo.toml | 3 +- sdk/note-codec/macros/src/expand.rs | 108 +++---- sdk/note-codec/macros/src/lib.rs | 7 +- sdk/note-codec/macros/src/registry.rs | 54 ++-- sdk/note-codec/macros/src/tests.rs | 91 ++++++ sdk/note-codec/src/lib.rs | 7 +- sdk/note-codec/tests/component_export.rs | 10 +- sdk/note-codec/wit-crate/Cargo.toml | 17 ++ sdk/note-codec/wit-crate/src/lib.rs | 8 + .../note-codec/wit-crate}/wit/note-codec.wit | 1 - sdk/note-codec/wit/note-codec.wit | 27 -- sdk/note-schema/Cargo.toml | 2 + sdk/note-schema/codegen/src/lib.rs | 232 +++++++++----- sdk/note-schema/codegen/src/tests.rs | 35 ++- sdk/note-schema/src/artifact.rs | 287 ++++++++++++------ sdk/note-schema/src/codec.rs | 69 ++++- sdk/note-schema/src/codec_component.rs | 107 ++++++- sdk/note-schema/src/lib.rs | 8 +- sdk/note-schema/src/schema.rs | 275 ++++++++++++----- sdk/note-schema/src/tests.rs | 106 ++++++- .../macros => note-schema}/wit/note-codec.wit | 1 - .../examples/note_schema_metadata.rs | 12 - 44 files changed, 1751 insertions(+), 630 deletions(-) create mode 100644 sdk/note-codec/wit-crate/Cargo.toml create mode 100644 sdk/note-codec/wit-crate/src/lib.rs rename {midenc-compile => sdk/note-codec/wit-crate}/wit/note-codec.wit (99%) delete mode 100644 sdk/note-codec/wit/note-codec.wit rename sdk/{note-codec/macros => note-schema}/wit/note-codec.wit (99%) diff --git a/.release/config.toml b/.release/config.toml index 8dc8e7884a..7aa2a9b5c6 100644 --- a/.release/config.toml +++ b/.release/config.toml @@ -247,6 +247,10 @@ unit = "sdk" name = "miden-note-codec-macros" unit = "sdk" +[[packages]] +name = "miden-note-codec-wit" +unit = "sdk" + [[packages]] name = "midenc-benchmark-runner" unit = "private" diff --git a/Cargo.lock b/Cargo.lock index bf2bb27134..7358064432 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3193,6 +3193,7 @@ dependencies = [ "quote", "semver 1.0.28", "syn 2.0.119", + "tempfile", "toml 1.1.4+spec-1.1.0", "wit-bindgen-core", "wit-bindgen-rust", @@ -3644,6 +3645,7 @@ dependencies = [ "miden-field", "miden-field-repr", "miden-note-codec-macros", + "miden-note-codec-wit", "miden-protocol", "tempfile", "wit-bindgen", @@ -3656,6 +3658,7 @@ name = "miden-note-codec-macros" version = "0.14.0" dependencies = [ "heck", + "miden-note-codec-wit", "miden-note-schema", "miden-note-schema-codegen", "prettyplease", @@ -3665,6 +3668,10 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "miden-note-codec-wit" +version = "0.14.0" + [[package]] name = "miden-note-schema" version = "0.14.0" @@ -3673,11 +3680,13 @@ dependencies = [ "miden-field", "miden-field-repr", "miden-mast-package", + "miden-note-codec-wit", "miden-protocol", "midenc-frontend-wasm", "midenc-frontend-wasm-metadata", "midenc-integration-test-support", "tempfile", + "toml 1.1.4+spec-1.1.0", "wasmtime", "wit-component", "wit-parser 0.247.0", @@ -4151,6 +4160,7 @@ dependencies = [ "miden-assembly", "miden-assembly-syntax", "miden-mast-package", + "miden-note-codec-wit", "miden-package-registry", "miden-thiserror", "midenc-codegen-masm", @@ -4162,6 +4172,8 @@ dependencies = [ "midenc-hir", "midenc-hir-transform", "midenc-session", + "sha2", + "tempfile", "toml_edit", "wat", "wit-component", diff --git a/Cargo.toml b/Cargo.toml index 7fdd3a525d..c81c012ab8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ members = [ "sdk/note-bindings/macros", "sdk/note-codec", "sdk/note-codec/macros", + "sdk/note-codec/wit-crate", "sdk/note-schema", "sdk/note-schema/codegen", "sdk/sdk", @@ -204,6 +205,7 @@ miden-base-sys = { version = "0.14.0", path = "sdk/base-sys" } miden-field-repr = { version = "0.14.0", path = "sdk/field-repr/repr" } miden-note-bindings-macros = { version = "0.14.0", path = "sdk/note-bindings/macros" } miden-note-codec-macros = { version = "0.14.0", path = "sdk/note-codec/macros" } +miden-note-codec-wit = { version = "0.14.0", path = "sdk/note-codec/wit-crate" } miden-note-schema = { version = "0.14.0", path = "sdk/note-schema" } miden-note-schema-codegen = { version = "0.14.0", path = "sdk/note-schema/codegen" } miden-stdlib-sys = { version = "0.14.0", path = "sdk/stdlib-sys" } diff --git a/examples/dex-note-codec/Cargo.lock b/examples/dex-note-codec/Cargo.lock index f411bed0fd..d2b590bc7f 100644 --- a/examples/dex-note-codec/Cargo.lock +++ b/examples/dex-note-codec/Cargo.lock @@ -1943,6 +1943,7 @@ dependencies = [ "miden-field", "miden-field-repr", "miden-note-codec-macros", + "miden-note-codec-wit", "miden-protocol", "wit-bindgen", ] @@ -1952,6 +1953,7 @@ name = "miden-note-codec-macros" version = "0.14.0-rc.1" dependencies = [ "heck", + "miden-note-codec-wit", "miden-note-schema", "miden-note-schema-codegen", "proc-macro-crate", @@ -1960,6 +1962,10 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "miden-note-codec-wit" +version = "0.14.0-rc.1" + [[package]] name = "miden-note-schema" version = "0.14.0-rc.1" @@ -1969,6 +1975,7 @@ dependencies = [ "miden-mast-package", "miden-protocol", "midenc-frontend-wasm-metadata", + "toml", "wit-parser", ] diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index 9957433030..763256b59c 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -27,10 +27,12 @@ use alloc::rc::Rc; use component::build_ir::translate_component; use error::WasmResult; -use midenc_frontend_wasm_metadata::PackageSections; +use midenc_frontend_wasm_metadata::{ + PackageSections, WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME, +}; use midenc_hir::{Context, dialects::builtin}; use module::build_ir::translate_module_as_component; -use wasmparser::WasmFeatures; +use wasmparser::{Payload, WasmFeatures}; #[cfg(feature = "std")] pub use self::emit::wasm_to_wat; @@ -60,6 +62,26 @@ pub fn translate( } } +/// Rejects note storage schema metadata from a core Wasm module. +fn reject_core_module_note_storage_schema(wasm: &[u8]) -> WasmResult<()> { + for payload in wasmparser::Parser::new(0).parse_all(wasm) { + let payload = payload.map_err(|error| -> midenc_session::diagnostics::Report { + WasmError::from(error).into() + })?; + if let Payload::CustomSection(section) = payload + && section.name() == WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME + { + return Err(WasmError::Unsupported( + "a core WebAssembly module contains a note storage schema that cannot be \ + preserved; compile the note crate as a WebAssembly component" + .to_owned(), + ) + .into()); + } + } + Ok(()) +} + /// The set of core WebAssembly features which we need to or wish to support pub(crate) fn supported_features() -> WasmFeatures { WasmFeatures::BULK_MEMORY @@ -79,3 +101,26 @@ pub(crate) fn supported_features() -> WasmFeatures { pub(crate) fn supported_component_model_features() -> WasmFeatures { supported_features() | WasmFeatures::COMPONENT_MODEL } + +#[cfg(test)] +mod tests { + use super::*; + + /// A `#[note]` crate compiled as a raw core module keeps its schema section: the + /// translation wraps the module as a component and propagates the captured sections. + #[test] + fn core_module_propagates_note_storage_schema_section() { + let wat = format!( + r#"(module (@custom "{WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME}" "schema"))"# + ); + let wasm = wat::parse_str(wat).expect("core module WAT must parse"); + let context = Rc::new(Context::default()); + let output = translate(&wasm, &WasmTranslationConfig::default(), context) + .expect("a core module with a schema section must translate"); + assert_eq!( + output.sections.note_storage_schema.as_deref(), + Some(b"schema".as_slice()), + "the schema section must be propagated into the frontend output" + ); + } +} diff --git a/midenc-compile/Cargo.toml b/midenc-compile/Cargo.toml index 77ff3ec8c7..c8291212cd 100644 --- a/midenc-compile/Cargo.toml +++ b/midenc-compile/Cargo.toml @@ -40,6 +40,7 @@ miden-assembly.workspace = true miden-assembly-syntax.workspace = true miden-mast-package.workspace = true miden-package-registry.workspace = true +miden-note-codec-wit.workspace = true midenc-frontend-wasm.workspace = true midenc-frontend-wasm-metadata.workspace = true midenc-frontend-masm.workspace = true @@ -48,6 +49,8 @@ midenc-dialect-hir.workspace = true midenc-hir.workspace = true midenc-hir-transform.workspace = true midenc-session.workspace = true +sha2.workspace = true +tempfile = { workspace = true, optional = true } toml_edit = { workspace = true, optional = true, features = ["parse", "display"] } thiserror.workspace = true wat = { workspace = true, optional = true } diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 00028824e7..0a2c21de8a 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -1,4 +1,4 @@ -use core::str::FromStr; +use core::{fmt::Write as _, str::FromStr}; use std::{ boxed::Box, env, fs, @@ -10,10 +10,12 @@ use std::{ vec::Vec, }; -use miden_assembly::SourceManager; +use miden_assembly::{SourceManager, serde::Serializable}; use miden_mast_package::Package as MastPackage; +use miden_note_codec_wit::NOTE_CODEC_WIT; use midenc_hir::Report; use midenc_session::{InputFile, LinkLibrary, Session, miden_project}; +use sha2::{Digest, Sha256}; use wit_component::{ComponentEncoder, DecodedWasm}; use wit_parser::{Function, FunctionKind, Resolve, Type, TypeDefKind, TypeId, WorldId, WorldItem}; @@ -28,11 +30,14 @@ const NOTE_CODEC_CRATE_PATH: &str = "path"; /// Rust target used for zero-import note codec components. const NOTE_CODEC_TARGET: &str = "wasm32-unknown-unknown"; -/// Compiler-provided path to the exact package consumed by `from_project!`. -const NOTE_CODEC_PACKAGE_PATH_ENV: &str = "MIDENC_NOTE_CODEC_PACKAGE_PATH"; +/// Directory used to exchange Miden packages with nested Cargo builds. +const PACKAGE_CACHE_ENV: &str = "MIDENC_PACKAGE_CACHE"; -/// Pinned author codec interface used to validate component signatures. -const NOTE_CODEC_WIT: &str = include_str!("../wit/note-codec.wit"); +/// One immutable staged package and its build-isolation key. +struct StagedNotePackage { + cache_dir: PathBuf, + build_key: String, +} /// Cargo-specific options extracted from the `Compiler` struct. /// @@ -48,6 +53,10 @@ pub struct CargoOptions { pub workspace: bool, /// Packages to build pub packages: Vec, + /// Require Cargo.lock to remain unchanged. + pub locked: bool, + /// Prevent Cargo from accessing the network. + pub offline: bool, } /// Represents a cargo package specifier. @@ -119,6 +128,8 @@ impl CargoOptions { manifest_path: options.manifest_path.clone(), workspace: options.workspace, packages, + locked: options.cargo_locked, + offline: options.cargo_offline, }) } } @@ -235,6 +246,8 @@ pub(crate) fn cargo_build( diagnostics: options.diagnostics, remap_path_prefixes: options.remap_path_prefixes.clone(), rustflags: options.rustflags.clone(), + cargo_locked: options.cargo_locked, + cargo_offline: options.cargo_offline, link_libraries: vec![LinkLibrary::core()], ..midenc_session::Options::new( Some(package_name.clone()), @@ -350,7 +363,7 @@ pub(crate) fn build_project_note_codec( return Ok(None); }; - build_note_codec_component(&codec_crate_dir, note_project_dir, note_package, session).map(Some) + build_note_codec_component(&codec_crate_dir, note_package, session).map(Some) } /// Reads the optional codec crate path from Miden project metadata. @@ -410,18 +423,16 @@ fn note_codec_crate_dir( /// Builds and componentizes one author-side note codec crate. fn build_note_codec_component( codec_crate_dir: &Path, - note_project_dir: &Path, note_package: &MastPackage, session: &Session, ) -> CompilerResult> { - sweep_legacy_note_codec_inputs(note_project_dir)?; let session_target_dir = if session.options.target_dir.is_absolute() { session.options.target_dir.clone() } else { session.options.current_dir.join(&session.options.target_dir) }; let work_dir = session_target_dir.join(&session.options.profile).join("note-codec"); - let staged_package = stage_note_package(&work_dir, note_package)?; + let staged_package = stage_note_package(&work_dir, codec_crate_dir, note_package)?; let manifest_path = codec_crate_dir.join("Cargo.toml"); if !manifest_path.is_file() { return Err(Report::msg(format!( @@ -439,7 +450,7 @@ fn build_note_codec_component( }; crate::rust::install_wasm32_target("unknown-unknown", toolchain.as_deref())?; - let cargo_target_dir = work_dir.join("cargo-target"); + let cargo_target_dir = work_dir.join("cargo-target").join(&staged_package.build_key); let mut cargo = Command::new(cargo_path); if let Some(toolchain) = toolchain.as_deref() { cargo.arg(format!("+{toolchain}")); @@ -458,12 +469,13 @@ fn build_note_codec_component( .arg(&cargo_target_dir) .arg("--message-format") .arg("json-render-diagnostics") - .env(NOTE_CODEC_PACKAGE_PATH_ENV, &staged_package) + .env(PACKAGE_CACHE_ENV, &staged_package.cache_dir) .env_remove("CARGO_BUILD_TARGET") .env_remove("CARGO_ENCODED_RUSTFLAGS") .env_remove("RUSTFLAGS") .stdout(Stdio::piped()) .stderr(Stdio::inherit()); + apply_cargo_policy(&mut cargo, session.options.cargo_locked, session.options.cargo_offline); let manifest_path = manifest_path.canonicalize().map_err(|error| { Report::msg(format!( @@ -471,7 +483,14 @@ fn build_note_codec_component( manifest_path.display() )) })?; - let artifacts = crate::rust::spawn_cargo(cargo, cargo_path)?; + let artifacts = crate::rust::spawn_cargo(cargo, cargo_path).map_err(|error| { + note_codec_cargo_error( + error, + &manifest_path, + session.options.cargo_locked, + session.options.cargo_offline, + ) + })?; let mut wasm_paths = artifacts .into_iter() .filter(|artifact| { @@ -525,71 +544,87 @@ fn build_note_codec_component( Ok(component) } -/// Stages the current in-memory note package at a stable compiler-owned path. -fn stage_note_package(work_dir: &Path, note_package: &MastPackage) -> CompilerResult { - let staging_dir = work_dir.join("input"); - fs::create_dir_all(&staging_dir).map_err(|error| { - Report::msg(format!( - "failed to create note package staging directory '{}': {error}", - staging_dir.display() - )) - })?; - note_package.write_masp_file(&staging_dir).map_err(|error| { - Report::msg(format!( - "failed to stage note package {}@{} for codec generation: {error}", - note_package.name, note_package.version - )) - })?; - let package_name: &str = ¬e_package.name; - staging_dir - .join(package_name) - .with_extension(MastPackage::EXTENSION) - .canonicalize() - .map_err(|error| { +/// Stages the current package in a content-addressed package-cache directory. +fn stage_note_package( + work_dir: &Path, + codec_crate_dir: &Path, + note_package: &MastPackage, +) -> CompilerResult { + let package_bytes = note_package.to_bytes(); + let mut hasher = Sha256::new(); + hasher.update(&package_bytes); + hasher.update([0]); + hasher.update(codec_crate_dir.as_os_str().to_string_lossy().as_bytes()); + let mut build_key = String::with_capacity(64); + for byte in hasher.finalize() { + write!(&mut build_key, "{byte:02x}").expect("writing to a string cannot fail"); + } + let cache_dir = work_dir.join("package-cache").join(&build_key); + let package_path = cache_dir.join(&*note_package.name).with_extension(MastPackage::EXTENSION); + + if package_path.is_file() { + let staged_bytes = fs::read(&package_path).map_err(|error| { Report::msg(format!( - "failed to resolve the staged note package in '{}': {error}", - staging_dir.display() + "failed to read staged note package '{}': {error}", + package_path.display() )) - }) -} - -/// Removes staging directories created by the temporary-directory implementation. -fn sweep_legacy_note_codec_inputs(note_project_dir: &Path) -> CompilerResult<()> { - let legacy_root = note_project_dir.join("target/miden"); - let entries = match fs::read_dir(&legacy_root) { - Ok(entries) => entries, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), - Err(error) => { + })?; + if staged_bytes != package_bytes { return Err(Report::msg(format!( - "failed to inspect legacy note codec staging root '{}': {error}", - legacy_root.display() + "content-addressed note package path '{}' contains different bytes", + package_path.display() ))); } - }; - for entry in entries { - let entry = entry.map_err(|error| { - Report::msg(format!( - "failed to inspect legacy note codec staging root '{}': {error}", - legacy_root.display() - )) - })?; - let name = entry.file_name(); - let file_type = entry.file_type().map_err(|error| { + } else { + write_package_atomic(note_package, &cache_dir).map_err(|error| { Report::msg(format!( - "failed to inspect legacy note codec staging entry '{}': {error}", - entry.path().display() + "failed to stage note package {}@{} for codec generation: {error}", + note_package.name, note_package.version )) })?; - if name.to_string_lossy().starts_with("zz-note-codec-input-") && file_type.is_dir() { - fs::remove_dir_all(entry.path()).map_err(|error| { - Report::msg(format!( - "failed to remove legacy note codec staging directory '{}': {error}", - entry.path().display() - )) - })?; - } } - Ok(()) + + Ok(StagedNotePackage { + cache_dir, + build_key, + }) +} + +/// Applies the outer Cargo resolution policy to a nested command. +fn apply_cargo_policy(cargo: &mut Command, locked: bool, offline: bool) { + if locked { + cargo.arg("--locked"); + } + if offline { + cargo.arg("--offline"); + } +} + +/// Adds lockfile and network recovery guidance to a nested Cargo failure. +fn note_codec_cargo_error( + error: Report, + manifest_path: &Path, + locked: bool, + offline: bool, +) -> Report { + if !locked && !offline { + return error; + } + + let mut guidance = format!( + "note codec build failed under the outer Cargo policy for '{}': {error}", + manifest_path.display() + ); + if locked { + guidance.push_str( + "; update and commit the codec workspace Cargo.lock before retrying with --locked", + ); + } + if offline { + guidance + .push_str("; fetch the codec dependencies while online before retrying with --offline"); + } + Report::msg(guidance) } /// Verifies the component sandbox and the versioned codec interface export. @@ -903,19 +938,34 @@ mod tests { } #[test] - fn note_codec_staging_is_stable_and_sweeps_legacy_directories() { + fn note_codec_staging_is_content_addressed() { let root = tempfile::TempDir::new().unwrap(); let package = midenc_codegen_masm::intrinsics::load(); + let codec_crate = root.path().join("codec"); + fs::create_dir(&codec_crate).unwrap(); + + let first = stage_note_package(root.path(), &codec_crate, &package).unwrap(); + let second = stage_note_package(root.path(), &codec_crate, &package).unwrap(); + assert_eq!(first.cache_dir, second.cache_dir); + assert_eq!(first.build_key, second.build_key); + assert_eq!(first.build_key.len(), 64); + let package_path = + first.cache_dir.join(&*package.name).with_extension(MastPackage::EXTENSION); + assert_eq!(fs::read(package_path).unwrap(), package.to_bytes()); + } - let first = stage_note_package(root.path(), &package).unwrap(); - let second = stage_note_package(root.path(), &package).unwrap(); - assert_eq!(first, second); - assert_eq!(first.parent().and_then(Path::file_name), Some("input".as_ref())); - - let legacy = root.path().join("project/target/miden/zz-note-codec-input-old"); - fs::create_dir_all(&legacy).unwrap(); - sweep_legacy_note_codec_inputs(&root.path().join("project")).unwrap(); - assert!(!legacy.exists()); + #[test] + fn note_codec_cargo_policy_is_forwarded() { + let mut cargo = Command::new("cargo"); + cargo.arg("build"); + apply_cargo_policy(&mut cargo, true, true); + let args = cargo + .get_args() + .map(|arg| arg.to_string_lossy().into_owned()) + .collect::>(); + + assert!(args.iter().any(|arg| arg == "--locked")); + assert!(args.iter().any(|arg| arg == "--offline")); } /// Resolves the codec interface from one complete WIT document. diff --git a/midenc-compile/src/compiler.rs b/midenc-compile/src/compiler.rs index b1d52e2b34..f009b851fe 100644 --- a/midenc-compile/src/compiler.rs +++ b/midenc-compile/src/compiler.rs @@ -383,6 +383,12 @@ pub struct Compiler { arg(long, short = 'p', value_name = "SPEC", conflicts_with("workspace"),) )] pub package: Vec, + /// Require Cargo.lock to remain unchanged in Cargo builds. + #[cfg_attr(feature = "std", arg(long, help_heading = "Compiler"))] + pub locked: bool, + /// Prevent Cargo builds from accessing the network. + #[cfg_attr(feature = "std", arg(long, help_heading = "Compiler"))] + pub offline: bool, /// Path to the package/project manifest /// /// If unspecified, the compiler will create a virtual manifest for the given input file, if @@ -710,6 +716,8 @@ impl Compiler { release: _, workspace, package, + locked, + offline, manifest_path, remap_path_prefixes, } = self; @@ -777,6 +785,8 @@ impl Compiler { options.entrypoint = entrypoint; options.workspace = workspace; options.packages = package; + options.cargo_locked = locked; + options.cargo_offline = offline; options.stop_after = stop_after; options.parse_only = parse_only; options.analyze_only = analyze_only; @@ -909,4 +919,12 @@ mod tests { fn no_stop_after_means_no_cap() { assert_eq!(options(&[]).stop_after, None); } + + /// Cargo resolution policy flags reach every nested build through the session options. + #[test] + fn cargo_resolution_policy_reaches_the_options() { + let options = options(&["--locked", "--offline"]); + assert!(options.cargo_locked); + assert!(options.cargo_offline); + } } diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index fcb3d1f2dc..efecd2659d 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -76,6 +76,7 @@ pub(crate) fn post_process_package( let has_note_codec = crate::cargo::has_project_note_codec(context.package.metadata()); validate_note_codec_declaration( has_note_codec, + package_has_note_target(&context.package), context.target.ty, sections.note_storage_schema.is_some(), context.target.name.inner(), @@ -101,7 +102,7 @@ pub(crate) fn post_process_package( .push(Section::new(SectionId::KERNEL, kernel_package.to_bytes())); } - if has_note_codec { + if has_note_codec && context.target.ty == TargetType::Note { attach_note_codec(package, context, session)?; } @@ -111,19 +112,20 @@ pub(crate) fn post_process_package( /// Validates the target and schema required by an author codec declaration. fn validate_note_codec_declaration( has_note_codec: bool, + package_has_note_target: bool, target_type: midenc_session::miden_project::TargetType, has_note_storage_schema: bool, target_name: &str, ) -> Result<(), Report> { use midenc_session::miden_project::TargetType; - if has_note_codec && target_type != TargetType::Note { + if has_note_codec && !package_has_note_target { return Err(Report::msg(format!( - "`[package.metadata.note-codec-crate]` is only valid for note targets, but target \ - '{target_name}' has type `{target_type}`" + "`[package.metadata.note-codec-crate]` requires a note target, but the package that \ + contains target '{target_name}' defines no note target" ))); } - if has_note_codec && !has_note_storage_schema { + if has_note_codec && target_type == TargetType::Note && !has_note_storage_schema { return Err(Report::msg(format!( "note target '{target_name}' declares `[package.metadata.note-codec-crate]` but \ emitted no note storage schema; add one named-field `#[note]` struct" @@ -132,6 +134,19 @@ fn validate_note_codec_declaration( Ok(()) } +/// Returns true when a package defines at least one note target. +fn package_has_note_target(package: &midenc_session::miden_project::Package) -> bool { + use midenc_session::miden_project::TargetType; + + package + .library_target() + .is_some_and(|target| target.inner().ty == TargetType::Note) + || package + .executable_targets() + .iter() + .any(|target| target.inner().ty == TargetType::Note) +} + /// Build and attach the note codec declared by the current project package. fn attach_note_codec( package: &mut Package, @@ -245,20 +260,29 @@ mod tests { } #[test] - fn codec_metadata_requires_a_note_target_with_a_schema() { - let wrong_target = - validate_note_codec_declaration(true, TargetType::Library, true, "library") + fn codec_metadata_skips_non_note_target_when_package_has_note_target() { + validate_note_codec_declaration(true, true, TargetType::Library, false, "library").unwrap(); + } + + #[test] + fn codec_metadata_requires_a_note_target_in_the_package() { + let error = + validate_note_codec_declaration(true, false, TargetType::Library, false, "library") .unwrap_err() .to_string(); - assert!(wrong_target.contains("only valid for note targets")); + assert!(error.contains("defines no note target")); + } + #[test] + fn codec_metadata_requires_a_schema_on_the_note_target() { let missing_schema = - validate_note_codec_declaration(true, TargetType::Note, false, "schema-less") + validate_note_codec_declaration(true, true, TargetType::Note, false, "schema-less") .unwrap_err() .to_string(); assert!(missing_schema.contains("emitted no note storage schema")); - validate_note_codec_declaration(true, TargetType::Note, true, "note").unwrap(); - validate_note_codec_declaration(false, TargetType::Library, false, "library").unwrap(); + validate_note_codec_declaration(true, true, TargetType::Note, true, "note").unwrap(); + validate_note_codec_declaration(false, false, TargetType::Library, false, "library") + .unwrap(); } } diff --git a/midenc-compile/src/pipeline/frontends/rust.rs b/midenc-compile/src/pipeline/frontends/rust.rs index 7696c83a0b..23da1a1678 100644 --- a/midenc-compile/src/pipeline/frontends/rust.rs +++ b/midenc-compile/src/pipeline/frontends/rust.rs @@ -867,6 +867,12 @@ fn build_cargo_args(manifest_path: &Path, options: &Options) -> Vec { if options.profile == "release" { args.push("--release".to_string()); } + if options.cargo_locked { + args.push("--locked".to_string()); + } + if options.cargo_offline { + args.push("--offline".to_string()); + } args.push("--manifest-path".to_string()); args.push(manifest_path.to_string_lossy().to_string()); @@ -2140,7 +2146,7 @@ pub(crate) mod manifest { } /// Builds the argument vector for the underlying `cargo build` invocation. - fn build_cargo_args(cargo_opts: &CargoOptions, opt_level: OptLevel) -> Vec { + pub(super) fn build_cargo_args(cargo_opts: &CargoOptions, opt_level: OptLevel) -> Vec { let mut args = vec!["build".to_string()]; // Add build-std flags required for Miden compilation @@ -2185,6 +2191,12 @@ pub(crate) mod manifest { if cargo_opts.release { args.push("--release".to_string()); } + if cargo_opts.locked { + args.push("--locked".to_string()); + } + if cargo_opts.offline { + args.push("--offline".to_string()); + } if let Some(ref manifest_path) = cargo_opts.manifest_path { args.push("--manifest-path".to_string()); @@ -3892,6 +3904,20 @@ path = "lib.rs" assert_eq!(merged, vec!["--cfg".to_string(), "miden".to_string()]); } + /// The root Cargo policy is forwarded to the manifest build command. + #[test] + fn the_cargo_resolution_policy_is_handed_to_the_nested_build() { + let options = crate::cargo::CargoOptions { + locked: true, + offline: true, + ..Default::default() + }; + let args = manifest::build_cargo_args(&options, midenc_session::OptLevel::None); + + assert!(args.iter().any(|arg| arg == "--locked")); + assert!(args.iter().any(|arg| arg == "--offline")); + } + /// A WebAssembly module with a body, for the lowering half of the entry point. const MANIFEST_WAT: &str = r#" (module diff --git a/midenc-session/src/options/mod.rs b/midenc-session/src/options/mod.rs index 78e914da36..e7e13b94ed 100644 --- a/midenc-session/src/options/mod.rs +++ b/midenc-session/src/options/mod.rs @@ -34,6 +34,10 @@ pub struct Options { pub workspace: bool, /// Build the specified packages in the current workspace (used by `cargo miden`) pub packages: Vec, + /// Require Cargo.lock to remain unchanged in nested Cargo builds. + pub cargo_locked: bool, + /// Prevent network access in nested Cargo builds. + pub cargo_offline: bool, /// The name of the current project target being compiled pub target: Option, /// The type of target that was requested @@ -162,6 +166,8 @@ impl Options { profile: "dev".to_string(), workspace: false, packages: vec![], + cargo_locked: false, + cargo_offline: false, target: None, target_type: target, entrypoint: None, diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index e6ad4b3dc5..d8e20d6e68 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -12,6 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added optional `codec-component` support to the new `miden-note-schema` host crate. It can load author-defined note codecs from a package without adding Wasmtime to the default feature set or the guest SDK dependency graph. +- Added the `miden-note-codec` author crate. Its codec-side `from_project!` and `from_package!` + macros generate host types from a note package, `AuthorTypeCodec` defines text conversion and + validation, `#[note_codec]` registers each custom type, and `export_codecs!` exports the + registered codecs as a component. Add this package-level metadata to `miden-project.toml` to + enable the codec build; `path` is relative to that manifest: + + ```toml + [package.metadata.note-codec-crate] + path = "../my-note-codec" + ``` +- Added the dependency-free `miden-note-codec-wit` crate as the canonical source for the note + codec component WIT contract. - Added typed host note-storage bindings through the new `miden-note-bindings` macros. Bindings can load a built note project or an exact `.masp`, generate native Rust storage types, and convert typed values to and from note storage. Its facade supplies all generated runtime dependencies, @@ -24,6 +36,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and can include nested types declared with `#[export_type]` before the note struct. Unit structs emit no schema. +### Fixed + +- Note storage schema handling now rejects conflicting `#[export_type]` registrations and local + types that only collide by name with SDK core types, resolves schema types through a bounded, + memoized graph, uses one canonical standard-leaf set across consumers, and caps untrusted author + codec components before compilation and during table allocation. +- Note package macros now select artifacts by canonical package identity and support shared Cargo + target directories. The note codec macros support renamed facade dependencies, reject a second + distinct schema in one crate, and report `export_codecs!` calls that appear before all codec + declarations. +- `adv_load_preimage` no longer truncates huge word counts into an undersized buffer on wasm32 + (a potential guest heap overflow); it now traps for counts of `2^30` words or more, whose felt + total cannot be represented in the 32-bit address space #1291 + ### Migration and breaking changes - `#[note]` storage types now require named-field or unit structs. Tuple structs no longer compile, diff --git a/sdk/base-macros/Cargo.toml b/sdk/base-macros/Cargo.toml index 79690f6747..3ff4fae567 100644 --- a/sdk/base-macros/Cargo.toml +++ b/sdk/base-macros/Cargo.toml @@ -44,6 +44,7 @@ miden-protocol = { workspace = true, features = ["std"] } miden-field.workspace = true miden-field-repr.workspace = true midenc-expect-test.workspace = true +tempfile.workspace = true wit-component.workspace = true [package.metadata.docs.rs] diff --git a/sdk/base-macros/src/export_type.rs b/sdk/base-macros/src/export_type.rs index 70f2a9fc6e..64379da583 100644 --- a/sdk/base-macros/src/export_type.rs +++ b/sdk/base-macros/src/export_type.rs @@ -2,7 +2,10 @@ use proc_macro::TokenStream; use quote::quote; use syn::{Item, parse_macro_input}; -use crate::types::{exported_type_from_enum, exported_type_from_struct, register_export_type}; +use crate::types::{ + exported_type_from_enum, exported_type_from_struct, register_export_type, + sdk_core_type_identity_guards, +}; pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { if !attr.is_empty() { @@ -20,8 +23,10 @@ pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { Item::Struct(item_struct) => { let span = item_struct.ident.span(); match exported_type_from_struct(&item_struct) { - Ok(def) => match register_export_type(def, span) { - Ok(()) => quote! { #item_struct }.into(), + Ok(def) => match sdk_core_type_identity_guards(&def, span) + .and_then(|guards| register_export_type(def, span).map(|()| guards)) + { + Ok(guards) => quote! { #item_struct #guards }.into(), Err(err) => err.to_compile_error().into(), }, Err(err) => err.to_compile_error().into(), @@ -30,8 +35,10 @@ pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { Item::Enum(item_enum) => { let span = item_enum.ident.span(); match exported_type_from_enum(&item_enum) { - Ok(def) => match register_export_type(def, span) { - Ok(()) => quote! { #item_enum }.into(), + Ok(def) => match sdk_core_type_identity_guards(&def, span) + .and_then(|guards| register_export_type(def, span).map(|()| guards)) + { + Ok(guards) => quote! { #item_enum #guards }.into(), Err(err) => err.to_compile_error().into(), }, Err(err) => err.to_compile_error().into(), diff --git a/sdk/base-macros/src/note_schema.rs b/sdk/base-macros/src/note_schema.rs index 858e6114d2..c28042a004 100644 --- a/sdk/base-macros/src/note_schema.rs +++ b/sdk/base-macros/src/note_schema.rs @@ -14,7 +14,7 @@ use crate::{ manifest_paths::SDK_WIT_SOURCE, types::{ ExportedField, ExportedTypeDef, ExportedTypeKind, TypeRef, doc_comments, - map_type_to_type_ref, registered_export_types, + map_type_to_type_ref, registered_export_types, sdk_core_type_identity_guards, }, util::NOTE_NAMED_FIELDS_ERROR, wit_builder::{WitBody, WitBuilder}, @@ -52,6 +52,11 @@ pub(crate) fn expand_note_storage_schema( ®istry, )?; validate_rendered_note_storage_schema(&rendered)?; + let identity_guards = rendered + .definitions + .iter() + .map(|definition| sdk_core_type_identity_guards(definition, rendered.span)) + .collect::, _>>()?; let mut bytes = rendered.source.into_bytes(); let padded_len = bytes.len().div_ceil(16) * 16; @@ -61,6 +66,8 @@ pub(crate) fn expand_note_storage_schema( let encoded_bytes = Literal::byte_string(&bytes); Ok(quote! { + #(#identity_guards)* + // Mach-O limits section names to 16 bytes. Wasm uses the canonical section name below. #[cfg_attr(target_os = "macos", unsafe(link_section = "rodata,miden_note_schem"))] #[cfg_attr( diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index 5542397a6a..fa22d840e8 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -5,8 +5,9 @@ use std::{ static EXPORTED_TYPES: OnceLock>> = OnceLock::new(); -use heck::ToKebabCase; -use proc_macro2::Span; +use heck::{ToKebabCase, ToUpperCamelCase}; +use proc_macro2::{Span, TokenStream}; +use quote::quote_spanned; use syn::{Attribute, ItemStruct, Type, spanned::Spanned}; use wit_bindgen_core::wit_parser::Type as WitType; @@ -102,17 +103,210 @@ pub(crate) enum StorageFieldType { StorageValue, } -pub(crate) fn register_export_type(def: ExportedTypeDef, _span: Span) -> Result<(), syn::Error> { +/// Registers one exported type while preserving the first definition seen by the macro process. +pub(crate) fn register_export_type(def: ExportedTypeDef, span: Span) -> Result<(), syn::Error> { let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(Vec::new())); let mut registry = registry.lock().expect("mutex poisoned"); - if let Some(existing) = registry.iter_mut().find(|existing| existing.wit_name == def.wit_name) { - *existing = def; - return Ok(()); + register_export_type_in(&mut registry, def, span) +} + +/// Applies exported-type identity rules to one registry snapshot. +fn register_export_type_in( + registry: &mut Vec, + def: ExportedTypeDef, + span: Span, +) -> Result<(), syn::Error> { + if let Some(existing) = registry.iter().find(|existing| existing.wit_name == def.wit_name) { + if existing.rust_name == def.rust_name && exported_type_shapes_match(existing, &def) { + // rust-analyzer can expand the same attribute more than once in one macro process. + return Ok(()); + } + + let identity = if existing.rust_name == def.rust_name { + format!("Rust type `{}`", def.rust_name) + } else { + format!("Rust types `{}` and `{}` both map to", existing.rust_name, def.rust_name) + }; + return Err(syn::Error::new( + span, + format!( + "conflicting #[export_type] registration: {identity} WIT type `{}` with different \ + identity or shape; the earlier registration is `{}`, while this registration is \ + `{}`. Rename one type or make both registrations structurally identical", + def.wit_name, + describe_exported_type_shape(existing), + describe_exported_type_shape(&def), + ), + )); } registry.push(def); Ok(()) } +/// Returns true when two definitions render the same structural WIT type. +fn exported_type_shapes_match(left: &ExportedTypeDef, right: &ExportedTypeDef) -> bool { + match (&left.kind, &right.kind) { + ( + ExportedTypeKind::Record { + fields: left_fields, + }, + ExportedTypeKind::Record { + fields: right_fields, + }, + ) => { + left_fields.len() == right_fields.len() + && left_fields.iter().zip(right_fields).all(|(left, right)| { + left.name.to_kebab_case() == right.name.to_kebab_case() + && type_ref_shapes_match(&left.ty, &right.ty) + }) + } + ( + ExportedTypeKind::Variant { + variants: left_variants, + }, + ExportedTypeKind::Variant { + variants: right_variants, + }, + ) => { + left_variants.len() == right_variants.len() + && left_variants.iter().zip(right_variants).all(|(left, right)| { + left.wit_name == right.wit_name + && match (&left.payload, &right.payload) { + (Some(left), Some(right)) => type_ref_shapes_match(left, right), + (None, None) => true, + _ => false, + } + }) + } + _ => false, + } +} + +/// Returns true when two references resolve to the same WIT identity and generic shape. +fn type_ref_shapes_match(left: &TypeRef, right: &TypeRef) -> bool { + left.wit_name == right.wit_name + && left.is_custom == right.is_custom + && left.dependencies.len() == right.dependencies.len() + && left + .dependencies + .iter() + .zip(&right.dependencies) + .all(|(left, right)| type_ref_shapes_match(left, right)) +} + +/// Formats one exported definition for a conflicting-registration diagnostic. +fn describe_exported_type_shape(def: &ExportedTypeDef) -> String { + match &def.kind { + ExportedTypeKind::Record { fields } => format!( + "record {} {{ {} }}", + def.wit_name, + fields + .iter() + .map(|field| format!("{}: {}", field.name.to_kebab_case(), field.ty.wit_name)) + .collect::>() + .join(", ") + ), + ExportedTypeKind::Variant { variants } => format!( + "variant {} {{ {} }}", + def.wit_name, + variants + .iter() + .map(|variant| match &variant.payload { + Some(payload) => format!("{}({})", variant.wit_name, payload.wit_name), + None => variant.wit_name.clone(), + }) + .collect::>() + .join(", ") + ), + } +} + +/// Emits nominal identity checks for references classified as SDK core types by their Rust name. +/// +/// Procedural macros cannot resolve a bare identifier such as `Word`. The generated check permits +/// a genuine `miden::Word` import but rejects a local same-named type unless it was registered with +/// `#[export_type]`, preventing the emitted WIT shape from drifting from the encoded Rust type. +pub(crate) fn sdk_core_type_identity_guards( + definition: &ExportedTypeDef, + span: Span, +) -> Result { + let mut guarded = HashSet::new(); + let mut guards = TokenStream::new(); + visit_exported_type_refs(definition, &mut |type_ref| { + collect_sdk_core_type_identity_guard(type_ref, span, &mut guarded, &mut guards) + })?; + Ok(guards) +} + +/// Visits every type reference contained in one exported definition. +fn visit_exported_type_refs( + definition: &ExportedTypeDef, + visitor: &mut impl FnMut(&TypeRef) -> Result<(), syn::Error>, +) -> Result<(), syn::Error> { + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + visit_type_ref_dependencies(&field.ty, visitor)?; + } + } + ExportedTypeKind::Variant { variants } => { + for payload in variants.iter().filter_map(|variant| variant.payload.as_ref()) { + visit_type_ref_dependencies(payload, visitor)?; + } + } + } + Ok(()) +} + +/// Visits one type reference and every nested generic dependency. +fn visit_type_ref_dependencies( + type_ref: &TypeRef, + visitor: &mut impl FnMut(&TypeRef) -> Result<(), syn::Error>, +) -> Result<(), syn::Error> { + visitor(type_ref)?; + for dependency in &type_ref.dependencies { + visit_type_ref_dependencies(dependency, visitor)?; + } + Ok(()) +} + +/// Appends one nominal SDK identity check when a core-type path has not already been guarded. +fn collect_sdk_core_type_identity_guard( + type_ref: &TypeRef, + span: Span, + guarded: &mut HashSet<(String, String)>, + guards: &mut TokenStream, +) -> Result<(), syn::Error> { + if !type_ref.requires_core_type_import() { + return Ok(()); + } + + let rust_path = type_ref.path.join("::"); + if !guarded.insert((rust_path.clone(), type_ref.wit_name.clone())) { + return Ok(()); + } + let rust_path = syn::parse_str::(&rust_path).map_err(|error| { + syn::Error::new( + span, + format!("failed to reconstruct SDK core-type path for an identity check: {error}"), + ) + })?; + let sdk_ident = syn::Ident::new(&type_ref.wit_name.to_upper_camel_case(), span); + guards.extend(quote_spanned! {span=> + const _: fn() = || { + fn __miden_core_type_name_collision_use_sdk_type_or_add_export_type( + _: ::core::marker::PhantomData, + _: ::core::marker::PhantomData, + ) {} + __miden_core_type_name_collision_use_sdk_type_or_add_export_type( + ::core::marker::PhantomData::<#rust_path>, + ::core::marker::PhantomData::<::miden::#sdk_ident>, + ); + }; + }); + Ok(()) +} + pub(crate) fn registered_export_types() -> Vec { let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(Vec::new())); registry.lock().expect("mutex poisoned").clone() diff --git a/sdk/base-macros/src/types/tests.rs b/sdk/base-macros/src/types/tests.rs index ac43fc0c36..e8e4881278 100644 --- a/sdk/base-macros/src/types/tests.rs +++ b/sdk/base-macros/src/types/tests.rs @@ -1,4 +1,9 @@ -use std::collections::HashSet; +use std::{ + collections::HashSet, + env, + io::Write, + process::{Command, Output, Stdio}, +}; use syn::parse_quote; @@ -383,3 +388,153 @@ fn forward_reference_between_export_types_is_allowed() { panic!("expected record kind"); } } + +#[test] +fn rejects_same_name_different_shape_export_type_registration() { + let first: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u64, + } + }; + let second: syn::ItemStruct = parse_quote! { + struct Fee { + amount: Word, + } + }; + let mut registry = Vec::new(); + register_export_type_in( + &mut registry, + exported_type_from_struct(&first).unwrap(), + Span::call_site(), + ) + .unwrap(); + + let error = register_export_type_in( + &mut registry, + exported_type_from_struct(&second).unwrap(), + Span::call_site(), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("conflicting #[export_type] registration")); + assert!(error.contains("record fee { amount: u64 }")); + assert!(error.contains("record fee { amount: word }")); + assert!(error.contains("Rename one type or make both registrations structurally identical")); + assert_eq!(registry.len(), 1); +} + +#[test] +fn allows_same_shape_export_type_reregistration() { + let first: syn::ItemStruct = parse_quote! { + /// Documentation from rustc's expansion. + struct Fee { + amount: u64, + } + }; + let second: syn::ItemStruct = parse_quote! { + /// Documentation from rust-analyzer's expansion. + struct Fee { + amount: u64, + } + }; + let mut registry = Vec::new(); + + register_export_type_in( + &mut registry, + exported_type_from_struct(&first).unwrap(), + Span::call_site(), + ) + .unwrap(); + register_export_type_in( + &mut registry, + exported_type_from_struct(&second).unwrap(), + Span::call_site(), + ) + .unwrap(); + + assert_eq!(registry.len(), 1); + assert_eq!(registry[0].docs, vec![" Documentation from rustc's expansion."]); +} + +#[test] +fn bare_core_type_name_collision_fails_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + value: Word, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = sdk_core_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +extern crate self as miden; +pub struct Word; +mod user {{ + pub struct Word; + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a local `Word` must fail the SDK identity guard"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("__miden_core_type_name_collision_use_sdk_type_or_add_export_type"), + "identity diagnostic is not actionable:\n{stderr}" + ); +} + +#[test] +fn bare_sdk_core_type_import_passes_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + value: Word, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = sdk_core_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +extern crate self as miden; +pub struct Word; +mod user {{ + use crate::Word; + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + output.status.success(), + "a genuine SDK import must pass the identity guard:\n{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +/// Compiles one standalone Rust source string for nominal identity-guard tests. +fn compile_rust_source(source: &str) -> Output { + let output_dir = tempfile::tempdir().expect("failed to create rustc output directory"); + let output_path = output_dir.path().join("identity_guard.rmeta"); + let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + let mut child = Command::new(rustc) + .args(["--crate-name", "identity_guard", "--edition=2024", "--emit=metadata", "-o"]) + .arg(output_path) + .arg("-") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to start rustc for an identity-guard test"); + child + .stdin + .take() + .expect("rustc stdin must be piped") + .write_all(source.as_bytes()) + .expect("failed to write the identity-guard source"); + child.wait_with_output().expect("failed to wait for rustc") +} diff --git a/sdk/note-bindings/macros/Cargo.toml b/sdk/note-bindings/macros/Cargo.toml index cb5bca123e..7b9dddbc97 100644 --- a/sdk/note-bindings/macros/Cargo.toml +++ b/sdk/note-bindings/macros/Cargo.toml @@ -23,7 +23,7 @@ miden-note-schema-codegen.workspace = true proc-macro2.workspace = true proc-macro-crate = { workspace = true } quote.workspace = true -syn = { workspace = true, features = ["visit-mut"] } +syn.workspace = true [dev-dependencies] midenc-expect-test.workspace = true diff --git a/sdk/note-bindings/macros/expected/custom.rs b/sdk/note-bindings/macros/expected/custom.rs index 7b76ed456a..fc127006f9 100644 --- a/sdk/note-bindings/macros/expected/custom.rs +++ b/sdk/note-bindings/macros/expected/custom.rs @@ -176,7 +176,7 @@ mod __miden_note_bindings_a3280bdaca3ec21e { ::miden_note_bindings::__private::miden_note_schema::Error::new( format!( "failed to decode {} from note storage: {error}", - stringify!(::miden_field::Felt), + stringify!(::miden_note_bindings::__private::miden_field::Felt), ), ) }) @@ -209,7 +209,7 @@ mod __miden_note_bindings_a3280bdaca3ec21e { ::miden_note_bindings::__private::miden_note_schema::Error::new( format!( "failed to decode {} from note storage: {error}", - stringify!(::miden_field::Word), + stringify!(::miden_note_bindings::__private::miden_field::Word), ), ) }) diff --git a/sdk/note-bindings/macros/expected/p2id.rs b/sdk/note-bindings/macros/expected/p2id.rs index bdb4fcac7f..fb1d75cbbd 100644 --- a/sdk/note-bindings/macros/expected/p2id.rs +++ b/sdk/note-bindings/macros/expected/p2id.rs @@ -176,7 +176,7 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { ::miden_note_bindings::__private::miden_note_schema::Error::new( format!( "failed to decode {} from note storage: {error}", - stringify!(::miden_field::Felt), + stringify!(::miden_note_bindings::__private::miden_field::Felt), ), ) }) @@ -209,7 +209,7 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { ::miden_note_bindings::__private::miden_note_schema::Error::new( format!( "failed to decode {} from note storage: {error}", - stringify!(::miden_field::Word), + stringify!(::miden_note_bindings::__private::miden_field::Word), ), ) }) diff --git a/sdk/note-bindings/macros/src/lib.rs b/sdk/note-bindings/macros/src/lib.rs index 5375b237f6..cbf80c6810 100644 --- a/sdk/note-bindings/macros/src/lib.rs +++ b/sdk/note-bindings/macros/src/lib.rs @@ -5,16 +5,14 @@ extern crate proc_macro; use miden_note_schema::{NotePackageArtifact, NotePackageResolver, NoteStorageSchema}; -use miden_note_schema_codegen::generate_host_types; +use miden_note_schema_codegen::{RuntimePaths, generate_host_types}; use proc_macro::TokenStream; use proc_macro_crate::{FoundCrate, crate_name}; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote}; -use syn::{ - LitStr, Token, parse::Parser, parse_macro_input, punctuated::Punctuated, visit_mut::VisitMut, -}; +use syn::{LitStr, parse_macro_input}; -/// Generates typed bindings from the freshest package built by a Miden project. +/// Generates typed bindings from the package built by a Miden project. /// /// The project path is relative to `CARGO_MANIFEST_DIR`. Build the note project with /// `cargo miden build` before compiling the consumer. @@ -74,6 +72,8 @@ fn expand_package_artifact( Ok(quote! { #[doc(hidden)] const _: &[u8] = include_bytes!(#tracked_path); + #[doc(hidden)] + const _: Option<&str> = option_env!("MIDENC_PACKAGE_CACHE"); #bindings }) } @@ -91,10 +91,11 @@ fn expand_schema( span: Span, scope_key: &str, ) -> syn::Result { - let generated = - generate_host_types(schema).map_err(|error| syn::Error::new(span, error.to_string()))?; let facade_path = binding_facade(); - let type_tokens = rewrite_runtime_paths(generated.tokens().clone(), &facade_path)?; + let runtime_paths = RuntimePaths::through_facade(quote!(#facade_path)); + let generated = generate_host_types(schema, &runtime_paths) + .map_err(|error| syn::Error::new(span, error.to_string()))?; + let type_tokens = generated.tokens(); let root_ident = generated.root_ident(); let type_idents = generated.type_idents(); let wit_text = schema.wit_text(); @@ -223,91 +224,6 @@ fn binding_facade() -> syn::Path { } } -/// Rewrites shared generated paths through the bindings facade. -fn rewrite_runtime_paths(tokens: TokenStream2, facade: &syn::Path) -> syn::Result { - let mut file = syn::parse2::(tokens)?; - RuntimePathRewriter { facade }.visit_file_mut(&mut file); - Ok(quote!(#file)) -} - -/// Rewrites runtime crate paths in shared host-profile code generation. -struct RuntimePathRewriter<'a> { - facade: &'a syn::Path, -} - -impl VisitMut for RuntimePathRewriter<'_> { - fn visit_item_struct_mut(&mut self, item: &mut syn::ItemStruct) { - add_felt_repr_crate_path(&mut item.attrs, self.facade); - syn::visit_mut::visit_item_struct_mut(self, item); - } - - fn visit_item_enum_mut(&mut self, item: &mut syn::ItemEnum) { - add_felt_repr_crate_path(&mut item.attrs, self.facade); - syn::visit_mut::visit_item_enum_mut(self, item); - } - - fn visit_attribute_mut(&mut self, attribute: &mut syn::Attribute) { - if attribute.path().is_ident("derive") { - let parser = Punctuated::::parse_terminated; - let mut paths = parser - .parse2(attribute.meta.require_list().expect("derive is a list").tokens.clone()) - .expect("generated derive paths must parse"); - for path in &mut paths { - self.visit_path_mut(path); - } - attribute.meta = syn::parse_quote!(derive(#paths)); - return; - } - syn::visit_mut::visit_attribute_mut(self, attribute); - } - - fn visit_path_mut(&mut self, path: &mut syn::Path) { - let Some(first) = path.segments.first() else { - return; - }; - if path.leading_colon.is_none() - || !matches!( - first.ident.to_string().as_str(), - "miden_field" | "miden_field_repr" | "miden_note_schema" | "miden_protocol" - ) - { - syn::visit_mut::visit_path_mut(self, path); - return; - } - - let crate_name = first.ident.clone(); - let tail = path.segments.iter().skip(1).cloned().collect::>(); - let facade = self.facade; - let mut rewritten: syn::Path = syn::parse_quote!(#facade::__private::#crate_name); - rewritten.segments.extend(tail); - *path = rewritten; - } -} - -/// Selects the facade's felt representation runtime for generated derives. -fn add_felt_repr_crate_path(attributes: &mut Vec, facade: &syn::Path) { - let has_felt_repr_derive = attributes.iter().any(|attribute| { - if !attribute.path().is_ident("derive") { - return false; - } - let parser = Punctuated::::parse_terminated; - parser - .parse2(attribute.meta.require_list().expect("derive is a list").tokens.clone()) - .expect("generated derive paths must parse") - .iter() - .any(|path| { - path.segments.last().is_some_and(|segment| { - matches!(segment.ident.to_string().as_str(), "ToFeltRepr" | "FromFeltRepr") - }) - }) - }); - if has_felt_repr_derive { - let path = format!("{}::__private::miden_field_repr", quote!(#facade)).replace(' ', ""); - let path = LitStr::new(&path, Span::call_site()); - attributes.push(syn::parse_quote!(#[felt_repr(crate_path = #path)])); - } -} - /// Returns a deterministic scope suffix for one macro input. fn stable_hash(value: &str) -> u64 { value.bytes().fold(0xcbf2_9ce4_8422_2325, |hash, byte| { diff --git a/sdk/note-codec/Cargo.toml b/sdk/note-codec/Cargo.toml index 520cef1a01..533f207d6b 100644 --- a/sdk/note-codec/Cargo.toml +++ b/sdk/note-codec/Cargo.toml @@ -20,6 +20,7 @@ doctest = false miden-field.workspace = true miden-field-repr.workspace = true miden-note-codec-macros.workspace = true +miden-note-codec-wit.workspace = true miden-protocol.workspace = true wit-bindgen = { workspace = true } diff --git a/sdk/note-codec/macros/Cargo.toml b/sdk/note-codec/macros/Cargo.toml index bd6829f4df..59635db4a3 100644 --- a/sdk/note-codec/macros/Cargo.toml +++ b/sdk/note-codec/macros/Cargo.toml @@ -19,12 +19,13 @@ doctest = false [dependencies] heck.workspace = true +miden-note-codec-wit.workspace = true miden-note-schema.workspace = true miden-note-schema-codegen.workspace = true proc-macro2.workspace = true proc-macro-crate = { workspace = true } quote.workspace = true -syn = { workspace = true, features = ["visit-mut"] } +syn.workspace = true [dev-dependencies] prettyplease = { workspace = true } diff --git a/sdk/note-codec/macros/src/expand.rs b/sdk/note-codec/macros/src/expand.rs index eaa5e79c4b..bca95ff7b9 100644 --- a/sdk/note-codec/macros/src/expand.rs +++ b/sdk/note-codec/macros/src/expand.rs @@ -1,17 +1,15 @@ //! Macro expansion for generated author types and component dispatch. +use miden_note_codec_wit::NOTE_CODEC_WIT; use miden_note_schema::{NotePackageArtifact, NotePackageResolver, NoteStorageSchema}; -use miden_note_schema_codegen::generate_host_types; +use miden_note_schema_codegen::{RuntimePaths, generate_host_types}; use proc_macro_crate::{FoundCrate, crate_name}; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote}; -use syn::{ItemImpl, LitStr, Type, visit_mut::VisitMut}; +use syn::{ItemImpl, LitStr, Type}; use crate::registry::{register_codec, register_schema, registered_codecs}; -/// The component world embedded in generated export glue. -const NOTE_CODEC_WIT: &str = include_str!("../wit/note-codec.wit"); - /// Expands a project-relative type generation request. pub(crate) fn from_project(input: &LitStr) -> syn::Result { let artifact = NotePackageResolver::new("miden-note-codec") @@ -42,41 +40,20 @@ fn expand_package_artifact(artifact: &NotePackageArtifact, span: Span) -> syn::R Ok(quote! { #[doc(hidden)] const _: &[u8] = include_bytes!(#tracked_path); + #[doc(hidden)] + const _: Option<&str> = option_env!("MIDENC_PACKAGE_CACHE"); #types }) } /// Generates host-profile types and records their WIT identities. fn expand_schema(schema: &NoteStorageSchema, span: Span) -> syn::Result { - let generated = - generate_host_types(schema).map_err(|error| syn::Error::new(span, error.to_string()))?; + let facade = note_codec_facade(); + let runtime = RuntimePaths::through_facade(quote!(#facade)); + let generated = generate_host_types(schema, &runtime) + .map_err(|error| syn::Error::new(span, error.to_string()))?; register_schema(schema, span)?; - let generated = rewrite_runtime_paths(generated.tokens().clone())?; - let felt_repr_alias = felt_repr_alias(); - Ok(quote! { - #felt_repr_alias - - #generated - }) -} - -/// Provides the fixed crate name used by the felt representation derives. -fn felt_repr_alias() -> TokenStream { - match crate_name("miden-field-repr") { - Ok(FoundCrate::Itself) => TokenStream::new(), - Ok(FoundCrate::Name(name)) if name == "miden_field_repr" => TokenStream::new(), - Ok(FoundCrate::Name(name)) => { - let name = syn::Ident::new(&name, Span::call_site()); - quote! { - #[doc(hidden)] - extern crate #name as miden_field_repr; - } - } - Err(_) => quote! { - #[doc(hidden)] - extern crate miden_note_codec as miden_field_repr; - }, - } + Ok(generated.tokens().clone()) } /// Validates and records one marked author codec implementation. @@ -114,6 +91,7 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { return Err(syn::Error::new_spanned(input, "export_codecs! does not accept arguments")); } let codecs = registered_codecs(Span::call_site())?; + let facade = note_codec_facade(); let registrations = codecs .iter() .map(|codec| { @@ -131,11 +109,11 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { let parse_arms = registrations.iter().map(|(fqn, ty)| { quote! { #fqn => { - let value = <#ty as ::miden_note_codec::AuthorTypeCodec>::parse(value)?; + let value = <#ty as #facade::AuthorTypeCodec>::parse(value)?; let mut felts = Vec::new(); <#ty as __MidenNoteEncode>::__write_note_felts( &value, - &mut ::miden_note_codec::__private::miden_field_repr::FeltWriter::new( + &mut #facade::__private::miden_field_repr::FeltWriter::new( &mut felts, ), ) @@ -143,16 +121,16 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { "failed to encode codec type `{}`: {error}", #fqn, ))?; - Ok(::miden_note_codec::felts_to_u64(&felts)) + Ok(#facade::felts_to_u64(&felts)) } } }); let display_arms = registrations.iter().map(|(fqn, ty)| { quote! { #fqn => { - let felts = ::miden_note_codec::felts_from_u64(value)?; + let felts = #facade::felts_from_u64(value)?; let mut reader = - ::miden_note_codec::__private::miden_field_repr::FeltReader::new(&felts); + #facade::__private::miden_field_repr::FeltReader::new(&felts); let value = <#ty as __MidenNoteDecode>::__read_note_felts(&mut reader) .map_err(|error| format!( "failed to decode codec type `{}`: {error}", @@ -162,16 +140,16 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { "codec type `{}` has trailing felt data: {error}", #fqn, ))?; - Ok(<#ty as ::miden_note_codec::AuthorTypeCodec>::display(&value)) + Ok(<#ty as #facade::AuthorTypeCodec>::display(&value)) } } }); let validate_arms = registrations.iter().map(|(fqn, ty)| { quote! { #fqn => { - let felts = ::miden_note_codec::felts_from_u64(value)?; + let felts = #facade::felts_from_u64(value)?; let mut reader = - ::miden_note_codec::__private::miden_field_repr::FeltReader::new(&felts); + #facade::__private::miden_field_repr::FeltReader::new(&felts); let value = <#ty as __MidenNoteDecode>::__read_note_felts(&mut reader) .map_err(|error| format!( "failed to decode codec type `{}`: {error}", @@ -181,11 +159,15 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { "codec type `{}` has trailing felt data: {error}", #fqn, ))?; - <#ty as ::miden_note_codec::AuthorTypeCodec>::validate(&value) + <#ty as #facade::AuthorTypeCodec>::validate(&value) } } }); let wit = NOTE_CODEC_WIT; + let wit_runtime_path = LitStr::new( + &format!("{facade}::__private::wit_bindgen::rt", facade = quote!(#facade)).replace(' ', ""), + Span::call_site(), + ); Ok(quote! { /// Native dispatch used by the note codec component adapter. @@ -231,10 +213,10 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { #[cfg(target_family = "wasm")] mod __miden_note_codec_component { - ::miden_note_codec::__private::wit_bindgen::generate!({ + #facade::__private::wit_bindgen::generate!({ inline: #wit, world: "note-codec", - runtime_path: "::miden_note_codec::__private::wit_bindgen::rt", + runtime_path: #wit_runtime_path, }); struct Component; @@ -274,37 +256,15 @@ fn rust_type_name(ty: &Type) -> syn::Result { .ok_or_else(|| syn::Error::new_spanned(ty, "#[note_codec] type path is empty")) } -/// Rewrites generated runtime paths through `miden-note-codec` re-exports. -fn rewrite_runtime_paths(tokens: TokenStream) -> syn::Result { - let mut file = syn::parse2::(tokens)?; - RuntimePathRewriter.visit_file_mut(&mut file); - Ok(quote!(#file)) -} - -/// Rewrites the four runtime crates referenced by shared host-profile codegen. -struct RuntimePathRewriter; - -impl VisitMut for RuntimePathRewriter { - fn visit_path_mut(&mut self, path: &mut syn::Path) { - let Some(first) = path.segments.first() else { - return; - }; - if path.leading_colon.is_none() - || !matches!( - first.ident.to_string().as_str(), - "miden_field" | "miden_field_repr" | "miden_note_schema" | "miden_protocol" - ) - { - syn::visit_mut::visit_path_mut(self, path); - return; +/// Resolves the note codec facade path in the consuming crate. +fn note_codec_facade() -> syn::Path { + match crate_name("miden-note-codec") { + Ok(FoundCrate::Itself) => syn::parse_quote!(crate), + Ok(FoundCrate::Name(name)) => { + let ident = syn::Ident::new(&name, Span::call_site()); + syn::parse_quote!(::#ident) } - - let crate_name = first.ident.clone(); - let tail = path.segments.iter().skip(1).cloned().collect::>(); - let mut rewritten: syn::Path = - syn::parse_quote!(::miden_note_codec::__private::#crate_name); - rewritten.segments.extend(tail); - *path = rewritten; + Err(_) => syn::parse_quote!(::miden_note_codec), } } diff --git a/sdk/note-codec/macros/src/lib.rs b/sdk/note-codec/macros/src/lib.rs index 5aeb17cc05..6d223e45a2 100644 --- a/sdk/note-codec/macros/src/lib.rs +++ b/sdk/note-codec/macros/src/lib.rs @@ -10,7 +10,7 @@ mod registry; use proc_macro::TokenStream; use syn::{ItemImpl, LitStr, parse_macro_input}; -/// Generates host-profile note types from the freshest package built by a Miden project. +/// Generates host-profile note types from the package built by a Miden project. /// /// The project path is relative to `CARGO_MANIFEST_DIR`. Build the note project with /// `cargo miden build` before compiling the codec crate. @@ -52,7 +52,10 @@ pub fn note_codec(args: TokenStream, input: TokenStream) -> TokenStream { .into() } -/// Exports all marked note codecs through the `miden:note-codec` component world. +/// Exports all codecs marked earlier in the crate through the `miden:note-codec` component world. +/// +/// Place this macro after the generated schema types and every `#[note_codec]` implementation. +/// Procedural macros register codecs in declaration order. #[proc_macro] pub fn export_codecs(input: TokenStream) -> TokenStream { expand::export_codecs(input.into()) diff --git a/sdk/note-codec/macros/src/registry.rs b/sdk/note-codec/macros/src/registry.rs index 29eb2359a7..cb360632de 100644 --- a/sdk/note-codec/macros/src/registry.rs +++ b/sdk/note-codec/macros/src/registry.rs @@ -6,10 +6,7 @@ use std::{ }; use heck::ToUpperCamelCase; -use miden_note_schema::{ - ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, FELT_FQN, NoteStorageSchema, SchemaCase, SchemaType, - SchemaTypeKind, WORD_FQN, -}; +use miden_note_schema::{NoteStorageSchema, SchemaCase, SchemaType, SchemaTypeKind}; use proc_macro2::Span; /// One marked author codec. @@ -22,8 +19,8 @@ pub(crate) struct CodecRegistration { /// Schema types and marked codecs registered by earlier macro expansions. #[derive(Default)] struct Registry { - schemas: BTreeSet, - types: BTreeMap>, + schema: Option, + types: BTreeMap, codecs: BTreeMap, } @@ -42,12 +39,19 @@ pub(crate) fn register_schema(schema: &NoteStorageSchema, span: Span) -> syn::Re let mut registry = registry() .lock() .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; - if !registry.schemas.insert(schema.wit_text().to_owned()) { - return Ok(()); - } - for (rust_name, fqn) in bindings { - registry.types.entry(rust_name).or_default().insert(fqn); + match registry.schema.as_deref() { + Some(existing) if existing == schema.wit_text() => return Ok(()), + Some(_) => { + return Err(syn::Error::new( + span, + "miden-note-codec supports one note schema per crate; remove the second distinct \ + from_project!, from_package!, or from_wit_text! invocation", + )); + } + None => {} } + registry.schema = Some(schema.wit_text().to_owned()); + registry.types = bindings; Ok(()) } @@ -56,7 +60,7 @@ pub(crate) fn register_codec(rust_name: &str, rust_type: String, span: Span) -> let mut registry = registry() .lock() .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; - let fqns = registry.types.get(rust_name).ok_or_else(|| { + let fqn = registry.types.get(rust_name).ok_or_else(|| { syn::Error::new( span, format!( @@ -65,16 +69,7 @@ pub(crate) fn register_codec(rust_name: &str, rust_type: String, span: Span) -> ), ) })?; - if fqns.len() != 1 { - return Err(syn::Error::new( - span, - format!( - "generated type `{rust_name}` is ambiguous across note schemas: {}", - fqns.iter().cloned().collect::>().join(", ") - ), - )); - } - let fqn = fqns.first().expect("one FQN was checked above").clone(); + let fqn = fqn.clone(); let registration = CodecRegistration { fqn: fqn.clone(), rust_type, @@ -96,6 +91,14 @@ pub(crate) fn registered_codecs(span: Span) -> syn::Result, bindings: &mut BTreeMap, ) -> syn::Result<()> { - if is_protocol_leaf(ty) { + if ty.standard_leaf().is_some() { return Ok(()); } if matches!(ty.kind(), SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_)) { @@ -141,11 +144,6 @@ fn collect_type_bindings( Ok(()) } -/// Returns true for protocol leaves mapped to existing host types. -fn is_protocol_leaf(ty: &SchemaType) -> bool { - matches!(ty.fqn(), Some(FELT_FQN | WORD_FQN | ACCOUNT_ID_FQN | ASSET_AMOUNT_FQN)) -} - #[cfg(test)] pub(crate) fn reset_for_tests() { if let Some(registry) = REGISTRY.get() { diff --git a/sdk/note-codec/macros/src/tests.rs b/sdk/note-codec/macros/src/tests.rs index 40415066c8..c318ce56ab 100644 --- a/sdk/note-codec/macros/src/tests.rs +++ b/sdk/note-codec/macros/src/tests.rs @@ -1,11 +1,20 @@ //! Tests for codec macro registration and expansion. +use std::sync::{Mutex, MutexGuard}; + use proc_macro2::Span; use quote::quote; use syn::{ItemImpl, LitStr}; use crate::{expand, registry::reset_for_tests}; +static REGISTRY_TEST_LOCK: Mutex<()> = Mutex::new(()); + +/// Serializes tests that exercise the process-global procedural-macro registry. +fn lock_registry() -> MutexGuard<'static, ()> { + REGISTRY_TEST_LOCK.lock().expect("registry test mutex is poisoned") +} + const SCHEMA: &str = r#" package example:codec-schema@1.0.0; @@ -23,8 +32,29 @@ interface note-storage { } "#; +const EMBEDDED_CORE_SCHEMA: &str = r#" +package example:embedded-core-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{digest}; + record embedded-core-note { value: digest } + type storage = embedded-core-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record word { a: felt, b: felt, c: felt, d: felt } + record digest { inner: word } + } +} +"#; + #[test] fn schema_and_codec_registration_generate_native_and_wasm_dispatch() { + let _guard = lock_registry(); reset_for_tests(); let schema = LitStr::new(SCHEMA, Span::call_site()); let generated = expand::from_wit_text(&schema).unwrap(); @@ -52,8 +82,69 @@ fn schema_and_codec_registration_generate_native_and_wasm_dispatch() { assert!(source.contains("pub struct CodecNote")); assert!(source.contains("pub struct Ratio")); assert!(source.contains("::miden_note_codec::__private::miden_field_repr")); + assert!(source.contains("felt_repr(crate_path")); + assert!(!source.contains("extern crate")); assert!(source.contains("example:codec-schema/note-storage@1.0.0.codec-note")); assert!(source.contains("example:codec-schema/note-storage@1.0.0.ratio")); assert!(source.contains("cfg(target_family = \"wasm\")")); assert!(source.contains("exports::miden::note_codec::codec::Guest")); } + +#[test] +fn a_crate_cannot_register_two_distinct_schemas() { + let _guard = lock_registry(); + reset_for_tests(); + let first = LitStr::new(SCHEMA, Span::call_site()); + expand::from_wit_text(&first).unwrap(); + + let second_schema = SCHEMA + .replace("example:codec-schema", "example:other-schema") + .replace("codec-note", "other-note"); + let second = LitStr::new(&second_schema, Span::call_site()); + let error = expand::from_wit_text(&second).unwrap_err().to_string(); + + assert!(error.contains("one note schema per crate")); + assert!(error.contains("second distinct")); +} + +#[test] +fn export_requires_earlier_codec_declarations() { + let _guard = lock_registry(); + reset_for_tests(); + let error = expand::export_codecs(quote!()).unwrap_err().to_string(); + + assert!(error.contains("found no registered codecs")); + assert!(error.contains("declaration order")); + assert!(error.contains("#[note_codec]")); +} + +#[test] +fn codec_registry_uses_canonical_standard_leaf_definition() { + let _guard = lock_registry(); + reset_for_tests(); + let schema = LitStr::new(EMBEDDED_CORE_SCHEMA, Span::call_site()); + let generated = expand::from_wit_text(&schema).unwrap(); + let digest_impl: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for Digest { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + expand::note_codec(quote!(), digest_impl).unwrap(); + + let word_impl: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for Word { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + let error = expand::note_codec(quote!(), word_impl).unwrap_err().to_string(); + + let source = prettyplease::unparse(&syn::parse2(quote!(#generated)).unwrap()); + assert!(source.contains("pub struct Digest")); + assert!(error.contains("not part of a registered note schema")); +} diff --git a/sdk/note-codec/src/lib.rs b/sdk/note-codec/src/lib.rs index a3197ccd8a..62e2ae1782 100644 --- a/sdk/note-codec/src/lib.rs +++ b/sdk/note-codec/src/lib.rs @@ -6,15 +6,14 @@ #![deny(missing_docs)] -pub use miden_field_repr::*; +use miden_field::Felt; +use miden_field_repr::{FeltReader, FromFeltRepr, ToFeltRepr}; #[doc(hidden)] pub use miden_note_codec_macros::from_wit_text; pub use miden_note_codec_macros::{export_codecs, from_package, from_project, note_codec}; +pub use miden_note_codec_wit::NOTE_CODEC_WIT; pub use miden_protocol::{account, asset}; -/// The WIT document implemented by generated note codec components. -pub const NOTE_CODEC_WIT: &str = include_str!("../wit/note-codec.wit"); - /// Parses, displays, and validates one author-defined note storage type. pub trait AuthorTypeCodec: Sized { /// Parses text into a typed value. diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index a1b2022b7e..2d4754ca9c 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -112,7 +112,7 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] -miden-note-codec = {{ path = {:?} }} +codec-facade = {{ package = "miden-note-codec", path = {:?} }} [workspace] "#, @@ -142,9 +142,9 @@ fn assert_command_succeeded(action: &str, output: &Output) { } const FIXTURE_SOURCE: &str = r##" -use miden_note_codec::AuthorTypeCodec; +use codec_facade::AuthorTypeCodec; -miden_note_codec::from_wit_text!(r#" +codec_facade::from_wit_text!(r#" package example:codec-schema@1.0.0; interface note-storage { @@ -161,7 +161,7 @@ interface note-storage { } "#); -#[miden_note_codec::note_codec] +#[codec_facade::note_codec] impl AuthorTypeCodec for Ratio { fn parse(value: &str) -> Result { let (numerator, denominator) = value @@ -186,5 +186,5 @@ impl AuthorTypeCodec for Ratio { } } -miden_note_codec::export_codecs!(); +codec_facade::export_codecs!(); "##; diff --git a/sdk/note-codec/wit-crate/Cargo.toml b/sdk/note-codec/wit-crate/Cargo.toml new file mode 100644 index 0000000000..401e48fcf6 --- /dev/null +++ b/sdk/note-codec/wit-crate/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "miden-note-codec-wit" +description = "Canonical WIT contract for Miden note codecs" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true + +[lib] +doctest = false diff --git a/sdk/note-codec/wit-crate/src/lib.rs b/sdk/note-codec/wit-crate/src/lib.rs new file mode 100644 index 0000000000..f0a72daf5e --- /dev/null +++ b/sdk/note-codec/wit-crate/src/lib.rs @@ -0,0 +1,8 @@ +//! Canonical WIT contract for Miden note codecs. + +#![deny(missing_docs)] +#![no_std] + +/// The WIT document implemented by note codec components. +pub const NOTE_CODEC_WIT: &str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/wit/note-codec.wit")); diff --git a/midenc-compile/wit/note-codec.wit b/sdk/note-codec/wit-crate/wit/note-codec.wit similarity index 99% rename from midenc-compile/wit/note-codec.wit rename to sdk/note-codec/wit-crate/wit/note-codec.wit index 3cde3d4442..d27dcc081e 100644 --- a/midenc-compile/wit/note-codec.wit +++ b/sdk/note-codec/wit-crate/wit/note-codec.wit @@ -24,4 +24,3 @@ interface codec { world note-codec { export codec; } - diff --git a/sdk/note-codec/wit/note-codec.wit b/sdk/note-codec/wit/note-codec.wit deleted file mode 100644 index 3cde3d4442..0000000000 --- a/sdk/note-codec/wit/note-codec.wit +++ /dev/null @@ -1,27 +0,0 @@ -package miden:note-codec@1.0.0; - -/// Parses, displays, and validates custom note storage types. -interface codec { - /// A canonical Miden field element at the component boundary. - /// - /// This is a `u64`, not the core-types `felt` record, because that record models the - /// compiler's guest felt value and does not expose its canonical integer representation. - type felt = u64; - - /// Returns the fully-qualified WIT names handled by this component. - supported-types: func() -> list; - - /// Parses text into a type's structural felt representation. - parse: func(type-fqn: string, value: string) -> result, string>; - - /// Displays a type's structural felt representation. - display: func(type-fqn: string, value: list) -> result; - - /// Validates a type's structural felt representation. - validate: func(type-fqn: string, value: list) -> result<_, string>; -} - -world note-codec { - export codec; -} - diff --git a/sdk/note-schema/Cargo.toml b/sdk/note-schema/Cargo.toml index cc0f6115a7..92ebc549f3 100644 --- a/sdk/note-schema/Cargo.toml +++ b/sdk/note-schema/Cargo.toml @@ -26,11 +26,13 @@ miden-field-repr.workspace = true miden-mast-package = { workspace = true, features = ["std"] } miden-protocol = { workspace = true, features = ["std"] } midenc-frontend-wasm-metadata.workspace = true +toml.workspace = true wit-parser.workspace = true wasmtime = { workspace = true, optional = true } [dev-dependencies] miden-core.workspace = true +miden-note-codec-wit.workspace = true midenc-frontend-wasm.workspace = true midenc-integration-test-support.workspace = true tempfile.workspace = true diff --git a/sdk/note-schema/codegen/src/lib.rs b/sdk/note-schema/codegen/src/lib.rs index 29f73e0ec1..5363ae44a0 100644 --- a/sdk/note-schema/codegen/src/lib.rs +++ b/sdk/note-schema/codegen/src/lib.rs @@ -9,10 +9,10 @@ use std::{ use heck::{ToSnakeCase, ToUpperCamelCase}; use miden_note_schema::{ - ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, FELT_FQN, NoteStorageSchema, PrimitiveType, SchemaCase, - SchemaField, SchemaType, SchemaTypeKind, WORD_FQN, + NoteStorageSchema, PrimitiveType, SchemaCase, SchemaField, SchemaType, SchemaTypeKind, + StandardLeaf, }; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, Literal, Span, TokenStream}; use quote::{format_ident, quote}; /// An error reported while generating Rust bindings. @@ -68,8 +68,58 @@ impl GeneratedTypes { } } +/// Runtime crate paths used by generated host-profile code. +#[derive(Clone)] +pub struct RuntimePaths { + miden_field: TokenStream, + miden_field_repr: TokenStream, + miden_note_schema: TokenStream, + miden_protocol: TokenStream, +} + +impl RuntimePaths { + /// Creates a runtime path set from four crate or module paths. + pub fn new( + miden_field: TokenStream, + miden_field_repr: TokenStream, + miden_note_schema: TokenStream, + miden_protocol: TokenStream, + ) -> Self { + Self { + miden_field, + miden_field_repr, + miden_note_schema, + miden_protocol, + } + } + + /// Creates paths through a facade crate's hidden runtime re-exports. + pub fn through_facade(facade: TokenStream) -> Self { + Self::new( + quote!(#facade::__private::miden_field), + quote!(#facade::__private::miden_field_repr), + quote!(#facade::__private::miden_note_schema), + quote!(#facade::__private::miden_protocol), + ) + } +} + +impl Default for RuntimePaths { + fn default() -> Self { + Self::new( + quote!(::miden_field), + quote!(::miden_field_repr), + quote!(::miden_note_schema), + quote!(::miden_protocol), + ) + } +} + /// Generates Rust host-profile types and structural felt conversion helpers. -pub fn generate_host_types(schema: &NoteStorageSchema) -> Result { +pub fn generate_host_types( + schema: &NoteStorageSchema, + runtime: &RuntimePaths, +) -> Result { schema .validate_native_leaf_shapes() .map_err(|error| CodegenError::new(error.to_string()))?; @@ -102,10 +152,10 @@ pub fn generate_host_types(schema: &NoteStorageSchema) -> Result, _>>()?; let type_idents = definitions .iter() @@ -137,7 +187,7 @@ fn collect_named_types<'a>( seen: &mut BTreeSet, definitions: &mut Vec<&'a SchemaType>, ) -> Result<(), CodegenError> { - if mapped_leaf(ty).is_some() { + if ty.standard_leaf().is_some() { return Ok(()); } @@ -173,14 +223,20 @@ fn collect_named_types<'a>( } /// Generates the private traits that keep protocol-leaf order separate from foreign trait impls. -fn generate_helper_traits() -> TokenStream { +fn generate_helper_traits(runtime: &RuntimePaths) -> TokenStream { + let RuntimePaths { + miden_field, + miden_field_repr, + miden_note_schema, + miden_protocol, + } = runtime; let primitive_impls = [ quote!(u64), quote!(u32), quote!(u8), quote!(bool), - quote!(::miden_field::Felt), - quote!(::miden_field::Word), + quote!(#miden_field::Felt), + quote!(#miden_field::Word), ] .into_iter() .map(|ty| { @@ -188,19 +244,19 @@ fn generate_helper_traits() -> TokenStream { impl __MidenNoteEncode for #ty { fn __write_note_felts( &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - ::miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { + #miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); Ok(()) } } impl __MidenNoteDecode for #ty { fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { - ::miden_field_repr::FromFeltRepr::from_felt_repr(reader).map_err(|error| { - ::miden_note_schema::Error::new(format!( + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { + #miden_field_repr::FromFeltRepr::from_felt_repr(reader).map_err(|error| { + #miden_note_schema::Error::new(format!( "failed to decode {} from note storage: {error}", stringify!(#ty), )) @@ -215,24 +271,24 @@ fn generate_helper_traits() -> TokenStream { trait __MidenNoteEncode { fn __write_note_felts( &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()>; + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()>; } #[doc(hidden)] trait __MidenNoteDecode: Sized { fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result; + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result; } #(#primitive_impls)* - impl __MidenNoteEncode for ::miden_protocol::account::AccountId { + impl __MidenNoteEncode for #miden_protocol::account::AccountId { fn __write_note_felts( &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { // The WIT record declares prefix before suffix. writer.write(self.prefix().as_felt()); writer.write(self.suffix()); @@ -240,23 +296,23 @@ fn generate_helper_traits() -> TokenStream { } } - impl __MidenNoteDecode for ::miden_protocol::account::AccountId { + impl __MidenNoteDecode for #miden_protocol::account::AccountId { fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { let prefix = reader.read().map_err(|error| { - ::miden_note_schema::Error::new(format!( + #miden_note_schema::Error::new(format!( "failed to decode account-id prefix: {error}" )) })?; let suffix = reader.read().map_err(|error| { - ::miden_note_schema::Error::new(format!( + #miden_note_schema::Error::new(format!( "failed to decode account-id suffix: {error}" )) })?; - ::miden_protocol::account::AccountId::try_from_elements(suffix, prefix).map_err( + #miden_protocol::account::AccountId::try_from_elements(suffix, prefix).map_err( |error| { - ::miden_note_schema::Error::new(format!( + #miden_note_schema::Error::new(format!( "invalid account-id in note storage: {error}" )) }, @@ -264,27 +320,27 @@ fn generate_helper_traits() -> TokenStream { } } - impl __MidenNoteEncode for ::miden_protocol::asset::AssetAmount { + impl __MidenNoteEncode for #miden_protocol::asset::AssetAmount { fn __write_note_felts( &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { - writer.write(::miden_field::Felt::from(*self)); + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { + writer.write(#miden_field::Felt::from(*self)); Ok(()) } } - impl __MidenNoteDecode for ::miden_protocol::asset::AssetAmount { + impl __MidenNoteDecode for #miden_protocol::asset::AssetAmount { fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { let value = reader.read().map_err(|error| { - ::miden_note_schema::Error::new(format!( + #miden_note_schema::Error::new(format!( "failed to decode asset-amount: {error}" )) })?; - ::miden_protocol::asset::AssetAmount::try_from(value).map_err(|error| { - ::miden_note_schema::Error::new(format!( + #miden_protocol::asset::AssetAmount::try_from(value).map_err(|error| { + #miden_note_schema::Error::new(format!( "invalid asset-amount in note storage: {error}" )) }) @@ -297,12 +353,12 @@ fn generate_helper_traits() -> TokenStream { { fn __write_note_felts( &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { match self { - None => writer.write(::miden_field::Felt::ZERO), + None => writer.write(#miden_field::Felt::ZERO), Some(value) => { - writer.write(::miden_field::Felt::ONE); + writer.write(#miden_field::Felt::ONE); value.__write_note_felts(writer)?; } } @@ -315,17 +371,17 @@ fn generate_helper_traits() -> TokenStream { T: __MidenNoteDecode, { fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { let tag = reader.read().map_err(|error| { - ::miden_note_schema::Error::new(format!( + #miden_note_schema::Error::new(format!( "failed to decode option tag: {error}" )) })?; match tag.as_canonical_u64() { 0 => Ok(None), 1 => Ok(Some(T::__read_note_felts(reader)?)), - tag => Err(::miden_note_schema::Error::new(format!( + tag => Err(#miden_note_schema::Error::new(format!( "invalid option tag {tag}; expected 0 or 1" ))), } @@ -338,20 +394,24 @@ fn generate_helper_traits() -> TokenStream { fn generate_type( definition: &SchemaType, rust_names: &BTreeMap, + runtime: &RuntimePaths, ) -> Result { let fqn = definition.fqn().expect("generated type definitions always have a FQN"); let ident = rust_names.get(fqn).expect("every generated type has a Rust identifier"); let docs = type_docs(definition, fqn); let derives = if supports_native_felt_repr(definition) { + let miden_field_repr = &runtime.miden_field_repr; + let crate_path = Literal::string(&miden_field_repr.to_string().replace(' ', "")); quote! { #[derive( Clone, Debug, PartialEq, Eq, - ::miden_field_repr::ToFeltRepr, - ::miden_field_repr::FromFeltRepr, + #miden_field_repr::ToFeltRepr, + #miden_field_repr::FromFeltRepr, )] + #[felt_repr(crate_path = #crate_path)] } } else { quote! { #[derive(Clone, Debug, PartialEq, Eq)] } @@ -359,10 +419,10 @@ fn generate_type( let (item, encode_impl, decode_impl) = match definition.kind() { SchemaTypeKind::Record(fields) => { - generate_record(ident, fields, rust_names, &docs, &derives)? + generate_record(ident, fields, rust_names, &docs, &derives, runtime)? } SchemaTypeKind::Variant(cases) => { - generate_variant(ident, cases, rust_names, &docs, &derives)? + generate_variant(ident, cases, rust_names, &docs, &derives, runtime)? } _ => { return Err(CodegenError::new(format!( @@ -391,12 +451,15 @@ fn generate_record( rust_names: &BTreeMap, docs: &TokenStream, derives: &TokenStream, + runtime: &RuntimePaths, ) -> Result<(TokenStream, TokenStream, TokenStream), CodegenError> { + let miden_field_repr = &runtime.miden_field_repr; + let miden_note_schema = &runtime.miden_note_schema; let rust_fields = fields .iter() .map(|field| { let ident = value_ident(field.name()); - let ty = rust_type(field.ty(), rust_names)?; + let ty = rust_type(field.ty(), rust_names, runtime)?; let docs = field_docs(field); Ok((ident, ty, docs)) }) @@ -419,8 +482,8 @@ fn generate_record( impl __MidenNoteEncode for #ident { fn __write_note_felts( &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { #(self.#field_idents.__write_note_felts(writer)?;)* Ok(()) } @@ -429,8 +492,8 @@ fn generate_record( let decode_impl = quote! { impl __MidenNoteDecode for #ident { fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { Ok(Self { #(#field_idents: <#field_types as __MidenNoteDecode>::__read_note_felts(reader)?,)* }) @@ -447,12 +510,17 @@ fn generate_variant( rust_names: &BTreeMap, docs: &TokenStream, derives: &TokenStream, + runtime: &RuntimePaths, ) -> Result<(TokenStream, TokenStream, TokenStream), CodegenError> { + let miden_field = &runtime.miden_field; + let miden_field_repr = &runtime.miden_field_repr; + let miden_note_schema = &runtime.miden_note_schema; let rust_cases = cases .iter() .map(|case| { let ident = type_ident(case.name()); - let payload = case.payload().map(|ty| rust_type(ty, rust_names)).transpose()?; + let payload = + case.payload().map(|ty| rust_type(ty, rust_names, runtime)).transpose()?; let docs = case_docs(case); Ok((ident, payload, docs)) }) @@ -473,13 +541,13 @@ fn generate_variant( match payload { Some(_) => quote! { Self::#case(value) => { - writer.write(::miden_field::Felt::from_u32(#ordinal)); + writer.write(#miden_field::Felt::from_u32(#ordinal)); value.__write_note_felts(writer)?; } }, None => quote! { Self::#case => { - writer.write(::miden_field::Felt::from_u32(#ordinal)); + writer.write(#miden_field::Felt::from_u32(#ordinal)); } }, } @@ -508,8 +576,8 @@ fn generate_variant( impl __MidenNoteEncode for #ident { fn __write_note_felts( &self, - writer: &mut ::miden_field_repr::FeltWriter<'_>, - ) -> ::miden_note_schema::Result<()> { + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { match self { #(#encode_arms)* } @@ -520,17 +588,17 @@ fn generate_variant( let decode_impl = quote! { impl __MidenNoteDecode for #ident { fn __read_note_felts( - reader: &mut ::miden_field_repr::FeltReader<'_>, - ) -> ::miden_note_schema::Result { + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { let tag = reader.read_u32().map_err(|error| { - ::miden_note_schema::Error::new(format!( + #miden_note_schema::Error::new(format!( "failed to decode {} tag: {error}", stringify!(#ident), )) })?; match tag { #(#decode_arms)* - tag => Err(::miden_note_schema::Error::new(format!( + tag => Err(#miden_note_schema::Error::new(format!( "invalid {} tag {tag}; expected a declaration ordinal below {}", stringify!(#ident), #case_count, @@ -546,8 +614,9 @@ fn generate_variant( fn rust_type( ty: &SchemaType, rust_names: &BTreeMap, + runtime: &RuntimePaths, ) -> Result { - if let Some(mapped) = mapped_leaf(ty) { + if let Some(mapped) = mapped_leaf(ty, runtime) { return Ok(mapped); } if let Some(fqn) = ty.fqn() @@ -562,31 +631,38 @@ fn rust_type( SchemaTypeKind::Primitive(PrimitiveType::U8) => Ok(quote!(u8)), SchemaTypeKind::Primitive(PrimitiveType::Bool) => Ok(quote!(bool)), SchemaTypeKind::Option(payload) => { - let payload = rust_type(payload, rust_names)?; + let payload = rust_type(payload, rust_names, runtime)?; Ok(quote!(Option<#payload>)) } SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_) => Err(CodegenError::new(format!( "named WIT type `{}` was not collected for Rust generation", ty.fqn().or(ty.name()).unwrap_or("") ))), - SchemaTypeKind::Felt => Ok(quote!(::miden_field::Felt)), + SchemaTypeKind::Felt => { + let miden_field = &runtime.miden_field; + Ok(quote!(#miden_field::Felt)) + } } } /// Returns the native Rust type for one standard WIT leaf. -fn mapped_leaf(ty: &SchemaType) -> Option { - match ty.fqn()? { - FELT_FQN => Some(quote!(::miden_field::Felt)), - WORD_FQN => Some(quote!(::miden_field::Word)), - ACCOUNT_ID_FQN => Some(quote!(::miden_protocol::account::AccountId)), - ASSET_AMOUNT_FQN => Some(quote!(::miden_protocol::asset::AssetAmount)), - _ => None, +fn mapped_leaf(ty: &SchemaType, runtime: &RuntimePaths) -> Option { + let RuntimePaths { + miden_field, + miden_protocol, + .. + } = runtime; + match ty.standard_leaf()? { + StandardLeaf::Felt => Some(quote!(#miden_field::Felt)), + StandardLeaf::Word => Some(quote!(#miden_field::Word)), + StandardLeaf::AccountId => Some(quote!(#miden_protocol::account::AccountId)), + StandardLeaf::AssetAmount => Some(quote!(#miden_protocol::asset::AssetAmount)), } } /// Returns true when all fields implement the native felt-repr traits without protocol adapters. fn supports_native_felt_repr(ty: &SchemaType) -> bool { - if matches!(ty.fqn(), Some(ACCOUNT_ID_FQN | ASSET_AMOUNT_FQN)) { + if matches!(ty.standard_leaf(), Some(StandardLeaf::AccountId | StandardLeaf::AssetAmount)) { return false; } match ty.kind() { diff --git a/sdk/note-schema/codegen/src/tests.rs b/sdk/note-schema/codegen/src/tests.rs index ecfb7292c7..2e61c417eb 100644 --- a/sdk/note-schema/codegen/src/tests.rs +++ b/sdk/note-schema/codegen/src/tests.rs @@ -2,7 +2,7 @@ use miden_note_schema::NoteStorageSchema; -use crate::generate_host_types; +use crate::{RuntimePaths, generate_host_types}; const P2ID_SCHEMA: &str = r#" package example:p2id-schema@1.0.0; @@ -61,10 +61,30 @@ package miden:base@1.0.0 { } "#; +const EMBEDDED_CORE_SCHEMA: &str = r#" +package example:embedded-core-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{digest}; + record embedded-core-note { value: digest } + type storage = embedded-core-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record word { a: felt, b: felt, c: felt, d: felt } + record digest { inner: word } + } +} +"#; + /// Formats generated tokens as Rust source. fn generate(wit: &str) -> (String, bool, String) { let schema = NoteStorageSchema::from_wit_text(wit).unwrap(); - let generated = generate_host_types(&schema).unwrap(); + let generated = generate_host_types(&schema, &RuntimePaths::default()).unwrap(); let file: syn::File = syn::parse2(generated.tokens().clone()).unwrap(); ( prettyplease::unparse(&file), @@ -93,3 +113,14 @@ fn derives_native_repr_for_custom_records_and_variants() { assert!(source.matches("::miden_field_repr::ToFeltRepr").count() >= 2); assert!(source.contains("example:dex-schema/note-storage@1.0.0.limit-price")); } + +#[test] +fn generates_nonstandard_embedded_core_record_as_schema_owned_type() { + let (source, has_nested_named_types, root) = generate(EMBEDDED_CORE_SCHEMA); + + assert_eq!(root, "EmbeddedCoreNote"); + assert!(has_nested_named_types); + assert!(source.contains("pub struct Digest")); + assert!(source.contains("pub value: Digest")); + assert!(!source.contains("pub struct Word")); +} diff --git a/sdk/note-schema/src/artifact.rs b/sdk/note-schema/src/artifact.rs index 51f3eacfc5..9e48fafc05 100644 --- a/sdk/note-schema/src/artifact.rs +++ b/sdk/note-schema/src/artifact.rs @@ -9,8 +9,8 @@ use miden_mast_package::Package; use crate::{Error, NoteStorageSchema, Result}; -/// Compiler-provided path to the package staged for note codec generation. -const NOTE_CODEC_PACKAGE_PATH_ENV: &str = "MIDENC_NOTE_CODEC_PACKAGE_PATH"; +/// Directory used to exchange Miden packages with nested Cargo builds. +const PACKAGE_CACHE_ENV: &str = "MIDENC_PACKAGE_CACHE"; /// A loaded package artifact and its note storage schema. pub struct NotePackageArtifact { @@ -41,12 +41,8 @@ impl<'a> NotePackageResolver<'a> { Self { macro_crate } } - /// Loads the freshest package built by a Miden project. + /// Loads the package built by a Miden project. pub fn from_project(&self, project: &str) -> Result { - if let Some(path) = self.compiler_staged_package()? { - return self.load_package(path); - } - let project_dir = self.resolve_manifest_path(project)?; if !project_dir.is_dir() { return Err(Error::new(format!( @@ -55,7 +51,7 @@ impl<'a> NotePackageResolver<'a> { project_dir.display() ))); } - let package_path = freshest_project_package(&project_dir) + let package_path = resolve_project_package(&project_dir) .map_err(|error| Error::new(format!("{}: {error}", self.macro_crate)))? .ok_or_else(|| { Error::new(missing_project_package_message(self.macro_crate, &project_dir)) @@ -91,24 +87,15 @@ impl<'a> NotePackageResolver<'a> { Ok(PathBuf::from(manifest_dir).join(path)) } - /// Returns the compiler-staged package when one is available. - fn compiler_staged_package(&self) -> Result> { - let Some(path) = env::var_os(NOTE_CODEC_PACKAGE_PATH_ENV) else { - return Ok(None); - }; - let path = PathBuf::from(path); - if !path.is_file() { - return Err(Error::new(format!( - "{}: {NOTE_CODEC_PACKAGE_PATH_ENV} points to missing Miden package '{}'", - self.macro_crate, - path.display() - ))); - } - Ok(Some(path)) - } - /// Loads the package and its unique schema section. fn load_package(&self, path: PathBuf) -> Result { + let path = path.canonicalize().map_err(|error| { + Error::new(format!( + "{}: failed to resolve Miden package '{}': {error}", + self.macro_crate, + path.display() + )) + })?; let package = Package::deserialize_from_file(&path).map_err(|error| { Error::new(format!( "{}: failed to read Miden package '{}': {error}", @@ -127,70 +114,173 @@ impl<'a> NotePackageResolver<'a> { } } -/// Returns the newest package directly inside a Miden project profile directory. -fn freshest_project_package(project_dir: &Path) -> Result> { - let target_dir = project_dir.join("target/miden"); - if !target_dir.is_dir() { - return Ok(None); +/// Resolves one project package by package identity and output-directory priority. +fn resolve_project_package(project_dir: &Path) -> Result> { + let stems = project_package_stems(project_dir); + + if let Some(cache_dir) = env::var_os(PACKAGE_CACHE_ENV) { + return find_project_package_in_dir(&absolutize(PathBuf::from(cache_dir))?, &stems); } - let mut candidates = Vec::new(); - for profile_dir in candidate_profile_dirs(&target_dir)? { - let entries = fs::read_dir(&profile_dir).map_err(|error| { - Error::new(format!("failed to read '{}': {error}", profile_dir.display())) - })?; - for entry in entries { - let entry = entry.map_err(|error| { - Error::new(format!( - "failed to read an entry in '{}': {error}", - profile_dir.display() - )) - })?; - let path = entry.path(); - if !path.is_file() || !path.extension().is_some_and(|extension| extension == "masp") { - continue; - } - let modified = - entry.metadata().and_then(|metadata| metadata.modified()).map_err(|error| { - Error::new(format!( - "failed to read modification time for '{}': {error}", - path.display() - )) - })?; - candidates.push((modified, path)); + let profiles = candidate_profiles(); + for output_dir in project_output_dirs(project_dir, &profiles) { + if let Some(package) = find_project_package_in_dir(&output_dir, &stems)? { + return Ok(Some(package)); } } - candidates.sort_by(|(left_time, left_path), (right_time, right_path)| { - left_time.cmp(right_time).then_with(|| left_path.cmp(right_path)) - }); - Ok(candidates.pop().map(|(_, path)| path)) + Ok(None) } -/// Returns project profile directories in deterministic candidate order. -fn candidate_profile_dirs(target_dir: &Path) -> Result> { +/// Returns Cargo and Miden profile names in lookup order. +fn candidate_profiles() -> Vec { let mut profiles = Vec::new(); + if let Ok(profile) = env::var("PROFILE") { + push_profile(&mut profiles, profile); + } push_profile(&mut profiles, "release".to_owned()); push_profile(&mut profiles, "debug".to_owned()); + push_profile(&mut profiles, "dev".to_owned()); + profiles +} - let entries = fs::read_dir(target_dir).map_err(|error| { - Error::new(format!("failed to read '{}': {error}", target_dir.display())) - })?; - let mut discovered = entries - .filter_map(core::result::Result::ok) - .filter(|entry| entry.path().is_dir()) - .filter_map(|entry| entry.file_name().into_string().ok()) - .filter(|name| name != "packages" && name != "generated-wit") - .collect::>(); - discovered.sort(); - for profile in discovered { - push_profile(&mut profiles, profile); +/// Returns candidate output directories for one project. +fn project_output_dirs(project_dir: &Path, profiles: &[String]) -> Vec { + let mut dirs = Vec::new(); + + // Keep this policy in parity with dependency_output_dirs in base-macros/src/fpi.rs. The + // crates cannot share the implementation without adding the full SDK macro dependency graph. + push_profile_dirs(&mut dirs, project_dir.join("target"), profiles); + push_manifest_ancestor_target_profile_dirs(&mut dirs, project_dir, profiles); + push_ancestor_target_profile_dirs(&mut dirs, project_dir, profiles); + + if let Some(target_dir) = env::var_os("CARGO_TARGET_DIR") { + push_profile_dirs(&mut dirs, PathBuf::from(target_dir), profiles); + } + if let Some(out_dir) = env::var_os("OUT_DIR") { + for ancestor in Path::new(&out_dir).ancestors() { + push_profile_dirs(&mut dirs, ancestor.to_path_buf(), profiles); + } } + if let Ok(current_dir) = env::current_dir() { + push_profile_dirs(&mut dirs, current_dir.join("target"), profiles); + push_manifest_ancestor_target_profile_dirs(&mut dirs, ¤t_dir, profiles); + push_ancestor_target_profile_dirs(&mut dirs, ¤t_dir, profiles); + } + dirs +} - Ok(profiles +/// Adds `target/miden/` directories while preserving order. +fn push_profile_dirs(dirs: &mut Vec, target_root: PathBuf, profiles: &[String]) { + for profile in profiles { + let dir = target_root.join("miden").join(profile); + if !dirs.contains(&dir) { + dirs.push(dir); + } + } +} + +/// Adds target directories found among the ancestors of `path`. +fn push_ancestor_target_profile_dirs(dirs: &mut Vec, path: &Path, profiles: &[String]) { + for ancestor in path.ancestors() { + if ancestor.file_name().is_some_and(|name| name == "target") { + push_profile_dirs(dirs, ancestor.to_path_buf(), profiles); + } + } +} + +/// Adds target directories for Cargo manifest ancestors of `path`. +fn push_manifest_ancestor_target_profile_dirs( + dirs: &mut Vec, + path: &Path, + profiles: &[String], +) { + for ancestor in path.ancestors() { + if ancestor.join("Cargo.toml").is_file() || ancestor.join("Cargo.lock").is_file() { + push_profile_dirs(dirs, ancestor.join("target"), profiles); + } + } +} + +/// Finds a package in one output directory by ordered package identity. +fn find_project_package_in_dir(dir: &Path, stems: &[String]) -> Result> { + if !dir.is_dir() { + return Ok(None); + } + + let mut packages = fs::read_dir(dir) + .map_err(|error| Error::new(format!("failed to read '{}': {error}", dir.display())))? + .collect::, _>>() + .map_err(|error| { + Error::new(format!("failed to read an entry in '{}': {error}", dir.display())) + })? .into_iter() - .map(|profile| target_dir.join(profile)) - .filter(|path| path.is_dir()) - .collect()) + .map(|entry| entry.path()) + .filter(|path| { + path.is_file() && path.extension().is_some_and(|extension| extension == "masp") + }) + .collect::>(); + packages.sort(); + + // The stems are ordered by identity. The canonical miden-project package name comes first, + // followed by legacy aliases. This matches base-macros/src/fpi.rs and prevents a newer legacy + // artifact from shadowing the canonical package. + for stem in stems { + if let Some(package) = packages + .iter() + .find(|path| path.file_stem().and_then(|value| value.to_str()) == Some(stem.as_str())) + { + return Ok(Some(package.clone())); + } + } + Ok(None) +} + +/// Returns ordered package filename stems for one project. +fn project_package_stems(project_dir: &Path) -> Vec { + let mut stems = Vec::new(); + if let Some(name) = package_name_from_manifest(&project_dir.join("miden-project.toml")) { + push_package_stem(&mut stems, &name); + } + if let Some(name) = package_name_from_manifest(&project_dir.join("Cargo.toml")) { + push_package_stem(&mut stems, &name); + } + if let Some(name) = project_dir.file_name().and_then(|name| name.to_str()) { + push_package_stem(&mut stems, name); + } + stems +} + +/// Reads a package name from one TOML manifest. +fn package_name_from_manifest(manifest_path: &Path) -> Option { + let manifest = fs::read_to_string(manifest_path).ok()?; + let manifest = manifest.parse::().ok()?; + manifest + .get("package") + .and_then(toml::Value::as_table) + .and_then(|package| package.get("name")) + .and_then(toml::Value::as_str) + .map(ToOwned::to_owned) +} + +/// Adds a package filename stem and its underscore alias. +fn push_package_stem(stems: &mut Vec, name: &str) { + if !name.is_empty() && !stems.iter().any(|existing| existing == name) { + stems.push(name.to_owned()); + } + let normalized = name.replace('-', "_"); + if !normalized.is_empty() && !stems.contains(&normalized) { + stems.push(normalized); + } +} + +/// Makes a cache path absolute for generated `include_bytes!` inputs. +fn absolutize(path: PathBuf) -> Result { + if path.is_absolute() { + return Ok(path); + } + Ok(env::current_dir() + .map_err(|error| Error::new(format!("failed to resolve current directory: {error}")))? + .join(path)) } /// Adds one profile name once. @@ -217,23 +307,44 @@ fn missing_project_package_message(macro_crate: &str, project_dir: &Path) -> Str #[cfg(test)] mod tests { - use std::{fs, thread, time::Duration}; + use std::fs; - use super::{freshest_project_package, missing_project_package_message}; + use super::{ + find_project_package_in_dir, missing_project_package_message, project_output_dirs, + project_package_stems, + }; #[test] - fn selects_the_freshest_package_across_profiles() { + fn canonical_project_identity_wins_over_legacy_aliases() { let temp = tempfile::tempdir().unwrap(); - let debug = temp.path().join("target/miden/debug"); - let release = temp.path().join("target/miden/release"); - fs::create_dir_all(&debug).unwrap(); - fs::create_dir_all(&release).unwrap(); - fs::write(debug.join("note.masp"), b"old").unwrap(); - thread::sleep(Duration::from_millis(20)); - fs::write(release.join("note.masp"), b"new").unwrap(); - - let selected = freshest_project_package(temp.path()).unwrap().unwrap(); - assert_eq!(selected, release.join("note.masp")); + fs::write( + temp.path().join("miden-project.toml"), + "[package]\nname='canonical-note'\nversion='0.1.0'", + ) + .unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='legacy-note'\nversion='0.1.0'") + .unwrap(); + let output = temp.path().join("output"); + fs::create_dir(&output).unwrap(); + fs::write(output.join("legacy-note.masp"), b"newer legacy package").unwrap(); + fs::write(output.join("canonical-note.masp"), b"canonical package").unwrap(); + + let stems = project_package_stems(temp.path()); + let selected = find_project_package_in_dir(&output, &stems).unwrap().unwrap(); + assert_eq!(selected, output.join("canonical-note.masp")); + } + + #[test] + fn manifest_ancestor_target_directories_are_candidates() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("Cargo.lock"), "").unwrap(); + let project = temp.path().join("crates/note"); + fs::create_dir_all(&project).unwrap(); + let profiles = vec!["release".to_owned()]; + + let dirs = project_output_dirs(&project, &profiles); + + assert!(dirs.contains(&temp.path().join("target/miden/release"))); } #[test] diff --git a/sdk/note-schema/src/codec.rs b/sdk/note-schema/src/codec.rs index 46cdf35d41..23dcb56f84 100644 --- a/sdk/note-schema/src/codec.rs +++ b/sdk/note-schema/src/codec.rs @@ -17,6 +17,43 @@ pub const ACCOUNT_ID_FQN: &str = "miden:base/core-types@1.0.0.account-id"; /// The canonical WIT FQN for `asset-amount`. pub const ASSET_AMOUNT_FQN: &str = "miden:base/core-types@1.0.0.asset-amount"; +/// A protocol type whose schema leaf maps directly to an existing host type and standard codec. +/// +/// This is the canonical standard-leaf definition used by schema traversal, Rust code generation, +/// and author-codec registration. Named types outside this set remain schema-owned, including +/// other records in the `miden:base/core-types` interface. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum StandardLeaf { + /// The one-element Miden base-field type. + Felt, + /// A group of four Miden base-field elements. + Word, + /// A two-element protocol account identifier. + AccountId, + /// A validated fungible-asset amount. + AssetAmount, +} + +impl StandardLeaf { + /// Every standard leaf in canonical registry order. + pub const ALL: [Self; 4] = [Self::Felt, Self::Word, Self::AccountId, Self::AssetAmount]; + + /// Returns the canonical WIT FQN for this standard leaf. + pub const fn fqn(self) -> &'static str { + match self { + Self::Felt => FELT_FQN, + Self::Word => WORD_FQN, + Self::AccountId => ACCOUNT_ID_FQN, + Self::AssetAmount => ASSET_AMOUNT_FQN, + } + } + + /// Classifies a canonical WIT FQN as a standard leaf. + pub fn from_fqn(fqn: &str) -> Option { + Self::ALL.into_iter().find(|leaf| leaf.fqn() == fqn) + } +} + /// Parses, displays, and validates one fully-qualified WIT leaf type. pub trait ConsumerTypeCodec: Send + Sync { /// Parses a string into its structural felt representation. @@ -52,10 +89,14 @@ impl CodecRegistry { /// Creates a registry containing all standard note storage codecs. pub fn with_standard_codecs() -> Self { let mut registry = Self::empty(); - registry.register(FELT_FQN, FeltCodec); - registry.register(WORD_FQN, WordCodec); - registry.register(ACCOUNT_ID_FQN, AccountIdCodec); - registry.register(ASSET_AMOUNT_FQN, AssetAmountCodec); + for leaf in StandardLeaf::ALL { + match leaf { + StandardLeaf::Felt => registry.register(leaf.fqn(), FeltCodec), + StandardLeaf::Word => registry.register(leaf.fqn(), WordCodec), + StandardLeaf::AccountId => registry.register(leaf.fqn(), AccountIdCodec), + StandardLeaf::AssetAmount => registry.register(leaf.fqn(), AssetAmountCodec), + } + } registry } @@ -255,13 +296,25 @@ mod tests { fn standard_registry_contains_canonical_versioned_fqns() { let registry = CodecRegistry::default(); - assert!(registry.contains(FELT_FQN)); - assert!(registry.contains(WORD_FQN)); - assert!(registry.contains(ACCOUNT_ID_FQN)); - assert!(registry.contains(ASSET_AMOUNT_FQN)); + for leaf in StandardLeaf::ALL { + assert!(registry.contains(leaf.fqn())); + } assert!(!CodecRegistry::empty().contains(FELT_FQN)); } + #[test] + fn standard_leaf_definition_is_pinned() { + assert_eq!( + StandardLeaf::ALL.map(StandardLeaf::fqn), + [FELT_FQN, WORD_FQN, ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN] + ); + for leaf in StandardLeaf::ALL { + assert_eq!(StandardLeaf::from_fqn(leaf.fqn()), Some(leaf)); + assert!(CodecRegistry::default().contains(leaf.fqn())); + } + assert_eq!(StandardLeaf::from_fqn("miden:base/core-types@1.0.0.digest"), None); + } + #[test] fn felt_codec_accepts_decimal_and_hex() { let codec = FeltCodec; diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 41728cc29a..8691a75f6a 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -15,9 +15,15 @@ use crate::{CodecRegistry, ConsumerTypeCodec, Error, NoteStorageSchema, Result}; /// Maximum Wasm instructions available to one codec operation. const CALL_FUEL: u64 = 10_000_000; +/// Maximum bytes accepted for one untrusted note codec component before Wasmtime compilation. +const MAX_COMPONENT_BYTES: usize = 4 * 1024 * 1024; + /// Maximum bytes available to one codec component linear memory. const MAX_COMPONENT_MEMORY_BYTES: usize = 16 * 1024 * 1024; +/// Maximum elements available to each codec component table. +const MAX_COMPONENT_TABLE_ELEMENTS: usize = 4_096; + /// Maximum FQNs accepted from `supported-types`. const MAX_SUPPORTED_TYPES: usize = 128; @@ -31,7 +37,7 @@ const MAX_RETURNED_FELTS: usize = 4_096; const MAX_RETURNED_STRING_BYTES: usize = 16 * 1024; wasmtime::component::bindgen!({ - path: "../note-codec/wit", + path: "wit", world: "note-codec", }); @@ -84,6 +90,7 @@ struct ComponentInstance { impl ComponentRuntime { /// Compiles a component with fuel accounting enabled. fn new(bytes: &[u8]) -> Result { + ensure_component_byte_limit(bytes.len())?; let mut config = Config::new(); config.wasm_component_model(true); config.consume_fuel(true); @@ -97,13 +104,7 @@ impl ComponentRuntime { /// Instantiates a zero-import component with fresh per-call limits. fn instantiate(&self) -> Result { let linker = Linker::new(&self.engine); - let limits = StoreLimitsBuilder::new() - .memory_size(MAX_COMPONENT_MEMORY_BYTES) - .instances(32) - .tables(32) - .memories(1) - .trap_on_grow_failure(true) - .build(); + let limits = component_store_limits(); let mut store = Store::new(&self.engine, ComponentStore { limits }); store.limiter(|state| &mut state.limits); store @@ -135,6 +136,29 @@ impl ComponentRuntime { } } +/// Builds the resource limits attached to every isolated component call store. +fn component_store_limits() -> StoreLimits { + StoreLimitsBuilder::new() + .memory_size(MAX_COMPONENT_MEMORY_BYTES) + .table_elements(MAX_COMPONENT_TABLE_ELEMENTS) + .instances(32) + .tables(32) + .memories(1) + .trap_on_grow_failure(true) + .build() +} + +/// Rejects oversized component bytes before validation or JIT compilation begins. +fn ensure_component_byte_limit(byte_len: usize) -> Result<()> { + if byte_len > MAX_COMPONENT_BYTES { + return Err(Error::new(format!( + "note codec component is {byte_len} bytes; the pre-compilation limit is \ + {MAX_COMPONENT_BYTES}" + ))); + } + Ok(()) +} + /// A registry entry that dispatches one FQN through isolated component instances. struct ComponentCodec { fqn: String, @@ -299,12 +323,14 @@ mod tests { }; use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; use tempfile::TempDir; + use wasmtime::ResourceLimiter; use wit_component::ComponentEncoder; use super::*; const WASM_TARGET: &str = "wasm32-unknown-unknown"; const FIXTURE_FQN: &str = "example:codec-schema/note-storage@1.0.0.ratio"; + const DIGEST_FQN: &str = "miden:base/core-types@1.0.0.digest"; const FIXTURE_SCHEMA: &str = r#" package example:codec-schema@1.0.0; @@ -320,8 +346,33 @@ interface note-storage { type storage = codec-note; } +"#; + const EMBEDDED_CORE_SCHEMA: &str = r#" +package example:embedded-core-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{digest}; + record embedded-core-note { value: digest } + type storage = embedded-core-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record word { a: felt, b: felt, c: felt, d: felt } + record digest { inner: word } + } +} "#; + #[test] + fn local_note_codec_wit_matches_canonical_document() { + let local = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/wit/note-codec.wit")); + assert_eq!(local, miden_note_codec_wit::NOTE_CODEC_WIT); + } + #[test] fn component_boundary_rejects_noncanonical_felts() { let error = @@ -330,6 +381,46 @@ interface note-storage { assert!(error.to_string().contains("noncanonical felt at index 0")); } + #[test] + fn oversized_component_is_rejected_before_compilation() { + let bytes = vec![0; MAX_COMPONENT_BYTES + 1]; + let error = ComponentRuntime::new(&bytes) + .err() + .expect("an oversized component must fail before compilation") + .to_string(); + + assert!(error.contains("pre-compilation limit")); + assert!(error.contains(&(MAX_COMPONENT_BYTES + 1).to_string())); + assert!(error.contains(&MAX_COMPONENT_BYTES.to_string())); + } + + #[test] + fn component_store_limits_bound_table_elements() { + let mut limits = component_store_limits(); + assert!( + ResourceLimiter::table_growing(&mut limits, 0, MAX_COMPONENT_TABLE_ELEMENTS, None,) + .unwrap() + ); + + let error = + ResourceLimiter::table_growing(&mut limits, 0, MAX_COMPONENT_TABLE_ELEMENTS + 1, None) + .unwrap_err() + .to_string(); + assert!(error.contains("growing table"), "unexpected table-limit error: {error}"); + } + + #[test] + fn nonstandard_embedded_core_type_is_author_codec_eligible() { + let schema = NoteStorageSchema::from_wit_text(EMBEDDED_CORE_SCHEMA).unwrap(); + let custom_types = schema.custom_type_fqns(); + + assert!(custom_types.contains(DIGEST_FQN)); + assert!(!custom_types.contains(crate::FELT_FQN)); + assert!(!custom_types.contains(crate::WORD_FQN)); + validate_reported_fqns(&[DIGEST_FQN.to_owned()], &custom_types, &CodecRegistry::default()) + .unwrap(); + } + #[test] fn package_component_registers_and_dispatches_author_codec() { if !wasm_target_is_installed() { diff --git a/sdk/note-schema/src/lib.rs b/sdk/note-schema/src/lib.rs index 03d86c3969..78425a7a79 100644 --- a/sdk/note-schema/src/lib.rs +++ b/sdk/note-schema/src/lib.rs @@ -30,13 +30,15 @@ mod tests; pub use artifact::{NotePackageArtifact, NotePackageResolver}; pub use builder::NoteStorageBuilder; pub use codec::{ - ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, CodecRegistry, ConsumerTypeCodec, FELT_FQN, WORD_FQN, + ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, CodecRegistry, ConsumerTypeCodec, FELT_FQN, StandardLeaf, + WORD_FQN, }; pub use error::{Error, Result}; pub use miden_field::Felt; pub use miden_protocol::note::NoteStorage; pub use schema::{ - FeltLayout, NoteStorageSchema, PrimitiveType, SchemaCase, SchemaField, SchemaType, - SchemaTypeKind, + FeltLayout, MAX_NOTE_STORAGE_SCHEMA_BYTES, MAX_NOTE_STORAGE_SCHEMA_DEPTH, + MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_TYPES, NoteStorageSchema, PrimitiveType, + SchemaCase, SchemaField, SchemaType, SchemaTypeKind, }; pub use value::{DecodedValue, DecodedValueKind}; diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs index b98afaf006..3377a2acc2 100644 --- a/sdk/note-schema/src/schema.rs +++ b/sdk/note-schema/src/schema.rs @@ -1,15 +1,41 @@ //! Resolved note storage schema model. -use std::collections::HashSet; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use miden_mast_package::Package; +use miden_protocol::MAX_NOTE_STORAGE_ITEMS; use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; use wit_parser::{Resolve, Type, TypeDefKind, TypeId, TypeOwner}; use crate::{ - CodecRegistry, DecodedValue, Error, NoteStorage, NoteStorageBuilder, Result, codec::FELT_FQN, + CodecRegistry, DecodedValue, Error, NoteStorage, NoteStorageBuilder, Result, StandardLeaf, + codec::FELT_FQN, }; +/// Maximum bytes accepted in an embedded note storage schema section, including alignment padding. +/// +/// The budget allows 64 bytes of schema description per protocol note-storage item. It is derived +/// from [`MAX_NOTE_STORAGE_ITEMS`] so schema parsing remains bounded with the protocol surface. +pub const MAX_NOTE_STORAGE_SCHEMA_BYTES: usize = MAX_NOTE_STORAGE_ITEMS * 64; + +/// Maximum number of resolved WIT type definitions in a note storage schema. +pub const MAX_NOTE_STORAGE_SCHEMA_TYPES: usize = MAX_NOTE_STORAGE_ITEMS; + +/// Maximum structural nesting depth accepted while resolving a note storage schema. +/// +/// Recursion is capped at one eighth of the protocol note-storage item limit, which leaves ample +/// room for legitimate models without allowing an attacker-controlled parser stack to grow to the +/// full storage width. +pub const MAX_NOTE_STORAGE_SCHEMA_DEPTH: usize = MAX_NOTE_STORAGE_ITEMS / 8; + +/// Maximum number of felts in the root note storage layout. +pub const MAX_NOTE_STORAGE_SCHEMA_FELTS: usize = MAX_NOTE_STORAGE_ITEMS; + +const _: () = assert!(MAX_NOTE_STORAGE_SCHEMA_DEPTH > 0); + /// The minimum and maximum felt count for a schema type. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct FeltLayout { @@ -55,6 +81,17 @@ impl FeltLayout { .maximum .checked_add(other.maximum) .ok_or_else(|| Error::new("note storage layout maximum width is too large"))?; + Self::bounded(minimum, maximum) + } + + /// Creates a layout within the protocol note-storage width. + fn bounded(minimum: usize, maximum: usize) -> Result { + if maximum > MAX_NOTE_STORAGE_SCHEMA_FELTS { + return Err(Error::new(format!( + "note storage schema layout has maximum width {maximum} felts; the protocol limit \ + is {MAX_NOTE_STORAGE_SCHEMA_FELTS}" + ))); + } Ok(Self { minimum, maximum }) } } @@ -82,7 +119,7 @@ pub enum SchemaTypeKind { /// A record with fields in declaration order. Record(Vec), /// An optional payload stored after a tag felt. - Option(Box), + Option(Arc), /// A variant with declaration-ordinal cases. Variant(Vec), } @@ -122,6 +159,11 @@ impl SchemaType { pub const fn layout(&self) -> FeltLayout { self.layout } + + /// Classifies this type as a standard protocol leaf. + pub fn standard_leaf(&self) -> Option { + self.fqn.as_deref().and_then(StandardLeaf::from_fqn) + } } /// A named record field in declaration order. @@ -129,7 +171,7 @@ impl SchemaType { pub struct SchemaField { name: String, docs: Option, - ty: SchemaType, + ty: Arc, } impl SchemaField { @@ -144,8 +186,8 @@ impl SchemaField { } /// Returns the field type. - pub const fn ty(&self) -> &SchemaType { - &self.ty + pub fn ty(&self) -> &SchemaType { + self.ty.as_ref() } } @@ -154,7 +196,7 @@ impl SchemaField { pub struct SchemaCase { name: String, docs: Option, - payload: Option, + payload: Option>, } impl SchemaCase { @@ -169,8 +211,8 @@ impl SchemaCase { } /// Returns the optional case payload. - pub const fn payload(&self) -> Option<&SchemaType> { - self.payload.as_ref() + pub fn payload(&self) -> Option<&SchemaType> { + self.payload.as_deref() } } @@ -178,7 +220,7 @@ impl SchemaCase { #[derive(Clone)] pub struct NoteStorageSchema { wit_text: String, - root: SchemaType, + root: Arc, codecs: CodecRegistry, } @@ -189,6 +231,7 @@ impl NoteStorageSchema { package, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, )?; + ensure_schema_byte_limit(bytes.len())?; let unpadded_len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); let text = core::str::from_utf8(&bytes[..unpadded_len]).map_err(|err| { Error::new(format!("note storage schema section is not valid UTF-8: {err}")) @@ -198,11 +241,19 @@ impl NoteStorageSchema { /// Resolves a note storage schema from a WIT document. pub fn from_wit_text(wit_text: &str) -> Result { + ensure_schema_byte_limit(wit_text.len())?; let wit_text = wit_text.trim_end_matches('\0'); let mut resolve = Resolve::default(); let package_id = resolve.push_str("note-storage-schema.wit", wit_text).map_err(|err| { Error::new(format!("failed to resolve note storage schema WIT: {err:#}")) })?; + if resolve.types.len() > MAX_NOTE_STORAGE_SCHEMA_TYPES { + return Err(Error::new(format!( + "note storage schema defines {} WIT types; the limit is \ + {MAX_NOTE_STORAGE_SCHEMA_TYPES}", + resolve.types.len() + ))); + } let package = &resolve.packages[package_id]; let interface_id = package.interfaces.get("note-storage").copied().ok_or_else(|| { Error::new(format!( @@ -216,12 +267,13 @@ impl NoteStorageSchema { })?; validate_resolved_core_types(&resolve)?; let root = ModelBuilder::new(&resolve).build(Type::Id(storage_id))?; - if !matches!(root.kind, SchemaTypeKind::Record(_)) { + if !matches!(root.kind(), SchemaTypeKind::Record(_)) { return Err(Error::new(format!( "the `note-storage.storage` alias must resolve to a record, found {}", - kind_name(&root.kind) + kind_name(root.kind()) ))); } + ensure_root_layout_limit(root.layout())?; let schema = Self { wit_text: wit_text.to_owned(), @@ -238,8 +290,8 @@ impl NoteStorageSchema { } /// Returns the root storage record. - pub const fn root(&self) -> &SchemaType { - &self.root + pub fn root(&self) -> &SchemaType { + self.root.as_ref() } /// Verifies native host mappings against the pinned standard type shapes. @@ -251,12 +303,12 @@ impl NoteStorageSchema { #[cfg(feature = "codec-component")] pub(crate) fn custom_type_fqns(&self) -> HashSet { let mut fqns = HashSet::new(); - collect_custom_type_fqns(&self.root, &mut fqns); + collect_custom_type_fqns(&self.root, &mut HashSet::new(), &mut fqns); fqns } /// Returns the root felt layout. - pub const fn layout(&self) -> FeltLayout { + pub fn layout(&self) -> FeltLayout { self.root.layout } @@ -299,6 +351,29 @@ impl NoteStorageSchema { } } +/// Enforces the parser input budget before WIT resolution or component work begins. +fn ensure_schema_byte_limit(byte_len: usize) -> Result<()> { + if byte_len > MAX_NOTE_STORAGE_SCHEMA_BYTES { + return Err(Error::new(format!( + "note storage schema section is {byte_len} bytes; the limit is \ + {MAX_NOTE_STORAGE_SCHEMA_BYTES}" + ))); + } + Ok(()) +} + +/// Enforces the protocol storage-width limit on the resolved root. +fn ensure_root_layout_limit(layout: FeltLayout) -> Result<()> { + if layout.maximum() > MAX_NOTE_STORAGE_SCHEMA_FELTS { + return Err(Error::new(format!( + "note storage schema root has maximum width {} felts; the protocol limit is \ + {MAX_NOTE_STORAGE_SCHEMA_FELTS}", + layout.maximum() + ))); + } + Ok(()) +} + /// Verifies the raw embedded core-types definitions before the model applies native mappings. fn validate_resolved_core_types(resolve: &Resolve) -> Result<()> { let Some((_, package_id)) = resolve.package_names.iter().find(|(name, _)| { @@ -314,12 +389,9 @@ fn validate_resolved_core_types(resolve: &Resolve) -> Result<()> { }; let interface = &resolve.interfaces[interface_id]; - for (name, fields) in [ - ("felt", &["inner"][..]), - ("word", &["a", "b", "c", "d"][..]), - ("account-id", &["prefix", "suffix"][..]), - ("asset-amount", &["inner"][..]), - ] { + for leaf in StandardLeaf::ALL { + let name = standard_leaf_name(leaf); + let fields = standard_leaf_fields(leaf); let Some(type_id) = interface.types.get(name).copied() else { continue; }; @@ -336,7 +408,7 @@ fn validate_resolved_core_types(resolve: &Resolve) -> Result<()> { { return Err(core_shape_error(name, fields)); } - if name == "felt" { + if leaf == StandardLeaf::Felt { if !resolves_to_primitive(resolve, record.fields[0].ty, Type::F32)? { return Err(core_shape_error(name, fields)); } @@ -401,20 +473,23 @@ fn validate_model_type_shapes(ty: &SchemaType, seen: &mut HashSet) -> Re return Ok(()); } - match ty.fqn() { - Some(crate::FELT_FQN) if !matches!(ty.kind(), SchemaTypeKind::Felt) => { - return Err(core_shape_error("felt", &["inner"])); - } - Some(crate::WORD_FQN) => { - validate_model_record(ty, "word", &["a", "b", "c", "d"], crate::FELT_FQN)? - } - Some(crate::ACCOUNT_ID_FQN) => { - validate_model_record(ty, "account-id", &["prefix", "suffix"], crate::FELT_FQN)? + match ty.standard_leaf() { + Some(StandardLeaf::Felt) if !matches!(ty.kind(), SchemaTypeKind::Felt) => { + return Err(core_shape_error( + standard_leaf_name(StandardLeaf::Felt), + standard_leaf_fields(StandardLeaf::Felt), + )); } - Some(crate::ASSET_AMOUNT_FQN) => { - validate_model_record(ty, "asset-amount", &["inner"], crate::FELT_FQN)? + Some(StandardLeaf::Felt) => {} + Some(leaf) => { + validate_model_record( + ty, + standard_leaf_name(leaf), + standard_leaf_fields(leaf), + crate::FELT_FQN, + )?; } - _ => {} + None => {} } match ty.kind() { @@ -434,6 +509,23 @@ fn validate_model_type_shapes(ty: &SchemaType, seen: &mut HashSet) -> Re Ok(()) } +/// Returns the terminal WIT name from a canonical standard-leaf FQN. +fn standard_leaf_name(leaf: StandardLeaf) -> &'static str { + leaf.fqn() + .rsplit_once('.') + .expect("standard-leaf FQNs always contain an interface separator") + .1 +} + +/// Returns the canonical record field order for a standard leaf. +fn standard_leaf_fields(leaf: StandardLeaf) -> &'static [&'static str] { + match leaf { + StandardLeaf::Felt | StandardLeaf::AssetAmount => &["inner"], + StandardLeaf::Word => &["a", "b", "c", "d"], + StandardLeaf::AccountId => &["prefix", "suffix"], + } +} + /// Verifies one mapped record in the owned schema model. fn validate_model_record( ty: &SchemaType, @@ -471,35 +563,42 @@ fn core_shape_error(name: &str, fields: &[&str]) -> Error { )) } -/// Collects schema-owned types and excludes the pinned SDK core-types package. +/// Collects schema-owned types and excludes only the canonical standard leaves. #[cfg(feature = "codec-component")] -fn collect_custom_type_fqns(ty: &SchemaType, fqns: &mut HashSet) { +fn collect_custom_type_fqns( + ty: &SchemaType, + seen: &mut HashSet<*const SchemaType>, + fqns: &mut HashSet, +) { + if !seen.insert(core::ptr::from_ref(ty)) { + return; + } if let Some(fqn) = ty.fqn() - && !fqn.starts_with("miden:base/core-types@") - && !fqn.starts_with("miden:base/core-types.") + && ty.standard_leaf().is_none() { fqns.insert(fqn.to_owned()); } match ty.kind() { SchemaTypeKind::Record(fields) => { for field in fields { - collect_custom_type_fqns(field.ty(), fqns); + collect_custom_type_fqns(field.ty(), seen, fqns); } } - SchemaTypeKind::Option(payload) => collect_custom_type_fqns(payload, fqns), + SchemaTypeKind::Option(payload) => collect_custom_type_fqns(payload, seen, fqns), SchemaTypeKind::Variant(cases) => { for payload in cases.iter().filter_map(SchemaCase::payload) { - collect_custom_type_fqns(payload, fqns); + collect_custom_type_fqns(payload, seen, fqns); } } SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {} } } -/// Builds an owned schema type tree from a resolved WIT graph. +/// Builds a memoized schema graph from a resolved WIT graph. struct ModelBuilder<'a> { resolve: &'a Resolve, active: HashSet, + memo: HashMap>, } impl<'a> ModelBuilder<'a> { @@ -508,18 +607,25 @@ impl<'a> ModelBuilder<'a> { Self { resolve, active: HashSet::new(), + memo: HashMap::new(), } } /// Resolves one WIT type. - fn build(mut self, ty: Type) -> Result { - self.build_type(ty) + fn build(mut self, ty: Type) -> Result> { + self.build_type(ty, 0) } /// Resolves a primitive or named type. - fn build_type(&mut self, ty: Type) -> Result { + fn build_type(&mut self, ty: Type, depth: usize) -> Result> { + if depth > MAX_NOTE_STORAGE_SCHEMA_DEPTH { + return Err(Error::new(format!( + "note storage schema nesting depth {depth} exceeds the limit of \ + {MAX_NOTE_STORAGE_SCHEMA_DEPTH}" + ))); + } match ty { - Type::Id(id) => self.build_type_id(id), + Type::Id(id) => self.build_type_id(id, depth), Type::U64 => self.primitive(PrimitiveType::U64, None, None, None), Type::U32 => self.primitive(PrimitiveType::U32, None, None, None), Type::U8 => self.primitive(PrimitiveType::U8, None, None, None), @@ -531,8 +637,11 @@ impl<'a> ModelBuilder<'a> { } /// Resolves aliases to the type definition that owns the structural type. - fn build_type_id(&mut self, id: TypeId) -> Result { + fn build_type_id(&mut self, id: TypeId, depth: usize) -> Result> { let id = self.follow_aliases(id)?; + if let Some(ty) = self.memo.get(&id) { + return Ok(Arc::clone(ty)); + } if !self.active.insert(id) { return Err(Error::new( "recursive WIT types are not supported in note storage schemas", @@ -544,21 +653,21 @@ impl<'a> ModelBuilder<'a> { let docs = definition.docs.contents.clone(); let fqn = self.type_fqn(id)?; let result = if fqn.as_deref() == Some(FELT_FQN) { - Ok(SchemaType { + Ok(Arc::new(SchemaType { name, fqn, docs, kind: SchemaTypeKind::Felt, layout: FeltLayout::fixed(1), - }) + })) } else { match definition.kind { - TypeDefKind::Type(ty) => self.build_named_alias(ty, name, fqn, docs), + TypeDefKind::Type(ty) => self.build_named_alias(ty, name, fqn, docs, depth), TypeDefKind::Record(record) => { let mut fields = Vec::with_capacity(record.fields.len()); let mut layout = FeltLayout::fixed(0); for field in record.fields { - let ty = self.build_type(field.ty)?; + let ty = self.build_type(field.ty, depth + 1)?; layout = layout.concatenate(ty.layout)?; fields.push(SchemaField { name: field.name, @@ -566,29 +675,27 @@ impl<'a> ModelBuilder<'a> { ty, }); } - Ok(SchemaType { + Ok(Arc::new(SchemaType { name, fqn, docs, kind: SchemaTypeKind::Record(fields), layout, - }) + })) } TypeDefKind::Option(payload) => { - let payload = Box::new(self.build_type(payload)?); - let layout = FeltLayout { - minimum: 1, - maximum: 1usize.checked_add(payload.layout.maximum).ok_or_else(|| { - Error::new("option layout maximum width is too large") - })?, - }; - Ok(SchemaType { + let payload = self.build_type(payload, depth + 1)?; + let maximum = 1usize + .checked_add(payload.layout.maximum) + .ok_or_else(|| Error::new("option layout maximum width is too large"))?; + let layout = FeltLayout::bounded(1, maximum)?; + Ok(Arc::new(SchemaType { name, fqn, docs, kind: SchemaTypeKind::Option(payload), layout, - }) + })) } TypeDefKind::Variant(variant) => { let mut cases = Vec::with_capacity(variant.cases.len()); @@ -596,17 +703,20 @@ impl<'a> ModelBuilder<'a> { cases.push(SchemaCase { name: case.name, docs: case.docs.contents, - payload: case.ty.map(|ty| self.build_type(ty)).transpose()?, + payload: case + .ty + .map(|ty| self.build_type(ty, depth + 1)) + .transpose()?, }); } let layout = variant_layout(&cases)?; - Ok(SchemaType { + Ok(Arc::new(SchemaType { name, fqn, docs, kind: SchemaTypeKind::Variant(cases), layout, - }) + })) } TypeDefKind::Enum(enum_) => { let cases = enum_ @@ -619,13 +729,13 @@ impl<'a> ModelBuilder<'a> { }) .collect::>(); let layout = variant_layout(&cases)?; - Ok(SchemaType { + Ok(Arc::new(SchemaType { name, fqn, docs, kind: SchemaTypeKind::Variant(cases), layout, - }) + })) } unsupported => Err(Error::new(format!( "WIT {} `{}` is not supported in note storage schemas", @@ -635,6 +745,9 @@ impl<'a> ModelBuilder<'a> { } }; self.active.remove(&id); + if let Ok(ty) = &result { + self.memo.insert(id, Arc::clone(ty)); + } result } @@ -645,9 +758,10 @@ impl<'a> ModelBuilder<'a> { name: Option, fqn: Option, docs: Option, - ) -> Result { + depth: usize, + ) -> Result> { match ty { - Type::Id(id) => self.build_type_id(id), + Type::Id(id) => self.build_type_id(id, depth), Type::U64 => self.primitive(PrimitiveType::U64, name, fqn, docs), Type::U32 => self.primitive(PrimitiveType::U32, name, fqn, docs), Type::U8 => self.primitive(PrimitiveType::U8, name, fqn, docs), @@ -665,18 +779,18 @@ impl<'a> ModelBuilder<'a> { name: Option, fqn: Option, docs: Option, - ) -> Result { + ) -> Result> { let width = match primitive { PrimitiveType::U64 => 2, PrimitiveType::U32 | PrimitiveType::U8 | PrimitiveType::Bool => 1, }; - Ok(SchemaType { + Ok(Arc::new(SchemaType { name, fqn, docs, kind: SchemaTypeKind::Primitive(primitive), layout: FeltLayout::fixed(width), - }) + })) } /// Follows `type = id` aliases to their defining type. @@ -739,14 +853,13 @@ fn variant_layout(cases: &[SchemaCase]) -> Result { .map(|case| case.payload.as_ref().map_or(0, |ty| ty.layout.maximum)) .max() .unwrap_or(0); - Ok(FeltLayout { - minimum: 1usize - .checked_add(minimum_payload) - .ok_or_else(|| Error::new("variant layout minimum width is too large"))?, - maximum: 1usize - .checked_add(maximum_payload) - .ok_or_else(|| Error::new("variant layout maximum width is too large"))?, - }) + let minimum = 1usize + .checked_add(minimum_payload) + .ok_or_else(|| Error::new("variant layout minimum width is too large"))?; + let maximum = 1usize + .checked_add(maximum_payload) + .ok_or_else(|| Error::new("variant layout maximum width is too large"))?; + FeltLayout::bounded(minimum, maximum) } /// Returns a stable name for a model kind. diff --git a/sdk/note-schema/src/tests.rs b/sdk/note-schema/src/tests.rs index ed97d2db19..5a1332964e 100644 --- a/sdk/note-schema/src/tests.rs +++ b/sdk/note-schema/src/tests.rs @@ -4,8 +4,9 @@ use miden_field_repr::ToFeltRepr; use miden_protocol::{account::AccountId, address::NetworkId}; use crate::{ - ACCOUNT_ID_FQN, CodecRegistry, DecodedValueKind, Felt, NoteStorage, NoteStorageSchema, - SchemaTypeKind, + ACCOUNT_ID_FQN, CodecRegistry, DecodedValueKind, Felt, MAX_NOTE_STORAGE_SCHEMA_BYTES, + MAX_NOTE_STORAGE_SCHEMA_DEPTH, MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_TYPES, + NoteStorage, NoteStorageSchema, SchemaTypeKind, }; const LAYOUT_SCHEMA: &str = r#" @@ -338,3 +339,104 @@ fn u64_layout_uses_shared_low_then_high_limb_encoding() { let value = 0x1234_5678_90ab_cdefu64; assert_eq!(value.to_felt_repr(), [Felt::from_u32(0x90ab_cdef), Felt::from_u32(0x1234_5678)]); } + +#[test] +fn repeated_pair_schema_is_memoized_and_fails_fast_at_width_limit() { + let schema = NoteStorageSchema::from_wit_text(&repeated_pair_schema(8)).unwrap(); + let SchemaTypeKind::Record(fields) = schema.root().kind() else { + panic!("the repeated-pair root must be a record"); + }; + assert!( + core::ptr::eq(fields[0].ty(), fields[1].ty()), + "both references to the completed named type must share one memoized node" + ); + + let error = + NoteStorageSchema::from_wit_text(&repeated_pair_schema(MAX_NOTE_STORAGE_SCHEMA_DEPTH / 2)) + .err() + .expect("an over-wide repeated-pair schema must fail") + .to_string(); + assert!(error.contains("maximum width"), "unexpected repeated-pair error: {error}"); + assert!( + error.contains(&MAX_NOTE_STORAGE_SCHEMA_FELTS.to_string()), + "the protocol width must be present in the diagnostic: {error}" + ); +} + +#[test] +fn deep_schema_chain_fails_fast_at_depth_limit() { + let error = + NoteStorageSchema::from_wit_text(&deep_chain_schema(MAX_NOTE_STORAGE_SCHEMA_DEPTH + 1)) + .err() + .expect("an over-deep schema must fail") + .to_string(); + + assert!(error.contains("nesting depth"), "unexpected deep-chain error: {error}"); + assert!( + error.contains(&MAX_NOTE_STORAGE_SCHEMA_DEPTH.to_string()), + "the nesting limit must be present in the diagnostic: {error}" + ); +} + +#[test] +fn schema_reader_enforces_documented_byte_type_and_root_width_limits() { + let oversized = " ".repeat(MAX_NOTE_STORAGE_SCHEMA_BYTES + 1); + let byte_error = NoteStorageSchema::from_wit_text(&oversized) + .err() + .expect("an oversized schema document must fail") + .to_string(); + assert!(byte_error.contains("schema section")); + assert!(byte_error.contains(&MAX_NOTE_STORAGE_SCHEMA_BYTES.to_string())); + + let mut too_many_types = + String::from("package example:many-types@1.0.0; interface note-storage { "); + for index in 0..=MAX_NOTE_STORAGE_SCHEMA_TYPES { + too_many_types.push_str(&format!("type t{index} = u8; ")); + } + too_many_types.push_str("record root { value: u8 } type storage = root; }"); + let type_error = NoteStorageSchema::from_wit_text(&too_many_types) + .err() + .expect("a schema with too many types must fail") + .to_string(); + assert!(type_error.contains("WIT types"), "unexpected type-count error: {type_error}"); + assert!(type_error.contains(&MAX_NOTE_STORAGE_SCHEMA_TYPES.to_string())); + + let mut wide_root = + String::from("package example:wide-root@1.0.0; interface note-storage { record root { "); + for index in 0..=MAX_NOTE_STORAGE_SCHEMA_FELTS { + wide_root.push_str(&format!("field-{index}: u8, ")); + } + wide_root.push_str("} type storage = root; }"); + let width_error = NoteStorageSchema::from_wit_text(&wide_root) + .err() + .expect("an over-wide schema root must fail") + .to_string(); + assert!(width_error.contains("maximum width"), "unexpected width error: {width_error}"); + assert!(width_error.contains(&MAX_NOTE_STORAGE_SCHEMA_FELTS.to_string())); +} + +/// Builds a linear-size WIT DAG whose resolved layout doubles at each level. +fn repeated_pair_schema(levels: usize) -> String { + let mut wit = String::from( + "package example:repeated-pair@1.0.0; interface note-storage { record t0 { value: u8 } ", + ); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record t{level} {{ left: t{previous}, right: t{previous} }} ")); + } + wit.push_str(&format!("type storage = t{levels}; }}")); + wit +} + +/// Builds a WIT record chain with one nested named type per level. +fn deep_chain_schema(levels: usize) -> String { + let mut wit = String::from( + "package example:deep-chain@1.0.0; interface note-storage { record t0 { value: u8 } ", + ); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record t{level} {{ value: t{previous} }} ")); + } + wit.push_str(&format!("type storage = t{levels}; }}")); + wit +} diff --git a/sdk/note-codec/macros/wit/note-codec.wit b/sdk/note-schema/wit/note-codec.wit similarity index 99% rename from sdk/note-codec/macros/wit/note-codec.wit rename to sdk/note-schema/wit/note-codec.wit index 3cde3d4442..d27dcc081e 100644 --- a/sdk/note-codec/macros/wit/note-codec.wit +++ b/sdk/note-schema/wit/note-codec.wit @@ -24,4 +24,3 @@ interface codec { world note-codec { export codec; } - diff --git a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs index 6551848df1..ad87857087 100644 --- a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs +++ b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs @@ -388,15 +388,3 @@ fn note_packages_carry_resolvable_storage_schema_metadata() { "#]], ); } - -/// The pinned note-codec world WIT is duplicated into each package that embeds it, because -/// `include_str!` paths must not escape a published package's root. This test locks the copies -/// together; update all three files when the world changes. -#[test] -fn note_codec_wit_copies_are_identical() { - let canonical = include_str!("../../../../../sdk/note-codec/wit/note-codec.wit"); - let macros_copy = include_str!("../../../../../sdk/note-codec/macros/wit/note-codec.wit"); - let compiler_copy = include_str!("../../../../../midenc-compile/wit/note-codec.wit"); - assert_eq!(canonical, macros_copy, "sdk/note-codec/macros/wit/note-codec.wit drifted"); - assert_eq!(canonical, compiler_copy, "midenc-compile/wit/note-codec.wit drifted"); -} From d842ef0b9af8e403f677b3474ec6ec63591de5e8 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 18 Aug 2026 14:27:54 +0300 Subject: [PATCH 08/43] build: link note codecs as components with the wasm32-wasip2 target The codec crate previously compiled for wasm32-unknown-unknown and a separate ComponentEncoder pass wrapped the module into a component. Building for wasm32-wasip2 lets rustc link the component directly through wasm-component-ld, removing the encoding step and its failure modes. The wasip2 standard library wires WASI interfaces into every component, so the zero-import property is replaced by an equivalent guarantee: build validation accepts `wasi:*` imports only, and consumers stub every import as a trapping function at instantiation, so codec code can still reach no host capability. The mockchain end-to-end test confirms a stubbed component instantiates and converts values normally. The component-export tests build through the same path as the compiler, and the note-schema crate drops its now-unused wit-component dev-dependency. --- Cargo.lock | 1 - midenc-compile/src/cargo.rs | 45 ++++++++----------- sdk/CHANGELOG.md | 4 ++ sdk/note-codec/tests/component_export.rs | 25 +++++------ sdk/note-schema/Cargo.toml | 1 - sdk/note-schema/src/codec_component.rs | 26 +++++------ .../src/mockchain/notes/schema.rs | 4 +- .../cargo-miden/tests/dex_note_codec_build.rs | 11 +++-- 8 files changed, 54 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7358064432..3760af2c63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3688,7 +3688,6 @@ dependencies = [ "tempfile", "toml 1.1.4+spec-1.1.0", "wasmtime", - "wit-component", "wit-parser 0.247.0", ] diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 0a2c21de8a..8c3ea2b295 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -16,7 +16,7 @@ use miden_note_codec_wit::NOTE_CODEC_WIT; use midenc_hir::Report; use midenc_session::{InputFile, LinkLibrary, Session, miden_project}; use sha2::{Digest, Sha256}; -use wit_component::{ComponentEncoder, DecodedWasm}; +use wit_component::DecodedWasm; use wit_parser::{Function, FunctionKind, Resolve, Type, TypeDefKind, TypeId, WorldId, WorldItem}; use crate::{CodegenOutput, CompilerResult}; @@ -27,8 +27,9 @@ const NOTE_CODEC_CRATE_METADATA: &str = "note-codec-crate"; /// Metadata field that contains the codec crate directory. const NOTE_CODEC_CRATE_PATH: &str = "path"; -/// Rust target used for zero-import note codec components. -const NOTE_CODEC_TARGET: &str = "wasm32-unknown-unknown"; +/// Rust target used to build note codec components; rustc links the cdylib as a +/// Wasm component, so no separate encoding step is necessary. +const NOTE_CODEC_TARGET: &str = "wasm32-wasip2"; /// Directory used to exchange Miden packages with nested Cargo builds. const PACKAGE_CACHE_ENV: &str = "MIDENC_PACKAGE_CACHE"; @@ -448,7 +449,7 @@ fn build_note_codec_component( } else { None }; - crate::rust::install_wasm32_target("unknown-unknown", toolchain.as_deref())?; + crate::rust::install_wasm32_target("wasip2", toolchain.as_deref())?; let cargo_target_dir = work_dir.join("cargo-target").join(&staged_package.build_key); let mut cargo = Command::new(cargo_path); @@ -513,28 +514,14 @@ fn build_note_codec_component( } let wasm_path = wasm_paths.pop().expect("one codec artifact was checked above"); - let module = fs::read(&wasm_path).map_err(|error| { + // The wasm32-wasip2 target links through wasm-component-ld, so the produced + // cdylib artifact is already a Wasm component. + let component = fs::read(&wasm_path).map_err(|error| { Report::msg(format!( - "note codec build produced an unreadable cdylib '{}': {error}", + "note codec build produced an unreadable component '{}': {error}", wasm_path.display() )) })?; - let component = ComponentEncoder::default() - .module(&module) - .map_err(|error| { - Report::msg(format!( - "note codec module '{}' is not component-ready: {error}", - wasm_path.display() - )) - })? - .validate(true) - .encode() - .map_err(|error| { - Report::msg(format!( - "failed to encode note codec module '{}' as a component: {error}", - wasm_path.display() - )) - })?; validate_note_codec_component(&component).map_err(|error| { Report::msg(format!( "note codec crate '{}' produced an invalid component: {error}", @@ -637,11 +624,15 @@ fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { return Err(Report::msg("note codec output is not a component")); }; let world = &resolve.worlds[world_id]; - if !world.imports.is_empty() { - return Err(Report::msg(format!( - "note codec component must have zero imports, found: {:#?}", - world.imports - ))); + // The wasm32-wasip2 standard library wires WASI interfaces into every component. + // Consumers stub them as trapping imports, so only `wasi:*` imports are permitted. + for (key, _) in world.imports.iter() { + let name = resolve.name_world_key(key); + if !name.starts_with("wasi:") { + return Err(Report::msg(format!( + "note codec component may import only `wasi:*` interfaces, found `{name}`" + ))); + } } if world.exports.len() != 1 { return Err(Report::msg(format!( diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index d8e20d6e68..9819d3c2bd 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Note codec crates build for the `wasm32-wasip2` target, and rustc links them directly as + Wasm components. The standard library imports WASI interfaces, so a codec component may + import `wasi:*` interfaces only; consumers stub every import as a trap at instantiation, + so no host capability is reachable from codec code. - Added optional `codec-component` support to the new `miden-note-schema` host crate. It can load author-defined note codecs from a package without adding Wasmtime to the default feature set or the guest SDK dependency graph. diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index 2d4754ca9c..14d777beb0 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -7,13 +7,13 @@ use std::{ }; use tempfile::TempDir; -use wit_component::{ComponentEncoder, DecodedWasm}; +use wit_component::DecodedWasm; use wit_parser::WorldItem; -const WASM_TARGET: &str = "wasm32-unknown-unknown"; +const WASM_TARGET: &str = "wasm32-wasip2"; #[test] -fn minimal_codec_crate_encodes_to_zero_import_component() { +fn minimal_codec_crate_builds_to_wasi_only_component() { if !wasm_target_is_installed() { eprintln!("skipping component export test: {WASM_TARGET} is not installed"); return; @@ -40,23 +40,20 @@ fn minimal_codec_crate_encodes_to_zero_import_component() { .expect("failed to run cargo for the component fixture"); assert_command_succeeded("building the component fixture", &output); - let module = fs::read( + let component = fs::read( target_dir.join(format!("{WASM_TARGET}/release/note_codec_component_fixture.wasm")), ) - .expect("component fixture did not produce a Wasm module"); - let component = ComponentEncoder::default() - .module(&module) - .expect("the fixture is not a component-ready core module") - .validate(true) - .encode() - .expect("failed to encode the codec component"); + .expect("component fixture did not produce a Wasm component"); let DecodedWasm::Component(resolve, world_id) = - wit_component::decode(&component).expect("failed to decode the encoded component") + wit_component::decode(&component).expect("failed to decode the built component") else { - panic!("ComponentEncoder did not produce a component"); + panic!("the fixture build did not produce a component"); }; let world = &resolve.worlds[world_id]; - assert!(world.imports.is_empty(), "codec component imports: {:#?}", world.imports); + for (key, _) in world.imports.iter() { + let name = resolve.name_world_key(key); + assert!(name.starts_with("wasi:"), "unexpected non-WASI import `{name}`"); + } assert_eq!(world.exports.len(), 1); let interface_id = world diff --git a/sdk/note-schema/Cargo.toml b/sdk/note-schema/Cargo.toml index 92ebc549f3..6edf8d6a12 100644 --- a/sdk/note-schema/Cargo.toml +++ b/sdk/note-schema/Cargo.toml @@ -36,4 +36,3 @@ miden-note-codec-wit.workspace = true midenc-frontend-wasm.workspace = true midenc-integration-test-support.workspace = true tempfile.workspace = true -wit-component.workspace = true diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 8691a75f6a..f09a361076 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -49,7 +49,7 @@ impl CodecRegistry { Self::load_from_component(bytes, &schema.custom_type_fqns()) } - /// Loads a zero-import note codec component. + /// Loads a note codec component whose only imports are stubbed WASI interfaces. fn load_from_component(bytes: &[u8], custom_type_fqns: &HashSet) -> Result { let runtime = Arc::new(ComponentRuntime::new(bytes)?); let supported_types = runtime.supported_types()?; @@ -101,9 +101,14 @@ impl ComponentRuntime { Ok(Self { engine, component }) } - /// Instantiates a zero-import component with fresh per-call limits. + /// Instantiates the component with trapping WASI stubs and fresh per-call limits. fn instantiate(&self) -> Result { - let linker = Linker::new(&self.engine); + let mut linker = Linker::new(&self.engine); + // The wasip2 standard library imports WASI interfaces the codec never needs at + // runtime. Stub every import as a trap so nothing outside the component is callable. + linker + .define_unknown_imports_as_traps(&self.component) + .map_err(|error| component_error("stub the note codec imports", error))?; let limits = component_store_limits(); let mut store = Store::new(&self.engine, ComponentStore { limits }); store.limiter(|state| &mut state.limits); @@ -111,7 +116,7 @@ impl ComponentRuntime { .set_fuel(CALL_FUEL) .map_err(|error| component_error("set the note codec fuel budget", error))?; let bindings = NoteCodec::instantiate(&mut store, &self.component, &linker) - .map_err(|error| component_error("instantiate the zero-import note codec", error))?; + .map_err(|error| component_error("instantiate the note codec", error))?; Ok(ComponentInstance { store, bindings }) } @@ -324,11 +329,10 @@ mod tests { use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; use tempfile::TempDir; use wasmtime::ResourceLimiter; - use wit_component::ComponentEncoder; use super::*; - const WASM_TARGET: &str = "wasm32-unknown-unknown"; + const WASM_TARGET: &str = "wasm32-wasip2"; const FIXTURE_FQN: &str = "example:codec-schema/note-storage@1.0.0.ratio"; const DIGEST_FQN: &str = "miden:base/core-types@1.0.0.digest"; const FIXTURE_SCHEMA: &str = r#" @@ -618,16 +622,10 @@ package miden:base@1.0.0 { .expect("failed to start fixture build"); assert_command_succeeded("building the component adapter fixture", &output); - let module = fs::read( + fs::read( target_dir.join(format!("{WASM_TARGET}/release/note_schema_component_fixture.wasm")), ) - .expect("component fixture did not produce its Wasm module"); - ComponentEncoder::default() - .module(&module) - .expect("fixture module is not component-ready") - .validate(true) - .encode() - .expect("failed to encode fixture component") + .expect("component fixture did not produce its Wasm component") } /// Writes a standalone codec crate for the component adapter test. diff --git a/tests/integration-network/src/mockchain/notes/schema.rs b/tests/integration-network/src/mockchain/notes/schema.rs index 76b2a20249..e6a72ea3f0 100644 --- a/tests/integration-network/src/mockchain/notes/schema.rs +++ b/tests/integration-network/src/mockchain/notes/schema.rs @@ -101,7 +101,7 @@ fn transfer_with_storage( #[test] fn dex_note_uses_embedded_schema_and_component_codec() { if !wasm_target_is_installed() { - eprintln!("skipping DEX note schema test: wasm32-unknown-unknown is not installed"); + eprintln!("skipping DEX note schema test: wasm32-wasip2 is not installed"); return; } let note_package = compile_rust_package("../../examples/dex-note", true); @@ -165,7 +165,7 @@ fn wasm_target_is_installed() -> bool { }; String::from_utf8_lossy(&output.stdout) .lines() - .any(|line| line.starts_with("wasm32-unknown-unknown") && line.contains("(installed)")) + .any(|line| line.starts_with("wasm32-wasip2") && line.contains("(installed)")) } /// Asserts that a package carries one named custom section. diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs index a5305e8596..c30b7ebbb6 100644 --- a/tools/cargo-miden/tests/dex_note_codec_build.rs +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -13,9 +13,9 @@ use wit_parser::WorldItem; use crate::utils::{current_dir_lock, workspace_root}; #[test] -fn dex_note_build_embeds_schema_and_zero_import_codec_component() { +fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { if !wasm_target_is_installed() { - eprintln!("skipping DEX note codec build test: wasm32-unknown-unknown is not installed"); + eprintln!("skipping DEX note codec build test: wasm32-wasip2 is not installed"); return; } let _cwd_lock = current_dir_lock(); @@ -76,7 +76,7 @@ fn wasm_target_is_installed() -> bool { }; String::from_utf8_lossy(&output.stdout) .lines() - .any(|line| line.starts_with("wasm32-unknown-unknown") && line.contains("(installed)")) + .any(|line| line.starts_with("wasm32-wasip2") && line.contains("(installed)")) } /// Verifies the sandbox and versioned interface exported by a note codec component. @@ -87,7 +87,10 @@ fn assert_note_codec_component(component: &[u8]) { panic!("note codec section is not a component"); }; let world = &resolve.worlds[world_id]; - assert!(world.imports.is_empty(), "note codec imports: {:#?}", world.imports); + for (key, _) in world.imports.iter() { + let name = resolve.name_world_key(key); + assert!(name.starts_with("wasi:"), "unexpected non-WASI import `{name}`"); + } assert_eq!(world.exports.len(), 1, "unexpected note codec exports: {:#?}", world.exports); let interface = world From 0fde09f543103e89507164d0612b15ec363f901e Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Mon, 24 Aug 2026 07:45:51 +0300 Subject: [PATCH 09/43] test: pin nested fixture builds to the workspace lockfile The component and consumer test fixtures are standalone crates in temporary directories, so their offline cargo builds resolved dependencies from scratch against the local registry index. Any release published after the workspace lockfile was last updated made the fresh resolution select a version that was never downloaded, and the offline build failed. A yanked release breaks such builds the same way. Seed each fixture with the workspace Cargo.lock before the build so resolution reuses the exact versions the workspace build already fetched into the cargo cache. --- sdk/note-bindings/tests/p2id_consumer.rs | 3 +++ sdk/note-codec/tests/component_export.rs | 4 ++++ sdk/note-schema/src/codec_component.rs | 3 +++ 3 files changed, 10 insertions(+) diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs index 95ee900438..cd95778baf 100644 --- a/sdk/note-bindings/tests/p2id_consumer.rs +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -81,6 +81,9 @@ fn generated_p2id_bindings_compile_and_run_in_a_consumer_crate() { let temp = tempfile::tempdir().unwrap(); fs::create_dir_all(temp.path().join("src")).unwrap(); + // Seed the workspace lockfile so the offline build resolves to the versions that the + // workspace already downloaded instead of racing the registry index. + fs::copy(workspace.join("Cargo.lock"), temp.path().join("Cargo.lock")).unwrap(); let second_package_dir = temp.path().join("packages/counter"); fs::create_dir_all(&second_package_dir).unwrap(); let mut second_package = (*p2id).clone(); diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index 14d777beb0..2f9bbdf2d0 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -98,6 +98,10 @@ fn wasm_target_is_installed() -> bool { /// Writes the minimal author codec crate used by the componentization test. fn write_fixture(root: &Path) { fs::create_dir(root.join("src")).expect("failed to create fixture source directory"); + // Seed the workspace lockfile so the offline build resolves to the versions that the + // workspace already downloaded instead of racing the registry index. + fs::copy(workspace_root().join("Cargo.lock"), root.join("Cargo.lock")) + .expect("failed to seed the fixture with the workspace lockfile"); let codec_path = Path::new(env!("CARGO_MANIFEST_DIR")); let manifest = format!( r#"[package] diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index f09a361076..398eecb677 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -631,6 +631,9 @@ package miden:base@1.0.0 { /// Writes a standalone codec crate for the component adapter test. fn write_fixture(root: &Path) { fs::create_dir(root.join("src")).unwrap(); + // Seed the workspace lockfile so the offline build resolves to the versions that the + // workspace already downloaded instead of racing the registry index. + fs::copy(workspace_root().join("Cargo.lock"), root.join("Cargo.lock")).unwrap(); let codec_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../note-codec"); let manifest = format!( r#"[package] From c41ecdcbf25c253a4d7026ef3c7d39f4c3bff1be Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 13:17:43 +0300 Subject: [PATCH 10/43] fix: enforce written-type identity in note schema and codec macros The schema renderer resolved note field types by their last path segment, so a foreign type that only shared the registered name could publish a schema whose shape differs from the guest encoding. #[export_type] now stamps each exported type with a hidden structural shape constant, and both export sites and #[note] sites emit compile-time checks that read the constant through the type path as written. A name-only match no longer compiles. The note site now emits SDK core-type identity guards only for the note struct's own fields. Registry definitions keep the guards at their #[export_type] sites, so a note that refers to exported types from other modules compiles. Both proc-macro registries are now keyed by the expanding crate and replace a re-registration from the same source location. Long-lived macro hosts such as the rust-analyzer proc-macro server no longer accumulate stale registrations that surface as phantom conflicts, and real conflict errors tell the user to restart the macro server when the error appears in an IDE. The macOS link-section name is now derived from the canonical section name constant instead of a hand-truncated copy. --- sdk/base-macros/Cargo.toml | 2 +- sdk/base-macros/src/export_type.rs | 41 +++--- sdk/base-macros/src/note_schema.rs | 50 ++++++-- sdk/base-macros/src/types.rs | 178 +++++++++++++++++++++++--- sdk/base-macros/src/types/tests.rs | 176 ++++++++++++++++++++++++- sdk/note-codec/macros/Cargo.toml | 2 +- sdk/note-codec/macros/src/registry.rs | 88 ++++++++++--- sdk/note-codec/macros/src/tests.rs | 33 ++++- 8 files changed, 506 insertions(+), 64 deletions(-) diff --git a/sdk/base-macros/Cargo.toml b/sdk/base-macros/Cargo.toml index 3ff4fae567..408eb8361e 100644 --- a/sdk/base-macros/Cargo.toml +++ b/sdk/base-macros/Cargo.toml @@ -20,7 +20,7 @@ default = [] internal-wit-emit = ["dep:wit-component"] [dependencies] -proc-macro2.workspace = true +proc-macro2 = { workspace = true, features = ["span-locations"] } quote.workspace = true miden-assembly-syntax.workspace = true miden-mast-package.workspace = true diff --git a/sdk/base-macros/src/export_type.rs b/sdk/base-macros/src/export_type.rs index 64379da583..3c9e92ddb7 100644 --- a/sdk/base-macros/src/export_type.rs +++ b/sdk/base-macros/src/export_type.rs @@ -1,12 +1,29 @@ use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::{Item, parse_macro_input}; use crate::types::{ + ExportedTypeDef, custom_type_shape_assertions, export_type_shape_const, exported_type_from_enum, exported_type_from_struct, register_export_type, - sdk_core_type_identity_guards, + registered_export_type_map, sdk_core_type_identity_guards, }; +/// Builds the guard and identity items emitted next to one exported type. +fn export_type_identity_items( + def: &ExportedTypeDef, + generics: &syn::Generics, + span: proc_macro2::Span, +) -> Result { + let guards = sdk_core_type_identity_guards(def, span)?; + register_export_type(def.clone(), span)?; + // The registry lookup runs after registration so a self-referential type sees itself. + let registry = registered_export_type_map(); + let assertions = custom_type_shape_assertions(def, ®istry, span)?; + let shape_const = export_type_shape_const(def, generics, span); + Ok(quote! { #guards #shape_const #assertions }) +} + pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { if !attr.is_empty() { return syn::Error::new_spanned( @@ -22,25 +39,19 @@ pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { match item { Item::Struct(item_struct) => { let span = item_struct.ident.span(); - match exported_type_from_struct(&item_struct) { - Ok(def) => match sdk_core_type_identity_guards(&def, span) - .and_then(|guards| register_export_type(def, span).map(|()| guards)) - { - Ok(guards) => quote! { #item_struct #guards }.into(), - Err(err) => err.to_compile_error().into(), - }, + match exported_type_from_struct(&item_struct) + .and_then(|def| export_type_identity_items(&def, &item_struct.generics, span)) + { + Ok(items) => quote! { #item_struct #items }.into(), Err(err) => err.to_compile_error().into(), } } Item::Enum(item_enum) => { let span = item_enum.ident.span(); - match exported_type_from_enum(&item_enum) { - Ok(def) => match sdk_core_type_identity_guards(&def, span) - .and_then(|guards| register_export_type(def, span).map(|()| guards)) - { - Ok(guards) => quote! { #item_enum #guards }.into(), - Err(err) => err.to_compile_error().into(), - }, + match exported_type_from_enum(&item_enum) + .and_then(|def| export_type_identity_items(&def, &item_enum.generics, span)) + { + Ok(items) => quote! { #item_enum #items }.into(), Err(err) => err.to_compile_error().into(), } } diff --git a/sdk/base-macros/src/note_schema.rs b/sdk/base-macros/src/note_schema.rs index c28042a004..dbfda57409 100644 --- a/sdk/base-macros/src/note_schema.rs +++ b/sdk/base-macros/src/note_schema.rs @@ -3,7 +3,9 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use heck::ToKebabCase; -use midenc_frontend_wasm_metadata::WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME; +use midenc_frontend_wasm_metadata::{ + WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME, pad_to_link_section_alignment, +}; use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; use quote::quote; use semver::Version; @@ -13,8 +15,8 @@ use wit_bindgen_core::wit_parser::Resolve; use crate::{ manifest_paths::SDK_WIT_SOURCE, types::{ - ExportedField, ExportedTypeDef, ExportedTypeKind, TypeRef, doc_comments, - map_type_to_type_ref, registered_export_types, sdk_core_type_identity_guards, + ExportedField, ExportedTypeDef, ExportedTypeKind, TypeRef, custom_type_shape_assertions, + doc_comments, map_type_to_type_ref, registered_export_types, sdk_core_type_identity_guards, }, util::NOTE_NAMED_FIELDS_ERROR, wit_builder::{WitBody, WitBuilder}, @@ -52,24 +54,34 @@ pub(crate) fn expand_note_storage_schema( ®istry, )?; validate_rendered_note_storage_schema(&rendered)?; - let identity_guards = rendered + // Identity items reference types as they are written at this `#[note]` site, so only the + // root definition (whose field types are in scope here) is checked. Registry definitions + // carry their own identity items at their `#[export_type]` sites. + let root_definition = rendered .definitions + .last() + .expect("a rendered schema always contains its root definition"); + let identity_guards = sdk_core_type_identity_guards(root_definition, rendered.span)?; + let registry_by_rust_name = registry .iter() - .map(|definition| sdk_core_type_identity_guards(definition, rendered.span)) - .collect::, _>>()?; + .cloned() + .map(|definition| (definition.rust_name.clone(), definition)) + .collect(); + let shape_assertions = + custom_type_shape_assertions(root_definition, ®istry_by_rust_name, rendered.span)?; - let mut bytes = rendered.source.into_bytes(); - let padded_len = bytes.len().div_ceil(16) * 16; - bytes.resize(padded_len, 0); + let bytes = pad_to_link_section_alignment(rendered.source.into_bytes()); let bytes_len = bytes.len(); let encoded_bytes = Literal::byte_string(&bytes); + let macos_link_section = macos_note_storage_schema_link_section(); Ok(quote! { - #(#identity_guards)* + #identity_guards + #shape_assertions // Mach-O limits section names to 16 bytes. Wasm uses the canonical section name below. - #[cfg_attr(target_os = "macos", unsafe(link_section = "rodata,miden_note_schem"))] + #[cfg_attr(target_os = "macos", unsafe(link_section = #macos_link_section))] #[cfg_attr( not(target_os = "macos"), unsafe(link_section = #WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME) @@ -80,6 +92,17 @@ pub(crate) fn expand_note_storage_schema( }) } +/// Derives the Mach-O link-section name from the canonical Wasm custom-section name. +fn macos_note_storage_schema_link_section() -> String { + let (segment, section) = WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME + .split_once(',') + .expect("the note storage schema section name must include a segment"); + let section = section + .get(..section.len().min(16)) + .expect("the note storage schema section name must be ASCII"); + format!("{segment},{section}") +} + /// Emits a fixed linker symbol that permits one note struct per crate. pub(crate) fn note_storage_schema_uniqueness_guard() -> TokenStream2 { quote! { @@ -651,6 +674,11 @@ mod tests { expect![[r#"const _ : () = { # [doc (hidden)] # [used] # [unsafe (export_name = "__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD")] static __miden_note_storage_schema_uniqueness_guard : u8 = 0 ; } ;"#]].assert_eq(&tokens); } + #[test] + fn derives_macos_note_storage_schema_link_section() { + assert_eq!(macos_note_storage_schema_link_section(), "rodata,miden_note_schem"); + } + #[test] fn renders_p2id_shaped_schema() { reset_export_type_registry_for_tests(); diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index fa22d840e8..17ba38913f 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -3,7 +3,8 @@ use std::{ sync::{Mutex, OnceLock}, }; -static EXPORTED_TYPES: OnceLock>> = OnceLock::new(); +static EXPORTED_TYPES: OnceLock>>> = + OnceLock::new(); use heck::{ToKebabCase, ToUpperCamelCase}; use proc_macro2::{Span, TokenStream}; @@ -103,43 +104,86 @@ pub(crate) enum StorageFieldType { StorageValue, } +/// One exported-type registration together with the source location that produced it. +#[derive(Clone, Debug)] +struct RegisteredExportType { + def: ExportedTypeDef, + location: ExpansionLocation, +} + +/// Source location of one macro expansion. +/// +/// The location tells a stale re-expansion of an edited item (same location) from a real +/// conflict between two items (different locations). +type ExpansionLocation = (String, usize, usize); + +/// Returns the (file, line, column) location of one expansion span. +fn expansion_location(span: Span) -> ExpansionLocation { + let start = span.start(); + (span.file(), start.line, start.column) +} + +/// Returns the key of the crate whose macro expansion is running. +/// +/// Long-lived macro hosts such as the rust-analyzer proc-macro server expand many crates in +/// one process; the key keeps their registrations apart. +fn macro_invocation_crate_key() -> String { + std::env::var("CARGO_MANIFEST_DIR") + .or_else(|_| std::env::var("CARGO_PKG_NAME")) + .unwrap_or_default() +} + /// Registers one exported type while preserving the first definition seen by the macro process. pub(crate) fn register_export_type(def: ExportedTypeDef, span: Span) -> Result<(), syn::Error> { - let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(Vec::new())); + let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(HashMap::new())); let mut registry = registry.lock().expect("mutex poisoned"); - register_export_type_in(&mut registry, def, span) + let entries = registry.entry(macro_invocation_crate_key()).or_default(); + register_export_type_in(entries, def, span, expansion_location(span)) } /// Applies exported-type identity rules to one registry snapshot. fn register_export_type_in( - registry: &mut Vec, + registry: &mut Vec, def: ExportedTypeDef, span: Span, + location: ExpansionLocation, ) -> Result<(), syn::Error> { - if let Some(existing) = registry.iter().find(|existing| existing.wit_name == def.wit_name) { - if existing.rust_name == def.rust_name && exported_type_shapes_match(existing, &def) { + if let Some(existing) = + registry.iter_mut().find(|existing| existing.def.wit_name == def.wit_name) + { + if existing.def.rust_name == def.rust_name + && exported_type_shapes_match(&existing.def, &def) + { // rust-analyzer can expand the same attribute more than once in one macro process. return Ok(()); } - let identity = if existing.rust_name == def.rust_name { + if existing.location == location { + // A long-lived macro host re-expanded an edited item; replace the stale shape. + existing.def = def; + return Ok(()); + } + + let identity = if existing.def.rust_name == def.rust_name { format!("Rust type `{}`", def.rust_name) } else { - format!("Rust types `{}` and `{}` both map to", existing.rust_name, def.rust_name) + format!("Rust types `{}` and `{}` both map to", existing.def.rust_name, def.rust_name) }; return Err(syn::Error::new( span, format!( "conflicting #[export_type] registration: {identity} WIT type `{}` with different \ identity or shape; the earlier registration is `{}`, while this registration is \ - `{}`. Rename one type or make both registrations structurally identical", + `{}`. Rename one type or make both registrations structurally identical. If this \ + error appears in your IDE after an edit, restart the rust-analyzer proc-macro \ + server", def.wit_name, - describe_exported_type_shape(existing), + describe_exported_type_shape(&existing.def), describe_exported_type_shape(&def), ), )); } - registry.push(def); + registry.push(RegisteredExportType { def, location }); Ok(()) } @@ -194,8 +238,11 @@ fn type_ref_shapes_match(left: &TypeRef, right: &TypeRef) -> bool { .all(|(left, right)| type_ref_shapes_match(left, right)) } -/// Formats one exported definition for a conflicting-registration diagnostic. -fn describe_exported_type_shape(def: &ExportedTypeDef) -> String { +/// Formats the canonical structural shape of one exported definition. +/// +/// The text serves conflict diagnostics and the compile-time shape checks that pin a written +/// type to its `#[export_type]` registration, so it must stay stable and structural. +pub(crate) fn describe_exported_type_shape(def: &ExportedTypeDef) -> String { match &def.kind { ExportedTypeKind::Record { fields } => format!( "record {} {{ {} }}", @@ -307,9 +354,110 @@ fn collect_sdk_core_type_identity_guard( Ok(()) } +/// Emits the hidden constant that records the structural shape of an exported type. +/// +/// Compile-time checks read this constant through a written type path, so a type that only +/// shares the registered name cannot pass for the registered type. +pub(crate) fn export_type_shape_const( + def: &ExportedTypeDef, + generics: &syn::Generics, + span: Span, +) -> TokenStream { + let ident = syn::Ident::new(&def.rust_name, span); + let shape = describe_exported_type_shape(def); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + quote_spanned! {span=> + impl #impl_generics #ident #ty_generics #where_clause { + #[doc(hidden)] + pub const __MIDEN_EXPORT_TYPE_SHAPE: &'static str = #shape; + } + } +} + +/// Emits compile-time checks that pin written custom types to their registrations. +/// +/// Each check reads the shape constant through the type path as it is written at the +/// expansion site. A type that is not the registered type fails to compile, either because +/// it has no shape constant or because its shape text differs. +pub(crate) fn custom_type_shape_assertions( + definition: &ExportedTypeDef, + registry: &HashMap, + span: Span, +) -> Result { + let mut asserted = HashSet::new(); + let mut checks = TokenStream::new(); + visit_exported_type_refs(definition, &mut |type_ref| { + collect_custom_type_shape_assertion(type_ref, registry, span, &mut asserted, &mut checks) + })?; + Ok(checks) +} + +/// Appends one shape check when a custom type path has not already been checked. +fn collect_custom_type_shape_assertion( + type_ref: &TypeRef, + registry: &HashMap, + span: Span, + asserted: &mut HashSet, + checks: &mut TokenStream, +) -> Result<(), syn::Error> { + if !type_ref.is_custom { + return Ok(()); + } + let written_path = type_ref.path.join("::"); + if !asserted.insert(written_path.clone()) { + return Ok(()); + } + let Some(rust_name) = type_ref.path.last() else { + return Ok(()); + }; + let Some(registered) = registry.get(rust_name) else { + // The schema resolution path reports unregistered custom types with full context. + return Ok(()); + }; + let expected = describe_exported_type_shape(registered); + let path = syn::parse_str::(&written_path).map_err(|error| { + syn::Error::new( + span, + format!("failed to reconstruct the path of custom type `{written_path}`: {error}"), + ) + })?; + let message = format!( + "type `{written_path}` does not match the #[export_type] registration named `{}`; write \ + the registered type here or rename one of the types", + registered.rust_name + ); + checks.extend(quote_spanned! {span=> + const _: () = { + const fn __miden_shape_text_eq(left: &str, right: &str) -> bool { + let (left, right) = (left.as_bytes(), right.as_bytes()); + if left.len() != right.len() { + return false; + } + let mut index = 0; + while index < left.len() { + if left[index] != right[index] { + return false; + } + index += 1; + } + true + } + assert!( + __miden_shape_text_eq(<#path>::__MIDEN_EXPORT_TYPE_SHAPE, #expected), + #message + ); + }; + }); + Ok(()) +} + pub(crate) fn registered_export_types() -> Vec { - let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(Vec::new())); - registry.lock().expect("mutex poisoned").clone() + let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(HashMap::new())); + let registry = registry.lock().expect("mutex poisoned"); + registry + .get(¯o_invocation_crate_key()) + .map(|entries| entries.iter().map(|entry| entry.def.clone()).collect()) + .unwrap_or_default() } pub(crate) fn registered_export_type_map() -> HashMap { diff --git a/sdk/base-macros/src/types/tests.rs b/sdk/base-macros/src/types/tests.rs index e8e4881278..afeda11aaa 100644 --- a/sdk/base-macros/src/types/tests.rs +++ b/sdk/base-macros/src/types/tests.rs @@ -406,6 +406,7 @@ fn rejects_same_name_different_shape_export_type_registration() { &mut registry, exported_type_from_struct(&first).unwrap(), Span::call_site(), + ("tests.rs".to_string(), 1, 0), ) .unwrap(); @@ -413,6 +414,7 @@ fn rejects_same_name_different_shape_export_type_registration() { &mut registry, exported_type_from_struct(&second).unwrap(), Span::call_site(), + ("tests.rs".to_string(), 2, 0), ) .unwrap_err() .to_string(); @@ -421,6 +423,7 @@ fn rejects_same_name_different_shape_export_type_registration() { assert!(error.contains("record fee { amount: u64 }")); assert!(error.contains("record fee { amount: word }")); assert!(error.contains("Rename one type or make both registrations structurally identical")); + assert!(error.contains("restart the rust-analyzer proc-macro server")); assert_eq!(registry.len(), 1); } @@ -444,17 +447,54 @@ fn allows_same_shape_export_type_reregistration() { &mut registry, exported_type_from_struct(&first).unwrap(), Span::call_site(), + ("tests.rs".to_string(), 1, 0), ) .unwrap(); register_export_type_in( &mut registry, exported_type_from_struct(&second).unwrap(), Span::call_site(), + ("tests.rs".to_string(), 2, 0), ) .unwrap(); assert_eq!(registry.len(), 1); - assert_eq!(registry[0].docs, vec![" Documentation from rustc's expansion."]); + assert_eq!(registry[0].def.docs, vec![" Documentation from rustc's expansion."]); +} + +#[test] +fn same_location_reregistration_replaces_a_stale_shape() { + let first: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u64, + } + }; + let second: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u32, + } + }; + let mut registry = Vec::new(); + let location = ("tests.rs".to_string(), 1, 0); + + register_export_type_in( + &mut registry, + exported_type_from_struct(&first).unwrap(), + Span::call_site(), + location.clone(), + ) + .unwrap(); + // A long-lived macro host re-expands the edited item from the same location. + register_export_type_in( + &mut registry, + exported_type_from_struct(&second).unwrap(), + Span::call_site(), + location, + ) + .unwrap(); + + assert_eq!(registry.len(), 1); + assert_eq!(describe_exported_type_shape(®istry[0].def), "record fee { amount: u32 }"); } #[test] @@ -516,6 +556,140 @@ fn main() {{}} ); } +#[test] +fn unregistered_same_named_type_fails_the_shape_check() { + let registered: syn::ItemStruct = parse_quote! { + struct Price { + amount: u64, + } + }; + let registered_def = exported_type_from_struct(®istered).unwrap(); + let root: syn::ItemStruct = parse_quote! { + struct Root { + price: other::Price, + } + }; + let root_def = exported_type_from_struct(&root).unwrap(); + let registry = HashMap::from([("Price".to_string(), registered_def.clone())]); + let shape_const = + export_type_shape_const(®istered_def, &syn::Generics::default(), Span::call_site()); + let assertions = custom_type_shape_assertions(&root_def, ®istry, Span::call_site()).unwrap(); + let source = format!( + r#" +mod registered {{ + pub struct Price {{ pub amount: u64 }} + {shape_const} +}} +mod other {{ + pub struct Price {{ pub amount: u32 }} +}} +{assertions} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + !output.status.success(), + "a type without #[export_type] must fail the shape check" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("__MIDEN_EXPORT_TYPE_SHAPE"), + "the missing shape constant is not reported: +{stderr}" + ); +} + +#[test] +fn same_named_type_with_a_different_shape_fails_the_shape_check() { + let registered: syn::ItemStruct = parse_quote! { + struct Price { + amount: u64, + } + }; + let impostor: syn::ItemStruct = parse_quote! { + struct Price { + amount: u32, + } + }; + let registered_def = exported_type_from_struct(®istered).unwrap(); + let impostor_def = exported_type_from_struct(&impostor).unwrap(); + let root: syn::ItemStruct = parse_quote! { + struct Root { + price: other::Price, + } + }; + let root_def = exported_type_from_struct(&root).unwrap(); + let registry = HashMap::from([("Price".to_string(), registered_def.clone())]); + let registered_const = + export_type_shape_const(®istered_def, &syn::Generics::default(), Span::call_site()); + let impostor_const = + export_type_shape_const(&impostor_def, &syn::Generics::default(), Span::call_site()); + let assertions = custom_type_shape_assertions(&root_def, ®istry, Span::call_site()).unwrap(); + let source = format!( + r#" +mod registered {{ + pub struct Price {{ pub amount: u64 }} + {registered_const} +}} +mod other {{ + pub struct Price {{ pub amount: u32 }} + {impostor_const} +}} +{assertions} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a different shape must fail the shape check"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("does not match the #[export_type] registration"), + "the shape diagnostic is not actionable: +{stderr}" + ); +} + +#[test] +fn the_registered_type_passes_the_shape_check() { + let registered: syn::ItemStruct = parse_quote! { + struct Price { + amount: u64, + } + }; + let registered_def = exported_type_from_struct(®istered).unwrap(); + let root: syn::ItemStruct = parse_quote! { + struct Root { + price: registered::Price, + } + }; + let root_def = exported_type_from_struct(&root).unwrap(); + let registry = HashMap::from([("Price".to_string(), registered_def.clone())]); + let shape_const = + export_type_shape_const(®istered_def, &syn::Generics::default(), Span::call_site()); + let assertions = custom_type_shape_assertions(&root_def, ®istry, Span::call_site()).unwrap(); + let source = format!( + r#" +mod registered {{ + pub struct Price {{ pub amount: u64 }} + {shape_const} +}} +{assertions} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + output.status.success(), + "the registered type must pass the shape check: +{}", + String::from_utf8_lossy(&output.stderr) + ); +} + /// Compiles one standalone Rust source string for nominal identity-guard tests. fn compile_rust_source(source: &str) -> Output { let output_dir = tempfile::tempdir().expect("failed to create rustc output directory"); diff --git a/sdk/note-codec/macros/Cargo.toml b/sdk/note-codec/macros/Cargo.toml index 59635db4a3..6b147a5395 100644 --- a/sdk/note-codec/macros/Cargo.toml +++ b/sdk/note-codec/macros/Cargo.toml @@ -22,7 +22,7 @@ heck.workspace = true miden-note-codec-wit.workspace = true miden-note-schema.workspace = true miden-note-schema-codegen.workspace = true -proc-macro2.workspace = true +proc-macro2 = { workspace = true, features = ["span-locations"] } proc-macro-crate = { workspace = true } quote.workspace = true syn.workspace = true diff --git a/sdk/note-codec/macros/src/registry.rs b/sdk/note-codec/macros/src/registry.rs index cb360632de..7687f75aba 100644 --- a/sdk/note-codec/macros/src/registry.rs +++ b/sdk/note-codec/macros/src/registry.rs @@ -16,19 +16,48 @@ pub(crate) struct CodecRegistration { pub(crate) rust_type: String, } +/// One marked codec together with the source location that produced it. +#[derive(Clone, Debug)] +struct RegisteredCodec { + registration: CodecRegistration, + location: ExpansionLocation, +} + /// Schema types and marked codecs registered by earlier macro expansions. #[derive(Default)] struct Registry { - schema: Option, + schema: Option<(String, ExpansionLocation)>, types: BTreeMap, - codecs: BTreeMap, + codecs: BTreeMap, +} + +static REGISTRY: OnceLock>> = OnceLock::new(); + +/// Source location of one macro expansion. +/// +/// The location tells a stale re-expansion of an edited invocation (same location) from a +/// real conflict between two invocations (different locations). +type ExpansionLocation = (String, usize, usize); + +/// Returns the (file, line, column) location of one expansion span. +fn expansion_location(span: Span) -> ExpansionLocation { + let start = span.start(); + (span.file(), start.line, start.column) } -static REGISTRY: OnceLock> = OnceLock::new(); +/// Returns the key of the crate whose macro expansion is running. +/// +/// Long-lived macro hosts such as the rust-analyzer proc-macro server expand many crates in +/// one process; the key keeps their registrations apart. +fn macro_invocation_crate_key() -> String { + std::env::var("CARGO_MANIFEST_DIR") + .or_else(|_| std::env::var("CARGO_PKG_NAME")) + .unwrap_or_default() +} -/// Returns the shared registry. -fn registry() -> &'static Mutex { - REGISTRY.get_or_init(|| Mutex::new(Registry::default())) +/// Returns the shared registry map keyed by expanding crate. +fn registry() -> &'static Mutex> { + REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new())) } /// Records every generated named type, including the storage root. @@ -36,30 +65,39 @@ pub(crate) fn register_schema(schema: &NoteStorageSchema, span: Span) -> syn::Re let mut bindings = BTreeMap::new(); collect_type_bindings(schema.root(), &mut BTreeSet::new(), &mut bindings)?; - let mut registry = registry() + let mut registries = registry() .lock() .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; - match registry.schema.as_deref() { - Some(existing) if existing == schema.wit_text() => return Ok(()), + let registry = registries.entry(macro_invocation_crate_key()).or_default(); + let location = expansion_location(span); + match ®istry.schema { + Some((existing, _)) if existing == schema.wit_text() => return Ok(()), + Some((_, existing_location)) if *existing_location == location => { + // A long-lived macro host re-expanded an edited invocation; replace the stale + // schema and drop the codecs that were registered against it. + registry.codecs.clear(); + } Some(_) => { return Err(syn::Error::new( span, "miden-note-codec supports one note schema per crate; remove the second distinct \ - from_project!, from_package!, or from_wit_text! invocation", + from_project!, from_package!, or from_wit_text! invocation. If this error \ + appears in your IDE after an edit, restart the rust-analyzer proc-macro server", )); } None => {} } - registry.schema = Some(schema.wit_text().to_owned()); + registry.schema = Some((schema.wit_text().to_owned(), location)); registry.types = bindings; Ok(()) } /// Resolves and records a marked codec implementation by generated Rust type name. pub(crate) fn register_codec(rust_name: &str, rust_type: String, span: Span) -> syn::Result<()> { - let mut registry = registry() + let mut registries = registry() .lock() .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; + let registry = registries.entry(macro_invocation_crate_key()).or_default(); let fqn = registry.types.get(rust_name).ok_or_else(|| { syn::Error::new( span, @@ -70,27 +108,41 @@ pub(crate) fn register_codec(rust_name: &str, rust_type: String, span: Span) -> ) })?; let fqn = fqn.clone(); + let location = expansion_location(span); let registration = CodecRegistration { fqn: fqn.clone(), rust_type, }; if let Some(existing) = registry.codecs.get(&fqn) - && existing != ®istration + && existing.registration != registration + // A re-registration from the same source location replaces a stale entry. + && existing.location != location { return Err(syn::Error::new( span, - format!("WIT type `{fqn}` already has a different #[note_codec] implementation"), + format!( + "WIT type `{fqn}` already has a different #[note_codec] implementation. If this \ + error appears in your IDE after an edit, restart the rust-analyzer proc-macro \ + server" + ), )); } - registry.codecs.insert(fqn, registration); + registry.codecs.insert( + fqn, + RegisteredCodec { + registration, + location, + }, + ); Ok(()) } /// Returns marked codecs in canonical FQN order. pub(crate) fn registered_codecs(span: Span) -> syn::Result> { - let registry = registry() + let mut registries = registry() .lock() .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; + let registry = registries.entry(macro_invocation_crate_key()).or_default(); if registry.codecs.is_empty() { return Err(syn::Error::new( span, @@ -99,7 +151,7 @@ pub(crate) fn registered_codecs(span: Span) -> syn::Result (Span, Span) { + let tokens: proc_macro2::TokenStream = "first +second" + .parse() + .expect("the span fixture must parse"); + let mut tokens = tokens.into_iter(); + let first = tokens.next().expect("first fixture token").span(); + let second = tokens.next().expect("second fixture token").span(); + assert_ne!(first.start().line, second.start().line, "fixture spans must differ"); + (first, second) +} + #[test] fn export_requires_earlier_codec_declarations() { let _guard = lock_registry(); From 80b893bd33c9231f74f5c82ac88853cc28586b16 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 13:18:00 +0300 Subject: [PATCH 11/43] fix: validate schemas at attach time and rework the nested codec build The compiler attached note storage schemas without applying the limits every consumer enforces on read, so an oversized schema failed at the consumer instead of the producer build. Package assembly now validates the schema with the consumer implementation before it attaches the section, and all section attachers share the one duplicate-rejecting policy. The nested codec build keyed its Cargo target directory by the staged package content, which rebuilt the whole codec dependency graph on every note change and never removed old directories. The target directory is now shared; the staged-cache path is recorded in dep-info, so a key change still re-expands the codec macros. Stale staged packages are removed after seven days, offline sessions no longer spawn a network-capable rustup install, and custom Miden profiles map to the Cargo dev profile that the independent codec workspace actually defines. --- Cargo.lock | 1 + midenc-compile/Cargo.toml | 1 + midenc-compile/src/cargo.rs | 86 +++++++++++++++---- midenc-compile/src/pipeline/assembly.rs | 85 +++++++++++++----- midenc-compile/src/pipeline/frontends/rust.rs | 6 +- midenc-compile/src/rust.rs | 36 +++++++- 6 files changed, 175 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3760af2c63..ffc8f697ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4160,6 +4160,7 @@ dependencies = [ "miden-assembly-syntax", "miden-mast-package", "miden-note-codec-wit", + "miden-note-schema", "miden-package-registry", "miden-thiserror", "midenc-codegen-masm", diff --git a/midenc-compile/Cargo.toml b/midenc-compile/Cargo.toml index c8291212cd..d4258ec99a 100644 --- a/midenc-compile/Cargo.toml +++ b/midenc-compile/Cargo.toml @@ -39,6 +39,7 @@ midenc-codegen-masm.workspace = true miden-assembly.workspace = true miden-assembly-syntax.workspace = true miden-mast-package.workspace = true +miden-note-schema.workspace = true miden-package-registry.workspace = true miden-note-codec-wit.workspace = true midenc-frontend-wasm.workspace = true diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 8c3ea2b295..b877f570d3 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -7,12 +7,14 @@ use std::{ rc::Rc, string::{String, ToString}, sync::Arc, + time::Duration, vec::Vec, }; use miden_assembly::{SourceManager, serde::Serializable}; use miden_mast_package::Package as MastPackage; use miden_note_codec_wit::NOTE_CODEC_WIT; +use midenc_frontend_wasm_metadata::package_cache; use midenc_hir::Report; use midenc_session::{InputFile, LinkLibrary, Session, miden_project}; use sha2::{Digest, Sha256}; @@ -31,13 +33,12 @@ const NOTE_CODEC_CRATE_PATH: &str = "path"; /// Wasm component, so no separate encoding step is necessary. const NOTE_CODEC_TARGET: &str = "wasm32-wasip2"; -/// Directory used to exchange Miden packages with nested Cargo builds. -const PACKAGE_CACHE_ENV: &str = "MIDENC_PACKAGE_CACHE"; +/// Maximum age of an unused staged note package. +const NOTE_PACKAGE_CACHE_MAX_AGE: Duration = Duration::from_secs(7 * 24 * 60 * 60); -/// One immutable staged package and its build-isolation key. +/// One immutable staged package. struct StagedNotePackage { cache_dir: PathBuf, - build_key: String, } /// Cargo-specific options extracted from the `Compiler` struct. @@ -449,9 +450,13 @@ fn build_note_codec_component( } else { None }; - crate::rust::install_wasm32_target("wasip2", toolchain.as_deref())?; + crate::rust::install_wasm32_target( + "wasip2", + toolchain.as_deref(), + session.options.cargo_offline, + )?; - let cargo_target_dir = work_dir.join("cargo-target").join(&staged_package.build_key); + let cargo_target_dir = work_dir.join("cargo-target"); let mut cargo = Command::new(cargo_path); if let Some(toolchain) = toolchain.as_deref() { cargo.arg(format!("+{toolchain}")); @@ -461,16 +466,16 @@ fn build_note_codec_component( .arg("build") .arg("--manifest-path") .arg(&manifest_path) - .arg("--lib") - .arg("--profile") - .arg(&session.options.profile) + .arg("--lib"); + apply_note_codec_profile(&mut cargo, &session.options.profile); + cargo .arg("--target") .arg(NOTE_CODEC_TARGET) .arg("--target-dir") .arg(&cargo_target_dir) .arg("--message-format") .arg("json-render-diagnostics") - .env(PACKAGE_CACHE_ENV, &staged_package.cache_dir) + .env(package_cache::PACKAGE_CACHE_ENV, &staged_package.cache_dir) .env_remove("CARGO_BUILD_TARGET") .env_remove("CARGO_ENCODED_RUSTFLAGS") .env_remove("RUSTFLAGS") @@ -528,6 +533,7 @@ fn build_note_codec_component( codec_crate_dir.display() )) })?; + gc_staged_note_packages(&staged_package.cache_dir); Ok(component) } @@ -571,10 +577,43 @@ fn stage_note_package( })?; } - Ok(StagedNotePackage { - cache_dir, - build_key, - }) + Ok(StagedNotePackage { cache_dir }) +} + +/// Removes staged note packages that have not changed for more than seven days. +fn gc_staged_note_packages(current_cache_dir: &Path) { + let Some(cache_parent) = current_cache_dir.parent() else { + return; + }; + let Ok(entries) = fs::read_dir(cache_parent) else { + return; + }; + + for entry in entries.flatten() { + let path = entry.path(); + if path == current_cache_dir { + continue; + } + let Ok(metadata) = entry.metadata() else { + continue; + }; + let Ok(modified) = metadata.modified() else { + continue; + }; + let Ok(age) = modified.elapsed() else { + continue; + }; + if metadata.is_dir() && age > NOTE_PACKAGE_CACHE_MAX_AGE { + let _ = fs::remove_dir_all(path); + } + } +} + +/// Maps a Miden build profile to the independent codec Cargo workspace. +fn apply_note_codec_profile(cargo: &mut Command, profile: &str) { + if profile == "release" { + cargo.arg("--release"); + } } /// Applies the outer Cargo resolution policy to a nested command. @@ -938,8 +977,8 @@ mod tests { let first = stage_note_package(root.path(), &codec_crate, &package).unwrap(); let second = stage_note_package(root.path(), &codec_crate, &package).unwrap(); assert_eq!(first.cache_dir, second.cache_dir); - assert_eq!(first.build_key, second.build_key); - assert_eq!(first.build_key.len(), 64); + assert_eq!(first.cache_dir.parent(), Some(root.path().join("package-cache").as_path())); + assert_eq!(first.cache_dir.file_name().unwrap().len(), 64); let package_path = first.cache_dir.join(&*package.name).with_extension(MastPackage::EXTENSION); assert_eq!(fs::read(package_path).unwrap(), package.to_bytes()); @@ -959,6 +998,21 @@ mod tests { assert!(args.iter().any(|arg| arg == "--offline")); } + #[test] + fn note_codec_profile_maps_release_and_custom_profiles() { + let mut release = Command::new("cargo"); + release.arg("build"); + apply_note_codec_profile(&mut release, "release"); + let release_args = release.get_args().collect::>(); + assert_eq!(release_args, ["build", "--release"]); + + let mut custom = Command::new("cargo"); + custom.arg("build"); + apply_note_codec_profile(&mut custom, "size-optimized"); + let custom_args = custom.get_args().collect::>(); + assert_eq!(custom_args, ["build"]); + } + /// Resolves the codec interface from one complete WIT document. fn resolve_codec_interface(wit: &str) -> (Resolve, wit_parser::InterfaceId) { let mut resolve = Resolve::default(); diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index efecd2659d..774ea44586 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -20,7 +20,6 @@ use alloc::vec::Vec; use miden_assembly::TargetAssemblyContext; use miden_mast_package::Package; use midenc_codegen_masm::{MasmComponent, intrinsics}; -use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; use midenc_session::{Session, diagnostics::Report}; /// Apply the session's link inputs to `assembler` before a project is assembled with it. @@ -82,8 +81,8 @@ pub(crate) fn post_process_package( context.target.name.inner(), )?; - attach_account_component_metadata(package, sections.account_component_metadata.as_deref()); - attach_component_wit(package, sections.component_wit.as_deref()); + attach_account_component_metadata(package, sections.account_component_metadata.as_deref())?; + attach_component_wit(package, sections.component_wit.as_deref())?; attach_note_storage_schema(package, sections.note_storage_schema.as_deref())?; extend_rodata_advice_map(package, &component.rodata); @@ -164,11 +163,7 @@ fn attach_note_codec( return Ok(()); }; - use miden_mast_package::SectionId; - let section_id = - SectionId::custom(midenc_frontend_wasm_metadata::PACKAGE_NOTE_CODEC_SECTION_ID).map_err( - |error| Report::msg(format!("the note codec package section id is invalid: {error}")), - )?; + let section_id = midenc_frontend_wasm_metadata::package_note_codec_section_id(); set_unique_section(package, section_id, component, "note codec") } @@ -177,16 +172,28 @@ fn attach_note_storage_schema( package: &mut Package, note_storage_schema: Option<&[u8]>, ) -> Result<(), Report> { - use miden_mast_package::SectionId; - if let Some(bytes) = note_storage_schema { - let section_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID) - .map_err(|err| Report::msg(format!("invalid note storage schema section id: {err}")))?; + validate_note_storage_schema(bytes)?; + let section_id = midenc_frontend_wasm_metadata::package_note_storage_schema_section_id(); set_unique_section(package, section_id, bytes.to_vec(), "note storage schema")?; } Ok(()) } +/// Validates a note storage schema with the rules every consumer applies on read. +/// +/// The check makes the producer build fail with the consumer diagnostic. Without it, a +/// package could publish a schema that every reader rejects, and the author would learn +/// about the defect from a consumer instead of the build. +fn validate_note_storage_schema(bytes: &[u8]) -> Result<(), Report> { + let text = core::str::from_utf8(bytes) + .map_err(|error| Report::msg(format!("the note storage schema is not UTF-8: {error}")))?; + miden_note_schema::NoteStorageSchema::from_wit_text(text).map_err(|error| { + Report::msg(format!("the note storage schema fails consumer validation: {error}")) + })?; + Ok(()) +} + /// Adds one package section and rejects an existing section with the same identifier. fn set_unique_section( package: &mut Package, @@ -209,22 +216,29 @@ fn set_unique_section( fn attach_account_component_metadata( package: &mut Package, account_component_metadata_bytes: Option<&[u8]>, -) { - use miden_mast_package::{Section, SectionId}; +) -> Result<(), Report> { + use miden_mast_package::SectionId; if let Some(bytes) = account_component_metadata_bytes { - package - .sections - .push(Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, bytes.to_vec())); + set_unique_section( + package, + SectionId::ACCOUNT_COMPONENT_METADATA, + bytes.to_vec(), + "account component metadata", + )?; } + Ok(()) } /// Attach the component's public WIT source to the assembled package. -fn attach_component_wit(package: &mut Package, component_wit_bytes: Option<&[u8]>) { - use miden_mast_package::Section; +fn attach_component_wit( + package: &mut Package, + component_wit_bytes: Option<&[u8]>, +) -> Result<(), Report> { if let Some(bytes) = component_wit_bytes { let id = midenc_frontend_wasm_metadata::package_wit_section_id(); - package.sections.push(Section::new(id, bytes.to_vec())); + set_unique_section(package, id, bytes.to_vec(), "component WIT")?; } + Ok(()) } /// Extend the package advice map with the component's rodata segments. @@ -246,6 +260,37 @@ mod tests { use super::*; + #[test] + fn attach_rejects_a_schema_that_consumers_cannot_read() { + let mut package = (*midenc_codegen_masm::intrinsics::load()).clone(); + + let error = attach_note_storage_schema(&mut package, Some(b"not wit at all")) + .unwrap_err() + .to_string(); + + assert!(error.contains("fails consumer validation"), "unexpected error: {error}"); + } + + #[test] + fn attach_accepts_a_schema_that_consumers_can_read() { + let mut package = (*midenc_codegen_masm::intrinsics::load()).clone(); + let schema = b"package test:note-schema@1.0.0; + +interface note-storage { + record note { + value: u64, + } + + type storage = note; +} +"; + + attach_note_storage_schema(&mut package, Some(schema.as_slice())).unwrap(); + + let id = midenc_frontend_wasm_metadata::package_note_storage_schema_section_id(); + assert!(package.sections.iter().any(|section| section.id == id)); + } + #[test] fn unique_sections_reject_an_existing_identifier() { let mut package = (*midenc_codegen_masm::intrinsics::load()).clone(); diff --git a/midenc-compile/src/pipeline/frontends/rust.rs b/midenc-compile/src/pipeline/frontends/rust.rs index 23da1a1678..df09a8e6c8 100644 --- a/midenc-compile/src/pipeline/frontends/rust.rs +++ b/midenc-compile/src/pipeline/frontends/rust.rs @@ -674,7 +674,7 @@ trim-paths = [\"diagnostics\", \"object\"] cargo.args(&cargo_build_args); // Handle the target for buildable commands - crate::rust::install_wasm32_target(wasi, toolchain.as_deref())?; + crate::rust::install_wasm32_target(wasi, toolchain.as_deref(), options.cargo_offline)?; cargo.arg("--target").arg(format!("wasm32-{wasi}")); @@ -2020,6 +2020,7 @@ pub(crate) mod manifest { &cargo_build_args, env, cargo_target_dir.as_deref(), + cargo_opts.offline, )?; assert_eq!(wasm_outputs.len(), 1, "expected only one Wasm artifact"); @@ -2221,6 +2222,7 @@ pub(crate) mod manifest { spawn_args: &[String], env: E, cargo_target_dir: Option<&Path>, + offline: bool, ) -> CompilerResult> where E: IntoIterator, @@ -2245,7 +2247,7 @@ pub(crate) mod manifest { cargo.args(spawn_args); // Handle the target for buildable commands - crate::rust::install_wasm32_target(wasi, None)?; + crate::rust::install_wasm32_target(wasi, None, offline)?; cargo.arg("--target").arg(format!("wasm32-{wasi}")); diff --git a/midenc-compile/src/rust.rs b/midenc-compile/src/rust.rs index 3a6170f0d2..48bcbb6df8 100644 --- a/midenc-compile/src/rust.rs +++ b/midenc-compile/src/rust.rs @@ -13,7 +13,12 @@ use midenc_hir::Report; use crate::CompilerResult; -pub fn install_wasm32_target(wasi: &str, toolchain: Option<&str>) -> CompilerResult<()> { +/// Ensures that the requested Wasm target is installed for the selected Rust toolchain. +pub fn install_wasm32_target( + wasi: &str, + toolchain: Option<&str>, + offline: bool, +) -> CompilerResult<()> { let Some(toolchain) = toolchain.map(ToString::to_string).or_else(rustup_toolchain) else { return Err(Report::msg(format!( "failed to find the `wasm32-{wasi}` target and `rustup` is not available. If you're \ @@ -24,6 +29,34 @@ pub fn install_wasm32_target(wasi: &str, toolchain: Option<&str>) -> CompilerRes log::info!(target: "driver", "verifying wasm32-{wasi} target is installed for the {toolchain} toolchain.."); + let target = format!("wasm32-{wasi}"); + if offline { + let output = Command::new("rustup") + .arg("target") + .arg("list") + .arg("--installed") + .args(["--toolchain", toolchain.as_str()]) + .output() + .map_err(|err| Report::msg(format!("failed to execute rustup: {err}")))?; + if !output.status.success() { + return Err(Report::msg(format!( + "failed to list installed targets for the `{toolchain}` toolchain" + ))); + } + if output + .stdout + .split(|byte| byte.is_ascii_whitespace()) + .any(|installed| installed == target.as_bytes()) + { + log::info!(target: "driver", "{target} is available"); + return Ok(()); + } + return Err(Report::msg(format!( + "the `{target}` target is not installed for the `{toolchain}` toolchain; install it \ + with `rustup target add --toolchain {toolchain} {target}` or drop `--offline`" + ))); + } + let sysroot = get_sysroot(Some(&toolchain))?; if sysroot.join(format!("lib/rustlib/wasm32-{wasi}")).exists() { log::info!(target: "driver", "wasm32-{wasi} is available"); @@ -32,7 +65,6 @@ pub fn install_wasm32_target(wasi: &str, toolchain: Option<&str>) -> CompilerRes log::info!(target: "driver", "installing wasm32-{wasi} target"); - let target = format!("wasm32-{wasi}"); let output = Command::new("rustup") .arg("target") .arg("add") From bd8c55be7780c5116a3e571dce76d07e15a9f48e Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 13:18:00 +0300 Subject: [PATCH 12/43] refactor: consolidate the package-section contract in wasm-metadata The note section identifiers, the 16-byte link-section padding, and the trailing-NUL stripping were each spelled at several call sites with small differences. wasm-metadata now owns section-id accessors and the padding helpers, and every producer and consumer uses them. Package discovery in miden-note-schema now follows the documented package-cache contract: an empty MIDENC_PACKAGE_CACHE value counts as unset, the shared environment constant and file-name helper replace local copies, and a cache miss names the searched directory and the expected file names instead of a wrong fallback path. --- frontend/wasm/src/module/module_env.rs | 7 +- sdk/base-macros/src/component_macro/mod.rs | 5 +- sdk/note-schema/src/artifact.rs | 115 +++++++++++++++--- sdk/note-schema/src/codec_component.rs | 27 ++-- sdk/note-schema/src/schema.rs | 9 +- sdk/note-schema/src/section.rs | 4 +- sdk/wasm-metadata/src/lib.rs | 42 +++++++ .../src/mockchain/notes/schema.rs | 16 ++- .../examples/note_schema_metadata.rs | 10 +- .../cargo-miden/tests/dex_note_codec_build.rs | 8 +- 10 files changed, 182 insertions(+), 61 deletions(-) diff --git a/frontend/wasm/src/module/module_env.rs b/frontend/wasm/src/module/module_env.rs index 561b0cf2eb..496bfeb8bd 100644 --- a/frontend/wasm/src/module/module_env.rs +++ b/frontend/wasm/src/module/module_env.rs @@ -7,6 +7,7 @@ use midenc_frontend_wasm_metadata::{ FrontendMetadata, PackageSections, WASM_ACCOUNT_COMPONENT_METADATA_CUSTOM_SECTION_NAME, WASM_COMPONENT_WIT_CUSTOM_SECTION_NAME, WASM_FRONTEND_METADATA_CUSTOM_SECTION_NAME, WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME, count_top_level_wit_packages, decode_section, + trim_trailing_nuls, }; use midenc_hir::{FxHashMap, FxHashSet, Ident, interner::Symbol}; use midenc_session::diagnostics::{DiagnosticsHandler, IntoDiagnostic, Report, Severity}; @@ -1125,9 +1126,3 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> { Ok(()) } } - -/// Removes the zero padding from a metadata section payload. -fn trim_trailing_nuls(bytes: &[u8]) -> &[u8] { - let len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); - &bytes[..len] -} diff --git a/sdk/base-macros/src/component_macro/mod.rs b/sdk/base-macros/src/component_macro/mod.rs index 0ca8f7bdbc..053781aeff 100644 --- a/sdk/base-macros/src/component_macro/mod.rs +++ b/sdk/base-macros/src/component_macro/mod.rs @@ -400,9 +400,8 @@ fn expand_component_storage( let component_metadata = acc_builder.build(call_site_span.into())?; - let mut metadata_bytes = component_metadata.to_bytes(); - let padded_len = metadata_bytes.len().div_ceil(16) * 16; - metadata_bytes.resize(padded_len, 0); + let metadata_bytes = + midenc_frontend_wasm_metadata::pad_to_link_section_alignment(component_metadata.to_bytes()); let link_section = generate_link_section(&metadata_bytes); let runtime_boilerplate = runtime_boilerplate(); diff --git a/sdk/note-schema/src/artifact.rs b/sdk/note-schema/src/artifact.rs index 9e48fafc05..a82c9c3831 100644 --- a/sdk/note-schema/src/artifact.rs +++ b/sdk/note-schema/src/artifact.rs @@ -6,12 +6,10 @@ use std::{ }; use miden_mast_package::Package; +use midenc_frontend_wasm_metadata::package_cache; use crate::{Error, NoteStorageSchema, Result}; -/// Directory used to exchange Miden packages with nested Cargo builds. -const PACKAGE_CACHE_ENV: &str = "MIDENC_PACKAGE_CACHE"; - /// A loaded package artifact and its note storage schema. pub struct NotePackageArtifact { path: PathBuf, @@ -51,10 +49,18 @@ impl<'a> NotePackageResolver<'a> { project_dir.display() ))); } - let package_path = resolve_project_package(&project_dir) + let stems = project_package_stems(&project_dir); + let cache_dir = package_cache_dir() + .map_err(|error| Error::new(format!("{}: {error}", self.macro_crate)))?; + let package_path = resolve_project_package(&project_dir, &stems, cache_dir.as_deref()) .map_err(|error| Error::new(format!("{}: {error}", self.macro_crate)))? .ok_or_else(|| { - Error::new(missing_project_package_message(self.macro_crate, &project_dir)) + Error::new(missing_project_package_message( + self.macro_crate, + &project_dir, + cache_dir.as_deref(), + &stems, + )) })?; self.load_package(package_path) } @@ -115,22 +121,32 @@ impl<'a> NotePackageResolver<'a> { } /// Resolves one project package by package identity and output-directory priority. -fn resolve_project_package(project_dir: &Path) -> Result> { - let stems = project_package_stems(project_dir); - - if let Some(cache_dir) = env::var_os(PACKAGE_CACHE_ENV) { - return find_project_package_in_dir(&absolutize(PathBuf::from(cache_dir))?, &stems); +fn resolve_project_package( + project_dir: &Path, + stems: &[String], + cache_dir: Option<&Path>, +) -> Result> { + if let Some(cache_dir) = cache_dir { + return Ok(find_project_package_in_cache(cache_dir, stems)); } let profiles = candidate_profiles(); for output_dir in project_output_dirs(project_dir, &profiles) { - if let Some(package) = find_project_package_in_dir(&output_dir, &stems)? { + if let Some(package) = find_project_package_in_dir(&output_dir, stems)? { return Ok(Some(package)); } } Ok(None) } +/// Returns the configured package cache directory, treating an empty value as unset. +fn package_cache_dir() -> Result> { + env::var_os(package_cache::PACKAGE_CACHE_ENV) + .filter(|value| !value.is_empty()) + .map(|value| absolutize(PathBuf::from(value))) + .transpose() +} + /// Returns Cargo and Miden profile names in lookup order. fn candidate_profiles() -> Vec { let mut profiles = Vec::new(); @@ -147,8 +163,8 @@ fn candidate_profiles() -> Vec { fn project_output_dirs(project_dir: &Path, profiles: &[String]) -> Vec { let mut dirs = Vec::new(); - // Keep this policy in parity with dependency_output_dirs in base-macros/src/fpi.rs. The - // crates cannot share the implementation without adding the full SDK macro dependency graph. + // Keep this policy in parity with base-macros/src/dependency_package.rs. The crates cannot + // share the implementation without adding the full SDK macro dependency graph. push_profile_dirs(&mut dirs, project_dir.join("target"), profiles); push_manifest_ancestor_target_profile_dirs(&mut dirs, project_dir, profiles); push_ancestor_target_profile_dirs(&mut dirs, project_dir, profiles); @@ -201,6 +217,14 @@ fn push_manifest_ancestor_target_profile_dirs( } } +/// Finds a project package in the build-owned package cache. +fn find_project_package_in_cache(dir: &Path, stems: &[String]) -> Option { + stems + .iter() + .map(|stem| dir.join(package_cache::package_file_name(stem))) + .find(|path| path.is_file()) +} + /// Finds a package in one output directory by ordered package identity. fn find_project_package_in_dir(dir: &Path, stems: &[String]) -> Result> { if !dir.is_dir() { @@ -222,8 +246,8 @@ fn find_project_package_in_dir(dir: &Path, stems: &[String]) -> Result, profile: String) { } /// Formats the diagnostic for a project without a built package. -fn missing_project_package_message(macro_crate: &str, project_dir: &Path) -> String { +fn missing_project_package_message( + macro_crate: &str, + project_dir: &Path, + cache_dir: Option<&Path>, + stems: &[String], +) -> String { let manifest = project_dir.join("Cargo.toml"); let build = if manifest.is_file() { format!("cargo miden build --manifest-path {} --release", manifest.display()) } else { "cargo miden build --release".to_owned() }; + if let Some(cache_dir) = cache_dir { + let expected_files = stems + .iter() + .map(|stem| format!("'{}'", package_cache::package_file_name(stem))) + .collect::>() + .join(", "); + return format!( + "{macro_crate} could not find a built `.masp` package for note project '{}'. Expected \ + one of these package names: {expected_files}. Searched {} directory '{}'. Build the \ + note project first with `{build}` so the package is available during macro expansion.", + project_dir.display(), + package_cache::PACKAGE_CACHE_ENV, + cache_dir.display(), + ); + } format!( "{macro_crate} could not find a built `.masp` package under '{}'. Build the note project \ first with `{build}`.", @@ -310,8 +354,8 @@ mod tests { use std::fs; use super::{ - find_project_package_in_dir, missing_project_package_message, project_output_dirs, - project_package_stems, + find_project_package_in_cache, find_project_package_in_dir, + missing_project_package_message, project_output_dirs, project_package_stems, }; #[test] @@ -352,9 +396,42 @@ mod tests { let temp = tempfile::tempdir().unwrap(); fs::write(temp.path().join("Cargo.toml"), "[package]\nname='note'\nversion='0.1.0'") .unwrap(); - let message = missing_project_package_message("test-note-macro", temp.path()); + let stems = project_package_stems(temp.path()); + let message = missing_project_package_message("test-note-macro", temp.path(), None, &stems); assert!(message.contains("test-note-macro")); assert!(message.contains("cargo miden build --manifest-path")); assert!(message.contains("--release")); } + + #[test] + fn missing_cached_package_diagnostic_names_cache_and_expected_files() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='test-note'\nversion='0.1.0'") + .unwrap(); + let cache_dir = temp.path().join("package-cache"); + let stems = project_package_stems(temp.path()); + + let message = missing_project_package_message( + "test-note-macro", + temp.path(), + Some(&cache_dir), + &stems, + ); + + assert!(message.contains(&cache_dir.display().to_string())); + assert!(message.contains("'test-note.masp'")); + assert!(message.contains("MIDENC_PACKAGE_CACHE")); + assert!(!message.contains("target/miden/")); + } + + #[test] + fn cache_lookup_uses_the_shared_package_file_name() { + let temp = tempfile::tempdir().unwrap(); + let package = temp.path().join("note.with.dot.masp"); + fs::write(&package, b"package").unwrap(); + + let found = find_project_package_in_cache(temp.path(), &["note.with.dot".to_owned()]); + + assert_eq!(found, Some(package)); + } } diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 398eecb677..140ecdec08 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -4,7 +4,7 @@ use std::{collections::HashSet, sync::Arc}; use miden_field::Felt; use miden_mast_package::Package; -use midenc_frontend_wasm_metadata::PACKAGE_NOTE_CODEC_SECTION_ID; +use midenc_frontend_wasm_metadata::{PACKAGE_NOTE_CODEC_SECTION_ID, package_note_codec_section_id}; use wasmtime::{ Config, Engine, Store, StoreLimits, StoreLimitsBuilder, component::{Component, Linker}, @@ -45,7 +45,11 @@ impl CodecRegistry { /// Loads the note codec component from a package and registers all reported types. pub fn load_from_package(package: &Package) -> Result { let schema = NoteStorageSchema::from_package(package)?; - let bytes = crate::section::unique_package_section(package, PACKAGE_NOTE_CODEC_SECTION_ID)?; + let bytes = crate::section::unique_package_section( + package, + package_note_codec_section_id(), + PACKAGE_NOTE_CODEC_SECTION_ID, + )?; Self::load_from_component(bytes, &schema.custom_type_fqns()) } @@ -323,10 +327,10 @@ mod tests { operations::Operation, }; use miden_mast_package::{ - PackageExport, PackageId, PathBuf as MastPathBuf, ProcedureExport, Section, SectionId, - TargetType, Version, + PackageExport, PackageId, PathBuf as MastPathBuf, ProcedureExport, Section, TargetType, + Version, }; - use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; + use midenc_frontend_wasm_metadata::package_note_storage_schema_section_id; use tempfile::TempDir; use wasmtime::ResourceLimiter; @@ -435,13 +439,10 @@ package miden:base@1.0.0 { let component = build_fixture_component(); let mut package = test_package(); package.sections.push(Section::new( - SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(), + package_note_storage_schema_section_id(), FIXTURE_SCHEMA.as_bytes().to_vec(), )); - package.sections.push(Section::new( - SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID).unwrap(), - component, - )); + package.sections.push(Section::new(package_note_codec_section_id(), component)); let registry = CodecRegistry::load_from_package(&package).unwrap(); let codec = registry.codec(FIXTURE_FQN).expect("fixture ratio codec was not registered"); @@ -533,8 +534,8 @@ package miden:base@1.0.0 { #[test] fn package_readers_reject_duplicate_schema_and_codec_sections() { - let schema_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(); - let codec_id = SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID).unwrap(); + let schema_id = package_note_storage_schema_section_id(); + let codec_id = package_note_codec_section_id(); let mut duplicate_schema = test_package(); duplicate_schema .sections @@ -552,7 +553,7 @@ package miden:base@1.0.0 { let mut duplicate_codec = test_package(); duplicate_codec.sections.push(Section::new( - SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(), + package_note_storage_schema_section_id(), FIXTURE_SCHEMA.as_bytes().to_vec(), )); duplicate_codec.sections.push(Section::new(codec_id.clone(), Vec::new())); diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs index 3377a2acc2..23e2167f62 100644 --- a/sdk/note-schema/src/schema.rs +++ b/sdk/note-schema/src/schema.rs @@ -7,7 +7,10 @@ use std::{ use miden_mast_package::Package; use miden_protocol::MAX_NOTE_STORAGE_ITEMS; -use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; +use midenc_frontend_wasm_metadata::{ + PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, package_note_storage_schema_section_id, + trim_trailing_nuls, +}; use wit_parser::{Resolve, Type, TypeDefKind, TypeId, TypeOwner}; use crate::{ @@ -229,11 +232,11 @@ impl NoteStorageSchema { pub fn from_package(package: &Package) -> Result { let bytes = crate::section::unique_package_section( package, + package_note_storage_schema_section_id(), PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, )?; ensure_schema_byte_limit(bytes.len())?; - let unpadded_len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); - let text = core::str::from_utf8(&bytes[..unpadded_len]).map_err(|err| { + let text = core::str::from_utf8(trim_trailing_nuls(bytes)).map_err(|err| { Error::new(format!("note storage schema section is not valid UTF-8: {err}")) })?; Self::from_wit_text(text) diff --git a/sdk/note-schema/src/section.rs b/sdk/note-schema/src/section.rs index 0b4e2b2653..6d19177ab7 100644 --- a/sdk/note-schema/src/section.rs +++ b/sdk/note-schema/src/section.rs @@ -7,11 +7,9 @@ use crate::{Error, Result}; /// Returns the only package section with `section_name`. pub(crate) fn unique_package_section<'a>( package: &'a Package, + section_id: SectionId, section_name: &str, ) -> Result<&'a [u8]> { - let section_id = SectionId::custom(section_name).map_err(|error| { - Error::new(format!("invalid package section id `{section_name}`: {error}")) - })?; let mut matches = package.sections.iter().filter(|section| section.id == section_id); let section = matches.next().ok_or_else(|| { Error::new(format!("package does not contain the `{section_name}` section")) diff --git a/sdk/wasm-metadata/src/lib.rs b/sdk/wasm-metadata/src/lib.rs index f5afd5649b..00f116f9f2 100644 --- a/sdk/wasm-metadata/src/lib.rs +++ b/sdk/wasm-metadata/src/lib.rs @@ -41,9 +41,41 @@ pub const WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME: &str = "rodata,miden_not /// Name of the Miden package section that stores a note storage schema. pub const PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID: &str = "note_storage_schema"; +/// Returns the Miden package section id that stores a note storage schema. +/// +/// One accessor keeps every producer and consumer on the same section contract. +pub fn package_note_storage_schema_section_id() -> miden_mast_package::SectionId { + miden_mast_package::SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID) + .expect("the note storage schema section id must be a valid custom section id") +} + /// Name of the Miden package section that stores a note codec. pub const PACKAGE_NOTE_CODEC_SECTION_ID: &str = "note_codec"; +/// Returns the Miden package section id that stores a note codec. +/// +/// One accessor keeps every producer and consumer on the same section contract. +pub fn package_note_codec_section_id() -> miden_mast_package::SectionId { + miden_mast_package::SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID) + .expect("the note codec section id must be a valid custom section id") +} + +/// Pads metadata bytes with NUL bytes to the 16-byte link-section alignment. +/// +/// Account-component-metadata link sections use this 16-byte padding. Note storage schema link +/// sections use the same contract. +pub fn pad_to_link_section_alignment(mut bytes: Vec) -> Vec { + let padded_len = bytes.len().div_ceil(16) * 16; + bytes.resize(padded_len, 0); + bytes +} + +/// Removes NUL padding from the end of a link-section payload. +pub fn trim_trailing_nuls(bytes: &[u8]) -> &[u8] { + let len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); + &bytes[..len] +} + /// The filesystem package-cache exchange contract. /// /// The compiler publishes compiled dependency packages — and its recorded dependency @@ -368,6 +400,16 @@ mod tests { use super::*; + /// Ensures shared link-section padding is reversible and aligned. + #[test] + fn link_section_padding_round_trips() { + let source = b"note schema"; + let padded = pad_to_link_section_alignment(source.to_vec()); + + assert_eq!(padded.len() % 16, 0); + assert_eq!(trim_trailing_nuls(&padded), source); + } + /// Ensures a single embedded component WIT source is recognized as one package. #[test] fn component_wit_counts_a_single_package_declaration() { diff --git a/tests/integration-network/src/mockchain/notes/schema.rs b/tests/integration-network/src/mockchain/notes/schema.rs index e6a72ea3f0..3f9619fa54 100644 --- a/tests/integration-network/src/mockchain/notes/schema.rs +++ b/tests/integration-network/src/mockchain/notes/schema.rs @@ -19,6 +19,7 @@ use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChain}; use midenc_frontend_wasm_metadata::{ PACKAGE_NOTE_CODEC_SECTION_ID, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, + package_note_codec_section_id, package_note_storage_schema_section_id, }; use super::super::support::{ @@ -105,8 +106,16 @@ fn dex_note_uses_embedded_schema_and_component_codec() { return; } let note_package = compile_rust_package("../../examples/dex-note", true); - assert_package_section(¬e_package, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID); - assert_package_section(¬e_package, PACKAGE_NOTE_CODEC_SECTION_ID); + assert_package_section( + ¬e_package, + package_note_storage_schema_section_id(), + PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, + ); + assert_package_section( + ¬e_package, + package_note_codec_section_id(), + PACKAGE_NOTE_CODEC_SECTION_ID, + ); let schema = NoteStorageSchema::from_package(¬e_package).unwrap(); let codecs = CodecRegistry::load_from_package(¬e_package).unwrap(); @@ -169,8 +178,7 @@ fn wasm_target_is_installed() -> bool { } /// Asserts that a package carries one named custom section. -fn assert_package_section(package: &Package, name: &str) { - let id = SectionId::custom(name).unwrap(); +fn assert_package_section(package: &Package, id: SectionId, name: &str) { assert!( package.sections.iter().any(|section| section.id == id), "package does not contain the `{name}` section" diff --git a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs index ad87857087..9980e346dd 100644 --- a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs +++ b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs @@ -2,10 +2,10 @@ use std::sync::Arc; -use miden_mast_package::{Package, SectionId}; +use miden_mast_package::Package; use midenc_expect_test::{Expect, expect}; use midenc_frontend_wasm::WasmTranslationConfig; -use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; +use midenc_frontend_wasm_metadata::{package_note_storage_schema_section_id, trim_trailing_nuls}; use wit_bindgen_core::wit_parser::{Resolve, Type as WitType, TypeDefKind}; use crate::CompilerTest; @@ -27,8 +27,7 @@ fn compile_project(project_path: &str) -> Arc { /// Returns the unpadded note storage schema text from a package. fn note_storage_schema(package: &Package) -> &str { - let section_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID) - .expect("schema section id must be valid"); + let section_id = package_note_storage_schema_section_id(); let bytes = package .sections .iter() @@ -37,8 +36,7 @@ fn note_storage_schema(package: &Package) -> &str { .data .as_ref(); assert_eq!(bytes.len() % 16, 0, "schema payload must use 16-byte padding"); - let len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); - str::from_utf8(&bytes[..len]).expect("note storage schema must be UTF-8") + str::from_utf8(trim_trailing_nuls(bytes)).expect("note storage schema must be UTF-8") } /// Checks a schema golden and resolves its root storage alias with wit-parser. diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs index c30b7ebbb6..ca10aac998 100644 --- a/tools/cargo-miden/tests/dex_note_codec_build.rs +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -3,9 +3,9 @@ use std::env; use cargo_miden::run; -use miden_mast_package::{Package, SectionId}; +use miden_mast_package::Package; use midenc_frontend_wasm_metadata::{ - PACKAGE_NOTE_CODEC_SECTION_ID, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, + package_note_codec_section_id, package_note_storage_schema_section_id, }; use wit_component::DecodedWasm; use wit_parser::WorldItem; @@ -46,13 +46,13 @@ fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { let package = Package::deserialize_from_file(&output[0]) .expect("failed to read the built dex-note package"); - let schema_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(); + let schema_id = package_note_storage_schema_section_id(); assert!( package.sections.iter().any(|section| section.id == schema_id), "dex-note package has no note storage schema section" ); - let codec_id = SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID).unwrap(); + let codec_id = package_note_codec_section_id(); let codec = package .sections .iter() From 52bdfdf9650729d905da91fc33fed82fd56d0017 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 13:18:13 +0300 Subject: [PATCH 13/43] test: serialize the p2id end-to-end builds and document their CI lane The two p2id end-to-end tests build the same example projects in place and rewrite the same package file, and the release-profile CI job runs them concurrently from two test binaries. Each test now holds a shared advisory file lock in the workspace target directory for the whole build-write-consume span. The unit-job exclusion in CI now says why these tests run in the release-profile job. --- .github/workflows/ci.yml | 2 ++ sdk/note-bindings/tests/p2id_consumer.rs | 23 +++++++++++++++--- sdk/note-schema/tests/p2id_package.rs | 30 ++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 175cf737df..414b91159b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,8 @@ jobs: run: | cargo make check --tests - name: Test + # The p2id end-to-end tests drive full nested Cargo builds, so the + # release-profile job runs them. run: | cargo make test -E 'not (package(midenc-integration-tests) or package(midenc-integration-network-tests) or package(cargo-miden) or package(midenc-template-tests) or test(~p2id_schema_builds_and_decodes_account_id_storage) or test(~generated_p2id_bindings_compile_and_run_in_a_consumer_crate))' diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs index cd95778baf..074141ff1f 100644 --- a/sdk/note-bindings/tests/p2id_consumer.rs +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -2,14 +2,15 @@ use std::{ env, fs, + fs::File, path::{Path, PathBuf}, process::Command, sync::Arc, }; -use miden_mast_package::{Package, Section, SectionId}; +use miden_mast_package::{Package, Section}; use midenc_frontend_wasm::WasmTranslationConfig; -use midenc_frontend_wasm_metadata::PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID; +use midenc_frontend_wasm_metadata::package_note_storage_schema_section_id; use midenc_integration_test_support::CompilerTest; /// Compiles one Cargo Miden project without debug output. @@ -27,6 +28,21 @@ fn workspace_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() } +/// Locks the shared p2id example outputs for the full build and consume span. +fn p2id_build_lock(workspace: &Path) -> File { + let target_dir = workspace.join("target"); + fs::create_dir_all(&target_dir).expect("failed to create the workspace target directory"); + let lock = File::options() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(target_dir.join("p2id-end-to-end-build.lock")) + .expect("failed to open the p2id end-to-end build lock"); + lock.lock().expect("failed to lock the p2id end-to-end build"); + lock +} + /// Returns the native rustc host target. fn host_target() -> String { let output = Command::new(env::var_os("RUSTC").unwrap_or_else(|| "rustc".into())) @@ -66,6 +82,7 @@ fn workspace_patch_section(workspace: &Path) -> String { #[test] fn generated_p2id_bindings_compile_and_run_in_a_consumer_crate() { let workspace = workspace_root(); + let _build_lock = p2id_build_lock(&workspace); let examples = workspace.join("examples"); let wallet_dir = examples.join("basic-wallet"); let wallet = compile_project(&wallet_dir); @@ -87,7 +104,7 @@ fn generated_p2id_bindings_compile_and_run_in_a_consumer_crate() { let second_package_dir = temp.path().join("packages/counter"); fs::create_dir_all(&second_package_dir).unwrap(); let mut second_package = (*p2id).clone(); - let schema_id = SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID).unwrap(); + let schema_id = package_note_storage_schema_section_id(); second_package.sections.retain(|section| section.id != schema_id); second_package.sections.push(Section::new( schema_id, diff --git a/sdk/note-schema/tests/p2id_package.rs b/sdk/note-schema/tests/p2id_package.rs index 917ced2114..795d2264dd 100644 --- a/sdk/note-schema/tests/p2id_package.rs +++ b/sdk/note-schema/tests/p2id_package.rs @@ -1,6 +1,10 @@ //! End-to-end test for a schema embedded in the p2id note package. -use std::{path::Path, sync::Arc}; +use std::{ + fs::{self, File}, + path::{Path, PathBuf}, + sync::Arc, +}; use miden_mast_package::Package; use miden_note_schema::{NoteStorage, NoteStorageSchema}; @@ -18,9 +22,31 @@ fn compile_project(project_path: &Path) -> Arc { test.compile_package() } +/// Returns the compiler workspace root. +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() +} + +/// Locks the shared p2id example outputs for the full build and consume span. +fn p2id_build_lock(workspace: &Path) -> File { + let target_dir = workspace.join("target"); + fs::create_dir_all(&target_dir).expect("failed to create the workspace target directory"); + let lock = File::options() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(target_dir.join("p2id-end-to-end-build.lock")) + .expect("failed to open the p2id end-to-end build lock"); + lock.lock().expect("failed to lock the p2id end-to-end build"); + lock +} + #[test] fn p2id_schema_builds_and_decodes_account_id_storage() { - let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples"); + let workspace = workspace_root(); + let _build_lock = p2id_build_lock(&workspace); + let examples = workspace.join("examples"); let wallet_dir = examples.join("basic-wallet"); let wallet = compile_project(&wallet_dir); wallet From dcd72392ffd2f9427c7ecd3adb7c1c14fb5f6794 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 13:18:13 +0300 Subject: [PATCH 14/43] docs: document note schema emission rules and codec usage The #[note] and #[export_type] reference documentation now covers the emitted WIT schema section, the supported field surface, the define-before-use ordering, the written-type identity checks, and the one-note-per-crate guard. The migration guide explains the new restriction, and the codec and bindings crates carry usage examples with the required macro ordering. The SDK changelog lost the sections that a rebase auto-merge had duplicated, and the note entries now reference their issue. --- sdk/CHANGELOG.md | 9 +++------ sdk/base-macros/src/lib.rs | 20 ++++++++++++++++++-- sdk/note-bindings/src/lib.rs | 10 ++++++++++ sdk/note-codec/src/lib.rs | 16 ++++++++++++++++ sdk/sdk/MIGRATION.md | 9 +++++++++ 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 9819d3c2bd..29861fffba 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -45,21 +45,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Note storage schema handling now rejects conflicting `#[export_type]` registrations and local types that only collide by name with SDK core types, resolves schema types through a bounded, memoized graph, uses one canonical standard-leaf set across consumers, and caps untrusted author - codec components before compilation and during table allocation. + codec components before compilation and during table allocation. (#1307) - Note package macros now select artifacts by canonical package identity and support shared Cargo target directories. The note codec macros support renamed facade dependencies, reject a second distinct schema in one crate, and report `export_codecs!` calls that appear before all codec - declarations. -- `adv_load_preimage` no longer truncates huge word counts into an undersized buffer on wasm32 - (a potential guest heap overflow); it now traps for counts of `2^30` words or more, whose felt - total cannot be represented in the 32-bit address space #1291 + declarations. (#1307) ### Migration and breaking changes - `#[note]` storage types now require named-field or unit structs. Tuple structs no longer compile, and note storage fields no longer accept `Vec`. Follow the [migration guidance](./sdk/MIGRATION.md#rewrite-tuple-note-and-vec-storage-layouts) to preserve - field order with named fields and replace dynamic vectors with a fixed schema. + field order with named fields and replace dynamic vectors with a fixed schema. (#1307) ## [0.14.0] diff --git a/sdk/base-macros/src/lib.rs b/sdk/base-macros/src/lib.rs index 336a08d74b..e0fc430acc 100644 --- a/sdk/base-macros/src/lib.rs +++ b/sdk/base-macros/src/lib.rs @@ -286,8 +286,11 @@ pub fn account_procedure( component_macro::expand_account_procedure(attr, item) } -/// Generates an equvalent type in the WIT interface. -/// Required for every type mentioned in the public methods of an account component. +/// Generates an equivalent type in the WIT interface. +/// +/// Use this macro for every custom type in the public methods of an account component. Exported +/// records and enums can also appear in note storage schemas. The macro emits a hidden shape +/// constant that lets `#[note]` confirm the exact Rust type at compile time. /// /// Intended to be used together with `#[component]` attribute macro. #[proc_macro_attribute] @@ -305,6 +308,19 @@ pub fn export_type( /// - the associated inherent `impl` block that contains an entrypoint method annotated with /// `#[note_script]` /// +/// # Note storage schema +/// +/// A named-field `#[note]` struct emits a WIT storage schema into the package's +/// `note_storage_schema` section. Field doc comments are copied into the schema. +/// +/// A note struct must use named fields or no fields. A tuple struct is a compile error. A storage +/// field cannot use `Vec`. Apply `#[export_type]` to each nested custom type, and place that type +/// definition before the `#[note]` struct. Each field must use the exact registered +/// `#[export_type]` Rust type. A different type with the same name fails the hidden shape check. +/// +/// A crate can contain only one `#[note]` struct. A second struct fails at link time because it +/// defines the duplicate `__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD` symbol. +/// /// # Foreign Procedure Invocation (FPI) /// /// Use `#[account(...)]` on an empty struct to generate typed active and foreign account wrappers diff --git a/sdk/note-bindings/src/lib.rs b/sdk/note-bindings/src/lib.rs index 082f8b20fc..5487f6ada7 100644 --- a/sdk/note-bindings/src/lib.rs +++ b/sdk/note-bindings/src/lib.rs @@ -1,4 +1,14 @@ //! Typed host bindings for Miden note storage schemas. +//! +//! Generate bindings from a built package, and then use the generated storage type. +//! +//! ```ignore +//! miden_note_bindings::from_package!("../p2id-note/target/miden/release/p2id.masp"); +//! +//! fn decode(storage: &miden_note_bindings::NoteStorage) -> P2idNote { +//! P2idNote::from_note_storage(storage).unwrap() +//! } +//! ``` #![deny(missing_docs)] diff --git a/sdk/note-codec/src/lib.rs b/sdk/note-codec/src/lib.rs index 62e2ae1782..fd7cb6b38e 100644 --- a/sdk/note-codec/src/lib.rs +++ b/sdk/note-codec/src/lib.rs @@ -3,6 +3,22 @@ //! Codec components exchange field elements as canonical `u64` values. This crate checks every //! integer before it enters [`Felt`], so a component cannot introduce a reduced or ambiguous field //! representation. +//! +//! Call `from_project!` or `from_package!` first. Then declare every `#[note_codec]` +//! implementation. Call `export_codecs!` last. These macros register items in declaration order. +//! +//! ```ignore +//! use miden_note_codec::AuthorTypeCodec; +//! +//! miden_note_codec::from_project!("../my-note"); +//! +//! #[miden_note_codec::note_codec] +//! impl AuthorTypeCodec for Ratio { +//! // Implement parse, display, and validate. +//! } +//! +//! miden_note_codec::export_codecs!(); +//! ``` #![deny(missing_docs)] diff --git a/sdk/sdk/MIGRATION.md b/sdk/sdk/MIGRATION.md index 3617b74c94..81a18d6992 100644 --- a/sdk/sdk/MIGRATION.md +++ b/sdk/sdk/MIGRATION.md @@ -146,6 +146,15 @@ path. Two different projects that contain a contract crate with the same package version and share one `CARGO_TARGET_DIR` reuse each other's build-script output — including the staged package cache. Use per-checkout target directories for such layouts. +### Keep one `#[note]` struct in each crate + +A crate can now contain only one `#[note]` struct. Two note structs compiled before this change. +Now the linker rejects the second struct because both structs define the +`__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD` symbol. + +Keep one note struct in the current crate. Move each extra note struct and its implementation into +a separate note crate. + ### Rewrite tuple-note and `Vec` storage layouts `#[note]` now emits a WIT storage schema and therefore requires each stored value to have a stable, From 85043090132ad6a31b64c2860454171b87cc8a37 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 13:18:13 +0300 Subject: [PATCH 15/43] chore: remove committed working documents and stale build config The implementation-plan and follow-up-issue drafts are working documents and leave the repository. The workspace manifest dropped a Cargo exclusion for a directory that no longer exists, the fanned-out guest .cargo configs now say why each crate carries its own copy, and the frontend lost a dead schema-rejection function that contradicted the shipped core-module propagation behavior. --- Cargo.toml | 1 - followup-issues-draft.md | 72 ------- frontend/wasm/src/lib.rs | 28 +-- i1307-implementation-plan.md | 256 ----------------------- sdk/alloc/.cargo/config.toml | 1 + sdk/base-macros/.cargo/config.toml | 1 + sdk/base-sys/.cargo/config.toml | 1 + sdk/base/.cargo/config.toml | 1 + sdk/field-repr/derive/.cargo/config.toml | 1 + sdk/field-repr/repr/.cargo/config.toml | 1 + sdk/field-repr/tests/.cargo/config.toml | 1 + sdk/sdk/.cargo/config.toml | 1 + sdk/stdlib-sys/.cargo/config.toml | 1 + sdk/wasm-metadata/.cargo/config.toml | 1 + 14 files changed, 14 insertions(+), 353 deletions(-) delete mode 100644 followup-issues-draft.md delete mode 100644 i1307-implementation-plan.md diff --git a/Cargo.toml b/Cargo.toml index c81c012ab8..3c0b19495d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,6 @@ exclude = [ # `name = "{{crate_name}}"`, and a nested workspace, neither of which Cargo # should ever try to interpret as part of this workspace. "extra", - "sdk/.cargo", "tests/", "examples", "tools/fuzza-agent", diff --git a/followup-issues-draft.md b/followup-issues-draft.md deleted file mode 100644 index 9310866589..0000000000 --- a/followup-issues-draft.md +++ /dev/null @@ -1,72 +0,0 @@ -# i1307 follow-up issues (draft — not filed) - -Source: discussion #1294 design, `review_claude.md` findings deferred from the 2026-08-07 fix -wave, and PoC restrictions. Each section is one proposed issue. - -## 1. Schema/codec sections and package identity (digest coverage, envelope, attestation) - -Custom `.masp` sections are digest-exempt by construction in `miden-mast-package`, so two -packages with identical identity can carry different `note_storage_schema` layouts and different -executable `note_codec` bytes (review finding 2d). Decide how these sections participate in -package identity: upstream digest coverage (a real `SectionId` + content-digest inclusion), or an -attested hash carried inside digest-covered data. In the same change, give the sections a -versioned envelope (storage-ABI version + owning package identity) — weigh against the #1294 -convention that the schema section is plain inspectable WIT text. Belongs with the #1290 -multi-component package redesign conversation. Duplicate-section rejection and exact-one readers -already landed on the branch. - -## 2. Codec build as a first-class pipeline phase - -The nested codec build now runs inside `post_process_package` (gated, session-threaded), but the -cleaner shape is a pipeline phase that produces a validated artifact before assembly, keeping -post-processing deterministic and side-effect-free (review finding 3, oracle suggestion). - -## 3. Sealed storage-ABI trait: one source for schema, encode, and decode - -A custom type with a manual (non-derived) `FromFeltRepr` impl can decode fields in a different -order than the emitted schema declares — a silent on-chain field swap (review finding 6). Derive -the schema node and the felt encode/decode from one source (sealed trait or equivalent) so the -"schema cannot drift from the code" guarantee covers manual impls too. Interim state on the -branch: expansion-time WIT resolution + the derive-based path; the manual-impl hole is -documented. - -## 4. Artifact index for note-project discovery + codec provenance - -`from_project!` discovery now prefers the compiler-staged path via env var, but profile -discovery still selects by newest mtime without package identity, and consumer bindings track -only the chosen file (review finding 4 residue). Have the build write a small artifact index -(package id/version/target/profile) that `from_project!` resolves through and tracks. Decide -whether codec crate sources/manifest/lockfile join the package-cache fingerprint legs (relates -to the i1302 fingerprint design). - -## 5. String-builder support for nested constructor payloads - -The builder rejects `option` and variant-with-record-payload leaves with a clear error -(landed); actually supporting them needs a constructor syntax or structured input (review -finding 8, deferred half). - -## 6. Migrate the remaining hand-encoded mockchain storage sites - -`support/helpers.rs`'s shared p2id path uses the schema builder; ~19 sites still hand-encode -felts (`to_core_felts`, p2ide 4-felt layout, swapp 13-felt `to_storage_felts`, FPI fixtures). -Migrate them to schema-driven construction; delete `to_core_felts` when the last user goes. - -## 7. `list` (Vec) support in note storage schemas - -`Vec` fields are rejected today (breaking change, documented in MIGRATION). Design the `list` -schema mapping (felt-repr already defines the len-prefixed layout) end to end: emitter, reader -layout interpreter (variable width), builder/decode UX, bindings codegen. - -## 8. Schema polish: subset pruning and JS-side validation - -- Embed only the transitively referenced `core-types` subset instead of the whole interface - (#1294 allows both; whole-interface embed was the PoC call). -- Validate the schema document and codec world against JS tooling (jco) — the wit-parser - header-form + braced-package layout is unverified there. - -## 9. Uniqueness-guard cycle cost (optional micro-optimization) - -The `#[note]` schema uniqueness guard is an exported `u8` static and costs +10 cycles per note -execution (+34 bytes MAST) via rodata/advice-map layout shift. Try a zero-sized guard static -(same `export_name` collision semantics, no rodata byte); if it works, apply to the frontend -metadata guard too. diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index 763256b59c..99cdaf7498 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -27,12 +27,10 @@ use alloc::rc::Rc; use component::build_ir::translate_component; use error::WasmResult; -use midenc_frontend_wasm_metadata::{ - PackageSections, WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME, -}; +use midenc_frontend_wasm_metadata::PackageSections; use midenc_hir::{Context, dialects::builtin}; use module::build_ir::translate_module_as_component; -use wasmparser::{Payload, WasmFeatures}; +use wasmparser::WasmFeatures; #[cfg(feature = "std")] pub use self::emit::wasm_to_wat; @@ -62,26 +60,6 @@ pub fn translate( } } -/// Rejects note storage schema metadata from a core Wasm module. -fn reject_core_module_note_storage_schema(wasm: &[u8]) -> WasmResult<()> { - for payload in wasmparser::Parser::new(0).parse_all(wasm) { - let payload = payload.map_err(|error| -> midenc_session::diagnostics::Report { - WasmError::from(error).into() - })?; - if let Payload::CustomSection(section) = payload - && section.name() == WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME - { - return Err(WasmError::Unsupported( - "a core WebAssembly module contains a note storage schema that cannot be \ - preserved; compile the note crate as a WebAssembly component" - .to_owned(), - ) - .into()); - } - } - Ok(()) -} - /// The set of core WebAssembly features which we need to or wish to support pub(crate) fn supported_features() -> WasmFeatures { WasmFeatures::BULK_MEMORY @@ -104,6 +82,8 @@ pub(crate) fn supported_component_model_features() -> WasmFeatures { #[cfg(test)] mod tests { + use midenc_frontend_wasm_metadata::WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME; + use super::*; /// A `#[note]` crate compiled as a raw core module keeps its schema section: the diff --git a/i1307-implementation-plan.md b/i1307-implementation-plan.md deleted file mode 100644 index 2a3aa3f003..0000000000 --- a/i1307-implementation-plan.md +++ /dev/null @@ -1,256 +0,0 @@ -# i1307 — PoC: schema for user-defined note types (WIT + Wasm) - -Implements the design in discussion [#1294](https://github.com/0xMiden/compiler/discussions/1294) (issue #1307). -Branch: `i1307-note-type-schema` (on `next`). All new crates are `publish = false` (PoC grade). - -## Settled decisions - -- Full design in scope: schema emission, runtime API, bindings macro, codec component. Codec last. -- `wasmtime` enters the workspace (feature-gated, consumer side only). -- Replicate the `PackageSections` refactor from the WIP `wit-in-package` branch on this branch (do not base on it). -- Examples: p2id = no-codec demo; new `dex-note` + `dex-note-codec` pair = custom-type + codec demo. -- Migration: only the shared p2id path in `tests/integration-network/src/mockchain/support/helpers.rs`. Other hand-encoded sites stay for a follow-up. -- Type surface (PoC): named-field structs → schema; unit structs → no schema section; tuple structs → compile error; `Vec` fields → "not supported yet" error; nested custom types need `#[export_type]` defined before the `#[note]` struct; doc comments are carried into the WIT; embed the whole `core-types` interface (no subset pruning). - -## Name registry (fixed up front) - -| Thing | Name | -|---|---| -| Wasm custom section (schema) | `rodata,miden_note_schema` | -| Wasm static (schema) | `__MIDEN_NOTE_STORAGE_SCHEMA_BYTES` | -| `.masp` section id (schema) | `note_storage_schema` (via `SectionId::custom`) | -| `.masp` section id (codec) | `note_codec` (via `SectionId::custom`) | -| Schema WIT package | `:-schema@`, interface `note-storage`, root alias `type storage = ;` | -| Codec world | `package miden:note-codec@1.0.0`, `world note-codec`, `type felt = u64` | -| Reader crate | `sdk/note-schema` → `miden-note-schema` | -| Shared codegen (internal) | `sdk/note-schema/codegen` → `miden-note-schema-codegen` | -| Bindings macro crate | `sdk/note-bindings` → `miden-note-bindings` (pure proc-macro) | -| Author codec crates | `sdk/note-codec` → `miden-note-codec` (lib) + `sdk/note-codec/macros` → `miden-note-codec-macros` | -| Codec-crate pointer | `[package.metadata] note-codec-crate = "…"` in the note's `miden-project.toml` (`MetadataSet` is free-form; no upstream `miden-project` change) | - -The normative felt layout rule (document it in `miden-note-schema` rustdoc): layout is structural -over the WIT type tree; the record `miden:base/core-types.felt` is the 1-felt bedrock; `u64` = 2 -felts (lo u32, hi u32); `u32`/`u8`/`bool` = 1 range-checked felt; `option` = 1 tag felt + -payload; `variant` = 1 tag felt (declaration ordinal) + case payload; records concatenate fields in -declaration order. This is exactly `miden-field-repr`'s documented layout, so `word` (4), -`account-id` (2), etc. need no special cases — they bottom out at `felt` structurally. Codecs never -change layout; they only bind string parse/display/validate to a WIT fqn. - ---- - -## Phase 0 — `PackageSections` plumbing refactor - -Goal: one struct carried through the pipeline instead of a per-payload `Option>` field, so -Phase 1 (and the wit-in-package rebase later) only add a field. - -1. `sdk/wasm-metadata/src/lib.rs`: - - Add consts `WASM_ACCOUNT_COMPONENT_METADATA_CUSTOM_SECTION_NAME = "rodata,miden_account"` - (replace the two hardcoded uses), `WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME`, - `PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID`, `PACKAGE_NOTE_CODEC_SECTION_ID`. - - Add `pub struct PackageSections { pub account_component_metadata: Option>, pub note_storage_schema: Option> }` - (mirror the shape on `origin/wit-in-package`, without its `component_wit` field). -2. Thread `PackageSections` through, replacing `account_component_metadata_bytes`: - - `frontend/wasm/src/module/module_env.rs` (`ParsedModule`), `frontend/wasm/src/component/translator.rs:176-192`, - `frontend/wasm/src/lib.rs` (`FrontendOutput`), `midenc-compile/src/pipeline/frontends/wasm.rs:470-487`, - `midenc-compile/src/pipeline/artifacts.rs` (`MidenComponent`, `CodegenOutput`), - `midenc-compile/src/pipeline/backend.rs` (`LoweredTarget`), - `midenc-compile/src/pipeline/assembly.rs::post_process_package` and its callers - (`backend.rs:828`, `frontends/hir.rs:392`, `frontends/wasm.rs:590`, `seed.rs:437`, `seed.rs:604`). -3. No behavior change. `cargo make test` must stay green with no expectation updates. - -## Phase 1 — Schema emission into the `.masp` - -### 1a. Macro side (`sdk/base-macros`) - -1. Registry doc support (`src/types.rs`, `src/export_type.rs`): add `docs: Vec` to - `ExportedTypeDef`, `ExportedField`, `ExportedVariant`; capture `#[doc]` attrs at registration. - The component-macro consumer ignores the new fields. -2. New module `src/note_schema.rs`: - - Input: the `#[note]` struct item (+ doc attrs), the export-type registry, package identity. - - Package identity: reuse the same source `build_note_script_wit` uses for the main WIT package - name (crate-name/manifest based) and append `-schema`; version from the same source. Must not - fail on fixtures without `miden-project.toml`. - - Map field types with the existing `map_type_to_type_ref`; wrap its rejections in a - note-specific diagnostic ("`Vec` is not supported in note storage schemas yet", etc.). - Resolve custom types through the registry (transitive closure; unregistered → error that names - `#[export_type]` and the ordering rule, like `ensure_custom_type_defined`). - - Render the multi-package WIT document. Settled layout (spike-verified, 2026-08-05 — the - all-braced form in the discussion sketch does NOT parse in wit-parser 0.247; the main package - must be first and in header form): - `package :-schema@;` header, then `interface note-storage { use …; records…; type storage = ; }` - at top level, then `package miden:base@1.0.0 { interface core-types { … } }` as a braced - block at the end — body extracted verbatim from `SDK_WIT_SOURCE` (textual block extraction; - unit-test it). `WitBuilder` needs a `package_block` (braced package) helper; doc comments - emitted as `///` lines. Field/record names kebab-cased with the existing helpers. - - Emit bytes: UTF-8 text, padded to a 16-byte multiple with NULs (mirror ACM padding; readers - trim trailing NULs). - - Emit the static: `#[unsafe(link_section = "rodata,miden_note_schema")] pub static __MIDEN_NOTE_STORAGE_SCHEMA_BYTES: [u8; N]` - (fixed name → duplicate-symbol link error enforces one storage schema per crate). -3. Hook into `expand_note_struct` (`src/note.rs:100`): named-field arm emits the schema static; - unit arm emits nothing; unnamed/tuple arm becomes a compile error ("note storage schema needs - named fields"). -4. Unit tests (`sdk/base-macros/tests/`, plus module tests using - `reset_export_type_registry_for_tests`): - - Golden expansion for a p2id-shaped struct and for a struct with a nested `#[export_type]` - record and an enum. - - Parse the emitted document with `wit_parser` (`wit-bindgen-core` re-exports it; wit-component - 0.247 is already a dev-dep) and assert it resolves; assert interface/alias/root are found. - - Error cases: tuple struct, `Vec` field, unregistered custom type. - -### 1b. Compiler side - -1. `frontend/wasm/src/module/module_env.rs`: new `Payload::CustomSection` arm for - `WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME` → store raw bytes (validate UTF-8 after NUL trim; - full WIT validation stays in tests/consumers for the PoC). -2. `translator.rs`: collect across nested modules, error on >1 (mirror the ACM logic) → the new - `PackageSections.note_storage_schema` field. -3. `midenc-compile/src/pipeline/assembly.rs`: `attach_note_storage_schema` — push - `Section::new(SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID)?, bytes)` whenever - present. Custom sections are digest-exempt in `miden-mast-package` 0.25.8 by construction — - nothing to do for digests. - -### 1c. Tests - -- New `tests/integration/src/end_to_end/examples/note_schema_metadata.rs` (mirror - `counter_metadata.rs`): build `examples/p2id-note`, read the section, expect-test the WIT text, - and round-trip it through `wit_parser::Resolve`. -- Same golden for the `swapp-note` fixture (6 fields: `Word`, `Felt`, `AccountId`, …) — the richest - existing storage struct. -- Update package-size expectations (`basic_wallet_package_sizes.rs` and friends) with - `UPDATE_EXPECT=1` — every named-field note package now carries the section. - -## Phase 2 — Runtime API: `miden-note-schema` - -New std host crate `sdk/note-schema` (workspace member, `publish = false`). -Deps: `miden-mast-package`, `wit-parser` (promote 0.247 to a workspace dep), `miden-protocol`, -`miden-field`, `miden-field-repr` (native mode — reuse `FeltReader`/`FeltWriter` so the layout -rules live in one place). - -1. Schema model + reader: - - `NoteStorageSchema::from_package(&Package)` — find section, trim NULs, parse UTF-8 WIT, - resolve, locate interface `note-storage` and alias `storage`, build an internal model - (ordered fields; type tree of records/variants/options/primitives/leaf fqns). - `from_wit_text(&str)` for tests. Surface validation errors are actionable. - - Layout interpreter over the model per the normative rule above (widths, walk order). -2. Codecs: - - `trait ConsumerTypeCodec { parse(&self,&str)->Result,_>; display(&self,&[Felt])->String; validate(&self,&[Felt])->Result<(),_> }`. - - `CodecRegistry` keyed by WIT fqn. `Default` = standard leaf codecs over protocol parsers: - `felt` (decimal/hex), `word` (hex / 4-felt), `account-id` (`AccountId::parse` bech32|hex → - `[prefix, suffix]` per the WIT record order), `asset-amount` (decimal u64). Keep the set small. -3. Build direction: `schema.builder()` — `set(name, &str)` (kebab accepted, snake normalized; - dotted paths reach nested-record leaves — the structural UX), leaf routing: codec fqn if - registered, else primitive parse, else error naming the fqn; `build()` does completeness + - range checks → `NoteStorage`. -4. Decode direction: `schema.decode(&NoteStorage)` → named value tree; `Display` uses the registry - when an fqn is registered, else structural rendering. -5. Tests: unit tests on `from_wit_text` (layout widths, builder round-trips, error paths); - integration test: build p2id `.masp`, `builder().set("target-account-id", bech32).build()` - equals the hand-built `[prefix, suffix]` storage; decode round-trip displays the bech32 back. - -## Phase 3 — Typed bindings: `miden-note-bindings` - -1. `sdk/note-schema/codegen` (`miden-note-schema-codegen`, internal lib): schema model → Rust - tokens for the host-profile types — one generator shared by Phases 3 and 4: - - standard leaves → `miden_protocol::account::AccountId`, `miden_field::Word`, - `miden_field::Felt`; primitives verbatim; custom records/variants → generated - structs/enums with `#[derive(ToFeltRepr, FromFeltRepr)]` (native) + a WIT-fqn const per type. - - protocol-leaf encode/decode helpers follow the WIT record order (`account-id` → - `[prefix, suffix]`). -2. `sdk/note-bindings` (pure proc-macro crate): - - `from_project!("../dex-note")` — resolve against `CARGO_MANIFEST_DIR`; find the freshest - `.masp` across `/target/miden//` (mirror the `fpi.rs:1671`/`:1704` candidate - logic); missing artifact → compile error naming `cargo miden build`. - - `from_package!("path/to.masp")` — exact path. Both read the section and delegate to the shared - codegen, then add the consumer surface per the design: `to_note_storage`, - `from_note_storage`, `from_str_values(&BTreeMap<_,_>, &CodecRegistry)`, `validate_with`, - `display_with`; when the schema has no custom types, drop the `codecs` parameters. - - Hidden `from_wit_text!` entry for golden expansion tests. -3. Tests: expansion goldens over schema WIT strings (p2id-shaped, custom-type-shaped); one - end-to-end test that builds `examples/p2id-note` and then compiles + runs a small temp consumer - crate (process-spawned `cargo`, pattern like `tools/cargo-miden/tests`). - -## Phase 4 — Codec component - -### 4a. World + author-side crates - -1. `sdk/note-codec` (`miden-note-codec`, lib): ships `wit/note-codec.wit` (the world exactly as in - the discussion, `type felt = u64` with the doc comment about why it is not the core-types - record); `trait AuthorTypeCodec { fn parse(&str)->Result; fn display(&self)->String; fn validate(&self)->Result<(),String> }`; - boundary glue: u64↔Felt via canonical u64 with canonicality checks (reject ≥ p at the boundary). -2. `sdk/note-codec/macros` (`miden-note-codec-macros`, re-exported from the lib): - - `from_project!` / `from_package!` — same artifact resolution as Phase 3, but generate only the - host-profile types + record the schema (incl. root) in a process-global registry for - `export_codecs!`. - - `#[note_codec]` — marks an `AuthorTypeCodec` impl; registers the type's fqn. - - `export_codecs!()` — wit-bindgen `generate!` for the `note-codec` world plus the `export!` - glue: `supported-types` from the marked set; `parse`/`display`/`validate` dispatch by fqn - through the marked impls and their felt-repr impls. Component glue gated - `cfg(target_family = "wasm")` so the crate still builds and unit-tests natively. -3. Componentization target: primary = `wasm32-unknown-unknown` cdylib + - `wit_component::ComponentEncoder` (no WASI imports — clean sandbox, jco-friendly); fallback if - friction = `wasm32-wasip2` (rustc emits a component directly, consumer then needs a WASI - context). Spike this first inside Phase 4. - -### 4b. `cargo-miden` orchestration - -In `tools/cargo-miden/src/commands/build.rs` after `compile_to_memory`, before `write_masp_file`: -read the note's `miden-project.toml` `[package.metadata] note-codec-crate` (via the -`miden-project` crate's `MetadataSet`); when set: `cargo build --release` for the codec crate at -the componentization target, componentize, and `package.sections.push(Section::new(SectionId::custom("note_codec")?, bytes))`. -Omitted key → no section (structural fallback). - -### 4c. Consumer adapter - -`miden-note-schema`, feature `codec-component` (off by default): `CodecRegistry::load_from_package` -— find the `note_codec` section, instantiate with `wasmtime` (component model), wrap in an adapter -implementing `ConsumerTypeCodec` per fqn reported by `supported-types`. `wasmtime` is a -feature-gated dependency of this crate only. - -### 4d. Example pair + end-to-end - -1. `examples/dex-note` (guest, standalone project like p2id): `#[export_type] #[derive(FromFeltRepr)] struct LimitPrice { numerator: u64, denominator: u64 }`, - `#[note] struct DexNote { target: AccountId, price: LimitPrice }`, trivial `#[note_script]` - against `basic-wallet` (p2id-like), doc comments on everything (they surface in the WIT). - `miden-project.toml` gets the `note-codec-crate` pointer. -2. `examples/dex-note-codec` (host, plain cargo, no wasm target config): depends only on - `miden-note-codec`; `from_project!("../dex-note")`; `#[note_codec] impl AuthorTypeCodec for LimitPrice` - (parse "3/2" and "1.5" forms, display, validate denominator ≠ 0); `export_codecs!()`. -3. Tests: - - `tools/cargo-miden/tests/dex_note_codec_build.rs`: `cargo miden build` on dex-note → package - has both `note_storage_schema` and `note_codec` sections. - - `tests/integration-network` mockchain test: consume a dex note whose storage was built from - strings (`"target"` = bech32 natively, `"price"` = `"1.5"` through the component), and decode - an incoming note back to `"1.5"` via `display`. - - p2id no-codec demo + migration: mockchain test building p2id storage via the schema string - builder; switch the shared p2id path in `support/helpers.rs` (`to_core_felts` call in - `build_asset_transfer_tx`) to the schema builder. - -## Cross-cutting finish work - -- `sdk/sdk/CHANGELOG.md`: entry for the `#[note]` schema emission + new `.masp` section (the - published macro crates change even though the new crates are `publish = false`); new PoC - restrictions (tuple structs, `Vec` fields) called out. No MIGRATION entry (additive; the - restrictions break no released code — verify no external breakage claim beyond the repo). -- Full sweep per repo rules: `cargo make test-all`, `cargo make clippy`, `cargo make format-rust`; - `UPDATE_EXPECT=1` only for the intended package-size/golden updates. - -## Risks / early spikes - -1. ~~**Multi-package WIT text** in wit-parser 0.247~~ — RESOLVED by spike: header-form main - package first + braced dependency package after works; all-braced does not. JS-tooling (jco) - parse compatibility is untested and out of PoC scope (Rust consumers only). -2. **Componentization target** (4a.3) — spike at Phase 4 start. -3. **Package identity in `expand_note_struct`** — must degrade gracefully for fixtures without - `miden-project.toml` (fall back to crate name/version, same as the note-script WIT). -4. **Link-section padding** — ACM pads to 16 bytes; keep the padding, trim NULs on every reader. -5. **`from_project!` freshness** — newest-mtime across profiles is the settled design rule - (schema is profile-invariant); reuse the fpi.rs candidate-dir logic rather than reinventing. -6. **Process-global registries in proc macros** — one schema registry per crate build is the same - trade the `#[export_type]` registry already makes; keep the reset-for-tests hook pattern. - -## Suggested execution order - -Phase 0 → 1 → 2 → 3 → 4, each landable and testable on its own. Phases 0–1 touch the compiler -pipeline and macros; 2–3 are pure new host crates; 4 touches `cargo-miden` + examples. Codex-sized -work packets: each phase is one packet, with Phase 4 split into (world+author crates), (cargo-miden -+ example), (consumer + e2e). diff --git a/sdk/alloc/.cargo/config.toml b/sdk/alloc/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/alloc/.cargo/config.toml +++ b/sdk/alloc/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/base-macros/.cargo/config.toml b/sdk/base-macros/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/base-macros/.cargo/config.toml +++ b/sdk/base-macros/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/base-sys/.cargo/config.toml b/sdk/base-sys/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/base-sys/.cargo/config.toml +++ b/sdk/base-sys/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/base/.cargo/config.toml b/sdk/base/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/base/.cargo/config.toml +++ b/sdk/base/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/field-repr/derive/.cargo/config.toml b/sdk/field-repr/derive/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/field-repr/derive/.cargo/config.toml +++ b/sdk/field-repr/derive/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/field-repr/repr/.cargo/config.toml b/sdk/field-repr/repr/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/field-repr/repr/.cargo/config.toml +++ b/sdk/field-repr/repr/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/field-repr/tests/.cargo/config.toml b/sdk/field-repr/tests/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/field-repr/tests/.cargo/config.toml +++ b/sdk/field-repr/tests/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/sdk/.cargo/config.toml b/sdk/sdk/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/sdk/.cargo/config.toml +++ b/sdk/sdk/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/stdlib-sys/.cargo/config.toml b/sdk/stdlib-sys/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/stdlib-sys/.cargo/config.toml +++ b/sdk/stdlib-sys/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/wasm-metadata/.cargo/config.toml b/sdk/wasm-metadata/.cargo/config.toml index 6b509f5b70..f03dba586a 100644 --- a/sdk/wasm-metadata/.cargo/config.toml +++ b/sdk/wasm-metadata/.cargo/config.toml @@ -1,2 +1,3 @@ +# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. [build] target = "wasm32-wasip1" From c630f7d2bccd009a9c683aa52a89ca78058b64cd Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 13:18:13 +0300 Subject: [PATCH 16/43] fix: box the concrete symbol type in hir-macros Enabling the proc-macro2 span-locations feature for the macro registries grows Span and with it syn::Type, which pushed the SymbolType enum over the clippy large-enum-variant threshold. The concrete variant now boxes the type. --- hir-macros/src/operation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hir-macros/src/operation.rs b/hir-macros/src/operation.rs index 6376833969..90e8ec2014 100644 --- a/hir-macros/src/operation.rs +++ b/hir-macros/src/operation.rs @@ -376,7 +376,7 @@ impl OpDefinition { }; let symbol = Symbol { name: field_name.clone(), - ty: SymbolType::Concrete(field_ty), + ty: SymbolType::Concrete(Box::new(field_ty)), }; create_params.push(OpCreateParam { param_ty: OpCreateParamType::Symbol(symbol.clone()), @@ -2955,7 +2955,7 @@ pub enum SymbolType { /// Any `Symbol + CallableOpInterface` implementation can be used Callable, /// Only the specific concrete type can be used, it must implement `Op` and `Symbol` traits - Concrete(syn::Type), + Concrete(Box), /// Any implementation of the provided trait can be used. /// /// The given trait type _must_ have `Symbol` as a supertrait. From df0b88c33d9e324964181d4b48dfb97c7b4a1c1d Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 16:15:33 +0300 Subject: [PATCH 17/43] fix: harden note codec production and consumption contracts Note codecs now always build with the Cargo release profile - a dev wasm32-wasip2 cdylib carries debug info far past the consumer size limit - and the compiler enforces the shared component size limit before it attaches the codec section, so a package that builds is a package that consumers accept. The staged-package probe now uses the package-cache file-name helper, so dotted package names hit the same file the writer produces and the content-identity guard covers them. The storage builder encodes a codec-registered record that was assembled from child-path values as one subtree and runs the codec validation over it, so the builder can no longer produce storage that the same registry rejects on decode. Both macro crates emit fully qualified rebuild-tracking constants and take the cache environment name from the shared constant. The codec world check derives the expected identity from the pinned WIT instead of literals. Memoized schema nodes record their subtree depth so reuse cannot exceed the schema depth bound. Dead code, duplicated resolution-policy arguments, and diagnostic literals are cleaned up, and the tricky spots called out by review carry explanatory comments. --- Cargo.lock | 4 + examples/dex-note-codec/Cargo.lock | 1 + midenc-compile/Cargo.toml | 2 +- midenc-compile/src/cargo.rs | 218 ++++++++++-------- midenc-compile/src/pipeline/assembly.rs | 11 +- midenc-compile/src/pipeline/frontends/rust.rs | 20 +- sdk/base-macros/src/types.rs | 2 + sdk/note-bindings/macros/Cargo.toml | 1 + sdk/note-bindings/macros/src/lib.rs | 10 +- sdk/note-codec/macros/Cargo.toml | 1 + sdk/note-codec/macros/src/expand.rs | 12 +- sdk/note-codec/macros/src/registry.rs | 2 + sdk/note-codec/wit-crate/wit/note-codec.wit | 2 + sdk/note-schema/codegen/src/lib.rs | 1 + sdk/note-schema/src/builder.rs | 46 +++- sdk/note-schema/src/codec.rs | 1 + sdk/note-schema/src/codec_component.rs | 46 ++-- sdk/note-schema/src/lib.rs | 6 +- sdk/note-schema/src/schema.rs | 173 +++++++++----- sdk/note-schema/src/tests.rs | 93 ++++++++ sdk/note-schema/wit/note-codec.wit | 2 + tests/support/src/lib.rs | 60 +++++ 22 files changed, 483 insertions(+), 231 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffc8f697ab..af0ea847a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -837,6 +837,7 @@ dependencies = [ "midenc-compile", "midenc-frontend-wasm-metadata", "midenc-hir", + "midenc-integration-test-support", "midenc-log", "midenc-session", "path-absolutize", @@ -3631,6 +3632,7 @@ dependencies = [ "miden-note-schema", "miden-note-schema-codegen", "midenc-expect-test", + "midenc-frontend-wasm-metadata", "prettyplease", "proc-macro-crate", "proc-macro2", @@ -3647,6 +3649,7 @@ dependencies = [ "miden-note-codec-macros", "miden-note-codec-wit", "miden-protocol", + "midenc-integration-test-support", "tempfile", "wit-bindgen", "wit-component", @@ -3661,6 +3664,7 @@ dependencies = [ "miden-note-codec-wit", "miden-note-schema", "miden-note-schema-codegen", + "midenc-frontend-wasm-metadata", "prettyplease", "proc-macro-crate", "proc-macro2", diff --git a/examples/dex-note-codec/Cargo.lock b/examples/dex-note-codec/Cargo.lock index d2b590bc7f..0e9f5a1758 100644 --- a/examples/dex-note-codec/Cargo.lock +++ b/examples/dex-note-codec/Cargo.lock @@ -1956,6 +1956,7 @@ dependencies = [ "miden-note-codec-wit", "miden-note-schema", "miden-note-schema-codegen", + "midenc-frontend-wasm-metadata", "proc-macro-crate", "proc-macro2", "quote", diff --git a/midenc-compile/Cargo.toml b/midenc-compile/Cargo.toml index d4258ec99a..6a9caa78ac 100644 --- a/midenc-compile/Cargo.toml +++ b/midenc-compile/Cargo.toml @@ -39,9 +39,9 @@ midenc-codegen-masm.workspace = true miden-assembly.workspace = true miden-assembly-syntax.workspace = true miden-mast-package.workspace = true +miden-note-codec-wit.workspace = true miden-note-schema.workspace = true miden-package-registry.workspace = true -miden-note-codec-wit.workspace = true midenc-frontend-wasm.workspace = true midenc-frontend-wasm-metadata.workspace = true midenc-frontend-masm.workspace = true diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index b877f570d3..1526860f86 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -19,12 +19,14 @@ use midenc_hir::Report; use midenc_session::{InputFile, LinkLibrary, Session, miden_project}; use sha2::{Digest, Sha256}; use wit_component::DecodedWasm; -use wit_parser::{Function, FunctionKind, Resolve, Type, TypeDefKind, TypeId, WorldId, WorldItem}; +use wit_parser::{ + Function, FunctionKind, Resolve, Type, TypeDefKind, TypeId, WorldId, WorldItem, WorldKey, +}; use crate::{CodegenOutput, CompilerResult}; /// Metadata table that points to an author-side note codec crate. -const NOTE_CODEC_CRATE_METADATA: &str = "note-codec-crate"; +pub(crate) const NOTE_CODEC_CRATE_METADATA: &str = "note-codec-crate"; /// Metadata field that contains the codec crate directory. const NOTE_CODEC_CRATE_PATH: &str = "path"; @@ -284,24 +286,6 @@ pub(crate) fn cargo_build( filesystem_cache_dir, context, ) - // We expect dependencies to *always* produce packages (.masp) - /* - let CodegenOutput { - component, - sections, - } = crate::pipeline::frontends::rust::compile_manifest(&manifest_path, None, context.clone())? - else { - panic!( - "expected cargo build of {package_name} to produce component, but got HIR output \ - instead", - ); - }; - - Ok(CodegenOutput { - component, - sections, - }) - */ //component.source_inputs(target, context.session()) @@ -466,9 +450,10 @@ fn build_note_codec_component( .arg("build") .arg("--manifest-path") .arg(&manifest_path) - .arg("--lib"); - apply_note_codec_profile(&mut cargo, &session.options.profile); - cargo + .arg("--lib") + // The codec is a host-side wasmtime artifact; a dev-profile cdylib carries debug + // info far past the consumer size limit, so every Miden profile builds it release. + .arg("--release") .arg("--target") .arg(NOTE_CODEC_TARGET) .arg("--target-dir") @@ -476,12 +461,13 @@ fn build_note_codec_component( .arg("--message-format") .arg("json-render-diagnostics") .env(package_cache::PACKAGE_CACHE_ENV, &staged_package.cache_dir) + // Outer Miden target settings and flags would poison this nested wasip2 codec build. .env_remove("CARGO_BUILD_TARGET") .env_remove("CARGO_ENCODED_RUSTFLAGS") .env_remove("RUSTFLAGS") .stdout(Stdio::piped()) .stderr(Stdio::inherit()); - apply_cargo_policy(&mut cargo, session.options.cargo_locked, session.options.cargo_offline); + cargo.args(apply_cargo_policy(session.options.cargo_locked, session.options.cargo_offline)); let manifest_path = manifest_path.canonicalize().map_err(|error| { Report::msg(format!( @@ -546,6 +532,7 @@ fn stage_note_package( let package_bytes = note_package.to_bytes(); let mut hasher = Sha256::new(); hasher.update(&package_bytes); + // Separate the variable-length package bytes from the codec-crate path in the build key. hasher.update([0]); hasher.update(codec_crate_dir.as_os_str().to_string_lossy().as_bytes()); let mut build_key = String::with_capacity(64); @@ -553,7 +540,7 @@ fn stage_note_package( write!(&mut build_key, "{byte:02x}").expect("writing to a string cannot fail"); } let cache_dir = work_dir.join("package-cache").join(&build_key); - let package_path = cache_dir.join(&*note_package.name).with_extension(MastPackage::EXTENSION); + let package_path = cache_dir.join(package_cache::package_file_name(¬e_package.name)); if package_path.is_file() { let staged_bytes = fs::read(&package_path).map_err(|error| { @@ -609,21 +596,14 @@ fn gc_staged_note_packages(current_cache_dir: &Path) { } } -/// Maps a Miden build profile to the independent codec Cargo workspace. -fn apply_note_codec_profile(cargo: &mut Command, profile: &str) { - if profile == "release" { - cargo.arg("--release"); - } -} - -/// Applies the outer Cargo resolution policy to a nested command. -fn apply_cargo_policy(cargo: &mut Command, locked: bool, offline: bool) { - if locked { - cargo.arg("--locked"); - } - if offline { - cargo.arg("--offline"); - } +/// Returns the outer Cargo resolution policy arguments for a nested command. +pub(crate) fn apply_cargo_policy( + locked: bool, + offline: bool, +) -> impl Iterator { + [locked.then_some("--locked"), offline.then_some("--offline")] + .into_iter() + .flatten() } /// Adds lockfile and network recovery guidance to a nested Cargo failure. @@ -655,6 +635,15 @@ fn note_codec_cargo_error( /// Verifies the component sandbox and the versioned codec interface export. fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { + // Enforce the consumer size limit at the producer, so a package that builds is a + // package that consumers accept. + if component.len() > miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES { + return Err(Report::msg(format!( + "note codec component is {} bytes, above the {}-byte limit that consumers enforce", + component.len(), + miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES, + ))); + } let DecodedWasm::Component(resolve, world_id) = wit_component::decode(component).map_err(|error| { Report::msg(format!("failed to decode the encoded note codec component: {error}")) @@ -662,6 +651,26 @@ fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { else { return Err(Report::msg("note codec output is not a component")); }; + + let mut expected = Resolve::default(); + let package_id = expected.push_str("note-codec.wit", NOTE_CODEC_WIT).map_err(|error| { + Report::msg(format!("failed to resolve the pinned note codec WIT: {error:#}")) + })?; + let expected_package = &expected.packages[package_id]; + if expected_package.worlds.len() != 1 { + return Err(Report::msg(format!( + "pinned note codec WIT must define exactly one world, found: {:#?}", + expected_package.worlds + ))); + } + let expected_world_id = *expected_package + .worlds + .values() + .next() + .expect("one pinned codec world was checked above"); + let (expected_identity, expected_interface_id) = + codec_world_export(&expected, expected_world_id)?; + let world = &resolve.worlds[world_id]; // The wasm32-wasip2 standard library wires WASI interfaces into every component. // Consumers stub them as trapping imports, so only `wasi:*` imports are permitted. @@ -675,27 +684,15 @@ fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { } if world.exports.len() != 1 { return Err(Report::msg(format!( - "note codec component must export only `miden:note-codec/codec@1.0.0`, found: {:#?}", - world.exports + "note codec component must export only `{expected_identity}`, found: {:#?}", + world.exports, ))); } - let (actual_key, actual_interface_id) = codec_world_export(&resolve, world_id)?; - - let mut expected = Resolve::default(); - let package_id = expected.push_str("note-codec.wit", NOTE_CODEC_WIT).map_err(|error| { - Report::msg(format!("failed to resolve the pinned note codec WIT: {error:#}")) - })?; - let expected_world_id = expected.packages[package_id] - .worlds - .get("note-codec") - .copied() - .ok_or_else(|| Report::msg("pinned note codec WIT has no `note-codec` world"))?; - let (expected_key, expected_interface_id) = codec_world_export(&expected, expected_world_id)?; - - if actual_key != expected_key { + let (actual_identity, actual_interface_id) = codec_world_export(&resolve, world_id)?; + if actual_identity != expected_identity { return Err(Report::msg(format!( - "note codec world exports `{actual_key}`, expected `{expected_key}`" + "note codec world exports `{actual_identity}`, expected `{expected_identity}`" ))); } compare_codec_interface_signatures( @@ -703,18 +700,21 @@ fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { actual_interface_id, &expected, expected_interface_id, + &expected_identity, ) } -/// Returns the canonical key and interface ID for the sole codec world export. +/// Returns the package-qualified identity and interface ID of the first codec world export. fn codec_world_export( resolve: &Resolve, world_id: WorldId, ) -> CompilerResult<(String, wit_parser::InterfaceId)> { let world = &resolve.worlds[world_id]; - let (key, item) = world.exports.iter().next().ok_or_else(|| { - Report::msg("note codec component does not export `miden:note-codec/codec@1.0.0`") - })?; + let (key, item) = world + .exports + .iter() + .next() + .ok_or_else(|| Report::msg("note codec world does not export an interface"))?; let WorldItem::Interface { id, .. } = item else { return Err(Report::msg(format!( "note codec world export `{}` is not an interface", @@ -722,18 +722,23 @@ fn codec_world_export( ))); }; let interface = &resolve.interfaces[*id]; + let interface_name = interface + .name + .as_deref() + .ok_or_else(|| Report::msg("note codec world exports an unnamed interface"))?; let package_id = interface.package.ok_or_else(|| { Report::msg("note codec world exports an interface without a package identity") })?; let package = &resolve.packages[package_id].name; - if interface.name.as_deref() != Some("codec") - || package.namespace != "miden" - || package.name != "note-codec" - || package.version.as_ref().is_none_or(|version| version.to_string() != "1.0.0") - { - return Err(Report::msg("component does not export `miden:note-codec/codec@1.0.0`")); + let interface_identity = package.interface_id(interface_name); + if !matches!(key, WorldKey::Interface(export_id) if export_id == id) { + return Err(Report::msg(format!( + "note codec world export `{}` must use the package-qualified interface identity \ + `{interface_identity}`", + resolve.name_world_key(key), + ))); } - Ok((resolve.name_canonicalized_world_key(key), *id)) + Ok((interface_identity, *id)) } /// Compares all exported codec function names and structural signatures. @@ -742,26 +747,28 @@ fn compare_codec_interface_signatures( actual_id: wit_parser::InterfaceId, expected_resolve: &Resolve, expected_id: wit_parser::InterfaceId, + expected_identity: &str, ) -> CompilerResult<()> { let actual = &actual_resolve.interfaces[actual_id]; let expected = &expected_resolve.interfaces[expected_id]; if actual.functions.len() != expected.functions.len() { return Err(Report::msg(format!( - "`miden:note-codec/codec@1.0.0` exports {} functions, expected {}", + "`{expected_identity}` exports {} functions, expected {}", actual.functions.len(), expected.functions.len() ))); } for (name, expected_function) in &expected.functions { - let actual_function = actual.functions.get(name).ok_or_else(|| { - Report::msg(format!("`miden:note-codec/codec@1.0.0` is missing `{name}`")) - })?; + let actual_function = actual + .functions + .get(name) + .ok_or_else(|| Report::msg(format!("`{expected_identity}` is missing `{name}`")))?; let actual_signature = function_signature(actual_resolve, actual_function)?; let expected_signature = function_signature(expected_resolve, expected_function)?; if actual_signature != expected_signature { return Err(Report::msg(format!( - "`miden:note-codec/codec@1.0.0.{name}` has signature `{actual_signature}`, \ - expected `{expected_signature}`" + "`{expected_identity}.{name}` has signature `{actual_signature}`, expected \ + `{expected_signature}`" ))); } } @@ -940,11 +947,13 @@ mod tests { #[test] fn codec_interface_validation_compares_function_signatures() { let (expected_resolve, expected_id) = resolve_codec_interface(NOTE_CODEC_WIT); + let expected_identity = codec_interface_identity(&expected_resolve, expected_id); compare_codec_interface_signatures( &expected_resolve, expected_id, &expected_resolve, expected_id, + &expected_identity, ) .unwrap(); @@ -958,6 +967,7 @@ mod tests { actual_id, &expected_resolve, expected_id, + &expected_identity, ) .unwrap_err() .to_string(); @@ -979,38 +989,40 @@ mod tests { assert_eq!(first.cache_dir, second.cache_dir); assert_eq!(first.cache_dir.parent(), Some(root.path().join("package-cache").as_path())); assert_eq!(first.cache_dir.file_name().unwrap().len(), 64); - let package_path = - first.cache_dir.join(&*package.name).with_extension(MastPackage::EXTENSION); + let package_path = first.cache_dir.join(package_cache::package_file_name(&package.name)); assert_eq!(fs::read(package_path).unwrap(), package.to_bytes()); + + let mut dotted_package = (*package).clone(); + dotted_package.name = miden_mast_package::PackageId::from("miden.note.with.dots"); + let dotted = stage_note_package(root.path(), &codec_crate, &dotted_package).unwrap(); + let dotted_path = + dotted.cache_dir.join(package_cache::package_file_name(&dotted_package.name)); + assert_eq!(dotted_path.file_name().unwrap(), "miden.note.with.dots.masp"); + assert_eq!(fs::read(&dotted_path).unwrap(), dotted_package.to_bytes()); + fs::write(&dotted_path, b"different package bytes").unwrap(); + let error = stage_note_package(root.path(), &codec_crate, &dotted_package) + .err() + .expect("the probe must reject different bytes at the writer path") + .to_string(); + assert!(error.contains("contains different bytes"), "unexpected error: {error}"); } #[test] fn note_codec_cargo_policy_is_forwarded() { - let mut cargo = Command::new("cargo"); - cargo.arg("build"); - apply_cargo_policy(&mut cargo, true, true); - let args = cargo - .get_args() - .map(|arg| arg.to_string_lossy().into_owned()) - .collect::>(); - - assert!(args.iter().any(|arg| arg == "--locked")); - assert!(args.iter().any(|arg| arg == "--offline")); + let args = apply_cargo_policy(true, true).collect::>(); + + assert_eq!(args, ["--locked", "--offline"]); } #[test] - fn note_codec_profile_maps_release_and_custom_profiles() { - let mut release = Command::new("cargo"); - release.arg("build"); - apply_note_codec_profile(&mut release, "release"); - let release_args = release.get_args().collect::>(); - assert_eq!(release_args, ["build", "--release"]); - - let mut custom = Command::new("cargo"); - custom.arg("build"); - apply_note_codec_profile(&mut custom, "size-optimized"); - let custom_args = custom.get_args().collect::>(); - assert_eq!(custom_args, ["build"]); + fn oversized_codec_components_fail_producer_validation() { + let oversized = vec![0u8; miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES + 1]; + let error = validate_note_codec_component(&oversized).unwrap_err().to_string(); + assert!(error.contains("above the"), "unexpected error: {error}"); + assert!( + error.contains(&miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES.to_string()), + "the limit is not named: {error}" + ); } /// Resolves the codec interface from one complete WIT document. @@ -1021,4 +1033,14 @@ mod tests { let (_, interface_id) = codec_world_export(&resolve, world_id).unwrap(); (resolve, interface_id) } + + /// Returns the package-qualified identity of one resolved codec interface. + fn codec_interface_identity( + resolve: &Resolve, + interface_id: wit_parser::InterfaceId, + ) -> String { + let interface = &resolve.interfaces[interface_id]; + let package = &resolve.packages[interface.package.unwrap()].name; + package.interface_id(interface.name.as_deref().unwrap()) + } } diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index 774ea44586..570d7cfc66 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -22,6 +22,8 @@ use miden_mast_package::Package; use midenc_codegen_masm::{MasmComponent, intrinsics}; use midenc_session::{Session, diagnostics::Report}; +use crate::cargo::NOTE_CODEC_CRATE_METADATA; + /// Apply the session's link inputs to `assembler` before a project is assembled with it. pub(crate) fn prepare_assembler( assembler: &mut miden_assembly::Assembler, @@ -120,14 +122,15 @@ fn validate_note_codec_declaration( if has_note_codec && !package_has_note_target { return Err(Report::msg(format!( - "`[package.metadata.note-codec-crate]` requires a note target, but the package that \ - contains target '{target_name}' defines no note target" + "`[package.metadata.{NOTE_CODEC_CRATE_METADATA}]` requires a note target, but the \ + package that contains target '{target_name}' defines no note target" ))); } if has_note_codec && target_type == TargetType::Note && !has_note_storage_schema { return Err(Report::msg(format!( - "note target '{target_name}' declares `[package.metadata.note-codec-crate]` but \ - emitted no note storage schema; add one named-field `#[note]` struct" + "note target '{target_name}' declares \ + `[package.metadata.{NOTE_CODEC_CRATE_METADATA}]` but emitted no note storage schema; \ + add one named-field `#[note]` struct" ))); } Ok(()) diff --git a/midenc-compile/src/pipeline/frontends/rust.rs b/midenc-compile/src/pipeline/frontends/rust.rs index df09a8e6c8..1b2f998c16 100644 --- a/midenc-compile/src/pipeline/frontends/rust.rs +++ b/midenc-compile/src/pipeline/frontends/rust.rs @@ -867,12 +867,10 @@ fn build_cargo_args(manifest_path: &Path, options: &Options) -> Vec { if options.profile == "release" { args.push("--release".to_string()); } - if options.cargo_locked { - args.push("--locked".to_string()); - } - if options.cargo_offline { - args.push("--offline".to_string()); - } + args.extend( + crate::cargo::apply_cargo_policy(options.cargo_locked, options.cargo_offline) + .map(ToString::to_string), + ); args.push("--manifest-path".to_string()); args.push(manifest_path.to_string_lossy().to_string()); @@ -2192,12 +2190,10 @@ pub(crate) mod manifest { if cargo_opts.release { args.push("--release".to_string()); } - if cargo_opts.locked { - args.push("--locked".to_string()); - } - if cargo_opts.offline { - args.push("--offline".to_string()); - } + args.extend( + crate::cargo::apply_cargo_policy(cargo_opts.locked, cargo_opts.offline) + .map(ToString::to_string), + ); if let Some(ref manifest_path) = cargo_opts.manifest_path { args.push("--manifest-path".to_string()); diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index 17ba38913f..82c697a427 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -115,6 +115,8 @@ struct RegisteredExportType { /// /// The location tells a stale re-expansion of an edited item (same location) from a real /// conflict between two items (different locations). +// Keep this registry identity/replacement policy aligned with +// sdk/note-codec/macros/src/registry.rs; changes must land in both. type ExpansionLocation = (String, usize, usize); /// Returns the (file, line, column) location of one expansion span. diff --git a/sdk/note-bindings/macros/Cargo.toml b/sdk/note-bindings/macros/Cargo.toml index 7b9dddbc97..522c421b45 100644 --- a/sdk/note-bindings/macros/Cargo.toml +++ b/sdk/note-bindings/macros/Cargo.toml @@ -20,6 +20,7 @@ doctest = false [dependencies] miden-note-schema.workspace = true miden-note-schema-codegen.workspace = true +midenc-frontend-wasm-metadata.workspace = true proc-macro2.workspace = true proc-macro-crate = { workspace = true } quote.workspace = true diff --git a/sdk/note-bindings/macros/src/lib.rs b/sdk/note-bindings/macros/src/lib.rs index cbf80c6810..9d75499e04 100644 --- a/sdk/note-bindings/macros/src/lib.rs +++ b/sdk/note-bindings/macros/src/lib.rs @@ -69,11 +69,11 @@ fn expand_package_artifact( let scope_key = artifact.path().to_string_lossy(); let bindings = expand_schema(artifact.schema(), span, &scope_key)?; let tracked_path = artifact.path().to_string_lossy(); + let package_cache_env = midenc_frontend_wasm_metadata::package_cache::PACKAGE_CACHE_ENV; Ok(quote! { - #[doc(hidden)] - const _: &[u8] = include_bytes!(#tracked_path); - #[doc(hidden)] - const _: Option<&str> = option_env!("MIDENC_PACKAGE_CACHE"); + // These constants exist only to register the package file and cache path as proc-macro rebuild inputs. + const _: &[u8] = ::core::include_bytes!(#tracked_path); + const _: ::core::option::Option<&str> = ::core::option_env!(#package_cache_env); #bindings }) } @@ -224,7 +224,7 @@ fn binding_facade() -> syn::Path { } } -/// Returns a deterministic scope suffix for one macro input. +/// Returns a deterministic FNV-1a scope suffix for one macro input. fn stable_hash(value: &str) -> u64 { value.bytes().fold(0xcbf2_9ce4_8422_2325, |hash, byte| { (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3) diff --git a/sdk/note-codec/macros/Cargo.toml b/sdk/note-codec/macros/Cargo.toml index 6b147a5395..9111a1aaf1 100644 --- a/sdk/note-codec/macros/Cargo.toml +++ b/sdk/note-codec/macros/Cargo.toml @@ -22,6 +22,7 @@ heck.workspace = true miden-note-codec-wit.workspace = true miden-note-schema.workspace = true miden-note-schema-codegen.workspace = true +midenc-frontend-wasm-metadata.workspace = true proc-macro2 = { workspace = true, features = ["span-locations"] } proc-macro-crate = { workspace = true } quote.workspace = true diff --git a/sdk/note-codec/macros/src/expand.rs b/sdk/note-codec/macros/src/expand.rs index bca95ff7b9..225da9c591 100644 --- a/sdk/note-codec/macros/src/expand.rs +++ b/sdk/note-codec/macros/src/expand.rs @@ -6,7 +6,7 @@ use miden_note_schema_codegen::{RuntimePaths, generate_host_types}; use proc_macro_crate::{FoundCrate, crate_name}; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote}; -use syn::{ItemImpl, LitStr, Type}; +use syn::{ItemImpl, LitStr, Type, spanned::Spanned}; use crate::registry::{register_codec, register_schema, registered_codecs}; @@ -37,11 +37,11 @@ pub(crate) fn from_wit_text(input: &LitStr) -> syn::Result { fn expand_package_artifact(artifact: &NotePackageArtifact, span: Span) -> syn::Result { let types = expand_schema(artifact.schema(), span)?; let tracked_path = artifact.path().to_string_lossy(); + let package_cache_env = midenc_frontend_wasm_metadata::package_cache::PACKAGE_CACHE_ENV; Ok(quote! { - #[doc(hidden)] - const _: &[u8] = include_bytes!(#tracked_path); - #[doc(hidden)] - const _: Option<&str> = option_env!("MIDENC_PACKAGE_CACHE"); + // These constants exist only to register the package file and cache path as proc-macro rebuild inputs. + const _: &[u8] = ::core::include_bytes!(#tracked_path); + const _: ::core::option::Option<&str> = ::core::option_env!(#package_cache_env); #types }) } @@ -267,5 +267,3 @@ fn note_codec_facade() -> syn::Path { Err(_) => syn::parse_quote!(::miden_note_codec), } } - -use syn::spanned::Spanned; diff --git a/sdk/note-codec/macros/src/registry.rs b/sdk/note-codec/macros/src/registry.rs index 7687f75aba..62b7b2de03 100644 --- a/sdk/note-codec/macros/src/registry.rs +++ b/sdk/note-codec/macros/src/registry.rs @@ -37,6 +37,8 @@ static REGISTRY: OnceLock>> = OnceLock::new(); /// /// The location tells a stale re-expansion of an edited invocation (same location) from a /// real conflict between two invocations (different locations). +// Keep this registry identity/replacement policy aligned with sdk/base-macros/src/types.rs; +// changes must land in both. type ExpansionLocation = (String, usize, usize); /// Returns the (file, line, column) location of one expansion span. diff --git a/sdk/note-codec/wit-crate/wit/note-codec.wit b/sdk/note-codec/wit-crate/wit/note-codec.wit index d27dcc081e..950aa77cb9 100644 --- a/sdk/note-codec/wit-crate/wit/note-codec.wit +++ b/sdk/note-codec/wit-crate/wit/note-codec.wit @@ -1,3 +1,5 @@ +// Mirror note: the wit-crate file is canonical; sdk/note-schema/wit/note-codec.wit exists because +// Wasmtime bindgen!(path:) needs package-local WIT, and an equality test guards both files. package miden:note-codec@1.0.0; /// Parses, displays, and validates custom note storage types. diff --git a/sdk/note-schema/codegen/src/lib.rs b/sdk/note-schema/codegen/src/lib.rs index 5363ae44a0..5fe4eac560 100644 --- a/sdk/note-schema/codegen/src/lib.rs +++ b/sdk/note-schema/codegen/src/lib.rs @@ -310,6 +310,7 @@ fn generate_helper_traits(runtime: &RuntimePaths) -> TokenStream { "failed to decode account-id suffix: {error}" )) })?; + // WIT declares prefix before suffix, but the constructor takes suffix first. #miden_protocol::account::AccountId::try_from_elements(suffix, prefix).map_err( |error| { #miden_note_schema::Error::new(format!( diff --git a/sdk/note-schema/src/builder.rs b/sdk/note-schema/src/builder.rs index 8bade27351..a31da2b670 100644 --- a/sdk/note-schema/src/builder.rs +++ b/sdk/note-schema/src/builder.rs @@ -5,8 +5,8 @@ use std::collections::BTreeMap; use miden_field_repr::FeltWriter; use crate::{ - CodecRegistry, Error, Felt, NoteStorage, NoteStorageSchema, PrimitiveType, Result, SchemaType, - SchemaTypeKind, + CodecRegistry, Error, Felt, NoteStorage, NoteStorageSchema, PrimitiveType, Result, SchemaField, + SchemaType, SchemaTypeKind, codec::{parse_felt, parse_unsigned, write_repr}, schema::normalize_name, value::validate_encoding, @@ -167,13 +167,22 @@ fn encode_type( } if let SchemaTypeKind::Record(fields) = ty.kind() { - for field in fields { - let field_path = if path.is_empty() { - field.name().to_owned() - } else { - format!("{path}.{}", field.name()) - }; - encode_type(field.ty(), &field_path, values, registry, writer)?; + let Some((fqn, codec)) = ty.fqn().and_then(|fqn| Some((fqn, registry.codec(fqn)?))) else { + return encode_record_fields(fields, path, values, registry, writer); + }; + // A record assembled from child-path values must still satisfy its registered + // codec, or the builder would produce storage that the same registry rejects on + // decode. Encode the subtree separately so the codec can validate it as one value. + let mut subtree = Vec::new(); + encode_record_fields(fields, path, values, registry, &mut FeltWriter::new(&mut subtree))?; + codec.validate(&subtree).map_err(|err| { + err.context(format!( + "codec `{fqn}` validation failed for the values assigned under `{}`", + if path.is_empty() { "" } else { path } + )) + })?; + for felt in subtree { + writer.write(felt); } return Ok(()); } @@ -184,6 +193,25 @@ fn encode_type( ))) } +/// Encodes every field of a record from complete path assignments. +fn encode_record_fields( + fields: &[SchemaField], + path: &str, + values: &BTreeMap, + registry: &CodecRegistry, + writer: &mut FeltWriter<'_>, +) -> Result<()> { + for field in fields { + let field_path = if path.is_empty() { + field.name().to_owned() + } else { + format!("{path}.{}", field.name()) + }; + encode_type(field.ty(), &field_path, values, registry, writer)?; + } + Ok(()) +} + /// Encodes one direct string value. fn encode_text_value(ty: &SchemaType, value: &str, registry: &CodecRegistry) -> Result> { if let Some(fqn) = ty.fqn() diff --git a/sdk/note-schema/src/codec.rs b/sdk/note-schema/src/codec.rs index 23dcb56f84..28c244f3ff 100644 --- a/sdk/note-schema/src/codec.rs +++ b/sdk/note-schema/src/codec.rs @@ -269,6 +269,7 @@ fn read_account_id(felts: &[Felt]) -> Result { reader .ensure_eof() .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?; + // WIT declares prefix before suffix, but the constructor takes suffix first. AccountId::try_from_elements(suffix, prefix) .map_err(|err| Error::new(format!("invalid account-id representation: {err}"))) } diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 140ecdec08..a7149b4b4b 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -16,7 +16,7 @@ use crate::{CodecRegistry, ConsumerTypeCodec, Error, NoteStorageSchema, Result}; const CALL_FUEL: u64 = 10_000_000; /// Maximum bytes accepted for one untrusted note codec component before Wasmtime compilation. -const MAX_COMPONENT_BYTES: usize = 4 * 1024 * 1024; +const MAX_COMPONENT_BYTES: usize = crate::schema::MAX_NOTE_CODEC_COMPONENT_BYTES; /// Maximum bytes available to one codec component linear memory. const MAX_COMPONENT_MEMORY_BYTES: usize = 16 * 1024 * 1024; @@ -331,6 +331,7 @@ mod tests { Version, }; use midenc_frontend_wasm_metadata::package_note_storage_schema_section_id; + use midenc_integration_test_support::wasm_target_is_installed; use tempfile::TempDir; use wasmtime::ResourceLimiter; @@ -616,6 +617,7 @@ package miden:base@1.0.0 { "--offline", ]) .env("CARGO_TARGET_DIR", &target_dir) + // Outer Miden target settings and flags would poison this nested wasip2 codec build. .env_remove("CARGO_BUILD_TARGET") .env_remove("CARGO_ENCODED_RUSTFLAGS") .env_remove("RUSTFLAGS") @@ -653,18 +655,7 @@ miden-note-codec = {{ path = {:?} }} codec_path ); fs::write(root.join("Cargo.toml"), manifest).unwrap(); - fs::write(root.join("src/lib.rs"), FIXTURE_SOURCE).unwrap(); - } - - /// Returns true when rustup reports the component target as installed. - fn wasm_target_is_installed() -> bool { - let Ok(output) = Command::new("rustup").args(["target", "list"]).output() else { - return false; - }; - output.status.success() - && String::from_utf8_lossy(&output.stdout) - .lines() - .any(|line| line.starts_with(WASM_TARGET) && line.contains("(installed)")) + fs::write(root.join("src/lib.rs"), fixture_source()).unwrap(); } /// Returns the compiler workspace root. @@ -686,25 +677,15 @@ miden-note-codec = {{ path = {:?} }} ); } - const FIXTURE_SOURCE: &str = r##" + /// Builds the fixture source around the schema shared with host-side assertions. + fn fixture_source() -> String { + [ + r##" use miden_note_codec::AuthorTypeCodec; -miden_note_codec::from_wit_text!(r#" -package example:codec-schema@1.0.0; - -interface note-storage { - record ratio { - numerator: u64, - denominator: u64, - } - - record codec-note { - ratio: ratio, - } - - type storage = codec-note; -} -"#); +miden_note_codec::from_wit_text!(r#""##, + FIXTURE_SCHEMA, + r##""#); #[miden_note_codec::note_codec] impl AuthorTypeCodec for Ratio { @@ -740,5 +721,8 @@ impl AuthorTypeCodec for Ratio { } miden_note_codec::export_codecs!(); -"##; +"##, + ] + .concat() + } } diff --git a/sdk/note-schema/src/lib.rs b/sdk/note-schema/src/lib.rs index 78425a7a79..87424ffb29 100644 --- a/sdk/note-schema/src/lib.rs +++ b/sdk/note-schema/src/lib.rs @@ -37,8 +37,8 @@ pub use error::{Error, Result}; pub use miden_field::Felt; pub use miden_protocol::note::NoteStorage; pub use schema::{ - FeltLayout, MAX_NOTE_STORAGE_SCHEMA_BYTES, MAX_NOTE_STORAGE_SCHEMA_DEPTH, - MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_TYPES, NoteStorageSchema, PrimitiveType, - SchemaCase, SchemaField, SchemaType, SchemaTypeKind, + FeltLayout, MAX_NOTE_CODEC_COMPONENT_BYTES, MAX_NOTE_STORAGE_SCHEMA_BYTES, + MAX_NOTE_STORAGE_SCHEMA_DEPTH, MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_TYPES, + NoteStorageSchema, PrimitiveType, SchemaCase, SchemaField, SchemaType, SchemaTypeKind, }; pub use value::{DecodedValue, DecodedValueKind}; diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs index 23e2167f62..61dd89e54c 100644 --- a/sdk/note-schema/src/schema.rs +++ b/sdk/note-schema/src/schema.rs @@ -37,6 +37,12 @@ pub const MAX_NOTE_STORAGE_SCHEMA_DEPTH: usize = MAX_NOTE_STORAGE_ITEMS / 8; /// Maximum number of felts in the root note storage layout. pub const MAX_NOTE_STORAGE_SCHEMA_FELTS: usize = MAX_NOTE_STORAGE_ITEMS; +/// Maximum bytes accepted for one note codec component before Wasmtime compilation. +/// +/// The compiler enforces the same limit when it attaches a codec, so a package that +/// builds is a package that consumers accept. +pub const MAX_NOTE_CODEC_COMPONENT_BYTES: usize = 4 * 1024 * 1024; + const _: () = assert!(MAX_NOTE_STORAGE_SCHEMA_DEPTH > 0); /// The minimum and maximum felt count for a schema type. @@ -412,6 +418,7 @@ fn validate_resolved_core_types(resolve: &Resolve) -> Result<()> { return Err(core_shape_error(name, fields)); } if leaf == StandardLeaf::Felt { + // Miden's canonical felt uses `f32` as its WIT-level placeholder representation. if !resolves_to_primitive(resolve, record.fields[0].ty, Type::F32)? { return Err(core_shape_error(name, fields)); } @@ -573,6 +580,7 @@ fn collect_custom_type_fqns( seen: &mut HashSet<*const SchemaType>, fqns: &mut HashSet, ) { + // Pointer identity is sufficient because ModelBuilder memoizes exactly one Arc per TypeId. if !seen.insert(core::ptr::from_ref(ty)) { return; } @@ -597,11 +605,18 @@ fn collect_custom_type_fqns( } } +/// One memoized schema node and its maximum depth below that node. +#[derive(Clone)] +struct MemoizedSchemaType { + ty: Arc, + maximum_subtree_depth: usize, +} + /// Builds a memoized schema graph from a resolved WIT graph. struct ModelBuilder<'a> { resolve: &'a Resolve, active: HashSet, - memo: HashMap>, + memo: HashMap, } impl<'a> ModelBuilder<'a> { @@ -616,11 +631,11 @@ impl<'a> ModelBuilder<'a> { /// Resolves one WIT type. fn build(mut self, ty: Type) -> Result> { - self.build_type(ty, 0) + self.build_type(ty, 0).map(|memoized| memoized.ty) } /// Resolves a primitive or named type. - fn build_type(&mut self, ty: Type, depth: usize) -> Result> { + fn build_type(&mut self, ty: Type, depth: usize) -> Result { if depth > MAX_NOTE_STORAGE_SCHEMA_DEPTH { return Err(Error::new(format!( "note storage schema nesting depth {depth} exceeds the limit of \ @@ -640,10 +655,17 @@ impl<'a> ModelBuilder<'a> { } /// Resolves aliases to the type definition that owns the structural type. - fn build_type_id(&mut self, id: TypeId, depth: usize) -> Result> { + fn build_type_id(&mut self, id: TypeId, depth: usize) -> Result { let id = self.follow_aliases(id)?; - if let Some(ty) = self.memo.get(&id) { - return Ok(Arc::clone(ty)); + if let Some(memoized) = self.memo.get(&id) { + let maximum_depth = depth.saturating_add(memoized.maximum_subtree_depth); + if maximum_depth > MAX_NOTE_STORAGE_SCHEMA_DEPTH { + return Err(Error::new(format!( + "note storage schema nesting depth {maximum_depth} exceeds the limit of \ + {MAX_NOTE_STORAGE_SCHEMA_DEPTH}" + ))); + } + return Ok(memoized.clone()); } if !self.active.insert(id) { return Err(Error::new( @@ -656,70 +678,92 @@ impl<'a> ModelBuilder<'a> { let docs = definition.docs.contents.clone(); let fqn = self.type_fqn(id)?; let result = if fqn.as_deref() == Some(FELT_FQN) { - Ok(Arc::new(SchemaType { - name, - fqn, - docs, - kind: SchemaTypeKind::Felt, - layout: FeltLayout::fixed(1), - })) + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Felt, + layout: FeltLayout::fixed(1), + }), + maximum_subtree_depth: 0, + }) } else { match definition.kind { TypeDefKind::Type(ty) => self.build_named_alias(ty, name, fqn, docs, depth), TypeDefKind::Record(record) => { let mut fields = Vec::with_capacity(record.fields.len()); let mut layout = FeltLayout::fixed(0); + let mut maximum_subtree_depth = 0; for field in record.fields { - let ty = self.build_type(field.ty, depth + 1)?; - layout = layout.concatenate(ty.layout)?; + let memoized = self.build_type(field.ty, depth + 1)?; + maximum_subtree_depth = + maximum_subtree_depth.max(1 + memoized.maximum_subtree_depth); + layout = layout.concatenate(memoized.ty.layout)?; fields.push(SchemaField { name: field.name, docs: field.docs.contents, - ty, + ty: memoized.ty, }); } - Ok(Arc::new(SchemaType { - name, - fqn, - docs, - kind: SchemaTypeKind::Record(fields), - layout, - })) + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Record(fields), + layout, + }), + maximum_subtree_depth, + }) } TypeDefKind::Option(payload) => { let payload = self.build_type(payload, depth + 1)?; let maximum = 1usize - .checked_add(payload.layout.maximum) + .checked_add(payload.ty.layout.maximum) .ok_or_else(|| Error::new("option layout maximum width is too large"))?; let layout = FeltLayout::bounded(1, maximum)?; - Ok(Arc::new(SchemaType { - name, - fqn, - docs, - kind: SchemaTypeKind::Option(payload), - layout, - })) + Ok(MemoizedSchemaType { + maximum_subtree_depth: 1 + payload.maximum_subtree_depth, + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Option(payload.ty), + layout, + }), + }) } TypeDefKind::Variant(variant) => { let mut cases = Vec::with_capacity(variant.cases.len()); + let mut maximum_subtree_depth = 0; for case in variant.cases { + let payload = match case.ty { + Some(ty) => { + let memoized = self.build_type(ty, depth + 1)?; + maximum_subtree_depth = + maximum_subtree_depth.max(1 + memoized.maximum_subtree_depth); + Some(memoized.ty) + } + None => None, + }; cases.push(SchemaCase { name: case.name, docs: case.docs.contents, - payload: case - .ty - .map(|ty| self.build_type(ty, depth + 1)) - .transpose()?, + payload, }); } let layout = variant_layout(&cases)?; - Ok(Arc::new(SchemaType { - name, - fqn, - docs, - kind: SchemaTypeKind::Variant(cases), - layout, - })) + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Variant(cases), + layout, + }), + maximum_subtree_depth, + }) } TypeDefKind::Enum(enum_) => { let cases = enum_ @@ -732,13 +776,16 @@ impl<'a> ModelBuilder<'a> { }) .collect::>(); let layout = variant_layout(&cases)?; - Ok(Arc::new(SchemaType { - name, - fqn, - docs, - kind: SchemaTypeKind::Variant(cases), - layout, - })) + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Variant(cases), + layout, + }), + maximum_subtree_depth: 0, + }) } unsupported => Err(Error::new(format!( "WIT {} `{}` is not supported in note storage schemas", @@ -748,13 +795,14 @@ impl<'a> ModelBuilder<'a> { } }; self.active.remove(&id); - if let Ok(ty) = &result { - self.memo.insert(id, Arc::clone(ty)); + if let Ok(memoized) = &result { + self.memo.insert(id, memoized.clone()); } result } - /// Resolves a named alias whose target is a primitive. + /// Resolves a primitive alias with its metadata, or follows an ID alias while discarding that + /// alias's name, FQN, and documentation. fn build_named_alias( &mut self, ty: Type, @@ -762,7 +810,7 @@ impl<'a> ModelBuilder<'a> { fqn: Option, docs: Option, depth: usize, - ) -> Result> { + ) -> Result { match ty { Type::Id(id) => self.build_type_id(id, depth), Type::U64 => self.primitive(PrimitiveType::U64, name, fqn, docs), @@ -782,18 +830,21 @@ impl<'a> ModelBuilder<'a> { name: Option, fqn: Option, docs: Option, - ) -> Result> { + ) -> Result { let width = match primitive { PrimitiveType::U64 => 2, PrimitiveType::U32 | PrimitiveType::U8 | PrimitiveType::Bool => 1, }; - Ok(Arc::new(SchemaType { - name, - fqn, - docs, - kind: SchemaTypeKind::Primitive(primitive), - layout: FeltLayout::fixed(width), - })) + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Primitive(primitive), + layout: FeltLayout::fixed(width), + }), + maximum_subtree_depth: 0, + }) } /// Follows `type = id` aliases to their defining type. diff --git a/sdk/note-schema/src/tests.rs b/sdk/note-schema/src/tests.rs index 5a1332964e..d8e34b3c38 100644 --- a/sdk/note-schema/src/tests.rs +++ b/sdk/note-schema/src/tests.rs @@ -235,6 +235,62 @@ fn decoder_uses_structural_fallback_without_a_codec() { assert_eq!(fields[1].to_string(), account_id.suffix().as_canonical_u64().to_string()); } +#[test] +fn builder_validates_codec_records_assembled_from_child_paths() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (account_id, _) = account_id(); + + // Child-path values that form no valid account id must fail the codec, not encode raw. + let error = schema + .builder() + .set("bedrock", "5") + .unwrap() + .set("nested.wide_count", "1") + .unwrap() + .set("nested.small-count", "1") + .unwrap() + .set("maybe_enabled", "none") + .unwrap() + .set("selected", "count(1)") + .unwrap() + .set("target_account_id.prefix", "1") + .unwrap() + .set("target_account_id.suffix", "1") + .unwrap() + .build() + .err() + .unwrap() + .to_string(); + assert!(error.contains("validation failed"), "unexpected error: {error}"); + assert!(error.contains("target-account-id"), "the path is not named: {error}"); + + // Valid child-path values pass the codec and encode like the whole-value assignment. + let storage = schema + .builder() + .set("bedrock", "5") + .unwrap() + .set("nested.wide_count", "1") + .unwrap() + .set("nested.small-count", "1") + .unwrap() + .set("maybe_enabled", "none") + .unwrap() + .set("selected", "count(1)") + .unwrap() + .set( + "target_account_id.prefix", + account_id.prefix().as_felt().as_canonical_u64().to_string(), + ) + .unwrap() + .set("target_account_id.suffix", account_id.suffix().as_canonical_u64().to_string()) + .unwrap() + .build() + .unwrap(); + let felts = storage.to_elements(); + assert_eq!(felts[felts.len() - 2], account_id.prefix().as_felt()); + assert_eq!(felts[felts.len() - 1], account_id.suffix()); +} + #[test] fn builder_reports_missing_unknown_conflicting_and_range_errors() { let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); @@ -378,6 +434,20 @@ fn deep_schema_chain_fails_fast_at_depth_limit() { ); } +#[test] +fn memoized_subtree_reuse_still_enforces_the_depth_limit() { + let error = NoteStorageSchema::from_wit_text(&memoized_reuse_depth_schema()) + .err() + .expect("deep reuse of a memoized subtree must fail") + .to_string(); + + assert!(error.contains("nesting depth"), "unexpected memoized-depth error: {error}"); + assert!( + error.contains(&MAX_NOTE_STORAGE_SCHEMA_DEPTH.to_string()), + "the nesting limit must be present in the diagnostic: {error}" + ); +} + #[test] fn schema_reader_enforces_documented_byte_type_and_root_width_limits() { let oversized = " ".repeat(MAX_NOTE_STORAGE_SCHEMA_BYTES + 1); @@ -440,3 +510,26 @@ fn deep_chain_schema(levels: usize) -> String { wit.push_str(&format!("type storage = t{levels}; }}")); wit } + +/// Builds two individually valid chains whose composition exceeds the depth limit only on reuse. +fn memoized_reuse_depth_schema() -> String { + let levels = MAX_NOTE_STORAGE_SCHEMA_DEPTH / 2; + let mut wit = String::from( + "package example:memoized-depth@1.0.0; interface note-storage { record shared0 { value: \ + u8 } ", + ); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record shared{level} {{ value: shared{previous} }} ")); + } + wit.push_str(&format!("record wrapper0 {{ value: shared{levels} }} ")); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record wrapper{level} {{ value: wrapper{previous} }} ")); + } + wit.push_str(&format!( + "record root {{ cached: shared{levels}, too-deep: wrapper{levels} }} type storage = root; \ + }}" + )); + wit +} diff --git a/sdk/note-schema/wit/note-codec.wit b/sdk/note-schema/wit/note-codec.wit index d27dcc081e..950aa77cb9 100644 --- a/sdk/note-schema/wit/note-codec.wit +++ b/sdk/note-schema/wit/note-codec.wit @@ -1,3 +1,5 @@ +// Mirror note: the wit-crate file is canonical; sdk/note-schema/wit/note-codec.wit exists because +// Wasmtime bindgen!(path:) needs package-local WIT, and an equality test guards both files. package miden:note-codec@1.0.0; /// Parses, displays, and validates custom note storage types. diff --git a/tests/support/src/lib.rs b/tests/support/src/lib.rs index 4ae60063d0..b1733ea27a 100644 --- a/tests/support/src/lib.rs +++ b/tests/support/src/lib.rs @@ -2,6 +2,16 @@ #![deny(warnings)] #![deny(missing_docs)] +use std::{ + fs::{self, File}, + path::{Path, PathBuf}, + process::Command, + sync::Arc, +}; + +use miden_mast_package::Package; +use midenc_frontend_wasm::WasmTranslationConfig; + /// Utilities for generating on-disk Cargo projects for tests. pub mod cargo_proj; /// Compiler test builders and pipeline assertions. @@ -20,3 +30,53 @@ pub use self::{ compiler_test::{CargoTest, CompilerTest, CompilerTestBuilder, RustcTest, WasmTest}, testing::setup::default_session, }; + +/// Compiles one Cargo Miden project without debug output. +pub fn compile_project(project_path: &Path) -> Arc { + let mut test = CompilerTest::rust_source_cargo_miden( + project_path, + WasmTranslationConfig::default(), + ["--debug".to_owned(), "none".to_owned()], + ); + test.compile_package() +} + +/// Returns the compiler workspace root. +pub fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() +} + +/// Locks the shared p2id example outputs for the full build and consume span. +pub fn p2id_build_lock(workspace: &Path) -> File { + let target_dir = workspace.join("target"); + fs::create_dir_all(&target_dir).expect("failed to create the workspace target directory"); + let lock = File::options() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(target_dir.join("p2id-end-to-end-build.lock")) + .expect("failed to open the p2id end-to-end build lock"); + lock.lock().expect("failed to lock the p2id end-to-end build"); + lock +} + +/// Returns true when rustup reports the codec component target as installed. +pub fn wasm_target_is_installed() -> bool { + const WASM_TARGET: &str = "wasm32-wasip2"; + + let output = match Command::new("rustup").args(["target", "list"]).output() { + Ok(output) if output.status.success() => output, + Ok(output) => { + eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); + return false; + } + Err(error) => { + eprintln!("could not run `rustup target list`: {error}"); + return false; + } + }; + String::from_utf8_lossy(&output.stdout) + .lines() + .any(|line| line.starts_with(WASM_TARGET) && line.contains("(installed)")) +} From f85c9dcc2ebc1ec827deaf6f1967d041cb727ae1 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 16:15:33 +0300 Subject: [PATCH 18/43] test: share the note end-to-end scaffolding The p2id build lock, the example compilation helper, the workspace root helper, and the wasm-target probe existed as verbatim copies across four crates. They now live in the integration test-support crate, and the component fixture no longer embeds its schema text as a second literal. --- .../tests/unit_note_trailing_data.rs | 1 + sdk/note-bindings/tests/p2id_consumer.rs | 45 ++----------------- sdk/note-codec/Cargo.toml | 1 + sdk/note-codec/tests/component_export.rs | 19 +------- sdk/note-schema/tests/p2id_package.rs | 40 +---------------- .../src/mockchain/notes/schema.rs | 21 +-------- tools/cargo-miden/Cargo.toml | 1 + .../cargo-miden/tests/dex_note_codec_build.rs | 19 +------- 8 files changed, 12 insertions(+), 135 deletions(-) diff --git a/sdk/base-macros/tests/unit_note_trailing_data.rs b/sdk/base-macros/tests/unit_note_trailing_data.rs index d1238da1e0..129e9f50e4 100644 --- a/sdk/base-macros/tests/unit_note_trailing_data.rs +++ b/sdk/base-macros/tests/unit_note_trailing_data.rs @@ -1,3 +1,4 @@ +// Keep this as a separate file because one crate may hold only one #[note] struct. //! Tests trailing-data rejection for unit note structs. use core::convert::TryFrom; diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs index 074141ff1f..6dfb5e9725 100644 --- a/sdk/note-bindings/tests/p2id_consumer.rs +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -1,47 +1,10 @@ //! End-to-end test for package discovery and generated p2id consumer bindings. -use std::{ - env, fs, - fs::File, - path::{Path, PathBuf}, - process::Command, - sync::Arc, -}; - -use miden_mast_package::{Package, Section}; -use midenc_frontend_wasm::WasmTranslationConfig; -use midenc_frontend_wasm_metadata::package_note_storage_schema_section_id; -use midenc_integration_test_support::CompilerTest; - -/// Compiles one Cargo Miden project without debug output. -fn compile_project(project_path: &Path) -> Arc { - let mut test = CompilerTest::rust_source_cargo_miden( - project_path, - WasmTranslationConfig::default(), - ["--debug".to_owned(), "none".to_owned()], - ); - test.compile_package() -} +use std::{env, fs, path::Path, process::Command}; -/// Returns the compiler workspace root. -fn workspace_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() -} - -/// Locks the shared p2id example outputs for the full build and consume span. -fn p2id_build_lock(workspace: &Path) -> File { - let target_dir = workspace.join("target"); - fs::create_dir_all(&target_dir).expect("failed to create the workspace target directory"); - let lock = File::options() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(target_dir.join("p2id-end-to-end-build.lock")) - .expect("failed to open the p2id end-to-end build lock"); - lock.lock().expect("failed to lock the p2id end-to-end build"); - lock -} +use miden_mast_package::Section; +use midenc_frontend_wasm_metadata::package_note_storage_schema_section_id; +use midenc_integration_test_support::{compile_project, p2id_build_lock, workspace_root}; /// Returns the native rustc host target. fn host_target() -> String { diff --git a/sdk/note-codec/Cargo.toml b/sdk/note-codec/Cargo.toml index 533f207d6b..ce91d3f7a5 100644 --- a/sdk/note-codec/Cargo.toml +++ b/sdk/note-codec/Cargo.toml @@ -25,6 +25,7 @@ miden-protocol.workspace = true wit-bindgen = { workspace = true } [dev-dependencies] +midenc-integration-test-support.workspace = true tempfile.workspace = true wit-component.workspace = true wit-parser.workspace = true diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index 2f9bbdf2d0..cea142674e 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -6,6 +6,7 @@ use std::{ process::{Command, Output}, }; +use midenc_integration_test_support::wasm_target_is_installed; use tempfile::TempDir; use wit_component::DecodedWasm; use wit_parser::WorldItem; @@ -77,24 +78,6 @@ fn minimal_codec_crate_builds_to_wasi_only_component() { ); } -/// Returns true when rustup reports the component target as installed. -fn wasm_target_is_installed() -> bool { - let output = match Command::new("rustup").args(["target", "list"]).output() { - Ok(output) if output.status.success() => output, - Ok(output) => { - eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); - return false; - } - Err(error) => { - eprintln!("could not run `rustup target list`: {error}"); - return false; - } - }; - String::from_utf8_lossy(&output.stdout) - .lines() - .any(|line| line.starts_with(WASM_TARGET) && line.contains("(installed)")) -} - /// Writes the minimal author codec crate used by the componentization test. fn write_fixture(root: &Path) { fs::create_dir(root.join("src")).expect("failed to create fixture source directory"); diff --git a/sdk/note-schema/tests/p2id_package.rs b/sdk/note-schema/tests/p2id_package.rs index 795d2264dd..277196732c 100644 --- a/sdk/note-schema/tests/p2id_package.rs +++ b/sdk/note-schema/tests/p2id_package.rs @@ -1,46 +1,8 @@ //! End-to-end test for a schema embedded in the p2id note package. -use std::{ - fs::{self, File}, - path::{Path, PathBuf}, - sync::Arc, -}; - -use miden_mast_package::Package; use miden_note_schema::{NoteStorage, NoteStorageSchema}; use miden_protocol::{account::AccountId, address::NetworkId}; -use midenc_frontend_wasm::WasmTranslationConfig; -use midenc_integration_test_support::CompilerTest; - -/// Compiles one Cargo Miden project without debug output. -fn compile_project(project_path: &Path) -> Arc { - let mut test = CompilerTest::rust_source_cargo_miden( - project_path, - WasmTranslationConfig::default(), - ["--debug".to_owned(), "none".to_owned()], - ); - test.compile_package() -} - -/// Returns the compiler workspace root. -fn workspace_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() -} - -/// Locks the shared p2id example outputs for the full build and consume span. -fn p2id_build_lock(workspace: &Path) -> File { - let target_dir = workspace.join("target"); - fs::create_dir_all(&target_dir).expect("failed to create the workspace target directory"); - let lock = File::options() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(target_dir.join("p2id-end-to-end-build.lock")) - .expect("failed to open the p2id end-to-end build lock"); - lock.lock().expect("failed to lock the p2id end-to-end build"); - lock -} +use midenc_integration_test_support::{compile_project, p2id_build_lock, workspace_root}; #[test] fn p2id_schema_builds_and_decodes_account_id_storage() { diff --git a/tests/integration-network/src/mockchain/notes/schema.rs b/tests/integration-network/src/mockchain/notes/schema.rs index 3f9619fa54..4618e37071 100644 --- a/tests/integration-network/src/mockchain/notes/schema.rs +++ b/tests/integration-network/src/mockchain/notes/schema.rs @@ -1,6 +1,6 @@ //! Schema-driven note storage tests on the mock chain. -use std::{process::Command, sync::Arc}; +use std::sync::Arc; use miden_client::{ account::{AccountComponent, component::InitStorageData}, @@ -21,6 +21,7 @@ use midenc_frontend_wasm_metadata::{ PACKAGE_NOTE_CODEC_SECTION_ID, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, package_note_codec_section_id, package_note_storage_schema_section_id, }; +use midenc_integration_test_support::wasm_target_is_installed; use super::super::support::{ assert_account_has_fungible_asset, build_send_notes_script, compile_rust_package, execute_tx, @@ -159,24 +160,6 @@ fn p2id_note_builds_storage_without_a_component_codec() { ); } -/// Returns true when rustup reports the codec component target as installed. -fn wasm_target_is_installed() -> bool { - let output = match Command::new("rustup").args(["target", "list"]).output() { - Ok(output) if output.status.success() => output, - Ok(output) => { - eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); - return false; - } - Err(error) => { - eprintln!("could not run `rustup target list`: {error}"); - return false; - } - }; - String::from_utf8_lossy(&output.stdout) - .lines() - .any(|line| line.starts_with("wasm32-wasip2") && line.contains("(installed)")) -} - /// Asserts that a package carries one named custom section. fn assert_package_section(package: &Package, id: SectionId, name: &str) { assert!( diff --git a/tools/cargo-miden/Cargo.toml b/tools/cargo-miden/Cargo.toml index 332ec23c79..fcb3bc4e19 100644 --- a/tools/cargo-miden/Cargo.toml +++ b/tools/cargo-miden/Cargo.toml @@ -61,5 +61,6 @@ sha2.workspace = true [dev-dependencies] miden-mast-package = { workspace = true, features = ["std"] } midenc-frontend-wasm-metadata.workspace = true +midenc-integration-test-support.workspace = true wit-component.workspace = true wit-parser.workspace = true diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs index ca10aac998..f92fdae1e1 100644 --- a/tools/cargo-miden/tests/dex_note_codec_build.rs +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -7,6 +7,7 @@ use miden_mast_package::Package; use midenc_frontend_wasm_metadata::{ package_note_codec_section_id, package_note_storage_schema_section_id, }; +use midenc_integration_test_support::wasm_target_is_installed; use wit_component::DecodedWasm; use wit_parser::WorldItem; @@ -61,24 +62,6 @@ fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { assert_note_codec_component(codec.data.as_ref()); } -/// Returns true when rustup reports the codec component target as installed. -fn wasm_target_is_installed() -> bool { - let output = match std::process::Command::new("rustup").args(["target", "list"]).output() { - Ok(output) if output.status.success() => output, - Ok(output) => { - eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); - return false; - } - Err(error) => { - eprintln!("could not run `rustup target list`: {error}"); - return false; - } - }; - String::from_utf8_lossy(&output.stdout) - .lines() - .any(|line| line.starts_with("wasm32-wasip2") && line.contains("(installed)")) -} - /// Verifies the sandbox and versioned interface exported by a note codec component. fn assert_note_codec_component(component: &[u8]) { let DecodedWasm::Component(resolve, world_id) = From 0d33905a557111bcae32224998d25df2f12e5caa Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 18:41:18 +0300 Subject: [PATCH 19/43] fix: report nested codec failures, keep staged caches live, guard WIT extraction A failed nested codec build ended the whole process with Cargo's status, so the recovery guidance for locked and offline builds never reached the user and in-process callers lost their test binary. The codec path now uses a returning Cargo runner; the frontend build paths keep their exit semantics. Staged note-package entries never refreshed their age on reuse, so the seven-day collector of one build could remove an entry another build was actively using. A cache hit now touches the entry, and the collector also removes stray files. The core-types interface extraction balanced braces by raw character count; a brace inside a WIT comment corrupted every emitted schema. The scan now tracks line and block comments while copying the text verbatim. The nested-build environment scrub also removes the Cargo rustflags variants, the unreachable alias arm and the dead tuple-note encoding arm are gone, the wit-bindgen pins moved to workspace dependencies with the wit-parser skew rationale, host-side per-crate Cargo configs carry accurate comments, and the load-bearing spots named by review (the package-cache handshake, the nested target directory, the codec attach ordering) and the remaining items without documentation now carry it. --- Cargo.lock | 1 - Cargo.toml | 3 ++ midenc-compile/src/cargo.rs | 46 +++++++++++++++-- midenc-compile/src/pipeline/assembly.rs | 2 + midenc-compile/src/rust.rs | 57 ++++++++++++++++++--- sdk/base-macros/.cargo/config.toml | 2 +- sdk/base-macros/Cargo.toml | 4 +- sdk/base-macros/src/export_type.rs | 1 + sdk/base-macros/src/note.rs | 22 +++----- sdk/base-macros/src/note_schema.rs | 64 ++++++++++++++++++++++-- sdk/base-macros/src/types.rs | 9 ++-- sdk/field-repr/derive/.cargo/config.toml | 2 +- sdk/field-repr/derive/src/lib.rs | 6 +++ sdk/note-codec/macros/src/registry.rs | 5 ++ sdk/note-schema/Cargo.toml | 1 - sdk/note-schema/src/codec_component.rs | 2 + sdk/note-schema/src/schema.rs | 10 ++-- sdk/wasm-metadata/.cargo/config.toml | 2 +- 18 files changed, 196 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af0ea847a4..05e2996ab1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3686,7 +3686,6 @@ dependencies = [ "miden-mast-package", "miden-note-codec-wit", "miden-protocol", - "midenc-frontend-wasm", "midenc-frontend-wasm-metadata", "midenc-integration-test-support", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 3c0b19495d..ef75cd7644 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,6 +147,9 @@ serde_json = { version = "1.0", default-features = false, features = ["alloc"] } heck = "0.5" prettyplease = "0.2" proc-macro-crate = "3.5" +# Deliberate skew: WIT emission uses wit-parser 0.233; attach and consume use 0.247. +wit-bindgen-core = "0.57" +wit-bindgen-rust = { version = "0.57", default-features = false } wit-bindgen = { version = "0.57", default-features = false, features = ["macros"] } smallvec = { version = "1.15", default-features = false, features = [ "union", diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 1526860f86..f1fa491d0b 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -40,6 +40,7 @@ const NOTE_PACKAGE_CACHE_MAX_AGE: Duration = Duration::from_secs(7 * 24 * 60 * 6 /// One immutable staged package. struct StagedNotePackage { + /// Content-addressed directory exposed to codec macro expansion as its package cache. cache_dir: PathBuf, } @@ -440,6 +441,7 @@ fn build_note_codec_component( session.options.cargo_offline, )?; + // Give nested Cargo a separate target directory. Sharing the outer lock can deadlock. let cargo_target_dir = work_dir.join("cargo-target"); let mut cargo = Command::new(cargo_path); if let Some(toolchain) = toolchain.as_deref() { @@ -460,10 +462,13 @@ fn build_note_codec_component( .arg(&cargo_target_dir) .arg("--message-format") .arg("json-render-diagnostics") + // Let `from_project!` find the staged note package during codec macro expansion. .env(package_cache::PACKAGE_CACHE_ENV, &staged_package.cache_dir) // Outer Miden target settings and flags would poison this nested wasip2 codec build. + .env_remove("CARGO_BUILD_RUSTFLAGS") .env_remove("CARGO_BUILD_TARGET") .env_remove("CARGO_ENCODED_RUSTFLAGS") + .env_remove("CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS") .env_remove("RUSTFLAGS") .stdout(Stdio::piped()) .stderr(Stdio::inherit()); @@ -475,7 +480,7 @@ fn build_note_codec_component( manifest_path.display() )) })?; - let artifacts = crate::rust::spawn_cargo(cargo, cargo_path).map_err(|error| { + let artifacts = crate::rust::run_cargo(cargo, cargo_path).map_err(|error| { note_codec_cargo_error( error, &manifest_path, @@ -555,6 +560,9 @@ fn stage_note_package( package_path.display() ))); } + // Refresh the entry mtime on reuse, or the age-based GC of a concurrent build + // could remove an actively used entry that was staged long ago. + touch_directory(&cache_dir); } else { write_package_atomic(note_package, &cache_dir).map_err(|error| { Report::msg(format!( @@ -567,7 +575,14 @@ fn stage_note_package( Ok(StagedNotePackage { cache_dir }) } -/// Removes staged note packages that have not changed for more than seven days. +/// Sets a directory's modification time to now; failures are ignored. +fn touch_directory(dir: &Path) { + if let Ok(handle) = fs::File::open(dir) { + let _ = handle.set_modified(std::time::SystemTime::now()); + } +} + +/// Removes staged note packages that have not been used for more than seven days. fn gc_staged_note_packages(current_cache_dir: &Path) { let Some(cache_parent) = current_cache_dir.parent() else { return; @@ -590,8 +605,13 @@ fn gc_staged_note_packages(current_cache_dir: &Path) { let Ok(age) = modified.elapsed() else { continue; }; - if metadata.is_dir() && age > NOTE_PACKAGE_CACHE_MAX_AGE { - let _ = fs::remove_dir_all(path); + if age > NOTE_PACKAGE_CACHE_MAX_AGE { + if metadata.is_dir() { + let _ = fs::remove_dir_all(path); + } else { + // A stray file under the cache parent is not a staged package; collect it too. + let _ = fs::remove_file(path); + } } } } @@ -977,6 +997,24 @@ mod tests { assert!(error.contains("value: string")); } + #[test] + fn staged_package_reuse_refreshes_the_entry_age() { + let root = tempfile::TempDir::new().unwrap(); + let package = midenc_codegen_masm::intrinsics::load(); + let codec_crate = root.path().join("codec"); + fs::create_dir(&codec_crate).unwrap(); + + let staged = stage_note_package(root.path(), &codec_crate, &package).unwrap(); + let old = std::time::SystemTime::now() - (NOTE_PACKAGE_CACHE_MAX_AGE * 2); + fs::File::open(&staged.cache_dir).unwrap().set_modified(old).unwrap(); + + // A cache hit must refresh the mtime so the GC never collects an active entry. + let reused = stage_note_package(root.path(), &codec_crate, &package).unwrap(); + assert_eq!(reused.cache_dir, staged.cache_dir); + let age = fs::metadata(&staged.cache_dir).unwrap().modified().unwrap().elapsed().unwrap(); + assert!(age < NOTE_PACKAGE_CACHE_MAX_AGE, "the entry age was not refreshed: {age:?}"); + } + #[test] fn note_codec_staging_is_content_addressed() { let root = tempfile::TempDir::new().unwrap(); diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index 570d7cfc66..a42ec14485 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -63,6 +63,7 @@ pub(crate) fn prepare_assembler( Ok(()) } +/// Attaches frontend metadata, advice-map data, and target-specific package sections. pub(crate) fn post_process_package( package: &mut Package, component: &MasmComponent, @@ -104,6 +105,7 @@ pub(crate) fn post_process_package( } if has_note_codec && context.target.ty == TargetType::Note { + // Run after schema and kernel attachment. The codec stages this package state and hashes it. attach_note_codec(package, context, session)?; } diff --git a/midenc-compile/src/rust.rs b/midenc-compile/src/rust.rs index 48bcbb6df8..232a1fb495 100644 --- a/midenc-compile/src/rust.rs +++ b/midenc-compile/src/rust.rs @@ -14,6 +14,9 @@ use midenc_hir::Report; use crate::CompilerResult; /// Ensures that the requested Wasm target is installed for the selected Rust toolchain. +/// +/// When `offline` is true, this only detects the target and fails if it is absent; it never tries +/// to install the target. pub fn install_wasm32_target( wasi: &str, toolchain: Option<&str>, @@ -119,7 +122,38 @@ pub fn get_sysroot(toolchain: Option<&str>) -> CompilerResult { Ok(sysroot) } -pub fn spawn_cargo(mut cmd: Command, cargo: &Path) -> CompilerResult> { +/// Runs a Cargo command and exits the process with Cargo's status when the build fails. +/// +/// The frontend build paths use this so a failed user build ends the tool with Cargo's own +/// exit code. Callers that must report the failure themselves use [`run_cargo`]. +pub fn spawn_cargo(cmd: Command, cargo: &Path) -> CompilerResult> { + let (status, artifacts) = run_cargo_inner(cmd, cargo)?; + if !status.success() { + std::process::exit(status.code().unwrap_or(1)); + } + Ok(artifacts) +} + +/// Runs a Cargo command and returns an error when the build fails. +/// +/// Nested builds such as the note codec build use this so their error context and recovery +/// guidance reach the user instead of the process ending with Cargo's status. +pub fn run_cargo(cmd: Command, cargo: &Path) -> CompilerResult> { + let (status, artifacts) = run_cargo_inner(cmd, cargo)?; + if !status.success() { + return Err(Report::msg(format!( + "`{cargo}` failed with {status}", + cargo = cargo.display() + ))); + } + Ok(artifacts) +} + +/// Spawns a Cargo command and collects its Wasm artifacts together with the exit status. +fn run_cargo_inner( + mut cmd: Command, + cargo: &Path, +) -> CompilerResult<(std::process::ExitStatus, Vec)> { use std::io::BufRead; log::debug!(target: "driver", "spawning command {cmd:?}"); @@ -174,11 +208,7 @@ pub fn spawn_cargo(mut cmd: Command, cargo: &Path) -> CompilerResult Option { @@ -259,3 +289,18 @@ impl BuildOutput { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn run_cargo_reports_a_failed_status_instead_of_exiting() { + let mut cmd = Command::new("sh"); + cmd.args(["-c", "exit 3"]).stdout(std::process::Stdio::piped()); + + let error = run_cargo(cmd, Path::new("sh")).unwrap_err().to_string(); + + assert!(error.contains("failed with"), "unexpected error: {error}"); + } +} diff --git a/sdk/base-macros/.cargo/config.toml b/sdk/base-macros/.cargo/config.toml index f03dba586a..cfd2bbd96e 100644 --- a/sdk/base-macros/.cargo/config.toml +++ b/sdk/base-macros/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: keeps this host-side crate on the host target; sdk/ has no directory-wide config. [build] target = "wasm32-wasip1" diff --git a/sdk/base-macros/Cargo.toml b/sdk/base-macros/Cargo.toml index 408eb8361e..5a964540db 100644 --- a/sdk/base-macros/Cargo.toml +++ b/sdk/base-macros/Cargo.toml @@ -33,8 +33,8 @@ syn = { workspace = true, features = ["full"] } heck.workspace = true miden-formatting.workspace = true midenc-frontend-wasm-metadata.workspace = true -wit-bindgen-core = "0.57" -wit-bindgen-rust = { version = "0.57", default-features = false } +wit-bindgen-core.workspace = true +wit-bindgen-rust.workspace = true wit-component = { workspace = true, optional = true } [dev-dependencies] diff --git a/sdk/base-macros/src/export_type.rs b/sdk/base-macros/src/export_type.rs index 3c9e92ddb7..8f816b1ba2 100644 --- a/sdk/base-macros/src/export_type.rs +++ b/sdk/base-macros/src/export_type.rs @@ -24,6 +24,7 @@ fn export_type_identity_items( Ok(quote! { #guards #shape_const #assertions }) } +/// Expands `#[export_type]` and registers the annotated record or enum for schema emission. pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { if !attr.is_empty() { return syn::Error::new_spanned( diff --git a/sdk/base-macros/src/note.rs b/sdk/base-macros/src/note.rs index ab2271d8e4..d829d9eb52 100644 --- a/sdk/base-macros/src/note.rs +++ b/sdk/base-macros/src/note.rs @@ -124,6 +124,7 @@ fn expand_method_marker_attr( quote!(#item_fn) } +/// Expands a note input struct with felt encoding, decoding, and schema metadata. fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { let struct_ident = &item_struct.ident; let uniqueness_guard = note_storage_schema_uniqueness_guard(); @@ -135,6 +136,9 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { ) .into_compile_error(); } + if let syn::Fields::Unnamed(fields) = &item_struct.fields { + return syn::Error::new(fields.span(), NOTE_NAMED_FIELDS_ERROR).into_compile_error(); + } let to_felt_repr_impl = note_storage_encoding(&item_struct); let (from_impl, schema_static) = match &item_struct.fields { @@ -181,9 +185,7 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { }; (from_impl, schema_static) } - syn::Fields::Unnamed(fields) => { - return syn::Error::new(fields.span(), NOTE_NAMED_FIELDS_ERROR).into_compile_error(); - } + syn::Fields::Unnamed(_) => unreachable!("tuple note structs are rejected above"), }; quote! { @@ -217,17 +219,9 @@ fn note_storage_encoding(item_struct: &ItemStruct) -> TokenStream2 { } }) .collect(), - syn::Fields::Unnamed(fields) => fields - .unnamed - .iter() - .enumerate() - .map(|(index, _)| { - let index = syn::Index::from(index); - quote! { - ::miden::felt_repr::ToFeltRepr::write_felt_repr(&self.#index, writer); - } - }) - .collect(), + syn::Fields::Unnamed(_) => { + unreachable!("tuple note structs are rejected before storage encoding") + } }; let writer_ident = if field_writes.is_empty() { diff --git a/sdk/base-macros/src/note_schema.rs b/sdk/base-macros/src/note_schema.rs index dbfda57409..b5b003689f 100644 --- a/sdk/base-macros/src/note_schema.rs +++ b/sdk/base-macros/src/note_schema.rs @@ -23,10 +23,15 @@ use crate::{ wit_world::ManifestPackage, }; +/// Fully qualified SDK core-types interface imported by generated schemas. const CORE_TYPES_PACKAGE: &str = "miden:base/core-types@1.0.0"; +/// SDK core-types package name used for the embedded dependency package. const CORE_TYPES_PACKAGE_NAME: &str = "miden:base"; +/// SDK interface copied into generated schemas. const CORE_TYPES_INTERFACE: &str = "core-types"; +/// Source name reported for generated schema validation errors. const NOTE_STORAGE_SCHEMA_SOURCE_NAME: &str = "note-storage-schema.wit"; +/// Storage types accepted by the note schema diagnostic. const NOTE_STORAGE_SUPPORTED_TYPES: &str = "`u64`, `u32`, `u8`, `bool`, SDK core-type records, \ `#[export_type]` records or enums, and `Option` \ over a supported type"; @@ -614,10 +619,44 @@ fn extract_interface_body<'a>(source: &'a str, interface_name: &str) -> Option<& let header = format!("interface {interface_name} {{"); let body_start = source.find(&header)? + header.len(); let mut depth = 1usize; - for (offset, ch) in source[body_start..].char_indices() { - match ch { - '{' => depth += 1, - '}' => { + let body = &source[body_start..]; + let bytes = body.as_bytes(); + let mut offset = 0usize; + let mut in_line_comment = false; + let mut block_comment_depth = 0usize; + while offset < bytes.len() { + if in_line_comment { + if bytes[offset] == b'\n' { + in_line_comment = false; + } + offset += 1; + continue; + } + if block_comment_depth > 0 { + if bytes[offset..].starts_with(b"/*") { + block_comment_depth += 1; + offset += 2; + } else if bytes[offset..].starts_with(b"*/") { + block_comment_depth -= 1; + offset += 2; + } else { + offset += 1; + } + continue; + } + if bytes[offset..].starts_with(b"//") { + in_line_comment = true; + offset += 2; + continue; + } + if bytes[offset..].starts_with(b"/*") { + block_comment_depth = 1; + offset += 2; + continue; + } + match bytes[offset] { + b'{' => depth += 1, + b'}' => { depth -= 1; if depth == 0 { return Some(&source[body_start..body_start + offset]); @@ -625,6 +664,7 @@ fn extract_interface_body<'a>(source: &'a str, interface_name: &str) -> Option<& } _ => {} } + offset += 1; } None } @@ -667,6 +707,22 @@ mod tests { assert!(extract_interface_body(SDK_WIT_SOURCE, CORE_TYPES_INTERFACE).is_some()); } + #[test] + fn ignores_comment_braces_while_extracting_interface_body() { + let source = + "package test:a;\ninterface core-types {\n /// A closing } before { opening \ + brace.\n /* Another } before { pair. */\n record nested {\n value: \ + u32,\n }\n}\n"; + let source_without_braces = source.replace("} before {", "before"); + let body = extract_interface_body(source, CORE_TYPES_INTERFACE).unwrap(); + let body_without_braces = + extract_interface_body(&source_without_braces, CORE_TYPES_INTERFACE).unwrap(); + + assert_eq!(body.replace("} before {", "before"), body_without_braces); + assert!(body.contains("/// A closing } before { opening brace.")); + assert!(body.contains("/* Another } before { pair. */")); + } + #[test] fn uniqueness_guard_expansion_matches_golden() { let tokens = note_storage_schema_uniqueness_guard().to_string(); diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index 82c697a427..d3b7d1d37e 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -1,11 +1,10 @@ +//! Rust-to-WIT type mapping and per-crate export registration. + use std::{ collections::{HashMap, HashSet}, sync::{Mutex, OnceLock}, }; -static EXPORTED_TYPES: OnceLock>>> = - OnceLock::new(); - use heck::{ToKebabCase, ToUpperCamelCase}; use proc_macro2::{Span, TokenStream}; use quote::quote_spanned; @@ -14,6 +13,10 @@ use wit_bindgen_core::wit_parser::Type as WitType; use crate::manifest_paths::SDK_WIT_SOURCE; +/// Exported types grouped by the crate currently being expanded. +static EXPORTED_TYPES: OnceLock>>> = + OnceLock::new(); + #[derive(Clone, Debug)] pub(crate) struct TypeRef { pub(crate) wit_name: String, diff --git a/sdk/field-repr/derive/.cargo/config.toml b/sdk/field-repr/derive/.cargo/config.toml index f03dba586a..cfd2bbd96e 100644 --- a/sdk/field-repr/derive/.cargo/config.toml +++ b/sdk/field-repr/derive/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: keeps this host-side crate on the host target; sdk/ has no directory-wide config. [build] target = "wasm32-wasip1" diff --git a/sdk/field-repr/derive/src/lib.rs b/sdk/field-repr/derive/src/lib.rs index a2b5e69f37..5a0050a69e 100644 --- a/sdk/field-repr/derive/src/lib.rs +++ b/sdk/field-repr/derive/src/lib.rs @@ -249,6 +249,9 @@ fn ensure_no_explicit_discriminants( /// Enums are encoded as a `u32` tag (variant ordinal, starting from `0`) /// followed by the selected variant payload encoded in declaration order. /// +/// Use `#[felt_repr(crate_path = "path::to::felt_repr")]` on the type to override the generated +/// runtime crate path. +/// /// # Example /// /// ```ignore @@ -409,6 +412,9 @@ fn derive_from_felt_repr_impl( /// Enums are encoded as a `u32` tag (variant ordinal, starting from `0`) /// followed by the selected variant payload encoded in declaration order. /// +/// Use `#[felt_repr(crate_path = "path::to::felt_repr")]` on the type to override the generated +/// runtime crate path. +/// /// # Example /// /// ```ignore diff --git a/sdk/note-codec/macros/src/registry.rs b/sdk/note-codec/macros/src/registry.rs index 62b7b2de03..40083978e3 100644 --- a/sdk/note-codec/macros/src/registry.rs +++ b/sdk/note-codec/macros/src/registry.rs @@ -12,7 +12,9 @@ use proc_macro2::Span; /// One marked author codec. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct CodecRegistration { + /// Fully qualified WIT name implemented by the codec. pub(crate) fqn: String, + /// Rust type path that implements the codec. pub(crate) rust_type: String, } @@ -26,8 +28,11 @@ struct RegisteredCodec { /// Schema types and marked codecs registered by earlier macro expansions. #[derive(Default)] struct Registry { + /// Registered schema source and the expansion that supplied it. schema: Option<(String, ExpansionLocation)>, + /// Generated Rust upper-camel type name to WIT FQN, used by `#[note_codec]` lookup. types: BTreeMap, + /// Marked codecs keyed by the WIT FQN they implement. codecs: BTreeMap, } diff --git a/sdk/note-schema/Cargo.toml b/sdk/note-schema/Cargo.toml index 6edf8d6a12..8a4b9ff5a8 100644 --- a/sdk/note-schema/Cargo.toml +++ b/sdk/note-schema/Cargo.toml @@ -33,6 +33,5 @@ wasmtime = { workspace = true, optional = true } [dev-dependencies] miden-core.workspace = true miden-note-codec-wit.workspace = true -midenc-frontend-wasm.workspace = true midenc-integration-test-support.workspace = true tempfile.workspace = true diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index a7149b4b4b..35b39787f4 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -618,8 +618,10 @@ package miden:base@1.0.0 { ]) .env("CARGO_TARGET_DIR", &target_dir) // Outer Miden target settings and flags would poison this nested wasip2 codec build. + .env_remove("CARGO_BUILD_RUSTFLAGS") .env_remove("CARGO_BUILD_TARGET") .env_remove("CARGO_ENCODED_RUSTFLAGS") + .env_remove("CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS") .env_remove("RUSTFLAGS") .output() .expect("failed to start fixture build"); diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs index 61dd89e54c..095c489ece 100644 --- a/sdk/note-schema/src/schema.rs +++ b/sdk/note-schema/src/schema.rs @@ -690,7 +690,7 @@ impl<'a> ModelBuilder<'a> { }) } else { match definition.kind { - TypeDefKind::Type(ty) => self.build_named_alias(ty, name, fqn, docs, depth), + TypeDefKind::Type(ty) => self.build_named_alias(ty, name, fqn, docs), TypeDefKind::Record(record) => { let mut fields = Vec::with_capacity(record.fields.len()); let mut layout = FeltLayout::fixed(0); @@ -801,18 +801,18 @@ impl<'a> ModelBuilder<'a> { result } - /// Resolves a primitive alias with its metadata, or follows an ID alias while discarding that - /// alias's name, FQN, and documentation. + /// Resolves a primitive alias while preserving its name, FQN, and documentation. + /// + /// [`Self::build_type_id`] follows ID aliases before dispatching here. fn build_named_alias( &mut self, ty: Type, name: Option, fqn: Option, docs: Option, - depth: usize, ) -> Result { match ty { - Type::Id(id) => self.build_type_id(id, depth), + Type::Id(_) => unreachable!("build_type_id must follow ID aliases first"), Type::U64 => self.primitive(PrimitiveType::U64, name, fqn, docs), Type::U32 => self.primitive(PrimitiveType::U32, name, fqn, docs), Type::U8 => self.primitive(PrimitiveType::U8, name, fqn, docs), diff --git a/sdk/wasm-metadata/.cargo/config.toml b/sdk/wasm-metadata/.cargo/config.toml index f03dba586a..cfd2bbd96e 100644 --- a/sdk/wasm-metadata/.cargo/config.toml +++ b/sdk/wasm-metadata/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: keeps this host-side crate on the host target; sdk/ has no directory-wide config. [build] target = "wasm32-wasip1" From f092d59e13abfc4c2558d626c8ba9234fa4da49d Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 18:41:18 +0300 Subject: [PATCH 20/43] test: cover every example build with the shared lock and slim test deps Two more test sites build the same example projects in the same CI lane as the locked pair; all four now hold the shared advisory lock, renamed to match its wider scope. The note-codec crate returns to a local wasm-target probe instead of pulling the compiler graph through the integration-support crate for one helper. The dex codec test restores its environment through a drop guard, so a panic cannot leak a removed variable into other tests, and the duplicated example compilation helper is gone. --- .../tests/unit_note_trailing_data.rs | 3 +- sdk/note-bindings/Cargo.toml | 1 - sdk/note-bindings/tests/p2id_consumer.rs | 4 +-- sdk/note-codec/Cargo.toml | 1 - sdk/note-codec/tests/component_export.rs | 19 +++++++++- sdk/note-schema/tests/p2id_package.rs | 4 +-- .../src/mockchain/notes/schema.rs | 8 ++++- tests/integration/Cargo.toml | 2 +- .../examples/note_schema_metadata.rs | 27 +++----------- tests/support/src/lib.rs | 10 +++--- .../cargo-miden/tests/dex_note_codec_build.rs | 11 +++--- tools/cargo-miden/tests/target_dir.rs | 36 ++++--------------- tools/cargo-miden/tests/utils.rs | 25 +++++++++++++ 13 files changed, 78 insertions(+), 73 deletions(-) diff --git a/sdk/base-macros/tests/unit_note_trailing_data.rs b/sdk/base-macros/tests/unit_note_trailing_data.rs index 129e9f50e4..605cf72815 100644 --- a/sdk/base-macros/tests/unit_note_trailing_data.rs +++ b/sdk/base-macros/tests/unit_note_trailing_data.rs @@ -1,5 +1,6 @@ -// Keep this as a separate file because one crate may hold only one #[note] struct. //! Tests trailing-data rejection for unit note structs. +//! +//! This stays separate because one crate may hold only one `#[note]` struct. use core::convert::TryFrom; diff --git a/sdk/note-bindings/Cargo.toml b/sdk/note-bindings/Cargo.toml index c7d241b458..4392696adc 100644 --- a/sdk/note-bindings/Cargo.toml +++ b/sdk/note-bindings/Cargo.toml @@ -26,6 +26,5 @@ miden-protocol = { workspace = true, features = ["std"] } [dev-dependencies] miden-mast-package = { workspace = true, features = ["std"] } midenc-frontend-wasm-metadata.workspace = true -midenc-frontend-wasm.workspace = true midenc-integration-test-support.workspace = true tempfile.workspace = true diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs index 6dfb5e9725..9319b37fa0 100644 --- a/sdk/note-bindings/tests/p2id_consumer.rs +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -4,7 +4,7 @@ use std::{env, fs, path::Path, process::Command}; use miden_mast_package::Section; use midenc_frontend_wasm_metadata::package_note_storage_schema_section_id; -use midenc_integration_test_support::{compile_project, p2id_build_lock, workspace_root}; +use midenc_integration_test_support::{compile_project, example_build_lock, workspace_root}; /// Returns the native rustc host target. fn host_target() -> String { @@ -45,7 +45,7 @@ fn workspace_patch_section(workspace: &Path) -> String { #[test] fn generated_p2id_bindings_compile_and_run_in_a_consumer_crate() { let workspace = workspace_root(); - let _build_lock = p2id_build_lock(&workspace); + let _build_lock = example_build_lock(&workspace); let examples = workspace.join("examples"); let wallet_dir = examples.join("basic-wallet"); let wallet = compile_project(&wallet_dir); diff --git a/sdk/note-codec/Cargo.toml b/sdk/note-codec/Cargo.toml index ce91d3f7a5..533f207d6b 100644 --- a/sdk/note-codec/Cargo.toml +++ b/sdk/note-codec/Cargo.toml @@ -25,7 +25,6 @@ miden-protocol.workspace = true wit-bindgen = { workspace = true } [dev-dependencies] -midenc-integration-test-support.workspace = true tempfile.workspace = true wit-component.workspace = true wit-parser.workspace = true diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index cea142674e..f66cba8dab 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -6,7 +6,6 @@ use std::{ process::{Command, Output}, }; -use midenc_integration_test_support::wasm_target_is_installed; use tempfile::TempDir; use wit_component::DecodedWasm; use wit_parser::WorldItem; @@ -125,6 +124,24 @@ fn assert_command_succeeded(action: &str, output: &Output) { ); } +/// Returns true when rustup reports the codec component target as installed. +fn wasm_target_is_installed() -> bool { + let output = match Command::new("rustup").args(["target", "list"]).output() { + Ok(output) if output.status.success() => output, + Ok(output) => { + eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); + return false; + } + Err(error) => { + eprintln!("could not run `rustup target list`: {error}"); + return false; + } + }; + String::from_utf8_lossy(&output.stdout) + .lines() + .any(|line| line.starts_with(WASM_TARGET) && line.contains("(installed)")) +} + const FIXTURE_SOURCE: &str = r##" use codec_facade::AuthorTypeCodec; diff --git a/sdk/note-schema/tests/p2id_package.rs b/sdk/note-schema/tests/p2id_package.rs index 277196732c..2dc30a9342 100644 --- a/sdk/note-schema/tests/p2id_package.rs +++ b/sdk/note-schema/tests/p2id_package.rs @@ -2,12 +2,12 @@ use miden_note_schema::{NoteStorage, NoteStorageSchema}; use miden_protocol::{account::AccountId, address::NetworkId}; -use midenc_integration_test_support::{compile_project, p2id_build_lock, workspace_root}; +use midenc_integration_test_support::{compile_project, example_build_lock, workspace_root}; #[test] fn p2id_schema_builds_and_decodes_account_id_storage() { let workspace = workspace_root(); - let _build_lock = p2id_build_lock(&workspace); + let _build_lock = example_build_lock(&workspace); let examples = workspace.join("examples"); let wallet_dir = examples.join("basic-wallet"); let wallet = compile_project(&wallet_dir); diff --git a/tests/integration-network/src/mockchain/notes/schema.rs b/tests/integration-network/src/mockchain/notes/schema.rs index 4618e37071..e96d65cac0 100644 --- a/tests/integration-network/src/mockchain/notes/schema.rs +++ b/tests/integration-network/src/mockchain/notes/schema.rs @@ -21,7 +21,9 @@ use midenc_frontend_wasm_metadata::{ PACKAGE_NOTE_CODEC_SECTION_ID, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, package_note_codec_section_id, package_note_storage_schema_section_id, }; -use midenc_integration_test_support::wasm_target_is_installed; +use midenc_integration_test_support::{ + example_build_lock, wasm_target_is_installed, workspace_root, +}; use super::super::support::{ assert_account_has_fungible_asset, build_send_notes_script, compile_rust_package, execute_tx, @@ -106,6 +108,8 @@ fn dex_note_uses_embedded_schema_and_component_codec() { eprintln!("skipping DEX note schema test: wasm32-wasip2 is not installed"); return; } + let workspace = workspace_root(); + let _build_lock = example_build_lock(&workspace); let note_package = compile_rust_package("../../examples/dex-note", true); assert_package_section( ¬e_package, @@ -141,6 +145,8 @@ fn dex_note_uses_embedded_schema_and_component_codec() { #[test] fn p2id_note_builds_storage_without_a_component_codec() { + let workspace = workspace_root(); + let _build_lock = example_build_lock(&workspace); let note_package = compile_rust_package("../../examples/p2id-note", true); let schema = NoteStorageSchema::from_package(¬e_package).unwrap(); diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index b8be07d7ed..cc59df41a2 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -50,5 +50,5 @@ concat-idents = "1.1" libloading = "0.8" wasmi = "1.1.0" wat.workspace = true -wit-bindgen-core = "0.57" +wit-bindgen-core.workspace = true midenc-frontend-wasm-metadata.workspace = true diff --git a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs index 9980e346dd..82121db4d4 100644 --- a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs +++ b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs @@ -1,30 +1,11 @@ //! Integration tests for note storage schemas stored in Miden packages. -use std::sync::Arc; - use miden_mast_package::Package; use midenc_expect_test::{Expect, expect}; -use midenc_frontend_wasm::WasmTranslationConfig; use midenc_frontend_wasm_metadata::{package_note_storage_schema_section_id, trim_trailing_nuls}; +use midenc_integration_test_support::{compile_project, example_build_lock, workspace_root}; use wit_bindgen_core::wit_parser::{Resolve, Type as WitType, TypeDefKind}; -use crate::CompilerTest; - -/// Disables debug output so compiled package content is stable. -fn no_debug_flags() -> [String; 2] { - ["--debug".to_string(), "none".to_string()] -} - -/// Compiles one project with the Cargo Miden frontend. -fn compile_project(project_path: &str) -> Arc { - let mut test = CompilerTest::rust_source_cargo_miden( - project_path, - WasmTranslationConfig::default(), - no_debug_flags(), - ); - test.compile_package() -} - /// Returns the unpadded note storage schema text from a package. fn note_storage_schema(package: &Package) -> &str { let section_id = package_note_storage_schema_section_id(); @@ -60,7 +41,9 @@ fn assert_note_storage_schema(package: &Package, expected_root: &str, expected: #[test] fn note_packages_carry_resolvable_storage_schema_metadata() { - let p2id = compile_project("../../examples/p2id-note"); + let workspace = workspace_root(); + let _build_lock = example_build_lock(&workspace); + let p2id = compile_project(&workspace.join("examples/p2id-note")); assert_note_storage_schema( &p2id, "p2id-note", @@ -215,7 +198,7 @@ fn note_packages_carry_resolvable_storage_schema_metadata() { "#]], ); - let swapp = compile_project("../fixtures/components/swapp-note"); + let swapp = compile_project(&workspace.join("tests/fixtures/components/swapp-note")); assert_note_storage_schema( &swapp, "swapp-note", diff --git a/tests/support/src/lib.rs b/tests/support/src/lib.rs index b1733ea27a..f42cf7cbb2 100644 --- a/tests/support/src/lib.rs +++ b/tests/support/src/lib.rs @@ -46,8 +46,8 @@ pub fn workspace_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() } -/// Locks the shared p2id example outputs for the full build and consume span. -pub fn p2id_build_lock(workspace: &Path) -> File { +/// Locks shared example outputs for the full build and consume span. +pub fn example_build_lock(workspace: &Path) -> File { let target_dir = workspace.join("target"); fs::create_dir_all(&target_dir).expect("failed to create the workspace target directory"); let lock = File::options() @@ -55,9 +55,9 @@ pub fn p2id_build_lock(workspace: &Path) -> File { .write(true) .create(true) .truncate(false) - .open(target_dir.join("p2id-end-to-end-build.lock")) - .expect("failed to open the p2id end-to-end build lock"); - lock.lock().expect("failed to lock the p2id end-to-end build"); + .open(target_dir.join("example-build.lock")) + .expect("failed to open the example build lock"); + lock.lock().expect("failed to lock example builds"); lock } diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs index f92fdae1e1..89c1b3b002 100644 --- a/tools/cargo-miden/tests/dex_note_codec_build.rs +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -11,7 +11,7 @@ use midenc_integration_test_support::wasm_target_is_installed; use wit_component::DecodedWasm; use wit_parser::WorldItem; -use crate::utils::{current_dir_lock, workspace_root}; +use crate::utils::{RestoreEnvironment, current_dir_lock, workspace_root}; #[test] fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { @@ -19,13 +19,15 @@ fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { eprintln!("skipping DEX note codec build test: wasm32-wasip2 is not installed"); return; } + // The command reads the process working directory, so serialize cwd changes. let _cwd_lock = current_dir_lock(); let _ = midenc_log::Builder::from_env("MIDENC_TRACE") .is_test(true) .format_timestamp(None) .try_init(); - let restore_target_dir = env::var_os("CARGO_TARGET_DIR"); + // Clear the outer override so the nested example build uses its own target layout. + let _restore_environment = RestoreEnvironment::new(["CARGO_TARGET_DIR"]); unsafe { env::remove_var("CARGO_TARGET_DIR"); } @@ -34,11 +36,6 @@ fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { env::set_current_dir(¬e_dir).unwrap(); let result = run(["cargo", "miden", "build", "--release"].into_iter().map(str::to_owned)); - match restore_target_dir { - Some(value) => unsafe { env::set_var("CARGO_TARGET_DIR", value) }, - None => unsafe { env::remove_var("CARGO_TARGET_DIR") }, - } - let output = result .expect("cargo miden build for dex-note failed") .expect("expected BuildCommandOutput") diff --git a/tools/cargo-miden/tests/target_dir.rs b/tools/cargo-miden/tests/target_dir.rs index b2e34ac847..25e88c68f3 100644 --- a/tools/cargo-miden/tests/target_dir.rs +++ b/tools/cargo-miden/tests/target_dir.rs @@ -2,7 +2,7 @@ use std::{env, fs}; use cargo_miden::run; -use crate::utils::{current_dir_lock, project_template_arg}; +use crate::utils::{RestoreEnvironment, current_dir_lock, project_template_arg}; /// A custom Midenc target is an umbrella for every artifact the compiler-owned build writes. /// @@ -11,35 +11,13 @@ use crate::utils::{current_dir_lock, project_template_arg}; /// manifest frontend redirects its nested Cargo invocation beneath the custom target. #[test] fn a_custom_midenc_target_contains_nested_cargo_artifacts() { - struct RestoreEnvironment { - cargo_target_dir: Option, - cargo_build_target_dir: Option, - midenc_target_dir: Option, - test: Option, - } - impl Drop for RestoreEnvironment { - fn drop(&mut self) { - for (name, value) in [ - ("CARGO_TARGET_DIR", self.cargo_target_dir.take()), - ("CARGO_BUILD_TARGET_DIR", self.cargo_build_target_dir.take()), - ("MIDENC_TARGET_DIR", self.midenc_target_dir.take()), - ("TEST", self.test.take()), - ] { - match value { - Some(value) => unsafe { env::set_var(name, value) }, - None => unsafe { env::remove_var(name) }, - } - } - } - } - let _cwd = current_dir_lock(); - let _restore_environment = RestoreEnvironment { - cargo_target_dir: env::var_os("CARGO_TARGET_DIR"), - cargo_build_target_dir: env::var_os("CARGO_BUILD_TARGET_DIR"), - midenc_target_dir: env::var_os("MIDENC_TARGET_DIR"), - test: env::var_os("TEST"), - }; + let _restore_environment = RestoreEnvironment::new([ + "CARGO_TARGET_DIR", + "CARGO_BUILD_TARGET_DIR", + "MIDENC_TARGET_DIR", + "TEST", + ]); unsafe { env::remove_var("CARGO_TARGET_DIR"); env::remove_var("CARGO_BUILD_TARGET_DIR"); diff --git a/tools/cargo-miden/tests/utils.rs b/tools/cargo-miden/tests/utils.rs index 1c194b017f..0600d215c6 100644 --- a/tools/cargo-miden/tests/utils.rs +++ b/tools/cargo-miden/tests/utils.rs @@ -63,6 +63,31 @@ pub(crate) fn current_dir_lock() -> CurrentDirGuard { } } +/// Restores selected process environment variables when dropped. +pub(crate) struct RestoreEnvironment { + values: Vec<(&'static str, Option)>, +} + +impl RestoreEnvironment { + /// Captures the current values of the selected environment variables. + pub(crate) fn new(names: [&'static str; N]) -> Self { + Self { + values: names.into_iter().map(|name| (name, env::var_os(name))).collect(), + } + } +} + +impl Drop for RestoreEnvironment { + fn drop(&mut self) { + for (name, value) in self.values.drain(..) { + match value { + Some(value) => unsafe { env::set_var(name, value) }, + None => unsafe { env::remove_var(name) }, + } + } + } +} + /// The directory the post-build package tests hand to the compiler as its package cache. /// /// A lease the compiler mints itself is deleted when the compiler finishes, so a test that From f5a79254c972040194c3e38aa685e3207c6aa4c7 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 18:41:18 +0300 Subject: [PATCH 21/43] docs: add migration notes for the export type identity changes Conflicting #[export_type] registrations are now a compile error and the macro reserves the shape-constant name on annotated types; the migration guide explains both required source changes and the changelog points at it. --- CHANGELOG.md | 6 ++++++ sdk/sdk/MIGRATION.md | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c56704b4..46ad00aac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Migration and breaking changes + +- BREAKING: `#[export_type]` now rejects conflicting registrations for the same WIT type and + reserves the inherent associated constant name `__MIDEN_EXPORT_TYPE_SHAPE`. See the + [migration guide](./sdk/sdk/MIGRATION.md) for both required source changes. + ## [0.10.0] ### Compiler and `midenc` diff --git a/sdk/sdk/MIGRATION.md b/sdk/sdk/MIGRATION.md index 81a18d6992..130a197ada 100644 --- a/sdk/sdk/MIGRATION.md +++ b/sdk/sdk/MIGRATION.md @@ -90,6 +90,19 @@ constructors can commit to the note script's MAST root. Rename any inherent item including methods, `#[note_constructor]` methods, associated constants, and items declared in a separate impl block. +### Keep duplicate `#[export_type]` registrations shape-compatible + +Two `#[export_type]` registrations that map to the same WIT type are now a compile error when +their record fields or enum cases differ. Previously, the last registration silently replaced the +first. Rename one Rust type so it maps to a different WIT name, or make both registered shapes +identical. + +### `#[export_type]` reserves `__MIDEN_EXPORT_TYPE_SHAPE` + +The `#[export_type]` macro now emits an inherent public associated constant named +`__MIDEN_EXPORT_TYPE_SHAPE` on the annotated type. Rename any user-defined associated item with +that exact name. + ### Contract crates gain a `build.rs` for IDE and plain-cargo builds New projects created by `cargo miden new` include a small `build.rs` in each contract crate and a From 35ca218df563517953188545f46294b3e3062eb6 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 19:58:42 +0300 Subject: [PATCH 22/43] fix: guard builtin type names and repair compile pipeline contracts Builtin names were trusted by their last path segment, so a foreign or shadowing type named Option, Result, or a primitive could change the encoded layout while the schema still declared the builtin. Every builtin reference now carries the same nominal identity check that pins SDK core types, proven against the ::core definitions. midenc-compile declares no_std with std-only dependencies behind the std feature, but the note-schema and sha2 dependencies were added unconditionally; both are optional now and the crate builds again without default features. Offline target detection probes the sysroot first and only gates the rustup install step, so linked toolchains work offline. Nested codec build failures always name the codec manifest, and the lockfile and network advice is phrased for the failures it can actually explain. Staged-cache freshness works through file mtimes, so the age refresh is portable, and the base-macros registry tests serialize on a shared lock. The macro registry docs no longer promise a declaration order the FQN-keyed map does not keep. --- Cargo.toml | 4 +- midenc-compile/Cargo.toml | 7 +- midenc-compile/src/cargo.rs | 135 ++++++++++++++++++++------ midenc-compile/src/compiler.rs | 14 ++- midenc-compile/src/lib.rs | 22 +++-- midenc-compile/src/rust.rs | 37 ++----- sdk/base-macros/src/export_type.rs | 6 +- sdk/base-macros/src/note.rs | 3 +- sdk/base-macros/src/note_schema.rs | 19 +++- sdk/base-macros/src/types.rs | 95 +++++++++++++++++- sdk/base-macros/src/types/tests.rs | 117 +++++++++++++++++++++- sdk/note-codec/macros/src/lib.rs | 3 +- sdk/note-codec/macros/src/registry.rs | 2 +- sdk/note-codec/macros/src/tests.rs | 3 +- sdk/note-codec/src/lib.rs | 7 +- 15 files changed, 386 insertions(+), 88 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef75cd7644..ee0e568878 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,7 +147,9 @@ serde_json = { version = "1.0", default-features = false, features = ["alloc"] } heck = "0.5" prettyplease = "0.2" proc-macro-crate = "3.5" -# Deliberate skew: WIT emission uses wit-parser 0.233; attach and consume use 0.247. +# Deliberate skew: WIT emission (wit-bindgen-core 0.57) and attach/consume validation +# (workspace wit-parser/wit-component) parse with 0.247; 0.233 enters only through wasmtime's +# component bindgen macros on the codec-consumption path. wit-bindgen-core = "0.57" wit-bindgen-rust = { version = "0.57", default-features = false } wit-bindgen = { version = "0.57", default-features = false, features = ["macros"] } diff --git a/midenc-compile/Cargo.toml b/midenc-compile/Cargo.toml index 6a9caa78ac..10632b373b 100644 --- a/midenc-compile/Cargo.toml +++ b/midenc-compile/Cargo.toml @@ -25,6 +25,9 @@ std = [ "midenc-session/std", "dep:cargo_metadata", "dep:clap", + "dep:miden-note-schema", + "dep:sha2", + "dep:tempfile", "dep:toml_edit", "dep:wat", "dep:wit-component", @@ -40,7 +43,7 @@ miden-assembly.workspace = true miden-assembly-syntax.workspace = true miden-mast-package.workspace = true miden-note-codec-wit.workspace = true -miden-note-schema.workspace = true +miden-note-schema = { workspace = true, optional = true } miden-package-registry.workspace = true midenc-frontend-wasm.workspace = true midenc-frontend-wasm-metadata.workspace = true @@ -50,7 +53,7 @@ midenc-dialect-hir.workspace = true midenc-hir.workspace = true midenc-hir-transform.workspace = true midenc-session.workspace = true -sha2.workspace = true +sha2 = { workspace = true, optional = true } tempfile = { workspace = true, optional = true } toml_edit = { workspace = true, optional = true, features = ["parse", "display"] } thiserror.workspace = true diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index f1fa491d0b..b985e690c9 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -560,9 +560,9 @@ fn stage_note_package( package_path.display() ))); } - // Refresh the entry mtime on reuse, or the age-based GC of a concurrent build + // Refresh the package mtime on reuse, or the age-based GC of a concurrent build // could remove an actively used entry that was staged long ago. - touch_directory(&cache_dir); + touch_file(&package_path); } else { write_package_atomic(note_package, &cache_dir).map_err(|error| { Report::msg(format!( @@ -575,13 +575,39 @@ fn stage_note_package( Ok(StagedNotePackage { cache_dir }) } -/// Sets a directory's modification time to now; failures are ignored. -fn touch_directory(dir: &Path) { - if let Ok(handle) = fs::File::open(dir) { +/// Sets a file's modification time to now; failures are ignored. +fn touch_file(file: &Path) { + if let Ok(handle) = fs::File::options().append(true).open(file) { let _ = handle.set_modified(std::time::SystemTime::now()); } } +/// Returns the newest modification time of a cache entry and its direct child files. +fn newest_cache_entry_mtime(path: &Path, metadata: &fs::Metadata) -> Option { + let mut newest = metadata.modified().ok(); + if !metadata.is_dir() { + return newest; + } + let Ok(entries) = fs::read_dir(path) else { + return newest; + }; + for entry in entries.flatten() { + let Ok(metadata) = entry.metadata() else { + continue; + }; + if !metadata.is_file() { + continue; + } + let Ok(modified) = metadata.modified() else { + continue; + }; + if newest.is_none_or(|current| modified > current) { + newest = Some(modified); + } + } + newest +} + /// Removes staged note packages that have not been used for more than seven days. fn gc_staged_note_packages(current_cache_dir: &Path) { let Some(cache_parent) = current_cache_dir.parent() else { @@ -599,7 +625,7 @@ fn gc_staged_note_packages(current_cache_dir: &Path) { let Ok(metadata) = entry.metadata() else { continue; }; - let Ok(modified) = metadata.modified() else { + let Some(modified) = newest_cache_entry_mtime(&path, &metadata) else { continue; }; let Ok(age) = modified.elapsed() else { @@ -626,29 +652,27 @@ pub(crate) fn apply_cargo_policy( .flatten() } -/// Adds lockfile and network recovery guidance to a nested Cargo failure. +/// Attributes a nested Cargo failure and adds applicable lockfile and network guidance. fn note_codec_cargo_error( error: Report, manifest_path: &Path, locked: bool, offline: bool, ) -> Report { - if !locked && !offline { - return error; - } - - let mut guidance = format!( - "note codec build failed under the outer Cargo policy for '{}': {error}", - manifest_path.display() - ); - if locked { - guidance.push_str( - "; update and commit the codec workspace Cargo.lock before retrying with --locked", - ); - } - if offline { - guidance - .push_str("; fetch the codec dependencies while online before retrying with --offline"); + let mut guidance = + format!("the nested note codec build for '{}' failed: {error}", manifest_path.display()); + if locked || offline { + guidance.push_str(". If this failure is about the lockfile or the network:"); + if locked { + guidance.push_str( + " update and commit the codec workspace Cargo.lock before retrying with --locked.", + ); + } + if offline { + guidance.push_str( + " fetch the codec dependencies while online before retrying with --offline.", + ); + } } Report::msg(guidance) } @@ -1005,16 +1029,73 @@ mod tests { fs::create_dir(&codec_crate).unwrap(); let staged = stage_note_package(root.path(), &codec_crate, &package).unwrap(); + let package_path = staged.cache_dir.join(package_cache::package_file_name(&package.name)); let old = std::time::SystemTime::now() - (NOTE_PACKAGE_CACHE_MAX_AGE * 2); - fs::File::open(&staged.cache_dir).unwrap().set_modified(old).unwrap(); - - // A cache hit must refresh the mtime so the GC never collects an active entry. + fs::File::options() + .write(true) + .open(&package_path) + .unwrap() + .set_modified(old) + .unwrap(); + + // A cache hit must refresh the package mtime used by the GC freshness rule. let reused = stage_note_package(root.path(), &codec_crate, &package).unwrap(); assert_eq!(reused.cache_dir, staged.cache_dir); - let age = fs::metadata(&staged.cache_dir).unwrap().modified().unwrap().elapsed().unwrap(); + let package_age = + fs::metadata(&package_path).unwrap().modified().unwrap().elapsed().unwrap(); + assert!( + package_age < NOTE_PACKAGE_CACHE_MAX_AGE, + "the package age was not refreshed: {package_age:?}" + ); + let metadata = fs::metadata(&staged.cache_dir).unwrap(); + let age = newest_cache_entry_mtime(&staged.cache_dir, &metadata) + .unwrap() + .elapsed() + .unwrap(); assert!(age < NOTE_PACKAGE_CACHE_MAX_AGE, "the entry age was not refreshed: {age:?}"); } + #[test] + fn note_codec_cargo_errors_are_always_attributed() { + let error = note_codec_cargo_error( + Report::msg("rustc failed"), + Path::new("/codec/Cargo.toml"), + false, + false, + ) + .to_string(); + + assert_eq!( + error, + "the nested note codec build for '/codec/Cargo.toml' failed: rustc failed" + ); + } + + #[test] + fn note_codec_cargo_error_guidance_is_conditional() { + let locked = note_codec_cargo_error( + Report::msg("resolution failed"), + Path::new("/codec/Cargo.toml"), + true, + false, + ) + .to_string(); + assert!(locked.contains("If this failure is about the lockfile or the network:")); + assert!(locked.contains("retrying with --locked")); + assert!(!locked.contains("retrying with --offline")); + + let offline = note_codec_cargo_error( + Report::msg("resolution failed"), + Path::new("/codec/Cargo.toml"), + false, + true, + ) + .to_string(); + assert!(offline.contains("If this failure is about the lockfile or the network:")); + assert!(!offline.contains("retrying with --locked")); + assert!(offline.contains("retrying with --offline")); + } + #[test] fn note_codec_staging_is_content_addressed() { let root = tempfile::TempDir::new().unwrap(); diff --git a/midenc-compile/src/compiler.rs b/midenc-compile/src/compiler.rs index f009b851fe..7fe4d752b7 100644 --- a/midenc-compile/src/compiler.rs +++ b/midenc-compile/src/compiler.rs @@ -1,6 +1,8 @@ #[cfg(feature = "std")] +use alloc::sync::Arc; +#[cfg(feature = "std")] use alloc::{borrow::ToOwned, format, string::ToString, vec}; -use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}; +use alloc::{boxed::Box, string::String, vec::Vec}; #[cfg(feature = "std")] use std::ffi::OsString; @@ -8,10 +10,11 @@ use std::ffi::OsString; use clap::{Parser, builder::ArgPredicate}; use miden_mast_package::TargetType; use midenc_session::{ - ColorChoice, DebugInfo, InputFile, IrFilter, LinkLibrary, OptLevel, Options, OutputFile, - OutputTypeSpec, OutputTypes, PathBuf, RemapPathPrefix, Session, Verbosity, Warnings, - add_target_link_libraries, diagnostics::Emitter, + ColorChoice, DebugInfo, IrFilter, LinkLibrary, OptLevel, Options, OutputFile, OutputTypeSpec, + OutputTypes, PathBuf, RemapPathPrefix, Verbosity, Warnings, add_target_link_libraries, }; +#[cfg(feature = "std")] +use midenc_session::{InputFile, Session, diagnostics::Emitter}; /// Compile a program from WebAssembly or Miden IR, to Miden Assembly. #[derive(Debug, Clone)] @@ -816,6 +819,7 @@ impl Compiler { } /// Use this configuration to obtain a [Session] used for compilation + #[cfg(feature = "std")] fn into_session( options: Box, input: Option, @@ -857,9 +861,11 @@ fn format_error(err: clap::Error) -> clap::Error { err.format(&mut cmd) } +#[cfg(feature = "std")] #[derive(Clone)] struct TargetTypeValueParser; +#[cfg(feature = "std")] impl clap::builder::TypedValueParser for TargetTypeValueParser { type Value = TargetType; diff --git a/midenc-compile/src/lib.rs b/midenc-compile/src/lib.rs index ce851985a2..f3cb332faa 100644 --- a/midenc-compile/src/lib.rs +++ b/midenc-compile/src/lib.rs @@ -14,21 +14,21 @@ pub mod pipeline; #[cfg(feature = "std")] pub mod rust; +#[cfg(feature = "std")] use alloc::rc::Rc; pub use midenc_hir::Context; +#[cfg(feature = "std")] use midenc_hir::Op; #[cfg(feature = "std")] use midenc_session::{OutputFile, OutputType}; -use midenc_session::{ - OutputMode, - diagnostics::{Diagnostic, Report, WrapErr, miette}, -}; +use midenc_session::diagnostics::{Diagnostic, Report, miette}; +#[cfg(feature = "std")] +use midenc_session::{OutputMode, diagnostics::WrapErr}; -pub use self::{ - compiler::Compiler, - pipeline::artifacts::{CodegenOutput, CompiledArtifact, MidenComponent}, -}; +pub use self::compiler::Compiler; +#[cfg(feature = "std")] +pub use self::pipeline::artifacts::{CodegenOutput, CompiledArtifact, MidenComponent}; pub type CompilerResult = Result; @@ -39,6 +39,7 @@ pub type CompilerResult = Result; pub struct CompilerStopped(&'static str); /// Run the compiler using the provided [midenc_session::Session] +#[cfg(feature = "std")] pub fn compile(context: Rc) -> CompilerResult<()> { use midenc_hir::formatter::DisplayHex; @@ -87,6 +88,7 @@ pub fn compile(context: Rc) -> CompilerResult<()> { } /// Same as `compile`, but return compiled artifacts to the caller +#[cfg(feature = "std")] pub fn compile_to_memory(context: Rc) -> CompilerResult { let session = context.session_rc(); let input = session.input.clone().ok_or_else(|| Report::msg("no inputs"))?; @@ -121,6 +123,7 @@ pub fn compile_to_memory(context: Rc) -> CompilerResult( link_output: MidenComponent, pre_assembly_stage: F, @@ -167,6 +170,7 @@ where /// emitted is nonetheless decided by the session: `Pipeline::compile` attaches an observer that /// renders the selected target's artifacts through the route's own declarations, and /// `Session::emit` writes only the output types the session asked for. +#[cfg(feature = "std")] fn run_pipeline( session: Rc, input: midenc_session::InputFile, @@ -197,6 +201,7 @@ fn run_pipeline( /// /// `CompiledArtifact::Lowered` is therefore unreachable from here. It survives because it is /// part of a public enum with an external consumer, and narrowing that is a separate change. +#[cfg(feature = "std")] fn artifact_from_outcome( outcome: pipeline::Outcome, stop: Option, @@ -219,6 +224,7 @@ fn artifact_from_outcome( Ok(CompiledArtifact::Assembled(package)) } +#[cfg(feature = "std")] pub(crate) fn emit_hir_if_requested( op: &midenc_hir::Operation, context: Rc, diff --git a/midenc-compile/src/rust.rs b/midenc-compile/src/rust.rs index 232a1fb495..ffc2d08e36 100644 --- a/midenc-compile/src/rust.rs +++ b/midenc-compile/src/rust.rs @@ -15,8 +15,9 @@ use crate::CompilerResult; /// Ensures that the requested Wasm target is installed for the selected Rust toolchain. /// -/// When `offline` is true, this only detects the target and fails if it is absent; it never tries -/// to install the target. +/// The target is detected directly in the selected toolchain's sysroot. When `offline` is true, +/// this fails with installation guidance if the target is absent; it never invokes rustup to +/// install the target. pub fn install_wasm32_target( wasi: &str, toolchain: Option<&str>, @@ -33,39 +34,18 @@ pub fn install_wasm32_target( log::info!(target: "driver", "verifying wasm32-{wasi} target is installed for the {toolchain} toolchain.."); let target = format!("wasm32-{wasi}"); + let sysroot = get_sysroot(Some(&toolchain))?; + if sysroot.join(format!("lib/rustlib/wasm32-{wasi}")).exists() { + log::info!(target: "driver", "wasm32-{wasi} is available"); + return Ok(()); + } if offline { - let output = Command::new("rustup") - .arg("target") - .arg("list") - .arg("--installed") - .args(["--toolchain", toolchain.as_str()]) - .output() - .map_err(|err| Report::msg(format!("failed to execute rustup: {err}")))?; - if !output.status.success() { - return Err(Report::msg(format!( - "failed to list installed targets for the `{toolchain}` toolchain" - ))); - } - if output - .stdout - .split(|byte| byte.is_ascii_whitespace()) - .any(|installed| installed == target.as_bytes()) - { - log::info!(target: "driver", "{target} is available"); - return Ok(()); - } return Err(Report::msg(format!( "the `{target}` target is not installed for the `{toolchain}` toolchain; install it \ with `rustup target add --toolchain {toolchain} {target}` or drop `--offline`" ))); } - let sysroot = get_sysroot(Some(&toolchain))?; - if sysroot.join(format!("lib/rustlib/wasm32-{wasi}")).exists() { - log::info!(target: "driver", "wasm32-{wasi} is available"); - return Ok(()); - } - log::info!(target: "driver", "installing wasm32-{wasi} target"); let output = Command::new("rustup") @@ -295,6 +275,7 @@ mod tests { use super::*; #[test] + #[cfg(unix)] fn run_cargo_reports_a_failed_status_instead_of_exiting() { let mut cmd = Command::new("sh"); cmd.args(["-c", "exit 3"]).stdout(std::process::Stdio::piped()); diff --git a/sdk/base-macros/src/export_type.rs b/sdk/base-macros/src/export_type.rs index 8f816b1ba2..be7b8c40e7 100644 --- a/sdk/base-macros/src/export_type.rs +++ b/sdk/base-macros/src/export_type.rs @@ -5,8 +5,8 @@ use syn::{Item, parse_macro_input}; use crate::types::{ ExportedTypeDef, custom_type_shape_assertions, export_type_shape_const, - exported_type_from_enum, exported_type_from_struct, register_export_type, - registered_export_type_map, sdk_core_type_identity_guards, + exported_type_from_enum, exported_type_from_struct, nominal_type_identity_guards, + register_export_type, registered_export_type_map, }; /// Builds the guard and identity items emitted next to one exported type. @@ -15,7 +15,7 @@ fn export_type_identity_items( generics: &syn::Generics, span: proc_macro2::Span, ) -> Result { - let guards = sdk_core_type_identity_guards(def, span)?; + let guards = nominal_type_identity_guards(def, span)?; register_export_type(def.clone(), span)?; // The registry lookup runs after registration so a self-referential type sees itself. let registry = registered_export_type_map(); diff --git a/sdk/base-macros/src/note.rs b/sdk/base-macros/src/note.rs index d829d9eb52..aa8c8373d7 100644 --- a/sdk/base-macros/src/note.rs +++ b/sdk/base-macros/src/note.rs @@ -1180,10 +1180,11 @@ mod tests { use syn::parse_quote; use super::*; - use crate::types::reset_export_type_registry_for_tests; + use crate::types::{lock_export_type_registry_for_tests, reset_export_type_registry_for_tests}; #[test] fn named_note_struct_emits_storage_schema_static() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let item_struct: ItemStruct = parse_quote! { struct PaymentNote { diff --git a/sdk/base-macros/src/note_schema.rs b/sdk/base-macros/src/note_schema.rs index b5b003689f..d876a948bd 100644 --- a/sdk/base-macros/src/note_schema.rs +++ b/sdk/base-macros/src/note_schema.rs @@ -16,7 +16,7 @@ use crate::{ manifest_paths::SDK_WIT_SOURCE, types::{ ExportedField, ExportedTypeDef, ExportedTypeKind, TypeRef, custom_type_shape_assertions, - doc_comments, map_type_to_type_ref, registered_export_types, sdk_core_type_identity_guards, + doc_comments, map_type_to_type_ref, nominal_type_identity_guards, registered_export_types, }, util::NOTE_NAMED_FIELDS_ERROR, wit_builder::{WitBody, WitBuilder}, @@ -66,7 +66,7 @@ pub(crate) fn expand_note_storage_schema( .definitions .last() .expect("a rendered schema always contains its root definition"); - let identity_guards = sdk_core_type_identity_guards(root_definition, rendered.span)?; + let identity_guards = nominal_type_identity_guards(root_definition, rendered.span)?; let registry_by_rust_name = registry .iter() .cloned() @@ -677,8 +677,8 @@ mod tests { use super::*; use crate::types::{ - ExportedVariant, exported_type_from_struct, map_type_to_type_ref, - reset_export_type_registry_for_tests, + ExportedVariant, exported_type_from_struct, lock_export_type_registry_for_tests, + map_type_to_type_ref, reset_export_type_registry_for_tests, }; /// Checks that the schema package contains the expected root alias. @@ -737,6 +737,7 @@ mod tests { #[test] fn renders_p2id_shaped_schema() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let note: ItemStruct = parse_quote! { struct P2idNote { @@ -900,6 +901,7 @@ mod tests { #[test] fn renders_nested_record_and_enum_schema() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let destination: syn::ItemStruct = parse_quote! { /// Destination details. @@ -1128,6 +1130,7 @@ mod tests { #[test] fn renders_multiline_doc_attributes_as_separate_wit_comments() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let note: ItemStruct = parse_quote! { #[doc = " First note line.\n Second note line."] @@ -1165,6 +1168,7 @@ mod tests { #[test] fn rejects_other_types_outside_the_storage_allow_list() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let note: ItemStruct = parse_quote! { struct FloatNote { @@ -1192,6 +1196,7 @@ mod tests { #[test] fn rejects_unsupported_fields_in_nested_records() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let nested: syn::ItemStruct = parse_quote! { struct Nested { @@ -1219,6 +1224,7 @@ mod tests { #[test] fn expansion_surfaces_wit_parser_errors_with_type_context() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let note: ItemStruct = parse_quote! { struct Type { @@ -1235,6 +1241,7 @@ mod tests { #[test] fn expansion_surfaces_wit_parser_errors_with_field_context() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let note: ItemStruct = parse_quote! { struct InvalidFieldNote { @@ -1251,6 +1258,7 @@ mod tests { #[test] fn rejects_tuple_note_structs() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let note: ItemStruct = parse_quote!( struct TupleNote(Felt); @@ -1263,6 +1271,7 @@ mod tests { #[test] fn rejects_vec_fields() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let note: ItemStruct = parse_quote! { struct VecNote { @@ -1277,6 +1286,7 @@ mod tests { #[test] fn rejects_custom_types_registered_after_the_note() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let note: ItemStruct = parse_quote! { struct CustomNote { @@ -1293,6 +1303,7 @@ mod tests { /// Checks one Rust field type against the note-specific allow-list diagnostic. fn assert_unsupported_note_field_type(rust_type: &str, expected_wit_type: &str) { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let note: ItemStruct = syn::parse_str(&format!("struct UnsupportedNote {{ value: {rust_type}, }}")) diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index d3b7d1d37e..b83385642f 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -17,6 +17,9 @@ use crate::manifest_paths::SDK_WIT_SOURCE; static EXPORTED_TYPES: OnceLock>>> = OnceLock::new(); +#[cfg(test)] +static EXPORTED_TYPES_TEST_LOCK: Mutex<()> = Mutex::new(()); + #[derive(Clone, Debug)] pub(crate) struct TypeRef { pub(crate) wit_name: String, @@ -278,18 +281,98 @@ pub(crate) fn describe_exported_type_shape(def: &ExportedTypeDef) -> String { /// Procedural macros cannot resolve a bare identifier such as `Word`. The generated check permits /// a genuine `miden::Word` import but rejects a local same-named type unless it was registered with /// `#[export_type]`, preventing the emitted WIT shape from drifting from the encoded Rust type. -pub(crate) fn sdk_core_type_identity_guards( +pub(crate) fn nominal_type_identity_guards( definition: &ExportedTypeDef, span: Span, ) -> Result { let mut guarded = HashSet::new(); let mut guards = TokenStream::new(); visit_exported_type_refs(definition, &mut |type_ref| { - collect_sdk_core_type_identity_guard(type_ref, span, &mut guarded, &mut guards) + collect_sdk_core_type_identity_guard(type_ref, span, &mut guarded, &mut guards)?; + collect_builtin_type_identity_guard(type_ref, span, &mut guarded, &mut guards) })?; Ok(guards) } +/// Appends one nominal builtin identity check when a builtin-named reference is not guarded. +/// +/// `Option`, `Result`, and the primitive names are classified by their last path segment, so +/// a same-named foreign or shadowing type could silently change the encoded layout. The check +/// proves the written type IS the `::core` builtin, mirroring the SDK core-type guard. +fn collect_builtin_type_identity_guard( + type_ref: &TypeRef, + span: Span, + guarded: &mut HashSet<(String, String)>, + guards: &mut TokenStream, +) -> Result<(), syn::Error> { + let Some(canonical) = builtin_canonical_type_text(type_ref) else { + return Ok(()); + }; + let written = written_type_text(type_ref); + if !guarded.insert((written.clone(), canonical.clone())) { + return Ok(()); + } + let written = parse_reconstructed_type(&written, span)?; + let canonical = parse_reconstructed_type(&canonical, span)?; + guards.extend(quote_spanned! {span=> + const _: fn() = || { + fn __miden_builtin_type_name_collision_use_the_core_type( + _: ::core::marker::PhantomData, + _: ::core::marker::PhantomData, + ) {} + __miden_builtin_type_name_collision_use_the_core_type( + ::core::marker::PhantomData::<#written>, + ::core::marker::PhantomData::<#canonical>, + ); + }; + }); + Ok(()) +} + +/// Returns the `::core` spelling of a builtin-named reference, or None for other types. +fn builtin_canonical_type_text(type_ref: &TypeRef) -> Option { + if type_ref.is_custom { + return None; + } + let last = type_ref.path.last()?; + match (last.as_str(), type_ref.dependencies.as_slice()) { + ("Option", [inner]) => { + Some(format!("::core::option::Option<{}>", written_type_text(inner))) + } + ("Result", [ok, err]) => Some(format!( + "::core::result::Result<{}, {}>", + written_type_text(ok), + written_type_text(err) + )), + (ident, []) if rust_type_to_wit_type(ident).is_some() => { + Some(format!("::core::primitive::{ident}")) + } + _ => None, + } +} + +/// Reconstructs the Rust source text of a reference as it was written. +fn written_type_text(type_ref: &TypeRef) -> String { + let path = type_ref.path.join("::"); + match (type_ref.path.last().map(String::as_str), type_ref.dependencies.as_slice()) { + (Some("Option"), [inner]) => format!("{path}<{}>", written_type_text(inner)), + (Some("Result"), [ok, err]) => { + format!("{path}<{}, {}>", written_type_text(ok), written_type_text(err)) + } + _ => path, + } +} + +/// Parses one reconstructed type text back into a type for guard emission. +fn parse_reconstructed_type(text: &str, span: Span) -> Result { + syn::parse_str::(text).map_err(|error| { + syn::Error::new( + span, + format!("failed to reconstruct type `{text}` for an identity check: {error}"), + ) + }) +} + /// Visits every type reference contained in one exported definition. fn visit_exported_type_refs( definition: &ExportedTypeDef, @@ -852,6 +935,14 @@ pub(crate) fn ensure_custom_type_defined( Ok(()) } +#[cfg(test)] +/// Serializes tests that mutate the process-global exported-type registry. +pub(crate) fn lock_export_type_registry_for_tests() -> std::sync::MutexGuard<'static, ()> { + EXPORTED_TYPES_TEST_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + #[cfg(test)] pub(crate) fn reset_export_type_registry_for_tests() { if let Some(registry) = EXPORTED_TYPES.get() { diff --git a/sdk/base-macros/src/types/tests.rs b/sdk/base-macros/src/types/tests.rs index afeda11aaa..aee54691a8 100644 --- a/sdk/base-macros/src/types/tests.rs +++ b/sdk/base-macros/src/types/tests.rs @@ -11,6 +11,7 @@ use super::*; #[test] fn emits_hint_for_missing_export_type() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("LocalType").unwrap(); let exported = HashMap::new(); @@ -26,6 +27,7 @@ fn emits_hint_for_missing_export_type() { #[test] fn allows_sdk_type_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("Asset").unwrap(); let exported = HashMap::new(); @@ -40,6 +42,7 @@ fn allows_sdk_type_without_export_attribute() { #[test] fn allows_block_number_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("BlockNumber").unwrap(); let exported = HashMap::new(); @@ -54,6 +57,7 @@ fn allows_block_number_without_export_attribute() { #[test] fn allows_nonce_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("Nonce").unwrap(); let exported = HashMap::new(); @@ -68,6 +72,7 @@ fn allows_nonce_without_export_attribute() { #[test] fn allows_asset_amount_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("AssetAmount").unwrap(); let exported = HashMap::new(); @@ -82,6 +87,7 @@ fn allows_asset_amount_without_export_attribute() { #[test] fn allows_wit_primitive_type_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("u64").unwrap(); let exported = HashMap::new(); @@ -96,6 +102,7 @@ fn allows_wit_primitive_type_without_export_attribute() { #[test] fn struct_fields_allow_wit_primitive_types() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let item: syn::ItemStruct = parse_quote! { struct Foo { @@ -120,6 +127,7 @@ fn struct_fields_allow_wit_primitive_types() { #[test] fn exported_types_capture_doc_attributes() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let item_struct: syn::ItemStruct = parse_quote! { /// Record documentation. @@ -152,6 +160,7 @@ fn exported_types_capture_doc_attributes() { #[test] fn maps_rust_primitive_types_to_wit_types() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let exported_names = HashSet::new(); @@ -181,6 +190,7 @@ fn maps_rust_primitive_types_to_wit_types() { #[test] fn rejects_unsupported_component_primitives() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); @@ -198,6 +208,7 @@ fn rejects_unsupported_component_primitives() { #[test] fn rejects_unsupported_component_primitives_nested_in_option_or_result() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); @@ -215,6 +226,7 @@ fn rejects_unsupported_component_primitives_nested_in_option_or_result() { #[test] fn maps_rust_option_type_to_wit_option() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let exported_names = HashSet::new(); @@ -230,6 +242,7 @@ fn maps_rust_option_type_to_wit_option() { #[test] fn option_type_tracks_nested_core_type_imports() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Option").unwrap(); @@ -244,6 +257,7 @@ fn option_type_tracks_nested_core_type_imports() { #[test] fn option_type_validates_nested_custom_type() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Option").unwrap(); @@ -260,6 +274,7 @@ fn option_type_validates_nested_custom_type() { #[test] fn maps_rust_result_type_to_wit_result() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let exported_names = HashSet::new(); @@ -275,6 +290,7 @@ fn maps_rust_result_type_to_wit_result() { #[test] fn result_type_tracks_nested_core_type_imports() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Result").unwrap(); @@ -289,6 +305,7 @@ fn result_type_tracks_nested_core_type_imports() { #[test] fn result_type_validates_nested_custom_type() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Result").unwrap(); @@ -305,6 +322,7 @@ fn result_type_validates_nested_custom_type() { #[test] fn result_type_maps_unit_argument_to_wit_placeholder() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Result<(), Felt>").unwrap(); @@ -315,6 +333,7 @@ fn result_type_maps_unit_argument_to_wit_placeholder() { #[test] fn struct_field_missing_export_type_hint() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let item: syn::ItemStruct = parse_quote! { struct Foo { @@ -337,6 +356,7 @@ fn struct_field_missing_export_type_hint() { #[test] fn enum_payload_missing_export_type_hint() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let item: syn::ItemEnum = parse_quote! { enum Foo { @@ -363,6 +383,7 @@ fn enum_payload_missing_export_type_hint() { #[test] fn forward_reference_between_export_types_is_allowed() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let first: syn::ItemStruct = parse_quote! { @@ -505,7 +526,7 @@ fn bare_core_type_name_collision_fails_identity_guard() { } }; let definition = exported_type_from_struct(&item).unwrap(); - let guards = sdk_core_type_identity_guards(&definition, Span::call_site()).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); let source = format!( r#" extern crate self as miden; @@ -535,7 +556,7 @@ fn bare_sdk_core_type_import_passes_identity_guard() { } }; let definition = exported_type_from_struct(&item).unwrap(); - let guards = sdk_core_type_identity_guards(&definition, Span::call_site()).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); let source = format!( r#" extern crate self as miden; @@ -690,6 +711,98 @@ fn main() {{}} ); } +#[test] +fn foreign_option_type_fails_the_builtin_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + value: fake::Option, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +mod fake {{ + #[allow(dead_code)] + pub struct Option(pub T); +}} +mod user {{ + use crate::fake; + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a foreign `Option` must fail the builtin guard"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("__miden_builtin_type_name_collision_use_the_core_type"), + "builtin diagnostic is not actionable: +{stderr}" + ); +} + +#[test] +fn a_shadowed_primitive_fails_the_builtin_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + value: u64, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +mod user {{ + #[allow(non_camel_case_types, dead_code)] + pub struct u64; + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a shadowed primitive must fail the builtin guard"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("__miden_builtin_type_name_collision_use_the_core_type"), + "builtin diagnostic is not actionable: +{stderr}" + ); +} + +#[test] +fn genuine_builtin_types_pass_the_builtin_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + maybe: Option, + either: core::result::Result, + flag: bool, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +mod user {{ + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + output.status.success(), + "genuine builtins must pass the builtin guard: +{}", + String::from_utf8_lossy(&output.stderr) + ); +} + /// Compiles one standalone Rust source string for nominal identity-guard tests. fn compile_rust_source(source: &str) -> Output { let output_dir = tempfile::tempdir().expect("failed to create rustc output directory"); diff --git a/sdk/note-codec/macros/src/lib.rs b/sdk/note-codec/macros/src/lib.rs index 6d223e45a2..e335919f57 100644 --- a/sdk/note-codec/macros/src/lib.rs +++ b/sdk/note-codec/macros/src/lib.rs @@ -55,7 +55,8 @@ pub fn note_codec(args: TokenStream, input: TokenStream) -> TokenStream { /// Exports all codecs marked earlier in the crate through the `miden:note-codec` component world. /// /// Place this macro after the generated schema types and every `#[note_codec]` implementation. -/// Procedural macros register codecs in declaration order. +/// Procedural macros register codecs as they expand; supported types are exported in canonical FQN +/// order. #[proc_macro] pub fn export_codecs(input: TokenStream) -> TokenStream { expand::export_codecs(input.into()) diff --git a/sdk/note-codec/macros/src/registry.rs b/sdk/note-codec/macros/src/registry.rs index 40083978e3..ec5b37dfba 100644 --- a/sdk/note-codec/macros/src/registry.rs +++ b/sdk/note-codec/macros/src/registry.rs @@ -155,7 +155,7 @@ pub(crate) fn registered_codecs(span: Span) -> syn::Result Date: Wed, 26 Aug 2026 19:58:43 +0300 Subject: [PATCH 23/43] test: serialize example builds in the shared helpers The example build lock moved from individual test sites into the shared compilation helpers, so every test that builds an example project holds it, including the sites the previous pass missed, and re-entrant double locking cannot happen. Test-side package files are written to a temporary name and renamed into place, so a concurrent reader never sees a partial file. The dex codec test, which builds its example in place, takes the lock directly, and the test-side nested builds scrub the same environment variables as the production sites. --- sdk/note-bindings/tests/p2id_consumer.rs | 23 +++++++------ sdk/note-codec/tests/component_export.rs | 3 ++ sdk/note-schema/tests/p2id_package.rs | 6 ++-- .../src/mockchain/notes/schema.rs | 8 +---- .../src/mockchain/support/helpers.rs | 3 +- .../examples/note_schema_metadata.rs | 3 +- tests/integration/src/sdk/mod.rs | 7 ++-- tests/support/Cargo.toml | 1 + tests/support/src/lib.rs | 33 ++++++++++++++++++- .../cargo-miden/tests/dex_note_codec_build.rs | 10 ++++-- 10 files changed, 63 insertions(+), 34 deletions(-) diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs index 9319b37fa0..f33b1130f2 100644 --- a/sdk/note-bindings/tests/p2id_consumer.rs +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -4,7 +4,9 @@ use std::{env, fs, path::Path, process::Command}; use miden_mast_package::Section; use midenc_frontend_wasm_metadata::package_note_storage_schema_section_id; -use midenc_integration_test_support::{compile_project, example_build_lock, workspace_root}; +use midenc_integration_test_support::{ + compile_project, scrub_nested_cargo_env, workspace_root, write_masp_file_atomic, +}; /// Returns the native rustc host target. fn host_target() -> String { @@ -45,18 +47,16 @@ fn workspace_patch_section(workspace: &Path) -> String { #[test] fn generated_p2id_bindings_compile_and_run_in_a_consumer_crate() { let workspace = workspace_root(); - let _build_lock = example_build_lock(&workspace); let examples = workspace.join("examples"); let wallet_dir = examples.join("basic-wallet"); let wallet = compile_project(&wallet_dir); - wallet - .write_masp_file(wallet_dir.join("target/miden/release")) + write_masp_file_atomic(&wallet, wallet_dir.join("target/miden/release")) .expect("failed to persist the basic-wallet dependency package"); let p2id_dir = examples.join("p2id-note"); let p2id = compile_project(&p2id_dir); let package_dir = p2id_dir.join("target/miden/release"); - p2id.write_masp_file(&package_dir).expect("failed to persist the p2id package"); + write_masp_file_atomic(&p2id, &package_dir).expect("failed to persist the p2id package"); let package_path = package_dir.join("p2id.masp"); let temp = tempfile::tempdir().unwrap(); @@ -80,8 +80,7 @@ interface note-storage { "# .to_vec(), )); - second_package - .write_masp_file(&second_package_dir) + write_masp_file_atomic(&second_package, &second_package_dir) .expect("failed to persist the second schema package"); let second_package_path = second_package_dir.join("p2id.masp"); @@ -159,17 +158,17 @@ fn main() {{ ); fs::write(temp.path().join("src/main.rs"), source).unwrap(); - let output = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + let mut cargo = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + cargo .args(["run", "--quiet", "--target"]) .arg(host_target()) .arg("--manifest-path") .arg(temp.path().join("Cargo.toml")) .current_dir(temp.path()) .env("CARGO_TARGET_DIR", workspace.join("target/note-bindings-consumer")) - .env("CARGO_NET_OFFLINE", "true") - .env_remove("CARGO_BUILD_TARGET") - .env_remove("CARGO_ENCODED_RUSTFLAGS") - .env_remove("RUSTFLAGS") + .env("CARGO_NET_OFFLINE", "true"); + scrub_nested_cargo_env(&mut cargo); + let output = cargo .output() .expect("failed to spawn Cargo for the generated bindings consumer"); assert!( diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index f66cba8dab..e77fda5b98 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -33,8 +33,11 @@ fn minimal_codec_crate_builds_to_wasi_only_component() { "--offline", ]) .env("CARGO_TARGET_DIR", &target_dir) + // Keep this aligned with production nested builds in midenc-compile and miden-note-schema. + .env_remove("CARGO_BUILD_RUSTFLAGS") .env_remove("CARGO_BUILD_TARGET") .env_remove("CARGO_ENCODED_RUSTFLAGS") + .env_remove("CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS") .env_remove("RUSTFLAGS") .output() .expect("failed to run cargo for the component fixture"); diff --git a/sdk/note-schema/tests/p2id_package.rs b/sdk/note-schema/tests/p2id_package.rs index 2dc30a9342..b49b2613e0 100644 --- a/sdk/note-schema/tests/p2id_package.rs +++ b/sdk/note-schema/tests/p2id_package.rs @@ -2,17 +2,15 @@ use miden_note_schema::{NoteStorage, NoteStorageSchema}; use miden_protocol::{account::AccountId, address::NetworkId}; -use midenc_integration_test_support::{compile_project, example_build_lock, workspace_root}; +use midenc_integration_test_support::{compile_project, workspace_root, write_masp_file_atomic}; #[test] fn p2id_schema_builds_and_decodes_account_id_storage() { let workspace = workspace_root(); - let _build_lock = example_build_lock(&workspace); let examples = workspace.join("examples"); let wallet_dir = examples.join("basic-wallet"); let wallet = compile_project(&wallet_dir); - wallet - .write_masp_file(wallet_dir.join("target/miden/release")) + write_masp_file_atomic(&wallet, wallet_dir.join("target/miden/release")) .expect("failed to persist the basic-wallet dependency package"); let p2id = compile_project(&examples.join("p2id-note")); diff --git a/tests/integration-network/src/mockchain/notes/schema.rs b/tests/integration-network/src/mockchain/notes/schema.rs index e96d65cac0..4618e37071 100644 --- a/tests/integration-network/src/mockchain/notes/schema.rs +++ b/tests/integration-network/src/mockchain/notes/schema.rs @@ -21,9 +21,7 @@ use midenc_frontend_wasm_metadata::{ PACKAGE_NOTE_CODEC_SECTION_ID, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, package_note_codec_section_id, package_note_storage_schema_section_id, }; -use midenc_integration_test_support::{ - example_build_lock, wasm_target_is_installed, workspace_root, -}; +use midenc_integration_test_support::wasm_target_is_installed; use super::super::support::{ assert_account_has_fungible_asset, build_send_notes_script, compile_rust_package, execute_tx, @@ -108,8 +106,6 @@ fn dex_note_uses_embedded_schema_and_component_codec() { eprintln!("skipping DEX note schema test: wasm32-wasip2 is not installed"); return; } - let workspace = workspace_root(); - let _build_lock = example_build_lock(&workspace); let note_package = compile_rust_package("../../examples/dex-note", true); assert_package_section( ¬e_package, @@ -145,8 +141,6 @@ fn dex_note_uses_embedded_schema_and_component_codec() { #[test] fn p2id_note_builds_storage_without_a_component_codec() { - let workspace = workspace_root(); - let _build_lock = example_build_lock(&workspace); let note_package = compile_rust_package("../../examples/p2id-note", true); let schema = NoteStorageSchema::from_package(¬e_package).unwrap(); diff --git a/tests/integration-network/src/mockchain/support/helpers.rs b/tests/integration-network/src/mockchain/support/helpers.rs index 7e6b70a0ca..187538ddcc 100644 --- a/tests/integration-network/src/mockchain/support/helpers.rs +++ b/tests/integration-network/src/mockchain/support/helpers.rs @@ -31,7 +31,7 @@ use miden_standards::{testing::note::NoteBuilder, tx_script::SendNotesTransactio use miden_testing::{MockChain, MockTransaction, MockTransactionBuilder}; use miden_tx_script_args::{EncodedScriptArgs, ScriptArgs}; use midenc_frontend_wasm::WasmTranslationConfig; -use midenc_integration_test_support::CompilerTestBuilder; +use midenc_integration_test_support::{CompilerTestBuilder, example_build_lock, workspace_root}; use rand::{SeedableRng, rngs::StdRng}; /// Host-side mirror of the transaction-script arguments declared in @@ -115,6 +115,7 @@ pub(crate) fn block_on(future: F) -> F::Output { /// Compiles a Rust project and returns its Miden package. pub(crate) fn compile_rust_package(project_path: impl AsRef, release: bool) -> Arc { + let _build_lock = example_build_lock(&workspace_root()); let project_path = project_path.as_ref(); let config = WasmTranslationConfig::default(); let mut builder = CompilerTestBuilder::rust_source_cargo_miden(project_path, config, []); diff --git a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs index 82121db4d4..d19e4876d0 100644 --- a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs +++ b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs @@ -3,7 +3,7 @@ use miden_mast_package::Package; use midenc_expect_test::{Expect, expect}; use midenc_frontend_wasm_metadata::{package_note_storage_schema_section_id, trim_trailing_nuls}; -use midenc_integration_test_support::{compile_project, example_build_lock, workspace_root}; +use midenc_integration_test_support::{compile_project, workspace_root}; use wit_bindgen_core::wit_parser::{Resolve, Type as WitType, TypeDefKind}; /// Returns the unpadded note storage schema text from a package. @@ -42,7 +42,6 @@ fn assert_note_storage_schema(package: &Package, expected_root: &str, expected: #[test] fn note_packages_carry_resolvable_storage_schema_metadata() { let workspace = workspace_root(); - let _build_lock = example_build_lock(&workspace); let p2id = compile_project(&workspace.join("examples/p2id-note")); assert_note_storage_schema( &p2id, diff --git a/tests/integration/src/sdk/mod.rs b/tests/integration/src/sdk/mod.rs index b099b11df1..73d57552eb 100644 --- a/tests/integration/src/sdk/mod.rs +++ b/tests/integration/src/sdk/mod.rs @@ -5,6 +5,7 @@ use miden_core::serde::Serializable; use miden_mast_package::{Package, PackageExport, ProcedureExport, QualifiedProcedureName}; use miden_protocol::note::NoteScript; use midenc_frontend_wasm::WasmTranslationConfig; +use midenc_integration_test_support::write_masp_file_atomic; use crate::{ CompilerTest, CompilerTestBuilder, @@ -908,11 +909,9 @@ fn rust_sdk_fpi_reexpands_after_only_package_cache_env_changes() { for cache in [&first_cache, &second_cache] { fs::create_dir_all(cache).unwrap(); } - first_package - .write_masp_file(&first_cache) + write_masp_file_atomic(&first_package, &first_cache) .expect("failed to prepopulate the first package cache"); - second_package - .write_masp_file(&second_cache) + write_masp_file_atomic(&second_package, &second_cache) .expect("failed to prepopulate the second package cache"); let cargo_target_dir = project.root().join("option-env-cargo-target"); diff --git a/tests/support/Cargo.toml b/tests/support/Cargo.toml index b4417eef17..0edc7f1a44 100644 --- a/tests/support/Cargo.toml +++ b/tests/support/Cargo.toml @@ -37,4 +37,5 @@ midenc-session.workspace = true midenc-compile.workspace = true proptest.workspace = true sha2.workspace = true +tempfile.workspace = true walkdir = "2.5.0" diff --git a/tests/support/src/lib.rs b/tests/support/src/lib.rs index f42cf7cbb2..8fb127fad8 100644 --- a/tests/support/src/lib.rs +++ b/tests/support/src/lib.rs @@ -33,6 +33,7 @@ pub use self::{ /// Compiles one Cargo Miden project without debug output. pub fn compile_project(project_path: &Path) -> Arc { + let _build_lock = example_build_lock(&workspace_root()); let mut test = CompilerTest::rust_source_cargo_miden( project_path, WasmTranslationConfig::default(), @@ -46,7 +47,7 @@ pub fn workspace_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() } -/// Locks shared example outputs for the full build and consume span. +/// Locks shared example outputs while one build is running. pub fn example_build_lock(workspace: &Path) -> File { let target_dir = workspace.join("target"); fs::create_dir_all(&target_dir).expect("failed to create the workspace target directory"); @@ -61,6 +62,36 @@ pub fn example_build_lock(workspace: &Path) -> File { lock } +/// Writes a Miden package through a same-directory temporary file and atomic rename. +pub fn write_masp_file_atomic( + package: &Package, + output_dir: impl AsRef, +) -> std::io::Result<()> { + let output_dir = output_dir.as_ref(); + fs::create_dir_all(output_dir)?; + let temporary = tempfile::Builder::new() + .prefix(".miden-package-") + .tempfile_in(output_dir)? + .into_temp_path(); + package.write_to_file(&temporary)?; + let package_name: &str = &package.name; + let destination = output_dir.join(package_name).with_extension(Package::EXTENSION); + fs::rename(&temporary, destination) +} + +/// Removes outer build settings that would poison a nested Cargo invocation. +pub fn scrub_nested_cargo_env(cmd: &mut Command) { + for variable in [ + "CARGO_BUILD_RUSTFLAGS", + "CARGO_BUILD_TARGET", + "CARGO_ENCODED_RUSTFLAGS", + "CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS", + "RUSTFLAGS", + ] { + cmd.env_remove(variable); + } +} + /// Returns true when rustup reports the codec component target as installed. pub fn wasm_target_is_installed() -> bool { const WASM_TARGET: &str = "wasm32-wasip2"; diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs index 89c1b3b002..55df037b92 100644 --- a/tools/cargo-miden/tests/dex_note_codec_build.rs +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -7,7 +7,7 @@ use miden_mast_package::Package; use midenc_frontend_wasm_metadata::{ package_note_codec_section_id, package_note_storage_schema_section_id, }; -use midenc_integration_test_support::wasm_target_is_installed; +use midenc_integration_test_support::{example_build_lock, wasm_target_is_installed}; use wit_component::DecodedWasm; use wit_parser::WorldItem; @@ -32,9 +32,13 @@ fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { env::remove_var("CARGO_TARGET_DIR"); } - let note_dir = workspace_root().join("examples/dex-note"); + let workspace = workspace_root(); + let note_dir = workspace.join("examples/dex-note"); env::set_current_dir(¬e_dir).unwrap(); - let result = run(["cargo", "miden", "build", "--release"].into_iter().map(str::to_owned)); + let result = { + let _build_lock = example_build_lock(&workspace); + run(["cargo", "miden", "build", "--release"].into_iter().map(str::to_owned)) + }; let output = result .expect("cargo miden build for dex-note failed") From da341d47e4c9489d8f9ccdbe290353346ca38ecd Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 19:58:43 +0300 Subject: [PATCH 24/43] docs: correct migration, config, and release metadata The migration guide now covers the duplicate-registration error for different Rust types with identical shapes. The per-crate Cargo config comments describe what the files actually set. The workspace comment on the wit-parser version skew states the direction the lockfile shows. The seven note crates move from the never-published section of the release configuration into the sdk unit they belong to, and the note-codec crate description names the canonical-value rule correctly. --- .release/config.toml | 12 ++++++------ sdk/base-macros/.cargo/config.toml | 2 +- sdk/field-repr/derive/.cargo/config.toml | 2 +- sdk/sdk/MIGRATION.md | 10 +++++----- sdk/wasm-metadata/.cargo/config.toml | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.release/config.toml b/.release/config.toml index 7aa2a9b5c6..7e7e8f6cdb 100644 --- a/.release/config.toml +++ b/.release/config.toml @@ -217,12 +217,6 @@ unit = "sdk" name = "midenc-frontend-wasm-metadata" unit = "sdk" -# --- private: repository infrastructure, never published ---------------------- - -[[packages]] -name = "miden-field-repr-tests" -unit = "private" - [[packages]] name = "miden-note-schema" unit = "sdk" @@ -251,6 +245,12 @@ unit = "sdk" name = "miden-note-codec-wit" unit = "sdk" +# --- private: repository infrastructure, never published ---------------------- + +[[packages]] +name = "miden-field-repr-tests" +unit = "private" + [[packages]] name = "midenc-benchmark-runner" unit = "private" diff --git a/sdk/base-macros/.cargo/config.toml b/sdk/base-macros/.cargo/config.toml index cfd2bbd96e..7f9b18c368 100644 --- a/sdk/base-macros/.cargo/config.toml +++ b/sdk/base-macros/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: keeps this host-side crate on the host target; sdk/ has no directory-wide config. +# Per-crate copy: check-each builds run in each member crate and default to the Miden Wasm target. [build] target = "wasm32-wasip1" diff --git a/sdk/field-repr/derive/.cargo/config.toml b/sdk/field-repr/derive/.cargo/config.toml index cfd2bbd96e..7f9b18c368 100644 --- a/sdk/field-repr/derive/.cargo/config.toml +++ b/sdk/field-repr/derive/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: keeps this host-side crate on the host target; sdk/ has no directory-wide config. +# Per-crate copy: check-each builds run in each member crate and default to the Miden Wasm target. [build] target = "wasm32-wasip1" diff --git a/sdk/sdk/MIGRATION.md b/sdk/sdk/MIGRATION.md index 130a197ada..649f24b9b0 100644 --- a/sdk/sdk/MIGRATION.md +++ b/sdk/sdk/MIGRATION.md @@ -90,12 +90,12 @@ constructors can commit to the note script's MAST root. Rename any inherent item including methods, `#[note_constructor]` methods, associated constants, and items declared in a separate impl block. -### Keep duplicate `#[export_type]` registrations shape-compatible +### Keep duplicate `#[export_type]` registrations nominally unique and shape-compatible -Two `#[export_type]` registrations that map to the same WIT type are now a compile error when -their record fields or enum cases differ. Previously, the last registration silently replaced the -first. Rename one Rust type so it maps to a different WIT name, or make both registered shapes -identical. +Two different Rust types that map to the same WIT name are now a compile error, even when their +record fields or enum cases are identical. Shape-identical duplicate registrations of the same Rust +type remain allowed, while conflicting shapes do not. Previously, the last registration silently +replaced the first. Rename one Rust type so it maps to a different WIT name. ### `#[export_type]` reserves `__MIDEN_EXPORT_TYPE_SHAPE` diff --git a/sdk/wasm-metadata/.cargo/config.toml b/sdk/wasm-metadata/.cargo/config.toml index cfd2bbd96e..7f9b18c368 100644 --- a/sdk/wasm-metadata/.cargo/config.toml +++ b/sdk/wasm-metadata/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: keeps this host-side crate on the host target; sdk/ has no directory-wide config. +# Per-crate copy: check-each builds run in each member crate and default to the Miden Wasm target. [build] target = "wasm32-wasip1" From 14de9decd55b7f687a9c5daf603dc9e8bae8408f Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 21:23:13 +0300 Subject: [PATCH 25/43] fix: repair unit result guards and make same-name registrations unambiguous The builtin identity guard rendered a unit Result argument as empty text, so Result<(), T> and Result fields in exported types failed to expand with an internal reconstruction error. The unit placeholder now renders as () and the supported shapes compile again. A duplicate #[export_type] registration was treated as benign by name and shape alone, so two different same-named types registered silently while the migration guide promised an error. The benign path now also requires the same expansion location; a second item with the shared name conflicts regardless of shape. The environment variables that must not leak into nested Cargo builds are one shared constant in wasm-metadata, used by every scrub site. The duplicated Cargo-argument builder in the Rust frontend collapsed into one function. The dead commented-out declaration left the embedded core-types WIT, which shrinks the schema section of every note package, and the identity-guard entry point documents both checks it emits. --- Cargo.lock | 1 + examples/dex-note-codec/src/lib.rs | 2 + midenc-compile/src/cargo.rs | 16 +-- midenc-compile/src/pipeline/frontends/rust.rs | 122 ++++++------------ sdk/base-macros/src/note_schema.rs | 6 - sdk/base-macros/src/types.rs | 30 +++-- sdk/base-macros/src/types/tests.rs | 65 +++++++++- sdk/base-macros/wit/miden.wit | 3 - sdk/note-schema/src/codec_component.rs | 21 ++- sdk/wasm-metadata/src/lib.rs | 12 +- 10 files changed, 154 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05e2996ab1..3326fa65f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4508,6 +4508,7 @@ dependencies = [ "midenc-session", "proptest", "sha2", + "tempfile", "walkdir", ] diff --git a/examples/dex-note-codec/src/lib.rs b/examples/dex-note-codec/src/lib.rs index f43ee44dd9..6deafba9dd 100644 --- a/examples/dex-note-codec/src/lib.rs +++ b/examples/dex-note-codec/src/lib.rs @@ -73,6 +73,8 @@ fn parse_part(value: &str, name: &str) -> Result { /// Displays finite fractions as decimals and other fractions as ratios. fn display_limit_price(value: &LimitPrice) -> String { if value.denominator == 0 { + // Guard the factor loop against a parsed-but-unvalidated zero denominator: it would + // otherwise never terminate. return format!("{}/{}", value.numerator, value.denominator); } let divisor = greatest_common_divisor(value.numerator, value.denominator); diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index b985e690c9..ff4e8d6cbd 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -14,7 +14,7 @@ use std::{ use miden_assembly::{SourceManager, serde::Serializable}; use miden_mast_package::Package as MastPackage; use miden_note_codec_wit::NOTE_CODEC_WIT; -use midenc_frontend_wasm_metadata::package_cache; +use midenc_frontend_wasm_metadata::{NESTED_CARGO_SCRUB_ENV, package_cache}; use midenc_hir::Report; use midenc_session::{InputFile, LinkLibrary, Session, miden_project}; use sha2::{Digest, Sha256}; @@ -463,15 +463,11 @@ fn build_note_codec_component( .arg("--message-format") .arg("json-render-diagnostics") // Let `from_project!` find the staged note package during codec macro expansion. - .env(package_cache::PACKAGE_CACHE_ENV, &staged_package.cache_dir) - // Outer Miden target settings and flags would poison this nested wasip2 codec build. - .env_remove("CARGO_BUILD_RUSTFLAGS") - .env_remove("CARGO_BUILD_TARGET") - .env_remove("CARGO_ENCODED_RUSTFLAGS") - .env_remove("CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS") - .env_remove("RUSTFLAGS") - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()); + .env(package_cache::PACKAGE_CACHE_ENV, &staged_package.cache_dir); + for &variable in NESTED_CARGO_SCRUB_ENV { + cargo.env_remove(variable); + } + cargo.stdout(Stdio::piped()).stderr(Stdio::inherit()); cargo.args(apply_cargo_policy(session.options.cargo_locked, session.options.cargo_offline)); let manifest_path = manifest_path.canonicalize().map_err(|error| { diff --git a/midenc-compile/src/pipeline/frontends/rust.rs b/midenc-compile/src/pipeline/frontends/rust.rs index 1b2f998c16..0b14a51ea5 100644 --- a/midenc-compile/src/pipeline/frontends/rust.rs +++ b/midenc-compile/src/pipeline/frontends/rust.rs @@ -621,7 +621,14 @@ trim-paths = [\"diagnostics\", \"object\"] std::fs::write(&manifest_path, &cargo_toml) .map_err(|err| Report::msg(format!("failed to generate temporary Cargo.toml: {err}")))?; - let cargo_build_args = build_cargo_args(&manifest_path, options); + let cargo_build_args = build_cargo_args( + Some(&manifest_path), + options.profile == "release", + options.workspace, + &options.packages, + (options.cargo_locked, options.cargo_offline), + "\"s\"", + ); // The same mandatory set and inherited-flag precedence as the manifest route: a present // `CARGO_ENCODED_RUSTFLAGS` is authoritative over plain `RUSTFLAGS`. @@ -822,7 +829,15 @@ fn rustc( Ok(output_file) } -fn build_cargo_args(manifest_path: &Path, options: &Options) -> Vec { +/// Builds the shared argument vector for a nested `cargo build` invocation. +pub(super) fn build_cargo_args( + manifest_path: Option<&Path>, + release: bool, + workspace: bool, + packages: &[P], + cargo_policy: (bool, bool), + release_opt_level: &str, +) -> Vec { let mut args = vec!["build".to_string()]; // Add build-std flags required for Miden compilation @@ -844,7 +859,7 @@ fn build_cargo_args(manifest_path: &Path, options: &Options) -> Vec { ("profile.dev.overflow-checks", "false"), ("profile.dev.debug", "true"), ("profile.dev.debug-assertions", "false"), - ("profile.release.opt-level", "\"s\""), + ("profile.release.opt-level", release_opt_level), ("profile.release.lto", "true"), ("profile.release.codegen-units", "1"), ("profile.release.panic", "\"abort\""), @@ -864,22 +879,23 @@ fn build_cargo_args(manifest_path: &Path, options: &Options) -> Vec { } // Forward cargo-specific options - if options.profile == "release" { + if release { args.push("--release".to_string()); } args.extend( - crate::cargo::apply_cargo_policy(options.cargo_locked, options.cargo_offline) - .map(ToString::to_string), + crate::cargo::apply_cargo_policy(cargo_policy.0, cargo_policy.1).map(ToString::to_string), ); - args.push("--manifest-path".to_string()); - args.push(manifest_path.to_string_lossy().to_string()); + if let Some(manifest_path) = manifest_path { + args.push("--manifest-path".to_string()); + args.push(manifest_path.to_string_lossy().to_string()); + } - if options.workspace { + if workspace { args.push("--workspace".to_string()); } - for package in &options.packages { + for package in packages { args.push("--package".to_string()); args.push(package.to_string()); } @@ -1990,7 +2006,14 @@ pub(crate) mod manifest { _source_manager: Arc, ) -> CompilerResult { let rustup_toolchain = crate::rust::rustup_toolchain(); - let cargo_build_args = build_cargo_args(cargo_opts, compiler_opts.optimize); + let cargo_build_args = super::build_cargo_args( + cargo_opts.manifest_path.as_deref(), + cargo_opts.release, + cargo_opts.workspace, + &cargo_opts.packages, + (cargo_opts.locked, cargo_opts.offline), + cargo_profile_opt_level(compiler_opts.optimize), + ); let inherited_encoded = std::env::var_os("CARGO_ENCODED_RUSTFLAGS"); let inherited_plain = std::env::var_os("RUSTFLAGS"); @@ -2144,74 +2167,6 @@ pub(crate) mod manifest { } } - /// Builds the argument vector for the underlying `cargo build` invocation. - pub(super) fn build_cargo_args(cargo_opts: &CargoOptions, opt_level: OptLevel) -> Vec { - let mut args = vec!["build".to_string()]; - - // Add build-std flags required for Miden compilation - args.extend( - [ - "-Z", - "build-std=core,alloc,panic_abort", - "-Z", - "build-std-features=optimize_for_size", - ] - .into_iter() - .map(|s| s.to_string()), - ); - - // Configure profile settings - let cfg_pairs: Vec<(&str, &str)> = vec![ - ("profile.dev.panic", "\"abort\""), - ("profile.dev.opt-level", "1"), - ("profile.dev.overflow-checks", "false"), - ("profile.dev.debug", "true"), - ("profile.dev.debug-assertions", "false"), - ("profile.release.opt-level", cargo_profile_opt_level(opt_level)), - ("profile.release.lto", "true"), - ("profile.release.codegen-units", "1"), - ("profile.release.panic", "\"abort\""), - // The guest Wasm needs DWARF (`debug = true` above) so the compiler can turn - // it into Miden debug information — but the host-side units of this nested - // build (build scripts, proc macros, and their whole native Miden dependency - // cone) do not. When a profile sets `debug` explicitly, the host units - // inherit it, which was measured at ~94% of a 310 MB proc-macro artifact, - // repeated in every build directory a test suite uses. - ("profile.dev.build-override.debug", "false"), - ("profile.release.build-override.debug", "false"), - ]; - - for (key, value) in cfg_pairs { - args.push("--config".to_string()); - args.push(format!("{key}={value}")); - } - - // Forward cargo-specific options - if cargo_opts.release { - args.push("--release".to_string()); - } - args.extend( - crate::cargo::apply_cargo_policy(cargo_opts.locked, cargo_opts.offline) - .map(ToString::to_string), - ); - - if let Some(ref manifest_path) = cargo_opts.manifest_path { - args.push("--manifest-path".to_string()); - args.push(manifest_path.to_string_lossy().to_string()); - } - - if cargo_opts.workspace { - args.push("--workspace".to_string()); - } - - for package in &cargo_opts.packages { - args.push("--package".to_string()); - args.push(package.to_string()); - } - - args - } - fn run_cargo( wasi: &str, toolchain: Option<&str>, @@ -3910,7 +3865,14 @@ path = "lib.rs" offline: true, ..Default::default() }; - let args = manifest::build_cargo_args(&options, midenc_session::OptLevel::None); + let args = build_cargo_args( + options.manifest_path.as_deref(), + options.release, + options.workspace, + &options.packages, + (options.locked, options.offline), + "2", + ); assert!(args.iter().any(|arg| arg == "--locked")); assert!(args.iter().any(|arg| arg == "--offline")); diff --git a/sdk/base-macros/src/note_schema.rs b/sdk/base-macros/src/note_schema.rs index d876a948bd..a5d083fd92 100644 --- a/sdk/base-macros/src/note_schema.rs +++ b/sdk/base-macros/src/note_schema.rs @@ -805,9 +805,6 @@ mod tests { suffix: felt } - /// Creates a new account ID from a field element. - //account-id-from-felt: func(felt: felt) -> account-id; - /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) record recipient { inner: word @@ -1034,9 +1031,6 @@ mod tests { suffix: felt } - /// Creates a new account ID from a field element. - //account-id-from-felt: func(felt: felt) -> account-id; - /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) record recipient { inner: word diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index b83385642f..b8402291d9 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -159,18 +159,19 @@ fn register_export_type_in( if let Some(existing) = registry.iter_mut().find(|existing| existing.def.wit_name == def.wit_name) { - if existing.def.rust_name == def.rust_name - && exported_type_shapes_match(&existing.def, &def) - { - // rust-analyzer can expand the same attribute more than once in one macro process. - return Ok(()); - } - if existing.location == location { + if existing.def.rust_name == def.rust_name + && exported_type_shapes_match(&existing.def, &def) + { + // rust-analyzer can expand the same attribute more than once in one process. + return Ok(()); + } // A long-lived macro host re-expanded an edited item; replace the stale shape. existing.def = def; return Ok(()); } + // Two different items that map to one WIT name are ambiguous even with equal + // shapes: a note field referring to the shared name cannot say which one it means. let identity = if existing.def.rust_name == def.rust_name { format!("Rust type `{}`", def.rust_name) @@ -276,11 +277,13 @@ pub(crate) fn describe_exported_type_shape(def: &ExportedTypeDef) -> String { } } -/// Emits nominal identity checks for references classified as SDK core types by their Rust name. +/// Emits nominal identity checks for names the classifier trusts without resolution. /// -/// Procedural macros cannot resolve a bare identifier such as `Word`. The generated check permits -/// a genuine `miden::Word` import but rejects a local same-named type unless it was registered with -/// `#[export_type]`, preventing the emitted WIT shape from drifting from the encoded Rust type. +/// Procedural macros cannot resolve identifiers. Two classes of names are pinned: +/// SDK core-type names (a genuine `miden::Word` import passes, a local same-named type +/// fails unless registered with `#[export_type]`), and the builtin names `Option`, +/// `Result`, and the primitives (proven to BE the `::core` definitions). Both checks stop +/// the emitted WIT shape from drifting from the encoded Rust type. pub(crate) fn nominal_type_identity_guards( definition: &ExportedTypeDef, span: Span, @@ -353,6 +356,11 @@ fn builtin_canonical_type_text(type_ref: &TypeRef) -> Option { /// Reconstructs the Rust source text of a reference as it was written. fn written_type_text(type_ref: &TypeRef) -> String { + // The unit type is recorded with an empty path (see the tuple arm of + // `map_type_to_type_ref`); render it as `()` so reconstructed generics parse. + if type_ref.path.is_empty() { + return "()".to_string(); + } let path = type_ref.path.join("::"); match (type_ref.path.last().map(String::as_str), type_ref.dependencies.as_slice()) { (Some("Option"), [inner]) => format!("{path}<{}>", written_type_text(inner)), diff --git a/sdk/base-macros/src/types/tests.rs b/sdk/base-macros/src/types/tests.rs index aee54691a8..7835eafc99 100644 --- a/sdk/base-macros/src/types/tests.rs +++ b/sdk/base-macros/src/types/tests.rs @@ -475,7 +475,7 @@ fn allows_same_shape_export_type_reregistration() { &mut registry, exported_type_from_struct(&second).unwrap(), Span::call_site(), - ("tests.rs".to_string(), 2, 0), + ("tests.rs".to_string(), 1, 0), ) .unwrap(); @@ -483,6 +483,41 @@ fn allows_same_shape_export_type_reregistration() { assert_eq!(registry[0].def.docs, vec![" Documentation from rustc's expansion."]); } +#[test] +fn same_shape_registrations_from_different_locations_conflict() { + let first: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u64, + } + }; + let second: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u64, + } + }; + let mut registry = Vec::new(); + + register_export_type_in( + &mut registry, + exported_type_from_struct(&first).unwrap(), + Span::call_site(), + ("tests.rs".to_string(), 1, 0), + ) + .unwrap(); + // A second item with the same name is ambiguous even when the shapes are equal. + let error = register_export_type_in( + &mut registry, + exported_type_from_struct(&second).unwrap(), + Span::call_site(), + ("tests.rs".to_string(), 2, 0), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("conflicting #[export_type] registration")); + assert_eq!(registry.len(), 1); +} + #[test] fn same_location_reregistration_replaces_a_stale_shape() { let first: syn::ItemStruct = parse_quote! { @@ -774,6 +809,34 @@ fn main() {{}} ); } +#[test] +fn unit_result_shapes_pass_the_builtin_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + done: Result<(), u32>, + partial: Result, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +mod user {{ + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + output.status.success(), + "unit result shapes must pass the builtin guard: +{}", + String::from_utf8_lossy(&output.stderr) + ); +} + #[test] fn genuine_builtin_types_pass_the_builtin_identity_guard() { let item: syn::ItemStruct = parse_quote! { diff --git a/sdk/base-macros/wit/miden.wit b/sdk/base-macros/wit/miden.wit index 719aae4e5f..20f25ecbf9 100644 --- a/sdk/base-macros/wit/miden.wit +++ b/sdk/base-macros/wit/miden.wit @@ -40,9 +40,6 @@ interface core-types { suffix: felt } - /// Creates a new account ID from a field element. - //account-id-from-felt: func(felt: felt) -> account-id; - /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) record recipient { inner: word diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 35b39787f4..5f03cc9bb5 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -330,7 +330,9 @@ mod tests { PackageExport, PackageId, PathBuf as MastPathBuf, ProcedureExport, Section, TargetType, Version, }; - use midenc_frontend_wasm_metadata::package_note_storage_schema_section_id; + use midenc_frontend_wasm_metadata::{ + NESTED_CARGO_SCRUB_ENV, package_note_storage_schema_section_id, + }; use midenc_integration_test_support::wasm_target_is_installed; use tempfile::TempDir; use wasmtime::ResourceLimiter; @@ -606,7 +608,8 @@ package miden:base@1.0.0 { let fixture = TempDir::new().expect("failed to create component fixture directory"); write_fixture(fixture.path()); let target_dir = workspace_root().join("target/note-schema-component-test"); - let output = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + let mut command = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + command .args([ "build", "--manifest-path", @@ -616,15 +619,11 @@ package miden:base@1.0.0 { WASM_TARGET, "--offline", ]) - .env("CARGO_TARGET_DIR", &target_dir) - // Outer Miden target settings and flags would poison this nested wasip2 codec build. - .env_remove("CARGO_BUILD_RUSTFLAGS") - .env_remove("CARGO_BUILD_TARGET") - .env_remove("CARGO_ENCODED_RUSTFLAGS") - .env_remove("CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS") - .env_remove("RUSTFLAGS") - .output() - .expect("failed to start fixture build"); + .env("CARGO_TARGET_DIR", &target_dir); + for &variable in NESTED_CARGO_SCRUB_ENV { + command.env_remove(variable); + } + let output = command.output().expect("failed to start fixture build"); assert_command_succeeded("building the component adapter fixture", &output); fs::read( diff --git a/sdk/wasm-metadata/src/lib.rs b/sdk/wasm-metadata/src/lib.rs index 00f116f9f2..5a2fefe66f 100644 --- a/sdk/wasm-metadata/src/lib.rs +++ b/sdk/wasm-metadata/src/lib.rs @@ -13,6 +13,15 @@ use alloc::{string::String, vec::Vec}; use serde::{Deserialize, Serialize}; +/// Environment variables that must not leak into nested Cargo builds. +pub const NESTED_CARGO_SCRUB_ENV: &[&str] = &[ + "CARGO_BUILD_RUSTFLAGS", + "CARGO_BUILD_TARGET", + "CARGO_ENCODED_RUSTFLAGS", + "CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS", + "RUSTFLAGS", +]; + /// Name of the Wasm custom section used to store frontend metadata bytes. pub const WASM_FRONTEND_METADATA_CUSTOM_SECTION_NAME: &str = "rodata,miden_account_component_frontend"; @@ -81,8 +90,7 @@ pub fn trim_trailing_nuls(bytes: &[u8]) -> &[u8] { /// The compiler publishes compiled dependency packages — and its recorded dependency /// resolution — into the directory named by [`package_cache::PACKAGE_CACHE_ENV`]; the SDK /// macros and the build-script support crate consume them. Every spelling of that contract lives -/// here so the producer and the consumers cannot drift apart. (The support crate is the one -/// deliberate exception: it is dependency-free by design and spells the same values inline.) +/// here so the producer and the consumers cannot drift apart. pub mod package_cache { use alloc::{format, string::String}; From 9e4223d988cf775aa2f52da0b882d02bc2fa2121 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 21:23:13 +0300 Subject: [PATCH 26/43] test: close the example-lock bypasses and align the test-side probes Two builders of the shared examples ran without the example build lock, so the serialization it promises did not hold; both take it now. The atomic package writer in the test-support crate delegates to the production writer, which keeps dotted package names intact. The wasm-target probes follow the sysroot-first rule production adopted, so linked toolchains run the gated tests instead of skipping them, and the core-types golden text exists once per expectation file. --- sdk/note-codec/Cargo.toml | 1 + sdk/note-codec/tests/component_export.rs | 34 +- .../examples/basic_wallet_package_sizes.rs | 32 +- .../examples/note_schema_metadata.rs | 476 +++++++----------- tests/support/Cargo.toml | 1 - tests/support/src/lib.rs | 38 +- .../cargo-miden/tests/dex_note_codec_build.rs | 6 +- .../tests/p2id_cargo_miden_build.rs | 15 +- tools/cargo-miden/tests/utils.rs | 8 - 9 files changed, 243 insertions(+), 368 deletions(-) diff --git a/sdk/note-codec/Cargo.toml b/sdk/note-codec/Cargo.toml index 533f207d6b..e3e1f663c0 100644 --- a/sdk/note-codec/Cargo.toml +++ b/sdk/note-codec/Cargo.toml @@ -25,6 +25,7 @@ miden-protocol.workspace = true wit-bindgen = { workspace = true } [dev-dependencies] +midenc-frontend-wasm-metadata.workspace = true tempfile.workspace = true wit-component.workspace = true wit-parser.workspace = true diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index e77fda5b98..aa4187931e 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -6,6 +6,7 @@ use std::{ process::{Command, Output}, }; +use midenc_frontend_wasm_metadata::NESTED_CARGO_SCRUB_ENV; use tempfile::TempDir; use wit_component::DecodedWasm; use wit_parser::WorldItem; @@ -22,7 +23,8 @@ fn minimal_codec_crate_builds_to_wasi_only_component() { let fixture = TempDir::new().expect("failed to create temporary codec crate"); write_fixture(fixture.path()); let target_dir = workspace_root().join("target/note-codec-component-test"); - let output = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + let mut command = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + command .args([ "build", "--manifest-path", @@ -32,15 +34,11 @@ fn minimal_codec_crate_builds_to_wasi_only_component() { WASM_TARGET, "--offline", ]) - .env("CARGO_TARGET_DIR", &target_dir) - // Keep this aligned with production nested builds in midenc-compile and miden-note-schema. - .env_remove("CARGO_BUILD_RUSTFLAGS") - .env_remove("CARGO_BUILD_TARGET") - .env_remove("CARGO_ENCODED_RUSTFLAGS") - .env_remove("CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS") - .env_remove("RUSTFLAGS") - .output() - .expect("failed to run cargo for the component fixture"); + .env("CARGO_TARGET_DIR", &target_dir); + for &variable in NESTED_CARGO_SCRUB_ENV { + command.env_remove(variable); + } + let output = command.output().expect("failed to run cargo for the component fixture"); assert_command_succeeded("building the component fixture", &output); let component = fs::read( @@ -127,22 +125,24 @@ fn assert_command_succeeded(action: &str, output: &Output) { ); } -/// Returns true when rustup reports the codec component target as installed. +/// Local copy of the sysroot probe in tests/support to keep this test dependency-light. fn wasm_target_is_installed() -> bool { - let output = match Command::new("rustup").args(["target", "list"]).output() { + let output = match Command::new("rustc").args(["--print", "sysroot"]).output() { Ok(output) if output.status.success() => output, Ok(output) => { - eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); + eprintln!( + "`rustc --print sysroot` failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); return false; } Err(error) => { - eprintln!("could not run `rustup target list`: {error}"); + eprintln!("could not run `rustc --print sysroot` (rustup may be unavailable): {error}"); return false; } }; - String::from_utf8_lossy(&output.stdout) - .lines() - .any(|line| line.starts_with(WASM_TARGET) && line.contains("(installed)")) + let sysroot = Path::new(String::from_utf8_lossy(&output.stdout).trim()).to_path_buf(); + sysroot.join("lib").join("rustlib").join(WASM_TARGET).exists() } const FIXTURE_SOURCE: &str = r##" diff --git a/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs b/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs index 27b608044d..0b3b950c95 100644 --- a/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs +++ b/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs @@ -1,5 +1,6 @@ use midenc_expect_test::expect; use midenc_frontend_wasm::WasmTranslationConfig; +use midenc_integration_test_support::{example_build_lock, workspace_root}; use crate::{CompilerTest, testing::stripped_mast_size_str}; @@ -10,12 +11,16 @@ fn no_debug_flags() -> [String; 2] { #[test] fn basic_wallet_and_p2id() { let config = WasmTranslationConfig::default(); - let mut account_test = CompilerTest::rust_source_cargo_miden( - "../../examples/basic-wallet", - config.clone(), - no_debug_flags(), - ); - let account_package = account_test.compile_package(); + let workspace = workspace_root(); + let account_package = { + let _build_lock = example_build_lock(&workspace); + let mut account_test = CompilerTest::rust_source_cargo_miden( + "../../examples/basic-wallet", + config.clone(), + no_debug_flags(), + ); + account_test.compile_package() + }; assert!(account_package.is_library(), "expected library"); expect!["8505"].assert_eq(stripped_mast_size_str(&account_package).as_str()); @@ -28,12 +33,15 @@ fn basic_wallet_and_p2id() { assert!(tx_script_package.is_library(), "expected library"); expect!["13784"].assert_eq(stripped_mast_size_str(&tx_script_package).as_str()); - let mut p2id_test = CompilerTest::rust_source_cargo_miden( - "../../examples/p2id-note", - config.clone(), - no_debug_flags(), - ); - let note_package = p2id_test.compile_package(); + let note_package = { + let _build_lock = example_build_lock(&workspace); + let mut p2id_test = CompilerTest::rust_source_cargo_miden( + "../../examples/p2id-note", + config.clone(), + no_debug_flags(), + ); + p2id_test.compile_package() + }; assert!(note_package.is_library(), "expected library"); expect!["21763"].assert_eq(stripped_mast_size_str(¬e_package).as_str()); // The note package exports both the note script and the `build-recipient` constructor; the diff --git a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs index d19e4876d0..948cdbd415 100644 --- a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs +++ b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs @@ -6,6 +6,141 @@ use midenc_frontend_wasm_metadata::{package_note_storage_schema_section_id, trim use midenc_integration_test_support::{compile_project, workspace_root}; use wit_bindgen_core::wit_parser::{Resolve, Type as WitType, TypeDefKind}; +/// Shared SDK core-types package appended to every generated note schema. +const EXPECTED_CORE_TYPES: &str = concat!( + r#"package miden:base@1.0.0 { + interface core-types { + /// Represents an on-chain felt. + /// + /// Field modulus M = 2^64 - 2^32 + 1. + record felt { + /// The backing type is `f32` which will be treated as a felt by the compiler. +"#, + " \t/// We're basically hijacking the Wasm `f32` type and treat as felt.\n", + r#" inner: f32, + } + + + /// A group of four field elements in the Miden base field. + record word { + a: felt, +"#, + " \tb: felt,\n", + " \tc: felt,\n", + " \td: felt,\n", + r#" } + + /// A cryptographic digest representing a 256-bit hash value. + /// This is a wrapper around `word` which contains 4 field elements. + record digest { + inner: word + } + + /// Unique identifier of an account. + /// + /// # Layout + /// + /// An `AccountId` consists of two field elements, where the first is called the prefix and the + /// second is called the suffix. It is laid out as follows: + /// + /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] + /// suffix: [zero bit | hash (55 bits) | 8 zero bits] + record account-id { +"#, + " \tprefix: felt,\n", + " \tsuffix: felt\n", + r#" } + + /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) + record recipient { + inner: word + } + + record tag { + inner: felt + } + + /// A fungible or a non-fungible asset. + /// + /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. + /// + /// The methodology for constructing fungible and non-fungible assets is described below. + /// + /// # Fungible assets + /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `[amount, 0, 0, 0]` + /// + /// # Non-fungible assets + /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `DATA_HASH` + record asset { + key: word, + value: word, + } + + /// A validated fungible asset amount, at most 2^63 - 2^31. + record asset-amount { + inner: felt + } + + /// Account nonce + record nonce { + inner: felt + } + + /// A block height in the chain + record block-number { + inner: felt + } + + /// Account hash + record account-hash { + inner: word + } + + /// Block hash + record block-hash { + inner: word + } + + /// Storage value + record storage-value { + inner: word + } + + /// Account storage root + record storage-root { + inner: word + } + + /// Account code root + record account-code-root { + inner: word + } + + /// Commitment to the account vault + record vault-commitment { + inner: word + } + + /// An index of the created note + record note-idx { + inner: felt + } + + record note-type { + inner: felt + } + + record note-execution-hint { + inner: felt + } + + } +} +"#, +); + /// Returns the unpadded note storage schema text from a package. fn note_storage_schema(package: &Package) -> &str { let section_id = package_note_storage_schema_section_id(); @@ -23,7 +158,12 @@ fn note_storage_schema(package: &Package) -> &str { /// Checks a schema golden and resolves its root storage alias with wit-parser. fn assert_note_storage_schema(package: &Package, expected_root: &str, expected: Expect) { let source = note_storage_schema(package); - expected.assert_eq(source); + let core_types_start = source + .find("package miden:base@1.0.0 {") + .expect("schema must contain the SDK core-types package"); + let (schema, core_types) = source.split_at(core_types_start); + expected.assert_eq(schema); + assert_eq!(core_types, EXPECTED_CORE_TYPES); let mut resolve = Resolve::default(); let package_id = resolve @@ -47,154 +187,24 @@ fn note_packages_carry_resolvable_storage_schema_metadata() { &p2id, "p2id-note", expect![[r#" - // This file is auto-generated by the `#[note]` macro. - // Do not edit this file manually. - - package miden:p2id-schema@0.1.0; - - use miden:base/core-types@1.0.0; - - interface note-storage { - use core-types.{account-id}; - - record p2id-note { - target-account-id: account-id, - } - - type storage = p2id-note; - } - - package miden:base@1.0.0 { - interface core-types { - /// Represents an on-chain felt. - /// - /// Field modulus M = 2^64 - 2^32 + 1. - record felt { - /// The backing type is `f32` which will be treated as a felt by the compiler. - /// We're basically hijacking the Wasm `f32` type and treat as felt. - inner: f32, - } - - - /// A group of four field elements in the Miden base field. - record word { - a: felt, - b: felt, - c: felt, - d: felt, - } - - /// A cryptographic digest representing a 256-bit hash value. - /// This is a wrapper around `word` which contains 4 field elements. - record digest { - inner: word - } + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. - /// Unique identifier of an account. - /// - /// # Layout - /// - /// An `AccountId` consists of two field elements, where the first is called the prefix and the - /// second is called the suffix. It is laid out as follows: - /// - /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] - /// suffix: [zero bit | hash (55 bits) | 8 zero bits] - record account-id { - prefix: felt, - suffix: felt - } + package miden:p2id-schema@0.1.0; - /// Creates a new account ID from a field element. - //account-id-from-felt: func(felt: felt) -> account-id; + use miden:base/core-types@1.0.0; - /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) - record recipient { - inner: word - } + interface note-storage { + use core-types.{account-id}; - record tag { - inner: felt - } - - /// A fungible or a non-fungible asset. - /// - /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. - /// - /// The methodology for constructing fungible and non-fungible assets is described below. - /// - /// # Fungible assets - /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` - /// - `value`: `[amount, 0, 0, 0]` - /// - /// # Non-fungible assets - /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` - /// - `value`: `DATA_HASH` - record asset { - key: word, - value: word, - } - - /// A validated fungible asset amount, at most 2^63 - 2^31. - record asset-amount { - inner: felt - } - - /// Account nonce - record nonce { - inner: felt - } - - /// A block height in the chain - record block-number { - inner: felt - } - - /// Account hash - record account-hash { - inner: word - } - - /// Block hash - record block-hash { - inner: word - } - - /// Storage value - record storage-value { - inner: word - } - - /// Account storage root - record storage-root { - inner: word - } - - /// Account code root - record account-code-root { - inner: word - } - - /// Commitment to the account vault - record vault-commitment { - inner: word - } - - /// An index of the created note - record note-idx { - inner: felt - } - - record note-type { - inner: felt - } - - record note-execution-hint { - inner: felt + record p2id-note { + target-account-id: account-id, } + type storage = p2id-note; } - } - "#]], + + "#]], ); let swapp = compile_project(&workspace.join("tests/fixtures/components/swapp-note")); @@ -202,169 +212,39 @@ fn note_packages_carry_resolvable_storage_schema_metadata() { &swapp, "swapp-note", expect![[r#" - // This file is auto-generated by the `#[note]` macro. - // Do not edit this file manually. - - package miden:swapp-note-schema@0.1.0; - - use miden:base/core-types@1.0.0; - - interface note-storage { - use core-types.{account-id, felt, word}; - - /// SWAPP note storage. - /// - /// The note creator stores the swap terms in the note storage; the fields below are decoded - /// from the storage elements in declaration order. - record swapp-note { - /// Vault key identifying the requested asset (faucet id, composition, callback flags). - requested-asset-key: word, - /// Total requested asset amount for the full offer. - requested-total: felt, - /// The account that created the swap offer and receives the requested asset. - creator: account-id, - /// Note type used for the notes created by this script (P2ID routing note and remainder - /// SWAPP note). - output-note-type: felt, - /// Tag routing the P2ID note to the creator. - p2id-tag: felt, - /// Script root of the P2ID note script used for the routing note. - p2id-script-root: word, - } - - type storage = swapp-note; - } - - package miden:base@1.0.0 { - interface core-types { - /// Represents an on-chain felt. - /// - /// Field modulus M = 2^64 - 2^32 + 1. - record felt { - /// The backing type is `f32` which will be treated as a felt by the compiler. - /// We're basically hijacking the Wasm `f32` type and treat as felt. - inner: f32, - } - - - /// A group of four field elements in the Miden base field. - record word { - a: felt, - b: felt, - c: felt, - d: felt, - } - - /// A cryptographic digest representing a 256-bit hash value. - /// This is a wrapper around `word` which contains 4 field elements. - record digest { - inner: word - } - - /// Unique identifier of an account. - /// - /// # Layout - /// - /// An `AccountId` consists of two field elements, where the first is called the prefix and the - /// second is called the suffix. It is laid out as follows: - /// - /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] - /// suffix: [zero bit | hash (55 bits) | 8 zero bits] - record account-id { - prefix: felt, - suffix: felt - } + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. - /// Creates a new account ID from a field element. - //account-id-from-felt: func(felt: felt) -> account-id; + package miden:swapp-note-schema@0.1.0; - /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) - record recipient { - inner: word - } + use miden:base/core-types@1.0.0; - record tag { - inner: felt - } + interface note-storage { + use core-types.{account-id, felt, word}; - /// A fungible or a non-fungible asset. + /// SWAPP note storage. /// - /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. - /// - /// The methodology for constructing fungible and non-fungible assets is described below. - /// - /// # Fungible assets - /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` - /// - `value`: `[amount, 0, 0, 0]` - /// - /// # Non-fungible assets - /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` - /// - `value`: `DATA_HASH` - record asset { - key: word, - value: word, - } - - /// A validated fungible asset amount, at most 2^63 - 2^31. - record asset-amount { - inner: felt - } - - /// Account nonce - record nonce { - inner: felt - } - - /// A block height in the chain - record block-number { - inner: felt - } - - /// Account hash - record account-hash { - inner: word - } - - /// Block hash - record block-hash { - inner: word - } - - /// Storage value - record storage-value { - inner: word - } - - /// Account storage root - record storage-root { - inner: word - } - - /// Account code root - record account-code-root { - inner: word - } - - /// Commitment to the account vault - record vault-commitment { - inner: word - } - - /// An index of the created note - record note-idx { - inner: felt - } - - record note-type { - inner: felt - } - - record note-execution-hint { - inner: felt + /// The note creator stores the swap terms in the note storage; the fields below are decoded + /// from the storage elements in declaration order. + record swapp-note { + /// Vault key identifying the requested asset (faucet id, composition, callback flags). + requested-asset-key: word, + /// Total requested asset amount for the full offer. + requested-total: felt, + /// The account that created the swap offer and receives the requested asset. + creator: account-id, + /// Note type used for the notes created by this script (P2ID routing note and remainder + /// SWAPP note). + output-note-type: felt, + /// Tag routing the P2ID note to the creator. + p2id-tag: felt, + /// Script root of the P2ID note script used for the routing note. + p2id-script-root: word, } + type storage = swapp-note; } - } - "#]], + + "#]], ); } diff --git a/tests/support/Cargo.toml b/tests/support/Cargo.toml index 0edc7f1a44..b4417eef17 100644 --- a/tests/support/Cargo.toml +++ b/tests/support/Cargo.toml @@ -37,5 +37,4 @@ midenc-session.workspace = true midenc-compile.workspace = true proptest.workspace = true sha2.workspace = true -tempfile.workspace = true walkdir = "2.5.0" diff --git a/tests/support/src/lib.rs b/tests/support/src/lib.rs index 8fb127fad8..639d527d19 100644 --- a/tests/support/src/lib.rs +++ b/tests/support/src/lib.rs @@ -11,6 +11,7 @@ use std::{ use miden_mast_package::Package; use midenc_frontend_wasm::WasmTranslationConfig; +use midenc_frontend_wasm_metadata::NESTED_CARGO_SCRUB_ENV; /// Utilities for generating on-disk Cargo projects for tests. pub mod cargo_proj; @@ -62,52 +63,39 @@ pub fn example_build_lock(workspace: &Path) -> File { lock } -/// Writes a Miden package through a same-directory temporary file and atomic rename. +/// Writes a Miden package through the production atomic package publisher. pub fn write_masp_file_atomic( package: &Package, output_dir: impl AsRef, ) -> std::io::Result<()> { - let output_dir = output_dir.as_ref(); - fs::create_dir_all(output_dir)?; - let temporary = tempfile::Builder::new() - .prefix(".miden-package-") - .tempfile_in(output_dir)? - .into_temp_path(); - package.write_to_file(&temporary)?; - let package_name: &str = &package.name; - let destination = output_dir.join(package_name).with_extension(Package::EXTENSION); - fs::rename(&temporary, destination) + midenc_session::registry::write_package_atomically(package, output_dir.as_ref()).map(|_| ()) } /// Removes outer build settings that would poison a nested Cargo invocation. pub fn scrub_nested_cargo_env(cmd: &mut Command) { - for variable in [ - "CARGO_BUILD_RUSTFLAGS", - "CARGO_BUILD_TARGET", - "CARGO_ENCODED_RUSTFLAGS", - "CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS", - "RUSTFLAGS", - ] { + for &variable in NESTED_CARGO_SCRUB_ENV { cmd.env_remove(variable); } } -/// Returns true when rustup reports the codec component target as installed. +/// Returns true when the active Rust sysroot contains the codec component target. pub fn wasm_target_is_installed() -> bool { const WASM_TARGET: &str = "wasm32-wasip2"; - let output = match Command::new("rustup").args(["target", "list"]).output() { + let output = match Command::new("rustc").args(["--print", "sysroot"]).output() { Ok(output) if output.status.success() => output, Ok(output) => { - eprintln!("`rustup target list` failed:\n{}", String::from_utf8_lossy(&output.stderr)); + eprintln!( + "`rustc --print sysroot` failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); return false; } Err(error) => { - eprintln!("could not run `rustup target list`: {error}"); + eprintln!("could not run `rustc --print sysroot` (rustup may be unavailable): {error}"); return false; } }; - String::from_utf8_lossy(&output.stdout) - .lines() - .any(|line| line.starts_with(WASM_TARGET) && line.contains("(installed)")) + let sysroot = Path::new(String::from_utf8_lossy(&output.stdout).trim()).to_path_buf(); + sysroot.join("lib").join("rustlib").join(WASM_TARGET).exists() } diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs index 55df037b92..e1cc76024b 100644 --- a/tools/cargo-miden/tests/dex_note_codec_build.rs +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -7,11 +7,13 @@ use miden_mast_package::Package; use midenc_frontend_wasm_metadata::{ package_note_codec_section_id, package_note_storage_schema_section_id, }; -use midenc_integration_test_support::{example_build_lock, wasm_target_is_installed}; +use midenc_integration_test_support::{ + example_build_lock, wasm_target_is_installed, workspace_root, +}; use wit_component::DecodedWasm; use wit_parser::WorldItem; -use crate::utils::{RestoreEnvironment, current_dir_lock, workspace_root}; +use crate::utils::{RestoreEnvironment, current_dir_lock}; #[test] fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { diff --git a/tools/cargo-miden/tests/p2id_cargo_miden_build.rs b/tools/cargo-miden/tests/p2id_cargo_miden_build.rs index c488f14f67..8176109d41 100644 --- a/tools/cargo-miden/tests/p2id_cargo_miden_build.rs +++ b/tools/cargo-miden/tests/p2id_cargo_miden_build.rs @@ -1,8 +1,9 @@ use std::{env, time::SystemTime}; use cargo_miden::run; +use midenc_integration_test_support::{example_build_lock, workspace_root}; -use crate::utils::{current_dir_lock, workspace_root}; +use crate::utils::current_dir_lock; /// A caller-provided `MIDENC_PACKAGE_CACHE` materializes a build's Miden dependencies on disk. /// @@ -27,7 +28,8 @@ fn p2id_build_materializes_basic_wallet_dependency() { env::remove_var("CARGO_TARGET_DIR"); } - let examples = workspace_root().join("examples"); + let workspace = workspace_root(); + let examples = workspace.join("examples"); let p2id_note_dir = examples.join("p2id-note"); // Build the p2id-note project, which pulls in basic-wallet as a Miden dependency. @@ -35,9 +37,12 @@ fn p2id_build_materializes_basic_wallet_dependency() { env::set_current_dir(&p2id_note_dir).unwrap(); let build_started_at = SystemTime::now(); let export_dir = crate::utils::exported_packages_dir(&p2id_note_dir); - let result = crate::utils::with_package_cache_env(&export_dir, || { - run(["cargo", "miden", "build", "--release"].into_iter().map(|s| s.to_string())) - }); + let result = { + let _build_lock = example_build_lock(&workspace); + crate::utils::with_package_cache_env(&export_dir, || { + run(["cargo", "miden", "build", "--release"].into_iter().map(|s| s.to_string())) + }) + }; env::set_current_dir(&restore_dir).unwrap(); // Restore `CARGO_TARGET_DIR` before asserting, so a build failure doesn't leak the unset state. diff --git a/tools/cargo-miden/tests/utils.rs b/tools/cargo-miden/tests/utils.rs index 0600d215c6..0f7da08f52 100644 --- a/tools/cargo-miden/tests/utils.rs +++ b/tools/cargo-miden/tests/utils.rs @@ -323,11 +323,3 @@ fn write_template( fs::write(template_root.join("src/lib.rs"), lib_rs)?; Ok(()) } - -pub(crate) fn workspace_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(Path::parent) - .expect("cargo-miden should live under tools/cargo-miden") - .to_path_buf() -} From 3755325a6ff4b6fdb6244f68f2670b8e81f41c4e Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 26 Aug 2026 21:23:13 +0300 Subject: [PATCH 27/43] chore: unify the per-crate Cargo config comments All ten fanned-out config copies carry one comment that is accurate for guest, host, and proc-macro crates alike. --- sdk/alloc/.cargo/config.toml | 2 +- sdk/base-macros/.cargo/config.toml | 2 +- sdk/base-sys/.cargo/config.toml | 2 +- sdk/base/.cargo/config.toml | 2 +- sdk/field-repr/derive/.cargo/config.toml | 2 +- sdk/field-repr/repr/.cargo/config.toml | 2 +- sdk/field-repr/tests/.cargo/config.toml | 2 +- sdk/sdk/.cargo/config.toml | 2 +- sdk/stdlib-sys/.cargo/config.toml | 2 +- sdk/wasm-metadata/.cargo/config.toml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/alloc/.cargo/config.toml b/sdk/alloc/.cargo/config.toml index f03dba586a..7fe3f77d7f 100644 --- a/sdk/alloc/.cargo/config.toml +++ b/sdk/alloc/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/base-macros/.cargo/config.toml b/sdk/base-macros/.cargo/config.toml index 7f9b18c368..7fe3f77d7f 100644 --- a/sdk/base-macros/.cargo/config.toml +++ b/sdk/base-macros/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: check-each builds run in each member crate and default to the Miden Wasm target. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/base-sys/.cargo/config.toml b/sdk/base-sys/.cargo/config.toml index f03dba586a..7fe3f77d7f 100644 --- a/sdk/base-sys/.cargo/config.toml +++ b/sdk/base-sys/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/base/.cargo/config.toml b/sdk/base/.cargo/config.toml index f03dba586a..7fe3f77d7f 100644 --- a/sdk/base/.cargo/config.toml +++ b/sdk/base/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/field-repr/derive/.cargo/config.toml b/sdk/field-repr/derive/.cargo/config.toml index 7f9b18c368..7fe3f77d7f 100644 --- a/sdk/field-repr/derive/.cargo/config.toml +++ b/sdk/field-repr/derive/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: check-each builds run in each member crate and default to the Miden Wasm target. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/field-repr/repr/.cargo/config.toml b/sdk/field-repr/repr/.cargo/config.toml index f03dba586a..7fe3f77d7f 100644 --- a/sdk/field-repr/repr/.cargo/config.toml +++ b/sdk/field-repr/repr/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/field-repr/tests/.cargo/config.toml b/sdk/field-repr/tests/.cargo/config.toml index f03dba586a..7fe3f77d7f 100644 --- a/sdk/field-repr/tests/.cargo/config.toml +++ b/sdk/field-repr/tests/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/sdk/.cargo/config.toml b/sdk/sdk/.cargo/config.toml index f03dba586a..7fe3f77d7f 100644 --- a/sdk/sdk/.cargo/config.toml +++ b/sdk/sdk/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/stdlib-sys/.cargo/config.toml b/sdk/stdlib-sys/.cargo/config.toml index f03dba586a..7fe3f77d7f 100644 --- a/sdk/stdlib-sys/.cargo/config.toml +++ b/sdk/stdlib-sys/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: guest crates default to the Miden Wasm target. sdk/ has no directory-wide config so the host-side note crates build natively. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" diff --git a/sdk/wasm-metadata/.cargo/config.toml b/sdk/wasm-metadata/.cargo/config.toml index 7f9b18c368..7fe3f77d7f 100644 --- a/sdk/wasm-metadata/.cargo/config.toml +++ b/sdk/wasm-metadata/.cargo/config.toml @@ -1,3 +1,3 @@ -# Per-crate copy: check-each builds run in each member crate and default to the Miden Wasm target. +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. [build] target = "wasm32-wasip1" From 02fd16552764ea36e7299b20776c67af7248f26a Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 2 Sep 2026 15:12:24 +0300 Subject: [PATCH 28/43] test: adapt the note test support to the rebased driver and note macro The atomic package publisher now reports through anyhow, so the test-support wrapper converts its error into the io error its callers expect. The #[note] expansion now also implements the SDK ActiveNote trait, so the standalone unit-note test provides the same trait stand-in as its sibling. The lockfile records the dependency edges of the merged manifests. --- Cargo.lock | 4 +--- midenc-compile/src/lib.rs | 2 +- sdk/base-macros/tests/unit_note_trailing_data.rs | 6 ++++++ tests/support/src/lib.rs | 4 +++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3326fa65f4..21a903c273 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3619,7 +3619,6 @@ dependencies = [ "miden-note-bindings-macros", "miden-note-schema", "miden-protocol", - "midenc-frontend-wasm", "midenc-frontend-wasm-metadata", "midenc-integration-test-support", "tempfile", @@ -3649,7 +3648,7 @@ dependencies = [ "miden-note-codec-macros", "miden-note-codec-wit", "miden-protocol", - "midenc-integration-test-support", + "midenc-frontend-wasm-metadata", "tempfile", "wit-bindgen", "wit-component", @@ -4508,7 +4507,6 @@ dependencies = [ "midenc-session", "proptest", "sha2", - "tempfile", "walkdir", ] diff --git a/midenc-compile/src/lib.rs b/midenc-compile/src/lib.rs index f3cb332faa..1338a3826f 100644 --- a/midenc-compile/src/lib.rs +++ b/midenc-compile/src/lib.rs @@ -20,9 +20,9 @@ use alloc::rc::Rc; pub use midenc_hir::Context; #[cfg(feature = "std")] use midenc_hir::Op; +use midenc_session::diagnostics::{Diagnostic, Report, miette}; #[cfg(feature = "std")] use midenc_session::{OutputFile, OutputType}; -use midenc_session::diagnostics::{Diagnostic, Report, miette}; #[cfg(feature = "std")] use midenc_session::{OutputMode, diagnostics::WrapErr}; diff --git a/sdk/base-macros/tests/unit_note_trailing_data.rs b/sdk/base-macros/tests/unit_note_trailing_data.rs index 605cf72815..34f4cb0656 100644 --- a/sdk/base-macros/tests/unit_note_trailing_data.rs +++ b/sdk/base-macros/tests/unit_note_trailing_data.rs @@ -14,6 +14,12 @@ pub mod felt_repr { pub use miden_field_repr::{FeltReader, FeltReprError, FeltWriter, FromFeltRepr, ToFeltRepr}; } +pub mod active_note { + /// Minimal stand-in for the SDK `ActiveNote` trait implemented by the `#[note]` struct + /// expansion. + pub trait ActiveNote {} +} + #[derive(Debug)] #[note] struct UnitNote; diff --git a/tests/support/src/lib.rs b/tests/support/src/lib.rs index 639d527d19..1e3fe26c4d 100644 --- a/tests/support/src/lib.rs +++ b/tests/support/src/lib.rs @@ -68,7 +68,9 @@ pub fn write_masp_file_atomic( package: &Package, output_dir: impl AsRef, ) -> std::io::Result<()> { - midenc_session::registry::write_package_atomically(package, output_dir.as_ref()).map(|_| ()) + midenc_session::registry::write_package_atomically(package, output_dir.as_ref()) + .map(|_| ()) + .map_err(std::io::Error::other) } /// Removes outer build settings that would poison a nested Cargo invocation. From bc1092fa29610caeee4296aeedc341ede8a68ab1 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 2 Sep 2026 15:14:07 +0300 Subject: [PATCH 29/43] test: share the nested build cache and slim debug info in note test builds The three nested Cargo builds in the note tests (the generated bindings consumer, the codec fixture, and the schema fixture) now share the hash-keyed build directory that the test-support crate already uses, so they stop rebuilding the native Miden dependency cone into private directories. The generated consumer project keeps only file and line debug information; it exists to prove that the bindings compile and run, and full DWARF for that dependency cone costs gigabytes in the shared build directory. --- sdk/note-bindings/tests/p2id_consumer.rs | 10 ++++++++++ sdk/note-codec/tests/component_export.rs | 6 +++++- sdk/note-schema/src/codec_component.rs | 6 +++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs index f33b1130f2..fb14072ffb 100644 --- a/sdk/note-bindings/tests/p2id_consumer.rs +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -96,6 +96,12 @@ edition = "2024" [workspace] +# Keep only file/line debug information: this project exists to prove the +# generated bindings compile and run, and full DWARF for the native Miden +# dependency cone costs gigabytes in the shared test build directory. +[profile.dev] +debug = "line-tables-only" + [dependencies] miden-note-bindings = {{ path = {bindings_dir:?} }} @@ -166,6 +172,10 @@ fn main() {{ .arg(temp.path().join("Cargo.toml")) .current_dir(temp.path()) .env("CARGO_TARGET_DIR", workspace.join("target/note-bindings-consumer")) + // Share the hash-keyed intermediates with the other test builds; only the + // name-keyed final artifacts stay in the directory above. Convention from + // `tests/support`. + .env("CARGO_BUILD_BUILD_DIR", workspace.join("target/miden_build_cache")) .env("CARGO_NET_OFFLINE", "true"); scrub_nested_cargo_env(&mut cargo); let output = cargo diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index aa4187931e..a2d43970ef 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -34,7 +34,11 @@ fn minimal_codec_crate_builds_to_wasi_only_component() { WASM_TARGET, "--offline", ]) - .env("CARGO_TARGET_DIR", &target_dir); + .env("CARGO_TARGET_DIR", &target_dir) + // Share the hash-keyed intermediates with the other test builds; only the + // name-keyed final artifacts stay in the directory above. Convention from + // `tests/support`. + .env("CARGO_BUILD_BUILD_DIR", workspace_root().join("target/miden_build_cache")); for &variable in NESTED_CARGO_SCRUB_ENV { command.env_remove(variable); } diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 5f03cc9bb5..3d034b2982 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -619,7 +619,11 @@ package miden:base@1.0.0 { WASM_TARGET, "--offline", ]) - .env("CARGO_TARGET_DIR", &target_dir); + .env("CARGO_TARGET_DIR", &target_dir) + // Share the hash-keyed intermediates with the other test builds; only the + // name-keyed final artifacts stay in the directory above. Convention from + // `tests/support`. + .env("CARGO_BUILD_BUILD_DIR", workspace_root().join("target/miden_build_cache")); for &variable in NESTED_CARGO_SCRUB_ENV { command.env_remove(variable); } From b5950de59a8d62bcb10e7720efa058aa3de641aa Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Fri, 4 Sep 2026 13:44:45 +0300 Subject: [PATCH 30/43] chore: refresh example lockfiles for sdk 0.14.0 and compiler 0.10.0 --- examples/auth-component-no-auth/Cargo.lock | 22 +++++++++---------- .../auth-component-rpo-falcon512/Cargo.lock | 22 +++++++++---------- examples/basic-wallet-tx-script/Cargo.lock | 22 +++++++++---------- examples/basic-wallet/Cargo.lock | 22 +++++++++---------- examples/collatz/Cargo.lock | 2 +- examples/counter-contract/Cargo.lock | 22 +++++++++---------- examples/counter-note/Cargo.lock | 22 +++++++++---------- examples/dex-note-codec/Cargo.lock | 16 +++++++------- examples/dex-note/Cargo.lock | 22 +++++++++---------- examples/fibonacci/Cargo.lock | 2 +- examples/is-prime/Cargo.lock | 2 +- examples/p2id-note/Cargo.lock | 22 +++++++++---------- examples/p2id-tx-script/Cargo.lock | 22 +++++++++---------- examples/p2ide-note/Cargo.lock | 22 +++++++++---------- examples/storage-example/Cargo.lock | 22 +++++++++---------- .../components/assert-debug-test/Cargo.lock | 20 ++++++++--------- .../component-macros-account/Cargo.lock | 20 ++++++++--------- .../cross-ctx-account-word-arg/Cargo.lock | 20 ++++++++--------- .../cross-ctx-account-word/Cargo.lock | 20 ++++++++--------- .../components/cross-ctx-account/Cargo.lock | 20 ++++++++--------- .../cross-ctx-note-word-arg/Cargo.lock | 20 ++++++++--------- .../components/cross-ctx-note-word/Cargo.lock | 20 ++++++++--------- .../components/cross-ctx-note/Cargo.lock | 20 ++++++++--------- .../fixtures/components/swapp-note/Cargo.lock | 20 ++++++++--------- 24 files changed, 222 insertions(+), 222 deletions(-) diff --git a/examples/auth-component-no-auth/Cargo.lock b/examples/auth-component-no-auth/Cargo.lock index f3ef9c100e..7dffb808b1 100644 --- a/examples/auth-component-no-auth/Cargo.lock +++ b/examples/auth-component-no-auth/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/auth-component-rpo-falcon512/Cargo.lock b/examples/auth-component-rpo-falcon512/Cargo.lock index 964a353654..b08b7fbf55 100644 --- a/examples/auth-component-rpo-falcon512/Cargo.lock +++ b/examples/auth-component-rpo-falcon512/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/basic-wallet-tx-script/Cargo.lock b/examples/basic-wallet-tx-script/Cargo.lock index 51651bd2e6..84e6bb00b1 100644 --- a/examples/basic-wallet-tx-script/Cargo.lock +++ b/examples/basic-wallet-tx-script/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/basic-wallet/Cargo.lock b/examples/basic-wallet/Cargo.lock index c9648c396c..35e66bf1fe 100644 --- a/examples/basic-wallet/Cargo.lock +++ b/examples/basic-wallet/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/collatz/Cargo.lock b/examples/collatz/Cargo.lock index 40f3ef060c..76d1a26a1b 100644 --- a/examples/collatz/Cargo.lock +++ b/examples/collatz/Cargo.lock @@ -11,4 +11,4 @@ dependencies = [ [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" diff --git a/examples/counter-contract/Cargo.lock b/examples/counter-contract/Cargo.lock index 34dc5734ed..8b34bdb550 100644 --- a/examples/counter-contract/Cargo.lock +++ b/examples/counter-contract/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/counter-note/Cargo.lock b/examples/counter-note/Cargo.lock index d751b8136e..601e794a03 100644 --- a/examples/counter-note/Cargo.lock +++ b/examples/counter-note/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/dex-note-codec/Cargo.lock b/examples/dex-note-codec/Cargo.lock index 0e9f5a1758..054ca18e4b 100644 --- a/examples/dex-note-codec/Cargo.lock +++ b/examples/dex-note-codec/Cargo.lock @@ -1821,7 +1821,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1829,7 +1829,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1938,7 +1938,7 @@ dependencies = [ [[package]] name = "miden-note-codec" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1950,7 +1950,7 @@ dependencies = [ [[package]] name = "miden-note-codec-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-note-codec-wit", @@ -1965,11 +1965,11 @@ dependencies = [ [[package]] name = "miden-note-codec-wit" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-note-schema" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1982,7 +1982,7 @@ dependencies = [ [[package]] name = "miden-note-schema-codegen" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-note-schema", @@ -2229,7 +2229,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/dex-note/Cargo.lock b/examples/dex-note/Cargo.lock index 11c5a0bead..438d3dddfa 100644 --- a/examples/dex-note/Cargo.lock +++ b/examples/dex-note/Cargo.lock @@ -952,7 +952,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1044,7 +1044,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1052,7 +1052,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1073,7 +1073,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1230,7 +1230,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1238,7 +1238,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1485,11 +1485,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1526,14 +1526,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1605,7 +1605,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/fibonacci/Cargo.lock b/examples/fibonacci/Cargo.lock index 430fec63c6..75a9b2d1f0 100644 --- a/examples/fibonacci/Cargo.lock +++ b/examples/fibonacci/Cargo.lock @@ -11,4 +11,4 @@ dependencies = [ [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" diff --git a/examples/is-prime/Cargo.lock b/examples/is-prime/Cargo.lock index bcb4d8e62f..67592bf65c 100644 --- a/examples/is-prime/Cargo.lock +++ b/examples/is-prime/Cargo.lock @@ -11,4 +11,4 @@ dependencies = [ [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" diff --git a/examples/p2id-note/Cargo.lock b/examples/p2id-note/Cargo.lock index 0d3c4ef203..b4b60a3a81 100644 --- a/examples/p2id-note/Cargo.lock +++ b/examples/p2id-note/Cargo.lock @@ -943,7 +943,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1064,7 +1064,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1476,11 +1476,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1517,14 +1517,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1596,7 +1596,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/p2id-tx-script/Cargo.lock b/examples/p2id-tx-script/Cargo.lock index b86fffb4bb..2279796154 100644 --- a/examples/p2id-tx-script/Cargo.lock +++ b/examples/p2id-tx-script/Cargo.lock @@ -933,7 +933,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1025,7 +1025,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1033,7 +1033,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1054,7 +1054,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1219,7 +1219,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1466,11 +1466,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1507,14 +1507,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1586,7 +1586,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/p2ide-note/Cargo.lock b/examples/p2ide-note/Cargo.lock index 91c3b9cfcb..4afbc389d5 100644 --- a/examples/p2ide-note/Cargo.lock +++ b/examples/p2ide-note/Cargo.lock @@ -943,7 +943,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1064,7 +1064,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1476,11 +1476,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1517,14 +1517,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1596,7 +1596,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/storage-example/Cargo.lock b/examples/storage-example/Cargo.lock index d8e4262ae2..c6e8f74861 100644 --- a/examples/storage-example/Cargo.lock +++ b/examples/storage-example/Cargo.lock @@ -943,7 +943,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1064,7 +1064,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1476,11 +1476,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1517,14 +1517,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1596,7 +1596,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/assert-debug-test/Cargo.lock b/tests/fixtures/components/assert-debug-test/Cargo.lock index 4e0d6b16cd..d43827d596 100644 --- a/tests/fixtures/components/assert-debug-test/Cargo.lock +++ b/tests/fixtures/components/assert-debug-test/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/component-macros-account/Cargo.lock b/tests/fixtures/components/component-macros-account/Cargo.lock index 1f7cbcfa62..06cba60213 100644 --- a/tests/fixtures/components/component-macros-account/Cargo.lock +++ b/tests/fixtures/components/component-macros-account/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-account-word-arg/Cargo.lock b/tests/fixtures/components/cross-ctx-account-word-arg/Cargo.lock index 4a5c46135f..60e31bb8e8 100644 --- a/tests/fixtures/components/cross-ctx-account-word-arg/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-account-word-arg/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-account-word/Cargo.lock b/tests/fixtures/components/cross-ctx-account-word/Cargo.lock index 892f782e6d..09ac7d485b 100644 --- a/tests/fixtures/components/cross-ctx-account-word/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-account-word/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-account/Cargo.lock b/tests/fixtures/components/cross-ctx-account/Cargo.lock index b90f596cc7..6854828b0a 100644 --- a/tests/fixtures/components/cross-ctx-account/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-account/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-note-word-arg/Cargo.lock b/tests/fixtures/components/cross-ctx-note-word-arg/Cargo.lock index a1a9557dae..13dc0117be 100644 --- a/tests/fixtures/components/cross-ctx-note-word-arg/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-note-word-arg/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-note-word/Cargo.lock b/tests/fixtures/components/cross-ctx-note-word/Cargo.lock index d4300213e2..e55b8093d7 100644 --- a/tests/fixtures/components/cross-ctx-note-word/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-note-word/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-note/Cargo.lock b/tests/fixtures/components/cross-ctx-note/Cargo.lock index aa1c2c6616..b2b96882c9 100644 --- a/tests/fixtures/components/cross-ctx-note/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-note/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/swapp-note/Cargo.lock b/tests/fixtures/components/swapp-note/Cargo.lock index 4f7e15686f..823b65aa74 100644 --- a/tests/fixtures/components/swapp-note/Cargo.lock +++ b/tests/fixtures/components/swapp-note/Cargo.lock @@ -943,7 +943,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1064,7 +1064,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1476,7 +1476,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1513,14 +1513,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1592,7 +1592,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", From f252e8bc2a67cff6df6d797d051f38b75b7171ba Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Fri, 4 Sep 2026 13:59:40 +0300 Subject: [PATCH 31/43] test: refresh cycle and package-size expectations after the nightly migration --- tests/integration-network/src/mockchain/counter/basic_auth.rs | 2 +- tests/integration-network/src/mockchain/counter/no_auth.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration-network/src/mockchain/counter/basic_auth.rs b/tests/integration-network/src/mockchain/counter/basic_auth.rs index 989ec3aa75..ce627e3308 100644 --- a/tests/integration-network/src/mockchain/counter/basic_auth.rs +++ b/tests/integration-network/src/mockchain/counter/basic_auth.rs @@ -67,7 +67,7 @@ pub fn counter_note_basic_auth_increments_storage() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); - expect!["9090"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["9097"].assert_eq(single_note_cycles(&tx_measurements)); // The counter contract storage value should be 2 after the note is consumed (incremented by 1). assert_counter_storage( diff --git a/tests/integration-network/src/mockchain/counter/no_auth.rs b/tests/integration-network/src/mockchain/counter/no_auth.rs index dca848961d..dffbb8b212 100644 --- a/tests/integration-network/src/mockchain/counter/no_auth.rs +++ b/tests/integration-network/src/mockchain/counter/no_auth.rs @@ -103,7 +103,7 @@ pub fn counter_note_no_auth_increments_storage_without_signature() { .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); expect!["2125"].assert_eq(auth_procedure_cycles(&tx_measurements)); - expect!["9090"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["9097"].assert_eq(single_note_cycles(&tx_measurements)); // The counter contract storage value should be 2 after the note is consumed assert_counter_storage( From bead23a3af7363261fc52ec95d1e153c476d8838 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Fri, 4 Sep 2026 15:52:27 +0300 Subject: [PATCH 32/43] refactor: make the note codec build session-free and namespace its manifest table The codec build read the session for its work directory, the profile, and the cargo policy flags. A package post-processor cannot borrow the session, so the build now derives everything from the codec crate: its work directory is `/target/midenc.note-codec/`, with the same content-addressed staging, nested cargo target, and age-based GC, and the nested build no longer inherits `--locked` and `--offline`. `post_process_package` loses its session parameter, of which the codec attach was the only consumer. The manifest table becomes `[package.metadata.midenc.note-codec]` with a `crate` key, grouped under the `midenc` namespace like the VM's event-handler plugin, and the reader rejects unknown keys and non-table values. This pre-adopts the shape of the future `PackagePostProcessor` plugin crate, so the move at the VM 0.31 migration is code motion only. --- examples/dex-note/miden-project.toml | 6 +- midenc-compile/src/cargo.rs | 282 +++++++++++------- midenc-compile/src/pipeline/assembly.rs | 20 +- midenc-compile/src/pipeline/backend.rs | 1 - midenc-compile/src/pipeline/frontends/hir.rs | 1 - midenc-compile/src/pipeline/frontends/rust.rs | 1 - midenc-compile/src/pipeline/frontends/wasm.rs | 1 - midenc-compile/src/pipeline/seed.rs | 2 - sdk/CHANGELOG.md | 6 +- .../cargo-miden/tests/dex_note_codec_build.rs | 11 +- 10 files changed, 192 insertions(+), 139 deletions(-) diff --git a/examples/dex-note/miden-project.toml b/examples/dex-note/miden-project.toml index b0e899ce0e..c391a6b534 100644 --- a/examples/dex-note/miden-project.toml +++ b/examples/dex-note/miden-project.toml @@ -12,6 +12,6 @@ miden-core = "*" miden-protocol = "*" basic-wallet = { path = "../basic-wallet" } -# MetadataSet represents each named metadata entry as a table. -[package.metadata.note-codec-crate] -path = "../dex-note-codec" +# The `midenc` namespace table groups compiler plugin configuration. +[package.metadata.midenc.note-codec] +crate = "../dex-note-codec" diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index ff4e8d6cbd..2f011e79b4 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -25,11 +25,17 @@ use wit_parser::{ use crate::{CodegenOutput, CompilerResult}; -/// Metadata table that points to an author-side note codec crate. -pub(crate) const NOTE_CODEC_CRATE_METADATA: &str = "note-codec-crate"; +/// Defines the metadata namespace for compiler plugin configuration. +const NOTE_CODEC_NAMESPACE: &str = "midenc"; -/// Metadata field that contains the codec crate directory. -const NOTE_CODEC_CRATE_PATH: &str = "path"; +/// Names the metadata table that configures the author-side note codec. +const NOTE_CODEC_TABLE: &str = "note-codec"; + +/// Defines the dotted metadata table name for the author-side note codec. +pub(crate) const NOTE_CODEC_TABLE_NAME: &str = "midenc.note-codec"; + +/// Names the metadata key that contains the codec crate directory. +const NOTE_CODEC_CRATE_KEY: &str = "crate"; /// Rust target used to build note codec components; rustc links the cdylib as a /// Wasm component, so no separate encoding step is necessary. @@ -333,7 +339,9 @@ pub fn write_package_atomic( /// Returns true when project metadata declares an author-side note codec crate. pub(crate) fn has_project_note_codec(metadata: &miden_project::MetadataSet) -> bool { - metadata.get(NOTE_CODEC_CRATE_METADATA).is_some() + metadata + .get(NOTE_CODEC_NAMESPACE) + .is_some_and(|namespace| namespace.get(NOTE_CODEC_TABLE).is_some()) } /// Builds the optional note codec component declared by a project package. @@ -342,7 +350,6 @@ pub(crate) fn build_project_note_codec( project_manifest_path: &Path, note_project_dir: &Path, note_package: &MastPackage, - session: &Session, ) -> CompilerResult>> { let Some(codec_crate_dir) = note_codec_crate_dir(project_package.metadata(), project_manifest_path, note_project_dir)? @@ -350,7 +357,7 @@ pub(crate) fn build_project_note_codec( return Ok(None); }; - build_note_codec_component(&codec_crate_dir, note_package, session).map(Some) + build_note_codec_component(&codec_crate_dir, note_package).map(Some) } /// Reads the optional codec crate path from Miden project metadata. @@ -359,31 +366,46 @@ fn note_codec_crate_dir( project_manifest_path: &Path, project_dir: &Path, ) -> CompilerResult> { - let Some(codec_metadata) = metadata.get(NOTE_CODEC_CRATE_METADATA) else { + let Some(namespace) = metadata.get(NOTE_CODEC_NAMESPACE) else { return Ok(None); }; - let path = codec_metadata - .get(NOTE_CODEC_CRATE_PATH) - .ok_or_else(|| { - Report::msg(format!( - "`[package.metadata.{NOTE_CODEC_CRATE_METADATA}]` in '{}' must define a string \ - `{NOTE_CODEC_CRATE_PATH}`", - project_manifest_path.display() - )) - })? - .inner() - .as_str() - .ok_or_else(|| { - Report::msg(format!( - "`[package.metadata.{NOTE_CODEC_CRATE_METADATA}].{NOTE_CODEC_CRATE_PATH}` in '{}' \ - must be a string", + let Some(value) = namespace.get(NOTE_CODEC_TABLE) else { + return Ok(None); + }; + let table = value.inner().as_table().ok_or_else(|| { + Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}]` in '{}' must be a table, but it is {}", + project_manifest_path.display(), + value.inner().type_str() + )) + })?; + for key in table.keys() { + if key != NOTE_CODEC_CRATE_KEY { + return Err(Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}]` in '{}' has unknown key '{key}'; \ + the table accepts only '{NOTE_CODEC_CRATE_KEY}'", project_manifest_path.display() - )) - })?; + ))); + } + } + let path = table.get(NOTE_CODEC_CRATE_KEY).ok_or_else(|| { + Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}]` in '{}' must define a string \ + `{NOTE_CODEC_CRATE_KEY}`", + project_manifest_path.display() + )) + })?; + let path = path.as_str().ok_or_else(|| { + Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}].{NOTE_CODEC_CRATE_KEY}` in '{}' must be \ + a string", + project_manifest_path.display() + )) + })?; if path.is_empty() { return Err(Report::msg(format!( - "`[package.metadata.{NOTE_CODEC_CRATE_METADATA}].{NOTE_CODEC_CRATE_PATH}` in '{}' \ - must not be empty", + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}].{NOTE_CODEC_CRATE_KEY}` in '{}' must not \ + be empty", project_manifest_path.display() ))); } @@ -392,33 +414,33 @@ fn note_codec_crate_dir( let codec_crate_dir = codec_crate_dir.canonicalize().map_err(|error| { Report::msg(format!( "note codec crate '{}' does not exist; update \ - `[package.metadata.{NOTE_CODEC_CRATE_METADATA}].{NOTE_CODEC_CRATE_PATH}` in '{}': \ - {error}", + `[package.metadata.{NOTE_CODEC_TABLE_NAME}].{NOTE_CODEC_CRATE_KEY}` in '{}': {error}", codec_crate_dir.display(), project_manifest_path.display() )) })?; if !codec_crate_dir.is_dir() { return Err(Report::msg(format!( - "note codec crate path '{}' is not a directory", - codec_crate_dir.display() + "note codec crate path '{}' is not a directory; update \ + `[package.metadata.{NOTE_CODEC_TABLE_NAME}].{NOTE_CODEC_CRATE_KEY}` in '{}'", + codec_crate_dir.display(), + project_manifest_path.display() ))); } Ok(Some(codec_crate_dir)) } /// Builds and componentizes one author-side note codec crate. +/// +/// The codec crate contains the work directory at `/target/midenc.note-codec`. The +/// build does not use the session target directory. It never shares a build lock with the outer +/// build. This layout matches the VM event handler plugin. That plugin uses `/target/midenc.event-handlers`. fn build_note_codec_component( codec_crate_dir: &Path, note_package: &MastPackage, - session: &Session, ) -> CompilerResult> { - let session_target_dir = if session.options.target_dir.is_absolute() { - session.options.target_dir.clone() - } else { - session.options.current_dir.join(&session.options.target_dir) - }; - let work_dir = session_target_dir.join(&session.options.profile).join("note-codec"); + let work_dir = codec_crate_dir.join("target").join(NOTE_CODEC_TABLE_NAME); let staged_package = stage_note_package(&work_dir, codec_crate_dir, note_package)?; let manifest_path = codec_crate_dir.join("Cargo.toml"); if !manifest_path.is_file() { @@ -435,11 +457,7 @@ fn build_note_codec_component( } else { None }; - crate::rust::install_wasm32_target( - "wasip2", - toolchain.as_deref(), - session.options.cargo_offline, - )?; + crate::rust::install_wasm32_target("wasip2", toolchain.as_deref(), false)?; // Give nested Cargo a separate target directory. Sharing the outer lock can deadlock. let cargo_target_dir = work_dir.join("cargo-target"); @@ -468,7 +486,6 @@ fn build_note_codec_component( cargo.env_remove(variable); } cargo.stdout(Stdio::piped()).stderr(Stdio::inherit()); - cargo.args(apply_cargo_policy(session.options.cargo_locked, session.options.cargo_offline)); let manifest_path = manifest_path.canonicalize().map_err(|error| { Report::msg(format!( @@ -476,14 +493,8 @@ fn build_note_codec_component( manifest_path.display() )) })?; - let artifacts = crate::rust::run_cargo(cargo, cargo_path).map_err(|error| { - note_codec_cargo_error( - error, - &manifest_path, - session.options.cargo_locked, - session.options.cargo_offline, - ) - })?; + let artifacts = crate::rust::run_cargo(cargo, cargo_path) + .map_err(|error| note_codec_cargo_error(error, &manifest_path))?; let mut wasm_paths = artifacts .into_iter() .filter(|artifact| { @@ -648,29 +659,12 @@ pub(crate) fn apply_cargo_policy( .flatten() } -/// Attributes a nested Cargo failure and adds applicable lockfile and network guidance. -fn note_codec_cargo_error( - error: Report, - manifest_path: &Path, - locked: bool, - offline: bool, -) -> Report { - let mut guidance = - format!("the nested note codec build for '{}' failed: {error}", manifest_path.display()); - if locked || offline { - guidance.push_str(". If this failure is about the lockfile or the network:"); - if locked { - guidance.push_str( - " update and commit the codec workspace Cargo.lock before retrying with --locked.", - ); - } - if offline { - guidance.push_str( - " fetch the codec dependencies while online before retrying with --offline.", - ); - } - } - Report::msg(guidance) +/// Attributes a nested Cargo failure to the codec manifest. +fn note_codec_cargo_error(error: Report, manifest_path: &Path) -> Report { + Report::msg(format!( + "the nested note codec build for '{}' failed: {error}", + manifest_path.display() + )) } /// Verifies the component sandbox and the versioned codec interface export. @@ -982,8 +976,104 @@ pub fn parse_cargo_frontmatter( #[cfg(test)] mod tests { + use miden_assembly_syntax::debuginfo::Span; + use super::*; + /// Builds metadata with one value in the note codec namespace. + fn note_codec_metadata(value: miden_project::Value) -> miden_project::MetadataSet { + let mut namespace = miden_project::Metadata::default(); + namespace.insert(Span::unknown(Arc::::from(NOTE_CODEC_TABLE)), Span::unknown(value)); + let mut metadata = miden_project::MetadataSet::default(); + metadata.insert(Span::unknown(Arc::::from(NOTE_CODEC_NAMESPACE)), namespace); + metadata + } + + /// Builds a note codec metadata table from the given entries. + fn note_codec_table( + entries: impl IntoIterator, + ) -> miden_project::Value { + miden_project::Value::Table( + entries.into_iter().map(|(key, value)| (key.to_string(), value)).collect(), + ) + } + + #[test] + fn note_codec_crate_resolves_from_namespaced_metadata() { + let root = tempfile::TempDir::new().unwrap(); + let codec_crate = root.path().join("codec"); + fs::create_dir(&codec_crate).unwrap(); + let metadata = note_codec_metadata(note_codec_table([( + NOTE_CODEC_CRATE_KEY, + miden_project::Value::String("codec".to_string()), + )])); + + let resolved = + note_codec_crate_dir(&metadata, &root.path().join("miden-project.toml"), root.path()) + .unwrap(); + + assert_eq!(resolved, Some(codec_crate.canonicalize().unwrap())); + } + + #[test] + fn note_codec_metadata_requires_crate_key() { + let root = tempfile::TempDir::new().unwrap(); + let manifest_path = root.path().join("miden-project.toml"); + let metadata = note_codec_metadata(note_codec_table([])); + + let error = note_codec_crate_dir(&metadata, &manifest_path, root.path()) + .unwrap_err() + .to_string(); + + assert!(error.contains(&manifest_path.display().to_string())); + assert!(error.contains("must define a string `crate`")); + } + + #[test] + fn note_codec_metadata_rejects_unknown_key() { + let root = tempfile::TempDir::new().unwrap(); + let manifest_path = root.path().join("miden-project.toml"); + let metadata = note_codec_metadata(note_codec_table([ + (NOTE_CODEC_CRATE_KEY, miden_project::Value::String("codec".to_string())), + ("module", miden_project::Value::String("codec.wasm".to_string())), + ])); + + let error = note_codec_crate_dir(&metadata, &manifest_path, root.path()) + .unwrap_err() + .to_string(); + + assert!(error.contains(&manifest_path.display().to_string())); + assert!(error.contains("unknown key 'module'")); + } + + #[test] + fn note_codec_metadata_value_must_be_a_table() { + let root = tempfile::TempDir::new().unwrap(); + let manifest_path = root.path().join("miden-project.toml"); + let metadata = note_codec_metadata(miden_project::Value::String("codec".to_string())); + + let error = note_codec_crate_dir(&metadata, &manifest_path, root.path()) + .unwrap_err() + .to_string(); + + assert!(error.contains(&manifest_path.display().to_string())); + assert!(error.contains("must be a table")); + } + + #[test] + fn note_codec_metadata_without_namespace_is_absent() { + let root = tempfile::TempDir::new().unwrap(); + + let resolved = note_codec_crate_dir( + &miden_project::MetadataSet::default(), + &root.path().join("miden-project.toml"), + root.path(), + ) + .unwrap(); + + assert_eq!(resolved, None); + } + #[test] fn codec_interface_validation_compares_function_signatures() { let (expected_resolve, expected_id) = resolve_codec_interface(NOTE_CODEC_WIT); @@ -1053,13 +1143,9 @@ mod tests { #[test] fn note_codec_cargo_errors_are_always_attributed() { - let error = note_codec_cargo_error( - Report::msg("rustc failed"), - Path::new("/codec/Cargo.toml"), - false, - false, - ) - .to_string(); + let error = + note_codec_cargo_error(Report::msg("rustc failed"), Path::new("/codec/Cargo.toml")) + .to_string(); assert_eq!( error, @@ -1067,31 +1153,6 @@ mod tests { ); } - #[test] - fn note_codec_cargo_error_guidance_is_conditional() { - let locked = note_codec_cargo_error( - Report::msg("resolution failed"), - Path::new("/codec/Cargo.toml"), - true, - false, - ) - .to_string(); - assert!(locked.contains("If this failure is about the lockfile or the network:")); - assert!(locked.contains("retrying with --locked")); - assert!(!locked.contains("retrying with --offline")); - - let offline = note_codec_cargo_error( - Report::msg("resolution failed"), - Path::new("/codec/Cargo.toml"), - false, - true, - ) - .to_string(); - assert!(offline.contains("If this failure is about the lockfile or the network:")); - assert!(!offline.contains("retrying with --locked")); - assert!(offline.contains("retrying with --offline")); - } - #[test] fn note_codec_staging_is_content_addressed() { let root = tempfile::TempDir::new().unwrap(); @@ -1122,13 +1183,6 @@ mod tests { assert!(error.contains("contains different bytes"), "unexpected error: {error}"); } - #[test] - fn note_codec_cargo_policy_is_forwarded() { - let args = apply_cargo_policy(true, true).collect::>(); - - assert_eq!(args, ["--locked", "--offline"]); - } - #[test] fn oversized_codec_components_fail_producer_validation() { let oversized = vec![0u8; miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES + 1]; diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index a42ec14485..f2fbfe20db 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -22,7 +22,7 @@ use miden_mast_package::Package; use midenc_codegen_masm::{MasmComponent, intrinsics}; use midenc_session::{Session, diagnostics::Report}; -use crate::cargo::NOTE_CODEC_CRATE_METADATA; +use crate::cargo::NOTE_CODEC_TABLE_NAME; /// Apply the session's link inputs to `assembler` before a project is assembled with it. pub(crate) fn prepare_assembler( @@ -63,13 +63,12 @@ pub(crate) fn prepare_assembler( Ok(()) } -/// Attaches frontend metadata, advice-map data, and target-specific package sections. +/// Attaches frontend metadata, advice-map data, and target-specific sections after assembly. pub(crate) fn post_process_package( package: &mut Package, component: &MasmComponent, sections: &midenc_frontend_wasm_metadata::PackageSections, context: &TargetAssemblyContext<'_>, - session: &Session, ) -> Result<(), Report> { use miden_assembly::serde::Serializable; use miden_mast_package::{Section, SectionId}; @@ -106,7 +105,7 @@ pub(crate) fn post_process_package( if has_note_codec && context.target.ty == TargetType::Note { // Run after schema and kernel attachment. The codec stages this package state and hashes it. - attach_note_codec(package, context, session)?; + attach_note_codec(package, context)?; } Ok(()) @@ -124,15 +123,14 @@ fn validate_note_codec_declaration( if has_note_codec && !package_has_note_target { return Err(Report::msg(format!( - "`[package.metadata.{NOTE_CODEC_CRATE_METADATA}]` requires a note target, but the \ - package that contains target '{target_name}' defines no note target" + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}]` requires a note target, but the package \ + that contains target '{target_name}' defines no note target" ))); } if has_note_codec && target_type == TargetType::Note && !has_note_storage_schema { return Err(Report::msg(format!( - "note target '{target_name}' declares \ - `[package.metadata.{NOTE_CODEC_CRATE_METADATA}]` but emitted no note storage schema; \ - add one named-field `#[note]` struct" + "note target '{target_name}' declares `[package.metadata.{NOTE_CODEC_TABLE_NAME}]` \ + but emitted no note storage schema; add one named-field `#[note]` struct" ))); } Ok(()) @@ -151,18 +149,16 @@ fn package_has_note_target(package: &midenc_session::miden_project::Package) -> .any(|target| target.inner().ty == TargetType::Note) } -/// Build and attach the note codec declared by the current project package. +/// Builds and attaches the note codec declared by the current project package. fn attach_note_codec( package: &mut Package, context: &TargetAssemblyContext<'_>, - session: &Session, ) -> Result<(), Report> { let Some(component) = crate::cargo::build_project_note_codec( context.package.as_ref(), context.manifest_path, context.project_root.as_ref(), package, - session, )? else { return Ok(()); diff --git a/midenc-compile/src/pipeline/backend.rs b/midenc-compile/src/pipeline/backend.rs index 5e238cc6dc..e22a5c123e 100644 --- a/midenc-compile/src/pipeline/backend.rs +++ b/midenc-compile/src/pipeline/backend.rs @@ -832,7 +832,6 @@ mod tests { &found.component, &found.sections, cx.assembly(), - &cx.session(), ) } diff --git a/midenc-compile/src/pipeline/frontends/hir.rs b/midenc-compile/src/pipeline/frontends/hir.rs index cf55d39721..0b62e5a1ad 100644 --- a/midenc-compile/src/pipeline/frontends/hir.rs +++ b/midenc-compile/src/pipeline/frontends/hir.rs @@ -394,7 +394,6 @@ impl Frontend for HirFrontend { &found.component, &found.sections, cx.assembly(), - &cx.session(), ) } diff --git a/midenc-compile/src/pipeline/frontends/rust.rs b/midenc-compile/src/pipeline/frontends/rust.rs index 0b14a51ea5..0b6f7635b0 100644 --- a/midenc-compile/src/pipeline/frontends/rust.rs +++ b/midenc-compile/src/pipeline/frontends/rust.rs @@ -1653,7 +1653,6 @@ impl Frontend for RustProjectFrontend { &found.component, &found.sections, cx.assembly(), - &cx.session(), ) } } diff --git a/midenc-compile/src/pipeline/frontends/wasm.rs b/midenc-compile/src/pipeline/frontends/wasm.rs index b2e22b22e2..9a5fc48fd7 100644 --- a/midenc-compile/src/pipeline/frontends/wasm.rs +++ b/midenc-compile/src/pipeline/frontends/wasm.rs @@ -592,7 +592,6 @@ impl Frontend for WasmFrontend { &found.component, &found.sections, cx.assembly(), - &cx.session(), ) } diff --git a/midenc-compile/src/pipeline/seed.rs b/midenc-compile/src/pipeline/seed.rs index b7d43455e1..f653af7cd7 100644 --- a/midenc-compile/src/pipeline/seed.rs +++ b/midenc-compile/src/pipeline/seed.rs @@ -439,7 +439,6 @@ impl Frontend for SeedFrontend { &found.component, &found.sections, cx.assembly(), - &cx.session(), ) } @@ -606,7 +605,6 @@ mod tests { &found.component, &found.sections, cx.assembly(), - &cx.session(), ) } diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 29861fffba..4942572eab 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -20,11 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 macros generate host types from a note package, `AuthorTypeCodec` defines text conversion and validation, `#[note_codec]` registers each custom type, and `export_codecs!` exports the registered codecs as a component. Add this package-level metadata to `miden-project.toml` to - enable the codec build; `path` is relative to that manifest: + enable the codec build. The `crate` directory is relative to that manifest: ```toml - [package.metadata.note-codec-crate] - path = "../my-note-codec" + [package.metadata.midenc.note-codec] + crate = "../my-note-codec" ``` - Added the dependency-free `miden-note-codec-wit` crate as the canonical source for the note codec component WIT contract. diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs index e1cc76024b..92cd0036f8 100644 --- a/tools/cargo-miden/tests/dex_note_codec_build.rs +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -28,7 +28,7 @@ fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { .format_timestamp(None) .try_init(); - // Clear the outer override so the nested example build uses its own target layout. + // Clear the outer override. The example package then uses its own target layout. let _restore_environment = RestoreEnvironment::new(["CARGO_TARGET_DIR"]); unsafe { env::remove_var("CARGO_TARGET_DIR"); @@ -46,6 +46,15 @@ fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { .expect("cargo miden build for dex-note failed") .expect("expected BuildCommandOutput") .unwrap_build_output(); + let codec_work_dir = workspace.join("examples/dex-note-codec/target/midenc.note-codec"); + assert!( + codec_work_dir.join("package-cache").is_dir(), + "the codec package cache is outside the codec crate" + ); + assert!( + codec_work_dir.join("cargo-target").is_dir(), + "the codec Cargo target directory is outside the codec crate" + ); assert_eq!(output.len(), 1, "expected one dex-note package artifact, got {output:?}"); let package = Package::deserialize_from_file(&output[0]) .expect("failed to read the built dex-note package"); From eb0af1e94f18cacc2363e59f4afa7187b54c483c Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 8 Sep 2026 16:06:05 +0300 Subject: [PATCH 33/43] fix: harden the bundled note codec sandbox against hostile components The codec consumer ran untrusted components under the engine's default feature set, with private limit constants, no bound on the work that compilation and instantiation cost before fuel applies, and one undifferentiated error for every call failure. This ports the policy ideas from the VM's Wasm event-handler runner onto the wasmtime consumer. Every Wasm proposal is now set by name: the wasip2 defaults stay on, floats stay on with NaN canonicalization, and everything else is off. A new feature-free structural validator caps functions, globals, tables, memories, segments, imports, exports, and signature widths per core module, the number and nesting of core modules, and the average function size, with the numbers wasmi's strict limits use; the producer applies the same function at build time, so a component every consumer would refuse fails the author's build. Runtime limits move into a `CodecLimits` struct with today's values as defaults and a `load_from_package_with_limits` entry point, documented as host policy that a package cannot set. Call failures carry a `CodecFailure` class, with limit hits recorded by a resource-limiter wrapper instead of parsed from engine messages. The nested codec build pins `RUSTFLAGS` to disable SIMD after scrubbing inherited flags, so a codec crate's own cargo config cannot enable a feature every consumer rejects. --- Cargo.lock | 2 + examples/dex-note-codec/Cargo.lock | 19 +- midenc-compile/src/cargo.rs | 125 ++++++-- sdk/CHANGELOG.md | 4 +- sdk/note-schema/Cargo.toml | 2 + sdk/note-schema/src/codec_component.rs | 411 ++++++++++++++++++++----- sdk/note-schema/src/codec_structure.rs | 348 +++++++++++++++++++++ sdk/note-schema/src/error.rs | 38 ++- sdk/note-schema/src/lib.rs | 9 +- sdk/note-schema/src/schema.rs | 4 + 10 files changed, 846 insertions(+), 116 deletions(-) create mode 100644 sdk/note-schema/src/codec_structure.rs diff --git a/Cargo.lock b/Cargo.lock index 21a903c273..ba226f4d7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3689,7 +3689,9 @@ dependencies = [ "midenc-integration-test-support", "tempfile", "toml 1.1.4+spec-1.1.0", + "wasmparser 0.248.0", "wasmtime", + "wat", "wit-parser 0.247.0", ] diff --git a/examples/dex-note-codec/Cargo.lock b/examples/dex-note-codec/Cargo.lock index 054ca18e4b..afc6ccfb2e 100644 --- a/examples/dex-note-codec/Cargo.lock +++ b/examples/dex-note-codec/Cargo.lock @@ -1977,6 +1977,7 @@ dependencies = [ "miden-protocol", "midenc-frontend-wasm-metadata", "toml", + "wasmparser 0.248.0", "wit-parser", ] @@ -3917,7 +3918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.247.0", ] [[package]] @@ -3929,7 +3930,7 @@ dependencies = [ "anyhow", "indexmap 2.14.0", "wasm-encoder", - "wasmparser", + "wasmparser 0.247.0", ] [[package]] @@ -3944,6 +3945,16 @@ dependencies = [ "semver 1.0.28", ] +[[package]] +name = "wasmparser" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a" +dependencies = [ + "bitflags 2.13.1", + "semver 1.0.28", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -4108,7 +4119,7 @@ dependencies = [ "serde_json", "wasm-encoder", "wasm-metadata", - "wasmparser", + "wasmparser 0.247.0", "wit-parser", ] @@ -4128,7 +4139,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.247.0", ] [[package]] diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 2f011e79b4..8f90be871d 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -461,30 +461,14 @@ fn build_note_codec_component( // Give nested Cargo a separate target directory. Sharing the outer lock can deadlock. let cargo_target_dir = work_dir.join("cargo-target"); - let mut cargo = Command::new(cargo_path); - if let Some(toolchain) = toolchain.as_deref() { - cargo.arg(format!("+{toolchain}")); - } - cargo - .current_dir(codec_crate_dir) - .arg("build") - .arg("--manifest-path") - .arg(&manifest_path) - .arg("--lib") - // The codec is a host-side wasmtime artifact; a dev-profile cdylib carries debug - // info far past the consumer size limit, so every Miden profile builds it release. - .arg("--release") - .arg("--target") - .arg(NOTE_CODEC_TARGET) - .arg("--target-dir") - .arg(&cargo_target_dir) - .arg("--message-format") - .arg("json-render-diagnostics") - // Let `from_project!` find the staged note package during codec macro expansion. - .env(package_cache::PACKAGE_CACHE_ENV, &staged_package.cache_dir); - for &variable in NESTED_CARGO_SCRUB_ENV { - cargo.env_remove(variable); - } + let mut cargo = note_codec_cargo_command( + cargo_path, + toolchain.as_deref(), + codec_crate_dir, + &manifest_path, + &cargo_target_dir, + &staged_package.cache_dir, + ); cargo.stdout(Stdio::piped()).stderr(Stdio::inherit()); let manifest_path = manifest_path.canonicalize().map_err(|error| { @@ -535,6 +519,45 @@ fn build_note_codec_component( Ok(component) } +/// Builds the nested Cargo command that compiles one note codec crate. +fn note_codec_cargo_command( + cargo_path: &Path, + toolchain: Option<&str>, + codec_crate_dir: &Path, + manifest_path: &Path, + cargo_target_dir: &Path, + package_cache_dir: &Path, +) -> Command { + let mut cargo = Command::new(cargo_path); + if let Some(toolchain) = toolchain { + cargo.arg(format!("+{toolchain}")); + } + cargo + .current_dir(codec_crate_dir) + .arg("build") + .arg("--manifest-path") + .arg(manifest_path) + .arg("--lib") + // The codec is a host-side wasmtime artifact; a dev-profile cdylib carries debug + // info far past the consumer size limit, so every Miden profile builds it release. + .arg("--release") + .arg("--target") + .arg(NOTE_CODEC_TARGET) + .arg("--target-dir") + .arg(cargo_target_dir) + .arg("--message-format") + .arg("json-render-diagnostics") + // Let `from_project!` find the staged note package during codec macro expansion. + .env(package_cache::PACKAGE_CACHE_ENV, package_cache_dir); + for &variable in NESTED_CARGO_SCRUB_ENV { + cargo.env_remove(variable); + } + // Pin the guest rustflags after the scrub. A codec crate cannot enable a Wasm feature + // that every consumer rejects, whatever its own cargo config says. + cargo.env("RUSTFLAGS", miden_note_schema::NOTE_CODEC_GUEST_RUSTFLAGS); + cargo +} + /// Stages the current package in a content-addressed package-cache directory. fn stage_note_package( work_dir: &Path, @@ -678,6 +701,12 @@ fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES, ))); } + // The structural caps are fixed policy, so the producer applies the consumer rules here. + miden_note_schema::validate_note_codec_structure(component).map_err(|error| { + Report::msg(format!( + "note codec component fails the structural limits consumers enforce: {error}" + )) + })?; let DecodedWasm::Component(resolve, world_id) = wit_component::decode(component).map_err(|error| { Report::msg(format!("failed to decode the encoded note codec component: {error}")) @@ -976,6 +1005,8 @@ pub fn parse_cargo_frontmatter( #[cfg(test)] mod tests { + use std::ffi::OsStr; + use miden_assembly_syntax::debuginfo::Span; use super::*; @@ -1194,6 +1225,52 @@ mod tests { ); } + #[test] + fn structurally_oversized_codec_components_fail_producer_validation() { + let globals = "(global i32 (i32.const 0))".repeat(1_001); + let component = wat::parse_str(format!("(component (core module {globals}))")).unwrap(); + + let error = validate_note_codec_component(&component).unwrap_err().to_string(); + + assert!( + error.contains("fails the structural limits consumers enforce"), + "unexpected error: {error}" + ); + assert!(error.contains("1001 globals"), "the observed count is not named: {error}"); + } + + #[test] + fn note_codec_cargo_command_pins_the_guest_build_flags() { + let command = note_codec_cargo_command( + Path::new("cargo"), + None, + Path::new("/codec"), + Path::new("/codec/Cargo.toml"), + Path::new("/codec/target/cargo-target"), + Path::new("/codec/target/package-cache"), + ); + + let environment = command.get_envs().collect::>(); + assert!( + environment.contains(&( + OsStr::new("RUSTFLAGS"), + Some(OsStr::new(miden_note_schema::NOTE_CODEC_GUEST_RUSTFLAGS)), + )), + "the guest rustflags are not pinned: {environment:?}" + ); + assert!( + environment.contains(&(OsStr::new("CARGO_ENCODED_RUSTFLAGS"), None)), + "the outer rustflags are not scrubbed: {environment:?}" + ); + + let args = command.get_args().collect::>(); + let target = args + .windows(2) + .find(|window| window[0] == OsStr::new("--target")) + .expect("the nested build does not select a target"); + assert_eq!(target[1], OsStr::new(NOTE_CODEC_TARGET)); + } + /// Resolves the codec interface from one complete WIT document. fn resolve_codec_interface(wit: &str) -> (Resolve, wit_parser::InterfaceId) { let mut resolve = Resolve::default(); diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 4942572eab..7b3a3e72fb 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -15,7 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 so no host capability is reachable from codec code. - Added optional `codec-component` support to the new `miden-note-schema` host crate. It can load author-defined note codecs from a package without adding Wasmtime to the default feature - set or the guest SDK dependency graph. + set or the guest SDK dependency graph. Consumers run codecs under an explicit Wasm feature + policy, fixed structural caps that the producer also enforces at build time, and host-policy + `CodecLimits`; call failures report a `CodecFailure` class. - Added the `miden-note-codec` author crate. Its codec-side `from_project!` and `from_package!` macros generate host types from a note package, `AuthorTypeCodec` defines text conversion and validation, `#[note_codec]` registers each custom type, and `export_codecs!` exports the diff --git a/sdk/note-schema/Cargo.toml b/sdk/note-schema/Cargo.toml index 8a4b9ff5a8..2c827f3183 100644 --- a/sdk/note-schema/Cargo.toml +++ b/sdk/note-schema/Cargo.toml @@ -27,6 +27,7 @@ miden-mast-package = { workspace = true, features = ["std"] } miden-protocol = { workspace = true, features = ["std"] } midenc-frontend-wasm-metadata.workspace = true toml.workspace = true +wasmparser.workspace = true wit-parser.workspace = true wasmtime = { workspace = true, optional = true } @@ -35,3 +36,4 @@ miden-core.workspace = true miden-note-codec-wit.workspace = true midenc-integration-test-support.workspace = true tempfile.workspace = true +wat.workspace = true diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 3d034b2982..6caac68e19 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -6,56 +6,85 @@ use miden_field::Felt; use miden_mast_package::Package; use midenc_frontend_wasm_metadata::{PACKAGE_NOTE_CODEC_SECTION_ID, package_note_codec_section_id}; use wasmtime::{ - Config, Engine, Store, StoreLimits, StoreLimitsBuilder, + Config, Engine, ResourceLimiter, Store, StoreLimits, StoreLimitsBuilder, Trap, component::{Component, Linker}, }; -use crate::{CodecRegistry, ConsumerTypeCodec, Error, NoteStorageSchema, Result}; - -/// Maximum Wasm instructions available to one codec operation. -const CALL_FUEL: u64 = 10_000_000; - -/// Maximum bytes accepted for one untrusted note codec component before Wasmtime compilation. -const MAX_COMPONENT_BYTES: usize = crate::schema::MAX_NOTE_CODEC_COMPONENT_BYTES; - -/// Maximum bytes available to one codec component linear memory. -const MAX_COMPONENT_MEMORY_BYTES: usize = 16 * 1024 * 1024; - -/// Maximum elements available to each codec component table. -const MAX_COMPONENT_TABLE_ELEMENTS: usize = 4_096; - -/// Maximum FQNs accepted from `supported-types`. -const MAX_SUPPORTED_TYPES: usize = 128; - -/// Maximum bytes accepted in one reported FQN. -const MAX_FQN_BYTES: usize = 512; - -/// Maximum felts accepted from `parse`. -const MAX_RETURNED_FELTS: usize = 4_096; +use crate::{ + CodecFailure, CodecRegistry, ConsumerTypeCodec, Error, NoteStorageSchema, Result, + validate_note_codec_structure, +}; -/// Maximum bytes accepted in one component-returned string. -const MAX_RETURNED_STRING_BYTES: usize = 16 * 1024; +/// Maximum bytes of stack available to one component call. +const MAX_WASM_STACK_BYTES: usize = 512 * 1024; wasmtime::component::bindgen!({ path: "wit", world: "note-codec", }); +/// Runtime limits a host applies to bundled note codecs. Limits are host policy. +/// A package carries no limit values and cannot raise them. Hosts in one +/// deployment should run identical limits, so a codec behaves the same everywhere. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CodecLimits { + /// Fuel budget for one codec call, about one unit per Wasm instruction. + pub fuel: u64, + /// Cap on the guest linear memory in bytes. + pub max_memory_bytes: usize, + /// Cap on the total table elements. + pub max_table_elements: usize, + /// Cap on the component size in bytes. + pub max_component_bytes: usize, + /// Cap on the number of types one component may report. + pub max_supported_types: usize, + /// Cap on one reported type name in bytes. + pub max_fqn_bytes: usize, + /// Cap on the felts one `parse` call may return. + pub max_returned_felts: usize, + /// Cap on one returned string in bytes. + pub max_returned_string_bytes: usize, +} + +impl Default for CodecLimits { + fn default() -> Self { + Self { + fuel: 10_000_000, + max_memory_bytes: 16 * 1024 * 1024, + max_table_elements: 4_096, + max_component_bytes: crate::schema::MAX_NOTE_CODEC_COMPONENT_BYTES, + max_supported_types: 128, + max_fqn_bytes: 512, + max_returned_felts: 4_096, + max_returned_string_bytes: 16 * 1024, + } + } +} + impl CodecRegistry { - /// Loads the note codec component from a package and registers all reported types. + /// Loads the note codec component from a package under the default limits. pub fn load_from_package(package: &Package) -> Result { + Self::load_from_package_with_limits(package, CodecLimits::default()) + } + + /// Loads the note codec component from a package and registers all reported types. + pub fn load_from_package_with_limits(package: &Package, limits: CodecLimits) -> Result { let schema = NoteStorageSchema::from_package(package)?; let bytes = crate::section::unique_package_section( package, package_note_codec_section_id(), PACKAGE_NOTE_CODEC_SECTION_ID, )?; - Self::load_from_component(bytes, &schema.custom_type_fqns()) + Self::load_from_component(bytes, &schema.custom_type_fqns(), limits) } /// Loads a note codec component whose only imports are stubbed WASI interfaces. - fn load_from_component(bytes: &[u8], custom_type_fqns: &HashSet) -> Result { - let runtime = Arc::new(ComponentRuntime::new(bytes)?); + fn load_from_component( + bytes: &[u8], + custom_type_fqns: &HashSet, + limits: CodecLimits, + ) -> Result { + let runtime = Arc::new(ComponentRuntime::new(bytes, limits)?); let supported_types = runtime.supported_types()?; let mut registry = Self::default(); validate_reported_fqns(&supported_types, custom_type_fqns, ®istry)?; @@ -78,11 +107,66 @@ impl CodecRegistry { struct ComponentRuntime { engine: Engine, component: Component, + limits: CodecLimits, } /// Store state that owns the component resource limits. struct ComponentStore { limits: StoreLimits, + /// Set when a memory or table growth was refused, so a failed call reports its class. + limit_hit: bool, +} + +// The inner limiter decides every question. This wrapper only records that a growth was +// refused, which a call failure reports as `CodecFailure::LimitExceeded`. +impl ResourceLimiter for ComponentStore { + fn memory_growing( + &mut self, + current: usize, + desired: usize, + maximum: Option, + ) -> wasmtime::Result { + let allowed = self.limits.memory_growing(current, desired, maximum); + if !matches!(allowed, Ok(true)) { + self.limit_hit = true; + } + allowed + } + + fn memory_grow_failed(&mut self, error: wasmtime::Error) -> wasmtime::Result<()> { + self.limit_hit = true; + self.limits.memory_grow_failed(error) + } + + fn table_growing( + &mut self, + current: usize, + desired: usize, + maximum: Option, + ) -> wasmtime::Result { + let allowed = self.limits.table_growing(current, desired, maximum); + if !matches!(allowed, Ok(true)) { + self.limit_hit = true; + } + allowed + } + + fn table_grow_failed(&mut self, error: wasmtime::Error) -> wasmtime::Result<()> { + self.limit_hit = true; + self.limits.table_grow_failed(error) + } + + fn instances(&self) -> usize { + self.limits.instances() + } + + fn tables(&self) -> usize { + self.limits.tables() + } + + fn memories(&self) -> usize { + self.limits.memories() + } } /// One isolated codec component call context. @@ -92,17 +176,43 @@ struct ComponentInstance { } impl ComponentRuntime { - /// Compiles a component with fuel accounting enabled. - fn new(bytes: &[u8]) -> Result { - ensure_component_byte_limit(bytes.len())?; + /// Compiles a component under the explicit engine policy and the given limits. + fn new(bytes: &[u8], limits: CodecLimits) -> Result { + ensure_component_byte_limit(bytes.len(), limits.max_component_bytes)?; + validate_note_codec_structure(bytes)?; let mut config = Config::new(); config.wasm_component_model(true); config.consume_fuel(true); + // Proposals the wasm32-wasip2 target emits by default. + config.wasm_bulk_memory(true); + config.wasm_multi_value(true); + // Validation policy: every other proposal is off by name. The engine defaults change + // between versions, and they decide which components load on every host. + config.wasm_simd(false); + config.wasm_relaxed_simd(false); + config.wasm_multi_memory(false); + config.wasm_memory64(false); + config.wasm_tail_call(false); + config.wasm_extended_const(false); + config.wasm_custom_page_sizes(false); + config.wasm_wide_arithmetic(false); + config.wasm_shared_everything_threads(false); + config.wasm_stack_switching(false); + config.wasm_exceptions(false); + // Floats stay on: codecs parse and format decimal text. NaN canonicalization keeps + // float results identical across hosts. + config.cranelift_nan_canonicalization(true); + config.max_wasm_stack(MAX_WASM_STACK_BYTES); + config.wasm_backtrace(false); let engine = Engine::new(&config) .map_err(|error| component_error("create the Wasmtime engine", error))?; let component = Component::new(&engine, bytes) .map_err(|error| component_error("compile the note codec component", error))?; - Ok(Self { engine, component }) + Ok(Self { + engine, + component, + limits, + }) } /// Instantiates the component with trapping WASI stubs and fresh per-call limits. @@ -113,11 +223,14 @@ impl ComponentRuntime { linker .define_unknown_imports_as_traps(&self.component) .map_err(|error| component_error("stub the note codec imports", error))?; - let limits = component_store_limits(); - let mut store = Store::new(&self.engine, ComponentStore { limits }); - store.limiter(|state| &mut state.limits); + let state = ComponentStore { + limits: component_store_limits(&self.limits), + limit_hit: false, + }; + let mut store = Store::new(&self.engine, state); + store.limiter(|state| state as &mut dyn ResourceLimiter); store - .set_fuel(CALL_FUEL) + .set_fuel(self.limits.fuel) .map_err(|error| component_error("set the note codec fuel budget", error))?; let bindings = NoteCodec::instantiate(&mut store, &self.component, &linker) .map_err(|error| component_error("instantiate the note codec", error))?; @@ -132,24 +245,25 @@ impl ComponentRuntime { .miden_note_codec_codec() .call_supported_types(&mut instance.store) .map_err(|error| component_error("call `supported-types`", error))?; - if fqns.len() > MAX_SUPPORTED_TYPES { + if fqns.len() > self.limits.max_supported_types { return Err(Error::new(format!( - "note codec component reported {} types; the limit is {MAX_SUPPORTED_TYPES}", - fqns.len() + "note codec component reported {} types; the limit is {}", + fqns.len(), + self.limits.max_supported_types ))); } for fqn in &fqns { - ensure_returned_string_limit("type FQN", fqn, MAX_FQN_BYTES)?; + ensure_returned_string_limit("type FQN", fqn, self.limits.max_fqn_bytes)?; } Ok(fqns) } } /// Builds the resource limits attached to every isolated component call store. -fn component_store_limits() -> StoreLimits { +fn component_store_limits(limits: &CodecLimits) -> StoreLimits { StoreLimitsBuilder::new() - .memory_size(MAX_COMPONENT_MEMORY_BYTES) - .table_elements(MAX_COMPONENT_TABLE_ELEMENTS) + .memory_size(limits.max_memory_bytes) + .table_elements(limits.max_table_elements) .instances(32) .tables(32) .memories(1) @@ -158,11 +272,10 @@ fn component_store_limits() -> StoreLimits { } /// Rejects oversized component bytes before validation or JIT compilation begins. -fn ensure_component_byte_limit(byte_len: usize) -> Result<()> { - if byte_len > MAX_COMPONENT_BYTES { +fn ensure_component_byte_limit(byte_len: usize, limit: usize) -> Result<()> { + if byte_len > limit { return Err(Error::new(format!( - "note codec component is {byte_len} bytes; the pre-compilation limit is \ - {MAX_COMPONENT_BYTES}" + "note codec component is {byte_len} bytes; the pre-compilation limit is {limit}" ))); } Ok(()) @@ -182,9 +295,41 @@ impl ComponentCodec { call: impl FnOnce(&NoteCodec, &mut Store) -> wasmtime::Result, ) -> Result { let mut instance = self.runtime.instantiate()?; - call(&instance.bindings, &mut instance.store).map_err(|error| { - component_error(&format!("call `{operation}` for codec `{}`", self.fqn), error) - }) + match call(&instance.bindings, &mut instance.store) { + Ok(value) => Ok(value), + Err(error) => Err(self.call_failure(operation, &instance.store, error)), + } + } + + /// Classifies why one component call did not return a value. + fn call_failure( + &self, + operation: &str, + store: &Store, + error: wasmtime::Error, + ) -> Error { + let fqn = &self.fqn; + if store.data().limit_hit { + Error::codec( + CodecFailure::LimitExceeded, + format!("note codec `{fqn}` exceeded a resource limit in `{operation}`"), + ) + } else if error.downcast_ref::() == Some(&Trap::OutOfFuel) { + Error::codec( + CodecFailure::OutOfFuel, + format!("note codec `{fqn}` ran out of fuel in `{operation}`"), + ) + } else { + Error::codec( + CodecFailure::Trapped, + format!("note codec `{fqn}` trapped in `{operation}`: {error:#}"), + ) + } + } + + /// Returns the limits the host applies to this codec. + fn limits(&self) -> &CodecLimits { + &self.runtime.limits } } @@ -196,11 +341,15 @@ impl ConsumerTypeCodec for ComponentCodec { let values = match result { Ok(values) => values, Err(message) => { - ensure_returned_string_limit("codec error", &message, MAX_RETURNED_STRING_BYTES)?; + ensure_returned_string_limit( + "codec error", + &message, + self.limits().max_returned_string_bytes, + )?; return Err(codec_rejection("parse", &self.fqn, message)); } }; - ensure_returned_felt_limit(&self.fqn, values.len())?; + ensure_returned_felt_limit(&self.fqn, values.len(), self.limits().max_returned_felts)?; component_values_to_felts(&self.fqn, &values) } @@ -212,11 +361,19 @@ impl ConsumerTypeCodec for ComponentCodec { let display = match result { Ok(display) => display, Err(message) => { - ensure_returned_string_limit("codec error", &message, MAX_RETURNED_STRING_BYTES)?; + ensure_returned_string_limit( + "codec error", + &message, + self.limits().max_returned_string_bytes, + )?; return Err(codec_rejection("display", &self.fqn, message)); } }; - ensure_returned_string_limit("display value", &display, MAX_RETURNED_STRING_BYTES)?; + ensure_returned_string_limit( + "display value", + &display, + self.limits().max_returned_string_bytes, + )?; Ok(display) } @@ -228,7 +385,11 @@ impl ConsumerTypeCodec for ComponentCodec { match result { Ok(()) => Ok(()), Err(message) => { - ensure_returned_string_limit("codec error", &message, MAX_RETURNED_STRING_BYTES)?; + ensure_returned_string_limit( + "codec error", + &message, + self.limits().max_returned_string_bytes, + )?; Err(codec_rejection("validate", &self.fqn, message)) } } @@ -278,10 +439,10 @@ fn ensure_returned_string_limit(kind: &str, value: &str, limit: usize) -> Result } /// Enforces the structural felt count cap on one `parse` result. -fn ensure_returned_felt_limit(fqn: &str, count: usize) -> Result<()> { - if count > MAX_RETURNED_FELTS { +fn ensure_returned_felt_limit(fqn: &str, count: usize, limit: usize) -> Result<()> { + if count > limit { return Err(Error::new(format!( - "codec `{fqn}` returned {count} felts from `parse`; the limit is {MAX_RETURNED_FELTS}" + "codec `{fqn}` returned {count} felts from `parse`; the limit is {limit}" ))); } Ok(()) @@ -310,11 +471,14 @@ fn component_error(action: &str, error: impl core::fmt::Display) -> Error { /// Creates a host error for an author codec rejection. fn codec_rejection(operation: &str, fqn: &str, message: String) -> Error { - Error::new(format!("codec `{fqn}` rejected `{operation}`: {message}")) + Error::codec( + CodecFailure::Rejected, + format!("codec `{fqn}` rejected `{operation}`: {message}"), + ) } #[cfg(test)] -mod tests { +pub(crate) mod tests { use std::{ env, fs, path::{Path, PathBuf}, @@ -335,7 +499,6 @@ mod tests { }; use midenc_integration_test_support::wasm_target_is_installed; use tempfile::TempDir; - use wasmtime::ResourceLimiter; use super::*; @@ -394,32 +557,67 @@ package miden:base@1.0.0 { #[test] fn oversized_component_is_rejected_before_compilation() { - let bytes = vec![0; MAX_COMPONENT_BYTES + 1]; - let error = ComponentRuntime::new(&bytes) + let limits = CodecLimits::default(); + let bytes = vec![0; limits.max_component_bytes + 1]; + let error = ComponentRuntime::new(&bytes, limits.clone()) .err() .expect("an oversized component must fail before compilation") .to_string(); assert!(error.contains("pre-compilation limit")); - assert!(error.contains(&(MAX_COMPONENT_BYTES + 1).to_string())); - assert!(error.contains(&MAX_COMPONENT_BYTES.to_string())); + assert!(error.contains(&(limits.max_component_bytes + 1).to_string())); + assert!(error.contains(&limits.max_component_bytes.to_string())); + } + + #[test] + fn default_limits_match_the_published_policy() { + let limits = CodecLimits::default(); + + assert_eq!(limits.fuel, 10_000_000); + assert_eq!(limits.max_memory_bytes, 16 * 1024 * 1024); + assert_eq!(limits.max_table_elements, 4_096); + assert_eq!(limits.max_component_bytes, crate::MAX_NOTE_CODEC_COMPONENT_BYTES); + assert_eq!(limits.max_supported_types, 128); + assert_eq!(limits.max_fqn_bytes, 512); + assert_eq!(limits.max_returned_felts, 4_096); + assert_eq!(limits.max_returned_string_bytes, 16 * 1024); } #[test] fn component_store_limits_bound_table_elements() { - let mut limits = component_store_limits(); + let policy = CodecLimits::default(); + let mut limits = component_store_limits(&policy); assert!( - ResourceLimiter::table_growing(&mut limits, 0, MAX_COMPONENT_TABLE_ELEMENTS, None,) + ResourceLimiter::table_growing(&mut limits, 0, policy.max_table_elements, None) .unwrap() ); let error = - ResourceLimiter::table_growing(&mut limits, 0, MAX_COMPONENT_TABLE_ELEMENTS + 1, None) + ResourceLimiter::table_growing(&mut limits, 0, policy.max_table_elements + 1, None) .unwrap_err() .to_string(); assert!(error.contains("growing table"), "unexpected table-limit error: {error}"); } + #[test] + fn simd_components_do_not_load() { + let component = wat::parse_str( + r#"(component + (core module + (func (export "simd") + v128.const i32x4 0 0 0 0 + drop)))"#, + ) + .unwrap(); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .err() + .expect("a SIMD component must not load") + .to_string(); + + assert!(error.contains("SIMD"), "unexpected engine policy error: {error}"); + } + #[test] fn nonstandard_embedded_core_type_is_author_codec_eligible() { let schema = NoteStorageSchema::from_wit_text(EMBEDDED_CORE_SCHEMA).unwrap(); @@ -469,23 +667,63 @@ package miden:base@1.0.0 { let registry = CodecRegistry::load_from_component( &build_fixture_component(), &schema.custom_type_fqns(), + CodecLimits::default(), ) .unwrap(); let codec = registry.codec(FIXTURE_FQN).unwrap(); - assert!(codec.parse("trap").unwrap_err().to_string().contains("call `parse`")); + let trap = codec.parse("trap").unwrap_err(); + assert!(trap.to_string().contains("trapped in `parse`"), "unexpected error: {trap}"); + assert_eq!(trap.codec_failure(), Some(CodecFailure::Trapped)); assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); - let oversized_input = "x".repeat(MAX_COMPONENT_MEMORY_BYTES + 1); - assert!(codec.parse(&oversized_input).is_err()); - assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); - - let fuel_error = codec.parse("loop").unwrap_err().to_string(); + let oversized_input = "x".repeat(CodecLimits::default().max_memory_bytes + 1); + let limit = codec.parse(&oversized_input).unwrap_err(); assert!( - fuel_error.contains("fuel") || fuel_error.contains("interrupt"), - "unexpected budget error: {fuel_error}" + limit.to_string().contains("exceeded a resource limit in `parse`"), + "unexpected error: {limit}" ); + assert_eq!(limit.codec_failure(), Some(CodecFailure::LimitExceeded)); assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); + + let fuel = codec.parse("loop").unwrap_err(); + assert!(fuel.to_string().contains("ran out of fuel"), "unexpected budget error: {fuel}"); + assert_eq!(fuel.codec_failure(), Some(CodecFailure::OutOfFuel)); + assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); + + // An author rejection is not a sandbox failure; it carries the codec's own message. + let rejected = codec.validate(&codec.parse("3/0").unwrap()).unwrap_err(); + assert!(rejected.to_string().contains("denominator"), "unexpected error: {rejected}"); + assert_eq!(rejected.codec_failure(), Some(CodecFailure::Rejected)); + } + + #[test] + fn limits_reject_a_component_that_reports_too_many_types() { + if !wasm_target_is_installed() { + eprintln!("skipping component adapter test: {WASM_TARGET} is not installed"); + return; + } + + let mut package = test_package(); + package.sections.push(Section::new( + package_note_storage_schema_section_id(), + FIXTURE_SCHEMA.as_bytes().to_vec(), + )); + package + .sections + .push(Section::new(package_note_codec_section_id(), build_fixture_component())); + let limits = CodecLimits { + max_supported_types: 0, + ..CodecLimits::default() + }; + + let error = CodecRegistry::load_from_package_with_limits(&package, limits) + .err() + .expect("a zero supported-type limit must reject the fixture codec") + .to_string(); + + assert!(error.contains("reported 1 types"), "unexpected limit error: {error}"); + assert!(error.contains("the limit is 0"), "unexpected limit error: {error}"); } #[test] @@ -520,18 +758,23 @@ package miden:base@1.0.0 { #[test] fn returned_values_are_size_limited() { - let long = "x".repeat(MAX_RETURNED_STRING_BYTES + 1); + let limits = CodecLimits::default(); + let long = "x".repeat(limits.max_returned_string_bytes + 1); assert!( - ensure_returned_string_limit("display value", &long, MAX_RETURNED_STRING_BYTES) + ensure_returned_string_limit("display value", &long, limits.max_returned_string_bytes) .unwrap_err() .to_string() .contains("the limit is") ); assert!( - ensure_returned_felt_limit(FIXTURE_FQN, MAX_RETURNED_FELTS + 1) - .unwrap_err() - .to_string() - .contains("the limit is") + ensure_returned_felt_limit( + FIXTURE_FQN, + limits.max_returned_felts + 1, + limits.max_returned_felts + ) + .unwrap_err() + .to_string() + .contains("the limit is") ); } @@ -598,7 +841,7 @@ package miden:base@1.0.0 { } /// Builds the minimal author codec used by the Phase 4a component spike. - fn build_fixture_component() -> Vec { + pub(crate) fn build_fixture_component() -> Vec { static COMPONENT: OnceLock> = OnceLock::new(); COMPONENT.get_or_init(build_fixture_component_uncached).clone() } diff --git a/sdk/note-schema/src/codec_structure.rs b/sdk/note-schema/src/codec_structure.rs new file mode 100644 index 0000000000..2b9b017c0e --- /dev/null +++ b/sdk/note-schema/src/codec_structure.rs @@ -0,0 +1,348 @@ +//! Structural limits for note codec components. +//! +//! The limits bound the work that parsing, compilation, and instantiation cost. That work +//! happens before any fuel budget applies, so a size cap alone does not bound it. A small +//! binary can still declare thousands of tiny functions, thousands of globals, or a deep +//! component tree. +//! +//! The rules are fixed policy. The producer applies them when it attaches a codec, and every +//! consumer applies them when it loads one. The numbers follow the strict compilation limits +//! that the Miden VM Wasm event handler runner enforces. + +use wasmparser::{Encoding, Parser, Payload, TypeRef}; + +use crate::{Error, Result}; + +/// Maximum functions in one core module, imported and defined. +const MAX_MODULE_FUNCTIONS: usize = 10_000; + +/// Maximum globals in one core module, imported and defined. +const MAX_MODULE_GLOBALS: usize = 1_000; + +/// Maximum tables in one core module, imported and defined. +const MAX_MODULE_TABLES: usize = 100; + +/// Maximum linear memories in one core module, imported and defined. +const MAX_MODULE_MEMORIES: usize = 1; + +/// Maximum element segments in one core module. +const MAX_MODULE_ELEMENT_SEGMENTS: usize = 1_000; + +/// Maximum data segments in one core module. +const MAX_MODULE_DATA_SEGMENTS: usize = 1_000; + +/// Maximum imports in one core module. +const MAX_MODULE_IMPORTS: usize = 1_024; + +/// Maximum exports in one core module. +const MAX_MODULE_EXPORTS: usize = 1_024; + +/// Maximum parameters in one core function type. +const MAX_FUNCTION_PARAMS: usize = 32; + +/// Maximum results in one core function type. +const MAX_FUNCTION_RESULTS: usize = 32; + +/// Code section size, in bytes, above which the average function size applies. +/// +/// Small modules stay below it, so a short helper module is never rejected for its shape. +const MIN_METERED_CODE_SECTION_BYTES: u32 = 1_000; + +/// Minimum average bytes per function body in a metered code section. +/// +/// Compiled Wasm averages far above this value. A module below it is a compilation bomb: +/// many tiny functions that each cost a fixed amount of host work. +const MIN_AVERAGE_FUNCTION_BYTES: u32 = 40; + +/// Maximum core modules in one component, at any nesting level. +const MAX_CORE_MODULES: usize = 16; + +/// Maximum component nesting depth. +const MAX_COMPONENT_DEPTH: usize = 4; + +/// Maximum entries in one component import or export section. +const MAX_COMPONENT_SECTION_ITEMS: usize = 256; + +/// Rejects a note codec component whose structure would make compilation or instantiation +/// expensive before any fuel applies. The rules are fixed policy, not host configuration: +/// the producer applies them at build time and every consumer applies them at load time. +pub fn validate_note_codec_structure(component: &[u8]) -> Result<()> { + let mut walk = StructureWalk::default(); + for payload in Parser::new(0).parse_all(component) { + walk.visit(payload.map_err(malformed)?)?; + } + Ok(()) +} + +/// The state carried while the parser walks one component. +#[derive(Default)] +struct StructureWalk { + /// One frame per core module or component the walk entered, innermost last. + frames: Vec, + /// Core modules seen anywhere in the component. + core_modules: usize, +} + +/// One nesting level of the walk. +enum Frame { + /// A core module, with the counters checked when the module ends. + Module(ModuleCounts), + /// A component, counted only for the nesting depth. + Component, +} + +/// Counters collected for one core module. +#[derive(Default)] +struct ModuleCounts { + functions: usize, + globals: usize, + tables: usize, + memories: usize, + element_segments: usize, + data_segments: usize, + imports: usize, + exports: usize, +} + +impl ModuleCounts { + /// Checks every per-module cap once the module ends. + fn check(&self) -> Result<()> { + ensure_module_cap("functions", self.functions, MAX_MODULE_FUNCTIONS)?; + ensure_module_cap("globals", self.globals, MAX_MODULE_GLOBALS)?; + ensure_module_cap("tables", self.tables, MAX_MODULE_TABLES)?; + ensure_module_cap("memories", self.memories, MAX_MODULE_MEMORIES)?; + ensure_module_cap("element segments", self.element_segments, MAX_MODULE_ELEMENT_SEGMENTS)?; + ensure_module_cap("data segments", self.data_segments, MAX_MODULE_DATA_SEGMENTS)?; + ensure_module_cap("imports", self.imports, MAX_MODULE_IMPORTS)?; + ensure_module_cap("exports", self.exports, MAX_MODULE_EXPORTS) + } +} + +impl StructureWalk { + /// Applies one parser payload to the current nesting level. + fn visit(&mut self, payload: Payload<'_>) -> Result<()> { + match payload { + Payload::Version { encoding, .. } => self.enter(encoding)?, + Payload::End(_) => self.leave()?, + Payload::TypeSection(reader) => { + for ty in reader.into_iter_err_on_gc_types() { + let ty = ty.map_err(malformed)?; + ensure_signature_cap("parameters", ty.params().len(), MAX_FUNCTION_PARAMS)?; + ensure_signature_cap("results", ty.results().len(), MAX_FUNCTION_RESULTS)?; + } + } + Payload::ImportSection(reader) => { + for import in reader.into_imports() { + let import = import.map_err(malformed)?; + let counts = self.module_counts()?; + counts.imports += 1; + match import.ty { + TypeRef::Func(_) | TypeRef::FuncExact(_) => counts.functions += 1, + TypeRef::Global(_) => counts.globals += 1, + TypeRef::Table(_) => counts.tables += 1, + TypeRef::Memory(_) => counts.memories += 1, + TypeRef::Tag(_) => {} + } + } + } + Payload::FunctionSection(reader) => { + self.module_counts()?.functions += reader.count() as usize; + } + Payload::GlobalSection(reader) => { + self.module_counts()?.globals += reader.count() as usize; + } + Payload::TableSection(reader) => { + self.module_counts()?.tables += reader.count() as usize; + } + Payload::MemorySection(reader) => { + self.module_counts()?.memories += reader.count() as usize; + } + Payload::ElementSection(reader) => { + self.module_counts()?.element_segments += reader.count() as usize; + } + Payload::DataSection(reader) => { + self.module_counts()?.data_segments += reader.count() as usize; + } + Payload::ExportSection(reader) => { + self.module_counts()?.exports += reader.count() as usize; + } + Payload::CodeSectionStart { count, size, .. } => { + ensure_average_function_size(count, size)?; + } + Payload::ComponentImportSection(reader) => { + ensure_component_section_cap("imports", reader.count() as usize)?; + } + Payload::ComponentExportSection(reader) => { + ensure_component_section_cap("exports", reader.count() as usize)?; + } + _ => {} + } + Ok(()) + } + + /// Opens one nesting level and checks the component-wide caps. + fn enter(&mut self, encoding: Encoding) -> Result<()> { + match encoding { + Encoding::Module => { + self.core_modules += 1; + if self.core_modules > MAX_CORE_MODULES { + return Err(Error::new(format!( + "note codec component has {} core modules; the limit is {MAX_CORE_MODULES}", + self.core_modules + ))); + } + self.frames.push(Frame::Module(ModuleCounts::default())); + } + Encoding::Component => { + self.frames.push(Frame::Component); + let depth = + self.frames.iter().filter(|frame| matches!(frame, Frame::Component)).count(); + if depth > MAX_COMPONENT_DEPTH { + return Err(Error::new(format!( + "note codec component nests components {depth} deep; the limit is \ + {MAX_COMPONENT_DEPTH}" + ))); + } + } + } + Ok(()) + } + + /// Closes one nesting level and checks the counters of a core module. + fn leave(&mut self) -> Result<()> { + match self.frames.pop() { + Some(Frame::Module(counts)) => counts.check(), + // A component frame carries no counters, and an unbalanced end cannot happen: + // the parser reports one `End` for every header it accepted. + _ => Ok(()), + } + } + + /// Returns the counters of the core module the walk is inside. + /// + /// Core sections appear only inside a core module. A core section anywhere else is a + /// malformed layout, and the walk fails closed instead of guessing a frame for it. + fn module_counts(&mut self) -> Result<&mut ModuleCounts> { + match self.frames.last_mut() { + Some(Frame::Module(counts)) => Ok(counts), + _ => Err(Error::new( + "note codec component is malformed: a core section appears outside a core module", + )), + } + } +} + +/// Reports a core module that is over one of its caps. +fn ensure_module_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { + if observed > limit { + return Err(Error::new(format!( + "note codec component has a core module with {observed} {kind}; the limit is {limit}" + ))); + } + Ok(()) +} + +/// Reports a core function type that is over its parameter or result cap. +fn ensure_signature_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { + if observed > limit { + return Err(Error::new(format!( + "note codec component has a function type with {observed} {kind}; the limit is {limit}" + ))); + } + Ok(()) +} + +/// Reports a component import or export section that is over its cap. +fn ensure_component_section_cap(kind: &str, observed: usize) -> Result<()> { + if observed > MAX_COMPONENT_SECTION_ITEMS { + return Err(Error::new(format!( + "note codec component has a section with {observed} component {kind}; the limit is \ + {MAX_COMPONENT_SECTION_ITEMS}" + ))); + } + Ok(()) +} + +/// Reports a code section built from many tiny functions. +fn ensure_average_function_size(count: u32, size: u32) -> Result<()> { + if size < MIN_METERED_CODE_SECTION_BYTES || count == 0 { + return Ok(()); + } + let average = size / count; + if average < MIN_AVERAGE_FUNCTION_BYTES { + return Err(Error::new(format!( + "note codec component has a core module with {count} functions in {size} bytes of \ + code, an average of {average} bytes; the limit is {MIN_AVERAGE_FUNCTION_BYTES} bytes \ + per function" + ))); + } + Ok(()) +} + +/// Reports bytes that do not parse as a component. +fn malformed(error: wasmparser::BinaryReaderError) -> Error { + Error::new(format!("note codec component is malformed: {error}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Wraps core module text in a component and assembles it. + fn component(core_module: &str) -> Vec { + wat::parse_str(format!("(component (core module {core_module}))")).unwrap() + } + + #[test] + fn minimal_component_passes() { + validate_note_codec_structure(&component("(func)")).unwrap(); + } + + #[test] + fn too_many_globals_are_rejected() { + let globals = "(global i32 (i32.const 0))".repeat(MAX_MODULE_GLOBALS + 1); + let error = validate_note_codec_structure(&component(&globals)).unwrap_err().to_string(); + + assert!(error.contains("1001 globals"), "unexpected error: {error}"); + assert!(error.contains("the limit is 1000"), "unexpected error: {error}"); + } + + #[test] + fn oversized_function_signatures_are_rejected() { + let params = "i32 ".repeat(MAX_FUNCTION_PARAMS + 1); + let module = format!("(type (func (param {params})))"); + let error = validate_note_codec_structure(&component(&module)).unwrap_err().to_string(); + + assert!(error.contains("33 parameters"), "unexpected error: {error}"); + assert!(error.contains("the limit is 32"), "unexpected error: {error}"); + } + + #[test] + fn many_tiny_functions_are_rejected() { + let error = validate_note_codec_structure(&component(&"(func)".repeat(600))) + .unwrap_err() + .to_string(); + + assert!(error.contains("600 functions"), "unexpected error: {error}"); + assert!(error.contains("bytes per function"), "unexpected error: {error}"); + } + + #[test] + fn malformed_bytes_are_rejected() { + let error = validate_note_codec_structure(b"not a component").unwrap_err().to_string(); + + assert!(error.contains("note codec component is malformed"), "unexpected error: {error}"); + } + + #[cfg(feature = "codec-component")] + #[test] + fn fixture_component_passes() { + if !midenc_integration_test_support::wasm_target_is_installed() { + eprintln!("skipping the structural limit fixture test: wasm32-wasip2 is not installed"); + return; + } + + validate_note_codec_structure(&crate::codec_component::tests::build_fixture_component()) + .unwrap(); + } +} diff --git a/sdk/note-schema/src/error.rs b/sdk/note-schema/src/error.rs index eb11106f6f..fcd015c889 100644 --- a/sdk/note-schema/src/error.rs +++ b/sdk/note-schema/src/error.rs @@ -2,10 +2,24 @@ use core::fmt; +/// Why a bundled codec call did not return a value. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CodecFailure { + /// The call used its whole fuel budget. + OutOfFuel, + /// The call hit a memory or table limit. + LimitExceeded, + /// The component trapped, or the engine rejected the call. + Trapped, + /// The codec returned its own rejection message. + Rejected, +} + /// An error reported while reading, encoding, or decoding a note storage schema. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Error { message: String, + codec_failure: Option, } impl Error { @@ -13,12 +27,34 @@ impl Error { pub fn new(message: impl Into) -> Self { Self { message: message.into(), + codec_failure: None, } } + /// Creates an error that reports how a bundled codec call failed. + /// + /// Only the bundled codec adapter reports a failure class. + #[cfg(feature = "codec-component")] + pub(crate) fn codec(kind: CodecFailure, message: impl Into) -> Self { + Self { + message: message.into(), + codec_failure: Some(kind), + } + } + + /// Returns the failure class of a bundled codec call. + /// + /// Errors from other sources return `None`. + pub fn codec_failure(&self) -> Option { + self.codec_failure + } + /// Adds context before the current error message. pub(crate) fn context(self, context: impl fmt::Display) -> Self { - Self::new(format!("{context}: {}", self.message)) + Self { + message: format!("{context}: {}", self.message), + codec_failure: self.codec_failure, + } } } diff --git a/sdk/note-schema/src/lib.rs b/sdk/note-schema/src/lib.rs index 87424ffb29..b92d8de24b 100644 --- a/sdk/note-schema/src/lib.rs +++ b/sdk/note-schema/src/lib.rs @@ -19,6 +19,7 @@ mod builder; mod codec; #[cfg(feature = "codec-component")] mod codec_component; +mod codec_structure; mod error; mod schema; mod section; @@ -33,12 +34,16 @@ pub use codec::{ ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, CodecRegistry, ConsumerTypeCodec, FELT_FQN, StandardLeaf, WORD_FQN, }; -pub use error::{Error, Result}; +#[cfg(feature = "codec-component")] +pub use codec_component::CodecLimits; +pub use codec_structure::validate_note_codec_structure; +pub use error::{CodecFailure, Error, Result}; pub use miden_field::Felt; pub use miden_protocol::note::NoteStorage; pub use schema::{ FeltLayout, MAX_NOTE_CODEC_COMPONENT_BYTES, MAX_NOTE_STORAGE_SCHEMA_BYTES, MAX_NOTE_STORAGE_SCHEMA_DEPTH, MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_TYPES, - NoteStorageSchema, PrimitiveType, SchemaCase, SchemaField, SchemaType, SchemaTypeKind, + NOTE_CODEC_GUEST_RUSTFLAGS, NoteStorageSchema, PrimitiveType, SchemaCase, SchemaField, + SchemaType, SchemaTypeKind, }; pub use value::{DecodedValue, DecodedValueKind}; diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs index 095c489ece..9e5c2367b2 100644 --- a/sdk/note-schema/src/schema.rs +++ b/sdk/note-schema/src/schema.rs @@ -43,6 +43,10 @@ pub const MAX_NOTE_STORAGE_SCHEMA_FELTS: usize = MAX_NOTE_STORAGE_ITEMS; /// builds is a package that consumers accept. pub const MAX_NOTE_CODEC_COMPONENT_BYTES: usize = 4 * 1024 * 1024; +/// Rustflags the nested codec build pins, so a codec crate's own cargo config cannot enable +/// a Wasm feature that every consumer rejects. Mirrors the VM event-handler plugin. +pub const NOTE_CODEC_GUEST_RUSTFLAGS: &str = "-C target-feature=-simd128"; + const _: () = assert!(MAX_NOTE_STORAGE_SCHEMA_DEPTH > 0); /// The minimum and maximum felt count for a schema type. From a724e15c33bf6f92135338b9f7697c3527a55ba2 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 8 Sep 2026 17:57:00 +0300 Subject: [PATCH 34/43] fix: bound the work a hostile note codec can force before its fuel applies The structural check counted core-module sections but not component-level ones. wasmtime's inliner expands every nested component instantiation at compile time, so a tiny component could force billions of initializers before any store limit or fuel applied. Core and component instantiations are now budgeted across the whole component tree, every other component-level section is capped, a component start function is rejected, and defined tables and memories are counted against the store's own limits. The Wasm feature policy was implicit: threads and GC were off only because of the wasmtime cargo features this crate selects, which feature unification in a host can flip back on, and the producer never checked features at all. The policy is now an explicit wasmparser feature set validated payload by payload, function bodies included, inside one shared entry point that both the producer's build check and every consumer's load run. Discovery lifted the reported type list eagerly through the generated bindings, so string descriptors aliasing one small region could force unbounded host allocation at load. The list is now read through a typed function with lazy cursors, the length checked before any element is read, and each name capped before it is copied. Failures during instantiation and discovery, and every host-side cap, now report their `CodecFailure` class. The limits documentation states the per-memory and per-table semantics, and the fixed store counts alias the structural budgets. A package without a codec section loads the base registry instead of failing. The manifest check precedes staging, the test fixture pins the guest rustflags like the real build, and `tempfile` becomes a dev-dependency. --- midenc-compile/Cargo.toml | 5 +- midenc-compile/src/cargo.rs | 60 +++- sdk/note-schema/src/codec_component.rs | 428 +++++++++++++++++++++---- sdk/note-schema/src/codec_structure.rs | 368 +++++++++++++++++++-- sdk/note-schema/src/error.rs | 11 +- sdk/note-schema/src/lib.rs | 4 +- 6 files changed, 761 insertions(+), 115 deletions(-) diff --git a/midenc-compile/Cargo.toml b/midenc-compile/Cargo.toml index 10632b373b..a546ed0eaa 100644 --- a/midenc-compile/Cargo.toml +++ b/midenc-compile/Cargo.toml @@ -27,7 +27,6 @@ std = [ "dep:clap", "dep:miden-note-schema", "dep:sha2", - "dep:tempfile", "dep:toml_edit", "dep:wat", "dep:wit-component", @@ -54,9 +53,11 @@ midenc-hir.workspace = true midenc-hir-transform.workspace = true midenc-session.workspace = true sha2 = { workspace = true, optional = true } -tempfile = { workspace = true, optional = true } toml_edit = { workspace = true, optional = true, features = ["parse", "display"] } thiserror.workspace = true wat = { workspace = true, optional = true } wit-component = { workspace = true, optional = true } wit-parser = { workspace = true, optional = true } + +[dev-dependencies] +tempfile.workspace = true diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 8f90be871d..e4f2d06118 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -32,6 +32,9 @@ const NOTE_CODEC_NAMESPACE: &str = "midenc"; const NOTE_CODEC_TABLE: &str = "note-codec"; /// Defines the dotted metadata table name for the author-side note codec. +/// +/// The same name is the work directory the codec build creates under the codec crate, so the +/// build artifacts sit next to the setting that asked for them. pub(crate) const NOTE_CODEC_TABLE_NAME: &str = "midenc.note-codec"; /// Names the metadata key that contains the codec crate directory. @@ -440,8 +443,8 @@ fn build_note_codec_component( codec_crate_dir: &Path, note_package: &MastPackage, ) -> CompilerResult> { - let work_dir = codec_crate_dir.join("target").join(NOTE_CODEC_TABLE_NAME); - let staged_package = stage_note_package(&work_dir, codec_crate_dir, note_package)?; + // Check the manifest before staging, or a mis-pointed `crate` path creates a work directory + // in a directory that is not a crate. let manifest_path = codec_crate_dir.join("Cargo.toml"); if !manifest_path.is_file() { return Err(Report::msg(format!( @@ -449,6 +452,8 @@ fn build_note_codec_component( manifest_path.display() ))); } + let work_dir = codec_crate_dir.join("target").join(NOTE_CODEC_TABLE_NAME); + let staged_package = stage_note_package(&work_dir, codec_crate_dir, note_package)?; let cargo_env = env::var_os("CARGO").map(PathBuf::from); let cargo_path = cargo_env.as_deref().unwrap_or_else(|| Path::new("cargo")); @@ -520,6 +525,12 @@ fn build_note_codec_component( } /// Builds the nested Cargo command that compiles one note codec crate. +/// +/// The command pins `RUSTFLAGS`. That environment variable replaces the rustflags in the codec +/// crate's own cargo config, so the crate cannot enable a Wasm feature that consumers reject. +/// Replacing them is safe only because the command always passes `--target`: with a target +/// selected, Cargo applies these flags to the codec crate alone, and host build scripts and +/// proc macros keep the flags of the host they build for. fn note_codec_cargo_command( cargo_path: &Path, toolchain: Option<&str>, @@ -692,17 +703,14 @@ fn note_codec_cargo_error(error: Report, manifest_path: &Path) -> Report { /// Verifies the component sandbox and the versioned codec interface export. fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { - // Enforce the consumer size limit at the producer, so a package that builds is a - // package that consumers accept. - if component.len() > miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES { - return Err(Report::msg(format!( - "note codec component is {} bytes, above the {}-byte limit that consumers enforce", - component.len(), - miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES, - ))); - } - // The structural caps are fixed policy, so the producer applies the consumer rules here. - miden_note_schema::validate_note_codec_structure(component).map_err(|error| { + // The byte cap, the Wasm feature set, and the structural caps are fixed policy, so the + // producer applies the consumer rules here through the one shared entry point: a package + // that builds is a package that consumers accept. + miden_note_schema::validate_note_codec_component( + component, + miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES, + ) + .map_err(|error| { Report::msg(format!( "note codec component fails the structural limits consumers enforce: {error}" )) @@ -1218,13 +1226,37 @@ mod tests { fn oversized_codec_components_fail_producer_validation() { let oversized = vec![0u8; miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES + 1]; let error = validate_note_codec_component(&oversized).unwrap_err().to_string(); - assert!(error.contains("above the"), "unexpected error: {error}"); + assert!( + error.contains("fails the structural limits consumers enforce"), + "unexpected error: {error}" + ); + assert!(error.contains("pre-compilation limit"), "unexpected error: {error}"); assert!( error.contains(&miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES.to_string()), "the limit is not named: {error}" ); } + #[test] + fn codec_components_outside_the_wasm_feature_policy_fail_producer_validation() { + let component = wat::parse_str( + r#"(component + (core module + (func (export "simd") + v128.const i32x4 0 0 0 0 + drop)))"#, + ) + .unwrap(); + + let error = validate_note_codec_component(&component).unwrap_err().to_string(); + + assert!( + error.contains("uses a Wasm feature the policy rejects"), + "unexpected error: {error}" + ); + assert!(error.contains("SIMD"), "the rejected proposal is not named: {error}"); + } + #[test] fn structurally_oversized_codec_components_fail_producer_validation() { let globals = "(global i32 (i32.const 0))".repeat(1_001); diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 6caac68e19..7310a9ca10 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -7,17 +7,40 @@ use miden_mast_package::Package; use midenc_frontend_wasm_metadata::{PACKAGE_NOTE_CODEC_SECTION_ID, package_note_codec_section_id}; use wasmtime::{ Config, Engine, ResourceLimiter, Store, StoreLimits, StoreLimitsBuilder, Trap, - component::{Component, Linker}, + component::{Component, Instance, Linker, WasmList, WasmStr}, }; use crate::{ CodecFailure, CodecRegistry, ConsumerTypeCodec, Error, NoteStorageSchema, Result, - validate_note_codec_structure, + codec_structure::{MAX_CORE_INSTANCES, MAX_DEFINED_MEMORIES, MAX_DEFINED_TABLES}, + validate_note_codec_component, }; -/// Maximum bytes of stack available to one component call. +/// Maximum bytes of Wasm stack available to one component call. const MAX_WASM_STACK_BYTES: usize = 512 * 1024; +/// The versioned interface a note codec component exports. +const CODEC_INTERFACE: &str = "miden:note-codec/codec@1.0.0"; + +/// The discovery function of the codec interface. +const SUPPORTED_TYPES: &str = "supported-types"; + +/// Maximum instances one codec call store may hold. +/// +/// The value is the structural budget for core instantiations, so the two cannot drift: a +/// component that passes the load policy can also instantiate. +const MAX_STORE_INSTANCES: usize = MAX_CORE_INSTANCES; + +/// Maximum tables one codec call store may hold. +/// +/// The value is the structural budget for defined tables. +const MAX_STORE_TABLES: usize = MAX_DEFINED_TABLES; + +/// Maximum linear memories one codec call store may hold. +/// +/// The value is the structural budget for defined memories. +const MAX_STORE_MEMORIES: usize = MAX_DEFINED_MEMORIES; + wasmtime::component::bindgen!({ path: "wit", world: "note-codec", @@ -26,13 +49,21 @@ wasmtime::component::bindgen!({ /// Runtime limits a host applies to bundled note codecs. Limits are host policy. /// A package carries no limit values and cannot raise them. Hosts in one /// deployment should run identical limits, so a codec behaves the same everywhere. +/// +/// The store counts are fixed policy instead of host configuration. One call store admits at +/// most `MAX_STORE_INSTANCES` instances, `MAX_STORE_TABLES` tables, and `MAX_STORE_MEMORIES` +/// linear memory, and one call gets `MAX_WASM_STACK_BYTES` of Wasm stack. The structural policy +/// in [`validate_note_codec_component`] uses the same counts, so a component that loads can also +/// instantiate. #[derive(Clone, Debug, PartialEq, Eq)] pub struct CodecLimits { /// Fuel budget for one codec call, about one unit per Wasm instruction. pub fuel: u64, - /// Cap on the guest linear memory in bytes. + /// Cap on one guest linear memory in bytes. Wasmtime applies the cap to each memory, and a + /// store admits `MAX_STORE_MEMORIES` of them. pub max_memory_bytes: usize, - /// Cap on the total table elements. + /// Cap on the elements of one table. Wasmtime applies the cap to each table, and a store + /// admits `MAX_STORE_TABLES` of them. pub max_table_elements: usize, /// Cap on the component size in bytes. pub max_component_bytes: usize, @@ -68,11 +99,19 @@ impl CodecRegistry { } /// Loads the note codec component from a package and registers all reported types. + /// + /// A package without a note codec section is the common case, so it is not an error: the + /// registry comes back with the standard codecs alone, the same base the bundled-codec path + /// registers on top of. More than one note codec section is still an error. pub fn load_from_package_with_limits(package: &Package, limits: CodecLimits) -> Result { let schema = NoteStorageSchema::from_package(package)?; + let section_id = package_note_codec_section_id(); + if !package.sections.iter().any(|section| section.id == section_id) { + return Ok(Self::default()); + } let bytes = crate::section::unique_package_section( package, - package_note_codec_section_id(), + section_id, PACKAGE_NOTE_CODEC_SECTION_ID, )?; Self::load_from_component(bytes, &schema.custom_type_fqns(), limits) @@ -172,22 +211,30 @@ impl ResourceLimiter for ComponentStore { /// One isolated codec component call context. struct ComponentInstance { store: Store, + /// The raw instance, used by the calls that lift their results lazily. + instance: Instance, bindings: NoteCodec, } impl ComponentRuntime { /// Compiles a component under the explicit engine policy and the given limits. fn new(bytes: &[u8], limits: CodecLimits) -> Result { - ensure_component_byte_limit(bytes.len(), limits.max_component_bytes)?; - validate_note_codec_structure(bytes)?; + // One entry point applies the byte cap, the Wasm feature policy, and the structural + // limits. The producer applies the same policy when it attaches a codec. + validate_note_codec_component(bytes, limits.max_component_bytes)?; let mut config = Config::new(); config.wasm_component_model(true); config.consume_fuel(true); // Proposals the wasm32-wasip2 target emits by default. config.wasm_bulk_memory(true); config.wasm_multi_value(true); - // Validation policy: every other proposal is off by name. The engine defaults change - // between versions, and they decide which components load on every host. + // Validation policy: `NOTE_CODEC_WASM_FEATURES` decides what loads, and the call above + // already applied it. The setters below repeat that policy in the engine wherever + // wasmtime exposes a setter. Threads, garbage collection, reference types and typed + // function references have no setter here, because wasmtime gates those setters behind + // Cargo features this crate does not enable. Another crate in the same build can turn + // those proposals back on by feature unification, which is why the accepted set is + // pinned in wasmparser instead of read back from the engine. config.wasm_simd(false); config.wasm_relaxed_simd(false); config.wasm_multi_memory(false); @@ -199,6 +246,7 @@ impl ComponentRuntime { config.wasm_shared_everything_threads(false); config.wasm_stack_switching(false); config.wasm_exceptions(false); + config.wasm_component_model_error_context(false); // Floats stay on: codecs parse and format decimal text. NaN canonicalization keeps // float results identical across hosts. config.cranelift_nan_canonicalization(true); @@ -232,29 +280,90 @@ impl ComponentRuntime { store .set_fuel(self.limits.fuel) .map_err(|error| component_error("set the note codec fuel budget", error))?; - let bindings = NoteCodec::instantiate(&mut store, &self.component, &linker) - .map_err(|error| component_error("instantiate the note codec", error))?; - Ok(ComponentInstance { store, bindings }) + // Instantiation runs the guest `_initialize` and every data-segment initializer under + // the same fuel and store limits as an exported call, so it fails in the same classes. + let instance = match linker.instantiate(&mut store, &self.component) { + Ok(instance) => instance, + Err(error) => return Err(start_failure(&store, error)), + }; + let bindings = match NoteCodec::new(&mut store, &instance) { + Ok(bindings) => bindings, + Err(error) => return Err(start_failure(&store, error)), + }; + Ok(ComponentInstance { + store, + instance, + bindings, + }) } /// Queries the component's supported FQNs once during registry construction. + /// + /// The call lifts its result lazily instead of going through the generated bindings. A + /// `list` is the one result a codec can inflate far past every limit: the elements + /// are pointer and length pairs that may all alias one small region, so lifting the whole + /// list into owned strings allocates before any cap applies. The per-value calls return one + /// string or one felt list, which guest memory already bounds, so they keep the bindings. fn supported_types(&self) -> Result> { - let mut instance = self.instantiate()?; - let fqns = instance - .bindings - .miden_note_codec_codec() - .call_supported_types(&mut instance.store) - .map_err(|error| component_error("call `supported-types`", error))?; - if fqns.len() > self.limits.max_supported_types { - return Err(Error::new(format!( - "note codec component reported {} types; the limit is {}", - fqns.len(), - self.limits.max_supported_types - ))); + let ComponentInstance { + mut store, + instance, + .. + } = self.instantiate()?; + let interface = + instance.get_export_index(&mut store, None, CODEC_INTERFACE).ok_or_else(|| { + Error::new(format!("note codec component exports no `{CODEC_INTERFACE}`")) + })?; + let export = instance + .get_export_index(&mut store, Some(&interface), SUPPORTED_TYPES) + .ok_or_else(|| { + Error::new(format!( + "note codec component exports no `{SUPPORTED_TYPES}` in `{CODEC_INTERFACE}`" + )) + })?; + let supported_types = instance + .get_typed_func::<(), (WasmList,)>(&mut store, &export) + .map_err(|error| component_error("find `supported-types`", error))?; + let (reported,) = match supported_types.call(&mut store, ()) { + Ok(reported) => reported, + Err(error) => { + return Err(Error::codec( + classify_failure(&store, &error), + format!("note codec component failed in `supported-types`: {error:#}"), + )); + } + }; + + // Check the list length before any element is read, then take the cursors. A cursor + // holds a pointer and a length, so this copies nothing out of guest memory. + if reported.len() > self.limits.max_supported_types { + return Err(Error::codec( + CodecFailure::LimitExceeded, + format!( + "note codec component reported {} types; the limit is {}", + reported.len(), + self.limits.max_supported_types + ), + )); } - for fqn in &fqns { - ensure_returned_string_limit("type FQN", fqn, self.limits.max_fqn_bytes)?; + let cursors = reported + .iter(&mut store) + .collect::>>() + .map_err(|error| component_error("read the reported type FQNs", error))?; + + let mut fqns = Vec::with_capacity(cursors.len()); + for cursor in cursors { + // `to_str` borrows guest memory for UTF-8, so the cap applies before the copy. + let fqn = cursor + .to_str(&store) + .map_err(|error| component_error("decode a reported type FQN", error))?; + ensure_returned_string_limit("type FQN", &fqn, self.limits.max_fqn_bytes)?; + fqns.push(fqn.into_owned()); } + // The typed-func API requires the post-return call once the results are read. + supported_types + .post_return(&mut store) + .map_err(|error| component_error("finish `supported-types`", error))?; Ok(fqns) } } @@ -264,23 +373,13 @@ fn component_store_limits(limits: &CodecLimits) -> StoreLimits { StoreLimitsBuilder::new() .memory_size(limits.max_memory_bytes) .table_elements(limits.max_table_elements) - .instances(32) - .tables(32) - .memories(1) + .instances(MAX_STORE_INSTANCES) + .tables(MAX_STORE_TABLES) + .memories(MAX_STORE_MEMORIES) .trap_on_grow_failure(true) .build() } -/// Rejects oversized component bytes before validation or JIT compilation begins. -fn ensure_component_byte_limit(byte_len: usize, limit: usize) -> Result<()> { - if byte_len > limit { - return Err(Error::new(format!( - "note codec component is {byte_len} bytes; the pre-compilation limit is {limit}" - ))); - } - Ok(()) -} - /// A registry entry that dispatches one FQN through isolated component instances. struct ComponentCodec { fqn: String, @@ -301,7 +400,7 @@ impl ComponentCodec { } } - /// Classifies why one component call did not return a value. + /// Reports why one component call did not return a value. fn call_failure( &self, operation: &str, @@ -309,21 +408,19 @@ impl ComponentCodec { error: wasmtime::Error, ) -> Error { let fqn = &self.fqn; - if store.data().limit_hit { - Error::codec( + match classify_failure(store, &error) { + CodecFailure::LimitExceeded => Error::codec( CodecFailure::LimitExceeded, format!("note codec `{fqn}` exceeded a resource limit in `{operation}`"), - ) - } else if error.downcast_ref::() == Some(&Trap::OutOfFuel) { - Error::codec( + ), + CodecFailure::OutOfFuel => Error::codec( CodecFailure::OutOfFuel, format!("note codec `{fqn}` ran out of fuel in `{operation}`"), - ) - } else { - Error::codec( - CodecFailure::Trapped, + ), + class => Error::codec( + class, format!("note codec `{fqn}` trapped in `{operation}`: {error:#}"), - ) + ), } } @@ -427,13 +524,38 @@ fn validate_reported_fqns( Ok(()) } +/// Classifies why a component call, or the instantiation that precedes it, failed. +/// +/// The store records a refused memory or table growth, and it outranks the trap the guest saw: +/// the trap is only how the refusal surfaced. +fn classify_failure(store: &Store, error: &wasmtime::Error) -> CodecFailure { + if store.data().limit_hit { + CodecFailure::LimitExceeded + } else if error.downcast_ref::() == Some(&Trap::OutOfFuel) { + CodecFailure::OutOfFuel + } else { + CodecFailure::Trapped + } +} + +/// Reports a component that did not reach its first exported call. +fn start_failure(store: &Store, error: wasmtime::Error) -> Error { + Error::codec( + classify_failure(store, &error), + format!("note codec failed to start: {error:#}"), + ) +} + /// Enforces a byte-size cap on one component-returned string. fn ensure_returned_string_limit(kind: &str, value: &str, limit: usize) -> Result<()> { if value.len() > limit { - return Err(Error::new(format!( - "note codec component returned a {kind} of {} bytes; the limit is {limit}", - value.len() - ))); + return Err(Error::codec( + CodecFailure::LimitExceeded, + format!( + "note codec component returned a {kind} of {} bytes; the limit is {limit}", + value.len() + ), + )); } Ok(()) } @@ -441,9 +563,10 @@ fn ensure_returned_string_limit(kind: &str, value: &str, limit: usize) -> Result /// Enforces the structural felt count cap on one `parse` result. fn ensure_returned_felt_limit(fqn: &str, count: usize, limit: usize) -> Result<()> { if count > limit { - return Err(Error::new(format!( - "codec `{fqn}` returned {count} felts from `parse`; the limit is {limit}" - ))); + return Err(Error::codec( + CodecFailure::LimitExceeded, + format!("codec `{fqn}` returned {count} felts from `parse`; the limit is {limit}"), + )); } Ok(()) } @@ -599,6 +722,139 @@ package miden:base@1.0.0 { assert!(error.contains("growing table"), "unexpected table-limit error: {error}"); } + /// Builds a component that exports the note codec interface with a chosen + /// `supported-types` body. + /// + /// `core_declarations` adds core module text, so a test can install a trapping start + /// function. The other three interface functions carry their real signatures, which the + /// generated bindings check, and share one core function that is never called. + fn codec_shaped_component(core_declarations: &str, supported_types_body: &str) -> Vec { + let text = format!( + r#"(component + (core module $m + (memory (export "memory") 1) + {core_declarations} + (func (export "supported-types") (result i32) {supported_types_body}) + (func (export "operation") (param i32 i32 i32 i32) (result i32) + (i32.const 1024)) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) + (i32.const 1024))) + (core instance $i (instantiate $m)) + (func $supported (result (list string)) + (canon lift (core func $i "supported-types") + (memory $i "memory") + (realloc (func $i "cabi_realloc")))) + (func $parse (param "type-fqn" string) (param "value" string) + (result (result (list u64) (error string))) + (canon lift (core func $i "operation") + (memory $i "memory") + (realloc (func $i "cabi_realloc")))) + (func $display (param "type-fqn" string) (param "value" (list u64)) + (result (result string (error string))) + (canon lift (core func $i "operation") + (memory $i "memory") + (realloc (func $i "cabi_realloc")))) + (func $validate (param "type-fqn" string) (param "value" (list u64)) + (result (result (error string))) + (canon lift (core func $i "operation") + (memory $i "memory") + (realloc (func $i "cabi_realloc")))) + (instance $codec + (export "supported-types" (func $supported)) + (export "parse" (func $parse)) + (export "display" (func $display)) + (export "validate" (func $validate))) + (export "{CODEC_INTERFACE}" (instance $codec)))"# + ); + wat::parse_str(text).unwrap() + } + + /// Returns core module text that reports `count` string descriptors, all aliasing one + /// four-byte string. + fn aliasing_descriptors(count: usize) -> String { + let length = (count as u32).to_le_bytes().map(|byte| format!("\\{byte:02x}")).concat(); + let descriptor = "\\00\\00\\00\\00\\04\\00\\00\\00".repeat(count); + format!( + r#"(data (i32.const 0) "abcd") + (data (i32.const 8) "\10\00\00\00{length}") + (data (i32.const 16) "{descriptor}")"# + ) + } + + #[test] + fn discovery_checks_the_reported_length_before_it_lifts_a_string() { + // Every descriptor points at the same four bytes, so lifting the whole list first would + // allocate far past the limit that rejects it. + let limits = CodecLimits::default(); + let reported = limits.max_supported_types + 1; + let component = codec_shaped_component(&aliasing_descriptors(reported), "(i32.const 8)"); + + let error = ComponentRuntime::new(&component, limits.clone()) + .unwrap() + .supported_types() + .expect_err("a component over the supported-type limit must be rejected"); + + assert!( + error.to_string().contains(&format!("reported {reported} types")), + "unexpected limit error: {error}" + ); + assert!( + error + .to_string() + .contains(&format!("the limit is {}", limits.max_supported_types)), + "unexpected limit error: {error}" + ); + assert_eq!(error.codec_failure(), Some(CodecFailure::LimitExceeded)); + } + + #[test] + fn a_trapping_start_function_is_classified() { + let component = codec_shaped_component( + r#"(func $boom unreachable) + (start $boom)"#, + "(i32.const 8)", + ); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .unwrap() + .supported_types() + .expect_err("a trapping start function must fail instantiation"); + + assert!(error.to_string().contains("failed to start"), "unexpected error: {error}"); + assert_eq!(error.codec_failure(), Some(CodecFailure::Trapped)); + } + + #[test] + fn discovery_that_runs_out_of_fuel_is_classified() { + let component = codec_shaped_component("", "(loop $spin (br $spin)) (i32.const 8)"); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .unwrap() + .supported_types() + .expect_err("an endless `supported-types` must exhaust its fuel"); + + assert!( + error.to_string().contains("failed in `supported-types`"), + "unexpected error: {error}" + ); + assert_eq!(error.codec_failure(), Some(CodecFailure::OutOfFuel)); + } + + #[test] + fn a_package_without_a_codec_section_keeps_the_standard_codecs() { + let mut package = test_package(); + package.sections.push(Section::new( + package_note_storage_schema_section_id(), + FIXTURE_SCHEMA.as_bytes().to_vec(), + )); + + let registry = CodecRegistry::load_from_package(&package).unwrap(); + + assert!(registry.contains(crate::FELT_FQN)); + assert!(registry.contains(crate::WORD_FQN)); + assert!(!registry.contains(FIXTURE_FQN)); + } + #[test] fn simd_components_do_not_load() { let component = wat::parse_str( @@ -618,6 +874,40 @@ package miden:base@1.0.0 { assert!(error.contains("SIMD"), "unexpected engine policy error: {error}"); } + #[test] + fn threads_components_do_not_load() { + let component = wat::parse_str( + r#"(component + (core module + (memory 1 1 shared)))"#, + ) + .unwrap(); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .err() + .expect("a threads component must not load") + .to_string(); + + assert!(error.contains("threads"), "unexpected engine policy error: {error}"); + } + + #[test] + fn gc_components_do_not_load() { + let component = wat::parse_str( + r#"(component + (core module + (type (struct))))"#, + ) + .unwrap(); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .err() + .expect("a GC component must not load") + .to_string(); + + assert!(error.contains("gc"), "unexpected engine policy error: {error}"); + } + #[test] fn nonstandard_embedded_core_type_is_author_codec_eligible() { let schema = NoteStorageSchema::from_wit_text(EMBEDDED_CORE_SCHEMA).unwrap(); @@ -766,16 +1056,14 @@ package miden:base@1.0.0 { .to_string() .contains("the limit is") ); - assert!( - ensure_returned_felt_limit( - FIXTURE_FQN, - limits.max_returned_felts + 1, - limits.max_returned_felts - ) - .unwrap_err() - .to_string() - .contains("the limit is") - ); + let felts = ensure_returned_felt_limit( + FIXTURE_FQN, + limits.max_returned_felts + 1, + limits.max_returned_felts, + ) + .unwrap_err(); + assert!(felts.to_string().contains("the limit is"), "unexpected error: {felts}"); + assert_eq!(felts.codec_failure(), Some(CodecFailure::LimitExceeded)); } #[test] @@ -840,7 +1128,7 @@ package miden:base@1.0.0 { .expect("failed to create test package") } - /// Builds the minimal author codec used by the Phase 4a component spike. + /// Builds the minimal author codec the component adapter tests dispatch through. pub(crate) fn build_fixture_component() -> Vec { static COMPONENT: OnceLock> = OnceLock::new(); COMPONENT.get_or_init(build_fixture_component_uncached).clone() @@ -870,6 +1158,8 @@ package miden:base@1.0.0 { for &variable in NESTED_CARGO_SCRUB_ENV { command.env_remove(variable); } + // Pin the guest rustflags after the scrub, the way the compiler builds a codec crate. + command.env("RUSTFLAGS", crate::NOTE_CODEC_GUEST_RUSTFLAGS); let output = command.output().expect("failed to start fixture build"); assert_command_succeeded("building the component adapter fixture", &output); diff --git a/sdk/note-schema/src/codec_structure.rs b/sdk/note-schema/src/codec_structure.rs index 2b9b017c0e..3401383177 100644 --- a/sdk/note-schema/src/codec_structure.rs +++ b/sdk/note-schema/src/codec_structure.rs @@ -1,18 +1,51 @@ -//! Structural limits for note codec components. +//! Wasm feature and structural policy for note codec components. //! -//! The limits bound the work that parsing, compilation, and instantiation cost. That work -//! happens before any fuel budget applies, so a size cap alone does not bound it. A small -//! binary can still declare thousands of tiny functions, thousands of globals, or a deep -//! component tree. +//! Parsing, compilation, and instantiation cost work before any fuel budget applies, so a byte +//! cap alone does not bound that work. A small binary can declare thousands of tiny functions, +//! thousands of globals, or a component tree whose instantiations expand exponentially. //! //! The rules are fixed policy. The producer applies them when it attaches a codec, and every -//! consumer applies them when it loads one. The numbers follow the strict compilation limits -//! that the Miden VM Wasm event handler runner enforces. +//! consumer applies them when it loads one. [`validate_note_codec_component`] runs the whole +//! policy in one place. It checks that: +//! +//! - the component fits in the caller's byte budget; +//! - the component validates under [`NOTE_CODEC_WASM_FEATURES`]; +//! - the component declares no start function; +//! - each core module stays under its own counts, and its code section keeps a plausible +//! average function size; +//! - the component tree stays under its budgets for core modules, nesting depth, core +//! instantiations, component instantiations, defined tables, and defined memories; +//! - each component-level section stays under its width cap. +//! +//! Nothing else is checked here. The producer checks the exported codec interface, and a +//! consumer bounds the run time of a codec call with fuel and store limits. -use wasmparser::{Encoding, Parser, Payload, TypeRef}; +use wasmparser::{ + Encoding, FuncValidatorAllocations, Parser, Payload, TypeRef, ValidPayload, Validator, + WasmFeatures, +}; use crate::{Error, Result}; +/// Wasm proposals a note codec component may use. +/// +/// The producer and every consumer validate against this constant, so the accepted set does not +/// depend on the Cargo features a host compiled its engine with. The set holds the proposals the +/// `wasm32-wasip2` target emits, the component model, and floating point. Every other proposal is +/// off, including SIMD, threads, garbage collection, typed function references, and the +/// asynchronous component-model additions. +/// +/// The value is written bit by bit instead of subtracting from `WasmFeatures::default()` or +/// `WasmFeatures::all()`, so a wasmparser upgrade cannot widen the policy. +pub const NOTE_CODEC_WASM_FEATURES: WasmFeatures = WasmFeatures::COMPONENT_MODEL + .union(WasmFeatures::FLOATS) + .union(WasmFeatures::MUTABLE_GLOBAL) + .union(WasmFeatures::SATURATING_FLOAT_TO_INT) + .union(WasmFeatures::SIGN_EXTENSION) + .union(WasmFeatures::REFERENCE_TYPES) + .union(WasmFeatures::MULTI_VALUE) + .union(WasmFeatures::BULK_MEMORY); + /// Maximum functions in one core module, imported and defined. const MAX_MODULE_FUNCTIONS: usize = 10_000; @@ -60,20 +93,90 @@ const MAX_CORE_MODULES: usize = 16; /// Maximum component nesting depth. const MAX_COMPONENT_DEPTH: usize = 4; -/// Maximum entries in one component import or export section. +/// Maximum entries in one component-level section. const MAX_COMPONENT_SECTION_ITEMS: usize = 256; -/// Rejects a note codec component whose structure would make compilation or instantiation -/// expensive before any fuel applies. The rules are fixed policy, not host configuration: -/// the producer applies them at build time and every consumer applies them at load time. +/// Maximum core instantiations in the whole component tree. +/// +/// The consumer store admits the same number of instances. The budget is tree-wide because a +/// nested component is expanded once per instantiation of its parent, so per-level budgets +/// multiply. +pub(crate) const MAX_CORE_INSTANCES: usize = 32; + +/// Maximum component instantiations in the whole component tree. +/// +/// A `wasm32-wasip2` codec instantiates one component instance per exported interface. +pub(crate) const MAX_COMPONENT_INSTANCES: usize = 8; + +/// Maximum tables defined in the whole component tree. +/// +/// The consumer store admits the same number of tables. +pub(crate) const MAX_DEFINED_TABLES: usize = 32; + +/// Maximum linear memories defined in the whole component tree. +/// +/// The consumer store admits the same number of memories. +pub(crate) const MAX_DEFINED_MEMORIES: usize = 1; + +/// Applies the whole note codec component policy: the byte cap, the Wasm feature set, and the +/// structural limits. +/// +/// `max_bytes` is the caller's byte budget. The producer passes +/// [`MAX_NOTE_CODEC_COMPONENT_BYTES`](crate::MAX_NOTE_CODEC_COMPONENT_BYTES), and a consumer +/// passes the cap in its own limits. +pub fn validate_note_codec_component(component: &[u8], max_bytes: usize) -> Result<()> { + ensure_component_byte_limit(component.len(), max_bytes)?; + validate_note_codec_structure(component) +} + +/// Rejects a note codec component whose Wasm features or structure fall outside the policy. +/// +/// [`validate_note_codec_component`] is the entry point both sides call. This function is public +/// for a caller that bounds the byte length itself. pub fn validate_note_codec_structure(component: &[u8]) -> Result<()> { + let mut validator = Validator::new_with_features(NOTE_CODEC_WASM_FEATURES); + // Function bodies are validated with one reusable allocation, not one per function. + let mut allocations = FuncValidatorAllocations::default(); let mut walk = StructureWalk::default(); for payload in Parser::new(0).parse_all(component) { - walk.visit(payload.map_err(malformed)?)?; + let payload = payload.map_err(malformed)?; + // Reject a start function ahead of the validator. A start function runs guest code when + // the component is instantiated, before the export call the limits are built around, and + // the validator reports only that component values are disabled. + if matches!(payload, Payload::ComponentStartSection { .. }) { + return Err(Error::new( + "note codec component declares a start function; the policy rejects a component \ + that runs code when it is instantiated", + )); + } + if let ValidPayload::Func(function, body) = + validator.payload(&payload).map_err(rejected_feature)? + { + let mut function = function.into_validator(allocations); + function.validate(&body).map_err(rejected_feature)?; + allocations = function.into_allocations(); + } + walk.visit(payload)?; } Ok(()) } +/// Rejects component bytes over the caller's budget, before any parsing or compilation. +fn ensure_component_byte_limit(byte_len: usize, limit: usize) -> Result<()> { + if byte_len <= limit { + return Ok(()); + } + let message = + format!("note codec component is {byte_len} bytes; the pre-compilation limit is {limit}"); + // A consumer classifies the byte cap like every other cap it applies to a codec. Without the + // consumer adapter there is no failure class to report. + #[cfg(feature = "codec-component")] + let error = Error::codec(crate::CodecFailure::LimitExceeded, message); + #[cfg(not(feature = "codec-component"))] + let error = Error::new(message); + Err(error) +} + /// The state carried while the parser walks one component. #[derive(Default)] struct StructureWalk { @@ -81,6 +184,14 @@ struct StructureWalk { frames: Vec, /// Core modules seen anywhere in the component. core_modules: usize, + /// Core instantiations declared anywhere in the component. + core_instances: usize, + /// Component instantiations declared anywhere in the component. + component_instances: usize, + /// Tables defined anywhere in the component, excluding imported tables. + defined_tables: usize, + /// Linear memories defined anywhere in the component, excluding imported memories. + defined_memories: usize, } /// One nesting level of the walk. @@ -125,8 +236,10 @@ impl StructureWalk { Payload::Version { encoding, .. } => self.enter(encoding)?, Payload::End(_) => self.leave()?, Payload::TypeSection(reader) => { + // The feature validator rejects a GC type before the walk sees the section, so + // only a core function type reaches this loop. for ty in reader.into_iter_err_on_gc_types() { - let ty = ty.map_err(malformed)?; + let ty = ty.map_err(rejected_feature)?; ensure_signature_cap("parameters", ty.params().len(), MAX_FUNCTION_PARAMS)?; ensure_signature_cap("results", ty.results().len(), MAX_FUNCTION_RESULTS)?; } @@ -152,10 +265,16 @@ impl StructureWalk { self.module_counts()?.globals += reader.count() as usize; } Payload::TableSection(reader) => { - self.module_counts()?.tables += reader.count() as usize; + let count = reader.count() as usize; + self.module_counts()?.tables += count; + self.defined_tables += count; + ensure_tree_cap("defined tables", self.defined_tables, MAX_DEFINED_TABLES)?; } Payload::MemorySection(reader) => { - self.module_counts()?.memories += reader.count() as usize; + let count = reader.count() as usize; + self.module_counts()?.memories += count; + self.defined_memories += count; + ensure_tree_cap("defined memories", self.defined_memories, MAX_DEFINED_MEMORIES)?; } Payload::ElementSection(reader) => { self.module_counts()?.element_segments += reader.count() as usize; @@ -169,6 +288,30 @@ impl StructureWalk { Payload::CodeSectionStart { count, size, .. } => { ensure_average_function_size(count, size)?; } + Payload::InstanceSection(reader) => { + self.core_instances += reader.count() as usize; + ensure_tree_cap("core instantiations", self.core_instances, MAX_CORE_INSTANCES)?; + } + Payload::ComponentInstanceSection(reader) => { + self.component_instances += reader.count() as usize; + ensure_tree_cap( + "component instantiations", + self.component_instances, + MAX_COMPONENT_INSTANCES, + )?; + } + Payload::CoreTypeSection(reader) => { + ensure_component_section_cap("core types", reader.count() as usize)?; + } + Payload::ComponentTypeSection(reader) => { + ensure_component_section_cap("types", reader.count() as usize)?; + } + Payload::ComponentAliasSection(reader) => { + ensure_component_section_cap("aliases", reader.count() as usize)?; + } + Payload::ComponentCanonicalSection(reader) => { + ensure_component_section_cap("canonical functions", reader.count() as usize)?; + } Payload::ComponentImportSection(reader) => { ensure_component_section_cap("imports", reader.count() as usize)?; } @@ -242,6 +385,16 @@ fn ensure_module_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { Ok(()) } +/// Reports a component tree that is over one of its whole-tree budgets. +fn ensure_tree_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { + if observed > limit { + return Err(Error::new(format!( + "note codec component has {observed} {kind}; the limit is {limit}" + ))); + } + Ok(()) +} + /// Reports a core function type that is over its parameter or result cap. fn ensure_signature_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { if observed > limit { @@ -252,7 +405,7 @@ fn ensure_signature_cap(kind: &str, observed: usize, limit: usize) -> Result<()> Ok(()) } -/// Reports a component import or export section that is over its cap. +/// Reports a component-level section that is over its width cap. fn ensure_component_section_cap(kind: &str, observed: usize) -> Result<()> { if observed > MAX_COMPONENT_SECTION_ITEMS { return Err(Error::new(format!( @@ -284,6 +437,11 @@ fn malformed(error: wasmparser::BinaryReaderError) -> Error { Error::new(format!("note codec component is malformed: {error}")) } +/// Reports a component that does not validate under [`NOTE_CODEC_WASM_FEATURES`]. +fn rejected_feature(error: wasmparser::BinaryReaderError) -> Error { + Error::new(format!("note codec component uses a Wasm feature the policy rejects: {error}")) +} + #[cfg(test)] mod tests { use super::*; @@ -293,15 +451,126 @@ mod tests { wat::parse_str(format!("(component (core module {core_module}))")).unwrap() } + /// Validates one component with a byte budget that never trips. + fn validate(component: &[u8]) -> Result<()> { + validate_note_codec_component(component, usize::MAX) + } + #[test] fn minimal_component_passes() { - validate_note_codec_structure(&component("(func)")).unwrap(); + validate(&component("(func)")).unwrap(); + } + + #[test] + fn the_wasm_feature_policy_is_exact() { + // Destructuring is exhaustive on purpose: a wasmparser upgrade that adds a proposal + // fails to compile here, so the policy cannot widen without a decision. + let wasmparser::WasmFeaturesInflated { + mutable_global, + saturating_float_to_int, + sign_extension, + reference_types, + multi_value, + bulk_memory, + simd, + relaxed_simd, + threads, + shared_everything_threads, + tail_call, + floats, + multi_memory, + exceptions, + memory64, + extended_const, + component_model, + function_references, + memory_control, + gc, + custom_page_sizes, + legacy_exceptions, + gc_types, + stack_switching, + wide_arithmetic, + cm_values, + cm_nested_names, + cm_async, + cm_async_stackful, + cm_more_async_builtins, + cm_threading, + cm_error_context, + cm_fixed_length_lists, + cm_gc, + call_indirect_overlong, + bulk_memory_opt, + custom_descriptors, + compact_imports, + cm_map, + cm64, + } = NOTE_CODEC_WASM_FEATURES.inflate(); + + for (name, required) in [ + ("component model", component_model), + ("floats", floats), + ("mutable globals", mutable_global), + ("saturating float to int", saturating_float_to_int), + ("sign extension", sign_extension), + ("reference types", reference_types), + ("call-indirect overlong", call_indirect_overlong), + ("multi value", multi_value), + ("bulk memory", bulk_memory), + ("bulk memory opt", bulk_memory_opt), + ] { + assert!(required, "the policy must accept {name}"); + } + + for (name, rejected) in [ + ("simd", simd), + ("relaxed simd", relaxed_simd), + ("threads", threads), + ("shared everything threads", shared_everything_threads), + ("tail call", tail_call), + ("multi memory", multi_memory), + ("exceptions", exceptions), + ("legacy exceptions", legacy_exceptions), + ("memory64", memory64), + ("extended const", extended_const), + ("function references", function_references), + ("memory control", memory_control), + ("gc", gc), + ("gc types", gc_types), + ("custom page sizes", custom_page_sizes), + ("stack switching", stack_switching), + ("wide arithmetic", wide_arithmetic), + ("component model values", cm_values), + ("component model nested names", cm_nested_names), + ("component model async", cm_async), + ("component model stackful async", cm_async_stackful), + ("component model async builtins", cm_more_async_builtins), + ("component model threading", cm_threading), + ("component model error context", cm_error_context), + ("component model fixed length lists", cm_fixed_length_lists), + ("component model gc", cm_gc), + ("component model maps", cm_map), + ("component model 64-bit contexts", cm64), + ("custom descriptors", custom_descriptors), + ("compact imports", compact_imports), + ] { + assert!(!rejected, "the policy must reject {name}"); + } + } + + #[test] + fn oversized_components_are_rejected_before_parsing() { + let error = validate_note_codec_component(&[0; 8], 4).unwrap_err().to_string(); + + assert!(error.contains("is 8 bytes"), "unexpected error: {error}"); + assert!(error.contains("the pre-compilation limit is 4"), "unexpected error: {error}"); } #[test] fn too_many_globals_are_rejected() { let globals = "(global i32 (i32.const 0))".repeat(MAX_MODULE_GLOBALS + 1); - let error = validate_note_codec_structure(&component(&globals)).unwrap_err().to_string(); + let error = validate(&component(&globals)).unwrap_err().to_string(); assert!(error.contains("1001 globals"), "unexpected error: {error}"); assert!(error.contains("the limit is 1000"), "unexpected error: {error}"); @@ -311,7 +580,7 @@ mod tests { fn oversized_function_signatures_are_rejected() { let params = "i32 ".repeat(MAX_FUNCTION_PARAMS + 1); let module = format!("(type (func (param {params})))"); - let error = validate_note_codec_structure(&component(&module)).unwrap_err().to_string(); + let error = validate(&component(&module)).unwrap_err().to_string(); assert!(error.contains("33 parameters"), "unexpected error: {error}"); assert!(error.contains("the limit is 32"), "unexpected error: {error}"); @@ -319,9 +588,7 @@ mod tests { #[test] fn many_tiny_functions_are_rejected() { - let error = validate_note_codec_structure(&component(&"(func)".repeat(600))) - .unwrap_err() - .to_string(); + let error = validate(&component(&"(func)".repeat(600))).unwrap_err().to_string(); assert!(error.contains("600 functions"), "unexpected error: {error}"); assert!(error.contains("bytes per function"), "unexpected error: {error}"); @@ -329,11 +596,61 @@ mod tests { #[test] fn malformed_bytes_are_rejected() { - let error = validate_note_codec_structure(b"not a component").unwrap_err().to_string(); + let error = validate(b"not a component").unwrap_err().to_string(); assert!(error.contains("note codec component is malformed"), "unexpected error: {error}"); } + #[test] + fn nested_component_instantiations_are_rejected() { + // Three nesting levels that each instantiate the level below 300 times. A per-level cap + // would still admit 300^3 expansions, so the budget counts the whole tree. + let instantiate = |name: &str| format!("(instance (instantiate {name}))").repeat(300); + let text = format!( + "(component + (component $outer + (component $middle + (component $inner (core module)) + {inner_uses}) + {middle_uses}) + {outer_uses})", + inner_uses = instantiate("$inner"), + middle_uses = instantiate("$middle"), + outer_uses = instantiate("$outer"), + ); + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!(error.contains("component instantiations"), "unexpected error: {error}"); + assert!( + error.contains(&format!("the limit is {MAX_COMPONENT_INSTANCES}")), + "unexpected error: {error}" + ); + } + + #[test] + fn defined_memories_are_counted_across_core_modules() { + let text = "(component (core module (memory 1)) (core module (memory 1)))"; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!(error.contains("2 defined memories"), "unexpected error: {error}"); + assert!( + error.contains(&format!("the limit is {MAX_DEFINED_MEMORIES}")), + "unexpected error: {error}" + ); + } + + #[test] + fn component_start_functions_are_rejected() { + let text = r#"(component + (core module $m (func (export "f"))) + (core instance $i (instantiate $m)) + (func $f (canon lift (core func $i "f"))) + (start $f))"#; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!(error.contains("declares a start function"), "unexpected error: {error}"); + } + #[cfg(feature = "codec-component")] #[test] fn fixture_component_passes() { @@ -342,7 +659,6 @@ mod tests { return; } - validate_note_codec_structure(&crate::codec_component::tests::build_fixture_component()) - .unwrap(); + validate(&crate::codec_component::tests::build_fixture_component()).unwrap(); } } diff --git a/sdk/note-schema/src/error.rs b/sdk/note-schema/src/error.rs index fcd015c889..d2644b3374 100644 --- a/sdk/note-schema/src/error.rs +++ b/sdk/note-schema/src/error.rs @@ -2,12 +2,16 @@ use core::fmt; -/// Why a bundled codec call did not return a value. +/// Why a bundled codec did not return a value. +/// +/// Only the bundled-codec adapter reports a class. It covers the whole life of a codec call: +/// the load of the component, the instantiation that precedes the call, the call itself, and +/// the host caps applied to what the call returned. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CodecFailure { /// The call used its whole fuel budget. OutOfFuel, - /// The call hit a memory or table limit. + /// A limit the host applies was exceeded, in the guest or in the returned value. LimitExceeded, /// The component trapped, or the engine rejected the call. Trapped, @@ -44,7 +48,8 @@ impl Error { /// Returns the failure class of a bundled codec call. /// - /// Errors from other sources return `None`. + /// Errors from other sources return `None`. Without the `codec-component` feature there is + /// no bundled-codec adapter, so every error returns `None`. pub fn codec_failure(&self) -> Option { self.codec_failure } diff --git a/sdk/note-schema/src/lib.rs b/sdk/note-schema/src/lib.rs index b92d8de24b..491bd27319 100644 --- a/sdk/note-schema/src/lib.rs +++ b/sdk/note-schema/src/lib.rs @@ -36,7 +36,9 @@ pub use codec::{ }; #[cfg(feature = "codec-component")] pub use codec_component::CodecLimits; -pub use codec_structure::validate_note_codec_structure; +pub use codec_structure::{ + NOTE_CODEC_WASM_FEATURES, validate_note_codec_component, validate_note_codec_structure, +}; pub use error::{CodecFailure, Error, Result}; pub use miden_field::Felt; pub use miden_protocol::note::NoteStorage; From 9204cbcfdc9d635e2854500b41a518bb3ae5a70a Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 8 Sep 2026 17:57:00 +0300 Subject: [PATCH 35/43] fix: bound schema expansion and keep generated code robust to hostile names A chain of empty records doubled forty times passes every schema limit with zero felts, yet decoding, builder validation, and code generation expand its tree exponentially. Every resolved type now carries its expanded node count next to its layout, and resolution rejects a type above a fixed budget, so one check protects every traversal on both the producer and the consumer. The native felt-repr check in the code generator is memoized by type identity. A schema failure in `#[note]` discarded the struct, so every use site reported a missing type and hid the real diagnostic. The struct is now emitted next to the error. Generated bindings and codec dispatch code used unqualified prelude names, so a WIT record named `vec`, `string`, or `option` shadowed them inside the generated module. Every emitted prelude type is now fully qualified, and a binding test compiles such a schema. --- sdk/base-macros/src/note.rs | 70 ++++++++++++++++++++- sdk/base-macros/src/test_support.rs | 34 +++++++++- sdk/base-macros/src/types/tests.rs | 31 +-------- sdk/note-bindings/macros/expected/custom.rs | 24 ++++--- sdk/note-bindings/macros/expected/p2id.rs | 24 ++++--- sdk/note-bindings/macros/src/lib.rs | 18 ++++-- sdk/note-bindings/tests/generated_custom.rs | 38 +++++++++++ sdk/note-codec/macros/src/expand.rs | 19 ++++-- sdk/note-schema/codegen/src/lib.rs | 60 +++++++++++++----- sdk/note-schema/src/lib.rs | 6 +- sdk/note-schema/src/schema.rs | 55 ++++++++++++++-- sdk/note-schema/src/tests.rs | 55 +++++++++++++++- 12 files changed, 351 insertions(+), 83 deletions(-) diff --git a/sdk/base-macros/src/note.rs b/sdk/base-macros/src/note.rs index aa8c8373d7..bad7322ea1 100644 --- a/sdk/base-macros/src/note.rs +++ b/sdk/base-macros/src/note.rs @@ -160,7 +160,15 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { syn::Fields::Named(fields) => { let schema_static = match expand_note_storage_schema(&item_struct) { Ok(schema_static) => schema_static, - Err(err) => return err.into_compile_error(), + // The struct is emitted with the error so that the schema diagnostic is not + // buried under "cannot find type" errors from every use site. + Err(err) => { + let error = err.into_compile_error(); + return quote! { + #item_struct + #error + }; + } }; let field_inits = fields.named.iter().map(|field| { let ident = field.ident.as_ref().expect("named fields must have identifiers"); @@ -1180,7 +1188,10 @@ mod tests { use syn::parse_quote; use super::*; - use crate::types::{lock_export_type_registry_for_tests, reset_export_type_registry_for_tests}; + use crate::{ + test_support::compile_rust_source, + types::{lock_export_type_registry_for_tests, reset_export_type_registry_for_tests}, + }; #[test] fn named_note_struct_emits_storage_schema_static() { @@ -1211,6 +1222,61 @@ mod tests { assert!(tokens.contains(crate::note_schema::NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD_SYMBOL)); } + #[test] + fn schema_failure_keeps_the_note_struct_next_to_the_error() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let item_struct: ItemStruct = parse_quote! { + struct VecNote { + values: Vec, + } + }; + + let tokens = expand_note_struct(item_struct).to_string(); + + assert!(tokens.contains("compile_error"), "the schema error must be reported: {tokens}"); + assert!(tokens.contains("struct VecNote"), "the struct must survive the error: {tokens}"); + assert!( + !tokens.contains("__MIDEN_NOTE_STORAGE_SCHEMA_BYTES"), + "no schema metadata is generated for a failed schema: {tokens}" + ); + } + + #[test] + fn schema_failure_reports_only_the_schema_diagnostic() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let item_struct: ItemStruct = parse_quote! { + struct VecNote { + values: Vec, + } + }; + let expansion = expand_note_struct(item_struct); + let source = format!( + r#" +mod user {{ + {expansion} + pub fn takes_note(_note: VecNote) {{}} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a failed note schema must fail the compilation"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("`Vec` is not supported in note storage schemas yet"), + "the schema diagnostic is missing: +{stderr}" + ); + assert!( + !stderr.contains("cannot find type"), + "the schema diagnostic must not cascade into missing-type errors: +{stderr}" + ); + } + #[test] fn tuple_note_struct_requires_named_fields() { let item_struct: ItemStruct = parse_quote!( diff --git a/sdk/base-macros/src/test_support.rs b/sdk/base-macros/src/test_support.rs index 3aa27cacfa..904959ba46 100644 --- a/sdk/base-macros/src/test_support.rs +++ b/sdk/base-macros/src/test_support.rs @@ -1,6 +1,12 @@ //! Shared fixtures for base-macros unit tests. -use std::{fs, path::Path, sync::Arc}; +use std::{ + env, fs, + io::Write, + path::Path, + process::{Command, Output, Stdio}, + sync::Arc, +}; use miden_assembly::{Assembler, DefaultSourceManager, ModuleParser, ast::ModuleKind}; use miden_mast_package::Package; @@ -38,3 +44,29 @@ pub(crate) fn write_masp_fixture(package_path: &Path, package_id: &str, wit: Opt .expect("package directory must be created"); fs::write(package_path, package.to_bytes()).expect("package fixture must be written"); } + +/// Compiles one standalone Rust source string and returns the rustc result. +/// +/// The source is compiled to metadata only, so a test can assert on the diagnostics that a macro +/// expansion produces in a real compilation. +pub(crate) fn compile_rust_source(source: &str) -> Output { + let output_dir = tempfile::tempdir().expect("failed to create rustc output directory"); + let output_path = output_dir.path().join("macro_expansion.rmeta"); + let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + let mut child = Command::new(rustc) + .args(["--crate-name", "macro_expansion", "--edition=2024", "--emit=metadata", "-o"]) + .arg(output_path) + .arg("-") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to start rustc for a macro expansion test"); + child + .stdin + .take() + .expect("rustc stdin must be piped") + .write_all(source.as_bytes()) + .expect("failed to write the macro expansion source"); + child.wait_with_output().expect("failed to wait for rustc") +} diff --git a/sdk/base-macros/src/types/tests.rs b/sdk/base-macros/src/types/tests.rs index 7835eafc99..3a011aeaad 100644 --- a/sdk/base-macros/src/types/tests.rs +++ b/sdk/base-macros/src/types/tests.rs @@ -1,13 +1,9 @@ -use std::{ - collections::HashSet, - env, - io::Write, - process::{Command, Output, Stdio}, -}; +use std::collections::HashSet; use syn::parse_quote; use super::*; +use crate::test_support::compile_rust_source; #[test] fn emits_hint_for_missing_export_type() { @@ -865,26 +861,3 @@ fn main() {{}} String::from_utf8_lossy(&output.stderr) ); } - -/// Compiles one standalone Rust source string for nominal identity-guard tests. -fn compile_rust_source(source: &str) -> Output { - let output_dir = tempfile::tempdir().expect("failed to create rustc output directory"); - let output_path = output_dir.path().join("identity_guard.rmeta"); - let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); - let mut child = Command::new(rustc) - .args(["--crate-name", "identity_guard", "--edition=2024", "--emit=metadata", "-o"]) - .arg(output_path) - .arg("-") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("failed to start rustc for an identity-guard test"); - child - .stdin - .take() - .expect("rustc stdin must be piped") - .write_all(source.as_bytes()) - .expect("failed to write the identity-guard source"); - child.wait_with_output().expect("failed to wait for rustc") -} diff --git a/sdk/note-bindings/macros/expected/custom.rs b/sdk/note-bindings/macros/expected/custom.rs index fc127006f9..bb5f239a48 100644 --- a/sdk/note-bindings/macros/expected/custom.rs +++ b/sdk/note-bindings/macros/expected/custom.rs @@ -297,7 +297,7 @@ mod __miden_note_bindings_a3280bdaca3ec21e { }) } } - impl __MidenNoteEncode for Option + impl __MidenNoteEncode for ::core::option::Option where T: __MidenNoteEncode, { @@ -321,7 +321,7 @@ mod __miden_note_bindings_a3280bdaca3ec21e { Ok(()) } } - impl __MidenNoteDecode for Option + impl __MidenNoteDecode for ::core::option::Option where T: __MidenNoteDecode, { @@ -535,7 +535,7 @@ mod __miden_note_bindings_a3280bdaca3ec21e { ) -> ::miden_note_bindings::__private::miden_note_schema::Result< ::miden_note_bindings::__private::miden_note_schema::NoteStorage, > { - let mut felts = Vec::new(); + let mut felts = ::std::vec::Vec::new(); self.__write_note_felts( &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter::new( &mut felts, @@ -567,7 +567,10 @@ mod __miden_note_bindings_a3280bdaca3ec21e { } /// Builds a typed value with a caller-provided codec registry. pub fn from_str_values_with( - values: &::std::collections::BTreeMap, + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, ) -> ::miden_note_bindings::__private::miden_note_schema::Result { let schema = __miden_note_storage_schema()?; @@ -580,7 +583,10 @@ mod __miden_note_bindings_a3280bdaca3ec21e { } /// Builds a typed value with the standard codec registry. pub fn from_str_values( - values: &::std::collections::BTreeMap, + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, ) -> ::miden_note_bindings::__private::miden_note_schema::Result { let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); Self::from_str_values_with(values, &codecs) @@ -605,7 +611,9 @@ mod __miden_note_bindings_a3280bdaca3ec21e { pub fn display_with( &self, codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, - ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::std::string::String, + > { let storage = self.to_note_storage()?; let decoded = __miden_note_storage_schema()? .decode_with_registry(&storage, codecs)?; @@ -614,7 +622,9 @@ mod __miden_note_bindings_a3280bdaca3ec21e { /// Displays this value with standard codecs and structural fallbacks. pub fn display( &self, - ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::std::string::String, + > { let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); self.display_with(&codecs) } diff --git a/sdk/note-bindings/macros/expected/p2id.rs b/sdk/note-bindings/macros/expected/p2id.rs index fb1d75cbbd..e4091e5951 100644 --- a/sdk/note-bindings/macros/expected/p2id.rs +++ b/sdk/note-bindings/macros/expected/p2id.rs @@ -297,7 +297,7 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { }) } } - impl __MidenNoteEncode for Option + impl __MidenNoteEncode for ::core::option::Option where T: __MidenNoteEncode, { @@ -321,7 +321,7 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { Ok(()) } } - impl __MidenNoteDecode for Option + impl __MidenNoteDecode for ::core::option::Option where T: __MidenNoteDecode, { @@ -401,7 +401,7 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { ) -> ::miden_note_bindings::__private::miden_note_schema::Result< ::miden_note_bindings::__private::miden_note_schema::NoteStorage, > { - let mut felts = Vec::new(); + let mut felts = ::std::vec::Vec::new(); self.__write_note_felts( &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter::new( &mut felts, @@ -433,7 +433,10 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { } /// Builds a typed value with a caller-provided codec registry. pub fn from_str_values_with( - values: &::std::collections::BTreeMap, + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, ) -> ::miden_note_bindings::__private::miden_note_schema::Result { let schema = __miden_note_storage_schema()?; @@ -446,7 +449,10 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { } /// Builds a typed value with the standard codec registry. pub fn from_str_values( - values: &::std::collections::BTreeMap, + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, ) -> ::miden_note_bindings::__private::miden_note_schema::Result { let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); Self::from_str_values_with(values, &codecs) @@ -471,7 +477,9 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { pub fn display_with( &self, codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, - ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::std::string::String, + > { let storage = self.to_note_storage()?; let decoded = __miden_note_storage_schema()? .decode_with_registry(&storage, codecs)?; @@ -480,7 +488,9 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { /// Displays this value with standard codecs and structural fallbacks. pub fn display( &self, - ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::std::string::String, + > { let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); self.display_with(&codecs) } diff --git a/sdk/note-bindings/macros/src/lib.rs b/sdk/note-bindings/macros/src/lib.rs index 9d75499e04..ce1d9e4e68 100644 --- a/sdk/note-bindings/macros/src/lib.rs +++ b/sdk/note-bindings/macros/src/lib.rs @@ -123,7 +123,7 @@ fn expand_schema( pub fn to_note_storage( &self, ) -> #runtime::miden_note_schema::Result<#runtime::miden_note_schema::NoteStorage> { - let mut felts = Vec::new(); + let mut felts = ::std::vec::Vec::new(); self.__write_note_felts( &mut #runtime::miden_field_repr::FeltWriter::new(&mut felts), )?; @@ -150,7 +150,10 @@ fn expand_schema( /// Builds a typed value with a caller-provided codec registry. pub fn from_str_values_with( - values: &::std::collections::BTreeMap, + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, codecs: &#runtime::miden_note_schema::CodecRegistry, ) -> #runtime::miden_note_schema::Result { let schema = __miden_note_storage_schema()?; @@ -164,7 +167,10 @@ fn expand_schema( /// Builds a typed value with the standard codec registry. pub fn from_str_values( - values: &::std::collections::BTreeMap, + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, ) -> #runtime::miden_note_schema::Result { let codecs = #runtime::miden_note_schema::CodecRegistry::with_standard_codecs(); @@ -192,7 +198,7 @@ fn expand_schema( pub fn display_with( &self, codecs: &#runtime::miden_note_schema::CodecRegistry, - ) -> #runtime::miden_note_schema::Result { + ) -> #runtime::miden_note_schema::Result<::std::string::String> { let storage = self.to_note_storage()?; let decoded = __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; @@ -200,7 +206,9 @@ fn expand_schema( } /// Displays this value with standard codecs and structural fallbacks. - pub fn display(&self) -> #runtime::miden_note_schema::Result { + pub fn display( + &self, + ) -> #runtime::miden_note_schema::Result<::std::string::String> { let codecs = #runtime::miden_note_schema::CodecRegistry::with_standard_codecs(); self.display_with(&codecs) diff --git a/sdk/note-bindings/tests/generated_custom.rs b/sdk/note-bindings/tests/generated_custom.rs index 5391481b73..103c13423b 100644 --- a/sdk/note-bindings/tests/generated_custom.rs +++ b/sdk/note-bindings/tests/generated_custom.rs @@ -94,3 +94,41 @@ fn custom_record_string_paths_use_the_registry_parameter() { } ); } + +/// Bindings whose WIT type and field names collide with Rust prelude names. +/// +/// The generated items shadow the prelude inside the generated module, so the test only compiles +/// when every prelude name in the generated code is fully qualified. +mod prelude_names { + miden_note_bindings::from_wit_text!( + r#" +package example:prelude-schema@1.0.0; + +interface note-storage { + record %option { + %string: u64, + } + + record %vec { + %string: u32, + %result: %option, + maybe: option, + } + + type storage = %vec; +} +"# + ); + + #[test] + fn prelude_named_types_round_trip() { + let value = Vec { + string: 9, + result: Option { string: 4 }, + maybe: ::core::option::Option::Some(1), + }; + let storage = value.to_note_storage().unwrap(); + assert_eq!(Vec::from_note_storage(&storage).unwrap(), value); + value.validate().unwrap(); + } +} diff --git a/sdk/note-codec/macros/src/expand.rs b/sdk/note-codec/macros/src/expand.rs index 225da9c591..f1f491c3fb 100644 --- a/sdk/note-codec/macros/src/expand.rs +++ b/sdk/note-codec/macros/src/expand.rs @@ -110,7 +110,7 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { quote! { #fqn => { let value = <#ty as #facade::AuthorTypeCodec>::parse(value)?; - let mut felts = Vec::new(); + let mut felts = ::std::vec::Vec::new(); <#ty as __MidenNoteEncode>::__write_note_felts( &value, &mut #facade::__private::miden_field_repr::FeltWriter::new( @@ -176,12 +176,15 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { use super::*; /// Returns all supported canonical WIT FQNs. - pub fn supported_types() -> Vec { + pub fn supported_types() -> ::std::vec::Vec<::std::string::String> { vec![#(#fqns.to_owned()),*] } /// Parses one value through its marked author codec. - pub fn parse(type_fqn: &str, value: &str) -> Result, String> { + pub fn parse( + type_fqn: &str, + value: &str, + ) -> ::core::result::Result<::std::vec::Vec, ::std::string::String> { match type_fqn { #(#parse_arms,)* _ => Err(format!( @@ -191,7 +194,10 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { } /// Displays one value through its marked author codec. - pub fn display(type_fqn: &str, value: &[u64]) -> Result { + pub fn display( + type_fqn: &str, + value: &[u64], + ) -> ::core::result::Result<::std::string::String, ::std::string::String> { match type_fqn { #(#display_arms,)* _ => Err(format!( @@ -201,7 +207,10 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { } /// Validates one value through its marked author codec. - pub fn validate(type_fqn: &str, value: &[u64]) -> Result<(), String> { + pub fn validate( + type_fqn: &str, + value: &[u64], + ) -> ::core::result::Result<(), ::std::string::String> { match type_fqn { #(#validate_arms,)* _ => Err(format!( diff --git a/sdk/note-schema/codegen/src/lib.rs b/sdk/note-schema/codegen/src/lib.rs index 5fe4eac560..6215b60fa4 100644 --- a/sdk/note-schema/codegen/src/lib.rs +++ b/sdk/note-schema/codegen/src/lib.rs @@ -3,7 +3,7 @@ #![deny(missing_docs)] use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap}, fmt, }; @@ -153,9 +153,10 @@ pub fn generate_host_types( let root_ident = rust_names.get(root_fqn).cloned().unwrap_or_else(|| type_ident(root_name)); let helper_traits = generate_helper_traits(runtime); + let mut felt_repr_support = FeltReprSupport::default(); let items = definitions .iter() - .map(|definition| generate_type(definition, &rust_names, runtime)) + .map(|definition| generate_type(definition, &rust_names, runtime, &mut felt_repr_support)) .collect::, _>>()?; let type_idents = definitions .iter() @@ -348,7 +349,7 @@ fn generate_helper_traits(runtime: &RuntimePaths) -> TokenStream { } } - impl __MidenNoteEncode for Option + impl __MidenNoteEncode for ::core::option::Option where T: __MidenNoteEncode, { @@ -367,7 +368,7 @@ fn generate_helper_traits(runtime: &RuntimePaths) -> TokenStream { } } - impl __MidenNoteDecode for Option + impl __MidenNoteDecode for ::core::option::Option where T: __MidenNoteDecode, { @@ -396,11 +397,12 @@ fn generate_type( definition: &SchemaType, rust_names: &BTreeMap, runtime: &RuntimePaths, + felt_repr_support: &mut FeltReprSupport, ) -> Result { let fqn = definition.fqn().expect("generated type definitions always have a FQN"); let ident = rust_names.get(fqn).expect("every generated type has a Rust identifier"); let docs = type_docs(definition, fqn); - let derives = if supports_native_felt_repr(definition) { + let derives = if felt_repr_support.supports_native_felt_repr(definition) { let miden_field_repr = &runtime.miden_field_repr; let crate_path = Literal::string(&miden_field_repr.to_string().replace(' ', "")); quote! { @@ -633,7 +635,7 @@ fn rust_type( SchemaTypeKind::Primitive(PrimitiveType::Bool) => Ok(quote!(bool)), SchemaTypeKind::Option(payload) => { let payload = rust_type(payload, rust_names, runtime)?; - Ok(quote!(Option<#payload>)) + Ok(quote!(::core::option::Option<#payload>)) } SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_) => Err(CodegenError::new(format!( "named WIT type `{}` was not collected for Rust generation", @@ -661,19 +663,43 @@ fn mapped_leaf(ty: &SchemaType, runtime: &RuntimePaths) -> Option { } } -/// Returns true when all fields implement the native felt-repr traits without protocol adapters. -fn supports_native_felt_repr(ty: &SchemaType) -> bool { - if matches!(ty.standard_leaf(), Some(StandardLeaf::AccountId | StandardLeaf::AssetAmount)) { - return false; +/// Memoized answers for the felt-repr support walk, keyed by resolved node identity. +#[derive(Default)] +struct FeltReprSupport { + answers: HashMap<*const SchemaType, bool>, +} + +impl FeltReprSupport { + /// Returns true when all fields implement the native felt-repr traits without protocol + /// adapters. + /// + /// The schema model is a DAG that shares one node per resolved WIT type, so the answers are + /// memoized by node identity. The walk is then linear in the number of distinct types instead + /// of the size of the expanded tree. + fn supports_native_felt_repr(&mut self, ty: &SchemaType) -> bool { + let key = ty as *const SchemaType; + if let Some(answer) = self.answers.get(&key) { + return *answer; + } + let answer = self.compute(ty); + self.answers.insert(key, answer); + answer } - match ty.kind() { - SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => true, - SchemaTypeKind::Record(fields) => { - fields.iter().all(|field| supports_native_felt_repr(field.ty())) + + /// Answers the felt-repr question for one node from its children. + fn compute(&mut self, ty: &SchemaType) -> bool { + if matches!(ty.standard_leaf(), Some(StandardLeaf::AccountId | StandardLeaf::AssetAmount)) { + return false; } - SchemaTypeKind::Option(payload) => supports_native_felt_repr(payload), - SchemaTypeKind::Variant(cases) => { - cases.iter().all(|case| case.payload().is_none_or(supports_native_felt_repr)) + match ty.kind() { + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => true, + SchemaTypeKind::Record(fields) => { + fields.iter().all(|field| self.supports_native_felt_repr(field.ty())) + } + SchemaTypeKind::Option(payload) => self.supports_native_felt_repr(payload), + SchemaTypeKind::Variant(cases) => cases.iter().all(|case| { + case.payload().is_none_or(|payload| self.supports_native_felt_repr(payload)) + }), } } } diff --git a/sdk/note-schema/src/lib.rs b/sdk/note-schema/src/lib.rs index 491bd27319..39ad7efa0a 100644 --- a/sdk/note-schema/src/lib.rs +++ b/sdk/note-schema/src/lib.rs @@ -44,8 +44,8 @@ pub use miden_field::Felt; pub use miden_protocol::note::NoteStorage; pub use schema::{ FeltLayout, MAX_NOTE_CODEC_COMPONENT_BYTES, MAX_NOTE_STORAGE_SCHEMA_BYTES, - MAX_NOTE_STORAGE_SCHEMA_DEPTH, MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_TYPES, - NOTE_CODEC_GUEST_RUSTFLAGS, NoteStorageSchema, PrimitiveType, SchemaCase, SchemaField, - SchemaType, SchemaTypeKind, + MAX_NOTE_STORAGE_SCHEMA_DEPTH, MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_NODES, + MAX_NOTE_STORAGE_SCHEMA_TYPES, NOTE_CODEC_GUEST_RUSTFLAGS, NoteStorageSchema, PrimitiveType, + SchemaCase, SchemaField, SchemaType, SchemaTypeKind, }; pub use value::{DecodedValue, DecodedValueKind}; diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs index 9e5c2367b2..dbf6b0128d 100644 --- a/sdk/note-schema/src/schema.rs +++ b/sdk/note-schema/src/schema.rs @@ -37,14 +37,29 @@ pub const MAX_NOTE_STORAGE_SCHEMA_DEPTH: usize = MAX_NOTE_STORAGE_ITEMS / 8; /// Maximum number of felts in the root note storage layout. pub const MAX_NOTE_STORAGE_SCHEMA_FELTS: usize = MAX_NOTE_STORAGE_ITEMS; -/// Maximum bytes accepted for one note codec component before Wasmtime compilation. +/// Maximum number of nodes in the expanded note storage schema tree. /// -/// The compiler enforces the same limit when it attaches a codec, so a package that -/// builds is a package that consumers accept. +/// The resolved model is a DAG, but every structural walk over it expands that DAG into a tree: +/// decoding, builder validation, and Rust code generation all visit a shared type once per +/// reference. A schema of zero-felt records that names one type per level stays below the byte, +/// type, depth, and felt limits while the expanded tree doubles at each level, so this budget +/// bounds the expanded tree directly. It allows four expanded nodes per protocol note-storage +/// item, which is far above any practical model. +pub const MAX_NOTE_STORAGE_SCHEMA_NODES: usize = MAX_NOTE_STORAGE_ITEMS * 4; + +/// Default maximum bytes accepted for one note codec component before Wasmtime compilation. +/// +/// This is the producer's cap and the default of `CodecLimits::max_component_bytes`, the +/// consumer-side policy struct behind the `codec-component` feature, so a package that builds is +/// a package that consumers accept. A host may tighten its own consumer cap. pub const MAX_NOTE_CODEC_COMPONENT_BYTES: usize = 4 * 1024 * 1024; /// Rustflags the nested codec build pins, so a codec crate's own cargo config cannot enable -/// a Wasm feature that every consumer rejects. Mirrors the VM event-handler plugin. +/// `simd128` in the guest. Mirrors the VM event-handler plugin. +/// +/// The pin covers one target feature only. The full policy is +/// [`NOTE_CODEC_WASM_FEATURES`](crate::NOTE_CODEC_WASM_FEATURES), which the producer enforces at +/// build time and every consumer enforces at load time. pub const NOTE_CODEC_GUEST_RUSTFLAGS: &str = "-C target-feature=-simd128"; const _: () = assert!(MAX_NOTE_STORAGE_SCHEMA_DEPTH > 0); @@ -387,6 +402,22 @@ fn ensure_root_layout_limit(layout: FeltLayout) -> Result<()> { Ok(()) } +/// Enforces the expanded-tree budget on one resolved schema type. +/// +/// The builder memoizes shared types, so resolution stays linear. Every consumer of the model +/// walks it as a tree, so the budget is applied to the expanded node count of each type as it is +/// resolved. The check therefore protects decoding, builder validation, and code generation. +fn ensure_expanded_node_limit(ty: &SchemaType, expanded_nodes: usize) -> Result<()> { + let limit = MAX_NOTE_STORAGE_SCHEMA_NODES; + if expanded_nodes > limit { + let name = ty.fqn().or_else(|| ty.name()).unwrap_or(""); + return Err(Error::new(format!( + "note storage type `{name}` expands to {expanded_nodes} nodes; the limit is {limit}" + ))); + } + Ok(()) +} + /// Verifies the raw embedded core-types definitions before the model applies native mappings. fn validate_resolved_core_types(resolve: &Resolve) -> Result<()> { let Some((_, package_id)) = resolve.package_names.iter().find(|(name, _)| { @@ -609,11 +640,13 @@ fn collect_custom_type_fqns( } } -/// One memoized schema node and its maximum depth below that node. +/// One memoized schema node, its maximum depth, and the size of its expanded subtree. #[derive(Clone)] struct MemoizedSchemaType { ty: Arc, maximum_subtree_depth: usize, + /// Number of nodes a structural walk visits below and including this node. + expanded_nodes: usize, } /// Builds a memoized schema graph from a resolved WIT graph. @@ -691,6 +724,7 @@ impl<'a> ModelBuilder<'a> { layout: FeltLayout::fixed(1), }), maximum_subtree_depth: 0, + expanded_nodes: 1, }) } else { match definition.kind { @@ -699,10 +733,12 @@ impl<'a> ModelBuilder<'a> { let mut fields = Vec::with_capacity(record.fields.len()); let mut layout = FeltLayout::fixed(0); let mut maximum_subtree_depth = 0; + let mut expanded_nodes = 1usize; for field in record.fields { let memoized = self.build_type(field.ty, depth + 1)?; maximum_subtree_depth = maximum_subtree_depth.max(1 + memoized.maximum_subtree_depth); + expanded_nodes = expanded_nodes.saturating_add(memoized.expanded_nodes); layout = layout.concatenate(memoized.ty.layout)?; fields.push(SchemaField { name: field.name, @@ -719,6 +755,7 @@ impl<'a> ModelBuilder<'a> { layout, }), maximum_subtree_depth, + expanded_nodes, }) } TypeDefKind::Option(payload) => { @@ -729,6 +766,7 @@ impl<'a> ModelBuilder<'a> { let layout = FeltLayout::bounded(1, maximum)?; Ok(MemoizedSchemaType { maximum_subtree_depth: 1 + payload.maximum_subtree_depth, + expanded_nodes: payload.expanded_nodes.saturating_add(1), ty: Arc::new(SchemaType { name, fqn, @@ -741,12 +779,15 @@ impl<'a> ModelBuilder<'a> { TypeDefKind::Variant(variant) => { let mut cases = Vec::with_capacity(variant.cases.len()); let mut maximum_subtree_depth = 0; + let mut expanded_nodes = 1usize; for case in variant.cases { let payload = match case.ty { Some(ty) => { let memoized = self.build_type(ty, depth + 1)?; maximum_subtree_depth = maximum_subtree_depth.max(1 + memoized.maximum_subtree_depth); + expanded_nodes = + expanded_nodes.saturating_add(memoized.expanded_nodes); Some(memoized.ty) } None => None, @@ -767,6 +808,7 @@ impl<'a> ModelBuilder<'a> { layout, }), maximum_subtree_depth, + expanded_nodes, }) } TypeDefKind::Enum(enum_) => { @@ -789,6 +831,7 @@ impl<'a> ModelBuilder<'a> { layout, }), maximum_subtree_depth: 0, + expanded_nodes: 1, }) } unsupported => Err(Error::new(format!( @@ -800,6 +843,7 @@ impl<'a> ModelBuilder<'a> { }; self.active.remove(&id); if let Ok(memoized) = &result { + ensure_expanded_node_limit(&memoized.ty, memoized.expanded_nodes)?; self.memo.insert(id, memoized.clone()); } result @@ -848,6 +892,7 @@ impl<'a> ModelBuilder<'a> { layout: FeltLayout::fixed(width), }), maximum_subtree_depth: 0, + expanded_nodes: 1, }) } diff --git a/sdk/note-schema/src/tests.rs b/sdk/note-schema/src/tests.rs index d8e34b3c38..f56bbab12c 100644 --- a/sdk/note-schema/src/tests.rs +++ b/sdk/note-schema/src/tests.rs @@ -5,8 +5,8 @@ use miden_protocol::{account::AccountId, address::NetworkId}; use crate::{ ACCOUNT_ID_FQN, CodecRegistry, DecodedValueKind, Felt, MAX_NOTE_STORAGE_SCHEMA_BYTES, - MAX_NOTE_STORAGE_SCHEMA_DEPTH, MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_TYPES, - NoteStorage, NoteStorageSchema, SchemaTypeKind, + MAX_NOTE_STORAGE_SCHEMA_DEPTH, MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_NODES, + MAX_NOTE_STORAGE_SCHEMA_TYPES, NoteStorage, NoteStorageSchema, SchemaTypeKind, }; const LAYOUT_SCHEMA: &str = r#" @@ -448,6 +448,45 @@ fn memoized_subtree_reuse_still_enforces_the_depth_limit() { ); } +#[test] +fn zero_width_record_doubling_fails_at_the_expanded_node_limit() { + let error = NoteStorageSchema::from_wit_text(&empty_pair_schema(40)) + .err() + .expect("a doubling chain of empty records must fail") + .to_string(); + + assert!(error.contains("expands to"), "unexpected expanded-node error: {error}"); + assert!( + error.contains(&MAX_NOTE_STORAGE_SCHEMA_NODES.to_string()), + "the node limit must be present in the diagnostic: {error}" + ); + // Level `n` of the chain expands to `2^(n + 1) - 1` nodes, so the first rejected level is the + // first one above the budget. The count in the message pins the counting rule. + let first_rejected = (1..) + .map(|level: u32| (1usize << (level + 1)) - 1) + .find(|nodes| *nodes > MAX_NOTE_STORAGE_SCHEMA_NODES) + .expect("the doubling chain crosses the node limit"); + assert!( + error.contains(&first_rejected.to_string()), + "the expanded node count must be present in the diagnostic: {error}" + ); +} + +#[test] +fn nested_records_below_the_expanded_node_limit_still_resolve() { + let levels = 11; + let schema = NoteStorageSchema::from_wit_text(&empty_pair_schema(levels)) + .expect("a doubling chain below the node limit must resolve"); + assert!( + (1usize << (levels + 1)) - 1 <= MAX_NOTE_STORAGE_SCHEMA_NODES, + "the chain must stay below the node budget" + ); + assert_eq!(schema.layout().maximum(), 0, "empty records have no felts"); + + NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA) + .expect("the layout schema stays below the node limit"); +} + #[test] fn schema_reader_enforces_documented_byte_type_and_root_width_limits() { let oversized = " ".repeat(MAX_NOTE_STORAGE_SCHEMA_BYTES + 1); @@ -485,6 +524,18 @@ fn schema_reader_enforces_documented_byte_type_and_root_width_limits() { assert!(width_error.contains(&MAX_NOTE_STORAGE_SCHEMA_FELTS.to_string())); } +/// Builds a linear-size WIT DAG of zero-felt records whose expanded tree doubles at each level. +fn empty_pair_schema(levels: usize) -> String { + let mut wit = + String::from("package example:empty-pair@1.0.0; interface note-storage { record t0 { } "); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record t{level} {{ left: t{previous}, right: t{previous} }} ")); + } + wit.push_str(&format!("type storage = t{levels}; }}")); + wit +} + /// Builds a linear-size WIT DAG whose resolved layout doubles at each level. fn repeated_pair_schema(levels: usize) -> String { let mut wit = String::from( From 951199b3287c3134c3cc314b3b1f69fc5d4fa60c Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 8 Sep 2026 17:57:00 +0300 Subject: [PATCH 36/43] docs: align note codec and schema documentation with the enforced rules The template skill still advertised `Vec` fields for `#[note]` structs, which the macro rejects; it now states the named-field-or-unit rule and the one-`#[note]`-per-crate rule, and the embedded template bundle is regenerated. The SDK changelog gains the one-note-per-crate breaking-change entry the migration guide documents. The codec crate docs show the manifest opt-in. The test-only registry reset is documented. Comments about `--locked` and `--offline` no longer claim every nested build inherits them. A dead sort of already-unique package paths is removed. --- .../.claude/skills/rust-sdk-patterns/SKILL.md | 2 +- midenc-compile/src/compiler.rs | 2 +- midenc-session/src/options/mod.rs | 9 +++++++-- sdk/CHANGELOG.md | 4 ++++ sdk/note-codec/macros/src/registry.rs | 4 ++++ sdk/note-codec/src/lib.rs | 8 ++++++++ sdk/note-schema/src/artifact.rs | 3 +-- tools/cargo-miden/templates.tar.gz | Bin 74519 -> 74654 bytes 8 files changed, 26 insertions(+), 6 deletions(-) diff --git a/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md b/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md index a3efcea92e..acf444b2d1 100644 --- a/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md +++ b/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md @@ -139,7 +139,7 @@ A note script reads from `active_note::*` and forwards work to a public account- The `#[note]` macro generates `TryFrom<&[Felt]>` for the note struct, so the note's serialized storage is deserialized into typed fields before the script runs. The `#[note_script]` method receives the deserialized note as `self` (by value) and never indexes a raw Felt slice manually. Alongside the required `Word` arg, the method may optionally accept a `&Account` or `&mut Account` parameter. See [compiler/sdk/base-macros/src/lib.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/lib.rs) for the macro contract and [compiler/sdk/base-macros/src/note.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/note.rs) for the generated deserialization (each named field is read via `::from_felt_repr(...)` and EOF is asserted at the end). -Supported field types include `Felt`, the unsigned integer scalars (`u64`, `u32`, `u8`), `bool`, `Option`, and `Vec` via the `FromFeltRepr` trait (`compiler/sdk/field-repr/repr/src/lib.rs`), plus any user type that opts in with `#[derive(FromFeltRepr)]` (this is how `AccountId` supports the macro - see `compiler/sdk/base-sys/src/bindings/types.rs`). Do **not** use `Asset` or `Word` directly as note struct fields; those types do not currently derive `FromFeltRepr`. If you need asset-shaped data inside the note, flatten it into supported scalar fields and reconstruct inside the script, or keep it on the side as a separate `active_note::get_assets()` read. +Supported field types include `Felt`, the unsigned integer scalars (`u64`, `u32`, `u8`), `bool`, and `Option` over a supported type via the `FromFeltRepr` trait (`compiler/sdk/field-repr/repr/src/lib.rs`), plus any user type that opts in with `#[derive(FromFeltRepr)]` (this is how `AccountId` supports the macro - see `compiler/sdk/base-sys/src/bindings/types.rs`). `Vec` is not supported: the note storage schema needs a stable, named position for each stored value, so a dynamic vector is a hard error. Two more rules follow from the schema: a `#[note]` struct must have named fields or be a unit struct (tuple structs are rejected), and a crate may contain only one `#[note]` struct (move each extra note struct into its own crate). Do **not** use `Asset` or `Word` directly as note struct fields; those types do not currently derive `FromFeltRepr`. If you need asset-shaped data inside the note, flatten it into supported scalar fields and reconstruct inside the script, or keep it on the side as a separate `active_note::get_assets()` read. For Cargo.toml wiring (cross-component dependencies + bindings import), see "Cross-Component Dependencies" above. See [increment-note/src/lib.rs](../../../contracts/increment-note/src/lib.rs) for the project-template's local example of the `#[note] struct + #[note] impl` macro form. diff --git a/midenc-compile/src/compiler.rs b/midenc-compile/src/compiler.rs index 7fe4d752b7..b7ba5bdc89 100644 --- a/midenc-compile/src/compiler.rs +++ b/midenc-compile/src/compiler.rs @@ -926,7 +926,7 @@ mod tests { assert_eq!(options(&[]).stop_after, None); } - /// Cargo resolution policy flags reach every nested build through the session options. + /// Cargo resolution policy flags reach the Rust project build through the session options. #[test] fn cargo_resolution_policy_reaches_the_options() { let options = options(&["--locked", "--offline"]); diff --git a/midenc-session/src/options/mod.rs b/midenc-session/src/options/mod.rs index e7e13b94ed..9716f8364c 100644 --- a/midenc-session/src/options/mod.rs +++ b/midenc-session/src/options/mod.rs @@ -34,9 +34,14 @@ pub struct Options { pub workspace: bool, /// Build the specified packages in the current workspace (used by `cargo miden`) pub packages: Vec, - /// Require Cargo.lock to remain unchanged in nested Cargo builds. + /// Require Cargo.lock to remain unchanged in the Rust project build. + /// + /// The note codec build does not read this option. That build is session-free, and it + /// resolves its own crate under its own lockfile. pub cargo_locked: bool, - /// Prevent network access in nested Cargo builds. + /// Prevent network access in the Rust project build. + /// + /// The note codec build does not read this option, for the same reason as `cargo_locked`. pub cargo_offline: bool, /// The name of the current project target being compiled pub target: Option, diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 7b3a3e72fb..ceb7de2720 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -59,6 +59,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and note storage fields no longer accept `Vec`. Follow the [migration guidance](./sdk/MIGRATION.md#rewrite-tuple-note-and-vec-storage-layouts) to preserve field order with named fields and replace dynamic vectors with a fixed schema. (#1307) +- A crate can now contain only one `#[note]` struct. The linker rejects a second struct because + both structs define the same note storage schema uniqueness guard symbol. Follow the + [migration guidance](./sdk/MIGRATION.md#keep-one-note-struct-in-each-crate) to move each extra + note struct into its own crate. (#1307) ## [0.14.0] diff --git a/sdk/note-codec/macros/src/registry.rs b/sdk/note-codec/macros/src/registry.rs index ec5b37dfba..f9f957cd1f 100644 --- a/sdk/note-codec/macros/src/registry.rs +++ b/sdk/note-codec/macros/src/registry.rs @@ -203,6 +203,10 @@ fn collect_type_bindings( Ok(()) } +/// Clears the process-wide macro registry between unit tests. +/// +/// The registry lives for the whole proc-macro process, so a test that expands macros must start +/// from an empty registry. Tests serialize on the registry test lock before they call this. #[cfg(test)] pub(crate) fn reset_for_tests() { if let Some(registry) = REGISTRY.get() { diff --git a/sdk/note-codec/src/lib.rs b/sdk/note-codec/src/lib.rs index 84c110dc37..d796883237 100644 --- a/sdk/note-codec/src/lib.rs +++ b/sdk/note-codec/src/lib.rs @@ -20,6 +20,14 @@ //! //! miden_note_codec::export_codecs!(); //! ``` +//! +//! The note project opts in to the codec build with package-level metadata in its +//! `miden-project.toml`. The `crate` directory is relative to that manifest: +//! +//! ```toml +//! [package.metadata.midenc.note-codec] +//! crate = "../my-note-codec" +//! ``` #![deny(missing_docs)] diff --git a/sdk/note-schema/src/artifact.rs b/sdk/note-schema/src/artifact.rs index a82c9c3831..4648c68aa0 100644 --- a/sdk/note-schema/src/artifact.rs +++ b/sdk/note-schema/src/artifact.rs @@ -231,7 +231,7 @@ fn find_project_package_in_dir(dir: &Path, stems: &[String]) -> Result, _>>() .map_err(|error| { @@ -243,7 +243,6 @@ fn find_project_package_in_dir(dir: &Path, stems: &[String]) -> Result>(); - packages.sort(); // The stems are ordered by identity. The canonical miden-project package name comes first, // followed by legacy aliases. This matches base-macros/src/dependency_package.rs and prevents diff --git a/tools/cargo-miden/templates.tar.gz b/tools/cargo-miden/templates.tar.gz index 3c60cb91b1aceb1b3133ed31f50b2f3f8ccc5492..1a48063f3e4aea7c3b729d32c4d0be60d13113ab 100644 GIT binary patch delta 38185 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

%*D9JCRnMIMTBIJwiJuGwER2+D5_UlINzkQM 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 F{{ifDMEU># delta 38088 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

YyTsZdX? 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 M(1U0^{JcE>4@J1~&Hw-a From 0a9ab235384ed41be52b128f5a3d939f747bce76 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 8 Sep 2026 18:14:13 +0300 Subject: [PATCH 37/43] test: refresh cycle and package-size expectations after the parent update The parent branch changed the SDK allocator and heap growth, which moved every note-script cycle count and note-package size by a constant. The expectations are re-measured; the branch's own deltas over the parent are unchanged. --- .../src/mockchain/notes/basic_wallet.rs | 8 ++++---- .../src/mockchain/notes/note_constructor.rs | 4 ++-- tests/integration-network/src/mockchain/swapp.rs | 8 ++++---- .../src/end_to_end/examples/basic_wallet_package_sizes.rs | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/integration-network/src/mockchain/notes/basic_wallet.rs b/tests/integration-network/src/mockchain/notes/basic_wallet.rs index 6d8497674b..7ec71cdb28 100644 --- a/tests/integration-network/src/mockchain/notes/basic_wallet.rs +++ b/tests/integration-network/src/mockchain/notes/basic_wallet.rs @@ -111,7 +111,7 @@ pub fn basic_wallet_p2id_transfers_asset_with_custom_tx_script() { .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); expect!["3473"].assert_eq(prologue_cycles(&tx_measurements)); - expect!["5030"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["5040"].assert_eq(single_note_cycles(&tx_measurements)); eprintln!("\n=== Checking Alice's account has the minted asset ==="); let alice_account = chain.committed_account(alice_id).unwrap(); @@ -142,7 +142,7 @@ pub fn basic_wallet_p2id_transfers_asset_with_custom_tx_script() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); - expect!["5030"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["5040"].assert_eq(single_note_cycles(&tx_measurements)); eprintln!("\n=== Checking Bob's account has the transferred asset ==="); let bob_account = chain.committed_account(bob_id).unwrap(); @@ -281,7 +281,7 @@ pub fn basic_wallet_p2ide_allows_recipient_claim() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); - expect!["5467"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["5477"].assert_eq(single_note_cycles(&tx_measurements)); // Step 5: verify balances let bob_account = chain.committed_account(bob_id).unwrap(); @@ -420,7 +420,7 @@ pub fn basic_wallet_p2ide_allows_sender_reclaim() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); - expect!["6042"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["6052"].assert_eq(single_note_cycles(&tx_measurements)); // Step 5: verify Alice has her original amount back let alice_account = chain.committed_account(alice_id).unwrap(); diff --git a/tests/integration-network/src/mockchain/notes/note_constructor.rs b/tests/integration-network/src/mockchain/notes/note_constructor.rs index 2b446a306d..9c994c58b1 100644 --- a/tests/integration-network/src/mockchain/notes/note_constructor.rs +++ b/tests/integration-network/src/mockchain/notes/note_constructor.rs @@ -173,7 +173,7 @@ pub fn tx_script_creates_p2id_note_via_note_constructor() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, create_tx); - expect!["8932"].assert_eq(tx_script_processing_cycles(&tx_measurements)); + expect!["8942"].assert_eq(tx_script_processing_cycles(&tx_measurements)); eprintln!("\n=== Step 4: Bob consumes the note created by the constructor ==="); let faucet_inputs = chain.get_foreign_account_inputs(faucet_id).unwrap(); @@ -184,7 +184,7 @@ pub fn tx_script_creates_p2id_note_via_note_constructor() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, consume_tx); - expect!["5030"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["5040"].assert_eq(single_note_cycles(&tx_measurements)); eprintln!("\n=== Checking Bob's account has the transferred asset ==="); let bob_account = chain.committed_account(bob_id).unwrap(); diff --git a/tests/integration-network/src/mockchain/swapp.rs b/tests/integration-network/src/mockchain/swapp.rs index 1d3b26e458..92caefa2b4 100644 --- a/tests/integration-network/src/mockchain/swapp.rs +++ b/tests/integration-network/src/mockchain/swapp.rs @@ -297,7 +297,7 @@ fn assert_no_fungible_asset(account: &Account, faucet_id: AccountId) { #[test] fn swapp_note_package_size() { let packages = compile_swapp_packages(); - expect!["42586"].assert_eq(stripped_mast_size_str(packages.swapp.as_ref()).as_str()); + expect!["42620"].assert_eq(stripped_mast_size_str(packages.swapp.as_ref()).as_str()); } /// Tests a full fill of a SWAPP note. @@ -353,7 +353,7 @@ fn swapp_note_full_fill_transfers_assets() { vec![p2id_note.id()], "full fill must create exactly the P2ID routing note" ); - expect!["12839"].assert_eq(single_note_cycles(executed_tx.measurements())); + expect!["12849"].assert_eq(single_note_cycles(executed_tx.measurements())); let bob_account = chain.committed_account(bob.id()).unwrap(); assert_account_has_fungible_asset(bob_account, usdc_faucet.id(), 50); @@ -440,7 +440,7 @@ fn swapp_note_partial_fill_creates_remainder_and_chains() { vec![first_p2id_note.id(), remainder_note.id()], "partial fill must create the P2ID routing note and the remainder note" ); - expect!["17931"].assert_eq(single_note_cycles(executed_tx.measurements())); + expect!["17941"].assert_eq(single_note_cycles(executed_tx.measurements())); let bob_account = chain.committed_account(bob.id()).unwrap(); assert_account_has_fungible_asset(bob_account, usdc_faucet.id(), 3); @@ -521,7 +521,7 @@ fn swapp_note_creator_reclaims_offered_asset() { output_note_ids(&executed_tx).is_empty(), "reclaiming the swap note must not create any output notes" ); - expect!["5573"].assert_eq(single_note_cycles(executed_tx.measurements())); + expect!["5583"].assert_eq(single_note_cycles(executed_tx.measurements())); let alice_account = chain.committed_account(alice.id()).unwrap(); assert_account_has_fungible_asset(alice_account, usdc_faucet.id(), 50); diff --git a/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs b/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs index 0b3b950c95..4f6f5bcc74 100644 --- a/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs +++ b/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs @@ -43,7 +43,7 @@ fn basic_wallet_and_p2id() { p2id_test.compile_package() }; assert!(note_package.is_library(), "expected library"); - expect!["21763"].assert_eq(stripped_mast_size_str(¬e_package).as_str()); + expect!["21797"].assert_eq(stripped_mast_size_str(¬e_package).as_str()); // The note package exports both the note script and the `build-recipient` constructor; the // constructor must not interfere with the `@note_script`-attributed export selection. assert!( @@ -60,5 +60,5 @@ fn basic_wallet_and_p2id() { ); let p2ide_package = p2ide_test.compile_package(); assert!(p2ide_package.is_library(), "expected library"); - expect!["16402"].assert_eq(stripped_mast_size_str(&p2ide_package).as_str()); + expect!["16436"].assert_eq(stripped_mast_size_str(&p2ide_package).as_str()); } From f81eae7e51f624bbc442bdb7b186fb9022431175 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 8 Sep 2026 18:14:13 +0300 Subject: [PATCH 38/43] test: copy the workspace patch table into the consumer crate only when it exists The consumer-crate test required a `[patch.crates-io]` table in the workspace manifest and failed once the workspace dropped its commented-out table. The copy is now optional: a workspace without patches gives the consumer crate no patches, which is the correct mirror. --- sdk/note-bindings/tests/p2id_consumer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs index fb14072ffb..cd9d52d2e4 100644 --- a/sdk/note-bindings/tests/p2id_consumer.rs +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -24,6 +24,10 @@ fn host_target() -> String { } /// Copies the workspace patch table into an isolated consumer manifest. +/// +/// The consumer crate must resolve the same patched dependencies as the workspace. A +/// workspace without a patch table has nothing to copy, and the function returns an empty +/// string. fn workspace_patch_section(workspace: &Path) -> String { let manifest = fs::read_to_string(workspace.join("Cargo.toml")).unwrap(); let mut section = String::new(); @@ -40,7 +44,6 @@ fn workspace_patch_section(workspace: &Path) -> String { section.push('\n'); } } - assert!(!section.is_empty(), "workspace manifest has no [patch.crates-io] section"); section } From d759a239bddacf7f4ed41de4bc22174b3ba716e6 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 8 Sep 2026 19:45:57 +0300 Subject: [PATCH 39/43] fix: account for what a codec component instantiates and close the remaining sandbox gaps The structural check capped component-level sections one section at a time, but a component may repeat a section, so the caps bounded nothing. Item counts are now accumulated across the whole tree per kind, core type sections gain a cap, and the module documentation states exactly which rules apply, including that only a component-level start function is rejected. The budgets also counted declarations while the store counts runtime objects: a module instantiated twice creates two memories, and a nested component multiplies whatever it creates. Each component frame now keeps its module and component index spaces, an imported or aliased entry is not instantiable, and every core or component instantiation adds what it creates, so a component that passes the check instantiates inside the store limits. Every structural rejection reports the limit class, load and lift failures report the trap class, and a store-count rejection at instantiation is classified as a limit. Loading a package without a codec section no longer requires a schema section first, so unit-note packages get the standard registry. Generated code fully qualifies its trait bounds and derives, the codec registry keys custom types by their WIT name with the generator's identifier rule so a reserved name resolves and a same-name collision is reported, and the written-type identity records a leading path separator. The artifact resolver reports the manifests it read so generated bindings track a package rename. Generated bindings parse the schema once, the export test pins the guest flags and validates its component, and several fields and helpers gain documentation. --- Cargo.lock | 2 +- examples/dex-note-codec/Cargo.lock | 1 - sdk/base-macros/src/component_macro/mod.rs | 3 + sdk/base-macros/src/note_schema.rs | 9 +- sdk/base-macros/src/types.rs | 28 +- sdk/base-macros/src/types/tests.rs | 36 ++ sdk/note-bindings/macros/expected/custom.rs | 44 +- sdk/note-bindings/macros/expected/p2id.rs | 28 +- sdk/note-bindings/macros/src/lib.rs | 31 +- sdk/note-codec/Cargo.toml | 1 + sdk/note-codec/macros/Cargo.toml | 1 - sdk/note-codec/macros/src/expand.rs | 11 +- sdk/note-codec/macros/src/registry.rs | 45 +- sdk/note-codec/macros/src/tests.rs | 39 ++ sdk/note-codec/tests/component_export.rs | 8 + sdk/note-schema/codegen/src/lib.rs | 31 +- sdk/note-schema/src/artifact.rs | 99 +++- sdk/note-schema/src/builder.rs | 3 +- sdk/note-schema/src/codec_component.rs | 84 ++- sdk/note-schema/src/codec_structure.rs | 557 ++++++++++++++++---- sdk/note-schema/src/error.rs | 23 +- sdk/note-schema/src/schema.rs | 25 +- sdk/wasm-metadata/src/lib.rs | 4 +- 23 files changed, 893 insertions(+), 220 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba226f4d7b..f70bbc9023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3647,6 +3647,7 @@ dependencies = [ "miden-field-repr", "miden-note-codec-macros", "miden-note-codec-wit", + "miden-note-schema", "miden-protocol", "midenc-frontend-wasm-metadata", "tempfile", @@ -3659,7 +3660,6 @@ dependencies = [ name = "miden-note-codec-macros" version = "0.14.0" dependencies = [ - "heck", "miden-note-codec-wit", "miden-note-schema", "miden-note-schema-codegen", diff --git a/examples/dex-note-codec/Cargo.lock b/examples/dex-note-codec/Cargo.lock index afc6ccfb2e..04ccd62d58 100644 --- a/examples/dex-note-codec/Cargo.lock +++ b/examples/dex-note-codec/Cargo.lock @@ -1952,7 +1952,6 @@ dependencies = [ name = "miden-note-codec-macros" version = "0.14.0" dependencies = [ - "heck", "miden-note-codec-wit", "miden-note-schema", "miden-note-schema-codegen", diff --git a/sdk/base-macros/src/component_macro/mod.rs b/sdk/base-macros/src/component_macro/mod.rs index 053781aeff..16e9a92017 100644 --- a/sdk/base-macros/src/component_macro/mod.rs +++ b/sdk/base-macros/src/component_macro/mod.rs @@ -1464,6 +1464,7 @@ mod tests { wit_name: "struct-a".into(), is_custom: true, path: vec!["StructA".into()], + leading_colon: false, dependencies: Vec::new(), }; @@ -1479,6 +1480,7 @@ mod tests { wit_name: "struct-a".into(), is_custom: true, path: vec!["StructA".into()], + leading_colon: false, dependencies: Vec::new(), }; let prefix = vec!["foo".to_string(), "bar".to_string()]; @@ -1498,6 +1500,7 @@ mod tests { wit_name: "struct-a".into(), is_custom: true, path: vec!["super".into(), "StructA".into()], + leading_colon: false, dependencies: Vec::new(), }; let prefix = vec!["foo".to_string(), "bar".to_string()]; diff --git a/sdk/base-macros/src/note_schema.rs b/sdk/base-macros/src/note_schema.rs index a5d083fd92..b6bd1ddc0a 100644 --- a/sdk/base-macros/src/note_schema.rs +++ b/sdk/base-macros/src/note_schema.rs @@ -93,6 +93,9 @@ pub(crate) fn expand_note_storage_schema( )] #[doc(hidden)] #[allow(clippy::octal_escapes)] + // No `#[used]`: the section survives without it, and on this target the + // attribute additionally lands the bytes in the module's data segments, + // growing every package by the size of its own schema. pub static __MIDEN_NOTE_STORAGE_SCHEMA_BYTES: [u8; #bytes_len] = *#encoded_bytes; }) } @@ -424,7 +427,11 @@ fn map_note_field_type( }) } -/// Returns true when a type contains `Vec` at any nesting depth. +/// Returns true when a path type names `Vec`, either directly or in an angle-bracketed +/// type argument at any nesting depth. +/// +/// The search looks through groups and parentheses. All other type forms, such as +/// references, arrays, slices, tuples, and pointers, give false. fn contains_vec(ty: &Type) -> bool { match ty { Type::Group(group) => contains_vec(&group.elem), diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index b8402291d9..e05b3bfb6d 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -17,14 +17,18 @@ use crate::manifest_paths::SDK_WIT_SOURCE; static EXPORTED_TYPES: OnceLock>>> = OnceLock::new(); +/// Guard that serializes tests which share the process-wide exported-type registry. #[cfg(test)] static EXPORTED_TYPES_TEST_LOCK: Mutex<()> = Mutex::new(()); +/// One Rust type as it was written at the expansion site, with its WIT identity. #[derive(Clone, Debug)] pub(crate) struct TypeRef { pub(crate) wit_name: String, pub(crate) is_custom: bool, pub(crate) path: Vec, + /// True when the written path starts with `::`. + pub(crate) leading_colon: bool, pub(crate) dependencies: Vec, } @@ -361,7 +365,7 @@ fn written_type_text(type_ref: &TypeRef) -> String { if type_ref.path.is_empty() { return "()".to_string(); } - let path = type_ref.path.join("::"); + let path = written_path(type_ref); match (type_ref.path.last().map(String::as_str), type_ref.dependencies.as_slice()) { (Some("Option"), [inner]) => format!("{path}<{}>", written_type_text(inner)), (Some("Result"), [ok, err]) => { @@ -371,6 +375,16 @@ fn written_type_text(type_ref: &TypeRef) -> String { } } +/// Returns the path of a reference as it was written, keeping any leading `::`. +fn written_path(type_ref: &TypeRef) -> String { + let path = type_ref.path.join("::"); + if type_ref.leading_colon { + format!("::{path}") + } else { + path + } +} + /// Parses one reconstructed type text back into a type for guard emission. fn parse_reconstructed_type(text: &str, span: Span) -> Result { syn::parse_str::(text).map_err(|error| { @@ -424,7 +438,7 @@ fn collect_sdk_core_type_identity_guard( return Ok(()); } - let rust_path = type_ref.path.join("::"); + let rust_path = written_path(type_ref); if !guarded.insert((rust_path.clone(), type_ref.wit_name.clone())) { return Ok(()); } @@ -499,7 +513,7 @@ fn collect_custom_type_shape_assertion( if !type_ref.is_custom { return Ok(()); } - let written_path = type_ref.path.join("::"); + let written_path = written_path(type_ref); if !asserted.insert(written_path.clone()) { return Ok(()); } @@ -588,6 +602,7 @@ pub(crate) fn map_type_to_type_ref( let path_segments: Vec = path.path.segments.iter().map(|segment| segment.ident.to_string()).collect(); + let leading_colon = path.path.leading_colon.is_some(); reject_unsupported_component_primitive(&ident, last.span())?; @@ -601,6 +616,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: false, path: path_segments, + leading_colon, dependencies: vec![inner], }); } @@ -615,6 +631,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: false, path: path_segments, + leading_colon, dependencies: vec![ok, err], }); } @@ -632,6 +649,7 @@ pub(crate) fn map_type_to_type_ref( wit_name: wit_type_name(wit_type).to_string(), is_custom: false, path: path_segments, + leading_colon, dependencies: Vec::new(), }); } @@ -641,6 +659,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: true, path: path_segments, + leading_colon, dependencies: Vec::new(), }); } @@ -650,6 +669,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: false, path: path_segments, + leading_colon, dependencies: Vec::new(), }); } @@ -658,6 +678,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: true, path: path_segments, + leading_colon, dependencies: Vec::new(), }) } @@ -715,6 +736,7 @@ fn map_result_argument_type_to_type_ref( wit_name: "_".to_string(), is_custom: false, path: Vec::new(), + leading_colon: false, dependencies: Vec::new(), }), _ => map_type_to_type_ref(ty, exported_types), diff --git a/sdk/base-macros/src/types/tests.rs b/sdk/base-macros/src/types/tests.rs index 3a011aeaad..5c325fb6d5 100644 --- a/sdk/base-macros/src/types/tests.rs +++ b/sdk/base-macros/src/types/tests.rs @@ -861,3 +861,39 @@ fn main() {{}} String::from_utf8_lossy(&output.stderr) ); } + +#[test] +fn leading_colon_path_round_trips() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let exported = HashMap::new(); + + let ty: Type = syn::parse_str("::core::primitive::u64").unwrap(); + let type_ref = map_type_to_type_ref(&ty, &exported).expect("primitive should resolve"); + assert!(type_ref.leading_colon); + assert_eq!(written_type_text(&type_ref), "::core::primitive::u64"); + // A fully qualified primitive reconstructs to the canonical text, so the builtin + // identity guard compares one type with itself. + assert_eq!( + builtin_canonical_type_text(&type_ref).as_deref(), + Some("::core::primitive::u64") + ); + + let ty: Type = syn::parse_str("u64").unwrap(); + let type_ref = map_type_to_type_ref(&ty, &exported).expect("primitive should resolve"); + assert!(!type_ref.leading_colon); + assert_eq!(written_type_text(&type_ref), "u64"); +} + +#[test] +fn leading_colon_generic_path_round_trips() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let exported = HashMap::new(); + + let ty: Type = syn::parse_str("::core::option::Option<::core::primitive::u32>").unwrap(); + let type_ref = map_type_to_type_ref(&ty, &exported).expect("option should resolve"); + + assert!(type_ref.leading_colon); + assert_eq!(written_type_text(&type_ref), "::core::option::Option<::core::primitive::u32>"); +} diff --git a/sdk/note-bindings/macros/expected/custom.rs b/sdk/note-bindings/macros/expected/custom.rs index bb5f239a48..9cdf61093a 100644 --- a/sdk/note-bindings/macros/expected/custom.rs +++ b/sdk/note-bindings/macros/expected/custom.rs @@ -10,7 +10,7 @@ mod __miden_note_bindings_a3280bdaca3ec21e { ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()>; } #[doc(hidden)] - trait __MidenNoteDecode: Sized { + trait __MidenNoteDecode: ::core::marker::Sized { fn __read_note_felts( reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< '_, @@ -351,7 +351,12 @@ mod __miden_note_bindings_a3280bdaca3ec21e { } } ///Rust binding for WIT type `example:dex-schema/note-storage@1.0.0.dex-note`. - #[derive(Clone, Debug, PartialEq, Eq)] + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, + )] pub struct DexNote { ///Value of the WIT `target` field. pub target: ::miden_note_bindings::__private::miden_protocol::account::AccountId, @@ -390,10 +395,10 @@ mod __miden_note_bindings_a3280bdaca3ec21e { } ///Selects order execution. #[derive( - Clone, - Debug, - PartialEq, - Eq, + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr, ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr, )] @@ -476,10 +481,10 @@ mod __miden_note_bindings_a3280bdaca3ec21e { } ///A ratio used as an order limit. #[derive( - Clone, - Debug, - PartialEq, - Eq, + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr, ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr, )] @@ -520,13 +525,24 @@ mod __miden_note_bindings_a3280bdaca3ec21e { } #[doc(hidden)] const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:dex-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n /// A ratio used as an order limit.\n record limit-price {\n numerator: u64,\n denominator: u64,\n }\n\n /// Selects order execution.\n variant order-kind {\n market,\n limit(limit-price),\n }\n\n record dex-note {\n target: account-id,\n kind: order-kind,\n }\n\n type storage = dex-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; + /// Returns the resolved schema, which is parsed once for the whole process. #[doc(hidden)] fn __miden_note_storage_schema() -> ::miden_note_bindings::__private::miden_note_schema::Result< - ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + &'static ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, > { - ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema::from_wit_text( - __MIDEN_NOTE_STORAGE_SCHEMA_WIT, - ) + static __MIDEN_NOTE_STORAGE_SCHEMA: ::std::sync::OnceLock< + ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + >, + > = ::std::sync::OnceLock::new(); + __MIDEN_NOTE_STORAGE_SCHEMA + .get_or_init(|| { + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) + }) + .as_ref() + .map_err(::core::clone::Clone::clone) } impl DexNote { /// Encodes this typed value as note storage in WIT declaration order. diff --git a/sdk/note-bindings/macros/expected/p2id.rs b/sdk/note-bindings/macros/expected/p2id.rs index e4091e5951..eac74f37ef 100644 --- a/sdk/note-bindings/macros/expected/p2id.rs +++ b/sdk/note-bindings/macros/expected/p2id.rs @@ -10,7 +10,7 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()>; } #[doc(hidden)] - trait __MidenNoteDecode: Sized { + trait __MidenNoteDecode: ::core::marker::Sized { fn __read_note_felts( reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< '_, @@ -351,7 +351,12 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { } } ///Rust binding for WIT type `example:p2id-schema/note-storage@1.0.0.p2id-note`. - #[derive(Clone, Debug, PartialEq, Eq)] + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, + )] pub struct P2idNote { ///Value of the WIT `target-account-id` field. pub target_account_id: ::miden_note_bindings::__private::miden_protocol::account::AccountId, @@ -386,13 +391,24 @@ mod __miden_note_bindings_f74ea5e7a6e77b2d { } #[doc(hidden)] const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:p2id-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n record p2id-note {\n target-account-id: account-id,\n }\n\n type storage = p2id-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; + /// Returns the resolved schema, which is parsed once for the whole process. #[doc(hidden)] fn __miden_note_storage_schema() -> ::miden_note_bindings::__private::miden_note_schema::Result< - ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + &'static ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, > { - ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema::from_wit_text( - __MIDEN_NOTE_STORAGE_SCHEMA_WIT, - ) + static __MIDEN_NOTE_STORAGE_SCHEMA: ::std::sync::OnceLock< + ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + >, + > = ::std::sync::OnceLock::new(); + __MIDEN_NOTE_STORAGE_SCHEMA + .get_or_init(|| { + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) + }) + .as_ref() + .map_err(::core::clone::Clone::clone) } impl P2idNote { /// Encodes this typed value as note storage in WIT declaration order. diff --git a/sdk/note-bindings/macros/src/lib.rs b/sdk/note-bindings/macros/src/lib.rs index ce1d9e4e68..c0838b96ad 100644 --- a/sdk/note-bindings/macros/src/lib.rs +++ b/sdk/note-bindings/macros/src/lib.rs @@ -69,10 +69,17 @@ fn expand_package_artifact( let scope_key = artifact.path().to_string_lossy(); let bindings = expand_schema(artifact.schema(), span, &scope_key)?; let tracked_path = artifact.path().to_string_lossy(); + let tracked_manifests = artifact + .tracked_inputs() + .iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect::>(); let package_cache_env = midenc_frontend_wasm_metadata::package_cache::PACKAGE_CACHE_ENV; Ok(quote! { - // These constants exist only to register the package file and cache path as proc-macro rebuild inputs. + // These constants exist only to register the package file, the manifests that named it, + // and the cache path as proc-macro rebuild inputs. const _: &[u8] = ::core::include_bytes!(#tracked_path); + #(const _: &[u8] = ::core::include_bytes!(#tracked_manifests);)* const _: ::core::option::Option<&str> = ::core::option_env!(#package_cache_env); #bindings }) @@ -110,12 +117,24 @@ fn expand_schema( #[doc(hidden)] const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = #wit_text; + /// Returns the resolved schema, which is parsed once for the whole process. #[doc(hidden)] - fn __miden_note_storage_schema( - ) -> #runtime::miden_note_schema::Result<#runtime::miden_note_schema::NoteStorageSchema> { - #runtime::miden_note_schema::NoteStorageSchema::from_wit_text( - __MIDEN_NOTE_STORAGE_SCHEMA_WIT, - ) + fn __miden_note_storage_schema() -> #runtime::miden_note_schema::Result< + &'static #runtime::miden_note_schema::NoteStorageSchema, + > { + static __MIDEN_NOTE_STORAGE_SCHEMA: ::std::sync::OnceLock< + #runtime::miden_note_schema::Result< + #runtime::miden_note_schema::NoteStorageSchema, + >, + > = ::std::sync::OnceLock::new(); + __MIDEN_NOTE_STORAGE_SCHEMA + .get_or_init(|| { + #runtime::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) + }) + .as_ref() + .map_err(::core::clone::Clone::clone) } impl #root_ident { diff --git a/sdk/note-codec/Cargo.toml b/sdk/note-codec/Cargo.toml index e3e1f663c0..fc5b949284 100644 --- a/sdk/note-codec/Cargo.toml +++ b/sdk/note-codec/Cargo.toml @@ -25,6 +25,7 @@ miden-protocol.workspace = true wit-bindgen = { workspace = true } [dev-dependencies] +miden-note-schema.workspace = true midenc-frontend-wasm-metadata.workspace = true tempfile.workspace = true wit-component.workspace = true diff --git a/sdk/note-codec/macros/Cargo.toml b/sdk/note-codec/macros/Cargo.toml index 9111a1aaf1..372b2015d4 100644 --- a/sdk/note-codec/macros/Cargo.toml +++ b/sdk/note-codec/macros/Cargo.toml @@ -18,7 +18,6 @@ proc-macro = true doctest = false [dependencies] -heck.workspace = true miden-note-codec-wit.workspace = true miden-note-schema.workspace = true miden-note-schema-codegen.workspace = true diff --git a/sdk/note-codec/macros/src/expand.rs b/sdk/note-codec/macros/src/expand.rs index f1f491c3fb..b301058050 100644 --- a/sdk/note-codec/macros/src/expand.rs +++ b/sdk/note-codec/macros/src/expand.rs @@ -37,10 +37,17 @@ pub(crate) fn from_wit_text(input: &LitStr) -> syn::Result { fn expand_package_artifact(artifact: &NotePackageArtifact, span: Span) -> syn::Result { let types = expand_schema(artifact.schema(), span)?; let tracked_path = artifact.path().to_string_lossy(); + let tracked_manifests = artifact + .tracked_inputs() + .iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect::>(); let package_cache_env = midenc_frontend_wasm_metadata::package_cache::PACKAGE_CACHE_ENV; Ok(quote! { - // These constants exist only to register the package file and cache path as proc-macro rebuild inputs. + // These constants exist only to register the package file, the manifests that named it, + // and the cache path as proc-macro rebuild inputs. const _: &[u8] = ::core::include_bytes!(#tracked_path); + #(const _: &[u8] = ::core::include_bytes!(#tracked_manifests);)* const _: ::core::option::Option<&str> = ::core::option_env!(#package_cache_env); #types }) @@ -164,6 +171,8 @@ pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { } }); let wit = NOTE_CODEC_WIT; + // `quote` prints `::` paths with spaces around each separator. `generate!` reads this value + // as one path, so the spaces come out. let wit_runtime_path = LitStr::new( &format!("{facade}::__private::wit_bindgen::rt", facade = quote!(#facade)).replace(' ', ""), Span::call_site(), diff --git a/sdk/note-codec/macros/src/registry.rs b/sdk/note-codec/macros/src/registry.rs index f9f957cd1f..08815d97d7 100644 --- a/sdk/note-codec/macros/src/registry.rs +++ b/sdk/note-codec/macros/src/registry.rs @@ -5,8 +5,8 @@ use std::{ sync::{Mutex, OnceLock}, }; -use heck::ToUpperCamelCase; use miden_note_schema::{NoteStorageSchema, SchemaCase, SchemaType, SchemaTypeKind}; +use miden_note_schema_codegen::generated_type_ident; use proc_macro2::Span; /// One marked author codec. @@ -30,12 +30,16 @@ struct RegisteredCodec { struct Registry { /// Registered schema source and the expansion that supplied it. schema: Option<(String, ExpansionLocation)>, - /// Generated Rust upper-camel type name to WIT FQN, used by `#[note_codec]` lookup. + /// Generated Rust type name to WIT FQN, used by `#[note_codec]` lookup. types: BTreeMap, /// Marked codecs keyed by the WIT FQN they implement. codecs: BTreeMap, } +/// The registrations of every crate this macro process expanded, keyed by crate. +/// +/// Procedural macros register as they expand, so `#[note_codec]` and `export_codecs!` read what +/// an earlier invocation in the same crate wrote. static REGISTRY: OnceLock>> = OnceLock::new(); /// Source location of one macro expansion. @@ -54,12 +58,16 @@ fn expansion_location(span: Span) -> ExpansionLocation { /// Returns the key of the crate whose macro expansion is running. /// -/// Long-lived macro hosts such as the rust-analyzer proc-macro server expand many crates in -/// one process; the key keeps their registrations apart. +/// Long-lived macro hosts such as the rust-analyzer proc-macro server expand many crates in one +/// process; the key keeps their registrations apart. One Cargo package builds many crates, such +/// as a library and its integration tests, so the key names the crate as well as the package +/// directory. fn macro_invocation_crate_key() -> String { - std::env::var("CARGO_MANIFEST_DIR") + let package = std::env::var("CARGO_MANIFEST_DIR") .or_else(|_| std::env::var("CARGO_PKG_NAME")) - .unwrap_or_default() + .unwrap_or_default(); + let crate_name = std::env::var("CARGO_CRATE_NAME").unwrap_or_default(); + format!("{package}\u{1f}{crate_name}") } /// Returns the shared registry map keyed by expanding crate. @@ -71,6 +79,7 @@ fn registry() -> &'static Mutex> { pub(crate) fn register_schema(schema: &NoteStorageSchema, span: Span) -> syn::Result<()> { let mut bindings = BTreeMap::new(); collect_type_bindings(schema.root(), &mut BTreeSet::new(), &mut bindings)?; + let bindings = index_by_rust_type_name(&bindings, span)?; let mut registries = registry() .lock() @@ -161,7 +170,27 @@ pub(crate) fn registered_codecs(span: Span) -> syn::Result, + span: Span, +) -> syn::Result> { + let mut index = BTreeMap::new(); + for (fqn, rust_name) in bindings { + if let Some(existing) = index.insert(rust_name.clone(), fqn.clone()) { + return Err(syn::Error::new( + span, + format!("WIT types `{existing}` and `{fqn}` both map to Rust type `{rust_name}`"), + )); + } + } + Ok(index) +} + +/// Collects reachable generated record and variant bindings, keyed by WIT FQN. fn collect_type_bindings( ty: &SchemaType, seen: &mut BTreeSet, @@ -183,7 +212,7 @@ fn collect_type_bindings( if !seen.insert(fqn.to_owned()) { return Ok(()); } - bindings.insert(name.to_upper_camel_case(), fqn.to_owned()); + bindings.insert(fqn.to_owned(), generated_type_ident(name)); } match ty.kind() { diff --git a/sdk/note-codec/macros/src/tests.rs b/sdk/note-codec/macros/src/tests.rs index 4dd81038dd..751d22870b 100644 --- a/sdk/note-codec/macros/src/tests.rs +++ b/sdk/note-codec/macros/src/tests.rs @@ -90,6 +90,45 @@ fn schema_and_codec_registration_generate_native_and_wasm_dispatch() { assert!(source.contains("exports::miden::note_codec::codec::Guest")); } +const RESERVED_NAME_SCHEMA: &str = r#" +package example:reserved-schema@1.0.0; + +interface note-storage { + record %self { + value: u64, + } + + record reserved-note { + inner: %self, + } + + type storage = reserved-note; +} +"#; + +#[test] +fn a_reserved_wit_name_registers_under_the_generated_rust_name() { + let _guard = lock_registry(); + reset_for_tests(); + let schema = LitStr::new(RESERVED_NAME_SCHEMA, Span::call_site()); + let generated = expand::from_wit_text(&schema).unwrap(); + // The generator escapes the Rust reserved word, so the registry key must match `Self_`. + let item: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for Self_ { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + expand::note_codec(quote!(), item).unwrap(); + let exported = expand::export_codecs(quote!()).unwrap(); + let source = prettyplease::unparse(&syn::parse2(quote!(#generated #exported)).unwrap()); + + assert!(source.contains("pub struct Self_")); + assert!(source.contains("example:reserved-schema/note-storage@1.0.0.self")); +} + #[test] fn a_crate_cannot_register_two_distinct_schemas() { let _guard = lock_registry(); diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs index a2d43970ef..fb2ea70bb2 100644 --- a/sdk/note-codec/tests/component_export.rs +++ b/sdk/note-codec/tests/component_export.rs @@ -42,6 +42,8 @@ fn minimal_codec_crate_builds_to_wasi_only_component() { for &variable in NESTED_CARGO_SCRUB_ENV { command.env_remove(variable); } + // Pin the guest rustflags after the scrub, the way the compiler builds a codec crate. + command.env("RUSTFLAGS", miden_note_schema::NOTE_CODEC_GUEST_RUSTFLAGS); let output = command.output().expect("failed to run cargo for the component fixture"); assert_command_succeeded("building the component fixture", &output); @@ -49,6 +51,12 @@ fn minimal_codec_crate_builds_to_wasi_only_component() { target_dir.join(format!("{WASM_TARGET}/release/note_codec_component_fixture.wasm")), ) .expect("component fixture did not produce a Wasm component"); + // A codec crate must build to a component every consumer accepts. + miden_note_schema::validate_note_codec_component( + &component, + miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES, + ) + .expect("the built component must pass the note codec load policy"); let DecodedWasm::Component(resolve, world_id) = wit_component::decode(&component).expect("failed to decode the built component") else { diff --git a/sdk/note-schema/codegen/src/lib.rs b/sdk/note-schema/codegen/src/lib.rs index 6215b60fa4..f26c1c97ca 100644 --- a/sdk/note-schema/codegen/src/lib.rs +++ b/sdk/note-schema/codegen/src/lib.rs @@ -277,7 +277,7 @@ fn generate_helper_traits(runtime: &RuntimePaths) -> TokenStream { } #[doc(hidden)] - trait __MidenNoteDecode: Sized { + trait __MidenNoteDecode: ::core::marker::Sized { fn __read_note_felts( reader: &mut #miden_field_repr::FeltReader<'_>, ) -> #miden_note_schema::Result; @@ -402,22 +402,33 @@ fn generate_type( let fqn = definition.fqn().expect("generated type definitions always have a FQN"); let ident = rust_names.get(fqn).expect("every generated type has a Rust identifier"); let docs = type_docs(definition, fqn); + // Every trait is named through its absolute path. A generated type takes the name of its + // WIT type, so a schema may declare a type called `Clone`, `Debug`, or `Sized`. let derives = if felt_repr_support.supports_native_felt_repr(definition) { let miden_field_repr = &runtime.miden_field_repr; + // `quote` prints `::` paths with spaces around each separator. The attribute value must + // be one path the derive macro can parse, so the spaces come out. let crate_path = Literal::string(&miden_field_repr.to_string().replace(' ', "")); quote! { #[derive( - Clone, - Debug, - PartialEq, - Eq, + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, #miden_field_repr::ToFeltRepr, #miden_field_repr::FromFeltRepr, )] #[felt_repr(crate_path = #crate_path)] } } else { - quote! { #[derive(Clone, Debug, PartialEq, Eq)] } + quote! { + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, + )] + } }; let (item, encode_impl, decode_impl) = match definition.kind() { @@ -731,6 +742,14 @@ fn case_docs(case: &SchemaCase) -> TokenStream { quote!(#[doc = #docs]) } +/// Returns the Rust type name the generator gives one WIT type or case name. +/// +/// A caller that maps generated types back to WIT names uses this function, so the two sides +/// agree on the upper camel case conversion and on the escape of a Rust reserved word. +pub fn generated_type_ident(name: &str) -> String { + type_ident(name).to_string() +} + /// Converts a WIT type or case name to a Rust type identifier. fn type_ident(name: &str) -> Ident { rust_ident(&name.to_upper_camel_case()) diff --git a/sdk/note-schema/src/artifact.rs b/sdk/note-schema/src/artifact.rs index 4648c68aa0..61f8ba08cc 100644 --- a/sdk/note-schema/src/artifact.rs +++ b/sdk/note-schema/src/artifact.rs @@ -14,6 +14,7 @@ use crate::{Error, NoteStorageSchema, Result}; pub struct NotePackageArtifact { path: PathBuf, schema: NoteStorageSchema, + tracked_inputs: Vec, } impl NotePackageArtifact { @@ -26,6 +27,15 @@ impl NotePackageArtifact { pub const fn schema(&self) -> &NoteStorageSchema { &self.schema } + + /// Returns every manifest the resolver read to derive the package file name. + /// + /// A caller registers these files as rebuild inputs next to the package itself. A rename of + /// the note package changes a manifest but leaves the old package file in place, so nothing + /// else tells the caller that the selection changed. + pub fn tracked_inputs(&self) -> &[PathBuf] { + &self.tracked_inputs + } } /// Resolves package artifacts for one note macro crate. @@ -49,20 +59,21 @@ impl<'a> NotePackageResolver<'a> { project_dir.display() ))); } - let stems = project_package_stems(&project_dir); + let identity = project_identity(&project_dir); + let stems = &identity.stems; let cache_dir = package_cache_dir() .map_err(|error| Error::new(format!("{}: {error}", self.macro_crate)))?; - let package_path = resolve_project_package(&project_dir, &stems, cache_dir.as_deref()) + let package_path = resolve_project_package(&project_dir, stems, cache_dir.as_deref()) .map_err(|error| Error::new(format!("{}: {error}", self.macro_crate)))? .ok_or_else(|| { Error::new(missing_project_package_message( self.macro_crate, &project_dir, cache_dir.as_deref(), - &stems, + stems, )) })?; - self.load_package(package_path) + self.load_package(package_path, identity.manifests) } /// Loads one exact Miden package path. @@ -75,7 +86,7 @@ impl<'a> NotePackageResolver<'a> { package_path.display() ))); } - self.load_package(package_path) + self.load_package(package_path, Vec::new()) } /// Resolves one path relative to the consuming crate manifest. @@ -94,7 +105,11 @@ impl<'a> NotePackageResolver<'a> { } /// Loads the package and its unique schema section. - fn load_package(&self, path: PathBuf) -> Result { + fn load_package( + &self, + path: PathBuf, + tracked_inputs: Vec, + ) -> Result { let path = path.canonicalize().map_err(|error| { Error::new(format!( "{}: failed to resolve Miden package '{}': {error}", @@ -116,11 +131,18 @@ impl<'a> NotePackageResolver<'a> { path.display() )) })?; - Ok(NotePackageArtifact { path, schema }) + Ok(NotePackageArtifact { + path, + schema, + tracked_inputs, + }) } } /// Resolves one project package by package identity and output-directory priority. +/// +/// A set package-cache directory replaces the search: the build owns that directory, so the +/// output directories of the project are not consulted. fn resolve_project_package( project_dir: &Path, stems: &[String], @@ -258,19 +280,28 @@ fn find_project_package_in_dir(dir: &Path, stems: &[String]) -> Result Vec { +/// The package file name stems of one project and the manifests they came from. +struct ProjectIdentity { + /// Ordered package file name stems, canonical identity first. + stems: Vec, + /// The manifests that were read, in the order they were read. + manifests: Vec, +} + +/// Returns ordered package filename stems for one project, and the manifests they came from. +fn project_identity(project_dir: &Path) -> ProjectIdentity { let mut stems = Vec::new(); - if let Some(name) = package_name_from_manifest(&project_dir.join("miden-project.toml")) { - push_package_stem(&mut stems, &name); - } - if let Some(name) = package_name_from_manifest(&project_dir.join("Cargo.toml")) { - push_package_stem(&mut stems, &name); + let mut manifests = Vec::new(); + for manifest in [project_dir.join("miden-project.toml"), project_dir.join("Cargo.toml")] { + if let Some(name) = package_name_from_manifest(&manifest) { + push_package_stem(&mut stems, &name); + manifests.push(manifest); + } } if let Some(name) = project_dir.file_name().and_then(|name| name.to_str()) { push_package_stem(&mut stems, name); } - stems + ProjectIdentity { stems, manifests } } /// Reads a package name from one TOML manifest. @@ -354,7 +385,7 @@ mod tests { use super::{ find_project_package_in_cache, find_project_package_in_dir, - missing_project_package_message, project_output_dirs, project_package_stems, + missing_project_package_message, project_identity, project_output_dirs, }; #[test] @@ -372,11 +403,41 @@ mod tests { fs::write(output.join("legacy-note.masp"), b"newer legacy package").unwrap(); fs::write(output.join("canonical-note.masp"), b"canonical package").unwrap(); - let stems = project_package_stems(temp.path()); + let stems = project_identity(temp.path()).stems; let selected = find_project_package_in_dir(&output, &stems).unwrap().unwrap(); assert_eq!(selected, output.join("canonical-note.masp")); } + #[test] + fn tracked_inputs_name_the_manifests_that_were_read() { + let temp = tempfile::tempdir().unwrap(); + fs::write( + temp.path().join("miden-project.toml"), + "[package]\nname='canonical-note'\nversion='0.1.0'", + ) + .unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='legacy-note'\nversion='0.1.0'") + .unwrap(); + + let identity = project_identity(temp.path()); + + assert_eq!( + identity.manifests, + [temp.path().join("miden-project.toml"), temp.path().join("Cargo.toml")] + ); + } + + #[test] + fn tracked_inputs_skip_a_manifest_that_is_absent() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='legacy-note'\nversion='0.1.0'") + .unwrap(); + + let identity = project_identity(temp.path()); + + assert_eq!(identity.manifests, [temp.path().join("Cargo.toml")]); + } + #[test] fn manifest_ancestor_target_directories_are_candidates() { let temp = tempfile::tempdir().unwrap(); @@ -395,7 +456,7 @@ mod tests { let temp = tempfile::tempdir().unwrap(); fs::write(temp.path().join("Cargo.toml"), "[package]\nname='note'\nversion='0.1.0'") .unwrap(); - let stems = project_package_stems(temp.path()); + let stems = project_identity(temp.path()).stems; let message = missing_project_package_message("test-note-macro", temp.path(), None, &stems); assert!(message.contains("test-note-macro")); assert!(message.contains("cargo miden build --manifest-path")); @@ -408,7 +469,7 @@ mod tests { fs::write(temp.path().join("Cargo.toml"), "[package]\nname='test-note'\nversion='0.1.0'") .unwrap(); let cache_dir = temp.path().join("package-cache"); - let stems = project_package_stems(temp.path()); + let stems = project_identity(temp.path()).stems; let message = missing_project_package_message( "test-note-macro", diff --git a/sdk/note-schema/src/builder.rs b/sdk/note-schema/src/builder.rs index a31da2b670..59a54438f3 100644 --- a/sdk/note-schema/src/builder.rs +++ b/sdk/note-schema/src/builder.rs @@ -117,8 +117,9 @@ fn reject_unsupported_constructor_path( /// Normalizes and validates a dotted field path. fn normalize_path(path: &str) -> Result> { + // `split` always yields one segment, so only an empty segment can fail here. let segments = path.split('.').map(normalize_name).collect::>(); - if segments.is_empty() || segments.iter().any(String::is_empty) { + if segments.iter().any(String::is_empty) { return Err(Error::new(format!("invalid empty note storage path `{path}`"))); } Ok(segments) diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs index 7310a9ca10..1057dd00af 100644 --- a/sdk/note-schema/src/codec_component.rs +++ b/sdk/note-schema/src/codec_component.rs @@ -12,7 +12,7 @@ use wasmtime::{ use crate::{ CodecFailure, CodecRegistry, ConsumerTypeCodec, Error, NoteStorageSchema, Result, - codec_structure::{MAX_CORE_INSTANCES, MAX_DEFINED_MEMORIES, MAX_DEFINED_TABLES}, + codec_structure::{MAX_CORE_INSTANCES, MAX_INSTANTIATED_MEMORIES, MAX_INSTANTIATED_TABLES}, validate_note_codec_component, }; @@ -27,19 +27,20 @@ const SUPPORTED_TYPES: &str = "supported-types"; /// Maximum instances one codec call store may hold. /// -/// The value is the structural budget for core instantiations, so the two cannot drift: a -/// component that passes the load policy can also instantiate. +/// The value is the structural budget for the core instances one instantiation creates. The load +/// policy counts what instantiation creates, so a component that loads instantiates within this +/// count. const MAX_STORE_INSTANCES: usize = MAX_CORE_INSTANCES; /// Maximum tables one codec call store may hold. /// -/// The value is the structural budget for defined tables. -const MAX_STORE_TABLES: usize = MAX_DEFINED_TABLES; +/// The value is the structural budget for the tables one instantiation creates. +const MAX_STORE_TABLES: usize = MAX_INSTANTIATED_TABLES; /// Maximum linear memories one codec call store may hold. /// -/// The value is the structural budget for defined memories. -const MAX_STORE_MEMORIES: usize = MAX_DEFINED_MEMORIES; +/// The value is the structural budget for the linear memories one instantiation creates. +const MAX_STORE_MEMORIES: usize = MAX_INSTANTIATED_MEMORIES; wasmtime::component::bindgen!({ path: "wit", @@ -53,8 +54,8 @@ wasmtime::component::bindgen!({ /// The store counts are fixed policy instead of host configuration. One call store admits at /// most `MAX_STORE_INSTANCES` instances, `MAX_STORE_TABLES` tables, and `MAX_STORE_MEMORIES` /// linear memory, and one call gets `MAX_WASM_STACK_BYTES` of Wasm stack. The structural policy -/// in [`validate_note_codec_component`] uses the same counts, so a component that loads can also -/// instantiate. +/// in [`validate_note_codec_component`] counts what one instantiation creates and applies the +/// same values, so a component that loads instantiates within these counts. #[derive(Clone, Debug, PartialEq, Eq)] pub struct CodecLimits { /// Fuel budget for one codec call, about one unit per Wasm instruction. @@ -104,7 +105,8 @@ impl CodecRegistry { /// registry comes back with the standard codecs alone, the same base the bundled-codec path /// registers on top of. More than one note codec section is still an error. pub fn load_from_package_with_limits(package: &Package, limits: CodecLimits) -> Result { - let schema = NoteStorageSchema::from_package(package)?; + // The section check comes first. A package without a codec section needs no schema, and + // a unit `#[note]` struct emits neither section. let section_id = package_note_codec_section_id(); if !package.sections.iter().any(|section| section.id == section_id) { return Ok(Self::default()); @@ -114,10 +116,11 @@ impl CodecRegistry { section_id, PACKAGE_NOTE_CODEC_SECTION_ID, )?; + let schema = NoteStorageSchema::from_package(package)?; Self::load_from_component(bytes, &schema.custom_type_fqns(), limits) } - /// Loads a note codec component whose only imports are stubbed WASI interfaces. + /// Loads a note codec component with every unresolved import stubbed as a trap. fn load_from_component( bytes: &[u8], custom_type_fqns: &HashSet, @@ -144,13 +147,17 @@ impl CodecRegistry { /// A compiled component used to create an isolated instance for each operation. struct ComponentRuntime { + /// The engine that holds the accepted Wasm proposals and the fuel setting. engine: Engine, + /// The compiled codec component. component: Component, + /// The host limits applied to every instance and every call. limits: CodecLimits, } /// Store state that owns the component resource limits. struct ComponentStore { + /// The inner limiter that decides every memory and table question. limits: StoreLimits, /// Set when a memory or table growth was refused, so a failed call reports its class. limit_hit: bool, @@ -210,9 +217,11 @@ impl ResourceLimiter for ComponentStore { /// One isolated codec component call context. struct ComponentInstance { + /// The store that holds the fuel budget and the resource limits of this call. store: Store, /// The raw instance, used by the calls that lift their results lazily. instance: Instance, + /// The generated typed bindings of the codec world. bindings: NoteCodec, } @@ -312,14 +321,21 @@ impl ComponentRuntime { } = self.instantiate()?; let interface = instance.get_export_index(&mut store, None, CODEC_INTERFACE).ok_or_else(|| { - Error::new(format!("note codec component exports no `{CODEC_INTERFACE}`")) + Error::codec( + CodecFailure::Trapped, + format!("note codec component exports no `{CODEC_INTERFACE}`"), + ) })?; let export = instance .get_export_index(&mut store, Some(&interface), SUPPORTED_TYPES) .ok_or_else(|| { - Error::new(format!( - "note codec component exports no `{SUPPORTED_TYPES}` in `{CODEC_INTERFACE}`" - )) + Error::codec( + CodecFailure::Trapped, + format!( + "note codec component exports no `{SUPPORTED_TYPES}` in \ + `{CODEC_INTERFACE}`" + ), + ) })?; let supported_types = instance .get_typed_func::<(), (WasmList,)>(&mut store, &export) @@ -382,7 +398,9 @@ fn component_store_limits(limits: &CodecLimits) -> StoreLimits { /// A registry entry that dispatches one FQN through isolated component instances. struct ComponentCodec { + /// The fully qualified WIT name this entry answers for. fqn: String, + /// The compiled component shared by every registry entry of one package. runtime: Arc, } @@ -530,14 +548,23 @@ fn validate_reported_fqns( /// the trap is only how the refusal surfaced. fn classify_failure(store: &Store, error: &wasmtime::Error) -> CodecFailure { if store.data().limit_hit { - CodecFailure::LimitExceeded - } else if error.downcast_ref::() == Some(&Trap::OutOfFuel) { - CodecFailure::OutOfFuel - } else { - CodecFailure::Trapped + return CodecFailure::LimitExceeded; + } + match error.downcast_ref::() { + Some(&Trap::OutOfFuel) => CodecFailure::OutOfFuel, + Some(_) => CodecFailure::Trapped, + // Wasmtime applies the store instance, table, and memory counts without asking the + // limiter, so that failure carries no trap and no recorded refusal. + None if reports_store_count_limit(error) => CodecFailure::LimitExceeded, + None => CodecFailure::Trapped, } } +/// Returns true when one wasmtime error reports a store count that was reached. +fn reports_store_count_limit(error: &wasmtime::Error) -> bool { + format!("{error:#}").contains("resource limit exceeded") +} + /// Reports a component that did not reach its first exported call. fn start_failure(store: &Store, error: wasmtime::Error) -> Error { Error::codec( @@ -588,8 +615,11 @@ fn component_values_to_felts(fqn: &str, values: &[u64]) -> Result> { } /// Creates a host error for a component runtime failure. +/// +/// The engine, the compilation, the import stubs, and the lifting of a returned value all fail +/// this way, and a consumer sees one class for a codec the engine did not run. fn component_error(action: &str, error: impl core::fmt::Display) -> Error { - Error::new(format!("failed to {action}: {error:#}")) + Error::codec(CodecFailure::Trapped, format!("failed to {action}: {error:#}")) } /// Creates a host error for an author codec rejection. @@ -855,6 +885,18 @@ package miden:base@1.0.0 { assert!(!registry.contains(FIXTURE_FQN)); } + #[test] + fn a_package_without_a_schema_or_a_codec_section_keeps_the_standard_codecs() { + // A unit `#[note]` struct emits neither section, and a host may probe any package. + let package = test_package(); + + let registry = CodecRegistry::load_from_package(&package).unwrap(); + + assert!(registry.contains(crate::FELT_FQN)); + assert!(registry.contains(crate::WORD_FQN)); + assert!(!registry.contains(FIXTURE_FQN)); + } + #[test] fn simd_components_do_not_load() { let component = wat::parse_str( diff --git a/sdk/note-schema/src/codec_structure.rs b/sdk/note-schema/src/codec_structure.rs index 3401383177..e3dcd53e10 100644 --- a/sdk/note-schema/src/codec_structure.rs +++ b/sdk/note-schema/src/codec_structure.rs @@ -10,22 +10,32 @@ //! //! - the component fits in the caller's byte budget; //! - the component validates under [`NOTE_CODEC_WASM_FEATURES`]; -//! - the component declares no start function; -//! - each core module stays under its own counts, and its code section keeps a plausible -//! average function size; -//! - the component tree stays under its budgets for core modules, nesting depth, core -//! instantiations, component instantiations, defined tables, and defined memories; -//! - each component-level section stays under its width cap. +//! - the component declares no component-level start function; +//! - each core module stays under its own counts, each of its function types stays under the +//! parameter and result caps, and its code section keeps a plausible average function size; +//! - the component tree stays under its budgets for core modules and nesting depth; +//! - one instantiation of any component in the tree stays under its budgets for core +//! instantiations, component instantiations, linear memories, and tables; +//! - each kind of component-level entry stays under its cap for the whole tree. +//! +//! The walk counts what an instantiation creates, not what the tree declares. A core module that +//! is instantiated twice counts twice, and a nested component contributes what one instantiation +//! of it creates, once per instantiation. A consumer store admits the same counts, so a component +//! that passes the walk instantiates inside the store limits. +//! +//! The walk rejects only a component-level start function. A core module keeps its own start +//! function, which runs at instantiation under the consumer fuel budget and store limits. //! //! Nothing else is checked here. The producer checks the exported codec interface, and a //! consumer bounds the run time of a codec call with fuel and store limits. use wasmparser::{ - Encoding, FuncValidatorAllocations, Parser, Payload, TypeRef, ValidPayload, Validator, - WasmFeatures, + ComponentAlias, ComponentExternalKind, ComponentInstance, ComponentOuterAliasKind, + ComponentTypeRef, Encoding, FuncValidatorAllocations, Instance, Parser, Payload, TypeRef, + ValidPayload, Validator, WasmFeatures, }; -use crate::{Error, Result}; +use crate::{CodecFailure, Error, Result}; /// Wasm proposals a note codec component may use. /// @@ -52,7 +62,13 @@ const MAX_MODULE_FUNCTIONS: usize = 10_000; /// Maximum globals in one core module, imported and defined. const MAX_MODULE_GLOBALS: usize = 1_000; +/// Maximum types in one core module. +const MAX_MODULE_TYPES: usize = 1_000; + /// Maximum tables in one core module, imported and defined. +/// +/// A module that is instantiated reaches the tighter tree-wide instantiated-table budget first. +/// This cap constrains imported tables, and the tables of a module that is never instantiated. const MAX_MODULE_TABLES: usize = 100; /// Maximum linear memories in one core module, imported and defined. @@ -93,30 +109,33 @@ const MAX_CORE_MODULES: usize = 16; /// Maximum component nesting depth. const MAX_COMPONENT_DEPTH: usize = 4; -/// Maximum entries in one component-level section. +/// Maximum entries of one kind of component-level item in the whole component tree. +/// +/// A component may split one kind over many sections, so the cap counts each kind over the +/// whole tree: core types, component types, aliases, canonical functions, imports, and exports. const MAX_COMPONENT_SECTION_ITEMS: usize = 256; -/// Maximum core instantiations in the whole component tree. +/// Maximum core instances one instantiation of a component creates. /// -/// The consumer store admits the same number of instances. The budget is tree-wide because a -/// nested component is expanded once per instantiation of its parent, so per-level budgets -/// multiply. +/// The consumer store admits the same number of instances. pub(crate) const MAX_CORE_INSTANCES: usize = 32; -/// Maximum component instantiations in the whole component tree. +/// Maximum nested component instances one instantiation of a component creates. /// -/// A `wasm32-wasip2` codec instantiates one component instance per exported interface. -pub(crate) const MAX_COMPONENT_INSTANCES: usize = 8; +/// A `wasm32-wasip2` codec builds one nested component instance per exported interface. The +/// budget has no consumer store counterpart: a component instance holds no core instance, +/// memory, or table of its own, and what it creates is counted through those budgets. +const MAX_COMPONENT_INSTANCES: usize = 8; -/// Maximum tables defined in the whole component tree. +/// Maximum tables one instantiation of a component creates. /// /// The consumer store admits the same number of tables. -pub(crate) const MAX_DEFINED_TABLES: usize = 32; +pub(crate) const MAX_INSTANTIATED_TABLES: usize = 32; -/// Maximum linear memories defined in the whole component tree. +/// Maximum linear memories one instantiation of a component creates. /// /// The consumer store admits the same number of memories. -pub(crate) const MAX_DEFINED_MEMORIES: usize = 1; +pub(crate) const MAX_INSTANTIATED_MEMORIES: usize = 1; /// Applies the whole note codec component policy: the byte cap, the Wasm feature set, and the /// structural limits. @@ -144,7 +163,7 @@ pub fn validate_note_codec_structure(component: &[u8]) -> Result<()> { // the component is instantiated, before the export call the limits are built around, and // the validator reports only that component values are disabled. if matches!(payload, Payload::ComponentStartSection { .. }) { - return Err(Error::new( + return Err(policy_rejection( "note codec component declares a start function; the policy rejects a component \ that runs code when it is instantiated", )); @@ -166,15 +185,9 @@ fn ensure_component_byte_limit(byte_len: usize, limit: usize) -> Result<()> { if byte_len <= limit { return Ok(()); } - let message = - format!("note codec component is {byte_len} bytes; the pre-compilation limit is {limit}"); - // A consumer classifies the byte cap like every other cap it applies to a codec. Without the - // consumer adapter there is no failure class to report. - #[cfg(feature = "codec-component")] - let error = Error::codec(crate::CodecFailure::LimitExceeded, message); - #[cfg(not(feature = "codec-component"))] - let error = Error::new(message); - Err(error) + Err(policy_rejection(format!( + "note codec component is {byte_len} bytes; the pre-compilation limit is {limit}" + ))) } /// The state carried while the parser walks one component. @@ -184,27 +197,86 @@ struct StructureWalk { frames: Vec, /// Core modules seen anywhere in the component. core_modules: usize, - /// Core instantiations declared anywhere in the component. - core_instances: usize, - /// Component instantiations declared anywhere in the component. - component_instances: usize, - /// Tables defined anywhere in the component, excluding imported tables. - defined_tables: usize, - /// Linear memories defined anywhere in the component, excluding imported memories. - defined_memories: usize, + /// Component-level items declared anywhere in the component, one count per kind. + component_items: ComponentItemCounts, +} + +/// Component-level items counted over the whole component tree. +#[derive(Default)] +struct ComponentItemCounts { + core_types: usize, + types: usize, + aliases: usize, + canonical_functions: usize, + imports: usize, + exports: usize, } /// One nesting level of the walk. enum Frame { /// A core module, with the counters checked when the module ends. Module(ModuleCounts), - /// A component, counted only for the nesting depth. - Component, + /// A component, with its index spaces and what one instantiation of it creates. + Component(ComponentFrame), +} + +/// One component nesting level. +/// +/// An index-space entry is `None` for a core module or a component the walk cannot read, such as +/// an imported or an aliased one. An instantiation of such an entry is rejected. +#[derive(Default)] +struct ComponentFrame { + /// The core module index space of this component. + core_modules: Vec>, + /// The component index space of this component. + components: Vec>, + /// What one instantiation of this component creates. + created: CreatedCounts, +} + +/// What one core module creates every time it is instantiated. +#[derive(Clone, Copy, Default)] +struct ModuleRuntimeCounts { + memories: usize, + tables: usize, +} + +/// What one instantiation of a component creates. +#[derive(Clone, Copy, Default)] +struct CreatedCounts { + core_instances: usize, + component_instances: usize, + memories: usize, + tables: usize, +} + +impl CreatedCounts { + /// Adds what one nested instantiation creates. + fn add(&mut self, other: Self) { + self.core_instances = self.core_instances.saturating_add(other.core_instances); + self.component_instances = + self.component_instances.saturating_add(other.component_instances); + self.memories = self.memories.saturating_add(other.memories); + self.tables = self.tables.saturating_add(other.tables); + } + + /// Checks every budget that bounds one instantiation. + fn check(&self) -> Result<()> { + ensure_created_cap("core instances", self.core_instances, MAX_CORE_INSTANCES)?; + ensure_created_cap( + "component instances", + self.component_instances, + MAX_COMPONENT_INSTANCES, + )?; + ensure_created_cap("linear memories", self.memories, MAX_INSTANTIATED_MEMORIES)?; + ensure_created_cap("tables", self.tables, MAX_INSTANTIATED_TABLES) + } } /// Counters collected for one core module. #[derive(Default)] struct ModuleCounts { + types: usize, functions: usize, globals: usize, tables: usize, @@ -213,11 +285,14 @@ struct ModuleCounts { data_segments: usize, imports: usize, exports: usize, + /// The memories and tables one instantiation of this module creates. + runtime: ModuleRuntimeCounts, } impl ModuleCounts { /// Checks every per-module cap once the module ends. fn check(&self) -> Result<()> { + ensure_module_cap("types", self.types, MAX_MODULE_TYPES)?; ensure_module_cap("functions", self.functions, MAX_MODULE_FUNCTIONS)?; ensure_module_cap("globals", self.globals, MAX_MODULE_GLOBALS)?; ensure_module_cap("tables", self.tables, MAX_MODULE_TABLES)?; @@ -236,6 +311,7 @@ impl StructureWalk { Payload::Version { encoding, .. } => self.enter(encoding)?, Payload::End(_) => self.leave()?, Payload::TypeSection(reader) => { + self.module_counts()?.types += reader.count() as usize; // The feature validator rejects a GC type before the walk sees the section, so // only a core function type reaches this loop. for ty in reader.into_iter_err_on_gc_types() { @@ -266,15 +342,15 @@ impl StructureWalk { } Payload::TableSection(reader) => { let count = reader.count() as usize; - self.module_counts()?.tables += count; - self.defined_tables += count; - ensure_tree_cap("defined tables", self.defined_tables, MAX_DEFINED_TABLES)?; + let counts = self.module_counts()?; + counts.tables += count; + counts.runtime.tables += count; } Payload::MemorySection(reader) => { let count = reader.count() as usize; - self.module_counts()?.memories += count; - self.defined_memories += count; - ensure_tree_cap("defined memories", self.defined_memories, MAX_DEFINED_MEMORIES)?; + let counts = self.module_counts()?; + counts.memories += count; + counts.runtime.memories += count; } Payload::ElementSection(reader) => { self.module_counts()?.element_segments += reader.count() as usize; @@ -289,34 +365,53 @@ impl StructureWalk { ensure_average_function_size(count, size)?; } Payload::InstanceSection(reader) => { - self.core_instances += reader.count() as usize; - ensure_tree_cap("core instantiations", self.core_instances, MAX_CORE_INSTANCES)?; + for instance in reader { + self.visit_core_instance(instance.map_err(malformed)?)?; + } } Payload::ComponentInstanceSection(reader) => { - self.component_instances += reader.count() as usize; - ensure_tree_cap( - "component instantiations", - self.component_instances, - MAX_COMPONENT_INSTANCES, - )?; + for instance in reader { + self.visit_component_instance(instance.map_err(malformed)?)?; + } } Payload::CoreTypeSection(reader) => { - ensure_component_section_cap("core types", reader.count() as usize)?; + self.component_items.core_types += reader.count() as usize; + ensure_component_item_cap("core types", self.component_items.core_types)?; } Payload::ComponentTypeSection(reader) => { - ensure_component_section_cap("types", reader.count() as usize)?; + self.component_items.types += reader.count() as usize; + ensure_component_item_cap("types", self.component_items.types)?; } Payload::ComponentAliasSection(reader) => { - ensure_component_section_cap("aliases", reader.count() as usize)?; + for alias in reader { + let alias = alias.map_err(malformed)?; + self.component_items.aliases += 1; + ensure_component_item_cap("aliases", self.component_items.aliases)?; + self.declare_aliased_item(&alias)?; + } } Payload::ComponentCanonicalSection(reader) => { - ensure_component_section_cap("canonical functions", reader.count() as usize)?; + self.component_items.canonical_functions += reader.count() as usize; + ensure_component_item_cap( + "canonical functions", + self.component_items.canonical_functions, + )?; } Payload::ComponentImportSection(reader) => { - ensure_component_section_cap("imports", reader.count() as usize)?; + for import in reader { + let import = import.map_err(malformed)?; + self.component_items.imports += 1; + ensure_component_item_cap("imports", self.component_items.imports)?; + match import.ty { + ComponentTypeRef::Module(_) => self.declare_core_module(None)?, + ComponentTypeRef::Component(_) => self.declare_component(None)?, + _ => {} + } + } } Payload::ComponentExportSection(reader) => { - ensure_component_section_cap("exports", reader.count() as usize)?; + self.component_items.exports += reader.count() as usize; + ensure_component_item_cap("exports", self.component_items.exports)?; } _ => {} } @@ -329,7 +424,7 @@ impl StructureWalk { Encoding::Module => { self.core_modules += 1; if self.core_modules > MAX_CORE_MODULES { - return Err(Error::new(format!( + return Err(policy_rejection(format!( "note codec component has {} core modules; the limit is {MAX_CORE_MODULES}", self.core_modules ))); @@ -337,11 +432,11 @@ impl StructureWalk { self.frames.push(Frame::Module(ModuleCounts::default())); } Encoding::Component => { - self.frames.push(Frame::Component); + self.frames.push(Frame::Component(ComponentFrame::default())); let depth = - self.frames.iter().filter(|frame| matches!(frame, Frame::Component)).count(); + self.frames.iter().filter(|frame| matches!(frame, Frame::Component(_))).count(); if depth > MAX_COMPONENT_DEPTH { - return Err(Error::new(format!( + return Err(policy_rejection(format!( "note codec component nests components {depth} deep; the limit is \ {MAX_COMPONENT_DEPTH}" ))); @@ -351,13 +446,131 @@ impl StructureWalk { Ok(()) } - /// Closes one nesting level and checks the counters of a core module. + /// Closes one nesting level and records what it creates in the component that declares it. fn leave(&mut self) -> Result<()> { match self.frames.pop() { - Some(Frame::Module(counts)) => counts.check(), - // A component frame carries no counters, and an unbalanced end cannot happen: - // the parser reports one `End` for every header it accepted. - _ => Ok(()), + Some(Frame::Module(counts)) => { + counts.check()?; + // The module now holds an index in the core module index space of the component + // that declares it. + self.declare_core_module(Some(counts.runtime)) + } + Some(Frame::Component(frame)) => self.declare_component(Some(frame.created)), + // An unbalanced end cannot happen: the parser reports one `End` for every header it + // accepted. + None => Ok(()), + } + } + + /// Counts one core instantiation and what it creates. + fn visit_core_instance(&mut self, instance: Instance<'_>) -> Result<()> { + // An instance built from exports names items other instances already created. + let created = match instance { + Instance::Instantiate { module_index, .. } => { + let module = self.instantiated_core_module(module_index)?; + CreatedCounts { + core_instances: 1, + component_instances: 0, + memories: module.memories, + tables: module.tables, + } + } + Instance::FromExports(_) => CreatedCounts { + core_instances: 1, + ..CreatedCounts::default() + }, + }; + self.record_created(created) + } + + /// Counts one component instantiation and what it creates. + fn visit_component_instance(&mut self, instance: ComponentInstance<'_>) -> Result<()> { + // An instance built from exports names items other instances already created. + let ComponentInstance::Instantiate { + component_index, .. + } = instance + else { + return Ok(()); + }; + let mut created = self.instantiated_component(component_index)?; + created.component_instances = created.component_instances.saturating_add(1); + self.record_created(created) + } + + /// Adds what one instantiation creates to the component the walk is inside. + fn record_created(&mut self, created: CreatedCounts) -> Result<()> { + let frame = self.component_frame()?; + frame.created.add(created); + frame.created.check() + } + + /// Returns what one instantiation of a core module of the current component creates. + fn instantiated_core_module(&mut self, module_index: u32) -> Result { + let frame = self.component_frame()?; + frame.core_modules.get(module_index as usize).copied().flatten().ok_or_else(|| { + policy_rejection(format!( + "note codec component instantiates core module {module_index}, which the policy \ + cannot read; a codec instantiates only the core modules it defines" + )) + }) + } + + /// Returns what one instantiation of a nested component of the current component creates. + fn instantiated_component(&mut self, component_index: u32) -> Result { + let frame = self.component_frame()?; + frame + .components + .get(component_index as usize) + .copied() + .flatten() + .ok_or_else(|| { + policy_rejection(format!( + "note codec component instantiates component {component_index}, which the \ + policy cannot read; a codec instantiates only the components it defines" + )) + }) + } + + /// Adds one core module to the index space of the component the walk is inside. + /// + /// A core module at the top level belongs to no component index space, so it is dropped. + fn declare_core_module(&mut self, created: Option) -> Result<()> { + if let Some(Frame::Component(frame)) = self.frames.last_mut() { + frame.core_modules.push(created); + } + Ok(()) + } + + /// Adds one component to the index space of the component the walk is inside. + /// + /// The root component belongs to no component index space, so it is dropped. + fn declare_component(&mut self, created: Option) -> Result<()> { + if let Some(Frame::Component(frame)) = self.frames.last_mut() { + frame.components.push(created); + } + Ok(()) + } + + /// Adds one aliased core module or component to the current index spaces. + fn declare_aliased_item(&mut self, alias: &ComponentAlias<'_>) -> Result<()> { + match aliased_item_kind(alias) { + Some(AliasedItem::CoreModule) => self.declare_core_module(None), + Some(AliasedItem::Component) => self.declare_component(None), + None => Ok(()), + } + } + + /// Returns the frame of the component the walk is inside. + /// + /// Component sections appear only inside a component. A component section anywhere else is a + /// malformed layout, and the walk fails closed instead of guessing a frame for it. + fn component_frame(&mut self) -> Result<&mut ComponentFrame> { + match self.frames.last_mut() { + Some(Frame::Component(frame)) => Ok(frame), + _ => Err(policy_rejection( + "note codec component is malformed: a component section appears outside a \ + component", + )), } } @@ -368,28 +581,58 @@ impl StructureWalk { fn module_counts(&mut self) -> Result<&mut ModuleCounts> { match self.frames.last_mut() { Some(Frame::Module(counts)) => Ok(counts), - _ => Err(Error::new( + _ => Err(policy_rejection( "note codec component is malformed: a core section appears outside a core module", )), } } } +/// An index space one alias adds an item to. +enum AliasedItem { + CoreModule, + Component, +} + +/// Returns the index space one alias adds an item to, if the walk tracks that space. +fn aliased_item_kind(alias: &ComponentAlias<'_>) -> Option { + match alias { + ComponentAlias::InstanceExport { + kind: ComponentExternalKind::Module, + .. + } + | ComponentAlias::Outer { + kind: ComponentOuterAliasKind::CoreModule, + .. + } => Some(AliasedItem::CoreModule), + ComponentAlias::InstanceExport { + kind: ComponentExternalKind::Component, + .. + } + | ComponentAlias::Outer { + kind: ComponentOuterAliasKind::Component, + .. + } => Some(AliasedItem::Component), + _ => None, + } +} + /// Reports a core module that is over one of its caps. fn ensure_module_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { if observed > limit { - return Err(Error::new(format!( + return Err(policy_rejection(format!( "note codec component has a core module with {observed} {kind}; the limit is {limit}" ))); } Ok(()) } -/// Reports a component tree that is over one of its whole-tree budgets. -fn ensure_tree_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { +/// Reports a component that creates too much when it is instantiated. +fn ensure_created_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { if observed > limit { - return Err(Error::new(format!( - "note codec component has {observed} {kind}; the limit is {limit}" + return Err(policy_rejection(format!( + "note codec component creates {observed} {kind} when it is instantiated; the limit is \ + {limit}" ))); } Ok(()) @@ -398,18 +641,18 @@ fn ensure_tree_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { /// Reports a core function type that is over its parameter or result cap. fn ensure_signature_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { if observed > limit { - return Err(Error::new(format!( + return Err(policy_rejection(format!( "note codec component has a function type with {observed} {kind}; the limit is {limit}" ))); } Ok(()) } -/// Reports a component-level section that is over its width cap. -fn ensure_component_section_cap(kind: &str, observed: usize) -> Result<()> { +/// Reports a component tree that is over the cap for one kind of component-level item. +fn ensure_component_item_cap(kind: &str, observed: usize) -> Result<()> { if observed > MAX_COMPONENT_SECTION_ITEMS { - return Err(Error::new(format!( - "note codec component has a section with {observed} component {kind}; the limit is \ + return Err(policy_rejection(format!( + "note codec component has {observed} component {kind}; the limit is \ {MAX_COMPONENT_SECTION_ITEMS}" ))); } @@ -423,7 +666,7 @@ fn ensure_average_function_size(count: u32, size: u32) -> Result<()> { } let average = size / count; if average < MIN_AVERAGE_FUNCTION_BYTES { - return Err(Error::new(format!( + return Err(policy_rejection(format!( "note codec component has a core module with {count} functions in {size} bytes of \ code, an average of {average} bytes; the limit is {MIN_AVERAGE_FUNCTION_BYTES} bytes \ per function" @@ -434,12 +677,22 @@ fn ensure_average_function_size(count: u32, size: u32) -> Result<()> { /// Reports bytes that do not parse as a component. fn malformed(error: wasmparser::BinaryReaderError) -> Error { - Error::new(format!("note codec component is malformed: {error}")) + policy_rejection(format!("note codec component is malformed: {error}")) } /// Reports a component that does not validate under [`NOTE_CODEC_WASM_FEATURES`]. fn rejected_feature(error: wasmparser::BinaryReaderError) -> Error { - Error::new(format!("note codec component uses a Wasm feature the policy rejects: {error}")) + policy_rejection(format!( + "note codec component uses a Wasm feature the policy rejects: {error}" + )) +} + +/// Creates an error for a component the structural load policy does not admit. +/// +/// Every rejection carries one class, so a consumer reports a codec the policy refused the same +/// way it reports a codec that ran past a host limit. +fn policy_rejection(message: impl Into) -> Error { + Error::codec(CodecFailure::LimitExceeded, message) } #[cfg(test)] @@ -602,43 +855,137 @@ mod tests { } #[test] - fn nested_component_instantiations_are_rejected() { - // Three nesting levels that each instantiate the level below 300 times. A per-level cap - // would still admit 300^3 expansions, so the budget counts the whole tree. - let instantiate = |name: &str| format!("(instance (instantiate {name}))").repeat(300); - let text = format!( - "(component - (component $outer - (component $middle - (component $inner (core module)) - {inner_uses}) - {middle_uses}) - {outer_uses})", - inner_uses = instantiate("$inner"), - middle_uses = instantiate("$middle"), - outer_uses = instantiate("$outer"), + fn what_a_nested_component_creates_is_counted_once_per_instantiation() { + // One instantiation of the nested component creates one memory, so two instantiations + // reach the memory budget. A nested component that is never instantiated creates + // nothing. + let text = "(component + (component $inner + (core module $m (memory 1)) + (core instance (instantiate $m))) + (instance (instantiate $inner)) + (instance (instantiate $inner)))"; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!( + error.contains("creates 2 linear memories when it is instantiated"), + "unexpected error: {error}" + ); + assert!( + error.contains(&format!("the limit is {MAX_INSTANTIATED_MEMORIES}")), + "unexpected error: {error}" ); + } + + #[test] + fn a_component_that_is_never_instantiated_creates_nothing() { + let text = "(component + (component $unused + (core module $m (memory 1)) + (core instance (instantiate $m))) + (core module $m (memory 1)) + (core instance (instantiate $m)))"; + + validate(&wat::parse_str(text).unwrap()).unwrap(); + } + + #[test] + fn too_many_component_instances_are_rejected() { + let instantiate = "(instance (instantiate $inner))".repeat(MAX_COMPONENT_INSTANCES + 1); + let text = format!("(component (component $inner) {instantiate})"); let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); - assert!(error.contains("component instantiations"), "unexpected error: {error}"); assert!( - error.contains(&format!("the limit is {MAX_COMPONENT_INSTANCES}")), + error.contains(&format!("creates {} component instances", MAX_COMPONENT_INSTANCES + 1)), "unexpected error: {error}" ); } #[test] - fn defined_memories_are_counted_across_core_modules() { - let text = "(component (core module (memory 1)) (core module (memory 1)))"; + fn instantiated_memories_are_counted_across_core_modules() { + let text = "(component + (core module $a (memory 1)) + (core module $b (memory 1)) + (core instance (instantiate $a)) + (core instance (instantiate $b)))"; let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); - assert!(error.contains("2 defined memories"), "unexpected error: {error}"); assert!( - error.contains(&format!("the limit is {MAX_DEFINED_MEMORIES}")), + error.contains("creates 2 linear memories when it is instantiated"), + "unexpected error: {error}" + ); + assert!( + error.contains(&format!("the limit is {MAX_INSTANTIATED_MEMORIES}")), "unexpected error: {error}" ); } + #[test] + fn one_module_instantiated_twice_is_counted_twice() { + // The budgets count what instantiation creates, not what the tree declares, so one + // memory-defining module reaches the memory budget when it is instantiated twice. + let text = r#"(component + (core module $m (memory 1) (func (export "f"))) + (core instance $a (instantiate $m)) + (core instance $b (instantiate $m)))"#; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!( + error.contains("creates 2 linear memories when it is instantiated"), + "unexpected error: {error}" + ); + } + + #[test] + fn a_core_module_the_walk_cannot_read_is_not_instantiable() { + let text = r#"(component + (import "m" (core module $m)) + (core instance (instantiate $m)))"#; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!(error.contains("which the policy cannot read"), "unexpected error: {error}"); + } + + #[test] + fn component_items_of_one_kind_are_capped_across_sections() { + // Component sections repeat, so the cap counts one kind over the whole tree. A core + // module between the two type sections keeps them apart in the encoding. + let types = |count: usize| "(type u8)".repeat(count); + let text = format!( + "(component {first} (core module) {second})", + first = types(MAX_COMPONENT_SECTION_ITEMS), + second = types(1), + ); + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!( + error.contains(&format!("{} component types", MAX_COMPONENT_SECTION_ITEMS + 1)), + "unexpected error: {error}" + ); + assert!( + error.contains(&format!("the limit is {MAX_COMPONENT_SECTION_ITEMS}")), + "unexpected error: {error}" + ); + } + + #[test] + fn too_many_module_types_are_rejected() { + let types = "(type (func (param i32)))".repeat(MAX_MODULE_TYPES + 1); + let error = validate(&component(&types)).unwrap_err().to_string(); + + assert!( + error.contains(&format!("{} types", MAX_MODULE_TYPES + 1)), + "unexpected error: {error}" + ); + } + + #[test] + fn every_structural_rejection_carries_a_class() { + let error = validate(b"not a component").unwrap_err(); + + assert_eq!(error.codec_failure(), Some(crate::CodecFailure::LimitExceeded)); + } + #[test] fn component_start_functions_are_rejected() { let text = r#"(component diff --git a/sdk/note-schema/src/error.rs b/sdk/note-schema/src/error.rs index d2644b3374..d134986fdb 100644 --- a/sdk/note-schema/src/error.rs +++ b/sdk/note-schema/src/error.rs @@ -4,16 +4,17 @@ use core::fmt; /// Why a bundled codec did not return a value. /// -/// Only the bundled-codec adapter reports a class. It covers the whole life of a codec call: -/// the load of the component, the instantiation that precedes the call, the call itself, and -/// the host caps applied to what the call returned. +/// The class covers the whole life of a codec call: the structural load policy, the compilation +/// of the component, the instantiation that precedes the call, the call itself, and the host +/// caps applied to what the call returned. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CodecFailure { /// The call used its whole fuel budget. OutOfFuel, - /// A limit the host applies was exceeded, in the guest or in the returned value. + /// The structural load policy rejected the component, or a host limit was exceeded in the + /// guest or in the returned value. LimitExceeded, - /// The component trapped, or the engine rejected the call. + /// The component trapped, or the engine rejected the component or the call. Trapped, /// The codec returned its own rejection message. Rejected, @@ -35,10 +36,9 @@ impl Error { } } - /// Creates an error that reports how a bundled codec call failed. + /// Creates an error that reports how a bundled codec failed. /// - /// Only the bundled codec adapter reports a failure class. - #[cfg(feature = "codec-component")] + /// The structural load policy and the bundled codec adapter report a failure class. pub(crate) fn codec(kind: CodecFailure, message: impl Into) -> Self { Self { message: message.into(), @@ -46,10 +46,11 @@ impl Error { } } - /// Returns the failure class of a bundled codec call. + /// Returns the failure class of a bundled codec failure. /// - /// Errors from other sources return `None`. Without the `codec-component` feature there is - /// no bundled-codec adapter, so every error returns `None`. + /// The structural load policy classifies every rejection it reports, and the bundled codec + /// adapter classifies every compilation, instantiation, call, and host cap failure. Errors + /// from other sources, such as a schema that does not parse, return `None`. pub fn codec_failure(&self) -> Option { self.codec_failure } diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs index dbf6b0128d..6f25057625 100644 --- a/sdk/note-schema/src/schema.rs +++ b/sdk/note-schema/src/schema.rs @@ -113,6 +113,9 @@ impl FeltLayout { } /// Creates a layout within the protocol note-storage width. + /// + /// Every composed layout passes through this function, so no resolved type, and no resolved + /// root, is wider than the protocol allows. fn bounded(minimum: usize, maximum: usize) -> Result { if maximum > MAX_NOTE_STORAGE_SCHEMA_FELTS { return Err(Error::new(format!( @@ -301,8 +304,6 @@ impl NoteStorageSchema { kind_name(root.kind()) ))); } - ensure_root_layout_limit(root.layout())?; - let schema = Self { wit_text: wit_text.to_owned(), root, @@ -390,18 +391,6 @@ fn ensure_schema_byte_limit(byte_len: usize) -> Result<()> { Ok(()) } -/// Enforces the protocol storage-width limit on the resolved root. -fn ensure_root_layout_limit(layout: FeltLayout) -> Result<()> { - if layout.maximum() > MAX_NOTE_STORAGE_SCHEMA_FELTS { - return Err(Error::new(format!( - "note storage schema root has maximum width {} felts; the protocol limit is \ - {MAX_NOTE_STORAGE_SCHEMA_FELTS}", - layout.maximum() - ))); - } - Ok(()) -} - /// Enforces the expanded-tree budget on one resolved schema type. /// /// The builder memoizes shared types, so resolution stays linear. Every consumer of the model @@ -643,7 +632,12 @@ fn collect_custom_type_fqns( /// One memoized schema node, its maximum depth, and the size of its expanded subtree. #[derive(Clone)] struct MemoizedSchemaType { + /// The resolved node. ty: Arc, + /// Levels of nesting below this node. + /// + /// A memoized node is reused at a deeper position than the one it was resolved at, so the + /// depth limit is checked again on every reuse with this value added to the new depth. maximum_subtree_depth: usize, /// Number of nodes a structural walk visits below and including this node. expanded_nodes: usize, @@ -651,8 +645,11 @@ struct MemoizedSchemaType { /// Builds a memoized schema graph from a resolved WIT graph. struct ModelBuilder<'a> { + /// The resolved WIT document the schema comes from. resolve: &'a Resolve, + /// The types the walk is inside, which reports a recursive type. active: HashSet, + /// The nodes already resolved, keyed by WIT type. memo: HashMap, } diff --git a/sdk/wasm-metadata/src/lib.rs b/sdk/wasm-metadata/src/lib.rs index 5a2fefe66f..cba8795685 100644 --- a/sdk/wasm-metadata/src/lib.rs +++ b/sdk/wasm-metadata/src/lib.rs @@ -90,7 +90,9 @@ pub fn trim_trailing_nuls(bytes: &[u8]) -> &[u8] { /// The compiler publishes compiled dependency packages — and its recorded dependency /// resolution — into the directory named by [`package_cache::PACKAGE_CACHE_ENV`]; the SDK /// macros and the build-script support crate consume them. Every spelling of that contract lives -/// here so the producer and the consumers cannot drift apart. +/// here so the producer and the consumers cannot drift apart. The one exception is +/// `miden-sdk-build-script-support`, which spells the variable name inline because it carries no +/// dependencies. pub mod package_cache { use alloc::{format, string::String}; From 6f66addd062f231e575cc723b2195ad878ad6700 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 8 Sep 2026 19:45:57 +0300 Subject: [PATCH 40/43] fix: keep the note impls on a schema error and serialize every package-size build A schema error in `#[note]` dropped every generated impl with the schema static, so a real note crate reported trait-bound errors from its `#[note] impl` block next to the schema diagnostic. The error now replaces the schema static only; the felt-repr impls, the `ActiveNote` impl, and the uniqueness guard are still emitted, and a rustc-driven test with a note-script body proves the diagnostic stands alone. Two of the four example builds in the package-size test bypassed the shared build lock that the network suite holds for the same examples. All four now build through the locking helper. The remaining hand-rolled nested-cargo environment scrubs in the integration tests use the shared helper. The codec WIT crate becomes an optional dependency of the compiler behind its `std` feature, and the script-arguments crate gains the per-crate cargo config its siblings carry so the per-member check targets Wasm. --- midenc-compile/Cargo.toml | 3 +- sdk/base-macros/src/export_type.rs | 2 + sdk/base-macros/src/note.rs | 105 ++++++++++++++++-- sdk/tx-script-args/.cargo/config.toml | 3 + .../examples/basic_wallet_package_sizes.rs | 47 ++------ tests/integration/src/sdk/build_script.rs | 18 +-- tests/integration/src/sdk/macros.rs | 11 +- tests/integration/src/sdk/mod.rs | 9 +- 8 files changed, 135 insertions(+), 63 deletions(-) create mode 100644 sdk/tx-script-args/.cargo/config.toml diff --git a/midenc-compile/Cargo.toml b/midenc-compile/Cargo.toml index a546ed0eaa..60f8a6e91c 100644 --- a/midenc-compile/Cargo.toml +++ b/midenc-compile/Cargo.toml @@ -25,6 +25,7 @@ std = [ "midenc-session/std", "dep:cargo_metadata", "dep:clap", + "dep:miden-note-codec-wit", "dep:miden-note-schema", "dep:sha2", "dep:toml_edit", @@ -41,7 +42,7 @@ midenc-codegen-masm.workspace = true miden-assembly.workspace = true miden-assembly-syntax.workspace = true miden-mast-package.workspace = true -miden-note-codec-wit.workspace = true +miden-note-codec-wit = { workspace = true, optional = true } miden-note-schema = { workspace = true, optional = true } miden-package-registry.workspace = true midenc-frontend-wasm.workspace = true diff --git a/sdk/base-macros/src/export_type.rs b/sdk/base-macros/src/export_type.rs index be7b8c40e7..41f55a7211 100644 --- a/sdk/base-macros/src/export_type.rs +++ b/sdk/base-macros/src/export_type.rs @@ -1,3 +1,5 @@ +//! Type export and identity guard expansion for `#[export_type]`. + use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; diff --git a/sdk/base-macros/src/note.rs b/sdk/base-macros/src/note.rs index bad7322ea1..e40171e888 100644 --- a/sdk/base-macros/src/note.rs +++ b/sdk/base-macros/src/note.rs @@ -1,3 +1,5 @@ +//! Note script and note storage struct expansion for `#[note]`. + use std::collections::BTreeSet; use heck::{ToKebabCase, ToSnakeCase}; @@ -158,17 +160,13 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { (from_impl, quote! {}) } syn::Fields::Named(fields) => { + // A schema error replaces the schema static only. Everything else is computable from + // the struct alone, so the macro still emits it. This keeps the schema diagnostic + // alone: neither the use sites nor the `#[note]` `impl` block add "cannot find type" + // or trait-bound errors on top of it. let schema_static = match expand_note_storage_schema(&item_struct) { Ok(schema_static) => schema_static, - // The struct is emitted with the error so that the schema diagnostic is not - // buried under "cannot find type" errors from every use site. - Err(err) => { - let error = err.into_compile_error(); - return quote! { - #item_struct - #error - }; - } + Err(err) => err.into_compile_error(), }; let field_inits = fields.named.iter().map(|field| { let ident = field.ident.as_ref().expect("named fields must have identifiers"); @@ -1242,6 +1240,70 @@ mod tests { ); } + /// Stand-in for the SDK items that a `#[note]` expansion references. + /// + /// `compile_rust_source` runs a bare `rustc` without external crates, so the compiled source + /// declares the SDK surface itself. + const MIDEN_SDK_STUB: &str = r#" +extern crate self as miden; + +#[derive(Debug)] +pub struct Felt; + +pub mod felt_repr { + use super::Felt; + + #[derive(Debug)] + pub struct FeltReprError; + + pub struct FeltReader<'a>(#[allow(dead_code)] &'a [Felt]); + + impl<'a> FeltReader<'a> { + pub fn new(felts: &'a [Felt]) -> Self { + Self(felts) + } + + pub fn ensure_eof(&self) -> Result<(), FeltReprError> { + Ok(()) + } + } + + pub struct FeltWriter<'a>(#[allow(dead_code)] &'a mut Vec); + + pub trait FromFeltRepr: Sized { + fn from_felt_repr(reader: &mut FeltReader<'_>) -> Result; + } + + pub trait ToFeltRepr { + fn write_felt_repr(&self, writer: &mut FeltWriter<'_>); + } + + impl FromFeltRepr for Vec { + fn from_felt_repr(_reader: &mut FeltReader<'_>) -> Result { + Ok(Vec::new()) + } + } + + impl ToFeltRepr for Vec { + fn write_felt_repr(&self, _writer: &mut FeltWriter<'_>) {} + } +} + +pub mod active_note { + use super::Felt; + + pub trait ActiveNote { + fn get_sender(&self) -> Felt { + Felt + } + } + + pub fn get_storage() -> Vec { + Vec::new() + } +} +"#; + #[test] fn schema_failure_reports_only_the_schema_diagnostic() { let _registry_guard = lock_export_type_registry_for_tests(); @@ -1252,11 +1314,26 @@ mod tests { } }; let expansion = expand_note_struct(item_struct); + // The whole `#[note]` `impl` expansion cannot compile outside a real SDK crate, because it + // calls the `miden::generate!` and `bindings::export!` proc macros. The note-script body + // below is built with the same generator that the `impl` expansion uses, so the decoding + // and `ActiveNote` use sites are the ones a real note crate gets. + let note_ty: syn::TypePath = parse_quote!(VecNote); + let note_init = note_instantiation(¬e_ty); let source = format!( r#" +{MIDEN_SDK_STUB} mod user {{ + use ::miden::active_note::ActiveNote as _; + {expansion} + pub fn takes_note(_note: VecNote) {{}} + + pub fn note_script() {{ + {note_init} + let _ = __miden_note.get_sender(); + }} }} fn main() {{}} "# @@ -1273,6 +1350,16 @@ fn main() {{}} assert!( !stderr.contains("cannot find type"), "the schema diagnostic must not cascade into missing-type errors: +{stderr}" + ); + assert!( + !stderr.contains("the trait bound"), + "the schema diagnostic must not cascade into trait-bound errors: +{stderr}" + ); + assert!( + !stderr.contains("is not implemented"), + "the schema diagnostic must not cascade into missing-impl errors: {stderr}" ); } diff --git a/sdk/tx-script-args/.cargo/config.toml b/sdk/tx-script-args/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/tx-script-args/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs b/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs index 4f6f5bcc74..5a72d4695d 100644 --- a/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs +++ b/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs @@ -1,47 +1,21 @@ -use midenc_expect_test::expect; -use midenc_frontend_wasm::WasmTranslationConfig; -use midenc_integration_test_support::{example_build_lock, workspace_root}; +use std::path::Path; -use crate::{CompilerTest, testing::stripped_mast_size_str}; +use midenc_expect_test::expect; +use midenc_integration_test_support::compile_project; -fn no_debug_flags() -> [String; 2] { - ["--debug".to_string(), "none".to_string()] -} +use crate::testing::stripped_mast_size_str; #[test] fn basic_wallet_and_p2id() { - let config = WasmTranslationConfig::default(); - let workspace = workspace_root(); - let account_package = { - let _build_lock = example_build_lock(&workspace); - let mut account_test = CompilerTest::rust_source_cargo_miden( - "../../examples/basic-wallet", - config.clone(), - no_debug_flags(), - ); - account_test.compile_package() - }; + let account_package = compile_project(Path::new("../../examples/basic-wallet")); assert!(account_package.is_library(), "expected library"); expect!["8505"].assert_eq(stripped_mast_size_str(&account_package).as_str()); - let mut tx_script_test = CompilerTest::rust_source_cargo_miden( - "../../examples/basic-wallet-tx-script", - config.clone(), - no_debug_flags(), - ); - let tx_script_package = tx_script_test.compile_package(); + let tx_script_package = compile_project(Path::new("../../examples/basic-wallet-tx-script")); assert!(tx_script_package.is_library(), "expected library"); expect!["13784"].assert_eq(stripped_mast_size_str(&tx_script_package).as_str()); - let note_package = { - let _build_lock = example_build_lock(&workspace); - let mut p2id_test = CompilerTest::rust_source_cargo_miden( - "../../examples/p2id-note", - config.clone(), - no_debug_flags(), - ); - p2id_test.compile_package() - }; + let note_package = compile_project(Path::new("../../examples/p2id-note")); assert!(note_package.is_library(), "expected library"); expect!["21797"].assert_eq(stripped_mast_size_str(¬e_package).as_str()); // The note package exports both the note script and the `build-recipient` constructor; the @@ -53,12 +27,7 @@ fn basic_wallet_and_p2id() { miden_protocol::note::NoteScript::from_package(¬e_package) .expect("expected the p2id note package to contain exactly one note script export"); - let mut p2ide_test = CompilerTest::rust_source_cargo_miden( - "../../examples/p2ide-note", - config, - no_debug_flags(), - ); - let p2ide_package = p2ide_test.compile_package(); + let p2ide_package = compile_project(Path::new("../../examples/p2ide-note")); assert!(p2ide_package.is_library(), "expected library"); expect!["16436"].assert_eq(stripped_mast_size_str(&p2ide_package).as_str()); } diff --git a/tests/integration/src/sdk/build_script.rs b/tests/integration/src/sdk/build_script.rs index 85409ac0d2..f4d251d268 100644 --- a/tests/integration/src/sdk/build_script.rs +++ b/tests/integration/src/sdk/build_script.rs @@ -19,6 +19,8 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use midenc_integration_test_support::scrub_nested_cargo_env; + use super::basic_wallet_swapp_note_project; use crate::cargo_proj::project; @@ -179,21 +181,23 @@ fn main() { /// concurrent tests reuse the fixture package names, so a shared cache would /// let one test observe another's generations. fn plain_cargo_check(consumer: &Path) -> Output { - Command::new("cargo") + let mut command = Command::new("cargo"); + scrub_nested_cargo_env(&mut command); + command .arg("check") .env("CARGO_MIDEN", cargo_miden_binary()) .env_remove("MIDENC_PACKAGE_CACHE") .env_remove("CARGO_TARGET_DIR") .env_remove("CARGO_BUILD_BUILD_DIR") - .env_remove("RUSTFLAGS") - .env_remove("CARGO_ENCODED_RUSTFLAGS") .current_dir(consumer) .output() .expect("failed to spawn cargo check") } fn counted_plain_cargo_check(consumer: &Path, counter: &Path) -> Output { - Command::new("cargo") + let mut command = Command::new("cargo"); + scrub_nested_cargo_env(&mut command); + command .arg("check") .env("CARGO_MIDEN", counting_cargo_miden_binary()) .env("MIDENC_TEST_REAL_CARGO_MIDEN", cargo_miden_binary()) @@ -201,8 +205,6 @@ fn counted_plain_cargo_check(consumer: &Path, counter: &Path) -> Output { .env_remove("MIDENC_PACKAGE_CACHE") .env_remove("CARGO_TARGET_DIR") .env_remove("CARGO_BUILD_BUILD_DIR") - .env_remove("RUSTFLAGS") - .env_remove("CARGO_ENCODED_RUSTFLAGS") .current_dir(consumer) .output() .expect("failed to spawn counted cargo check") @@ -686,7 +688,9 @@ path = "src/lib.rs" .file("src/lib.rs", "") .build(); - let output = std::process::Command::new("cargo") + let mut command = Command::new("cargo"); + scrub_nested_cargo_env(&mut command); + let output = command .arg("check") .env("CARGO_MIDEN", project.root().join("definitely-missing-cargo-miden")) .env_remove("MIDENC_PACKAGE_CACHE") diff --git a/tests/integration/src/sdk/macros.rs b/tests/integration/src/sdk/macros.rs index 55d990ea31..d6ebbf9816 100644 --- a/tests/integration/src/sdk/macros.rs +++ b/tests/integration/src/sdk/macros.rs @@ -1,16 +1,19 @@ use std::panic::{self, AssertUnwindSafe}; +use midenc_integration_test_support::scrub_nested_cargo_env; + use super::*; fn cargo_check_miden_target(project: &crate::cargo_proj::Project) -> std::process::Output { - std::process::Command::new("cargo") + let mut command = std::process::Command::new("cargo"); + // Scrub first: the helper also clears `RUSTFLAGS`, which this build sets below. Cargo prefers + // the encoded variable, so an inherited value would silently replace those flags. + scrub_nested_cargo_env(&mut command); + command .arg("check") .arg("--target") .arg("wasm32-wasip2") .env("RUSTFLAGS", "--cfg miden -C target-feature=+bulk-memory,+wide-arithmetic") - // Cargo prefers the encoded variable; an inherited value would silently replace the - // `RUSTFLAGS` set above. - .env_remove("CARGO_ENCODED_RUSTFLAGS") // The macros read dependency packages only from this directory, the way a driven build // or the contract build script exposes it. .env( diff --git a/tests/integration/src/sdk/mod.rs b/tests/integration/src/sdk/mod.rs index 73d57552eb..5975a89f3d 100644 --- a/tests/integration/src/sdk/mod.rs +++ b/tests/integration/src/sdk/mod.rs @@ -5,7 +5,7 @@ use miden_core::serde::Serializable; use miden_mast_package::{Package, PackageExport, ProcedureExport, QualifiedProcedureName}; use miden_protocol::note::NoteScript; use midenc_frontend_wasm::WasmTranslationConfig; -use midenc_integration_test_support::write_masp_file_atomic; +use midenc_integration_test_support::{scrub_nested_cargo_env, write_masp_file_atomic}; use crate::{ CompilerTest, CompilerTestBuilder, @@ -355,7 +355,11 @@ fn build_consumer_wat_with_package_cache( cargo_target_dir: &Path, package_cache_dir: &Path, ) -> String { - let output = std::process::Command::new("cargo") + let mut command = std::process::Command::new("cargo"); + // Scrub first: the helper also clears `RUSTFLAGS`, which this build sets below. Cargo prefers + // the encoded variable, so an inherited value would silently replace those flags. + scrub_nested_cargo_env(&mut command); + let output = command .args(["build", "--release", "--locked", "--manifest-path"]) .arg(consumer.join("Cargo.toml")) .env("CARGO_TARGET_DIR", cargo_target_dir) @@ -364,7 +368,6 @@ fn build_consumer_wat_with_package_cache( package_cache_dir, ) .env("RUSTFLAGS", "--cfg miden -C target-feature=+bulk-memory,+wide-arithmetic") - .env_remove("CARGO_ENCODED_RUSTFLAGS") .current_dir(consumer) .output() .expect("failed to spawn Cargo for the option_env isolation fixture"); From a31e09c94a9655a20d296a53060835efd3bff3c2 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Tue, 8 Sep 2026 19:45:57 +0300 Subject: [PATCH 41/43] docs: correct the note field-type guidance and record the compiler-side changes The template skill claimed a custom field type only needs the felt-repr derive and forbade `Asset` and `Word` as note fields. A custom type must carry `#[export_type]` and be declared before the `#[note]` struct, and the SDK core records that implement the felt-repr traits are accepted. The guidance now lists the supported surface, and the embedded template bundle is regenerated. The compiler changelog gains the `--locked` and `--offline` flags, the post-assembly note codec build, and the note storage schema section. The one-`#[note]`-per-crate rule is stated as one per linked artifact in the migration guide, the SDK changelog, and the macro documentation, since a note crate cannot depend on another note crate. --- CHANGELOG.md | 11 +++++++++++ .../.claude/skills/rust-sdk-patterns/SKILL.md | 2 +- sdk/CHANGELOG.md | 5 +++-- sdk/base-macros/src/lib.rs | 5 +++-- sdk/sdk/MIGRATION.md | 9 +++++---- tools/cargo-miden/templates.tar.gz | Bin 74654 -> 74709 bytes 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ad00aac2..cc96461969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Compiler and `midenc` + +- Added `--locked` and `--offline`, which forward the matching Cargo flags to Rust builds. Use them + to keep `Cargo.lock` unchanged and to build without network access. +- Note packages built from a named-field `#[note]` struct now carry a `note_storage_schema` section. + The section holds the WIT document that describes the note's storage layout, so a host can decode + the note storage without the note's source. +- A note project can declare an author codec crate with `[package.metadata.midenc.note-codec]`. The + compiler builds that crate to a Wasm component after assembly and attaches it to the note package, + which gives hosts typed parsing and display for the note's storage types. + ### Migration and breaking changes - BREAKING: `#[export_type]` now rejects conflicting registrations for the same WIT type and diff --git a/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md b/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md index acf444b2d1..ae4797d53f 100644 --- a/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md +++ b/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md @@ -139,7 +139,7 @@ A note script reads from `active_note::*` and forwards work to a public account- The `#[note]` macro generates `TryFrom<&[Felt]>` for the note struct, so the note's serialized storage is deserialized into typed fields before the script runs. The `#[note_script]` method receives the deserialized note as `self` (by value) and never indexes a raw Felt slice manually. Alongside the required `Word` arg, the method may optionally accept a `&Account` or `&mut Account` parameter. See [compiler/sdk/base-macros/src/lib.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/lib.rs) for the macro contract and [compiler/sdk/base-macros/src/note.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/note.rs) for the generated deserialization (each named field is read via `::from_felt_repr(...)` and EOF is asserted at the end). -Supported field types include `Felt`, the unsigned integer scalars (`u64`, `u32`, `u8`), `bool`, and `Option` over a supported type via the `FromFeltRepr` trait (`compiler/sdk/field-repr/repr/src/lib.rs`), plus any user type that opts in with `#[derive(FromFeltRepr)]` (this is how `AccountId` supports the macro - see `compiler/sdk/base-sys/src/bindings/types.rs`). `Vec` is not supported: the note storage schema needs a stable, named position for each stored value, so a dynamic vector is a hard error. Two more rules follow from the schema: a `#[note]` struct must have named fields or be a unit struct (tuple structs are rejected), and a crate may contain only one `#[note]` struct (move each extra note struct into its own crate). Do **not** use `Asset` or `Word` directly as note struct fields; those types do not currently derive `FromFeltRepr`. If you need asset-shaped data inside the note, flatten it into supported scalar fields and reconstruct inside the script, or keep it on the side as a separate `active_note::get_assets()` read. +The supported field types are `Felt`, `Word`, the SDK core-type records that implement the felt-representation traits (`AccountId`, `Asset`, `Recipient`, `Tag`, `NoteType` - see `compiler/sdk/base-sys/src/bindings/types.rs`), the unsigned integer scalars (`u64`, `u32`, `u8`), `bool`, `Option` over any of those, and your own records or enums that are marked `#[export_type]` and derive both felt-representation traits with `#[derive(FromFeltRepr, ToFeltRepr)]`. A custom field type must be declared **before** the `#[note]` struct; otherwise the macro reports that the type "needs #[export_type] on its definition before the #[note] struct". See [compiler/examples/dex-note/src/lib.rs](https://github.com/0xMiden/compiler/blob/main/examples/dex-note/src/lib.rs) for a custom `LimitPrice` field type in the required form. `Vec` is not supported: the note storage schema needs a stable, named position for each stored value, so a dynamic vector is a hard error. Two more rules follow from the schema: a `#[note]` struct must have named fields or be a unit struct (tuple structs are rejected), and a crate may contain only one `#[note]` struct (move each extra note struct into its own crate). For Cargo.toml wiring (cross-component dependencies + bindings import), see "Cross-Component Dependencies" above. See [increment-note/src/lib.rs](../../../contracts/increment-note/src/lib.rs) for the project-template's local example of the `#[note] struct + #[note] impl` macro form. diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index ceb7de2720..d708342b58 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -59,8 +59,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and note storage fields no longer accept `Vec`. Follow the [migration guidance](./sdk/MIGRATION.md#rewrite-tuple-note-and-vec-storage-layouts) to preserve field order with named fields and replace dynamic vectors with a fixed schema. (#1307) -- A crate can now contain only one `#[note]` struct. The linker rejects a second struct because - both structs define the same note storage schema uniqueness guard symbol. Follow the +- There can now be only one `#[note]` struct per linked artifact, so a note crate cannot depend on + another note crate. The linker rejects a second struct because both structs define the same note + storage schema uniqueness guard symbol. Follow the [migration guidance](./sdk/MIGRATION.md#keep-one-note-struct-in-each-crate) to move each extra note struct into its own crate. (#1307) diff --git a/sdk/base-macros/src/lib.rs b/sdk/base-macros/src/lib.rs index e0fc430acc..7225a806f5 100644 --- a/sdk/base-macros/src/lib.rs +++ b/sdk/base-macros/src/lib.rs @@ -318,8 +318,9 @@ pub fn export_type( /// definition before the `#[note]` struct. Each field must use the exact registered /// `#[export_type]` Rust type. A different type with the same name fails the hidden shape check. /// -/// A crate can contain only one `#[note]` struct. A second struct fails at link time because it -/// defines the duplicate `__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD` symbol. +/// There can be only one `#[note]` struct per linked artifact, so a note crate cannot depend on +/// another note crate. A second struct fails at link time because it defines the duplicate +/// `__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD` symbol. /// /// # Foreign Procedure Invocation (FPI) /// diff --git a/sdk/sdk/MIGRATION.md b/sdk/sdk/MIGRATION.md index 649f24b9b0..5c1734ac69 100644 --- a/sdk/sdk/MIGRATION.md +++ b/sdk/sdk/MIGRATION.md @@ -161,12 +161,13 @@ the staged package cache. Use per-checkout target directories for such layouts. ### Keep one `#[note]` struct in each crate -A crate can now contain only one `#[note]` struct. Two note structs compiled before this change. -Now the linker rejects the second struct because both structs define the -`__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD` symbol. +There can now be only one `#[note]` struct per linked artifact. Two note structs compiled before +this change. Now the linker rejects the second struct because both structs define the +`__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD` symbol. The guard covers the whole link, so a note +crate cannot depend on another note crate either. Keep one note struct in the current crate. Move each extra note struct and its implementation into -a separate note crate. +a separate note crate, and do not depend on that crate from a note crate. ### Rewrite tuple-note and `Vec` storage layouts diff --git a/tools/cargo-miden/templates.tar.gz b/tools/cargo-miden/templates.tar.gz index 1a48063f3e4aea7c3b729d32c4d0be60d13113ab..ca921b5fa5b4e97d81ffe307e90c662561e29c6b 100644 GIT binary patch delta 39989 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 Date: Wed, 9 Sep 2026 14:55:49 +0300 Subject: [PATCH 42/43] fix: build and attach the note codec only for the root note target The codec build ran for every assembled note target, including a note pulled in as a source dependency of another project. That cost a nested cargo build on every dependency assembly, and a dependency copy kept in a package store could carry a stale codec, because the codec crate's inputs are outside the build provenance the store keys on. The codec is now built and attached only when the note is the root target of the build; the declaration checks still run for every role. A consumer that needs the codec loads the note's own package, and the package post-processor that will take over the codec build behaves the same way. A cargo-miden test builds a scratch project that depends on the dex-note example and checks the exported dependency package carries the schema section and no codec section. --- midenc-compile/src/pipeline/assembly.rs | 36 +++- midenc-compile/src/pipeline/backend.rs | 1 + midenc-compile/src/pipeline/frontends/hir.rs | 1 + midenc-compile/src/pipeline/frontends/rust.rs | 1 + midenc-compile/src/pipeline/frontends/wasm.rs | 1 + midenc-compile/src/pipeline/seed.rs | 2 + sdk/note-codec/src/lib.rs | 5 + .../tests/dex_note_dependency_build.rs | 169 ++++++++++++++++++ tools/cargo-miden/tests/mod.rs | 1 + 9 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 tools/cargo-miden/tests/dex_note_dependency_build.rs diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index f2fbfe20db..544ee6a510 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -64,11 +64,15 @@ pub(crate) fn prepare_assembler( } /// Attaches frontend metadata, advice-map data, and target-specific sections after assembly. +/// +/// The author codec is attached only when the note is the root target of the build, as +/// [`attaches_note_codec`] decides. Every other section is attached for every role. pub(crate) fn post_process_package( package: &mut Package, component: &MasmComponent, sections: &midenc_frontend_wasm_metadata::PackageSections, context: &TargetAssemblyContext<'_>, + role: crate::pipeline::observer::TargetRole, ) -> Result<(), Report> { use miden_assembly::serde::Serializable; use miden_mast_package::{Section, SectionId}; @@ -103,7 +107,7 @@ pub(crate) fn post_process_package( .push(Section::new(SectionId::KERNEL, kernel_package.to_bytes())); } - if has_note_codec && context.target.ty == TargetType::Note { + if attaches_note_codec(has_note_codec, context.target.ty, role) { // Run after schema and kernel attachment. The codec stages this package state and hashes it. attach_note_codec(package, context)?; } @@ -111,6 +115,25 @@ pub(crate) fn post_process_package( Ok(()) } +/// Returns true when the assembled package must carry an author codec section. +/// +/// The codec is built only for a note that is the root target of the build. A note that is a +/// source dependency of another project gets no codec section, for two reasons. The codec +/// crate's inputs are outside the build provenance, so a dependency copy that is kept in a +/// package store can carry a stale codec after the codec crate changes. And every dependency +/// assembly would otherwise run a nested cargo build. A consumer that needs the codec loads the +/// note's own package. This is also the behavior of the package post-processor that will take +/// over the codec build, because a post-processor never runs on a dependency. +fn attaches_note_codec( + has_note_codec: bool, + target_type: midenc_session::miden_project::TargetType, + role: crate::pipeline::observer::TargetRole, +) -> bool { + use midenc_session::miden_project::TargetType; + + has_note_codec && target_type == TargetType::Note && role.is_root() +} + /// Validates the target and schema required by an author codec declaration. fn validate_note_codec_declaration( has_note_codec: bool, @@ -331,4 +354,15 @@ interface note-storage { validate_note_codec_declaration(false, false, TargetType::Library, false, "library") .unwrap(); } + + #[test] + fn codec_attaches_only_to_a_root_note_that_declares_one() { + use crate::pipeline::observer::TargetRole; + + assert!(attaches_note_codec(true, TargetType::Note, TargetRole::Root)); + assert!(!attaches_note_codec(true, TargetType::Note, TargetRole::Dependency)); + assert!(!attaches_note_codec(true, TargetType::Note, TargetRole::RequiredLibrary)); + assert!(!attaches_note_codec(true, TargetType::Library, TargetRole::Root)); + assert!(!attaches_note_codec(false, TargetType::Note, TargetRole::Root)); + } } diff --git a/midenc-compile/src/pipeline/backend.rs b/midenc-compile/src/pipeline/backend.rs index e22a5c123e..1e944859a1 100644 --- a/midenc-compile/src/pipeline/backend.rs +++ b/midenc-compile/src/pipeline/backend.rs @@ -832,6 +832,7 @@ mod tests { &found.component, &found.sections, cx.assembly(), + cx.role(), ) } diff --git a/midenc-compile/src/pipeline/frontends/hir.rs b/midenc-compile/src/pipeline/frontends/hir.rs index 0b62e5a1ad..8dacbd3b1b 100644 --- a/midenc-compile/src/pipeline/frontends/hir.rs +++ b/midenc-compile/src/pipeline/frontends/hir.rs @@ -394,6 +394,7 @@ impl Frontend for HirFrontend { &found.component, &found.sections, cx.assembly(), + cx.role(), ) } diff --git a/midenc-compile/src/pipeline/frontends/rust.rs b/midenc-compile/src/pipeline/frontends/rust.rs index 0b6f7635b0..201b66149f 100644 --- a/midenc-compile/src/pipeline/frontends/rust.rs +++ b/midenc-compile/src/pipeline/frontends/rust.rs @@ -1653,6 +1653,7 @@ impl Frontend for RustProjectFrontend { &found.component, &found.sections, cx.assembly(), + cx.role(), ) } } diff --git a/midenc-compile/src/pipeline/frontends/wasm.rs b/midenc-compile/src/pipeline/frontends/wasm.rs index 9a5fc48fd7..c2a3218bfb 100644 --- a/midenc-compile/src/pipeline/frontends/wasm.rs +++ b/midenc-compile/src/pipeline/frontends/wasm.rs @@ -592,6 +592,7 @@ impl Frontend for WasmFrontend { &found.component, &found.sections, cx.assembly(), + cx.role(), ) } diff --git a/midenc-compile/src/pipeline/seed.rs b/midenc-compile/src/pipeline/seed.rs index f653af7cd7..00f2e242a5 100644 --- a/midenc-compile/src/pipeline/seed.rs +++ b/midenc-compile/src/pipeline/seed.rs @@ -439,6 +439,7 @@ impl Frontend for SeedFrontend { &found.component, &found.sections, cx.assembly(), + cx.role(), ) } @@ -605,6 +606,7 @@ mod tests { &found.component, &found.sections, cx.assembly(), + cx.role(), ) } diff --git a/sdk/note-codec/src/lib.rs b/sdk/note-codec/src/lib.rs index d796883237..e55fdfb77d 100644 --- a/sdk/note-codec/src/lib.rs +++ b/sdk/note-codec/src/lib.rs @@ -28,6 +28,11 @@ //! [package.metadata.midenc.note-codec] //! crate = "../my-note-codec" //! ``` +//! +//! The codec is embedded only when the note is built as its own project. A note that another +//! project pulls in as a source dependency carries no codec section, because the codec crate's +//! inputs are outside the build provenance, so a stored dependency copy could keep a stale codec +//! after the codec crate changes. Load the note's own package to get the codec. #![deny(missing_docs)] diff --git a/tools/cargo-miden/tests/dex_note_dependency_build.rs b/tools/cargo-miden/tests/dex_note_dependency_build.rs new file mode 100644 index 0000000000..bc99e5d880 --- /dev/null +++ b/tools/cargo-miden/tests/dex_note_dependency_build.rs @@ -0,0 +1,169 @@ +//! Integration test for a note that is built as a dependency of another project. +//! +//! A note carries its author codec only when it is the root target of the build. A note that +//! another project pulls in as a source dependency gets the note storage schema, but no codec +//! section: the codec crate's inputs are outside the build provenance, so a stored dependency +//! copy could keep a stale codec, and every dependency assembly would otherwise run a nested +//! cargo build. + +use std::{env, fs, path::Path}; + +use cargo_miden::run; +use miden_mast_package::Package; +use midenc_frontend_wasm_metadata::{ + package_note_codec_section_id, package_note_storage_schema_section_id, +}; +use midenc_integration_test_support::{ + example_build_lock, wasm_target_is_installed, workspace_root, +}; + +use crate::utils::{RestoreEnvironment, current_dir_lock, with_package_cache_env}; + +#[test] +fn dex_note_built_as_a_dependency_has_the_schema_but_no_codec() { + if !wasm_target_is_installed() { + eprintln!("skipping DEX note dependency build test: wasm32-wasip2 is not installed"); + return; + } + // The command reads the process working directory, so serialize cwd changes. + let _cwd_lock = current_dir_lock(); + let _ = midenc_log::Builder::from_env("MIDENC_TRACE") + .is_test(true) + .format_timestamp(None) + .try_init(); + + // Clear the outer override. The scratch project then uses its own target layout. + let _restore_environment = RestoreEnvironment::new(["CARGO_TARGET_DIR"]); + unsafe { + env::remove_var("CARGO_TARGET_DIR"); + } + + let workspace = workspace_root(); + let project_dir = tempfile::tempdir().expect("failed to create the dependent project dir"); + let dependent = project_dir.path().join("counter-contract"); + scaffold_dependent_project(&workspace, &dependent); + + // A caller-provided package cache is adopted and left in place, so the dependency packages + // this build publishes can be read after the compiler exits. + let cache_dir = tempfile::tempdir().expect("failed to create the package cache dir"); + let export_dir = cache_dir.path().to_path_buf(); + + env::set_current_dir(&dependent).unwrap(); + let result = { + let _build_lock = example_build_lock(&workspace); + with_package_cache_env(&export_dir, || { + run(["cargo", "miden", "build", "--release"].into_iter().map(str::to_owned)) + }) + }; + + let output = result + .expect("cargo miden build for the dex-note dependent failed") + .expect("expected BuildCommandOutput") + .unwrap_build_output(); + assert_eq!(output.len(), 1, "expected one dependent package artifact, got {output:?}"); + assert!(output[0].exists(), "the dependent package was not written to {:?}", output[0]); + + let dependency_package = export_dir.join("dex-note.masp"); + assert!( + dependency_package.exists(), + "expected the dex-note dependency package at {}; the cache holds {:?}", + dependency_package.display(), + exported_file_names(&export_dir) + ); + let package = Package::deserialize_from_file(&dependency_package) + .expect("failed to read the dex-note dependency package"); + + let schema_id = package_note_storage_schema_section_id(); + assert!( + package.sections.iter().any(|section| section.id == schema_id), + "the dex-note dependency package has no note storage schema section" + ); + let codec_id = package_note_codec_section_id(); + assert!( + !package.sections.iter().any(|section| section.id == codec_id), + "the dex-note dependency package must carry no note codec section" + ); +} + +/// Copies the counter-contract example to `destination` and makes it depend on `dex-note`. +/// +/// The copy is built outside the compiler workspace, so every relative path in its manifests is +/// rewritten to the workspace copy it names. +fn scaffold_dependent_project(workspace: &Path, destination: &Path) { + let source = workspace.join("examples/counter-contract"); + copy_project(&source, destination); + + for manifest in ["Cargo.toml", "miden-project.toml"] { + let path = destination.join(manifest); + let rewritten = absolutize_manifest_paths(&fs::read_to_string(&path).unwrap(), &source); + fs::write(&path, rewritten).unwrap(); + } + + let manifest = destination.join("miden-project.toml"); + let contents = fs::read_to_string(&manifest).unwrap(); + let dex_note = workspace.join("examples/dex-note"); + let with_dependency = contents.replace( + "[dependencies]\n", + &format!("[dependencies]\ndex-note = {{ path = \"{}\" }}\n", dex_note.display()), + ); + assert_ne!(with_dependency, contents, "the example manifest has no `[dependencies]` table"); + fs::write(&manifest, with_dependency).unwrap(); +} + +/// Copies a project directory, skipping build outputs. +fn copy_project(source: &Path, destination: &Path) { + fs::create_dir_all(destination).unwrap(); + for entry in fs::read_dir(source).unwrap() { + let entry = entry.unwrap(); + let name = entry.file_name(); + if name == "target" { + continue; + } + let target = destination.join(&name); + if entry.file_type().unwrap().is_dir() { + copy_project(&entry.path(), &target); + } else { + fs::copy(entry.path(), &target).unwrap(); + } + } +} + +/// Rewrites every relative `path = "..."` value to the location it names under `manifest_dir`. +fn absolutize_manifest_paths(manifest: &str, manifest_dir: &Path) -> String { + const KEY: &str = "path = \""; + + let mut out = String::with_capacity(manifest.len()); + let mut rest = manifest; + while let Some(start) = rest.find(KEY) { + let (head, tail) = rest.split_at(start + KEY.len()); + out.push_str(head); + let end = tail.find('"').expect("unterminated path value in the manifest"); + let (value, tail) = tail.split_at(end); + let path = Path::new(value); + if path.is_absolute() { + out.push_str(value); + } else { + let resolved = manifest_dir.join(path); + // `src/lib.rs` and the like must stay relative to the copy, not point back to it. + match resolved.canonicalize() { + Ok(resolved) if !resolved.starts_with(manifest_dir) => { + out.push_str(&resolved.display().to_string()) + } + _ => out.push_str(value), + } + } + rest = tail; + } + out.push_str(rest); + out +} + +/// The file names the build left in the package cache, for assertion messages. +fn exported_file_names(export_dir: &Path) -> Vec { + let Ok(entries) = fs::read_dir(export_dir) else { + return Vec::new(); + }; + entries + .filter_map(|entry| Some(entry.ok()?.file_name().to_string_lossy().into_owned())) + .collect() +} diff --git a/tools/cargo-miden/tests/mod.rs b/tools/cargo-miden/tests/mod.rs index fa5adc5e1d..4b0760f623 100755 --- a/tools/cargo-miden/tests/mod.rs +++ b/tools/cargo-miden/tests/mod.rs @@ -1,4 +1,5 @@ mod dex_note_codec_build; +mod dex_note_dependency_build; mod masm_dependency; mod p2id_cargo_miden_build; mod target_dir; From c8a20e90e78c7e8bba2b97184046d49edcb96e4e Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 9 Sep 2026 14:55:49 +0300 Subject: [PATCH 43/43] docs: drop the hand-written changelog entries The changelogs are generated from the commit messages at release time, so the entries this branch accumulated in the compiler and SDK changelogs are removed and both files match the parent branch again. --- CHANGELOG.md | 17 -------------- sdk/CHANGELOG.md | 58 ------------------------------------------------ 2 files changed, 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc96461969..e8c56704b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,23 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Compiler and `midenc` - -- Added `--locked` and `--offline`, which forward the matching Cargo flags to Rust builds. Use them - to keep `Cargo.lock` unchanged and to build without network access. -- Note packages built from a named-field `#[note]` struct now carry a `note_storage_schema` section. - The section holds the WIT document that describes the note's storage layout, so a host can decode - the note storage without the note's source. -- A note project can declare an author codec crate with `[package.metadata.midenc.note-codec]`. The - compiler builds that crate to a Wasm component after assembly and attaches it to the note package, - which gives hosts typed parsing and display for the note's storage types. - -### Migration and breaking changes - -- BREAKING: `#[export_type]` now rejects conflicting registrations for the same WIT type and - reserves the inherent associated constant name `__MIDEN_EXPORT_TYPE_SHAPE`. See the - [migration guide](./sdk/sdk/MIGRATION.md) for both required source changes. - ## [0.10.0] ### Compiler and `midenc` diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index d708342b58..38828f847a 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -7,64 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added - -- Note codec crates build for the `wasm32-wasip2` target, and rustc links them directly as - Wasm components. The standard library imports WASI interfaces, so a codec component may - import `wasi:*` interfaces only; consumers stub every import as a trap at instantiation, - so no host capability is reachable from codec code. -- Added optional `codec-component` support to the new `miden-note-schema` host crate. It can - load author-defined note codecs from a package without adding Wasmtime to the default feature - set or the guest SDK dependency graph. Consumers run codecs under an explicit Wasm feature - policy, fixed structural caps that the producer also enforces at build time, and host-policy - `CodecLimits`; call failures report a `CodecFailure` class. -- Added the `miden-note-codec` author crate. Its codec-side `from_project!` and `from_package!` - macros generate host types from a note package, `AuthorTypeCodec` defines text conversion and - validation, `#[note_codec]` registers each custom type, and `export_codecs!` exports the - registered codecs as a component. Add this package-level metadata to `miden-project.toml` to - enable the codec build. The `crate` directory is relative to that manifest: - - ```toml - [package.metadata.midenc.note-codec] - crate = "../my-note-codec" - ``` -- Added the dependency-free `miden-note-codec-wit` crate as the canonical source for the note - codec component WIT contract. -- Added typed host note-storage bindings through the new `miden-note-bindings` macros. Bindings - can load a built note project or an exact `.masp`, generate native Rust storage types, and convert - typed values to and from note storage. Its facade supplies all generated runtime dependencies, - and generated string, validation, and display APIs keep stable standard-registry and - caller-provided-registry forms as schemas gain nested types. -- The `FromFeltRepr`/`ToFeltRepr` derives accept an internal `#[felt_repr(crate_path = "...")]` - attribute so macro-generated code can reference the runtime crate through a facade re-export. -- `#[note]` now embeds a WIT storage schema for named-field note structs in the - `note_storage_schema` section of the compiled `.masp`. Schema records preserve Rust doc comments - and can include nested types declared with `#[export_type]` before the note struct. Unit structs - emit no schema. - -### Fixed - -- Note storage schema handling now rejects conflicting `#[export_type]` registrations and local - types that only collide by name with SDK core types, resolves schema types through a bounded, - memoized graph, uses one canonical standard-leaf set across consumers, and caps untrusted author - codec components before compilation and during table allocation. (#1307) -- Note package macros now select artifacts by canonical package identity and support shared Cargo - target directories. The note codec macros support renamed facade dependencies, reject a second - distinct schema in one crate, and report `export_codecs!` calls that appear before all codec - declarations. (#1307) - -### Migration and breaking changes - -- `#[note]` storage types now require named-field or unit structs. Tuple structs no longer compile, - and note storage fields no longer accept `Vec`. Follow the - [migration guidance](./sdk/MIGRATION.md#rewrite-tuple-note-and-vec-storage-layouts) to preserve - field order with named fields and replace dynamic vectors with a fixed schema. (#1307) -- There can now be only one `#[note]` struct per linked artifact, so a note crate cannot depend on - another note crate. The linker rejects a second struct because both structs define the same note - storage schema uniqueness guard symbol. Follow the - [migration guidance](./sdk/MIGRATION.md#keep-one-note-struct-in-each-crate) to move each extra - note struct into its own crate. (#1307) - ## [0.14.0] ### Added