From 594850db1adc5de6f93e92e16d5c96c528ddea29 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Fri, 1 May 2026 09:29:21 +0200 Subject: [PATCH 1/2] chore: DWARF / witness mapping discovery for fused output (#130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit meld currently passes DWARF custom sections through fusion byte-for-byte. DWARF addresses encode byte offsets into the per-input code section, but fusion rewrites function bodies into a new merged code section — so every preserved DWARF address points at the wrong instruction (or out of range). The pulseengine `witness` MC/DC tool reads those addresses via gimli, so coverage attribution for fused modules is currently silently wrong. This change: - Adds `meld-core/tests/dwarf_passthrough.rs` pinning the current lossy-but-present behaviour as five green tests so any future remapping work has a clear oracle to flip. - Documents the witness DWARF contract and the cross-repo integration shape in the test's module docstring (witness intentionally stays out of meld-core's dep graph). No production code changes — defaults are unchanged. Phased plan (Phase 1.5 explicit policy, Phase 2 DWARF remap, Phase 3 adapter DIEs) tracked in #130. Refs #130 Co-Authored-By: Claude Opus 4.7 (1M context) --- meld-core/tests/dwarf_passthrough.rs | 366 +++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 meld-core/tests/dwarf_passthrough.rs diff --git a/meld-core/tests/dwarf_passthrough.rs b/meld-core/tests/dwarf_passthrough.rs new file mode 100644 index 0000000..8ea5354 --- /dev/null +++ b/meld-core/tests/dwarf_passthrough.rs @@ -0,0 +1,366 @@ +//! DWARF-passthrough discovery test (Phase 1 of the witness-mapping epic). +//! +//! # What this test pins down +//! +//! After `meld fuse` rewrites N component-model `.wasm` files into one +//! fused core module, the per-input DWARF custom sections (`.debug_info`, +//! `.debug_line`, `.debug_str`, …) are still byte-addressed against the +//! ORIGINAL per-component code section. Once the merger relocates and +//! re-encodes function bodies into the merged module's code section, +//! every DWARF address inside those passed-through sections points at +//! the wrong instruction — or at no instruction at all. +//! +//! This file is the discovery oracle for the +//! `chore/dwarf-witness-discovery` PR. It does **not** fix the bug; it +//! pins down today's broken-but-not-empty behaviour so that any change +//! (Phase 1.5 passthrough, Phase 2 remap, Phase 3 adapter handling) has +//! a green-to-red signal to compare against. +//! +//! # Findings (verified by this test) +//! +//! 1. The default `CustomSectionHandling` is `Merge`, NOT `Drop`. So +//! DWARF sections from every input core module are emitted into the +//! fused module unchanged — they are present but their offsets are +//! wrong against the new code section. +//! 2. With multiple debug-info-bearing inputs (or one P2 component that +//! embeds multiple core modules with debug info), the fused module +//! contains N duplicate `.debug_info` / `.debug_line` / etc. sections +//! rather than a single merged one. +//! 3. No code in `meld-core/src/` currently parses, rewrites, or +//! reconciles `.debug_*` sections. `merger.rs` (around line 2010) +//! does a naive `Vec::push` per source module: +//! +//! ```ignore +//! for (name, data) in &module.custom_sections { +//! merged.custom_sections.push((name.clone(), data.clone())); +//! } +//! ``` +//! +//! The encoder in `lib.rs::encode_output` then emits them all when +//! `custom_sections != Drop`. +//! +//! # Cross-repo dependency +//! +//! The pulseengine `witness` tool (sibling repo, currently v0.11.x) is +//! the consumer that breaks. It calls `gimli` to build a +//! `(code-section byte offset) -> (file, line)` map and uses that map +//! to attribute MC/DC `br_if` decisions to source lines. After meld +//! fusion every offset is wrong, so witness produces incorrect coverage +//! attribution for fused modules. +//! +//! Witness is intentionally NOT a dependency of meld-core — it lives in +//! its own workspace and depends on `wasmtime`, `walrus`, and `gimli` +//! that we don't want to pull into core fusion. The integration test +//! therefore lives at the cross-repo level (a future scripted check, or +//! a fixture exchanged via the `pulseengine/wasm-component-examples` +//! release pipeline). This in-tree test only covers the meld-side +//! invariants — i.e. "DWARF is passed through, semantics are wrong" — +//! which witness's mapping logic relies on. +//! +//! # When to flip these assertions +//! +//! - **Phase 1.5 (deliberate passthrough policy)**: this test stays +//! green; we just gain a documented config knob. +//! - **Phase 2 (DWARF remap)**: the duplicate-section assertion flips. +//! The fused module should contain a single, merged, address-correct +//! `.debug_info` / `.debug_line` pair. Update the test then. +//! - **Phase 3 (adapter / inlined-code coverage)**: synthetic DIEs +//! covering adapter ranges land. Update the test to assert their +//! presence then. + +use meld_core::{CustomSectionHandling, Fuser, FuserConfig, MemoryStrategy, OutputFormat}; + +/// One of the wit-bindgen integration fixtures known to embed core +/// modules compiled with `debuginfo = 2`. Verified via `wasm-tools +/// objdump` to carry `.debug_abbrev`, `.debug_info`, `.debug_ranges`, +/// `.debug_str`, `.debug_line`, `.debug_loc` inside its embedded core +/// modules at the time of writing. +const DEBUG_INFO_FIXTURE: &str = "../tests/wit_bindgen/fixtures/lists.wasm"; + +/// All DWARF custom-section names meld might encounter from a Rust / +/// Clang component. Mirrors `witness-core::decisions::extract_dwarf_sections`. +const DWARF_SECTION_NAMES: &[&str] = &[ + ".debug_abbrev", + ".debug_info", + ".debug_line", + ".debug_str", + ".debug_line_str", + ".debug_str_offsets", + ".debug_addr", + ".debug_rnglists", + ".debug_loclists", + ".debug_ranges", + ".debug_loc", +]; + +fn fixture_available() -> bool { + if std::path::Path::new(DEBUG_INFO_FIXTURE).is_file() { + true + } else { + eprintln!("skipping: fixture not found at {DEBUG_INFO_FIXTURE}"); + false + } +} + +/// Walk a wasm binary at the **outermost** level and tally DWARF custom +/// sections by name. For a P2 component this only sees component-level +/// custom sections (i.e. zero — DWARF lives inside the embedded core +/// modules). For a fused core module it sees every DWARF section meld +/// emitted. +fn count_dwarf_sections_at_top_level(bytes: &[u8]) -> std::collections::BTreeMap { + let mut counts: std::collections::BTreeMap = std::collections::BTreeMap::new(); + let parser = wasmparser::Parser::new(0); + for payload in parser.parse_all(bytes) { + let payload = match payload { + Ok(p) => p, + Err(_) => break, + }; + if let wasmparser::Payload::CustomSection(reader) = payload { + let name = reader.name(); + if DWARF_SECTION_NAMES.contains(&name) { + *counts.entry(name.to_string()).or_insert(0) += 1; + } + } + } + counts +} + +/// Walk a wasm binary and tally every `.debug_*` custom section. +/// `wasmparser::Parser::parse_all` flattens nested modules into a +/// single payload stream, so this single walk covers component-level +/// sections AND sections inside every embedded core module. Used to +/// confirm the input fixture has DWARF SOMEWHERE before we try to +/// fuse it. +fn count_dwarf_sections_recursive(bytes: &[u8]) -> std::collections::BTreeMap { + let mut counts: std::collections::BTreeMap = std::collections::BTreeMap::new(); + let parser = wasmparser::Parser::new(0); + for payload in parser.parse_all(bytes) { + let payload = match payload { + Ok(p) => p, + Err(_) => break, + }; + if let wasmparser::Payload::CustomSection(reader) = payload { + let name = reader.name(); + if DWARF_SECTION_NAMES.contains(&name) { + *counts.entry(name.to_string()).or_insert(0) += 1; + } + } + } + counts +} + +fn fuse_default(input: &[u8]) -> Vec { + let mut fuser = Fuser::new(FuserConfig { + memory_strategy: MemoryStrategy::MultiMemory, + attestation: false, + address_rebasing: false, + preserve_names: false, + // Default in production is Merge, but we set it explicitly so + // the test reads as a self-contained spec of meld's current + // policy. See `current_default_is_merge_not_drop` below. + custom_sections: CustomSectionHandling::Merge, + output_format: OutputFormat::CoreModule, + opaque_resources: Vec::new(), + }); + fuser + .add_component_named(input, Some("dwarf-fixture")) + .expect("add_component"); + fuser.fuse().expect("fuse") +} + +fn fuse_with_drop(input: &[u8]) -> Vec { + let mut fuser = Fuser::new(FuserConfig { + memory_strategy: MemoryStrategy::MultiMemory, + attestation: false, + address_rebasing: false, + preserve_names: false, + custom_sections: CustomSectionHandling::Drop, + output_format: OutputFormat::CoreModule, + opaque_resources: Vec::new(), + }); + fuser + .add_component_named(input, Some("dwarf-fixture")) + .expect("add_component"); + fuser.fuse().expect("fuse") +} + +#[test] +fn current_default_is_merge_not_drop() { + // Pin: any future change to the default must be a deliberate edit + // to this test, not a silent drift in `FuserConfig::default()`. + assert_eq!( + FuserConfig::default().custom_sections, + CustomSectionHandling::Merge, + "FuserConfig::default().custom_sections drifted from Merge. \ + If this was intentional (Phase 1.5 etc.), update DWARF \ + passthrough docs and the witness-integration discovery issue." + ); +} + +#[test] +fn fixture_carries_dwarf_in_some_embedded_module() { + if !fixture_available() { + return; + } + let bytes = std::fs::read(DEBUG_INFO_FIXTURE).expect("read fixture"); + let counts = count_dwarf_sections_recursive(&bytes); + assert!( + !counts.is_empty(), + "fixture {DEBUG_INFO_FIXTURE} no longer carries any \ + `.debug_*` custom sections — pick a different fixture or \ + rebuild this one with debuginfo = 2" + ); + // `.debug_info` is the most universally present DWARF section; if + // it's missing from a debug-bearing module something is very wrong. + assert!( + counts.contains_key(".debug_info"), + "expected `.debug_info` somewhere in fixture; saw: {counts:?}" + ); +} + +#[test] +fn fused_output_inherits_dwarf_byte_for_byte_today() { + // CURRENT BEHAVIOUR (Phase 1, broken): + // + // - Default `CustomSectionHandling::Merge` causes meld to copy each + // input core module's DWARF sections verbatim into the fused core + // module's custom-section slot. + // - DWARF addresses inside these sections are byte offsets into + // the ORIGINAL per-input code section. The fused module has a + // different code section (rewritten function bodies, different + // layout, adapter functions appended), so every passed-through + // address is meaningless — best case `gimli` resolves it to the + // wrong source line, worst case it falls outside the new code + // section entirely. + // + // This test asserts the lossy-but-present invariant. Flip it once + // Phase 2 (real DWARF remapping) lands. + if !fixture_available() { + return; + } + let bytes = std::fs::read(DEBUG_INFO_FIXTURE).expect("read fixture"); + let fused = fuse_default(&bytes); + + let top_level_dwarf_in_fused = count_dwarf_sections_at_top_level(&fused); + assert!( + !top_level_dwarf_in_fused.is_empty(), + "Phase 1 expectation: meld passes DWARF through verbatim, so \ + the fused core module must carry at least one `.debug_*` \ + section at the module level. Saw none, which means \ + something has changed (likely a default flip to Drop, or a \ + new strip pass). Update this test and the tracking issue." + ); + + // TODO(phase-2): once meld remaps DWARF, the fused module should + // carry exactly one of each `.debug_*` section (deduplicated + + // address-corrected). Until then, multiple inputs / multiple core + // modules produce duplicate sections — flagged here so it's + // visible in test output. + eprintln!( + "DWARF sections at top level of fused module (current — wrong addresses): {top_level_dwarf_in_fused:?}" + ); +} + +#[test] +fn drop_policy_strips_dwarf_completely() { + // The escape hatch a witness/coverage user has TODAY: opt into + // `CustomSectionHandling::Drop` to get a fused module with no + // misleading DWARF. Coverage attribution becomes impossible (the + // entire module reports as un-attributed), but at least the user + // isn't told the WRONG source line. + // + // Phase 1.5 will make this the recommended default until Phase 2 + // ships real remapping. + if !fixture_available() { + return; + } + let bytes = std::fs::read(DEBUG_INFO_FIXTURE).expect("read fixture"); + let fused = fuse_with_drop(&bytes); + + let counts = count_dwarf_sections_at_top_level(&fused); + assert!( + counts.is_empty(), + "Drop policy should strip every `.debug_*` custom section. \ + Saw: {counts:?}" + ); +} + +#[test] +fn dwarf_addresses_in_fused_output_are_known_to_be_wrong() { + // This is the documenting-a-bug test. We don't have witness in + // this workspace, so we can't run gimli end-to-end here. What we + // CAN do is sanity-check the structural invariant that proves + // the addresses are wrong: the input's code section length and + // the fused output's code section length differ. DWARF addresses + // that were valid in the input cannot be valid in the output if + // the code section has a different size and layout — they refer + // to a different range of bytes. + // + // When Phase 2 lands and `.debug_line` etc. are remapped to the + // fused code section, the addresses themselves will be different + // values (still encoding the same source lines). This test will + // then need a different oracle — probably "build line map, walk + // each address, assert it falls inside the fused code section + // range and lands on a function-body byte boundary". + if !fixture_available() { + return; + } + let bytes = std::fs::read(DEBUG_INFO_FIXTURE).expect("read fixture"); + let fused = fuse_default(&bytes); + + let input_code_len = code_section_len_recursive(&bytes); + let fused_code_len = code_section_len(&fused); + + assert!( + input_code_len.is_some(), + "fixture {DEBUG_INFO_FIXTURE} has no code section visible to \ + the recursive walker — pick a different fixture" + ); + assert!( + fused_code_len.is_some(), + "fused module has no code section, fusion must have produced \ + an empty module — fixture or fuser regressed" + ); + let input_code_len = input_code_len.unwrap(); + let fused_code_len = fused_code_len.unwrap(); + + eprintln!( + "input code section (sum across embedded modules): {input_code_len} bytes \ + | fused code section: {fused_code_len} bytes" + ); + + // Pin the structural change. If these ever match exactly, either + // the merger went degenerate (single passthrough — bug) or + // something genuinely deduplicated and that needs a fresh look. + assert_ne!( + input_code_len, fused_code_len, + "code-section length unchanged across fusion — DWARF \ + addresses might be coincidentally valid, but more likely \ + the merger is no longer doing what this test thinks it is" + ); +} + +fn code_section_len(bytes: &[u8]) -> Option { + let parser = wasmparser::Parser::new(0); + for payload in parser.parse_all(bytes) { + let payload = payload.ok()?; + if let wasmparser::Payload::CodeSectionStart { range, .. } = payload { + return Some(range.end - range.start); + } + } + None +} + +fn code_section_len_recursive(bytes: &[u8]) -> Option { + let parser = wasmparser::Parser::new(0); + let mut total = 0usize; + let mut saw_any = false; + for payload in parser.parse_all(bytes) { + let payload = payload.ok()?; + if let wasmparser::Payload::CodeSectionStart { range, .. } = payload { + total = total.saturating_add(range.end - range.start); + saw_any = true; + } + } + if saw_any { Some(total) } else { None } +} From 4e72d470284b5693682fb9f329df405c26dc41d2 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 18 May 2026 18:55:08 +0200 Subject: [PATCH 2/2] test(dwarf): refresh discovery test for post-Phase-1.5 default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1.5 shipped on main in PR #135 (commit c7a2c0b) during the 16 days this branch sat idle. The DWARF default flipped from PassThrough (broken-but-present) to Strip (correct-but-lossy), and `FuserConfig` gained a new `dwarf_handling` field. Three rebase-driven updates: 1. Add the `dwarf_handling` field to both helper builders. The PassThrough path is the one Phase 2 (#143) will flip from broken to correct, so the existing tests' premise — pin the broken behaviour as a Phase-2 oracle — needs the explicit opt-in now that Strip is the default. 2. Rename `fuse_default` → `fuse_passthrough` to make its scope explicit. The discovery oracle was never about "whatever the default does" — it was about pinning passthrough behaviour for Phase 2 to flip — and that's clearer post-1.5. 3. Re-purpose the `current_default_is_merge_not_drop` test to `current_default_is_strip_post_phase_1_5`. The original was written to flag a future default flip; the flip has now happened, so the assertion inverts. Future changes (e.g. Phase 2 making PassThrough the new default) continue to be surfaced by this same test. 4. Update docstrings and inline comments referencing Phase 1.5 as "will ship" to "shipped in #135 / PR c7a2c0b". All 5 discovery tests still pass against the updated helpers, and the full meld-core lib suite (236/236) is unaffected. Co-Authored-By: Claude Opus 4.7 --- meld-core/tests/dwarf_passthrough.rs | 88 +++++++++++++++++++--------- 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/meld-core/tests/dwarf_passthrough.rs b/meld-core/tests/dwarf_passthrough.rs index 8ea5354..ed55a65 100644 --- a/meld-core/tests/dwarf_passthrough.rs +++ b/meld-core/tests/dwarf_passthrough.rs @@ -59,16 +59,22 @@ //! //! # When to flip these assertions //! -//! - **Phase 1.5 (deliberate passthrough policy)**: this test stays -//! green; we just gain a documented config knob. -//! - **Phase 2 (DWARF remap)**: the duplicate-section assertion flips. -//! The fused module should contain a single, merged, address-correct -//! `.debug_info` / `.debug_line` pair. Update the test then. -//! - **Phase 3 (adapter / inlined-code coverage)**: synthetic DIEs -//! covering adapter ranges land. Update the test to assert their -//! presence then. +//! - **Phase 1.5 (deliberate passthrough policy)**: ✅ shipped in +//! v0.7 / PR #135. `FuserConfig::default().dwarf_handling` is now +//! `Strip`, and tests below explicitly opt into +//! `DwarfHandling::PassThrough` via `fuse_passthrough()` to pin +//! the broken-but-present behaviour for Phase 2 to flip. +//! - **Phase 2 (DWARF remap, #143)**: the duplicate-section assertion +//! flips. The fused module should contain a single, merged, +//! address-correct `.debug_info` / `.debug_line` pair. Update the +//! test then. +//! - **Phase 3 (adapter / inlined-code coverage, #144)**: synthetic +//! DIEs covering adapter ranges land. Update the test to assert +//! their presence then. -use meld_core::{CustomSectionHandling, Fuser, FuserConfig, MemoryStrategy, OutputFormat}; +use meld_core::{ + CustomSectionHandling, DwarfHandling, Fuser, FuserConfig, MemoryStrategy, OutputFormat, +}; /// One of the wit-bindgen integration fixtures known to embed core /// modules compiled with `debuginfo = 2`. Verified via `wasm-tools @@ -149,18 +155,22 @@ fn count_dwarf_sections_recursive(bytes: &[u8]) -> std::collections::BTreeMap Vec { +/// Build a fuser that exercises the **PassThrough** DWARF policy, the +/// path Phase 2 (#143) will flip from broken to correct. Phase 1.5 +/// (#135) made `DwarfHandling::Strip` the production default; this +/// test fixture pins the explicit-opt-in path so the green-to-red +/// signal for Phase 2 lives here rather than relying on a default +/// that has already shifted once. +fn fuse_passthrough(input: &[u8]) -> Vec { let mut fuser = Fuser::new(FuserConfig { memory_strategy: MemoryStrategy::MultiMemory, attestation: false, address_rebasing: false, preserve_names: false, - // Default in production is Merge, but we set it explicitly so - // the test reads as a self-contained spec of meld's current - // policy. See `current_default_is_merge_not_drop` below. custom_sections: CustomSectionHandling::Merge, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), + dwarf_handling: DwarfHandling::PassThrough, }); fuser .add_component_named(input, Some("dwarf-fixture")) @@ -177,6 +187,7 @@ fn fuse_with_drop(input: &[u8]) -> Vec { custom_sections: CustomSectionHandling::Drop, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), + dwarf_handling: DwarfHandling::Strip, }); fuser .add_component_named(input, Some("dwarf-fixture")) @@ -185,15 +196,27 @@ fn fuse_with_drop(input: &[u8]) -> Vec { } #[test] -fn current_default_is_merge_not_drop() { - // Pin: any future change to the default must be a deliberate edit - // to this test, not a silent drift in `FuserConfig::default()`. +fn current_default_is_strip_post_phase_1_5() { + // Pin: Phase 1.5 (#135, commit c7a2c0b) flipped the default DWARF + // handling from PassThrough (broken-but-present) to Strip + // (correct-but-lossy). The discovery oracle was originally + // authored against the pre-Phase-1.5 default and inverted in this + // commit to reflect shipped behaviour. Any future change to the + // default (e.g. Phase 2 making PassThrough correct + the new + // default) must be a deliberate edit to this test. + assert_eq!( + FuserConfig::default().dwarf_handling, + DwarfHandling::Strip, + "FuserConfig::default().dwarf_handling drifted from Strip. \ + If this was intentional (Phase 2 making PassThrough address- \ + correct etc.), update DWARF policy docs and the \ + witness-integration discovery issue." + ); + // The custom-sections default remains Merge — Phase 1.5 only + // changed DWARF policy, not the wider custom-section default. assert_eq!( FuserConfig::default().custom_sections, CustomSectionHandling::Merge, - "FuserConfig::default().custom_sections drifted from Merge. \ - If this was intentional (Phase 1.5 etc.), update DWARF \ - passthrough docs and the witness-integration discovery issue." ); } @@ -220,11 +243,13 @@ fn fixture_carries_dwarf_in_some_embedded_module() { #[test] fn fused_output_inherits_dwarf_byte_for_byte_today() { - // CURRENT BEHAVIOUR (Phase 1, broken): + // CURRENT BEHAVIOUR with `DwarfHandling::PassThrough` (Phase 1, + // broken; opt-in-only since Phase 1.5 / PR #135 made `Strip` the + // default): // - // - Default `CustomSectionHandling::Merge` causes meld to copy each - // input core module's DWARF sections verbatim into the fused core - // module's custom-section slot. + // - PassThrough + `CustomSectionHandling::Merge` causes meld to + // copy each input core module's DWARF sections verbatim into + // the fused core module's custom-section slot. // - DWARF addresses inside these sections are byte offsets into // the ORIGINAL per-input code section. The fused module has a // different code section (rewritten function bodies, different @@ -233,13 +258,14 @@ fn fused_output_inherits_dwarf_byte_for_byte_today() { // wrong source line, worst case it falls outside the new code // section entirely. // - // This test asserts the lossy-but-present invariant. Flip it once - // Phase 2 (real DWARF remapping) lands. + // This test asserts the lossy-but-present invariant on the + // PassThrough code path. Flip it once Phase 2 (#143, real DWARF + // remapping) lands. if !fixture_available() { return; } let bytes = std::fs::read(DEBUG_INFO_FIXTURE).expect("read fixture"); - let fused = fuse_default(&bytes); + let fused = fuse_passthrough(&bytes); let top_level_dwarf_in_fused = count_dwarf_sections_at_top_level(&fused); assert!( @@ -269,8 +295,12 @@ fn drop_policy_strips_dwarf_completely() { // entire module reports as un-attributed), but at least the user // isn't told the WRONG source line. // - // Phase 1.5 will make this the recommended default until Phase 2 - // ships real remapping. + // Note: Phase 1.5 (#135) shipped `DwarfHandling::Strip` as the + // default, so the `Drop` escape hatch is now mostly redundant for + // DWARF specifically — `Strip` removes only `.debug_*`, `Drop` + // removes all custom sections. This test still exercises the + // `Drop` path because the underlying invariant ("opting out + // produces no `.debug_*`") is what witness relies on. if !fixture_available() { return; } @@ -306,7 +336,7 @@ fn dwarf_addresses_in_fused_output_are_known_to_be_wrong() { return; } let bytes = std::fs::read(DEBUG_INFO_FIXTURE).expect("read fixture"); - let fused = fuse_default(&bytes); + let fused = fuse_passthrough(&bytes); let input_code_len = code_section_len_recursive(&bytes); let fused_code_len = code_section_len(&fused);