Skip to content

Port LowParse combinators to Pulse#271

Merged
tahina-pro merged 122 commits intoproject-everest:masterfrom
tahina-pro:_taramana_pulse_parse
Apr 3, 2026
Merged

Port LowParse combinators to Pulse#271
tahina-pro merged 122 commits intoproject-everest:masterfrom
tahina-pro:_taramana_pulse_parse

Conversation

@tahina-pro
Copy link
Copy Markdown
Member

@tahina-pro tahina-pro commented Mar 27, 2026

In light of the upcoming deprecation of Low*, this PR is porting LowParse combinators to Pulse.

This PR is also porting some LowParse.Pulse combinators from ACM CCS 2025 to a new separation logic predicate, pts_to_parsed, which relies on the parser specification rather than the serializer specification, so that (most of) those combinators can be used with non-injective parsers.

Coarse-grained roadmap:

  • LowParse combinators
  • LowParse Low* tests (but not SLow, which are slated to disappear)
  • QuackyDucky
  • QuackyDucky tests

Left unported: offset reasoning and finalizers for serialization. These should be replaced with wholesale serializers, from our ACM CCS 2025 paper

tahina-pro and others added 30 commits March 16, 2026 22:02
Define parsing combinators that use pts_to_parsed (parser-only) instead of
pts_to_serialized (which requires a serializer and parser injectivity).

PulseParse.Base (extended):
- leaf_reader/reader types using pts_to_parsed
- leaf_reader_of_reader, reader_of_leaf_reader conversions
- leaf_reader_of_serialized, reader_of_serialized bridges
- read_parsed_from_validator_success (requires ParserStrong)
- ifthenelse_reader, reader_ext utilities

PulseParse.Combinators (new):
- Ghost lemmas: pts_to_parsed_synth_{intro,elim,trade,l2r,r2l,l2r_trade}
- Ghost lemmas: pts_to_parsed_filter_{intro,elim,elim_trade}
- Read: read_synth, read_synth', read_filter with helper types
- Validate: validate_filter, validate_filter', validate_dtuple2
- Jump: jump_dtuple2
- Ret/empty: leaf_read_ret, read_ret, leaf_read_empty, read_empty

PulseParse.BitSum (new):
- read_bitsum' (composition of read_filter and read_synth)

PulseParse.VCList (new):
- Ghost: pts_to_parsed_nlist_1_{intro,elim}, pts_to_parsed_nlist_ext
- Split: nlist_hd_tl, nlist_hd, nlist_tl
- Note: sub-slices have half permission (pm /. 2.0R) due to
  non-injective pts_to_parsed_intro

PulseParse.Recursive (new, stub):
- All Recursive combinators require serialize_recursive_param and cannot
  be ported without a complete redesign of the recursive infrastructure.

Combinators that cannot be rewritten without serializers:
- Writers (l2r_write_*, l2r_leaf_writer): write serialized bytes
- Size (compute_remaining_size): compute serialized byte lengths
- Copy/Gather: need serializer injectivity
- zero_copy_parse: returns trade to pts_to_serialized
- peek_trade_gen/validate_filter_gen: need serializer for sub-slice creation
- split_dtuple2/nondep_then (Combinators): tied to serializer byte sequences
- read_dtuple2: depends on split_dtuple2
- All Recursive operations: inseparable from serialize_recursive_param

Technical notes:
- pts_to_parsed_synth_{elim,l2r,r2l,l2r_trade} require synth_injective f
  (stronger than the Pulse counterparts) because without known bytes we
  need injectivity to recover the pre-image from parse_synth results
- read_parsed_from_validator_success requires ParserStrong to use
  parse_strong_prefix for sub-slice correctness
- VCList nlist_hd_tl produces sub-slices at pm /. 2.0R because
  pts_to_parsed_intro for non-injective parsers halves permission

Skipped modules:
- LowParse.Pulse.Int (per user request)
- LowParse.Pulse.Endianness (no serializer usage)
- LowParse.Pulse.SeqBytes (all operations write serialized data)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port validator combinators from LowParse.Low.* to Pulse validators in
LowParse.PulseParse.*, replacing LowParse.Low.Base.validator (Low*/Stack)
with LowParse.Pulse.Base.validator (Pulse/stt). All verified with
make lowparse, no admit() or assume except FStar.SizeT.fits_u64
(taken as squash parameter where needed).

New files:
- LowParse.PulseParse.FLData.fst: validate_fldata, jump_fldata
  Validates fixed-length data by splitting the input slice at the
  expected boundary, running the inner validator on the prefix, and
  checking the consumed length matches exactly.

- LowParse.PulseParse.List.fst: validate_list
  Validates parse_list (zero-or-more elements until end of input).
  Uses a while loop with invariant tracking Some?-equivalence of
  parse_list at the original vs current offset. Requires
  ParserStrong and parser_kind_low > 0.

- LowParse.PulseParse.Option.fst: validate_option
  Validates parse_option: tries the inner validator; on success
  returns true (Some case), on failure resets offset and returns
  true (None case — parse_option always succeeds).

- LowParse.PulseParse.VLData.fst: validate_vldata_gen,
  validate_bounded_vldata', validate_bounded_vldata
  Validates variable-length data with a bounded-integer length prefix.
  Reads the length prefix via leaf_reader, checks the filter predicate,
  then splits the input at the payload boundary and validates the
  payload, checking consumed == declared length. Takes a
  PPB.leaf_reader for parse_bounded_integer as parameter.

Extended files:
- LowParse.PulseParse.Combinators.fst (+167 lines):
  * validate_false: always fails (parse_false returns None)
  * validate_strengthen: strengthen parser kind (delegates to validate_ext)
  * validate_lift_parser: lift thunked parser (delegates to validate_ext)
  * validate_filter_ret: filter on parse_ret without needing a reader
  * validate_filter_and_then: validate filter then dependent parse,
    requires ParserStrong and a leaf_reader for the first parser
  * validate_synth: validate through synth (requires synth_injective)
  * validate_nondep_then: sequential validation of two parsers
  * validate_compose_context: re-index a parser family

- LowParse.PulseParse.VCList.fst (+114 lines):
  * validate_nlist: validate exactly n elements in a loop. Simple
    while loop counting down from n, calling the inner validator each
    iteration. This is the general-purpose version (contrast with
    validate_tot_nlist_recursive in LowParse.Pulse.Recursive which
    handles recursive parsers with serialize_recursive_param).
  * validate_vclist: validate a variable-count list (length prefix +
    nlist). Reads the count via leaf_reader, bounds-checks it, then
    delegates to validate_nlist. Requires fits_u64 for U32-to-SZ
    conversion.

Validators NOT ported (with reasons):
- validate_deplen: parse_deplen requires a serializer in its type
  signature (parse_deplen_data_t is parameterized by serializer)
- Sum/DSum validators (validate_sum, validate_dsum, ~20 helpers):
  require complex dispatch infrastructure (enum tables, sum case
  matching) not available in Pulse; the spec types themselves are
  serializer-heavy
- Enum validators (validate_enum_key, validate_maybe_enum_key):
  dependent on enum/sum infrastructure
- validate_ifthenelse: requires Low*-specific test_ifthenelse_tag
  type; would need new Pulse-specific test infrastructure
- Error code variants (validate_with_error_code, etc.): Pulse
  validators return bool, not error codes
- Low*-specific wrappers (validate_no_read, validate_with_comment):
  no Pulse equivalent needed
- LowParse.Low.Int validators: omitted per user request; these are
  validate_total_constant_size wrappers
- LowParse.Pulse.Recursive validators: already exist in Pulse and
  require serialize_recursive_param (cannot remove serializer dep)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adapt LowParse.Pulse.Base.peek_trade_gen to PulseParse, replacing
pts_to_serialized with pts_to_parsed. Requires ParserStrong.

Given a successful validator result (offset, off), splits the input
at offset and at offset+consumed to isolate the parsed sub-slice,
uses parse_strong_prefix to establish the parse on the sub-slice,
then pts_to_parsed_intro to produce a pts_to_parsed resource.

Returns pts_to_parsed p input' #(pm /. 2.0R) v' with a trade back
to pts_to input #pm v. Permission halving (pm /. 2.0R) is inherent
to pts_to_parsed_intro for non-injective parsers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the admit() in validate_sum_aux with a verified implementation
following the Low.Sum pattern:

- Call parse_sum_eq'' to decompose parse_sum into tag + payload parsing
- Validate the tag with the tag validator
- Read the tag value using read_parsed_from_validator_success (requires
  ParserStrong on the tag parser kind, added as squash parameter)
- Connect sub-slice equivalence with Seq.lemma_eq_elim
- Delegate to v_payload callback for payload validation/dispatch

Also fix validate_sum_aux_payload_postcond for the Unknown case to include
SZ.v off <= Seq.length v, which is required by validator_postcond even on
failure.

No admit() or assume left. Verified with --z3rlimit 64.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port validate_sum_aux_payload', validate_sum_aux_payload, and validate_sum
from LowParse.Low.Sum to Pulse:

- validate_sum_aux_payload': Pulse fn with match on maybe_enum_key —
  for Known k, delegates to pc32 k; for Unknown, returns false
- validate_sum_aux_payload: uses dep_maybe_enum_destr_t to dispatch
  the payload validation per tag repr value
- validate_sum: top-level combinator wiring everything together

Also fix validate_sum_aux to restore poffset on payload failure, since
validate_sum_aux_payload_postcond for Unknown doesn't constrain the
offset (needed by validator_postcond which requires SZ.v off <= len
even on failure). Revert the postcondition strengthening that caused
Pulse type-checking issues with Unknown instantiation.

No admit() or assume. Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port all 12 BitSum validator combinators from LowParse.Low.BitSum to
LowParse.PulseParse.BitSum, using Pulse's validator type (LPS.validator):

Pure F* combinators (kept as Tot let definitions):
- validate_bitsum': composes validate_filter + validate_synth
- validate_bitsum_cases_t: type definition for case dispatch
- validate_bitsum_cases_bitstop: leaf base case
- validate_bitsum_cases_bitfield: intermediate bitfield node
- validate_bitsum_cases_bitsum_gen: enum-based dispatch
- validate_bitsum_cases_bitsum'_t: list iterator type
- validate_bitsum_cases_bitsum'_intro: initialize list iteration
- validate_bitsum_cases_bitsum'_nil: unreachable case (false_elim)
- validate_bitsum_cases_bitsum'_cons: conditional case matching
- mk_validate_bitsum_cases_bitsum'_t': recursive list traversal
- mk_validate_bitsum_cases_t': recursive structure traversal

Pulse fn (ported from Low* Stack effect):
- validate_bitsum: main entry point — validates tag with
  validate_bitsum', reads tag via read_parsed_from_validator_success,
  dispatches to case validator via vs

Key adaptations:
- Dropped #rrel #rel sl pos validator args, return B.validator directly
- Fixed universe: Type0 instead of Type u#r (Pulse validator requires Type0)
- Added ParserStrong squash for leaf_reader-based tag reading
- Reorder lemma calls (parse_synth_eq, parse_filter_eq) before
  read_parsed_from_validator_success to establish validator_success p
- Restore poffset on failure for validator_postcond off bound

No admit() or assume. Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add DSum validator combinators ported from LowParse.Low.Sum to Pulse:

- validate_dsum_cases_t: type alias for DSum case validators
- validate_dsum_cases_if'/validate_dsum_cases_if: if-combinator for
  dep_maybe_enum_destr_t dispatch (Pulse fn wrapping cond branch)
- validate_dsum_cases': case validator using validate_synth for Known/Unknown
- validate_dsum_cases_dispatch: pure Tot wrapper for destr call (needed
  because dep_maybe_enum_destr_t refinement checking fails inside Pulse fn
  bodies; mirrors validate_sum_aux_payload pattern)
- validate_dsum: top-level Pulse validator for parse_dsum, validates tag,
  reads repr via read_parsed_from_validator_success, dispatches to case
  validator via destr, restores offset on failure

Key technical insight: dep_maybe_enum_destr_t calls must happen in pure Tot
context (not inside Pulse fn), because Pulse's type checker cannot prove the
trivial refinement (eq_trivial) on the return type. Solution: extract the
destr call into validate_dsum_cases_dispatch (pure Tot let), then call the
resulting validator from the Pulse fn body.

All verification conditions discharged, no admit() or assume used.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create LowParse.PulseParse.IfThenElse.fst with validate_ifthenelse ported
from LowParse.Low.IfThenElse. Instead of the Low* test_ifthenelse_tag
callback (which reads the tag from the buffer via Stack effect), uses a
leaf_reader to read the tag via read_parsed_from_validator_success, then
applies parse_ifthenelse_tag_cond to get the branch condition.

Requires ParserStrong on the tag parser kind (squash parameter).

No admit() or assume. Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create LowParse.PulseParse.ListUpTo.fst with validate_list_up_to ported
from LowParse.Low.ListUpTo to Pulse.

Uses a while loop that iterates: validate one element, read it via
read_parsed_from_validator_success, check the termination condition. If
cond is true, stop with success. If cond is false, advance and continue.
If element validation fails, stop with failure.

Loop invariant tracks consumed bytes: relates parse result at the
initial offset to parse result at the current offset, ensuring the
total consumed bytes are consistent across iterations.

Requires ParserStrong on the element parser (for leaf_reader).

No admit() or assume. Verified with --z3rlimit 32.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add serialized_of_leaf_reader: converts a PulseParse leaf_reader (works
on pts_to_parsed p) to a Pulse leaf_reader (works on pts_to_serialized s),
given a serializer. This is the reverse of the existing
leaf_reader_of_serialized.

The conversion uses pts_to_serialized_parsed to go from pts_to_serialized
to pts_to_parsed, calls the PulseParse reader, then uses
pts_to_parsed_serialized to convert back, chaining the trades.

Together with the existing leaf_reader_of_serialized, this provides
bidirectional conversion between the two leaf_reader types.

No admit() or assume. Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create LowParse.PulseParse.BCVLI.fst with both BCVLI validators ported
from LowParse.Low.BCVLI:

- validate_bcvli: validates variable-length integer (1/3/5 bytes).
  Validates 1-byte header with validate_total_constant_size, reads it
  via read_parsed_from_validator_success, then branches:
  - <253: value is header byte, done
  - 253: validate+read 2-byte LE payload, check non-malleability (>=253)
  - 254: validate+read 4-byte LE payload, check non-malleability (>=65536)
  - 255: error

- validate_bounded_bcvli: same structure plus bounds checking (min/max).
  Early-rejects impossible branches based on bounds.

Both take leaf_readers for parse_bounded_integer_le 1/2/4 as parameters,
allowing callers to provide the appropriate readers.

Restores poffset on failure for validator_postcond.

No admit() or assume. Verified with --z3rlimit 32.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create LowParse.PulseParse.BoundedInt.fst with the constant-size
bounded integer validators:

- validate_bounded_integer (i): BE, sz=1/2/3/4 — validate_total_constant_size
- validate_bounded_integer' (i): runtime-sized variant (needs fits_u64)
- validate_bounded_integer_le (i): LE variant
- validate_u16_le, validate_u32_le: LE integer validators

The bounded int32 composite validators (validate_bounded_int32',
validate_bounded_int32_le', validate_bounded_int32_le_fixed_size and
their size-specialized variants) are NOT included because they require
equational lemmas relating parse_bounded_int32 to parse_bounded_integer
+ filter, but the Spec.BoundedInt module does not expose these equations
and its definitions are opaque due to caching (--already_cached). Adding
parse_bounded_int32_eq to LowParse.Spec.BoundedInt.fsti would enable
these validators.

No admit() or assume. Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add equational lemmas to LowParse.Spec.BoundedInt.fsti exposing the
compositional structure of parse_bounded_int32, parse_bounded_int32_le,
and parse_bounded_int32_le_fixed_size. These are needed because the
Pulse build caches Spec modules (--already_cached), making their .fst
definitions opaque. The Low* validators don't need these lemmas because
they use 'friend' with the uncached Spec module.

New equational lemmas in Spec.BoundedInt.fsti:
- parse_bounded_int32_eq: decomposes into parse_bounded_integer + filter
- parse_bounded_int32_le_eq: same for LE
- parse_bounded_int32_le_fixed_size_eq: decomposes into parse_u32_le + filter

New validators in PulseParse.BoundedInt (all 20 from Low.BoundedInt):

Constant-size (5):
- validate_bounded_integer, validate_bounded_integer' (BE)
- validate_bounded_integer_le (LE)
- validate_u16_le, validate_u32_le

Bounded int32 BE (4): validate_bounded_int32_{1,2,3,4}
Bounded int32 LE (4): validate_bounded_int32_le_{1,2,3,4}
Fixed-size LE (1): validate_bounded_int32_le_fixed_size

Each composite validator: validates constant-size bytes, reads value via
read_parsed_from_validator_success, checks bounds, restores poffset on
failure. Uses the new equational lemmas to connect the parse result.

No admit() or assume (except fits_u64 in validate_bounded_integer').
Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the remaining BoundedInt validators to PulseParse:

- validate_bounded_int32': generic BE, takes sz + sub-validator + reader
- validate_bounded_int32_le': generic LE variant
- validate_bounded_int32: runtime dispatcher, branches on max32 to call _1/_2/_3/_4
- validate_bounded_int32_le: LE runtime dispatcher

The generic versions take sz as integer_size with refinement
sz == log256' max, plus the base validator and leaf_reader for
parse_bounded_integer sz. The caller provides the concrete sz.

The dispatchers take all 4 leaf_readers and branch at runtime.

No admit() or assume. Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add three FLData validators:

- validate_fldata_gen: alias for validate_fldata (already handles
  non-ConsumesAll parsers via consumed == sz check)
- validate_fldata_consumes_all: alias for validate_fldata with
  ParserConsumesAll squash (existing impl handles both cases)
- validate_fldata_strong: validates parse_fldata_strong s sz, which
  refines parse_fldata with a serializer length predicate. F* sees
  through coerce_parser/parse_strengthen/bare_parse_strengthen to
  connect the postcondition directly to validate_fldata.

No admit() or assume. Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add three VLData validators:

- validate_vldata_payload: wraps validate_fldata with validate_weaken
  for the vldata_payload parser kind
- validate_bounded_vldata_strong': validates parse_bounded_vldata_strong'
  by delegating to validate_bounded_vldata' (F* sees through
  coerce_parser/parse_strengthen)
- validate_bounded_vldata_strong: default log256' version

No admit() or assume (except fits_u64). Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create three new PulseParse modules:

LowParse.PulseParse.VLGen.fst (3 validators):
- validate_bounded_vlgen: Pulse fn — validates length prefix, reads it
  via read_parsed_from_validator_success, delegates to validate_fldata_strong
- validate_vlgen: validate_synth wrapper over validate_bounded_vlgen
  with synth_vlgen coercion
- validate_vlgen_weak: Pulse fn — like validate_bounded_vlgen but uses
  validate_fldata (no serializer constraint on payload)

LowParse.PulseParse.Bytes.fst (5 validators):
- validate_flbytes: validate_total_constant_size wrapper
- validate_all_bytes: Pulse fn — accepts all remaining bytes
- validate_bounded_vlbytes': validate_synth over
  validate_bounded_vldata_strong' with synth_bounded_vlbytes
- validate_bounded_vlbytes: default log256' wrapper
- validate_bounded_vlgenbytes: validate_synth over
  validate_bounded_vlgen with serialize_all_bytes

LowParse.PulseParse.Array.fst (3 validators):
- validate_array': validate_synth over validate_fldata_strong +
  validate_list with fldata_to_array
- validate_array: total constant-size when metadata is Total,
  otherwise delegates to validate_array'
- validate_vlarray: validate_synth over validate_bounded_vldata_strong
  + validate_list with vldata_to_vlarray

No admit() or assume (except fits_u64). Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create LowParse.Pulse.DepLen.fst with validate_deplen ported from
LowParse.Low.DepLen. This goes in LowParse.Pulse (not PulseParse)
because parse_deplen requires a serializer in its type definition.

The validator:
1. Validates header with hv, reads it via read_from_validator_success
2. Computes payload length with dlf(header_value)
3. Checks remaining bytes >= payload_len
4. Splits input at off1 + payload_len, validates payload in sub-slice
5. Checks consumed == payload_len
6. Uses serializer_correct_implies_complete to establish the
   Seq.length(serialize ps t) == consumed condition needed by
   parse_deplen_unfold2

Requires ParserStrong on header parser (for reading) and fits_u64.

No admit() or assume (except fits_u64). Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create two new modules:

LowParse.PulseParse.Int.fst (3 validators):
- validate_u64_le: validate_total_constant_size (8 bytes LE)
- validate_int32le: validate_total_constant_size (4 bytes LE)
- validate_constint32le: validates parse_int32le then reads and compares
  value against expected constant. Takes leaf_reader for parse_int32le.

LowParse.Pulse.DER.fst (2 validators, serializer-based):
- validate_der_length_payload32: multi-branch DER length payload
  validator. Branches on tag byte: <128 (zero payload), 129 (1-byte
  u8 payload), 130-132 (2/3/4-byte bounded_integer payload). Checks
  non-malleability (value >= pow2(8*(len-1))).
- validate_bounded_der_length32: validates u8 tag, checks tag range
  against min/max bounds, then inlines the DER payload validation +
  bounds checking in one pass.

Both DER validators take leaf_reader serialize_u8 and a function
providing leaf_reader (serialize_bounded_integer i) for each size.

No admit() or assume. Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add jumper implementations across 13 files, porting from LowParse.Low.*
to LowParse.Pulse.* and LowParse.PulseParse.* modules.

Tier 1 - Constant-size jumpers:
- BoundedInt: jump_bounded_integer, jump_bounded_integer_le,
  jump_u16_le, jump_u32_le, jump_bounded_int32_{1,2,3,4},
  jump_bounded_int32_le_{1,2,3,4}, jump_bounded_int32_le_fixed_size
- Int: jump_u64_le, jump_int32le, jump_constint32le
- Bytes: jump_flbytes, jump_all_bytes
- FLData: jump_fldata_strong

Tier 2 - Simple wrappers:
- VLData: jump_vldata_gen, jump_bounded_vldata', jump_bounded_vldata,
  jump_bounded_vldata_strong', jump_bounded_vldata_strong
- VLGen: jump_bounded_vlgen, jump_vlgen
- Bytes: jump_bounded_vlbytes', jump_bounded_vlbytes,
  jump_bounded_vlgenbytes
- Enum: jump_enum_key, jump_maybe_enum_key
- IfThenElse: jump_ifthenelse

Tier 3 - Complex jumpers:
- BCVLI: jump_bcvli, jump_bounded_bcvli
- DER: jump_der_length_payload32, jump_bounded_der_length32
- Sum: jump_sum_cases_aux, jump_sum_cases, jump_sum_aux, jump_sum,
  jump_dsum_cases', jump_dsum_cases_dispatch, jump_dsum
- VCList: jump_vclist
- Array: jump_array, jump_vlarray

All 29 Pulse module files verified successfully with no admits.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create share/everparse/tests/lowparse/ with:

- Makefile: build system for Pulse LowParse tests, includes
  lowparse/pulse in the path
- LowParseTestLib.Pulse.fst: test library with test_validator_accept
  and test_validator_reject functions (stubs with admit for now,
  implementations will be in C at extraction time)
- LowParseTestLib_Pulse_c.c: C implementations of print_string,
  exit_failure, and load_file utility

This provides the foundation for porting Low* LowParse tests from
tests/lowparse/ to Pulse. Test functions take a validator, a test name,
and a byte slice, then check whether the validator accepts/rejects as
expected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add zero_copy_parse type and combinators using pts_to_parsed (parser-only)
instead of pts_to_serialized (serializer-based).

LowParse.PulseParse.Base.fst (8 definitions):
- pts_to_parsed_with_perm: analog of pts_to_serialized_with_perm
- zero_copy_parse: type taking parser p instead of serializer s,
  producing vmatch res v with trade back to pts_to_parsed p input
- zero_copy_parse_id: returns input with pts_to_parsed_with_perm
- zero_copy_parse_lens: transforms vmatch via lens
- zero_copy_parse_read: uses PulseParse leaf_reader p
- zero_copy_parse_ignore: ignores parsed value
- zero_copy_parse_ext: parser extensionality
- zero_copy_parse_ifthenelse: conditional dispatch

LowParse.PulseParse.Combinators.fst (5 definitions):
- zero_copy_parse_synth: for parse_synth
- zero_copy_parse_filter: for parse_filter
- zero_copy_parse_nondep_then: pairs two parsers, uses jumper for p1
  to compute split point (replaces serializer-based split)
- zero_copy_parse_dtuple2: dependent pair, uses jumper
- read_and_zero_copy_parse_dtuple2: reads first, zero-copy second

Key differences from serializer-based versions:
- Permission halving: pts_to_parsed_intro gives pm /. 2.0R
- Jumpers replace serializer-computed split points
- No serializer dependency in types

No admit() or assume. Verified with make lowparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add zero_copy_parse combinators that port Low* accessor patterns
(accessor_fst, accessor_snd, etc.) to PulseParse using zero_copy_parse
from LowParse.PulseParse.Base.

Combinators (LowParse.PulseParse.Combinators.fst):
- zero_copy_parse_fst: extract first field of nondep_then
- zero_copy_parse_snd: extract second field of nondep_then
- zero_copy_parse_dtuple2_fst: extract first field of dtuple2

FLData (LowParse.PulseParse.FLData.fst):
- pts_to_parsed_fldata_payload_trade: ghost conversion between
  parse_fldata and underlying parser predicates
- zero_copy_parse_fldata: lift zero_copy_parse through parse_fldata

VLData (LowParse.PulseParse.VLData.fst):
- zero_copy_parse_bounded_vldata_payload': access payload of bounded
  vldata by reading length prefix, splitting slice, and delegating
  to the underlying zero_copy_parse
- zero_copy_parse_bounded_vldata_payload: convenience wrapper with
  default log

Sum (LowParse.PulseParse.Sum.fst):
- read_sum_tag: read the tag from a parsed sum value
- vmatch_sum_payload: vmatch type for sum payload access
- zero_copy_parse_sum_payload: access payload of a sum for a known
  tag by jumping past the tag and zero-copy parsing the case payload

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create share/everparse/tests/lowparse/LowParseExample3.Pulse.fst —
Pulse port of tests/lowparse/LowParseExample3, demonstrating:

- Spec-level type definition: t = { a: U16.t; b: U32.t; c: U16.t }
- Parser: parse_synth (nondep_then (nondep_then parse_u16 parse_u32) parse_u16) synth_t
- PulseParse leaf_readers via leaf_reader_of_serialized
- PulseParse validator via validate_synth + validate_nondep_then
- Test function 'dummy': validates input, then reads individual
  fields (a/b/c) by re-validating sub-parsers at known offsets
  and using read_parsed_from_validator_success

Also fix Makefile to include both lowparse spec and pulse paths.

No admit() or assume. Verified with make.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rewrite LowParseExample3.Pulse to use composed zero_copy_parse
accessors instead of re-validating sub-parsers at known offsets:

- access_a: zero_copy_parse_synth + zero_copy_parse_fst (outer) +
  zero_copy_parse_fst (inner) + zero_copy_parse_read
- access_b: zero_copy_parse_synth + zero_copy_parse_fst (outer) +
  zero_copy_parse_snd (inner) + zero_copy_parse_read
- access_c: zero_copy_parse_synth + zero_copy_parse_snd +
  zero_copy_parse_read

The dummy test function:
1. Validates with validate_t
2. Uses peek_trade_gen to get pts_to_parsed sub-slice
3. Calls the appropriate accessor (access_a/b/c)
4. Eliminates the trade chain (accessor trade -> peek trade)

Uses simple vmatch_a/b/c aliases (pure (x == v.field)) for clean
unfold at the call site.

No admit() or assume. Verified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace R.alloc/R.free with let mut for the validator offset
reference. Cleaner and avoids deprecation warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add extractable test and main functions:

- test(): creates an 8-byte array using let mut + array initializer
  syntax ([| 0uy ; 8sz |]), converts to slice with from_array, writes
  test bytes through slice indexing, calls dummy to validate and
  read fields, then converts back with to_array.

- main(): calls test(), returns EXIT_SUCCESS.

These are designed to be extracted to C and run as an executable test.

No admit() or assume. Verified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename LowParseExample3.Pulse.fst to LowParseExample3.fst (matching
the Low* naming convention, dropping the .Pulse suffix).

Update Makefile with extraction rules:
- verify target: F* verification via common.Makefile
- extract-all: extracts to C via KaRaMeL, compiles, and runs
- Uses -bundle and -no-prefix for clean C output
- FSTAR_DEP_OPTIONS with Pulse extraction settings

The test successfully extracts to C, compiles, and runs:
  make Example3  →  Example3/a.out runs and exits 0

Add .gitignore for build artifacts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tahina-pro and others added 26 commits March 29, 2026 17:25
- QD: Use validate_dsum_cases_fn/jump_dsum_cases_fn with
  dep_maybe_enum_destr_t_tac (handles both Known and Unknown branches
  via tactic) instead of validate_dsum_cases/jump_dsum_cases with
  dep_enum_destr_tac (Known only, Unknown handled by manual match
  that caused PCMReference.read leak during extraction)

- QD: Add inline_for_extraction to parse_cases definitions

- QD: Make synth_cases/synth_cases_recip/synth_known_cases noextract
  when -pulse is active (no SLow code generated)

- QD: Make -pulse automatically disable SLow (emit_high := false)

- QD: Suppress KaRaMeL warning 26 in extracted.Makefile

- PulseParse.Sum: Make f (parse_cases) argument Ghost.erased in
  validate_dsum_cases_t, validate_dsum_cases_if, validate_dsum_cases',
  validate_dsum_cases_dispatch, validate_dsum_cases'_destr,
  validate_dsum_cases, validate_dsum_cases_fn, and all jump_dsum_*
  counterparts

- PulseParse.Sum: Convert validate_dsum_cases' to Pulse fn with
  direct g' call in Unknown branch (no B.validate_synth wrapper)

- PulseParse.Combinators: Eta-expand validate_compose_context and
  jump_compose_context (matching Low* pattern)

- PulseParse.Combinators: Add inline_for_extraction to
  jump_compose_context

- Pulse.Base: Eta-expand validate_ext

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- QD: Generate Pulse leaf_reader for enum types using
  PPE.mk_read_enum_key / PPE.mk_read_maybe_enum_key + read_synth'
- QD: Generate Pulse leaf_reader for struct types using
  PPC.leaf_read_nondep_then composition + read_synth'
- QD: Move Pulse reader generation after validator generation
  to satisfy F* definition ordering requirements
- QD: Make synth_cases/synth_cases_recip/synth_known_cases noextract
  when -pulse (no SLow), add inline_for_extraction to parse_cases
- QD: Make -pulse disable SLow (emit_high := false)
- QD: Suppress KaRaMeL warning 26 in extracted.Makefile

Note: sum/dsum leaf_readers (read_sum, read_dsum) not yet implemented
in PulseParse - T38/T39/T40/T41/T15_body readers still Low*-only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add leaf_reader dispatch infrastructure for sum types:
- read_sum_cases_t: type alias for per-key leaf_reader
- read_sum_cases_t_if': Pulse fn for if-combinator dispatch
- read_sum_cases_t_if: if_combinator wrapper
- read_sum_cases': per-key reader using read_synth' + synth_sum_case
- read_sum_cases: dep_enum_destr-based dispatch

The full read_sum combinator (read tag + jump + dispatch) is not yet
implemented due to ghost ownership management complexity with
pts_to_parsed narrowing from parse_sum to parse_sum_cases'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Read a sum type value by:
1. Reading the tag with read_sum_tag
2. Getting the payload sub-slice with accessor_clens_sum_payload
3. Reading the raw case value with pc32 k
4. Trading back to restore original ownership
5. Applying synth_sum_case to produce the sum value

Also includes read_sum_cases infrastructure (read_sum_cases_t,
read_sum_cases_t_if, read_sum_cases', read_sum_cases) for
dep_enum_destr-based dispatch, though read_sum itself uses the
simpler accessor-based approach.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ssors

- Add read_dsum leaf_reader: uses accessor_dsum_tag to read tag,
  accessor_clens_dsum_payload to get payload sub-slice, then reads
  Known/Unknown case directly with f32/g32 and applies synth_dsum_case

- Make f parameter Ghost.erased in all dsum accessor combinators:
  accessor_dsum_tag, accessor_clens_dsum_payload,
  accessor_clens_dsum_payload', accessor_clens_dsum_unknown_payload,
  accessor_clens_dsum_unknown_payload',
  accessor_clens_dsum_cases_known_payload,
  accessor_clens_dsum_cases_unknown_payload

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- PulseParse.Combinators: add read_false() leaf_reader for parse_false
- QD: Generate Pulse read_cases for sum/dsum types
- QD: Generate Pulse read_sum/read_dsum calls for sum/dsum readers
- QD: Fix pulse_leaf_reader_name for Empty (PPC.leaf_read_empty)
  and Fail (PPC.read_false ())

All types verify and extract to .krml. C compilation has a KaRaMeL
issue with sum reader return type initialization in T39.c.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Generate PulseParse accessor tree using PPC.accessor_then_fst,
  PPC.accessor_then_snd, PPC.accessor_id, PPC.accessor_compose,
  PPC.accessor_ext, PPC.accessor_synth_inv
- Generate clens definitions using LowParse.CLens
- No gaccessor needed (PulseParse doesn't have it)

All 44 types verify, extract to C, compile, link, and test passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Generate Pulse accessor for vlgen payload types using
PPVG.accessor_vlgen_payload with jumper and leaf_reader
for the length header.

All 44 types verify, extract, compile, link, and test passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- PulseParse.Combinators: add clens_synth_fwd and accessor_synth_fwd
  for forward synth direction (p → parse_synth p f)
- QD: Generate Pulse tag accessors using PPS.accessor_sum_tag/dsum_tag
  composed with accessor_synth_fwd for the user-defined tag type
- QD: Generate Pulse payload accessors using
  PPS.accessor_clens_sum_payload/dsum_payload with accessor_ext
- QD: Generate Pulse Unknown payload accessors using
  PPS.accessor_clens_dsum_unknown_payload with accessor_ext
- QD: Generate clens definitions using LowParse.CLens for Pulse

All 44 types verify, extract, compile, link, and test passes.
Low* quackyducky tests also pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Write a non-dependent pair by writing the first component then the
second. Proves the Seq.slice composition postcondition using
slice_slice, lemma_split, and lemma_eq_intro.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- LowParse.Pulse.Base: extract l2r_leaf_writer postcondition into
  l2r_leaf_writer_postcond prop; add l2r_leaf_writer_ext_squash variant
  taking parser equivalence as separate squash argument

- LowParse.Pulse.Combinators: add l2r_leaf_write_enum_key,
  l2r_leaf_write_maybe_enum_key (enum writers using destructor);
  l2r_leaf_write_sum_cases_t/_aux/_cases (sum case dispatcher);
  l2r_leaf_write_false (unreachable type writer)

- LowParse.Pulse.Sum (new): l2r_leaf_write_sum and l2r_leaf_write_dsum
  Pulse fn writers that sequentially write tag then payload, with
  squash lemmas for precondition/postcondition bridging;
  l2r_leaf_write_dsum_type_of_tag/_cases_aux/_cases (dsum helpers)

- QD codegen (rfc_fstar_compiler.ml): generate Pulse writers wherever
  Low* generates lserializers - enum writers (l2r_leaf_write_enum_key +
  l2r_leaf_write_synth), struct writers (l2r_leaf_write_nondep_then +
  l2r_leaf_write_synth), sum writers (l2r_leaf_write_sum with
  dep_enum_destr_tac), dsum writers (l2r_leaf_write_dsum with
  dep_enum_destr_tac), type alias writers; add pulse_leaf_writer_name
  mapping, write_cases table generation, LPPS module alias

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mark implicit parser_kind arguments as Ghost.erased in
PulseParse.Bytes, PulseParse.Combinators, PulseParse.VCList,
and PulseParse.VLGen to avoid top-level ghost effects (Warning 272)
that prevent F* extraction to .krml.

Add --warn_error '+272' to extracted.Makefile to catch
top-level effect issues early.

bitcoin.rfc types now fully verify, extract, compile, link, and run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tahina-pro tahina-pro force-pushed the _taramana_pulse_parse branch from 5e89c6b to 6de4df8 Compare April 3, 2026 02:48
@tahina-pro
Copy link
Copy Markdown
Member Author

Some accessors are missing: array_nth and the like

@tahina-pro tahina-pro marked this pull request as ready for review April 3, 2026 05:22
@tahina-pro tahina-pro merged commit eb66348 into project-everest:master Apr 3, 2026
37 of 41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant