Skip to content

feat(wam-scala): float arithmetic, more builtins, and Phase S7 fact seam#1701

Merged
s243a merged 1 commit intomainfrom
feat/wam-scala-s7-builtins-float
Apr 29, 2026
Merged

feat(wam-scala): float arithmetic, more builtins, and Phase S7 fact seam#1701
s243a merged 1 commit intomainfrom
feat/wam-scala-s7-builtins-float

Conversation

@s243a
Copy link
Copy Markdown
Owner

@s243a s243a commented Apr 29, 2026

Summary

Three thematic additions in one PR. All four new smoke tests pass under
scalac + scala.

Before After
Generator tests 10 10
Smoke tests 18 22

This PR closes the bottom three open items on the WAM Scala plan list
that didn't depend on external infra: float arithmetic, the fail/\+
builtin gap-fill, and the start of Phase S7 — the fact-backend seam
that lets relations be supplied as declarative tuples instead of WAM
clauses.

What's in the diff

Float arithmetic — arith_float smoke test

  • evalArith now returns a Num ADT (NInt | NDouble) instead of a
    raw Int. Mixed Int/Double operations promote to Double; true
    division / always returns Double (matching SWI-Prolog); integer
    division is reachable via // and mod if a future emitter needs
    them.
  • Codegen detects float literals in get_constant / put_constant /
    set_constant / unify_constant via constant_to_scala_term/2 and
    emits FloatTerm(N) instead of treating them as atoms.
  • The CLI parser recognises floats (anything with . / e / E that
    Java parses as a Double) and emits FloatTerm; otherwise it falls
    through to the existing int / atom / list / struct logic.
  • The comparison family (=:=, =\=, <, >, =<, >=) takes a
    (Num, Num) => Boolean operator and uses Double-promoted comparison
    whenever either operand is Double.

Bug fix uncovered by float arithmetic

parse_functor_arity/3 was finding the first / in the functor
token, which broke for the division operator (rendered as //2 in WAM
text — functor / of arity 2). The compiled output was emitting
Raw("put_structure //2 A2") instead of a real PutStructure, so
Y is X / 2.0 silently failed. Fixed to use the last slash as the
arity separator. Other functors that use / would have hit this too;
this is a real fix, not just a float-specific patch.

Builtin gap-fill — builtin_fail, builtin_negation smoke tests

  • fail/0 — single-line backtrack handler.
  • \+/1 (negation-as-failure) — implemented via a meta-call helper
    metaCallSucceeds that snapshots the relevant outer state, runs
    the goal in the same WamState until completion (callStack starts
    empty so Proceed marks success), then restores everything.
    Bindings made during the goal are unwound on exit so \+ never
    propagates partial bindings to the outer scope.

Phase S7 fact backend seam — fact_source_inline_vs_sidecar test

  • New option scala_fact_sources([source(P/A, Tuples), ...]) where
    each Tuple is a list of Arity atoms (or numbers). The codegen
    expands each source into:
    1. a foreign_predicates entry (so the WAM body becomes a
      CallForeign stub);
    2. a synthesised ForeignHandler that returns ForeignMulti with
      one solution per tuple — the runtime's existing applyBindings
      • backtracking machinery filters tuples against the input args;
    3. intern_atoms entries for every atom appearing in any tuple.
  • Expansion happens via expand_fact_sources_in_options/2 at the top
    of write_wam_scala_project/3, so the rest of the pipeline never
    needs to know about fact sources as a distinct concept. User-supplied
    foreign_predicates / scala_foreign_handlers / intern_atoms are
    preserved by union with the synthesised entries.
  • The parity test runs the same query set against:
    1. wam_pair_inline/2 — WAM-compiled facts (3 ground clauses);
    2. wam_pair_sidecar/2 — same tuples passed through
      scala_fact_sources.
      Both return identical answers across positive and negative cases.

Test plan

  • swipl -g 'use_module(library(plunit)),consult("tests/test_wam_scala_generator.pl"),run_tests,halt' -t 'halt(1)' → 10/10 pass.
  • With scalac on PATH or SCALA_SMOKE_TESTS=1:
    swipl -g 'use_module(library(plunit)),consult("tests/test_wam_scala_runtime_smoke.pl"),run_tests,halt' -t 'halt(1)' → 22/22 pass.
  • Without scalac: smoke unit reports "No tests to run" (gated by scala_available/0).

Out of scope

  • Sidecar fact sources backed by external storage — the current source(...)
    form is inline tuples only. File / LMDB backends slot into the same
    scala_fact_sources option in S8.
  • call/1, call/2+, findall/3, bagof/3 — the meta-call
    scaffolding from \+/1 makes call/1 straightforward to add later
    but it isn't included here.
  • Floating-point trig / log / pow — only +, -, *, /, mod, and
    unary +/- are supported in evalArith.

🤖 Generated with Claude Code

Three thematic additions in one PR. All four new smoke tests pass under
scalac+scala. Test counts: 10 generator (unchanged) + 22 e2e smoke
(was 18).

Float arithmetic — one new smoke test (`arith_float`):

- `evalArith` now returns a Num ADT (NInt | NDouble) instead of a raw
  Int. Mixed Int/Double operations promote to Double; true division
  `/` always returns Double (matching SWI-Prolog); integer division
  is reachable via `//` and `mod` if a future emitter needs them.
- Codegen detects float literals in get_constant / put_constant /
  set_constant / unify_constant via constant_to_scala_term/2 and emits
  FloatTerm(N) instead of treating them as atoms.
- The CLI parser recognises floats (anything with `.` / `e` / `E` that
  Java parses as a Double) and emits FloatTerm; otherwise it falls
  through to the existing int / atom / list / struct logic.
- The numeric comparison family (=:=, =\\=, <, >, =<, >=) takes a
  (Num, Num) => Boolean operator and uses Double-promoted comparison
  whenever either operand is Double.

Bug fix uncovered by float arithmetic:

`parse_functor_arity/3` was finding the *first* "/" in the functor
token, which broke for the division operator (rendered as `//2` in
WAM text — functor `/` of arity 2). The compiled output was emitting
`Raw("put_structure //2 A2")` instead of a real PutStructure, so
`Y is X / 2.0` silently failed. Fixed to use the *last* slash as the
arity separator.

Builtin gap-fill — two new smoke tests (`builtin_fail`,
`builtin_negation`):

- `fail/0` — single-line backtrack handler.
- `\+/1` (negation-as-failure) — implemented via a meta-call helper
  `metaCallSucceeds` that snapshots the relevant outer state, runs
  the goal in the same WamState until completion (callStack starts
  empty so Proceed marks success), then restores everything. Bindings
  made during the goal are unwound on exit so \+ never propagates
  partial bindings.

Phase S7 fact backend seam — one new smoke test
(`fact_source_inline_vs_sidecar`):

- New `scala_fact_sources([source(P/A, Tuples) | ...])` option, where
  each Tuple is a list of Arity atoms (or numbers). The codegen
  expands each source into:
    * a foreign_predicates entry (so the WAM body becomes a
      CallForeign stub);
    * a synthesised ForeignHandler that returns ForeignMulti with one
      solution per tuple (the runtime's existing applyBindings +
      backtracking machinery filters tuples against the input args);
    * intern_atoms entries for every atom appearing in any tuple.
- The expansion happens via expand_fact_sources_in_options/2 at the
  top of write_wam_scala_project/3, so the rest of the pipeline never
  needs to know about fact sources as a distinct concept. User-supplied
  foreign_predicates / scala_foreign_handlers / intern_atoms are
  preserved by union with the synthesised entries.
- The parity smoke test runs the same query set against:
    1. wam_pair_inline/2 — WAM-compiled facts (3 ground clauses);
    2. wam_pair_sidecar/2 — same tuples passed through scala_fact_sources.
  Both return identical answers across positive and negative cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@s243a s243a merged commit 1b1dcbc into main Apr 29, 2026
4 checks 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

Development

Successfully merging this pull request may close these issues.

1 participant