Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/enumerate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: enumerate

# Short enumeration tier for CI (new file; does not edit ci.yml).
on:
pull_request:
push:
branches: [main]
workflow_dispatch:

jobs:
enumerate-quick:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: install zig 0.16.0
run: |
curl -fsSL https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz -o zig.tar.xz
sudo tar -xJf zig.tar.xz -C /opt
echo "/opt/zig-x86_64-linux-0.16.0" >> "$GITHUB_PATH"
- name: Install objcopy toolchain
run: sudo apt-get update && sudo apt-get install -y binutils gcc
- name: Quick enumeration
run: bash scripts/run-enumerate.sh --quick
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: enumeration-quick
path: |
docs/evidence/fragments/enumeration.json
docs/evidence/divergences.md
docs/evidence/enumeration.log
docs/evidence/enumeration.md
- name: Step Summary
run: |
python3 tools/scorecard.py .
python3 - <<'PY'
import json, os
from pathlib import Path
card = json.loads(Path("docs/evidence/scorecard.json").read_text())
enum = card.get("enumeration", {})
Path(os.environ["GITHUB_STEP_SUMMARY"]).write_text(
"## Enumeration\n\n```json\n"
+ json.dumps(enum, indent=2)[:6000]
+ "\n```\n"
)
PY
15 changes: 15 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,18 @@ Entries that only restate a diff will be deleted.
- Evidence: `docs/evidence/coverage.md`,
`docs/evidence/fragments/coverage-c.json`,
`docs/evidence/fragments/coverage-port.json`.

## D020 — In-process enumeration via objcopy symbol rename

- Constraint: linking the C original and the Zig static library in one process
collides on `te_compile` / `te_eval` / `te_free` / `te_interp` / `te_print`.
- Options: two subprocesses per input; rename Zig exports; rename C symbols.
- Choice: compile the pinned C translation unit and run
`objcopy --redefine-sym te_*=c_te_*` (and `next_token` when exported) before
linking with the Zig archive and `tools/enumerate.c`.
- Consequence: exhaustive comparison stays in-process and fast enough for
11505 inputs in about one second on the measurement host; the technique is
documented in `fuzz/README.md` and `docs/evidence/enumeration.md`.
- Evidence: `scripts/run-enumerate.sh`,
`docs/evidence/fragments/enumeration.json` (`total` 11505, `divergences` 0
at string length 3 / AST depth 1).
12 changes: 11 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,17 @@ pub fn build(b: *std.Build) void {
const cover_step = b.step("cover", "lcov coverage of original C + evidence fragment");
cover_step.dependOn(&cover_cmd.step);
_ = b.step("bench", "Prompt 5+: benchmarks");
_ = b.step("enumerate", "Prompt 4+: exhaustive enumeration");

const enumerate_cmd = b.addSystemCommand(&.{ "bash", "scripts/run-enumerate.sh" });
enumerate_cmd.setCwd(b.path("."));
const enumerate_step = b.step("enumerate", "Exhaustive C vs Zig differential enumeration");
enumerate_step.dependOn(&enumerate_cmd.step);

const enumerate_quick_cmd = b.addSystemCommand(&.{ "bash", "scripts/run-enumerate.sh", "--quick" });
enumerate_quick_cmd.setCwd(b.path("."));
const enumerate_quick_step = b.step("enumerate-quick", "Short differential-enumeration tier");
enumerate_quick_step.dependOn(&enumerate_quick_cmd.step);

_ = b.step("alloc-fault", "Prompt 4+: malloc failure injection");
_ = b.step("demo", "Prompt 9: demo assets");
}
37 changes: 37 additions & 0 deletions docs/evidence/divergences.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Differential enumerator divergences

Scope: inputs where the C original does not exhibit undefined behaviour.
Deep nesting that overflows the C stack (Issue #136) is intentional and is
not fed through the in-process harness (it would abort the process).

## Completed bound (string length ≤ 3, AST depth ≤ 1)

`bash scripts/run-enumerate.sh` compared **11505** inputs (11155 strings +
350 AST renders) with zero divergences across compile-nullness, error
position, f64 bit patterns, and te_print bytes (constant prints).

## Probe-confirmed divergences (AST depth 2 family)

These appear once the AST enumerator nests `fac` / `ncr` over NaN-producing
subexpressions such as `0/0` and `1%0`. Measured with the same in-process
`c_te_*` / `te_*` harness on 2026-08-02:

| Enumerator | Minimized input | Observed | Root cause |
| --- | --- | --- | --- |
| probe | `fac(0/0)` | c_bits=`0x3ff0000000000000` (1.0), z_bits=`0x7ff0000000000000` (+Inf) | `0/0` is NaN. C `fac` does not treat NaN specially; `(unsigned)nan` is implementation-defined and yields 0 here, so the product loop returns 1. Zig `@intFromFloat(NaN)` takes a path that returns +Inf. Measured; C is the surprising 1.0. |
| probe | `fac((0/0))` | same as above | Same after parentheses. |
| probe | `fac(ncr(0,1))` | c=1.0, z=+Inf | `ncr(0,1)` is NaN (`n < r`); `fac(NaN)` hits the same C cast path. |
| intentional | deep nesting > 2048 | C: stack overflow (UB); Zig: compile NULL / err=-1 | D008 nesting bound. Not run in-process against C. |

Control probes that **agree**:

| Input | Notes |
| --- | --- |
| `0/0` | both NaN (`0xfff8000000000000`) |
| `1%0` | both NaN |
| `ncr(0,1)` | both NaN |

## Wasm

See `docs/evidence/fragments/enumeration-wasm.json`. Sample only, not the
full enumeration.
12 changes: 12 additions & 0 deletions docs/evidence/enumeration.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
=== enumeration metadata ===
started_at_utc=2026-08-02T05:08:00Z
git_sha=b800d61228b0f6202ff6f7a2d634e8f28cef5d57
host=Linux elmengo 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 18 21:54:43 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
mode=full
alphabet=0123456789.+-*/^%(), a
string_max=3
ast_depth=1
renamed_symbols=next_token te_compile te_eval te_free te_free_parameters te_interp te_print
harness_exit=0
completed_at_utc=2026-08-02T05:08:01Z
{"alphabet":"0123456789.+-*/^%(), a","alphabet_size":22,"string_max_length":3,"string_total":11155,"ast_depth":1,"ast_total":350,"total":11505,"divergences":0}
79 changes: 79 additions & 0 deletions docs/evidence/enumeration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Differential enumeration method

The enumerator runs the original C and Zig ABI implementations in the same
process. The original object has each externally exported `te_*` name renamed
to `c_te_*` with `objcopy --redefine-sym`; `next_token` is renamed when it is
exported. Unprefixed `te_*` symbols resolve to the Zig static library. This
avoids the dual-definition link failure that would otherwise bind both sides
to one implementation.

## Four observables per input

1. Compile success / NULL
2. Error position (`int`), including the offset-zero-reports-one quirk
3. `double` result as a `u64` bit pattern (never `==`)
4. `te_print` bytes for constant prints (function prints embed allocator
pointers, so only shape is compared there)

## String enumeration (A)

Alphabet (size **22**):

```text
0123456789.+-*/^%(), a
```

Included: digits, decimal point, operators `+ - * / ^ %`, parentheses, comma,
space, and one identifier character `a`.

Excluded: further letters (same token class as `a`), quotes/escapes (not in
the grammar), tabs/newlines (covered elsewhere by corpora).

Exact string counts:

```text
sum(k=0..N) 22^k
N=2 (quick): 1 + 22 + 484 = 507
N=3 (full): 1 + 22 + 484 + 10648 = 11155
```

## AST enumeration (B)

Leaves: `0`, `1`, `-0`, `1e-320`, `1e308`.
Unary ops: `+`, `-`. Binary ops: `+ - * / ^ %`.
Unary builtins: `sin cos abs sqrt fac log floor`.
Binary builtins: `atan2 pow ncr npr min max`.

Completed depth bound for the full campaign: **D = 1** (exactly **350**
trees). Depth 2 expands to roughly 1.47e6 trees and surfaces the `fac(NaN)`
family; those cases are recorded via targeted probes in
`docs/evidence/divergences.md` rather than as a completed depth-2 total.

## Completed totals (full mode)

| Tier | Bound | Inputs | Divergences |
|------|-------|--------|-------------|
| quick | strings ≤2, AST ≤1 | 857 | 0 |
| full | strings ≤3, AST ≤1 | **11505** | **0** |

## Equivalence scope

Claimed for the completed bounds above. Not claimed for:

- C undefined behaviour on deep nesting (Issue #136); the port returns an
error (D008).
- `fac(NaN)`-class inputs such as `fac(0/0)`: C returns `1.0` via an
implementation-defined cast of NaN to `unsigned`; Zig returns `+Inf`.
Documented as a real divergence; Zig is the more defensive reading.

## Wasm

`scripts/run-enumerate-wasm-sample.sh` prepares a sample only. It is not the
full enumeration. See `docs/evidence/fragments/enumeration-wasm.json`.

## Reproduce

```bash
zig build enumerate-quick
zig build enumerate
```
21 changes: 21 additions & 0 deletions docs/evidence/fragments/enumeration-deep-probes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"schema_version": 1,
"command": "scripts/probe (in-process c_te_* vs te_*)",
"git_sha": "b800d61228b0f6202ff6f7a2d634e8f28cef5d57",
"host": "Linux-6.18.33.2-microsoft-standard-WSL2-x86_64-with-glibc2.39",
"utc": "2026-08-02T05:08:01Z",
"status": "measured_probes",
"note": "AST depth-2 full space not completed in wall-clock; NaN/fac family confirmed by targeted probes after partial deep runs.",
"divergences_unique": [
{
"input": "fac(0/0)",
"c_bits": "0x3ff0000000000000",
"z_bits": "0x7ff0000000000000"
},
{
"input": "fac(ncr(0,1))",
"c_bits": "0x3ff0000000000000",
"z_bits": "0x7ff0000000000000"
}
]
}
8 changes: 8 additions & 0 deletions docs/evidence/fragments/enumeration-wasm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"schema_version": 1,
"command": "bash scripts/run-enumerate-wasm-sample.sh",
"status": "sample_not_full_enumeration",
"scope": "wasm32-wasi is a fourth data point on a sample only; full string/AST enumeration remains native in-process C vs Zig ABI",
"sample_count": "not_run_this_session",
"note": "wasmtime is available on the measurement host from prior prompts; a dedicated WASI-exported differential harness is deferred rather than half-claimed"
}
31 changes: 31 additions & 0 deletions docs/evidence/fragments/enumeration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"alphabet": "0123456789.+-*/^%(), a",
"alphabet_size": 22,
"ast_depth": 1,
"ast_total": 350,
"command": "bash scripts/run-enumerate.sh",
"comparisons_per_input": [
"compile_success_nullness",
"error_position",
"f64_u64_bits",
"te_print_bytes"
],
"compiler": "cc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0",
"divergences": 0,
"equivalence_scope": "inputs where the C original does not exhibit undefined behaviour; deep-nesting C crashes are intentional divergences when present",
"git_sha": "b800d61228b0f6202ff6f7a2d634e8f28cef5d57",
"host": "Linux elmengo 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 18 21:54:43 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux",
"mode": "full",
"objcopy": "GNU objcopy (GNU Binutils for Ubuntu) 2.42",
"requested_alphabet": "0123456789.+-*/^%(), a",
"requested_ast_depth": 1,
"requested_string_max_length": 3,
"schema_version": 1,
"string_max_length": 3,
"string_total": 11155,
"symbol_rename": "objcopy --redefine-sym te_*=c_te_*",
"total": 11505,
"utc_end": "2026-08-02T05:08:01Z",
"utc_start": "2026-08-02T05:08:00Z",
"zig": "0.16.0"
}
12 changes: 12 additions & 0 deletions docs/journal.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,15 @@ them; the report stays on compiler_rt or the C driver). Port line percent is
recorded as not_measured rather than guessed. Nesting probe on the ABI driver:
2100 deep parens yields `err=-1`, NULL (port guard).

## 2026-08-02 elmengo enumeration-pillar3

In-process enumerator via `objcopy --redefine-sym te_*=c_te_*`. Alphabet size
22 (`0123456789.+-*/^%(), a`). Completed full bound: string length 3 (11155
strings) plus AST depth 1 (350 trees) = **11505** inputs, **0** divergences on
nullness / error position / f64 bits / te_print.

AST depth 2 surfaces `fac(0/0)`: C returns `1.0`, Zig `+Inf`, because C casts
NaN to `unsigned` in `fac`. Recorded as a real divergence with Zig the more
defensive side. Deep nesting remains an intentional C-UB / port-error split
and is not run against C in-process.

28 changes: 26 additions & 2 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
# Fuzz harnesses

Differential and coverage-driven harnesses land in later commits.
## Differential enumeration

`zig build enumerate` builds one Linux executable containing both
implementations and compares them in-process. `scripts/run-enumerate.sh`:

1. compiles `third_party/tinyexpr-c/tinyexpr.c` into an object;
2. uses `objcopy --redefine-sym` to prefix every exported `te_*` symbol (and
`next_token` when externally visible) as `c_*`;
3. links that renamed object with the Zig static library and
`tools/enumerate.c`.

Original entry points become `c_te_compile`, `c_te_eval`, `c_te_free`,
`c_te_interp`, `c_te_print`, and `c_te_free_parameters`; unprefixed `te_*`
entries are Zig. The harness compares compile NULLness, error position, `f64`
bits as `u64`, and `te_print` bytes (constant prints).

```bash
zig build enumerate-quick # strings ≤2, AST ≤1 → 857 inputs
zig build enumerate # strings ≤3, AST ≤1 → 11505 inputs
```

Artifacts:

- `docs/evidence/fragments/enumeration.json`
- `docs/evidence/divergences.md`
- `docs/evidence/enumeration.md`

- `corpus-seed/`: small tracked seed inputs.
- `logs/`: gitignored runtime logs from `scripts/run-logged.sh`.
- `corpus/`: gitignored growing corpus.
Loading
Loading