Skip to content

Add core math builtins: pow, sqrt, log, exp, sin, cos#162

Merged
danieljohnmorris merged 6 commits into
mainfrom
fix/math-builtins
May 11, 2026
Merged

Add core math builtins: pow, sqrt, log, exp, sin, cos#162
danieljohnmorris merged 6 commits into
mainfrom
fix/math-builtins

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Adds the six core transcendentals as builtins backed by Rust's std::f64. Resolves the only remaining silent-miscompile entry in the assessment doc.

The doc cited hand-rolled Taylor expx "quietly returns wildly wrong values for x > 10⁴" — every numerics-heavy program was forced to either roll its own with silent precision loss or skip the analysis. For an AI-targeted language that lists data work as a core use case, that was a real reliability hole — exactly the failure mode the manifesto exists to prevent.

What's in the diff

  1. builtins: add core transcendentals (pow, sqrt, log, exp, sin, cos) — six new builtins available across tree-walker, VM, and Cranelift JIT. Each gets its own opcode (OP_POW/OP_SQRT/OP_LOG/OP_EXP/OP_SIN/OP_COS at 97-102) with a jit_* extern "C" helper, matching the precedent set by abs/flr/cel/mod/min/max. Domain-invalid inputs (sqrt -1, log 0) return NaN silently as std::f64 does — same no-Result-wrapping convention as the existing math builtins.

    Cross-engine NaN consistency: for non-numeric inputs (which the verifier should prevent but defensively handled), all three bytecode paths uniformly return NaN. Caught in review — the JIT helpers previously returned TAG_NIL, the VM transmuted unchecked bits, only the tree-walker behaved correctly. All three now agree.

  2. tests + example: pin math builtin behaviour across engines — 12 cross-engine regression tests + 2 unit tests pinning the cross-engine NaN contract. Numeric tolerance < 1e-10. sin_large_argument exercises sin 100000 which the hand-rolled Taylor used to fail catastrophically on. examples/math.ilo demonstrates a Euclidean dist helper, compound-interest via pow, and log/exp round-trip with -- run: / -- out: directives.

Test plan

  • cargo test --release --features cranelift — full suite green, +14 tests (12 regression + 2 NaN-consistency unit)
  • cargo fmt --all -- --check clean
  • cargo clippy --all-targets --features cranelift -- -D warnings clean
  • pow 2 10 → 1024, sqrt 2 → 1.4142..., sin 0 → 0, cos 0 → 1 across all three engines
  • log exp 5 → 5 round-trip
  • pow 4 0.5 → 2 (fractional exponent works)
  • Code-reviewed by subagent; cross-engine NaN divergence on non-numeric input was caught and fixed before commit

Opcode allocation

Claims opcodes 97-102. The parallel fix/list-indexing branch (PR #161) renumbered its OP_AT to 103 to avoid collision; both PRs can merge in either order.

Skipped on first cut

tan, log10, log2, atan2 — kept the initial surface minimal. Easy follow-up if agents need them.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

❌ Patch coverage is 97.59825% with 11 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/vm/mod.rs 96.08% 11 Missing ⚠️

📢 Thoughts on this report? Let us know!

The doc cited hand-rolled Taylor `expx` "quietly returns wildly
wrong values for x > 10⁴" - the only remaining silent-miscompile
class in the feedback log. Every numerics-heavy program was forced
to either roll its own with silent precision loss or skip the
analysis. For an AI-targeted language that lists "data work" as a
core use case, this was a real reliability hole.

Adding six core transcendentals backed by std::f64, available
across tree-walker, VM, and Cranelift JIT:
- pow x y (2-arg)
- sqrt, log (natural log), exp, sin, cos (1-arg, radians)

Skipped tan/log10/log2/atan2 to keep the initial surface minimal -
easy to add later if agents ask for them.

Each builtin gets its own opcode (OP_POW/OP_SQRT/OP_LOG/OP_EXP/
OP_SIN/OP_COS at 97-102) with a jit_* extern "C" helper, matching
the precedent set by abs/flr/cel/mod/min/max. No generic dispatch
- every existing math builtin has a dedicated opcode and uniform
fast-path treatment in the numeric classifier passes.

Domain-invalid inputs (sqrt -1, log 0) return NaN silently as
std::f64 does, matching the no-Result-wrapping convention of the
existing math builtins.

For non-numeric inputs (which the verifier should prevent but
defensively handled), all three bytecode paths now uniformly
return NaN: the JIT helpers (was TAG_NIL), VM dispatch (was
unchecked transmute), and tree-walker (still raises RuntimeError
- the type-checked tree path is self-consistent and the verifier
guarantees it won't fire in well-formed programs). Cross-engine
consistency restored.
12 cross-engine regression tests covering each of the six new
builtins (pow, sqrt, log, exp, sin, cos) across tree, vm, and
cranelift, plus two unit tests pinning the cross-engine NaN
contract for non-numeric input (jit_math_helpers_return_nan_on_
non_number, vm_math_ops_return_nan_on_non_number).

Numeric tolerance for the transcendentals uses |result - expected|
< 1e-10, loose enough for f64 imprecision but tight enough to
catch real bugs. The sin_large_argument test exercises sin 100000
which the prior hand-rolled Taylor produced wildly wrong values
for; the new std::f64 path is accurate across the full float
range.

examples/math.ilo demonstrates the new builtins via a Euclidean
`dist` helper, a compound-interest one-liner via pow, and a log/
exp round-trip. Each with `-- run:` / `-- out:` directives so
tests/examples_engines.rs exercises them across every engine.
check_infix_on_call and check_single_atom_after_op both wrote to
fixed-name temp files. Under cargo llvm-cov's parallel test runner,
two threads writing different content to the same path raced;
whichever finished writing last "won" but the other test's ilo
process might have already opened the file with mid-write content,
producing ILO-R002 undefined function failures intermittently.

Same fix shape as PR #163's run_file: AtomicU64 counter per
function, appended to filename along with pid for uniqueness.
Relaxed ordering matches the eval_inline.rs precedent (the counter
only needs atomic increment; no cross-variable synchronization
required).
PR #162's codecov/patch flagged the new math-builtin lines as
under-covered at the patch level: the cross-engine integration
tests do hit every path but codecov's patch heuristic doesn't
weight spawned-binary tests the same as in-process unit tests.

Added 12 in-process unit tests:
- 6 VM-dispatch happy paths (vm_op_pow_happy etc.) via a small
  run_unary_math_op helper that crafts a tiny bytecode chunk
- 6 jit_* helper happy paths exercising the extern "C" surface
  directly (cranelift-gated)

Expanded the existing vm_math_ops_return_nan_on_non_number to
cover OP_LOG, OP_EXP, OP_SIN, OP_COS too (previously only SQRT
and POW), so the NaN-fallback contract is now pinned uniformly
for all six new opcodes.

Local lib-test count: 2738 → 2750. Coverage on src/vm/mod.rs:
87.22% → 87.68%. The integration tests already exercise the
Cranelift translator branches; adding unit tests for those would
require scaffolding that's not justified now.
PR #162's codecov/patch still failing after the first round of unit
tests. Identified 38 uncovered new lines split across three buckets:
- AOT Cranelift translator arms (`vm/compile_cranelift.rs`) which
  --run-cranelift doesn't exercise because it uses the JIT, not AOT
- Interpreter runtime-type-error arms for the unary math builtins
  (the verifier catches these statically, but runtime checks remained
  untested)
- Defensive `panic!` fallbacks inside test code (unreachable by design)

Added 12 targeted tests:
- 6 codegen tests in src/vm/compile_cranelift.rs (one per new opcode)
  that build a chunk and run it through the AOT pipeline to confirm
  object emission
- 6 interpreter error-path tests in src/interpreter/mod.rs covering
  the non-number type-error arms for sqrt/log/exp/sin/cos/pow

Coverage deltas:
- interpreter/mod.rs: 96.50% → 96.76%
- vm/compile_cranelift.rs: 88.08% → 88.81%
- vm/mod.rs: 87.70% → 87.83%

The remaining uncovered new lines are 5 `panic!` arms inside test
functions whose inputs guarantee a non-panic return - covering them
would require making the tests fail. If codecov still flags them
after this push, the appropriate fix is a codecov-ignore on test
code rather than adding misleading tests.
@danieljohnmorris danieljohnmorris merged commit 256466f into main May 11, 2026
5 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/math-builtins branch May 11, 2026 20:12
danieljohnmorris added a commit that referenced this pull request May 11, 2026
Follow-up to PR #162 math builtins. Adds the four remaining transcendentals every analysis program needs: phase angles atan2, decibels log10, bit-depth log2, oscillations tan. atan2 takes y first per C/Python convention. Opcodes 105-108.
danieljohnmorris added a commit that referenced this pull request May 11, 2026
Follow-up to PR #162 math builtins. Adds the four remaining transcendentals every analysis program needs: phase angles atan2, decibels log10, bit-depth log2, oscillations tan. atan2 takes y first per C/Python convention. Opcodes 105-108.
danieljohnmorris added a commit that referenced this pull request May 12, 2026
Follow-up to PR #162 math builtins. Adds the four remaining transcendentals every analysis program needs: phase angles atan2, decibels log10, bit-depth log2, oscillations tan. atan2 takes y first per C/Python convention. Opcodes 105-108.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant