Skip to main content

synth_core/
wsc_facts.rs

1//! `wsc.facts` custom-section ingestion — VCR-PERF-002 / #494 **Phase 1**.
2//!
3//! loom (the PROVER, loom#231/loom#240) forwards invariants its Z3 translation
4//! validator already discharges in a custom section named `wsc.facts` (sibling
5//! of `wsc.transformation.attestation`), keyed by `(function index, value id)`.
6//! synth is a CONDITIONAL optimizer: a later phase consumes each fact as a
7//! premise behind a per-elision ordeal obligation (see
8//! `docs/design/proof-carrying-specialization.md`). THIS module is ingestion
9//! only — parse and represent; **no codegen path reads the result yet**, so
10//! emitted bytes are unchanged by construction (frozen-safe).
11//!
12//! Binary contract: `docs/design/wsc-facts-encoding.md` (schema v1). The
13//! normative fail-safe skew rule (loom#231 Q4) is implemented here:
14//!
15//! - missing section / unknown version / structurally unparseable section →
16//!   NO facts (the whole section is ignored, never an error);
17//! - unknown fact kind → that record is skipped via its length prefix;
18//! - known kind whose body doesn't decode exactly → that record is dropped.
19//!
20//! Facts are optional accelerators: there is no error path out of this module.
21//! [`parse_wsc_facts`] is total — it returns a (possibly empty) fact list for
22//! every input.
23
24/// Name of the custom section loom emits (loom#231).
25pub const WSC_FACTS_SECTION_NAME: &str = "wsc.facts";
26
27/// Schema version this parser understands. The version byte is FIRST in the
28/// payload so any incompatible future layout is detected before a single
29/// record is read (fail-safe skew: unknown version ⇒ whole section ignored).
30pub const WSC_FACTS_SCHEMA_VERSION: u8 = 1;
31
32/// One proven invariant forwarded by loom, keyed by `(function index, value
33/// id)`. `value_id` is the 0-based index of the producing operator within the
34/// function body's operator sequence — the same index space as
35/// [`FunctionOps::ops`]/[`FunctionOps::op_offsets`] (loom#231 Q1; see the
36/// encoding doc's "Value identification"). Phase 1 stores facts verbatim; an
37/// out-of-range `value_id` is vacuous and a Phase-2 consumer must ignore it.
38///
39/// [`FunctionOps::ops`]: crate::wasm_decoder::FunctionOps::ops
40/// [`FunctionOps::op_offsets`]: crate::wasm_decoder::FunctionOps::op_offsets
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct WscFact {
43    /// Full wasm function index (imported functions first, then local ones) —
44    /// the same space as [`FunctionOps::index`].
45    ///
46    /// [`FunctionOps::index`]: crate::wasm_decoder::FunctionOps::index
47    pub func_index: u32,
48    /// 0-based producing-operator index within the function body (see above).
49    pub value_id: u32,
50    /// The invariant itself.
51    pub kind: FactKind,
52}
53
54/// The fact kinds of encoding schema v1, in silicon-payoff order (the numbers
55/// are the on-wire `kind` bytes; `0x00` is deliberately unassigned so a zeroed
56/// buffer never parses as a fact).
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum FactKind {
59    /// `0x01` — the value is within `[lo, hi]`, signed, inclusive both ends
60    /// (drives the dead-clamp elision, the `gust_mix` shape).
61    ValueRange { lo: i64, hi: i64 },
62    /// `0x02` — the shift amount is `< bound` (bare `lsl/lsr`, no mask).
63    ShiftBound { bound: u32 },
64    /// `0x03` — the divisor is never zero (the `div/rem` trap guard is dead).
65    DivisorNonZero,
66    /// `0x04` — the address plus an `access_width`-byte access stays within
67    /// linear-memory bounds (the bounds check is dead).
68    InBoundsAccess { access_width: u32 },
69    /// `0x05` — both `select` arms are safe to evaluate: branchless `IT`
70    /// lowering is admissible (pairs with VCR-SEL-004).
71    SelectTotality,
72    /// `0x06` — this address and the one produced by `other_value_id` (same
73    /// function) never alias: values may stay in registers across the store.
74    MemoryDisjointness { other_value_id: u32 },
75}
76
77/// Result of parsing a `wsc.facts` payload: the facts plus the diagnostic
78/// counters the decoder surfaces as WARNINGS (never errors — the rivet
79/// `VCR-PERF-002` phase-1 criterion is "ignored with a diagnostic, never an
80/// error"). `Default` is the missing-section value: no facts, nothing to
81/// report.
82#[derive(Debug, Clone, PartialEq, Eq, Default)]
83pub struct WscFactsParse {
84    /// The facts that decoded cleanly. Empty whenever `section_ignored` is set.
85    pub facts: Vec<WscFact>,
86    /// `Some(reason)` when the WHOLE section was ignored: unknown schema
87    /// version or structural unparseability (truncated/overrun framing,
88    /// trailing bytes). Facts already decoded are dropped too — an
89    /// unparseable section is not trusted piecemeal.
90    pub section_ignored: Option<String>,
91    /// Records dropped individually while the section itself parsed: unknown
92    /// fact kinds (a newer loom) and known kinds whose length-prefixed body
93    /// was not exactly the v1 fields (an extended body). Emitter/consumer
94    /// skew signal — worth a warning, never an error.
95    pub records_skipped: u32,
96}
97
98/// Parse a `wsc.facts` custom-section payload (schema v1). TOTAL by design —
99/// this is the fail-safe skew rule (loom#231 Q4) and there is deliberately no
100/// `Result` in the signature: facts are optional accelerators, so no input can
101/// produce an error that changes a compilation outcome.
102///
103/// Whole-section ignore (reported via [`WscFactsParse::section_ignored`]):
104/// unknown schema version, truncated/overrun framing, count mismatch, or
105/// trailing bytes after the last record. Per-record skip (counted in
106/// [`WscFactsParse::records_skipped`], parsing continues): unknown fact kinds
107/// and known kinds whose length-prefixed body doesn't decode to exactly the
108/// expected fields.
109pub fn parse_wsc_facts(payload: &[u8]) -> WscFactsParse {
110    let mut out = WscFactsParse::default();
111    if parse_wsc_facts_inner(payload, &mut out).is_none() {
112        return WscFactsParse {
113            facts: Vec::new(),
114            section_ignored: Some(
115                "unknown schema version or truncated/overrun record framing".to_string(),
116            ),
117            records_skipped: 0,
118        };
119    }
120    out
121}
122
123/// Structural parse into `out`: `None` ⇒ the whole section is unparseable and
124/// must be ignored entirely (including records already decoded into `out`).
125fn parse_wsc_facts_inner(payload: &[u8], out: &mut WscFactsParse) -> Option<()> {
126    let mut r = Reader::new(payload);
127    if r.byte()? != WSC_FACTS_SCHEMA_VERSION {
128        return None;
129    }
130    let count = r.leb_u32()?;
131    for _ in 0..count {
132        let kind = r.byte()?;
133        let func_index = r.leb_u32()?;
134        let value_id = r.leb_u32()?;
135        let body_len = r.leb_u32()?;
136        let body = r.bytes(body_len as usize)?;
137        // Per-record tolerance: an unknown kind byte, or a known kind whose
138        // body isn't exactly the expected fields (a newer emitter may have
139        // extended it), drops THIS record only — the framing above already
140        // consumed it, so the rest of the section still parses.
141        match decode_body(kind, body) {
142            Some(kind) => out.facts.push(WscFact {
143                func_index,
144                value_id,
145                kind,
146            }),
147            None => out.records_skipped += 1,
148        }
149    }
150    // Trailing bytes after the declared records ⇒ structurally unparseable.
151    if !r.is_empty() {
152        return None;
153    }
154    Some(())
155}
156
157/// Decode one record body. `None` ⇒ drop the record (unknown kind, or a body
158/// that is not exactly the v1 fields for its kind).
159fn decode_body(kind: u8, body: &[u8]) -> Option<FactKind> {
160    let mut r = Reader::new(body);
161    let fact = match kind {
162        0x01 => FactKind::ValueRange {
163            lo: r.leb_s64()?,
164            hi: r.leb_s64()?,
165        },
166        0x02 => FactKind::ShiftBound {
167            bound: r.leb_u32()?,
168        },
169        0x03 => FactKind::DivisorNonZero,
170        0x04 => FactKind::InBoundsAccess {
171            access_width: r.leb_u32()?,
172        },
173        0x05 => FactKind::SelectTotality,
174        0x06 => FactKind::MemoryDisjointness {
175            other_value_id: r.leb_u32()?,
176        },
177        _ => return None,
178    };
179    // Exact-body rule: trailing bytes mean an extended (newer) body — treat
180    // like an unknown kind rather than half-reading it.
181    if !r.is_empty() {
182        return None;
183    }
184    Some(fact)
185}
186
187/// Minimal bounds-checked cursor with wasm-spec LEB128 readers. Hand-rolled
188/// (~30 lines) rather than borrowing `wasmparser::BinaryReader` so this
189/// contract cannot drift under a wasmparser major bump — the encoding doc is
190/// the single source of truth.
191struct Reader<'a> {
192    data: &'a [u8],
193    pos: usize,
194}
195
196impl<'a> Reader<'a> {
197    fn new(data: &'a [u8]) -> Self {
198        Self { data, pos: 0 }
199    }
200
201    fn is_empty(&self) -> bool {
202        self.pos >= self.data.len()
203    }
204
205    fn byte(&mut self) -> Option<u8> {
206        let b = *self.data.get(self.pos)?;
207        self.pos += 1;
208        Some(b)
209    }
210
211    fn bytes(&mut self, n: usize) -> Option<&'a [u8]> {
212        let end = self.pos.checked_add(n)?;
213        let s = self.data.get(self.pos..end)?;
214        self.pos = end;
215        Some(s)
216    }
217
218    /// Unsigned LEB128, max 5 bytes (wasm-spec `u32`).
219    fn leb_u32(&mut self) -> Option<u32> {
220        let mut result: u32 = 0;
221        for shift in [0u32, 7, 14, 21, 28] {
222            let b = self.byte()?;
223            let low = u32::from(b & 0x7f);
224            // Bits that would fall off the top make the encoding invalid.
225            if low.checked_shl(shift)? >> shift != low {
226                return None;
227            }
228            result |= low << shift;
229            if b & 0x80 == 0 {
230                return Some(result);
231            }
232        }
233        None // continuation bit set on the 5th byte
234    }
235
236    /// Signed LEB128, max 10 bytes (wasm-spec `s64`).
237    fn leb_s64(&mut self) -> Option<i64> {
238        let mut result: i64 = 0;
239        let mut shift: u32 = 0;
240        loop {
241            let b = self.byte()?;
242            if shift >= 63 {
243                // 10th byte: only 1 payload bit left. It must terminate and
244                // must be a pure sign-extension byte (0x00 or 0x7f).
245                if b & 0x80 != 0 || (b != 0x00 && b != 0x7f) {
246                    return None;
247                }
248                if b == 0x7f {
249                    result |= -1i64 << shift;
250                }
251                return Some(result);
252            }
253            result |= i64::from(b & 0x7f) << shift;
254            shift += 7;
255            if b & 0x80 == 0 {
256                // Sign-extend from the last payload bit.
257                if shift < 64 && b & 0x40 != 0 {
258                    result |= -1i64 << shift;
259                }
260                return Some(result);
261            }
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    /// Facts-only view for the happy-path assertions; the skew tests assert
271    /// the diagnostic fields explicitly.
272    fn facts(payload: &[u8]) -> Vec<WscFact> {
273        parse_wsc_facts(payload).facts
274    }
275
276    // ---- test-side encoder (mirrors docs/design/wsc-facts-encoding.md) ----
277
278    fn leb_u32(mut v: u32, out: &mut Vec<u8>) {
279        loop {
280            let mut b = (v & 0x7f) as u8;
281            v >>= 7;
282            if v != 0 {
283                b |= 0x80;
284            }
285            out.push(b);
286            if v == 0 {
287                return;
288            }
289        }
290    }
291
292    fn leb_s64(mut v: i64, out: &mut Vec<u8>) {
293        loop {
294            let mut b = (v & 0x7f) as u8;
295            v >>= 7; // arithmetic shift
296            let done = (v == 0 && b & 0x40 == 0) || (v == -1 && b & 0x40 != 0);
297            if !done {
298                b |= 0x80;
299            }
300            out.push(b);
301            if done {
302                return;
303            }
304        }
305    }
306
307    fn record(kind: u8, func_index: u32, value_id: u32, body: &[u8]) -> Vec<u8> {
308        let mut out = vec![kind];
309        leb_u32(func_index, &mut out);
310        leb_u32(value_id, &mut out);
311        leb_u32(body.len() as u32, &mut out);
312        out.extend_from_slice(body);
313        out
314    }
315
316    fn section(version: u8, records: &[Vec<u8>]) -> Vec<u8> {
317        let mut out = vec![version];
318        leb_u32(records.len() as u32, &mut out);
319        for r in records {
320            out.extend_from_slice(r);
321        }
322        out
323    }
324
325    fn value_range_body(lo: i64, hi: i64) -> Vec<u8> {
326        let mut b = Vec::new();
327        leb_s64(lo, &mut b);
328        leb_s64(hi, &mut b);
329        b
330    }
331
332    // ---- well-formed sections: every v1 fact kind round-trips ----
333
334    #[test]
335    fn parses_value_range_494() {
336        // The gust_mix premise: v ∈ [524, 1524] on (func 3, value 7).
337        let s = section(1, &[record(0x01, 3, 7, &value_range_body(524, 1524))]);
338        assert_eq!(
339            facts(&s),
340            vec![WscFact {
341                func_index: 3,
342                value_id: 7,
343                kind: FactKind::ValueRange { lo: 524, hi: 1524 },
344            }]
345        );
346    }
347
348    #[test]
349    fn parses_negative_and_extreme_range_bounds_494() {
350        let s = section(
351            1,
352            &[record(0x01, 0, 0, &value_range_body(i64::MIN, i64::MAX))],
353        );
354        assert_eq!(
355            facts(&s),
356            vec![WscFact {
357                func_index: 0,
358                value_id: 0,
359                kind: FactKind::ValueRange {
360                    lo: i64::MIN,
361                    hi: i64::MAX,
362                },
363            }]
364        );
365    }
366
367    #[test]
368    fn parses_shift_bound_494() {
369        let mut body = Vec::new();
370        leb_u32(32, &mut body);
371        let s = section(1, &[record(0x02, 1, 4, &body)]);
372        assert_eq!(
373            facts(&s),
374            vec![WscFact {
375                func_index: 1,
376                value_id: 4,
377                kind: FactKind::ShiftBound { bound: 32 },
378            }]
379        );
380    }
381
382    #[test]
383    fn parses_divisor_nonzero_494() {
384        let s = section(1, &[record(0x03, 2, 9, &[])]);
385        assert_eq!(
386            facts(&s),
387            vec![WscFact {
388                func_index: 2,
389                value_id: 9,
390                kind: FactKind::DivisorNonZero,
391            }]
392        );
393    }
394
395    #[test]
396    fn parses_in_bounds_access_494() {
397        let mut body = Vec::new();
398        leb_u32(4, &mut body);
399        let s = section(1, &[record(0x04, 5, 11, &body)]);
400        assert_eq!(
401            facts(&s),
402            vec![WscFact {
403                func_index: 5,
404                value_id: 11,
405                kind: FactKind::InBoundsAccess { access_width: 4 },
406            }]
407        );
408    }
409
410    #[test]
411    fn parses_select_totality_494() {
412        let s = section(1, &[record(0x05, 6, 13, &[])]);
413        assert_eq!(
414            facts(&s),
415            vec![WscFact {
416                func_index: 6,
417                value_id: 13,
418                kind: FactKind::SelectTotality,
419            }]
420        );
421    }
422
423    #[test]
424    fn parses_memory_disjointness_494() {
425        let mut body = Vec::new();
426        leb_u32(21, &mut body);
427        let s = section(1, &[record(0x06, 7, 17, &body)]);
428        assert_eq!(
429            facts(&s),
430            vec![WscFact {
431                func_index: 7,
432                value_id: 17,
433                kind: FactKind::MemoryDisjointness { other_value_id: 21 },
434            }]
435        );
436    }
437
438    #[test]
439    fn parses_multiple_facts_in_order_494() {
440        let s = section(
441            1,
442            &[
443                record(0x01, 0, 1, &value_range_body(-8, 8)),
444                record(0x03, 0, 2, &[]),
445            ],
446        );
447        let facts = facts(&s);
448        assert_eq!(facts.len(), 2);
449        assert_eq!(facts[0].kind, FactKind::ValueRange { lo: -8, hi: 8 });
450        assert_eq!(facts[1].kind, FactKind::DivisorNonZero);
451    }
452
453    #[test]
454    fn parses_empty_fact_list_494() {
455        // A well-formed zero-fact section is NOT a skew condition: no facts,
456        // no diagnostics.
457        let parsed = parse_wsc_facts(&section(1, &[]));
458        assert_eq!(parsed, WscFactsParse::default());
459    }
460
461    // ---- fail-safe skew rule: tolerance without any error path ----
462
463    #[test]
464    fn unknown_kind_is_skipped_others_kept_494() {
465        // kind 0x2A from a future loom, length-prefixed body — skipped via the
466        // prefix; the known records around it still parse.
467        let s = section(
468            1,
469            &[
470                record(0x03, 0, 1, &[]),
471                record(0x2a, 0, 2, &[0xde, 0xad, 0xbe]),
472                record(0x05, 0, 3, &[]),
473            ],
474        );
475        let parsed = parse_wsc_facts(&s);
476        assert_eq!(parsed.facts.len(), 2);
477        assert_eq!(parsed.facts[0].kind, FactKind::DivisorNonZero);
478        assert_eq!(parsed.facts[1].kind, FactKind::SelectTotality);
479        // The skip is DIAGNOSED (skew signal), not a whole-section ignore.
480        assert_eq!(parsed.records_skipped, 1);
481        assert_eq!(parsed.section_ignored, None);
482    }
483
484    #[test]
485    fn kind_zero_is_not_a_fact_494() {
486        // 0x00 is deliberately unassigned (zeroed buffers must not parse).
487        let s = section(1, &[record(0x00, 0, 0, &[])]);
488        assert_eq!(facts(&s), vec![]);
489    }
490
491    #[test]
492    fn extended_body_on_known_kind_drops_that_record_only_494() {
493        // divisor-nonzero with a 2-byte body: a newer emitter extended it —
494        // exact-body rule drops it, the neighbor survives.
495        let s = section(
496            1,
497            &[record(0x03, 0, 1, &[0x01, 0x02]), record(0x05, 0, 2, &[])],
498        );
499        let facts = facts(&s);
500        assert_eq!(facts.len(), 1);
501        assert_eq!(facts[0].kind, FactKind::SelectTotality);
502    }
503
504    #[test]
505    fn short_body_on_known_kind_drops_that_record_only_494() {
506        // value-range with only `lo` present — the body under-decodes.
507        let mut body = Vec::new();
508        leb_s64(524, &mut body);
509        let s = section(1, &[record(0x01, 0, 1, &body), record(0x03, 0, 2, &[])]);
510        let facts = facts(&s);
511        assert_eq!(facts.len(), 1);
512        assert_eq!(facts[0].kind, FactKind::DivisorNonZero);
513    }
514
515    #[test]
516    fn version_mismatch_ignores_whole_section_494() {
517        for version in [0, 2, 0xff] {
518            let parsed = parse_wsc_facts(&section(version, &[record(0x03, 0, 1, &[])]));
519            assert_eq!(parsed.facts, vec![]);
520            // Whole-section ignore is DIAGNOSED, never an error.
521            assert!(parsed.section_ignored.is_some());
522        }
523    }
524
525    #[test]
526    fn empty_and_truncated_sections_ignored_494() {
527        assert_eq!(facts(&[]), vec![]); // no version byte
528        assert_eq!(facts(&[0x01]), vec![]); // no count
529        // count says 1 record, no record bytes follow
530        assert_eq!(facts(&[0x01, 0x01]), vec![]);
531        // record framing truncated mid-header (kind present, indices missing)
532        assert_eq!(facts(&[0x01, 0x01, 0x01]), vec![]);
533    }
534
535    #[test]
536    fn body_len_overrun_ignores_whole_section_494() {
537        // body_len = 200 but only 2 body bytes present — framing overrun, and
538        // the earlier GOOD record is dropped too (unparseable ⇒ ignore whole).
539        let good = record(0x03, 0, 1, &[]);
540        let mut bad = vec![0x01];
541        leb_u32(0, &mut bad);
542        leb_u32(0, &mut bad);
543        leb_u32(200, &mut bad);
544        bad.extend_from_slice(&[0xaa, 0xbb]);
545        let s = section(1, &[good, bad]);
546        assert_eq!(facts(&s), vec![]);
547    }
548
549    #[test]
550    fn trailing_bytes_after_records_ignore_whole_section_494() {
551        let mut s = section(1, &[record(0x03, 0, 1, &[])]);
552        s.push(0xff);
553        assert_eq!(facts(&s), vec![]);
554    }
555
556    #[test]
557    fn overlong_leb_ignores_whole_section_494() {
558        // func_index as a 6-byte LEB (continuation on the 5th byte) — invalid
559        // wasm-spec u32.
560        let mut s = vec![0x01, 0x01, 0x03];
561        s.extend_from_slice(&[0x80, 0x80, 0x80, 0x80, 0x80, 0x00]); // func_index
562        s.push(0x00); // value_id
563        s.push(0x00); // body_len
564        assert_eq!(facts(&s), vec![]);
565    }
566
567    #[test]
568    fn garbage_is_ignored_never_panics_494() {
569        // A few adversarial byte soups: total function, empty result.
570        for garbage in [
571            &[0xff_u8, 0xff, 0xff, 0xff][..],
572            &[0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff][..],
573            &[0x00, 0x00, 0x00][..],
574        ] {
575            assert_eq!(facts(garbage), vec![]);
576        }
577    }
578}