Repository navigation
Exact int64 %, // and to_base; VALUE_ERROR on wrong-length destructuring (fixes #149) - #150
Conversation
howlcipher
left a comment
There was a problem hiding this comment.
Verdict: merge-ready (COMMENT review, since APPROVE is blocked for this account). Reviewed head b98801cb01bb2e161b1231f073fa0a07439c4f6b against base 65020331 in one fresh checkout. No blockers.
1. Independent differential test (Python floor semantics, equivalent to Go big.Int floor division)
- 3000 random int64 pairs. 45% sit at boundaries ±3 (0, ±1, ±2, ±3, 7, ±10, 1000, ±2^52±1, ±2^53±1, ±2^62±1, MaxInt64, MinInt64), 30% are uniform int64, 25% are small.
- 428
to_basecases in bases 2, 10, 16 and 36, including MaxInt64, MinInt64, 0, ±1 and ±2^53. - Every case ran on interpreter
-run, governed-run-bcand HFIR-run-bc: 471 program runs, 0 mismatches. MinInt64 // -1givesRUNTIME_ERROR integer overflowon all 3 paths (base gaveCONVERSION_ERROR).// 0and% 0givedivision by zero, including big-int operands.
2. Float detection by printed form
- I ran every probe base vs head on all 3 paths. Output is identical in every case, and the 3 paths agree:
- 2.0 and 7.0 (print as
2and7) - -0.0 (prints
0) - 4503599627370497.0 (prints
4.503599627370497e+15) - 1e33 and 1e296 (CONVERSION_ERROR on both base and head)
- +Inf, and NaN from Inf-Inf (
invalid floating-point resulton both) - mixed int/float, such as
(// 9007199254740993 2.0)
- 2.0 and 7.0 (print as
- Integral floats that print without a dot take the exact path but give the same value, as the comment says.
- Exponent literals (
1e20,1.0e5) don't lex as floats on either base or head, so you can't write them directly. - A string containing
ncan't reach the detector. Strings, bools and nil are rejected statically on every path. Only the message changed:/ requires numeric operandsbecame< compares incompatible types string and int. That's a nit. - Pre-existing, not from this PR:
(% MaxInt64 1e33)prints1.0000000000000092e+33and(// MaxInt64 1e33)prints-1, where Python gives 9.22e18 and 0. Base does the same. That's float-formula cancellation and worth a follow-up issue.
3. + 0 wrapper: at run time it only ever sees ints, and non-numerics are rejected before it runs. I saw no coercion and no new error codes.
4. Cost (governed -run-bc, bisected with -max-instructions)
- Small-operand ops:
%takes 30 instructions on base and 56 on head;//takes 39 and 94. These match the PR text. - Slow path:
(% MaxInt64 10)takes 228,(// MinInt64 7)takes 219, and(to_base MaxInt64 36)takes 3281. (to_base MinInt64 2)takes 13,407 instructions. On base it failed outright.- Tight loop, 20k iterations of
(+ (% i 7) (// i 3)): 1,514,285 instructions on base vs 3,134,285 on head. The two ops go from about 61 to 142 per iteration, roughly 2.3x.- Wall time is 76→113 ms on the interpreter, 84→178 ms on governed bytecode and 89→153 ms on HFIR.
- Under the default 100k limit, a loop doing one
%and one//per iteration now runs out at roughly 700 iterations instead of 1600. - The desugar adds no CALL, so MaxCallDepth is unaffected.
- All benchmark and example solutions still pass under the limits (60/60, 32/32, 34/34, 37/37, 37/37).
5. Wrong-length destructuring: VALUE_ERROR: destructuring expects N elements, exit 1, on all 3 paths, for too short and too long. Nit: unlike the runtime JSON errors it has no line or column. It matches the existing to_base VALUE_ERROR style (stderr plus exit). Could be done in a follow-up.
6. Pins and fences
- I installed tiktoken 0.8.0 and ran all 5 benchmark scripts: token-efficiency-v0, heldout-v0, heldout-v1, heldout-v2
run.shandrun_postcf.sh. Each exited 0 andgit statusstayed clean, so output is byte-identical. - Freeze, arity, drift, fuzz,
TestProdFlipCriteriaLockand Issue141/149 tests pass (exit 0). - Go/JS still reject with UNSUPPORTED_ON_TARGET (
TestIssue149RejectedOnGoJS).
7. Stale construct-table notes: nit, not a blocker. The code comments and the change_log are accurate.
8. Build checks: CI build is green. gofmt -l is clean. go test ./... exit 0, and no apps/* flake showed up this run.
9. Hard nos intact: the diff touches only desugar_controlflow.go, the new test file and change_log.md. There's no -compile-bc/HFIR default flip, TestProdFlipCriteriaLock is untouched, the tip-lock is unchanged, and I make no live LLM or H1 claims.
Nits: (a) the changed error text for non-numeric % and //; (b) no line or column on the destructuring VALUE_ERROR; (c) the 2x instruction cost cuts the default-limit loop headroom, so it's worth noting in the docs; (d) the stale construct-table notes; (e) a follow-up issue for the pre-existing float %/// cancellation with huge float divisors.
Fixes #149
What changed
%,//andto_baseare still pure desugar (internal/ast/desugar_controlflow.go): no new opcode, capability or effect.D*Qnever overflows. A second float division of the small remainder (biased by 1) and a loop of a few steps then make the remainder exact. After that come the floor or sign fixups.(% MaxInt64 10)= 7,(// 123456789012345678 1000)= 123456789012345.MinInt64 // -1gives the existing RUNTIME_ERRORinteger overflow(via(- 0 a)), not CONVERSION_ERROR.(% MinInt64 -1)= 0. Division by zero is stilldivision by zero.//dividesa - r. With a 2^53 bound,(// (2^53-1) -3)gave -3002399751580330 instead of -…331. This was a pre-existing off-by-one on main, below 2^53..,e,n(Inf) orN(NaN) takes the old float formula. An int64 never prints any of those. Integral floats above 2^52 always print with an exponent.%results are wrapped as(+ (+ x (* 0 a)) (* 0 b)), and//leaves asto_int. That adds 0 at run time, but gives the checker the same static type as the float branch, which it requires of bothifbranches.to_basenow walks non-positive N, so(to_base MinInt64 b)works. Before, it negated first.letdestructuring checks the length first:(let ((a b) (list 1 2 3)) …)→VALUE_ERROR: destructuring expects 2 elements, exit 1, on every path.Tests (
issue149_int_math_cli_test.go)-run, governed AST bytecode-run-bc, HFIR bytecode): 256//and%pairs over 2^53-1, 2^53, 2^53+1, MaxInt64, MinInt64, the negatives of these, and small or negative operands. Expected values come from Python.to_baseat MaxInt64/MinInt64 in bases 2, 16 and 36, float cases, and errors: MinInt64//-1 overflow (asserts no CONVERSION_ERROR), division by zero for int, big-int and float operands, and wrong-length destructuring.UNSUPPORTED_ON_TARGET.Checks
go build ./...,go vet ./...andgo test ./...pass, including the arity, drift, fuzz and existing control-flow tests.git statusclean after each): token-efficiency-v0, heldout-v0, heldout-v1, heldout-v2run.sh, andrun_postcf.sh.Costs / caveats
%or//adds a fast-path range check, so the bytecode instruction cost of a small-operand op roughly doubles ((% 7 3)goes from 30 to 56 instructions,(// 7 3)from 39 to 94), and the expanded code is larger. A slow-path op costs ~200–230 instructions. No benchmark task gets close to the default 100k limit (an earlier 2^53-bound draft did push w25 over; the final version doesn't).to_baseat MinInt64 in base 2 (~15k instructions) is tested in one program per case.Hard nos intact: no production
-compile-bc/HFIR flip, #90 stays Partial, no live LLM.