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
27 changes: 27 additions & 0 deletions .github/workflows/readme-scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: readme-scorecard
on:
push:
pull_request:

permissions:
contents: read

jobs:
readme-rendered:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: regenerate scorecard and check README
run: |
python3 tools/scorecard.py .
bash scripts/check-readme-rendered.sh
bash scripts/check-readme-claims.sh
- name: scorecard summary
if: always()
run: |
{
echo "## Scorecard"
echo '```'
python3 -c "import json; from pathlib import Path; sc=json.loads(Path('docs/evidence/scorecard.json').read_text()); print('suites', {k: f\"{v['pass']}/{v['total']}\" for k,v in sc.get('original_suite',{}).items()}); eh=sc.get('escape_hatches',{}); print('escape_hatches', eh.get('total'), 'threshold', eh.get('threshold'))"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Zig
.zig-cache/
zig-out/
zig-out-*/

# C build artifacts
*.o
Expand Down Expand Up @@ -48,6 +49,7 @@ bench/plots/*.png
# Local scratch
.local/
*.tmp
docs/evidence/*.tmp
*.swp
.DS_Store
Thumbs.db
Expand Down
59 changes: 59 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Contributing

## Branches, commits, and merges

- Put every unit of work on a branch named `scope/short-kebab`, for example
`cover/c-baseline`, `fuzz/enum-strings`, or `chore/parallel-work-safety`.
- Land work through a pull request with a real description. Never push straight
to `main`; the already-completed eight-commit push that unblocked teammates is
the sole exception.
- Nobody merges a red PR.
- Use commit subjects in the form `scope: imperative`, at most 72 characters.
Valid scopes are `port`, `abi`, `test`, `fuzz`, `cover`, `bench`, `ci`,
`docs`, `tool`, `upstream`, and `chore`.
- Do not edit `tests/original/` or `third_party/`.

Before every merge, the original suite must pass at these exact counts:

| Config | Required count |
| --- | --- |
| default | 4930/4930 |
| pow (`-Dpow-from-right=true`) | 4924/4924 |
| natlog (`-Dnat-log=true`) | 4934/4934 |
| pow_natlog (both flags) | 4928/4928 |

Run all four commands:

```bash
bash scripts/run-original-suite.sh default
bash scripts/run-original-suite.sh pow -Dpow-from-right=true
bash scripts/run-original-suite.sh natlog -Dnat-log=true
bash scripts/run-original-suite.sh pow_natlog -Dpow-from-right=true -Dnat-log=true
```

The exact local verification entry points are:

```bash
zig build verify
docker compose up --build verify
```

## Parallel-work territories

Treat these territories as a coordination contract: ask the owning machine
before changing a path outside your territory.

- This machine owns `tools/`, `fuzz/`, `scripts/`, `src/`, `tests/port/`, and
new CI workflow files it creates.
- The second machine owns `bench/`, `examples/`, documentation prose, and demo
assets.
- `DECISIONS.md` is shared: this machine adds D016–D029 only; the second
machine adds D030 and later.
- `docs/journal.md` is shared and append-only. Add entries at the end under
`## 2026-08-02 <hostname> <topic>`; when entries conflict, keep both.
- `.github/workflows/` is shared. Add new workflow files rather than editing
`ci.yml` from both machines.

Generated files are never hand-merged. For a conflict in
`docs/evidence/scorecard.json` or the README scorecard block, take either side
and re-run the generators.
34 changes: 34 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,37 @@ Entries that only restate a diff will be deleted.
- Evidence: the native AST stores `Fn0`–`Fn7` and `Cl0`–`Cl7`; C ABI variable
conversion selects the matching variant from `TE_FUNCTION0`–`7` and
`TE_CLOSURE0`–`7`.

## D017 — Fragment-assembled scorecard and generated README region

- Constraint: parallel coverage, fuzz, allocation-fault, benchmark, and
upstream-review campaigns will finish independently, but the scorecard and
README must remain one truthful, reviewable view.
- Options: hand-edit the aggregate files; make each campaign rewrite the whole
scorecard; compose measured base artifacts with small campaign fragments.
- Choice: `tools/scorecard.py` builds a measured base from the suite, hatch,
layout, cross-compile, and hash artifacts, then deep-merges
`docs/evidence/fragments/*.json`. `tools/render_readme.py` owns the README
region between its scorecard markers.
- Consequence: generated files are never hand-merged. Resolve a conflict in
`scorecard.json` or the README region by taking either side and rerunning the
generators; absent campaign values are explicit `not_measured`, never null.
- Evidence: `tools/scorecard.py`, `tools/render_readme.py`, and
`scripts/check-readme-rendered.sh`.

## D018 — Organizer thresholds and differential-harness fallback

- Constraint: the organizer may define C-to-Zig escape-hatch thresholds or
publish a Port Mortem harness template after kickoff, but shipping depends on
a concrete gate now.
- Options: wait for unpublished material; invent a claimed official threshold;
retain an operational local threshold and build the missing harness.
- Choice: searches of `portmortem.devfolio.co`, `raptors.dev`, the promises in
`DETAILS.md`, and GitHub for a Port Mortem harness template or C-to-Zig
escape-hatch thresholds found nothing published. Keep the operational
threshold of 20, confined to `src/c_abi.zig`, and build a bespoke
differential harness in later prompts.
- Consequence: the threshold is an explicit project policy rather than an
organizer claim; future published rules can replace it with evidence.
- Evidence: D013 and `tools/count-escape-hatches.py`; no matching published
organizer material was found in the named searches.
39 changes: 17 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Port of [codeplea/tinyexpr](https://github.com/codeplea/tinyexpr) to Zig.

Pinned upstream: `4a7456e2eab88b4c76053c1c4157639ccb930e2b`.

<!-- scorecard:begin -->
## Status

Original `smoke.c` / `minctest.h` (byte-identical) link against the Zig static
Expand All @@ -23,14 +24,22 @@ disagrees with the scorecard.
| natlog (`TE_NAT_LOG`) | 4934/4934 | 4934/4934 |
| pow_natlog (`smoke_pr`) | 4928/4928 | 4928/4928 |

Escape hatches: 16 total in `src/c_abi.zig` (797 audit-surface lines,
6 exported `te_*` functions → ~2.67 hatches per export), gated by
`tools/count-escape-hatches.py` (threshold 20, ratchets down as count
drops). Per-kloc density is a weak comparator for a tiny FFI shim;
absolute audit surface and hatches-per-export matter more than uv's
0.21/kloc reference. Layout of `te_expr` /
`te_variable` matches the C original on x86_64 Linux (independent
`tools/layout_probe.c`, transcript `docs/evidence/layout-compare.txt`).
Escape hatches: 16 total in `src/c_abi.zig` (799 audit-surface lines, 6 exported `te_*` functions → ~2.67 hatches per export), gated by `tools/count-escape-hatches.py` (threshold 20).

Cross-compile (`scripts/cross-compile.sh` → `docs/evidence/cross-compile.json`):
each target is labelled `builds_only`, `unit_tests_pass`, or
`full_suite_passes` (not collapsed to a checkmark).

| Target | Status |
|--------|--------|
| x86_64-linux-gnu | builds_only |
| x86_64-linux-musl | builds_only |
| aarch64-linux-gnu | full_suite_passes |
| x86_64-windows-gnu | builds_only |
| aarch64-macos | builds_only |
| riscv64-linux | unit_tests_pass |
| wasm32-wasi | unit_tests_pass |
<!-- scorecard:end -->

## Judge one-liner

Expand All @@ -55,20 +64,6 @@ zig build fmt-check
zig build cross
```

Cross-compile (`scripts/cross-compile.sh` → `docs/evidence/cross-compile.json`):
each target is labelled `builds_only`, `unit_tests_pass`, or
`full_suite_passes` (not collapsed to a checkmark).

| Target | Status |
|--------|--------|
| x86_64-linux-gnu | builds_only |
| x86_64-linux-musl | builds_only |
| aarch64-linux-gnu | full_suite_passes (qemu; musl vehicle) |
| x86_64-windows-gnu | builds_only |
| aarch64-macos | builds_only |
| riscv64-linux | unit_tests_pass (qemu) |
| wasm32-wasi | unit_tests_pass (wasmtime) |

Build options:

| Option | Default | Meaning |
Expand Down
14 changes: 7 additions & 7 deletions docs/evidence/escape-hatches.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,31 +66,31 @@
},
{
"file": "src/c_abi.zig",
"line": 470,
"line": 472,
"kind": "@constCast"
},
{
"file": "src/c_abi.zig",
"line": 480,
"line": 482,
"kind": "@ptrCast"
},
{
"file": "src/c_abi.zig",
"line": 480,
"line": 482,
"kind": "@alignCast"
},
{
"file": "src/c_abi.zig",
"line": 566,
"line": 568,
"kind": "@constCast"
}
],
"lines_of_zig_src": 2026,
"per_thousand_lines": 7.897334649555775,
"lines_of_zig_src": 2028,
"per_thousand_lines": 7.889546351084813,
"allowed_file": "src/c_abi.zig",
"outside_c_abi": 0,
"threshold": 20,
"audit_surface_lines": 797,
"audit_surface_lines": 799,
"distinct_operations": 4,
"exported_abi_functions": 6,
"hatches_per_exported_abi_fn": 2.6666666666666665,
Expand Down
8 changes: 3 additions & 5 deletions docs/evidence/escape-hatches.table.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ file count
src/c_abi.zig 16
------------------------------------------------
TOTAL 16
LOC src/*.zig: 2026
audit surface lines (src/c_abi.zig): 797
LOC src/*.zig: 2028
audit surface lines (src/c_abi.zig): 799
distinct hatch kinds: 4
exported te_* functions: 6
hatches per exported te_* fn: 2.6667
per thousand lines: 7.8973
per thousand lines: 7.8895
outside src/c_abi.zig: 0
threshold: 20
pass: True
wrote docs/evidence/escape-hatches.json
wrote docs/evidence/escape-hatches.table.txt
1 change: 1 addition & 0 deletions docs/evidence/fragments/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

44 changes: 34 additions & 10 deletions docs/evidence/scorecard.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@
"per_file": {
"src/c_abi.zig": 16
},
"per_thousand_lines": 7.897334649555775,
"per_thousand_lines": 7.889546351084813,
"outside_c_abi": 0,
"threshold": 20,
"audit_surface_lines": 797,
"audit_surface_lines": 799,
"distinct_operations": 4,
"hatches_per_exported_abi_fn": 2.6666666666666665,
"exported_abi_functions": 6,
Expand Down Expand Up @@ -150,22 +150,46 @@
}
},
"coverage": {
"status": "pending_prompt_4"
"status": "not_measured"
},
"enumeration": {
"status": "pending_prompt_4"
"status": "not_measured"
},
"fuzz": {
"status": "pending_prompt_4"
"status": "not_measured"
},
"alloc_fault": {
"status": "pending_prompt_4"
"status": "not_measured"
},
"benchmark": {
"status": "pending_prompt_5"
"status": "not_measured"
},
"upstream_findings": {
"count": 0,
"status": "pending"
}
"status": "not_measured"
},
"alphabet": "01+-*/()",
"ast_depth": 2,
"ast_total": 202,
"command": "bash scripts/run-enumerate.sh --quick",
"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,
"git_sha": "adbdb1d2cf5826b218c8f69e69c24a49112e8dcd",
"host": "Linux-6.18.33.2-microsoft-standard-WSL2-x86_64-with-glibc2.39",
"mode": "quick",
"objcopy": "GNU objcopy (GNU Binutils for Ubuntu) 2.42",
"requested_ast_depth": 2,
"requested_string_max_length": 3,
"schema_version": 1,
"string_max_length": 3,
"string_total": 585,
"total": 787,
"utc_end": "2026-08-02T03:31:05Z",
"utc_start": "2026-08-02T03:30:56Z",
"zig": "0.16.0"
}
6 changes: 6 additions & 0 deletions docs/journal.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,9 @@ measurement over the plan. Three new unverified leads live in
`docs/findings.md`: non-static `next_token`, signed-char ctype/isdigit UB,
and strtod/LC_NUMERIC hang risk.

## 2026-08-02 elmengo parallel-work-safety

Task 0 unblocks the second machine: CONTRIBUTING territories are live, the
unification push is complete, and generated scorecard ownership is explicit.
CI now has a rendered-README freshness check through the verification path.

3 changes: 3 additions & 0 deletions scripts/check-readme-claims.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
# Fail if README.md numeric claims disagree with docs/evidence/scorecard.json.
# Regenerates the README scorecard region from the current scorecard first so
# verify pipelines that refresh escape-hatches/scorecard stay consistent.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SC="$ROOT/docs/evidence/scorecard.json"
Expand All @@ -11,5 +13,6 @@ if [[ ! -f "$SC" ]]; then
exit 1
fi

python3 "$ROOT/tools/render_readme.py" "$ROOT"
python3 "$CHECKER" "$SC" "$README"
python3 "$CHECKER" --self-test
6 changes: 6 additions & 0 deletions scripts/check-readme-rendered.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Fail if README.md's generated scorecard region is stale.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"

python3 "$ROOT/tools/render_readme.py" --check "$ROOT"
12 changes: 7 additions & 5 deletions src/c_abi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -413,11 +413,13 @@ const CAbiBuilder = struct {

switch (callee) {
.builtin => |op| node.data = .{ .function = abiFunction(op) },
.function => |binding| node.data = .{ .function = switch (binding.ptr) {
// Invariant: ast.FunctionPtr's arity-specific C-callconv pointer is
// representable as tinyexpr's opaque C function pointer field.
inline else => |typed| functionPointer(typed),
} },
.function => |binding| node.data = .{
.function = switch (binding.ptr) {
// Invariant: ast.FunctionPtr's arity-specific C-callconv pointer is
// representable as tinyexpr's opaque C function pointer field.
inline else => |typed| functionPointer(typed),
},
},
.closure => |binding| switch (binding.ptr) {
.a0 => |closure| {
node.data = .{ .function = functionPointer(closure.f) };
Expand Down
Loading
Loading