Skip to content

Fix put return value, exact int64 encode_json, NaN ordering; narrow difftest HFIR skip (#161, #156, #158) - #162

Merged
howlcipher merged 1 commit into
mainfrom
fix-161-156-158
Oct 12, 2026
Merged

howlcipher merged 1 commit into
mainfrom
fix-161-156-158

Conversation

@howlcipher

Copy link
Copy Markdown
Owner

Fixes #161, Fixes #156, Fixes #158

Changes

No new opcode or capability. No capability or spawn code was touched.

Tests

  • issue161_156_158_cli_test.go checks exact stdout on the interpreter, governed AST bytecode and HFIR bytecode for put-as-expression (flat, nested and statement), int64 JSON, the NaN relational matrix and the NaN sort/min/max/_by outputs.
  • gofmt is clean, and go build ./... and go vet ./... pass. go test ./... passes locally, including the arity, drift, fuzz, lock and freeze tests and TestHFIRBenchmarkSolutionParity. The one exception is examples/TestHTTPServerServeHFBC, which failed locally only because a stray process was holding port 8080 on the box. CI will run it clean.
  • The benchmark proof scripts (write-cost, real-apps) give output identical to base 54009fb, so pins are unchanged.
  • go run ./tools/difftest: optimization_signature.howl now prints SKIP. The test_env.howl interpreter failure also happens on base in this local environment.

No production flip. #90 stays Partial.

@howlcipher howlcipher left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of #162 at 2607b20c5a473e511dc60efc0146f9aa21297532 (base 54009fb2)

Verdict: merge-ready. No blockers. A few nits below.

I used a fresh clone in /workspace, with a base worktree, and built hf on both. Every check below runs the interpreter (-run), governed AST bytecode and HFIR bytecode.

1. NaN (#158)

  • <, <=, > and >= with NaN on either side, int or float, including NaN vs NaN: all false on all 3 paths. Base gave true for (<= n n) and (>= 1 n).
  • == and != are unchanged and follow IEEE: (== n n) is false, (!= n n) is true and (== n 1.0) is false. That matches base on all paths.
  • Non-NaN differential: 300 programs gave 3,000 random int and float lists (MaxInt64, MinInt64, 2^53±1, -0.0, ±Inf, 1e308, 5e-324, -1e-308) plus 3,000 range calls. sort, min, max, sort_by, max_by, min_by and range gave 0 mismatches between base and head, with all 3 paths agreeing and every run exiting 0.
  • NaN in lists: 50 programs with 500 lists containing NaN gave output byte-identical to base and identical across the 3 paths. No panic, hang or non-zero exit. The result is deterministic but not a total order, because it depends on position. For example, (sort (list n 3.0 1.0 n 2.0)) gives [NaN 1 2 3 NaN], (min (list n 1.0 2.0)) gives NaN, and (min (list 1.0 n)) gives 1.
  • #153 compare-boundary differential: 4,704 cases (the 6 operators over 28×28 int and float boundaries, including 2^63 and ±Inf), checked against an exact Fraction reference on 3 paths. 0 mismatches.

2. encode_json (#156)

  • MaxInt64, MinInt64, 2^53±1, -2^53-1 and -42 come out exact. Nested dicts and lists with int64 come out exact. The round trip encode_json → parse_json → encode_json matches byte for byte, and (== (parse_json Any "9007199254740993") 9007199254740993) is true.
  • Float formatting: 716 values, including 0.1, 1.5, -0.0, 1e20, 1e-7, 2.0, ±2^53, 1e21, 5e-324, MaxFloat64, 500 random floats across 1e-30 to 1e30 and 200 random ints within ±2^53. Output is byte-identical to base on all 3 paths.
  • 2.0 still encodes as 2, as it did on base, so JSON output doesn't tell floats from ints. That's unchanged and not a regression. Inside the language, floats and ints stay distinct types, because homogeneous lists still reject a mix.
  • Inf and NaN give the same clean ENCODE_JSON_ERROR / unsupported value as base on all paths.

3. put (#161)

  • put returns the dict on all 3 paths for flat, nested and statement-position forms, and (encode_json (put d k v)) matches. Base failed every expression-position case.
  • Aliasing: put mutates its input and returns that same dict, so (let (e (put d "y" 2)) ...) gives an e that aliases d. This matches base, where map_set on a let alias also mutates the original. Under #137 only lists are values, so this is consistent.
  • Fuzz in the style of #160: 600 random nested put/let/map_get/encode_json programs. 0 cross-path mismatches and 0 STACK_UNDERFLOW. 245 of them deliberately hit the TYPE_ERROR for putting into a list value, and they fail at the same point with the same stdout on all paths. On base, all of the first 100 diverged.

4. difftest skip

  • Only optimize_signature is on hfirFencedConstructs. It prints SKIP optimization_signature.howl: HFIR leg fenced (optimize_signature); interpreter, go and bytecode legs agree.
  • Counts went from Passed 17 / Skipped 31 / Failed 1 on base to Passed 16 / Skipped 32 / Failed 1 on head, so the skip is no longer counted as Passed.
  • The one failure is test_env.howl (interpreter: capability denied: environment), and it fails the same way on base. TestHFIRFencedConstructIsExplicit passes.

5. Pins, freeze and locks

The diff touches only desugar_records.go, vm.go, difftest and two test files. It changes no pin or freeze file and doesn't touch the TestProdFlipCriteriaLock source. These all pass: TestHFIRBenchmarkSolutionParity, the arity, drift and fuzz tests, TestProdFlipCriteriaLock, the freeze tests and the new Issue161/156/158 tests.

6. CI and tooling

  • CI build is green on the head SHA.
  • gofmt -l reports 0 files, and go vet ./... exits 0.
  • go test ./... -count=1 exits 0. A first run failed examples/TestHTTPServerServeHFBC with unexpected listener on port 8080. The cause was a stale /tmp/hf160 process left on the box from my #160 review. I killed it, and that test alone and ./examples/... ./apps/... both passed. The full suite then exited 0. It was not a PR issue.

7. Hard nos

  • No prod -compile-bc/HFIR default flip, and #90 stays Partial.
  • No tip-lock 4d74dbc.
  • No live-LLM run, and no H1 PASS/KILL claim.

Nits (non-blocking)

  1. NaN in sort/min/max depends on position, as shown above. Consider documenting it, or defining a total order with NaN last, in a follow-up.
  2. Document that put returns and mutates the same dict, since it can now be used as an expression and aliasing is easy to miss.
  3. test_env.howl fails in difftest on both base and head. Pass -allow-caps environment to that interpreter leg, or skip it explicitly.
  4. JSON encodes 2.0 as 2. That's pre-existing; worth noting next to #156.

@howlcipher
howlcipher marked this pull request as ready for review October 12, 2026 23:36
@howlcipher
howlcipher merged commit 3ab40ff into main Oct 12, 2026
1 check 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

1 participant