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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,14 @@ docs/evidence/coverage-port.log
# Layout probe host binaries
layout-c
layout-zig

# Bug-repro build products (sources and run logs stay tracked)
docs/evidence/repro/ctype_highbit
docs/evidence/repro/ctype_highbit_trap
docs/evidence/repro/fac_nan
docs/evidence/repro/locale_decimals
docs/evidence/repro/nest_overflow
docs/evidence/repro/next_token_dyn
docs/evidence/repro/next_token_multi_other_first
docs/evidence/repro/next_token_multi_te_first
docs/evidence/repro/*.err
30 changes: 30 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,33 @@ Entries that only restate a diff will be deleted.
- Evidence: pre-fix empty-array unbound variable and `mapfile: command not
found` on this Mac; suites then measured at 4930/4924/4934/4928
(`docs/REPRODUCTION.md`).

## D033 - Divergence table and scoped equivalence claim

- Constraint: judges need every intentional behavioural difference in one
place, and an equivalence claim that does not silently cover UB or locale
traps.
- Options: scatter notes across DECISIONS only; omit scope; one table plus one
sentence.
- Choice: add an intentional-divergences table to `README.md` and keep the
authoritative detail in `docs/findings.md`. Equivalence sentence: C locale,
below nesting bound, no C UB.
- Consequence: nest bound, optional fac flag, locale/`strtod`, and ctype UB
are explicit. Locale is shared behaviour, not a port win.
- Evidence: nest thresholds and locale tables measured 2026-08-02
(`docs/evidence/repro/run-log-*.txt`); upstream #136/#105/#141/#142.

## D034 - Upstream engagement for Bug Catcher

- Constraint: bonus and Innovation credit require filing during the hackathon,
without false novelty or severity inflation.
- Options: claim #136 as ours; file many thin issues; file only what the
survey shows is unfiled and confirm the rest.
- Choice: comment on #136 and #105 with independent measurements; open #141
(`next_token`) and #142 (locale); open minimal PRs #143 and #144. Record
allocator criterion as not satisfied.
- Consequence: two new issues, two PRs, two confirmation comments. Track G
allocator-misuse remains an honest negative.
- Evidence: URLs in `docs/findings.md`; survey in
`docs/evidence/upstream-issues.md`; fragment
`docs/evidence/fragments/upstream-findings.json`.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,27 @@ what it covered. We aim at original-suite ABI parity, coverage-driven
agreement, exhaustive enumeration, and malloc-failure injection. Numbers
appear here only after scripts produce them.

## Equivalence scope

Bit-exact agreement between the C original and the Zig C ABI is claimed for
expressions evaluated under the C locale (`LC_NUMERIC`), at nesting depths
below the port bound, on inputs where the C original does not invoke
undefined behaviour (including signed-`char` ctype on high-bit bytes and
stack overflow on deep nests). Measured thresholds and upstream URLs live in
`docs/findings.md`.

## Intentional divergences

| Input class | C original | Port | Why |
| --- | --- | --- | --- |
| Deep nested parentheses | Stack overflow (issue #136) | Bounded parse error / NULL compile | Fail closed (D008) |
| `-Dfix-fac-overflow=true` | Truncates non-integer `fac` args | Returns NaN for non-integers | Optional hardening; default matches smoke (D006) |
| Decimals after comma-decimal `setlocale` | `strtod` follows `LC_NUMERIC` | Same via `libm.strtod` | Bit-exact parity (D025); scope to C locale |
| High-bit first byte on signed `char` | UB at `isalpha`/`isdigit` (#105) | No C ctype on plain `char` | Avoid copying that UB |

Full write-up and measured host thresholds: `docs/findings.md`.
Upstream survey: `docs/evidence/upstream-issues.md`.

## Licence

zlib. Altered derivative of TinyExpr. See `LICENSE` and `THIRD_PARTY.md`.
53 changes: 27 additions & 26 deletions docs/evidence/divergences.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,43 @@
# Differential enumerator divergences
# Divergences and equivalence scope

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).
Authoritative severity write-ups and upstream URLs: `docs/findings.md`.
README carries the short judge table.

## Completed bound (string length ≤ 3, AST depth ≤ 1)
**Equivalence scope:** bit-exact C↔Zig ABI agreement is claimed for the C
locale (`LC_NUMERIC`), at nesting depths below the port bound, on inputs
where the C original does not invoke undefined behaviour.

## Intentional / scoped divergences

| Input class | C original | Port | Why | Equivalence |
| --- | --- | --- | --- | --- |
| Deep nested parentheses | SIGSEGV near host-dependent depth (issue #136; Darwin arm64 interp crash 23764; Docker aarch64 17471) | Bounded fail: NULL compile / typed nest error (Darwin Zig ABI paren fail at 341) | Fail closed (D008) | Excludes C stack-overflow UB |
| `-Dfix-fac-overflow=true` | Truncates non-integer `fac` args | NaN for non-integers | Optional hardening; default matches smoke (D006) | Only when the flag is on |
| Decimals after comma-decimal `setlocale` | `strtod` follows `LC_NUMERIC` (`1.5` fails; `1,5` works) | Same via `libm.strtod` | Bit-exact parity (D025) | Decimal literals: C locale only |
| High-bit first byte on signed `char` | UB at `isalpha`/`isdigit` (#105) | No C ctype on plain `char` | Avoid copying C99 7.4p1 UB | Excludes that UB surface |

## Differential enumerator (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. |
Scope for that campaign: inputs where the C original does not exhibit
undefined behaviour. Deep nesting that overflows the C stack is not fed
through the in-process harness.

Control probes that **agree**:
## Probe notes (`fac` over NaN)

| Input | Notes |
| --- | --- |
| `0/0` | both NaN (`0xfff8000000000000`) |
| `1%0` | both NaN |
| `ncr(0,1)` | both NaN |
| `fac(0/0)`, `fac(ncr(0,1))` | C returns `1.0` on Darwin and Ubuntu Docker (this session). Zig ABI on Darwin also returned `1.0` here; an earlier WSL probe reported Zig `+Inf`. Treat C↔Zig for this class as host/opt-mode sensitive; related to open #133 / #62. |
| `0/0`, `1%0`, `ncr(0,1)` | Both sides NaN on agreement controls. |

## Locale scope (Lead C)
## Locale (shared behaviour, not a port win)

Under `setlocale(LC_ALL, "de_DE.UTF-8")`, both C and Zig (via `libm.strtod`)
reject `1.5` and accept `1,5`. This is shared behaviour, not a port bug.
Bit-exact equivalence for decimal literals is claimed for the C locale only.
See `docs/findings.md` Lead C.
Under `setlocale(LC_ALL, "de_DE.UTF-8")`, both C and Zig reject `1.5` and
accept `1,5`. Hang on `.5` was **not** reproduced. See upstream #142 and
`docs/findings.md`.

## Wasm

Expand Down
31 changes: 31 additions & 0 deletions docs/evidence/fragments/enumeration-full.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"
}
19 changes: 19 additions & 0 deletions docs/evidence/fragments/fuzz-warmup-progress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"executions": 1604740,
"divergences": 0,
"first_token_reject": 61024,
"seconds": 1.000001,
"exec_per_sec": 1604738.75,
"seed": 1785662748,
"stopped_early": false,
"schema_version": 1,
"command": "bash scripts/run-fuzz.sh 1",
"git_sha": "b1d804343c433779920fa1080b4a411c993bc068",
"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",
"utc_start": "2026-08-02T09:24:22Z",
"utc_end": "2026-08-02T09:24:24Z",
"mode": "warmup-progress",
"requested_seconds": 1.0,
"harness_exit": 0,
"zig": "0.16.0"
}
18 changes: 18 additions & 0 deletions docs/evidence/fragments/fuzz-warmup.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"executions": 149237,
"divergences": 0,
"first_token_reject": 5693,
"seconds": 1.000727,
"exec_per_sec": 149128.53,
"seed": 1785627365,
"schema_version": 1,
"command": "bash scripts/run-fuzz.sh 1",
"git_sha": "b5e302ed15281e3d96c0998e64e0b8aa20afeee0",
"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",
"utc_start": "2026-08-02T05:23:26Z",
"utc_end": "2026-08-02T05:23:35Z",
"mode": "warmup",
"requested_seconds": 1.0,
"harness_exit": 0,
"zig": "0.16.0"
}
60 changes: 60 additions & 0 deletions docs/evidence/fragments/upstream-findings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"schema_version": 1,
"command": "gh issue/pr create and comment; docs/evidence/repro/run-all.sh",
"git_sha": "609f03618213b8ab925f07d98785808c1907c672",
"host": "Darwin Kavyas-MacBook-Air-2.local 24.1.0 arm64; Ubuntu 24.04 aarch64 Docker",
"utc": "2026-08-02T10:35:00Z",
"allocator_misuse_track_g": "not_satisfied_negative_result",
"confirmed_existing": [
{
"id": 136,
"title": "Security: Stack overflow via deeply nested expressions (CWE-674)",
"state": "OPEN",
"our_action": "independent_reproduction_comment",
"url": "https://github.com/codeplea/tinyexpr/issues/136",
"comment": "https://github.com/codeplea/tinyexpr/issues/136#issuecomment-5157204950"
},
{
"id": 105,
"title": "Importing into gcc gives error: array subscript has type char",
"state": "OPEN",
"our_action": "confirmation_comment_and_fix_pr",
"url": "https://github.com/codeplea/tinyexpr/issues/105",
"comment": "https://github.com/codeplea/tinyexpr/issues/105#issuecomment-5157205043",
"pr": "https://github.com/codeplea/tinyexpr/pull/144"
}
],
"newly_filed": [
{
"id": 141,
"title": "next_token has external linkage and can silently bind the wrong function",
"state": "OPEN",
"url": "https://github.com/codeplea/tinyexpr/issues/141",
"pr": "https://github.com/codeplea/tinyexpr/pull/143",
"severity": "medium"
},
{
"id": 142,
"title": "Decimal literals depend on LC_NUMERIC via strtod after setlocale",
"state": "OPEN",
"url": "https://github.com/codeplea/tinyexpr/issues/142",
"severity": "medium",
"hang_reproduced": false
}
],
"pull_requests": [
"https://github.com/codeplea/tinyexpr/pull/143",
"https://github.com/codeplea/tinyexpr/pull/144"
],
"counts": {
"new_issues": 2,
"confirmation_comments": 2,
"fix_prs": 2
},
"nest_thresholds_measured": {
"darwin_arm64_interp_first_crash": 23764,
"linux_docker_aarch64_interp_first_crash": 17471,
"zig_abi_darwin_paren_last_ok": 340,
"zig_abi_darwin_paren_first_fail": 341
}
}
15 changes: 15 additions & 0 deletions docs/evidence/repro/ctype_highbit.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* Lead B: signed-char ctype surface.
* Feeds a single 0x80 byte into te_interp. On signed-char platforms this
* reaches isalpha with a negative argument (C99 7.4p1). */
#include <stdio.h>
#include <limits.h>
#include "tinyexpr.h"

int main(void) {
char buf[2] = {(char)0x80, 0};
int err = 0;
printf("CHAR_MIN=%d CHAR_MAX=%d buf0=%d\n", (int)CHAR_MIN, (int)CHAR_MAX, (int)buf[0]);
double v = te_interp(buf, &err);
printf("te_interp=%.17g err=%d\n", v, err);
return 0;
}
26 changes: 26 additions & 0 deletions docs/evidence/repro/ctype_trap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* Trapping ctype header: abort if isalpha/isdigit see negative args.
* Compile with: cc -include ctype_trap.h ... */
#ifndef CTYPE_TRAP_H
#define CTYPE_TRAP_H
#include <stdio.h>
#include <stdlib.h>
#include_next <ctype.h>
#undef isalpha
#undef isdigit
static inline int trap_isalpha(int c) {
if (c < 0 || c > 255) {
fprintf(stderr, "ctype_trap: isalpha(%d)\n", c);
abort();
}
return (c == EOF) ? 0 : (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
static inline int trap_isdigit(int c) {
if (c < 0 || c > 255) {
fprintf(stderr, "ctype_trap: isdigit(%d)\n", c);
abort();
}
return (c == EOF) ? 0 : (c >= '0' && c <= '9');
}
#define isalpha(c) trap_isalpha(c)
#define isdigit(c) trap_isdigit(c)
#endif
22 changes: 22 additions & 0 deletions docs/evidence/repro/fac_nan.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* fac(NaN) / fac(0/0) probe: C returns 1.0 on this class of inputs. */
#include <stdio.h>
#include <string.h>
#include "tinyexpr.h"

static void show(const char *expr) {
int err = 0;
double v = te_interp(expr, &err);
unsigned long long bits = 0;
memcpy(&bits, &v, sizeof bits);
printf("expr=%s err=%d value=%.17g bits=0x%llx\n", expr, err, v, (unsigned long long)bits);
}

int main(void) {
show("0/0");
show("fac(0/0)");
show("fac((0/0))");
show("fac(ncr(0,1))");
show("ncr(0,1)");
show("fac(21)");
return 0;
}
24 changes: 24 additions & 0 deletions docs/evidence/repro/locale_decimals.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* Lead C / locale: setlocale then te_interp on decimal expressions. */
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "tinyexpr.h"

static void try_expr(const char *expr) {
int err = 0;
double v = te_interp(expr, &err);
printf("expr=%s err=%d value=%.17g\n", expr, err, v);
}

int main(int argc, char **argv) {
const char *loc = (argc > 1) ? argv[1] : "de_DE.UTF-8";
char *got = setlocale(LC_ALL, loc);
printf("setlocale(%s)=%s\n", loc, got ? got : "(null)");
if (!got) return 2;
try_expr("1.5");
try_expr(".5");
try_expr("1,5");
try_expr("2.25+3.5");
return 0;
}
Loading
Loading