Skip to content

Port 3D from Low\* to Pulse (--pulse) - #318

Open
tahina-pro wants to merge 104 commits into
project-everest:masterfrom
tahina-pro:_taramana_3d_pulse
Open

Port 3D from Low\* to Pulse (--pulse)#318
tahina-pro wants to merge 104 commits into
project-everest:masterfrom
tahina-pro:_taramana_3d_pulse

Conversation

@tahina-pro

Copy link
Copy Markdown
Member

This branch adds a second, complete code-generation backend to 3D, built on
Pulse instead of Low*, selected by a new --pulse flag. The Low* backend is
untouched and remains the default: every change is either additive or guarded
by Options.get_pulse ().

The port is functionally complete. The new runtime has no admits, the
generated C compiles under -Wall -Wextra -Werror, and a full parallel copy of
the 3D test corpus builds, links and runs against it.

299 files changed, ~27,000 insertions
  lib/everparse/3d       11,234   the Pulse combinator library (20 modules)
  share/everparse/tests  10,301   the --pulse test corpus (37 .3d + 18 subdirs)
  src/3d                  4,412   frontend: --pulse emission, wrappers, Z3TestGen
  src/lowparse              607   ArrayPtr readers, read_u64_le
  .github/workflows         377   --pulse coverage in package/nightly/release CI

What is here

1. lib/everparse/3d — the Pulse combinator library

A Pulse counterpart to src/3d/prelude: Prelude, Kinds, ErrorCode,
InputStream.{Base,Buffer,Extern,Static}, CopyBuffer, AppCtxt,
Actions.{Base,Common}, ProbeActions, State, and Interpreter.

Three structural departures from Low*:

Memory locations and invariants become extra_state. Low* threads
loc/inv/liveness through every validator and action. Pulse replaces all
of it with a state dictionary (EverParse3d.State): a keyed product of
resources, where disjointness is a syntactic property of distinct string keys
rather than an SMT obligation. Pulse.Lib.ForEvery carries the pointwise
ownership, and the call combinators rename keys at call boundaries. Almost
all of the reasoning discharges by normalization instead of Z3.

One input buffer becomes three arguments. input_stream_inst base_t len_t pos_t, with pts_to : base_t -> len_t -> pos_t -> Seq.seq U8.t -> Seq.seq U8.t -> slprop. The buffer instance uses Pulse.Lib.ArrayPtr.ptr U8.t,
SZ.t and ref SZ.t; extern and static instantiate the unused ones to
unit, and KaRaMeL erases those arguments.

Backend-conditional actions without a backend flag. Rather than a flag,
the interpreter takes option field_ptr_t, option field_ptr_after_t etc.,
and Action_field_ptr carries a squash (Some? ...). The frontend supplies
the instance, so field_ptr stays buffer-only and field_ptr_after
extern/static-only, exactly as in Low*, with no linking or --include tricks.

The library is extracted to .krml (lib/everparse/3d/krml) and bundled into
the generated C, rather than shipped as a separate C library the way the Low*
prelude is.

2. src/3d — the frontend

--pulse switches F* emission, the --include path, the KaRaMeL invocation
and the C wrapper. Notable points:

  • Wrappers keep their existing argument types, with _Static_asserts that
    size_t is at least as wide as uint32_t and uint64_t.
  • Error handler macros: buffer, extern and static each own a
    [@@CMacro] assume val error_handler_macro, and the frontend instantiates
    validate_with_error_handler accordingly.
  • Frontend checks reject probes for extern/static, and reject nested
    probes that would reuse one copy buffer.
  • Z3 test-case generation works under --pulse, in both harness modes,
    including probes. The SMT half is backend-independent — it runs on the
    InterpreterTarget AST — and produces byte-identical witnesses and verdicts
    on both backends; only the emitted C harness differs.
  • Generated parser kinds are reduced to literals under --pulse
    (norm [delta_namespace ["EverParse3d"; "LowParse"]; ...]). Without this,
    and_then_kind/glb nests unfold exponentially during extraction:
    FieldDependence0.3d went from OOM at 166 GB to 57 ms.

3. share/everparse/tests/3d — the test corpus

A full copy of src/3d/tests adapted to --pulse; src/3d/tests is
unchanged. 37 top-level .3d files (10 negative) plus 18 subdirectories, all
of which generate, verify, extract, compile and run.

4. Packaging and CI

package.sh ships lib/everparse/3d (sources, .checked and pre-extracted
.krml) plus lib/pulse. Every binary-package test job in
package-{linux,macos,windows}.yml, nightly.yml and release.yml gained a
--pulse step, as did the cross-platform hash-interop jobs and the Z3
test-generation steps.


Notable fixes along the way

Several of these are pre-existing bugs the port exposed rather than caused:

  • Z3TestGen argument swapMain.fst passed
    (print_c_initializers, use_ptr) to do_test/do_diff_test, which declare
    (use_ptr, print_c_initializer); do_diff_test had a second, compensating
    swap. Both are bool, so it typechecked silently, and every existing test
    set the two flags to the same value. a1b6f3df6.
  • Copy-buffer reuse — a buffer reused at a second probe site was validated
    from wherever the first probe left it. Fixed with a reset method on the
    copy_buffer class. No test outside specialize_* would have caught it.
  • make clean gapskrml/extract.Makefile listed clean-extracted in
    clean_rules without defining it; tests/3d/iter/Makefile had no clean at
    all (its Low* original has the same gap); and the subdirectory clean loop ran
    without set -e, swallowing both. 68909c201.
  • Makefile dependency bug — hand-written .o rules listed the generated
    wrapper header but not EverParse.h. Harmless under Low*, where it is
    copied at the start of the pipeline, but under --pulse KaRaMeL emits it at
    the end, so parallel builds raced. 4f409e1cc.

Remaining work to reach near-parity with Low*

Nothing below blocks the branch; none of it is a correctness regression
against the Low* backend.

Correctness coverage

  1. validate_t_exact on a non-buffer stream is untested. Under extern,
    stream_truncate returns (base_t & unit & unit), and how KaRaMeL renders
    that struct is unverified — no subdirectory exercises it. This is the only
    genuine unknown left, as opposed to polish.
  2. The standalone F# hash checker has never run against --pulse output.
    Low*'s three hashchk-* targets have no Pulse counterpart.

Feature gaps

  1. Compile-time comments are not extracted. act_with_comment carries a
    TODO at three sites in Actions.Base.fst; T_with_comment and the
    validate_filter strings are dropped, so --pulse C is cosmetically poorer
    than Low*'s. Needs Pulse-side support.
  2. 3d-doc-test has no --pulse snapshot. doc/3d-snapshot/ is
    regenerated and diffed for Low* only. Best done after (3), to avoid
    re-baselining.
  3. The --pulse client ABI is undocumented. doc/3d-lang.rst gives only
    the Low* copy-buffer hooks; under --pulse a client must also define
    EverParseStreamPos (returning size_t *), and EverParseStreamLen
    returns size_t. The flag itself and the (base, len, pos)
    validator/error-handler ABI are undocumented.

Verification debt

  1. Four TotGTot TODOs on the erased bijection argument
    (State.fst:785, Actions.Base.fst:{741,1285}, .fsti:{343,485}).
    Erasure hygiene only; needs ghost bijections to exist first.
  2. Actions.Base.fst:284 — an if/else that would read better as a
    non-terminal early exit with an ensures.

Accepted differences (by design, not oversight)

  • static and extern are byte-identical at the C level. KaRaMeL emits
    plain extern for every assumed val, so Low*'s static inline prelude trick
    is unreproducible. They remain separate F* modules only because each owns its
    error_handler_macro. Consequently field_ptr_after is allowed for both,
    where Low* restricts it to extern.
  • Probes are rejected for --input_stream extern|static, as agreed.
  • Z3TestGen reports the current position to the error handler where Low*
    reports each field's start position. Verdicts and witnesses agree; only the
    position column in the trace comments differs.
  • Copy buffers carry their own position. Low* passes the start position to
    the validator as an argument; in Pulse it lives in the stream, hence the extra
    EverParseStreamPos hook and the reset method.

Trust footprint

lib/everparse/3d has no admits. The deliberate assume vals mirror
Low*'s one-for-one: the input-stream primitives per backend, the probe
functions, the [@@CMacro] error_handler_macros, and the copy-buffer handle
with its three projections. That last one is exactly Low*'s bargain —
stream_of is assumed and is not really a function of its argument; Low* pays
for it in liveness, Pulse in ownership, and in both the fresh region is
admitted by fiat inside the already-assumed probe functions.


Testing

  • make 3d-pulse-prelude — verifies the library (20 modules).
  • make 3d-pulse-krml — extracts it (148 .krml).
  • make 3d-pulse-test — the full corpus: generation, F* verification, KaRaMeL
    extraction, C compilation under -Wall -Wextra -Werror, and execution.
    Wired into make test, and parallel-safe (-j16, ~11 min from clean).
  • Low* regressions (src/3d/tests, including z3-testgen-probe-test) were
    re-run and are unaffected.

tahina-pro and others added 19 commits August 23, 2026 16:17
…reuse

Ports funptr, ifdefs, iter, tcpip, goto_return and the four specialize
tests to --pulse. All fifteen ported subdirectories now build, link and
run as part of the pulse suite.

The specialize tests exposed a real bug. Low* probe_then_validate calls
the validator for the probed type at position 0uL, because there the
position is an argument rather than part of the buffer. The Pulse port
passed the copy buffer position ref instead, so a copy buffer reused at a
second probe site was validated from wherever the first probe left it, and
the second validation reported not enough data. The copy_buffer class
gains a reset method, and probe_then_validate rewinds before validating.

The other adaptations follow from ABI changes already made:

- A copy buffer is now the buffer backend input buffer passed by value,
  so clients read cb_base and cb_len instead of casting to a descriptor
  of their own, and supply a position cell.

- The extern and static primitives no longer take the application
  context, so funptr moves its vtable into the stream object. The test is
  unchanged in substance: no stream operation is resolved at link time.

- The goto_return snapshot is regenerated for the three input buffer
  arguments and the uint8_t status code.

Two subdirectories stay out. probe and probe_error_handler_macro build
the z3 test-case generator, which still uses the Low* interpreter, and
their ProbeInPlace callback repoints the copy buffer at the source
instead of copying into it, which a by-value copy buffer cannot express.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This definition failed to verify at the rlimit of 16 granted by the
enclosing push-options block, which broke the Low* 3d prelude and hence
the whole Low* test suite. It verifies deterministically at 128.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two optimizations present in the Low* prelude had no Pulse counterpart.

validate_nlist_total_constant_size{,_mod_ok,_mod_ko}: when the payload has
total constant size, the whole list does too, so validating it is a single
bounds check rather than an element loop. validate_nlist_constant_size_
without_actions accepted payload_is_constant_size but ignored it.

validate_dep_pair_with_refinement{,_and_action}_total_zero_parser: when the
tag parser is total and consumes no input it can neither fail nor consume, so
the tag validator is skipped and the leaf reader runs directly. Here too the
p1_is_constant_size_without_actions argument was accepted and ignored.

The Pulse interpreter now passes not ha1 for that flag, where ha1 is the
has_action index of the tag dtyp: that is exactly the without-actions
criterion, and it is known at specialization time. The Low* interpreter
hard-codes false there, so the optimization was unreachable on its
interpreted path.

UINT8 f1[10] in TestFieldPtr.3d now generates a single bounds check with no
loop. The full share/everparse/tests/3d suite still passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
package.sh now copies everything 3d.exe --pulse looks up relative to
EVERPARSE_HOME: lib/everparse/3d (sources and .checked), its extracted
.krml files, the Pulse stdlib, and the EverParsePulse headers. The
Pulse stdlib copy moves out of the EVERPARSE_ONLY_3D block so that the
3d-only release package supports --pulse too. The root Makefile adds
3d-pulse-krml to the packaged subset, guarded by NO_PULSE.

Validated against a simulated package tree with PULSE_HOME, KRML_EXE,
FSTAR_EXE and EVERPARSE_HOME unset: all paths resolve inside the
package and the generated C compiles.

Adds a Test EverParse (--pulse) step to all six binary-package test
jobs. Inputs come from share/everparse/tests/3d so that
--check_hashes inplace does not clobber the hashes the Low* step just
wrote. The --z3_test steps get no --pulse counterpart, since Z3TestGen
still uses the Low* interpreter.

FieldDependence0.3d is deliberately omitted from the --pulse steps: it
verifies fine but fstar --codegen krml on the generated module runs
away past 32GB. It is excluded from positive_tests in both suites, so
this is a pre-existing gap rather than a regression.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous commit covered package-{linux,macos,windows}.yml and the
three release.yml test jobs, but missed nightly.yml, which has the same
three binary-package test jobs, and the cross-platform hash interop
jobs in both nightly.yml and release.yml.

Each test job now also uploads everparse-test-pulse-<os>, holding the
--pulse Comments*, and each interop job downloads it into <os>-pulse/
and re-runs --pulse --check_hashes inplace there. A separate artifact
is needed because both files are called Comments.3d: putting them in
one artifact would move the archive root and break the existing Low*
interop steps.

nightly-fstar2.yml, ci.yml, ci-macos.yml and cbor.yml need no change:
they run the in-tree make test, which already reaches 3d-pulse-test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
FieldDependence0.3d generated and verified fine under --pulse, but
"fstar.exe --codegen krml" on the generated module never finished: it
reached 166 GB RSS after 50 minutes, and died with "allocation failure
during minor GC" under a 32 GB cap.

Root cause: the generated "kind_X" definitions are left as an unreduced
nest of "and_then_kind" / "glb" applications. Those two functions build a
parser_kind record whose four fields each mention their arguments, so
"and_then_kind k1 k2" mentions k2 six times and k1 four times. They are
"inline_for_extraction", so F* extraction unfolds the whole nest, and the
term grows exponentially with its depth. That is why the blowup needed
both a casetype mixing strong-prefix and weak cases (deep "glb" nest) and
preceding struct fields (deep "and_then_kind" nest): a bisection over
(number of cases) x (number of preceding fields) shows the cost is
multiplicative in the two, while all-strong-kind casetypes and plain
field chains of any depth stay flat.

Fix: wrap the kind expression in a "norm" that reduces the nest to a
literal record. Extraction of FieldDependence0.validate__s2 goes from
129 s (then OOM) to 57 ms, and the whole file now builds in 20 s / 1.2 GB.

The normalization is restricted to the EverParse3d and LowParse
namespaces. A plain "delta" also works for FieldDependence0 but changes
which branch of validate_nlist_constant_size_without_actions is selected
for ELF.3d, leaving a reference to Pulse.Lib.Array.Core.mk_carrier that
KaRaMeL cannot compile.

Only the --pulse output changes; the Low* generated F* is byte-identical.
FieldDependence0.3d is therefore re-enabled in the --pulse steps of the
binary-package tests in all five workflows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Pulse test suite excluded only FineGrainedProbe, Specialize2,
Specialize3 and Specialize5 on Windows, but src/3d/tests also excludes
FineGrainedProbeSpecialize and Specialize6 for the same reason (UlongToPtr
is defined in Windows DLLs and conflicts with those examples).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ports the remaining src/3d/tests scenarios that had no --pulse counterpart:

- elf-test: build and link the C++ TestELF.cpp driver against the
  --pulse-generated ELF parser (not run, as in src/3d/tests: it needs an
  ELF file to read).
- pulse-cc now goes through the Makefile.basic that KaRaMeL emits, so the
  generated C is held to the same -Wall -Wextra -Werror standard as the
  Low* suite, and links and *runs* test.cpp. This is the first time a
  --pulse-generated validator is executed by the test suite.
- pulse-cleanup-test: --cleanup --skip_c_makefiles must leave behind
  exactly the C artifacts. Compared to Low*, the expected set also has
  EverParse.c, EverParse.h and internal/, because the Pulse backend
  extracts EverParseErrorReasonOfResult as a real function rather than a
  static inline in the hand-written prelude header.
- pulse-inplace-hash-test: --check_hashes inplace and --check_inplace_hash
  on Comments.3d.
- pulse-batch now saves strong hashes and re-checks them, as batch-test
  does for the Low* backend.

Also factors the Windows exclusions into an exclude_modules list, so that
the cleanup expected-file list drops the same modules.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Main.fst passed (print_c_initializers, use_ptr) to Z3TestGen.do_test and
do_diff_test, but both callees declare their parameters in the order
(use_ptr, print_c_initializer). Z3TestGen.do_diff_test contained a second,
compensating swap when forwarding to do_diff_test_for.

Net effect: do_test was wholly wrong, while do_diff_test was correct except
for the cout_test_probe_functions call, which received the wrong flag. Both
parameters are bool, so the mistake typechecked silently.

The bug was masked because every existing test sets both flags to the same
value: src/3d/tests/probe/src passes --z3_use_ptr without
--z3_skip_c_initializers (both true), and the ELF z3-testgen test passes
neither (both false). Any single-flag or mixed-flag invocation misbehaved:
--z3_use_ptr --z3_skip_c_initializers silently fell back to array-index
probe mode and also ignored --z3_skip_c_initializers.

Verified: --z3_use_ptr --z3_skip_c_initializers on Probe.3d now emits the
pointer-mode copy_buffer_t typedef together with the pointer-patching
statements and no designated initializers in the echoed witness. The
existing probe and z3-testgen tests are unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
--z3_test, --z3_diff_test and --test_checker now work under --pulse.

The SMT half needed no change: Z3TestGen.produce_decls consumes the
InterpreterTarget AST, which is backend-independent. Only the emitted C
harness was Low*-specific. Bridge the ABI differences:

  - Pulse validators return a uint8_t status (0 on success) and report the
    consumed length through a size_t* position, rather than returning the
    consumed length or an error code.
  - Their error handler takes the input as a (base, len, pos) triple instead
    of an EVERPARSE_INPUT_BUFFER, and error codes are uint8_t.
  - EverParseIsError and EVERPARSE_INPUT_BUFFER do not exist under --pulse.
  - KaRaMeL only declares EVERPARSE_COPY_BUFFER_T when the module actually
    uses it, so cout_test_probe_functions must emit nothing under --pulse.
    This is what made probe-free modules fail to compile.

Reject two combinations rather than emitting C that does not compile:
--z3_use_ptr with --pulse (rejected in Main.go before any file is written),
and parsers taking a copy buffer. Both have the same root cause: the Pulse
EVERPARSE_COPY_BUFFER_T is passed by value, so a probe callback cannot
repoint it, which is exactly what pointer mode relies on.

To make the second rejection abort cleanly, produce_z3_and_test_gen gained a
pre_check parameter, run on the main thread before with_z3_thread_or spawns
the Z3 worker. A failure raised from that worker printed its message but let
the rest of the batch keep running, so the build failed later at link time
with an unrelated error.

Verified on ELFTestGen._ELFTestGen with --z3_witnesses 10 --z3_branch_depth 5:
both backends produce byte-identical witnesses and an identical sequence of
170 ACCEPTED/REJECTED verdicts. The harness compiles under -Wall -Wextra
-Werror -std=c11 and runs. --test_checker likewise agrees on verdict and exit
code across backends.

Reported positions do differ: Low* passes each field's start position to the
error handler, whereas Pulse has no such parameter and reports the current
position. The harness matches what the generated *Wrapper.c error handler
already does.

Add pulse-z3-testgen-test to share/everparse/tests/3d, mirroring the Low*
z3-testgen-test. There is no counterpart to z3-testgen-probe-test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A Pulse copy buffer was the buffer backend's input buffer itself, a
by-value (base, len, pos) triple whose components were pure projections of
an immutable handle. Neither the C ABI nor the spec could then express a
probe callback repointing the buffer at the probed bytes, which is what
in-place probing does in Low*, where copy_buffer_t is an abstract type
behind a void* and stream_of is a spec-only projection.

Put one reference between the handle and the triple, above the input
stream class so that probe_then_validate still runs the same validator,
with the same input_stream_inst, on the copy buffer. The input stream
specs and instances are untouched.

EverParse3d.CopyBuffer: the class now carries a descriptor type and its
three projections, plus a descr handle -> ref descr_t. pts_to quantifies
the descriptor existentially, so a probe function's postcondition admits
a new one without its signature changing, and the caller never learns
which region the buffer points at. The descriptor type is a class field
rather than a record here so that each backend picks a monomorphic one:
a polymorphic record is monomorphized by KaRaMeL under a mangled name,
emitted into whichever generated module uses it first.

EverParse3d.InputStream.Buffer: copy_buffer_descr is that record, and
copy_buffer_t a ref to it. KaRaMeL emits EVERPARSE_COPY_BUFFER_DESCR into
EverParse.h and inlines the abbreviation, so EverParsePulseEndianness.h
defines EVERPARSE_COPY_BUFFER_T as a pointer to it. Test clients that used
to build a copy buffer by value now allocate a descriptor and pass its
address; the ones that include EverParseEndianness.h directly now go
through EverParsePulseEndianness.h, so the legacy void* typedef stays
suppressed whatever the include order.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Now that EVERPARSE_COPY_BUFFER_T is an indirection, a probe callback can
repoint it, so --pulse no longer has to reject probes in z3-testgen.

Z3TestGen: add --pulse variants of the probe harness.  The harness
copy_buffer_t embeds an EVERPARSE_COPY_BUFFER_DESCR as its first member so
that callbacks can cast the copy buffer back to it; EverParseStreamOf and
EverParseStreamLen are no longer emitted, since the validator reads
cb_base/cb_len/cb_pos directly.  The definitions are only emitted for modules
that actually have probes, because KaRaMeL only puts the descriptor type in
EverParse.h for those.  Drop check_pulse_supported, the pre_check parameter
of produce_z3_and_test_gen, and the --z3_use_ptr rejection in Main.

Target: the probe wrapper reads probeDest->cb_base under --pulse instead of
calling EverParseStreamOf.

tests: port share/everparse/tests/3d/probe and probe_error_handler_macro to
--pulse, and add a pulse-z3-testgen-probe-test target covering both the
default and the --z3_use_ptr harness modes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the reference-to-descriptor indirection with the Low* shape: an
abstract copy_buffer_t plus assumed projections that the client implements
in C.  The earlier objection to this -- that pure projections cannot express
a repointing probe -- was wrong: the pre and post of a probe differ in the
stream contents, so a probe does change the slprop.  What had actually
broken was purely a C-level problem, namely that EVERPARSE_COPY_BUFFER_T was
a by-value struct that a callee could not repoint.  An opaque void* handle
can be repointed, and EverParseStreamOf then returns the new pointer, which
is exactly how the Low* backend behaves.

The copy_buffer class loses descr_t, descr and the existential in pts_to,
keeping only base_of/len_of/pos_of and reset.  The buffer backend assumes
the type and the three projections, named so that the EverParse bundle
renames them to EverParseStreamOf, EverParseStreamLen and EverParseStreamPos.
The first two are the Low* hooks verbatim, modulo EverParseStreamLen
returning size_t since Pulse stream lengths are SZ.t; the third has no Low*
counterpart and returns the position cell, which Low* validators instead
take as an argument.

Consequently EVERPARSE_COPY_BUFFER_T is void* again, straight from
EverParseEndianness.h, so EverParsePulseEndianness.h no longer redefines it
-- which removes the include-order conflict that had forced six hand-written
test stream headers to change includes.  It does republish the three
prototypes, because KaRaMeL emits them into internal/EverParse.h rather than
the public header, and without them a client's definitions would not be
signature-checked.  Target and Z3TestGen drop their --pulse special cases
for probes, and probe_then_validate and the ProbeActions error handler lose
their reference bookkeeping.

Client C code is now source-compatible with Low* apart from adding
EverParseStreamPos and widening EverParseStreamLen to size_t; the probe and
specialize test clients are updated accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The subdirectory Makefiles were copied from src/3d/tests, where a rule that
compiles a hand-written .c file lists the generated wrapper header it
includes, but not EverParse.h.  Under the Low* backend that is harmless:
EverParse.h is copied from the prelude by the same copy_everparse_h step that
produces EverParseEndianness.h, at the very start of the pipeline, so by the
time any wrapper header exists it is already there.

Under --pulse it is not copied at all.  KaRaMeL emits it, carrying the
bundled runtime, at the same time as the generated .c files -- that is, at
the very end.  The prerequisite list is then genuinely wrong, and a parallel
build compiles main.c as soon as the wrapper header appears, failing with
"EverParse.h: No such file or directory".  A serial build only masked this by
accident of ordering.

The generated EverParse.Makefile already models this correctly; only the
hand-written rules were missing it.  With them fixed, the whole suite passes
from clean under -j16, in about a third of the serial time.

Also drops a comment that still described copy buffers as an indirection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Pulse suite copied src/3d/tests' exclusion of FieldDependence0.3d and
ActAndCheck.3d from positive_tests, but not the second batch target that
covers them, so ActAndCheck.3d was exercised nowhere.

That split is historical. 3D used to have two translation backends,
Translate.fst (specialized) and TranslateForInterpreter.fst, and :act/:check
were only ever supported by the latter, so ActAndCheck.3d was excluded from
positive_tests by the very commit that added it (7b2764b). Translate.fst is
long gone -- Main.fst only calls TranslateForInterpreter.translate_decls -- so
batch-test and batch-interpret-test now drive the same pipeline and differ
only in which files they feed it.

Mirror it anyway, so this Makefile stays diffable against src/3d/tests and so
that pulse-cleanup-test keeps asserting an exact output file list over the
narrower set. The new target needs its own output directory, because
EverParse.h is generated per batch and carries that batch's external
typedefs. Unlike the Low* version it needs neither --no_copy_everparse_h nor a
copy of the auxiliary headers: -I $(CURDIR) already finds them.

interpretable_tests also drops exclude_modules, which INTERPRETABLE_FILES
fails to do; those modules do not build on Windows for a reason unrelated to
which files a target selects.

Both extra modules generate, verify, extract, compile and link under --pulse
with no change to the prelude or the frontend.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the --pulse package tests were added, the --z3_test steps were
deliberately left without a counterpart, because Z3TestGen only emitted a
Low* harness. That stopped being true once it learned to emit a Pulse one and
then to handle probes, but the workflows were never revisited, so nine
ELFTestGen steps and three probe steps stayed Low*-only across all five
workflows.

This is worth covering rather than assuming it follows from the in-tree
suite: the emitted harness is where the two backends diverge most. Pulse
validators return a uint8_t status and report the consumed length through a
size_t* position, and the probe harness additionally defines the copy-buffer
hooks, including EverParseStreamPos, which has no Low* counterpart. None of
that C is compiled by any other packaged test.

Each new step mirrors its Low* neighbour exactly, taking its input from
share/everparse/tests/3d rather than src/3d/tests, as the other --pulse steps
already do, and writing to its own --odir so the two runs cannot collide.
All nine affected jobs already sparse-checkout the Pulse corpus. Both
commands were run locally against the source tree first.

The probe steps remain Windows-only, as they have been since they were
introduced; this change does not widen their platform coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The C generated by `3d --pulse` is self-contained: the Pulse runtime is
bundled into EverParse.c/EverParse.h, and nothing refers to krmllib. But
KaRaMeL's misc/Makefile.basic unconditionally puts krmllib's headers on the
include path and links every executable against libkrmllib.a, which a KaRaMeL
built with LOWSTAR=false does not even produce -- so linking the z3 test-case
generation harness failed.

Rather than patch KaRaMeL, ship our own:

- share/everparse/3d/Makefile.basic, a standalone variant with no krmllib
  dependency. Since it is used without a Makefile.include, it discovers the
  generated C files with a wildcard over the output directory.
- Under --pulse, invoke KaRaMeL with -skip-makefiles unconditionally, so
  neither Makefile.basic nor Makefile.include is emitted (making
  --skip_c_makefiles a no-op as far as KaRaMeL is concerned), and drive the
  C build through EverParse's copy instead, both from 3d.exe itself
  (build_test_exe) and from the --pulse test Makefiles.
- Install it in the binary package alongside the rest of the Pulse runtime.

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

Neither tree was reachable from the top-level clean. clean-3d cleans the Low*
prelude under src/3d/prelude, and clean-3d-tests the Low* corpus under
src/3d/tests, so the Pulse prelude's .checked and .krml files and the whole
Pulse test corpus survived a full clean.

Three latent bugs surfaced while wiring this up.

krml/extract.Makefile added clean-extracted to clean_rules but never defined
it, so `make -f extract.Makefile clean` died with "No rule to make target
clean-extracted". It had presumably never been run; krml/Makefile carried its
own `rm -rf extracted` instead, which is why nobody noticed. Define the rule
and have krml/Makefile delegate rather than duplicate it. It has to remove
the directory, not just its .krml files as common.Makefile's clean-krml does,
because the F* dep file lives inside it.

share/everparse/tests/3d/iter/Makefile had no clean at all -- it only
recurses into coarse/ and fine/ for the default target -- so both obj/
directories survived. Its Low* original has the same gap.

And the loop that cleans the subdirectories ran without set -e, so that
failure was swallowed and the loop carried on. Add the guard, so the next
missing clean is loud rather than silent.

Verified by cleaning both trees to a state where git status --ignored is
empty, checking the targets are idempotent, and rebuilding the prelude from
scratch afterwards: 20 .checked and 148 .krml, as before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tahina-pro
tahina-pro requested a review from nikswamy August 28, 2026 01:06
Comment thread opt/Makefile Outdated
tahina-pro and others added 8 commits August 27, 2026 22:38
Under --pulse, KaRaMeL was emitting an EverParse.c holding the handful of
runtime helpers that survive specialization, which the generated Makefile
then had to compile and link. The Low* backend has no such file: it ships a
self-contained EverParse.h whose helpers are all static inline.

Extend -static-header to cover the two runtime modules that actually
contribute those bodies, mirroring the Low* list minus EverParse3d.CopyBuffer
and EverParse3d.InputStream.*. Those two hold the assume vals the client
implements in C; Low* can list them because it also passes -library, which
drops their declarations outright, whereas here KaRaMeL would emit them as
static prototypes that never get a definition and clash with the client's
non-static one.

That alone left one resident behind. EverParse3d.Actions.Base re-exports
is_range_okay as a bare alias so the frontend can emit the unqualified name;
F* extracts an alias as a global holding a function reference, and KaRaMeL
emits it as a function pointer variable. So every is_range_okay call in
generated code was an indirect call through a global, where Low* has a direct
call to a static inline. Marking the alias inline_for_extraction removes the
global and the indirection.

With no EverParse.c left, drop the rules that produced and compiled it and
the link lines that referenced it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9280194c-7c30-4916-8e1c-9b786e884b25
Under --pulse the KaRaMeL bundle listed a single API module,
EverParse3d.Actions.Common. Everything else in the bundle was therefore
private, and any of it that survived as a cross-translation-unit symbol was
raised to internal/EverParse.h -- a header client code is not meant to
include. That covered the ErrorCode helpers, the StaticHeader bitfield
accessors, the copy-buffer projections and, in extern/static mode, the
input-stream hooks, all of which the Low* prelude declares in EverParse.h.
It also left EverParseErrorReasonOfResult non-public, unlike in Low*.

Widen the API list so those modules are public. Two constraints shape it:

- Bundle APIs are per-module, so a module can only be listed if all of its
  extracted declarations belong in the public ABI. EverParse3d.InputStream.
  Buffer does not qualify: its stream helpers are inline_for_extraction but
  not noextract, and going public forces KaRaMeL to materialise each of them
  as a real function, resurrecting EverParse.c. Move the four assumed
  copy-buffer declarations to a new leaf module, EverParse3d.CopyBuffer.
  Buffer, which the API list can name on its own; InputStream.Buffer
  re-exports them so existing uses are unchanged. For the same reason, mark
  field_ptr_after_with_setter_impl noextract in InputStream.Extern.

- Each of the Buffer, Extern and Static backends owns a [@@cmacro]
  error_handler_macro, and they only coexist because the unselected ones are
  private. Making two public at once collides on
  EVERPARSE_ERROR_HANDLER_MACRO (KaRaMeL warning 23), so only the selected
  backend is listed. call_krml now takes the input stream binding to decide.
  Static has no extracted declarations of its own and needs nothing public.

EverParsePulseEndianness.h re-declared the three copy-buffer projections as a
workaround for their living in internal/. That workaround is now not just
redundant but wrong: the duplicate sits outside the extern "C" guard, so C++
clients see conflicting linkage against the generated EverParse.h. Drop it.

internal/ still holds one header, for the monomorphized (base, len, pos)
triple that stream_truncate returns; KaRaMeL places monomorphized instances
in the generated module that uses them first, which no bundle can reach.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9280194c-7c30-4916-8e1c-9b786e884b25
…lued

input_stream_inst.truncate returned a (base_t & len_t & pos_t) triple. That
extracts to a C struct, and KaRaMeL assigns a monomorphized instance to
whichever *generated* module uses it first -- a client module no bundle can
reach -- so once a second generated module used it too it had to be shared
through internal/. That was the one remaining difference between the file set
--pulse emits and the Low* one.

Neither backend actually needs all three components: truncating a byte array
leaves the base pointer and the position cell alone and only shortens the
length, while an extern stream keeps its length and position on the far side
of the C boundary and returns a fresh base handle. So give the class an
associated type trunc_t naming the single component the backend modifies,
plus trunc_base/trunc_len/trunc_pos to recover the other two from the
original stream. Buffer sets trunc_t = len_t, extern sets trunc_t = base_t.

Both backends now extract to a scalar-returning function, no struct is ever
built, and internal/ disappears. The extern ABI is unchanged --
EverParseStreamTruncate still returns EVERPARSE_INPUT_STREAM_BASE -- and the
buffer call sites get strictly simpler C: three plain locals in place of a
struct literal that was immediately projected three ways.

The projections are ordinary Tot functions, so the three callers in
Actions.Base keep passing a single value around and no proof structure
changes; they just bind tb/tl/tp and rewrite. The rewrites have to name the
record projector (inst.trunc_base) rather than the typeclass method, since
Pulse frame inference matches syntactically and does not delta-reduce.

An alternative was to pass three ref out-parameters. That works too -- krml
erases ref unit arguments and collapses the let-mut locals when the writer is
inlined. It was rejected for the extern backend: there the writer is an assumed
val and is not inlined, so the refs become real stack slots whose addresses are
passed, turning a scalar return into an out-parameter and breaking the C ABI.
It also adds boilerplate at each of the three call sites (three refs, three
reads, three more existentials to open) rather than removing any.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9280194c-7c30-4916-8e1c-9b786e884b25
The stream_trunc_base/len/pos projections are inline_for_extraction
noextract lets, so they are delta-reducible; when type-checking an
instance's stream_truncate against the class field type, F* substitutes
the earlier fields of the same record literal.  That makes the directly-
stated spec convertible with the projection-stated one.

So each backend can now say what it actually means:

  buffer: stream_pts_to b res pos ...  (only the length shortens)
  extern: stream_pts_to res () () ...  (only the base changes)

instead of routing every component through stream_trunc_*.  No change to
the generated C: extraction, the absence of internal/ and EverParse.c,
and the full share/everparse/tests/3d suite are all unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9280194c-7c30-4916-8e1c-9b786e884b25
`3d.exe --pulse` rebuilt the whole Pulse runtime header on every invocation,
bundling it into the client's output directory with -static-header.  That is
wasteful: the result does not depend on the .3d input at all.  Verified by
generating it for five very different inputs (TestAllBytes, Pair, ELF,
Arithmetic, FineGrainedProbe) -- byte-identical every time, 159 lines.  It
depends only on the input stream backend, and extern and static agree.

So do what the Low* backend does.  lib/everparse/3d/krml/header.Makefile now
generates one EverParse.h per backend at build time, from the prelude .krml
files alone, into lib/everparse/3d/krml/<backend>/ -- the exact analogue of
src/3d/prelude/<backend>/EverParse.h.  Batch.ml passes KaRaMeL -library
instead of -static-header, which turns the runtime into plain `extern`
declarations resolved by that header, and copies the shipped header into the
output directory.

This works because everything the runtime contributes to the generated
validators is inline_for_extraction and has already been inlined by F*
itself; -library only has to cover the residue, which is what the shipped
header defines.  Confirmed: the generated .c/.h are byte-identical either
way for all five inputs above.

Two visible consequences, both convergences with Low*:

  - EverParse.h no longer #includes itself (an artifact of the global
    -add-include '"EverParse.h"', harmless but silly).
  - --no_copy_everparse_h now leaves no EverParse.h behind, so the
    pulse-cleanup-test expectation is now literally the Low* one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9280194c-7c30-4916-8e1c-9b786e884b25
The new lib/everparse/3d/krml/header.Makefile derived DDD_HOME from
$(realpath ../../../../src), which under Cygwin make yields a
/cygdrive/d/... path. KRML_EXE on Windows is a native krml.exe, which
cannot open such a path, so all three backend header rules died with

  Fatal error: exception Sys_error("/cygdrive/d/a/everparse/everparse/src/3d/noheader.txt: No such file or directory")

failing 3d-pulse-krml and hence the whole nuget-noversion package build.

Include src/windows.Makefile, which rewrites EVERPARSE_SRC_PATH with
cygpath -m on Windows, and derive DDD_HOME after it. This is exactly what
the sibling extract.Makefile already does. No-op on Linux: the three
generated headers are byte-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9280194c-7c30-4916-8e1c-9b786e884b25
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