Skip to content

Own backend, step 4: numbers - #43

Merged
jeany55 merged 5 commits into
trigscript-4from
numbers
Oct 20, 2026
Merged

jeany55 merged 5 commits into
trigscript-4from
numbers

Conversation

@jeany55

@jeany55 jeany55 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Step 4 of the way off eudplib: every number a program works with, in a map of our own. All 59 of the ledger's number rows are taken. Each part has its own section in docs/v4-plan.md.

Follows #42 (merged first; this PR was stacked on it).

Ledger Before After
3 Arrays of a fixed length 53 of 54 54 of 54
4 Numbers 0 of 59 59 of 59

No other row changed.

What it adds

  1. Products, quotients and remainders of two values and by a constant, signed and u32. A product adds one side Horner's way in a doubling trigger. A division doubles the remainder, with one shared step holding the divisor.
  2. & | ^ as values, and shifts by a value. Bits are masked writes: an action's mask field holds the other side, as eudplib does it, without the calls. Shift counts of 32 or more saturate, as TrigScript has it.
  3. Fixed point: products and quotients by a constant and of two values, a constant over a value, and a fixed number printed.
  4. Random numbers. random(n) and random() draw the game's own random bits through Set Switch to a random state. A draw that comes out at n or more is made again, so every number below n is exactly as likely. eudplib's f_dwrand % n leans slightly towards small numbers.
  5. Math.sqrt, sin, cos, atan2, and u8 / u16.
    • The square root keeps the root at the place its bits end up, so it only gains constants, and the remainder doubles instead. A shared compare trigger steps its own next pointer into a pair of triggers laid out for each step.
    • Sine, cosine and arc tangent are statements the emitter already makes. The arc tangent's quotient takes 17 steps, not 47.
    • Stores into u8 and u16 stop at their top.

Measured

Each probe was built both ways and played in the lifted 1.16.1 game, weighed with Remastered's costs. Every line printed is the same both ways and ends "ok". Times are for 200 of each:

Ours eudplib
products of two values 0.677 ms 0.943 ms
quotients of two values 0.825 ms 1.175 ms
shifts by a value (three kinds) 0.834 ms 2.399 ms
& | ^ of two values 0.086 ms 0.217 ms
fixed x * y 0.524 ms 0.677 ms
fixed x / y 0.693 ms 1.093 ms
random(j % 10 + 1) 0.257 ms 1.836 ms
Math.sqrt(x), x fixed 0.389 ms 1.348 ms
Math.sqrt(n), n whole 0.653 ms 3.672 ms
Math.sin / Math.cos 1.085 ms 2.261 ms
Math.atan2 1.343 ms 2.300 ms

The probes are probes/own-numbers.ts, own-bits.ts, own-fixed.ts, own-random.ts and own-math.ts.

Fixed on the way, in both builds

  • A whole number's square root below zero. It is read from 0 up, so the answer is the largest fixed number, but its value range was 0. Math.sqrt(w) == … was then worked out wrong when the map was built (compiler/ranges.ts; test in tests/fixed.test.ts).

Fixed on the way, in our own build

  • Shift counts of 32 or more now saturate; the existing x << k took the count's remainder.
  • a >> 0 and a shift whose count always works out to the same number were refused; both are now taken.
  • xs[a >> 1], the last refused row of step 3, is now taken.

Checked

  • Emitter tests: random programs with every operation above, as graphs and from the laid-out bytes. 3,000 of 3,000 programs with functions are taken. The test interpreters now apply the game's masked-write semantics.
  • Random numbers: each value below the bound comes up within a fifth of its share, over 400 draws a value. The ledger's random scripts were run 200 times each way, against the simulator, with the same shares.
  • Ledger: every taken number script against the simulator.
  • Suite: 1,246 passing.
  • Test time: a random program now holds four heavy operations at most (products, quotients, shifts by a value, square roots, sines), and these tests may take 300 s. With step 4's operations they had run past the 120 s limit on CI (283 s). Here they take 23 s and 47 s, against 12 s and 25 s before step 4.

Left for later (in the plan)

  • Sine time: a sine is still mostly copies. A table of differences, or a read that gives two cells at once, would remove them.
  • Map size: each square root, sine and arc tangent is written out where it is used, so own-math.ts is 9 MB. A routine shared by every use, as the digits routine is, is the fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg

jeany55 and others added 5 commits October 20, 2026 01:05
a * b of two values, a / b and a % b of two values and by a constant, signed and u32, written where they are
used. A product tests its narrower side a bit at a time and adds the other side: its multiples as fields of
their own when the tested side is narrow (a trigger a bit), else Horner's way, the product doubling in one
action of a trigger that walks a run of bit triggers by moving its own next pointer on. A quotient by a
constant is eudplib's chain inlined, as long as the ranges allow; of two values, long division with the
remainder doubling and one shared step holding the divisor in two fields. Operands the plan copies are routed
into a routine's fields where it starts, never kept as multiples. Fills fold their scratch cell's conditions
and routes too; copies into multiples go over only the bits a range allows.

probes/own-numbers.ts, lifted game: 200 products 0.700 ms against eudplib's 0.943; by constants 0.572 / 0.730;
quotients 0.848 / 1.175; remainders 0.750 / 1.148; small numbers 0.514 / 0.764. Ledger: step 4, 17 of 59.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg
& | ^ as values, with a constant and of two values, are masked writes of a copy: the game works a masked write
on the bits under its mask alone, an addition's carry past it lost, so & and | are one write whose mask is the
other side (an action's mask is a field like any number, flipped for & by two masked additions of all ones),
and ^ two masked additions over the even and the odd bits. A shift left by a value walks a doubling trigger
through runs of empty triggers, one a bit of the count; a shift right is Horner's way over the bits that stay,
the doubling trigger counting. Counts of 32 or more leave nothing (or the sign), as TrigScript has it; the
existing << by a constant took the remainder, and >> 0 is the number itself. A scratch value read by one field
only is worked in that field. The tests' interpreters do masked writes as the game does.

probes/own-bits.ts, lifted game: masks 0.063 ms against eudplib's 0.146; bits of two values 0.086 / 0.217;
shifts by a value 0.834 / 2.399. Products and quotients moved to 1.4–1.6x with the fill into one field.
Ledger: step 4, 27 of 59.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg
Fixed (16.16) products and quotients by a constant and of two values, a
constant over a value, and a fixed number printed, each faster than the
eudplib build in the lifted game (x*y 0.52 vs 0.68 ms for 200, x/y 0.69 vs
1.09, nine printed 0.015 vs 0.17). random(n) and random() draw the game's own
random bits through Set Switch to a random state and draw again past the
bound, so every number below n is as likely; 200 draws below a value cost
0.26 ms against eudplib's 1.84. An index that is more than a sum is worked
out first, which takes pick(list) and step 3's last row, xs[a >> 1].

Ledger: step 3 54 of 54, step 4 44 of 59 (left: sqrt, sin, cos, atan2,
u8, u16).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg
…d u16

Step 4 is done: the ledger's 59 rows of numbers are all taken by a map of
our own.

A square root keeps the root at the place its bits end up, so it only gains
constants, and doubles the remainder instead; a shared compare trigger
steps its own next pointer into a pair of triggers laid out for each step.
Sines, cosines and arc tangents are written as statements the emitter
already makes, and the arc tangent's quotient takes 17 steps, not 47. Stores
into u8 and u16 stop at their top. In the lifted game, 200 of each against
the eudplib build: sqrt 0.39 vs 1.35 ms, of a whole number 0.65 vs 3.67,
sin and cos 1.09 vs 2.26, atan2 1.34 vs 2.30.

Also fixed: a whole number's square root below zero had the value range 0,
so comparing it was worked out wrong when the map was built, in both
builds; and a shift whose count comes to a constant was refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg
CI timed out on #43: the random programs with functions took 130 s there and
the ones on a stack 283 s, past the 120 s allowed, once step 4 put products,
quotients, square roots and sines in them (a program of twenty of them is
thousands of triggers to lay out and walk). A program now holds four at most;
each kind still comes in thousands of programs. Here they take 23 s and 47 s
(12 s and 25 s before step 4), about three times that on CI, so the limit is
300 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg
@jeany55
jeany55 changed the base branch from wrong-builds to trigscript-4 October 20, 2026 12:24
@jeany55
jeany55 merged commit f4d7394 into trigscript-4 Oct 20, 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

Development

Successfully merging this pull request may close these issues.

1 participant