Context
specs/tri/math/probability.t27 is the discrete-and-continuous distribution module of the tri
math family (it sits beside constants.t27, bezier.t27, statistics.t27, polynomial.t27,
matrix.t27, measurement.t27, math.t27). It declares no types at all: it opens with
module TriProbability;, two use lines (base::types, math::constants), one section banner
3. Core Functions, and five functions - bernoulli, binomial, poisson, normal,
exponential. All five are stubs. There is no 2. Types section, which is itself informative:
unlike bezier.t27 (which declares Point and BezierCurve) this module is meant to be pure
scalar arithmetic - f64 in, f64 out, no struct, no allocator, no deinit.
This file has two further problems beyond the empty bodies, and fixing both is part of the task.
Problem one: the signatures are generated placeholders and are wrong. Every one of the five
returns -> void, and every one takes exactly one parameter. A probability function that returns
nothing computes nothing, and binomial(n: usize) cannot be a binomial anything - the binomial
pmf needs a success probability and an outcome, and it has neither. This is the one place where
this work order departs from its sibling work orders for polynomial.t27 and linked_list.t27:
there you were told to keep every signature byte-for-byte. Here you must replace them. The
exact replacements are given below, and the comment line above each function must be updated to
match it.
Problem two: the five test blocks are generated placeholders in the dead test form. Each one
reads given input = default_input() / when result = <fn>(input) / then result != undefined.
default_input() is not declared anywhere in this file - the sibling specs/tri/math/bezier.t27
had to add one at line 122 to make the same generated placeholders name something real, and this
file has no such helper. Worse, these blocks are written in the gherkin form
(test <bare_name> followed by given / when / then lines). That form parses, and the parser
even counts it as a test block, but its body is silently discarded: nothing inside the
given / when / then lines is retained, so the block asserts nothing whatsoever. The reported
upstream figure is 7623 such blocks asserting nothing. Measured over specs/** in this checkout:
845 .t27 files carry 12933 gherkin-form test headers against 1623 brace-form ones, and 560 of
the gherkin blocks are the exact given input = default_input() placeholder shape. Five of those
560 are in this file.
So: a green-looking test count here is worth nothing today. Your tests must be written in the
brace form, which is the only form whose body survives:
test "embedding_is_normalized" {
const formula = Formula {
.id = "test",
.name = "test",
.sector = .qcd,
.value = 0.118034,
.complexity = 3,
};
const embedding = embed_formula(formula);
const norm = l2_norm(&embedding.vector);
assert(abs(norm - 1.0) < 0.001);
assert(embedding.normalized == true);
}
That is specs/memory/formula_embed.t27 lines 170-182, verbatim - quoted test name, braces,
const locals, assert(...) calls each ending in a semicolon, and a float compared against a
tolerance rather than with ==. Copy that shape.
.t27 is a hand-authored source language, not compiler output. You are writing source.
Exact current state
$ cd "$(git rev-parse --show-toplevel)"
$ wc -l specs/tri/math/probability.t27
66 specs/tri/math/probability.t27
$ grep -c 'TODO: Implement' specs/tri/math/probability.t27
5
$ grep -cE '^ fn ' specs/tri/math/probability.t27
5
$ grep -c 'default_input' specs/tri/math/probability.t27
5
$ grep -c 'void' specs/tri/math/probability.t27
10
Ten, not five: each function contributes a -> void on its fn line and a second void on the
generated comment line directly above it. Both have to go.
$ grep -cE '^[[:space:]]*(given|and|when|then)[[:space:]]' specs/tri/math/probability.t27
15
Five placeholder blocks, three dead lines each.
$ grep -cE '^[[:space:]]*test "' specs/tri/math/probability.t27
0
Not one surviving test body in the whole file.
$ grep -cE '^[[:space:]]*fn (bernoulli|binomial|poisson|normal|exponential)\(.*\) -> f64 \{' specs/tri/math/probability.t27
0
Sixty-six lines, five declared functions, five stubs, five placeholder tests that assert nothing,
and zero functions that return a number.
Two baselines this work order does not state, because the mirror it was written against
contains only specs/** and t27c could not be run: the three t27c parse counters and the
t27c count node-type total. Run both before you touch anything and record what they print - that
is step zero, and criteria 1d, 7 and 8 are measured against what you record.
What to write
1. Replace the five signatures
Each replacement keeps the function's existing name, keeps its existing first parameter with the
same name and the same type and in the same position, appends only the parameters the distribution
mathematically requires, and turns -> void into -> f64. No new types are introduced: f64 and
usize are the only two types the file already uses. Write these exactly, each with its comment
line directly above it:
// bernoulli(p: f64, k: usize) -> f64
fn bernoulli(p: f64, k: usize) -> f64 {
// binomial(n: usize, k: usize, p: f64) -> f64
fn binomial(n: usize, k: usize, p: f64) -> f64 {
// poisson(lambda: f64, k: usize) -> f64
fn poisson(lambda: f64, k: usize) -> f64 {
// normal(mean: f64, std_dev: f64, x: f64) -> f64
fn normal(mean: f64, std_dev: f64, x: f64) -> f64 {
// exponential(lambda: f64, x: f64) -> f64
fn exponential(lambda: f64, x: f64) -> f64 {
The comment line is not decoration and not optional. The generated comments in the file today
restate the signature (// bernoulli(p: f64) ...), so a comment left unchanged above a changed
fn line is a comment that lies. Update all five. Use the ASCII -> in the comment, as written
above.
Every one of the five returns a probability mass (bernoulli, binomial, poisson) or a
probability density (normal, exponential) evaluated at one point. All five are pure: no
allocation, no I/O, no mutation of anything outside the function.
2. Fill the five bodies
bernoulli(p, k). Domain: p in [0.0, 1.0], k in {0, 1}. Returns p for k == 1 and
1.0 - p for k == 0. Boundary: k >= 2 is outside the support - return 0.0. p < 0.0 or
p > 1.0 is outside the domain - return 0.0 for both values of k, and do not clamp. The
reason is concrete: without that guard bernoulli(1.5, 0) computes 1.0 - 1.5 and hands the
caller a probability of -0.5.
binomial(n, k, p). Domain: k in 0..=n, p in [0.0, 1.0]. Returns
C(n, k) * p^k * (1 - p)^(n - k). Boundaries: k > n returns 0.0; p outside [0.0, 1.0]
returns 0.0; n == 0 is the empty-input case and is not an error - zero trials produce zero
successes with certainty, so binomial(0, 0, p) is 1.0 (the empty product) and
binomial(0, k, p) for any k >= 1 is 0.0. Compute C(n, k) multiplicatively -
c = 1; for i in 1..=k: c = c * (n - k + i) / i - using min(k, n - k) as the loop bound. Do not
compute it as n! / (k! * (n - k)!): 21! already overflows a 64-bit unsigned integer, and this
function is expected to work for n far above 20.
poisson(lambda, k). Domain: lambda >= 0.0, k any usize. Returns
exp(-lambda) * lambda^k / k!. Boundaries: lambda < 0.0 returns 0.0; lambda == 0.0 is the
degenerate point mass at zero, so poisson(0.0, 0) is 1.0 and poisson(0.0, k) for k >= 1 is
0.0 - and note that this case must be handled before you evaluate lambda^k, so that
0.0^0 never has to be decided by the floating point unit. Accumulate lambda^k / k! as the
running product for i in 1..=k: term = term * lambda / i, never as a power divided by a
factorial: for lambda = 20, k = 25 the numerator and the denominator both overflow while the
quotient is an ordinary number near 0.05.
normal(mean, std_dev, x). Domain: std_dev > 0.0; mean and x range over the whole real
line. Returns (1.0 / (std_dev * sqrt(2 * pi))) * exp(-0.5 * z * z) where z = (x - mean) / std_dev.
Boundary - zero variance: std_dev <= 0.0 returns 0.0, and this must be a guard placed
before any division. A zero-variance normal is a Dirac delta with no finite density; naive code
divides by zero and returns inf at x == mean and NaN everywhere else, because the exponent
becomes 0.0 / 0.0. Neither is an acceptable return value from this function.
exponential(lambda, x). Domain: lambda > 0.0, x >= 0.0. Returns lambda * exp(-lambda * x).
Boundaries: x < 0.0 is below the support - return 0.0; lambda <= 0.0 is outside the domain -
return 0.0. Note that the density at the left edge of the support is lambda itself, not 1.0
and not 0.0, and that it exceeds 1.0 whenever lambda > 1.0: a density is not a
probability and is not capped at one. This is the distribution the module header comment names, so
it is the one to get exactly right.
The one rule that covers all five: any argument outside the stated domain, and any outcome
outside the stated support, returns exactly 0.0. Never a negative number, never inf, never
NaN. The only two returns of 1.0 from a boundary are the two genuine degenerate point masses
named above: binomial(0, 0, p) and poisson(0.0, 0).
Use the builtins @exp and @sqrt for the transcendentals. Both are established in this corpus -
@exp at specs/ml/activation/gelu_activation.t27 line 71, @sqrt at specs/numeric/gf16.t27
line 448 and specs/vsa/vsa_core.t27 lines 234-235. Do not hand-roll a Taylor series unless
you also do range reduction and state the resulting error bound in a comment: the 12-term series in
specs/math/radix_economy.t27 lines 139-151 is the house example of the naive version, and its
next omitted term at x = -2 is 2^13 / 13! = 1.32e-6, which is already coarser than the
tolerance this task asks for.
Body style: specs/tri/math/bezier.t27 lines 28-69 is the house form of a filled -> type { }
body in this exact directory. Constructs measured to parse clean there: if (cond) { },
let and let mut locals, for (0..n) |i| { }, return expr;, field access, indexing, struct
literals, &x, and @as / @floatFromInt / @constCast.
3. Replace the five placeholder tests
Delete all five of these blocks. They are generated stubs, default_input() does not exist in this
file, and their bodies are discarded by the parser anyway:
test bernoulli_basic_case
given input = default_input()
when result = bernoulli(input)
then result != undefined
(and the identical binomial_basic_case, poisson_basic_case, normal_basic_case,
exponential_basic_case.)
Do not rescue them by adding a default_input() helper the way bezier.t27 did. A helper that
feeds a dead block still produces a test that asserts nothing.
Write brace-form tests in their place. Every float comparison carries an explicit epsilon and uses
@abs, which is the form the two finished siblings in this directory use (@abs appears 7 times
in bezier.t27 and 4 times in constants.t27; the bare abs( spelling also exists in the corpus,
at specs/memory/formula_embed.t27 line 180 and specs/rings/ring_103_phi_sgd.t27 line 52 - match
your directory and use @abs). Never compare a float with ==, not even where the value is exact
in binary.
Two tolerances, and each test must state which it is using by writing the literal:
1e-12 for every value that is exact in binary floating point - all the Bernoulli and
binomial values, every boundary 0.0, and exponential(2.0, 0.0). No transcendental is involved,
so there is nothing to give away.
1e-9 for every value that passes through @exp or @sqrt. The expected values below are
given to 15 significant digits, which is why the tolerance is 1e-9 and not 1e-15.
These are the expected values. Each one must appear in a test, with the arithmetic that produces
it reproduced in a comment above the assert so a reviewer can re-derive it.
Constants used below, to 15 significant digits:
e^-1 = 0.367879441171442, e^-2 = 0.135335283236613, e^-0.5 = 0.606530659712633,
sqrt(2*pi) = 2.506628274631000, 1 / sqrt(2*pi) = 0.398942280401433.
| call |
expected |
how it is derived |
eps |
bernoulli(0.25, 1) |
0.25 |
p |
1e-12 |
bernoulli(0.25, 0) |
0.75 |
1 - 0.25 |
1e-12 |
bernoulli(0.25, 0) + bernoulli(0.25, 1) |
1.0 |
0.75 + 0.25, the pmf sums to one |
1e-12 |
bernoulli(0.25, 2) |
0.0 |
k outside the support {0, 1} |
1e-12 |
bernoulli(1.5, 1) |
0.0 |
p outside [0, 1]; the guard that stops -0.5 |
1e-12 |
binomial(5, 2, 0.5) |
0.3125 |
C(5,2) * 0.5^2 * 0.5^3 = 10 * 0.25 * 0.125 = 10/32 |
1e-12 |
binomial(0, 0, 0.5) |
1.0 |
C(0,0) = 1, empty product; zero trials, zero successes |
1e-12 |
binomial(0, 1, 0.5) |
0.0 |
one success is impossible in zero trials |
1e-12 |
binomial(5, 6, 0.5) |
0.0 |
k > n |
1e-12 |
binomial(5, 0..5, 0.5) summed |
1.0 |
(1+5+10+10+5+1)/32 = 32/32 |
1e-12 |
poisson(2.0, 3) |
0.180447044315484 |
e^-2 * 2^3/3! = 0.135335283236613 * 4/3; 0.135335283236613 * 4 = 0.541341132946452, / 3 |
1e-9 |
poisson(1.0, 0) |
0.367879441171442 |
e^-1 * 1^0/0! = e^-1 * 1 |
1e-9 |
poisson(0.0, 0) |
1.0 |
degenerate point mass at zero |
1e-12 |
poisson(0.0, 3) |
0.0 |
same degenerate mass, k >= 1 |
1e-12 |
poisson(-1.0, 2) |
0.0 |
lambda outside the domain |
1e-12 |
normal(0.0, 1.0, 0.0) |
0.398942280401433 |
1 / (1 * sqrt(2*pi)); 2*pi = 6.28318530717959, sqrt = 2.506628274631000, reciprocal |
1e-9 |
normal(0.0, 1.0, 1.0) |
0.241970724519143 |
0.398942280401433 * e^-0.5 = 0.398942280401433 * 0.606530659712633 |
1e-9 |
normal(2.0, 1.0, 2.0) |
0.398942280401433 |
shift invariance: z = (2-2)/1 = 0, same as the peak above |
1e-9 |
normal(0.0, 2.0, 0.0) |
0.199471140200716 |
1 / (2 * sqrt(2*pi)) = 0.398942280401433 / 2; doubling the spread halves the peak |
1e-9 |
normal(0.0, 0.0, 0.0) |
0.0 |
zero variance; the guard that stops inf and NaN |
1e-12 |
exponential(2.0, 0.0) |
2.0 |
lambda * e^0 = 2 * 1; the density at zero is lambda, and it is above one |
1e-12 |
exponential(1.0, 1.0) |
0.367879441171442 |
1 * e^-1 |
1e-9 |
exponential(2.0, 0.5) |
0.735758882342885 |
2 * e^(-2*0.5) = 2 * e^-1 = 2 * 0.367879441171442 |
1e-9 |
exponential(2.0, -1.0) |
0.0 |
x below the support [0, inf) |
1e-12 |
exponential(0.0, 1.0) |
0.0 |
lambda outside the domain |
1e-12 |
Twenty-four required values. Group them into at least twelve brace-form test blocks - at least two
per function, one for the interior of the domain and one for the boundary, plus the two
sums-to-one invariants. A block looks like this:
test "exponential_density_at_zero_equals_lambda" {
// lambda * exp(0) = 2.0 * 1.0 = 2.0; a density is not capped at 1.0
const d = exponential(2.0, 0.0);
assert(@abs(d - 2.0) < 1e-12);
}
test "poisson_pmf_matches_hand_computed_value" {
// exp(-2) * 2^3 / 3! = 0.135335283236613 * 8 / 6 = 0.180447044315484
const p = poisson(2.0, 3);
assert(@abs(p - 0.180447044315484) < 1e-9);
}
Do not rewrite the module TriProbability; line, the two use lines, the SPDX header, the
module header comment, or the 3. Core Functions / TDD: Tests banners. The module header comment
carries the family identity line; leave it exactly as it is and do not reproduce it in new code.
The file declares no constant for it and you must not invent a cross-module call to fetch one -
there is no resolver check in this pipeline, so a call to a symbol that does not exist here will
parse happily and mean nothing.
Acceptance criteria
Every criterion below is a command with an expected output, and every one of them
fails on the file as it stands today. The now column was produced by running the
command against this file, except for the two t27c rows, which are marked record it
because the mirror this work order was written against carries only specs/**. Re-run
every one of them before you start; the measured ones will give you the same numbers,
and that is the point - a criterion that already passes before any work is done proves
nothing.
-
- all 5 named functions still exist, all 5 return
f64, every function body is non-empty, and t27c parse reports recovery-events 0, declarations-swallowed 0, lexer-discarded-chars 0
-
grep -c 'TODO: Implement' specs/tri/math/probability.t27 prints 0
-
grep -c 'void' specs/tri/math/probability.t27 prints 0
-
grep -c 'default_input' specs/tri/math/probability.t27 prints 0 and the gherkin keyword-line count is 0
-
grep -cE '^[[:space:]]*test "' specs/tri/math/probability.t27 prints at least 12 and grep -cE 'assert\(' specs/tri/math/probability.t27 prints at least 24
-
grep -cE '@abs\(.*\) < 1e-' specs/tri/math/probability.t27 prints at least 24
-
- the real-code node count is at least 10
-
t27c parse specs/tri/math/probability.t27 | grep -c 'kind: TestBlock' prints at least 12 and t27c count specs/tri/math/probability.t27 | head -1 reports at least 11 node types
| # |
check |
now (before you start) |
required when you are done |
| 1 |
names + -> f64 + non-empty bodies + parse |
names=5, f64=0, fns=5 nonempty=0 |
names=5, f64=5, fns=n nonempty=n (n>=5), clean |
| 2 |
stub marker |
5 |
0 |
| 3 |
void returns and void comments |
10 |
0 |
| 4 |
placeholder tests |
default_input=5, gherkin=15 |
0, 0 |
| 5 |
brace-form tests / asserts |
0 / 0 |
>= 12 / >= 24 |
| 6 |
epsilon-bearing float comparisons |
0 |
>= 24 |
| 7 |
real-code nodes |
record it |
>= 10 |
| 8 |
test blocks / node types |
record it |
>= 12 / >= 11 |
The exact commands (run from the repository root)
$ cd "$(git rev-parse --show-toplevel)"
Criterion 1 is four commands that must all hold at once. It is one criterion because
each part is worthless alone: the name grep passes today, and a file with every function
deleted would pass the body scan.
1a - the 5 names survive. Deleting or renaming a function fails here.
$ grep -cE '^[[:space:]]*(pub[[:space:]]+)?fn (bernoulli|binomial|poisson|normal|exponential)\(' specs/tri/math/probability.t27
Must print 5. It prints 5 today - this part is a guard against regression, not a task.
1b - all 5 signatures were corrected. This is the part that is a task.
$ grep -cE '^[[:space:]]*fn (bernoulli|binomial|poisson|normal|exponential)\(.*\) -> f64 \{' specs/tri/math/probability.t27
Today it prints 0. It must print 5.
1c - every body is real. This counts declared functions and how many have a body that is
more than comments. Deleting a TODO comment and leaving the braces empty does not move it.
$ python3 - specs/tri/math/probability.t27 <<'EOF'
import re,sys
s=open(sys.argv[1],encoding='utf-8').read(); n=e=0
for m in re.finditer(r'^[ \t]*(?:pub[ \t]+)?fn[ \t]+\w+',s,re.M):
i=s.find('{',m.end())
if i<0: continue
d=0
for j in range(i,len(s)):
if s[j]=='{': d+=1
elif s[j]=='}':
d-=1
if d==0: break
n+=1
if re.sub(r'//[^\n]*','',s[i+1:j]).strip(): e+=1
print('fns=%d nonempty=%d'%(n,e))
EOF
Today it prints fns=5 nonempty=0. The two numbers must be equal, and the first must be
at least 5. A private helper function you add is welcome - it raises both numbers together
and 1a still pins the 5 public names. Note that a helper will also have to satisfy 1b's sibling
rule of honesty: give it a real return type, not void, or criterion 3 will catch it.
1d - the file still parses clean.
$ ./target/release/t27c parse specs/tri/math/probability.t27 2>&1 | grep -E '^(recovery-events|declarations-swallowed|lexer-discarded-chars):'
All three must read 0. Use this grep -E form and never | tail -3: on a file with
recovery events the parser prints discarded: diagnostic lines after the three metrics,
so tail -3 shows diagnostics and makes a broken file look clean.
Criterion 2 - the stub marker is gone.
$ grep -c 'TODO: Implement' specs/tri/math/probability.t27
Today it prints 5. It must print 0.
Criterion 3 - no function returns nothing, and no comment says it does.
$ grep -c 'void' specs/tri/math/probability.t27
Today it prints 10: five -> void return types and five generated comment lines that
restate them. It must print 0. This single grep is the check that the signatures really
changed and that the comments changed with them - a corrected fn line under a stale comment
leaves this at 5, not 0.
Criterion 4 - the five placeholder tests are gone.
$ grep -c 'default_input' specs/tri/math/probability.t27
$ grep -cE '^[[:space:]]*(given|and|when|then)[[:space:]]' specs/tri/math/probability.t27
Today they print 5 and 15. Both must print 0. The first kills the calls to a function
this file never declares; the second kills the gherkin form itself, which is the form whose
body the parser throws away. Both halves are needed: a gherkin block that no longer mentions
default_input is still a block that asserts nothing.
Criterion 5 - real test blocks, with real asserts in them.
$ grep -cE '^[[:space:]]*test "' specs/tri/math/probability.t27
$ grep -cE 'assert\(' specs/tri/math/probability.t27
Today they print 0 and 0. They must print at least 12 and at least 24. The 12 is two
blocks per function (interior and boundary) plus the two sums-to-one invariants; the 24 is
one assert per row of the expected-value table above.
Criterion 6 - every float comparison names its tolerance.
$ grep -cE '@abs\(.*\) < 1e-' specs/tri/math/probability.t27
Today it prints 0. It must print at least 24 - one per row of the table. This is the
criterion that stops assert(poisson(2.0, 3) == 0.180447044315484);, which is the single most
likely way to make this file look finished while pinning a value that no correct implementation
can ever hit exactly.
Criterion 7 - real code, not prose. Comment-only bodies produce no statement or
expression nodes at all. This counts the node kinds only a real body can produce:
$ ./target/release/t27c parse specs/tri/math/probability.t27 2>&1 | grep -cE 'kind: (StmtLocal|ExprReturn|ExprCall|ExprBinary|ExprIndex|ExprFieldAccess)'
Record what it prints before you start; every body in the file is comment-only today, so
expect a very small number. It must end at at least 10. The bar is two nodes per stubbed
function (5 x 2 = 10). For scale, and quoting the measurement made for the sibling work
orders on polynomial.t27 and linked_list.t27: 88.0% of the stub-free, parse-clean files
in specs/ carry at least 2 such nodes per function and the median is 8.8. This is a floor,
not a target - a correct binomial alone will clear it.
Criterion 8 - the tests are counted by the parser, and the file became structurally richer.
$ ./target/release/t27c parse specs/tri/math/probability.t27 2>&1 | grep -c 'kind: TestBlock'
$ ./target/release/t27c count specs/tri/math/probability.t27 | head -1
Record both before you start. The source holds 5 test blocks today (counted above), so the
first is unlikely to surprise you. When you are done the first must print at least 12 and the
second must report at least 11 node types. Counting through t27c parse rather than
grep 'test ' over the source is deliberate: only blocks the parser actually accepted are
counted, so a malformed test block scores nothing. Note the trap this file walks into: the
parser counts a gherkin block too, so criterion 8 alone cannot tell a real test from a dead one.
That is exactly why criteria 4, 5 and 6 exist beside it.
| head -1 is not optional. The rest of the t27c count table is not reproducible:
rows with equal counts print in a different order on every run (measured: 3 distinct
orderings in 8 consecutive runs over the same unchanged file), and the table stops after
15 rows with ... and N more. 14 files in specs/ contain test blocks that
t27c count | grep TestBlock cannot see for exactly that reason - and they are the
richly-implemented ones, which is the state this task is trying to reach. Never read a
number off that table; the first line is the only stable part of the output.
What is not evidence
t27c typecheck is not a gate. It returns {"ok":true,"errors":0} on 500 words of
English prose and on a Python file. Do not cite it for anything.
- There is no t27 interpreter. Your test blocks will be parsed and counted, never
executed. Write them - a test block beside a function is how this corpus states what a
function must do - but never claim you ran one, that one passed, or that you verified a
numeric value by running the code. The expected values in the table above were computed by
hand and the arithmetic is shown; if you disagree with one of them, say so in your report
and show your own arithmetic. Do not silently substitute a different number.
- A rising
kind: TestBlock count is not evidence that the tests assert anything. Five
blocks in this file already assert nothing while being counted. Criteria 4, 5 and 6 are the
ones that carry the weight here.
- ASCII only. Do not introduce Cyrillic anywhere in the file: it fails
cargo build in
bootstrap/ through build.rs. English only in comments and test names.
Your VERDICT
The first thing in your final message must be the block below, with each criterion in the
issue's own words on one line, ending in exactly met, unmet or could-not-check:
## VERDICT
- 1. all 5 named functions still exist, all 5 return f64, every function body is non-empty, and t27c parse reports recovery-events 0, declarations-swallowed 0, lexer-discarded-chars 0: met
- 2. grep -c 'TODO: Implement' specs/tri/math/probability.t27 prints 0: met
- 3. grep -c 'void' specs/tri/math/probability.t27 prints 0: met
- 4. grep -c 'default_input' specs/tri/math/probability.t27 prints 0 and the gherkin keyword-line count is 0: met
- 5. grep -cE '^[[:space:]]*test "' prints at least 12 and grep -cE 'assert\(' prints at least 24: met
- 6. grep -cE '@abs\(.*\) < 1e-' prints at least 24: met
- 7. the real-code node count is at least 10: met
- 8. t27c parse | grep -c 'kind: TestBlock' prints at least 12 and t27c count | head -1 reports at least 11 node types: met
Scope
That file and nothing else. Every other issue in this set owns a different file, and two
agents editing one file is the exact failure the worktree isolation exists to prevent.
specs/tri/math/statistics.t27 is the nearest temptation - it has the identical disease
(6 stubs, 6 -> void signatures, 6 default_input placeholders) and it belongs to a
different issue. Leave it alone. Do not edit any other .t27 file, do not touch compiler/
or bootstrap/, do not add new files, and do not create any .sh or .bash file.
Boundary
specs/tri/math/probability.t27
Context
specs/tri/math/probability.t27is the discrete-and-continuous distribution module of thetrimath family (it sits beside
constants.t27,bezier.t27,statistics.t27,polynomial.t27,matrix.t27,measurement.t27,math.t27). It declares no types at all: it opens withmodule TriProbability;, twouselines (base::types,math::constants), one section banner3. Core Functions, and five functions -bernoulli,binomial,poisson,normal,exponential. All five are stubs. There is no2. Typessection, which is itself informative:unlike
bezier.t27(which declaresPointandBezierCurve) this module is meant to be purescalar arithmetic -
f64in,f64out, no struct, no allocator, nodeinit.This file has two further problems beyond the empty bodies, and fixing both is part of the task.
Problem one: the signatures are generated placeholders and are wrong. Every one of the five
returns
-> void, and every one takes exactly one parameter. A probability function that returnsnothing computes nothing, and
binomial(n: usize)cannot be a binomial anything - the binomialpmf needs a success probability and an outcome, and it has neither. This is the one place where
this work order departs from its sibling work orders for
polynomial.t27andlinked_list.t27:there you were told to keep every signature byte-for-byte. Here you must replace them. The
exact replacements are given below, and the comment line above each function must be updated to
match it.
Problem two: the five
testblocks are generated placeholders in the dead test form. Each onereads
given input = default_input()/when result = <fn>(input)/then result != undefined.default_input()is not declared anywhere in this file - the siblingspecs/tri/math/bezier.t27had to add one at line 122 to make the same generated placeholders name something real, and this
file has no such helper. Worse, these blocks are written in the gherkin form
(
test <bare_name>followed bygiven/when/thenlines). That form parses, and the parsereven counts it as a test block, but its body is silently discarded: nothing inside the
given/when/thenlines is retained, so the block asserts nothing whatsoever. The reportedupstream figure is 7623 such blocks asserting nothing. Measured over
specs/**in this checkout:845
.t27files carry 12933 gherkin-formtestheaders against 1623 brace-form ones, and 560 ofthe gherkin blocks are the exact
given input = default_input()placeholder shape. Five of those560 are in this file.
So: a green-looking test count here is worth nothing today. Your tests must be written in the
brace form, which is the only form whose body survives:
That is
specs/memory/formula_embed.t27lines 170-182, verbatim - quoted test name, braces,constlocals,assert(...)calls each ending in a semicolon, and a float compared against atolerance rather than with
==. Copy that shape..t27is a hand-authored source language, not compiler output. You are writing source.Exact current state
Ten, not five: each function contributes a
-> voidon itsfnline and a secondvoidon thegenerated comment line directly above it. Both have to go.
Five placeholder blocks, three dead lines each.
Not one surviving test body in the whole file.
Sixty-six lines, five declared functions, five stubs, five placeholder tests that assert nothing,
and zero functions that return a number.
Two baselines this work order does not state, because the mirror it was written against
contains only
specs/**andt27ccould not be run: the threet27c parsecounters and thet27c countnode-type total. Run both before you touch anything and record what they print - thatis step zero, and criteria 1d, 7 and 8 are measured against what you record.
What to write
1. Replace the five signatures
Each replacement keeps the function's existing name, keeps its existing first parameter with the
same name and the same type and in the same position, appends only the parameters the distribution
mathematically requires, and turns
-> voidinto-> f64. No new types are introduced:f64andusizeare the only two types the file already uses. Write these exactly, each with its commentline directly above it:
The comment line is not decoration and not optional. The generated comments in the file today
restate the signature (
// bernoulli(p: f64) ...), so a comment left unchanged above a changedfnline is a comment that lies. Update all five. Use the ASCII->in the comment, as writtenabove.
Every one of the five returns a probability mass (
bernoulli,binomial,poisson) or aprobability density (
normal,exponential) evaluated at one point. All five are pure: noallocation, no I/O, no mutation of anything outside the function.
2. Fill the five bodies
bernoulli(p, k). Domain:
pin[0.0, 1.0],kin{0, 1}. Returnspfork == 1and1.0 - pfork == 0. Boundary:k >= 2is outside the support - return0.0.p < 0.0orp > 1.0is outside the domain - return0.0for both values ofk, and do not clamp. Thereason is concrete: without that guard
bernoulli(1.5, 0)computes1.0 - 1.5and hands thecaller a probability of
-0.5.binomial(n, k, p). Domain:
kin0..=n,pin[0.0, 1.0]. ReturnsC(n, k) * p^k * (1 - p)^(n - k). Boundaries:k > nreturns0.0;poutside[0.0, 1.0]returns
0.0;n == 0is the empty-input case and is not an error - zero trials produce zerosuccesses with certainty, so
binomial(0, 0, p)is1.0(the empty product) andbinomial(0, k, p)for anyk >= 1is0.0. ComputeC(n, k)multiplicatively -c = 1; for i in 1..=k: c = c * (n - k + i) / i- usingmin(k, n - k)as the loop bound. Do notcompute it as
n! / (k! * (n - k)!):21!already overflows a 64-bit unsigned integer, and thisfunction is expected to work for
nfar above 20.poisson(lambda, k). Domain:
lambda >= 0.0,kanyusize. Returnsexp(-lambda) * lambda^k / k!. Boundaries:lambda < 0.0returns0.0;lambda == 0.0is thedegenerate point mass at zero, so
poisson(0.0, 0)is1.0andpoisson(0.0, k)fork >= 1is0.0- and note that this case must be handled before you evaluatelambda^k, so that0.0^0never has to be decided by the floating point unit. Accumulatelambda^k / k!as therunning product
for i in 1..=k: term = term * lambda / i, never as a power divided by afactorial: for
lambda = 20, k = 25the numerator and the denominator both overflow while thequotient is an ordinary number near 0.05.
normal(mean, std_dev, x). Domain:
std_dev > 0.0;meanandxrange over the whole realline. Returns
(1.0 / (std_dev * sqrt(2 * pi))) * exp(-0.5 * z * z)wherez = (x - mean) / std_dev.Boundary - zero variance:
std_dev <= 0.0returns0.0, and this must be a guard placedbefore any division. A zero-variance normal is a Dirac delta with no finite density; naive code
divides by zero and returns
infatx == meanandNaNeverywhere else, because the exponentbecomes
0.0 / 0.0. Neither is an acceptable return value from this function.exponential(lambda, x). Domain:
lambda > 0.0,x >= 0.0. Returnslambda * exp(-lambda * x).Boundaries:
x < 0.0is below the support - return0.0;lambda <= 0.0is outside the domain -return
0.0. Note that the density at the left edge of the support islambdaitself, not1.0and not
0.0, and that it exceeds1.0wheneverlambda > 1.0: a density is not aprobability and is not capped at one. This is the distribution the module header comment names, so
it is the one to get exactly right.
The one rule that covers all five: any argument outside the stated domain, and any outcome
outside the stated support, returns exactly
0.0. Never a negative number, neverinf, neverNaN. The only two returns of1.0from a boundary are the two genuine degenerate point massesnamed above:
binomial(0, 0, p)andpoisson(0.0, 0).Use the builtins
@expand@sqrtfor the transcendentals. Both are established in this corpus -@expatspecs/ml/activation/gelu_activation.t27line 71,@sqrtatspecs/numeric/gf16.t27line 448 and
specs/vsa/vsa_core.t27lines 234-235. Do not hand-roll a Taylor series unlessyou also do range reduction and state the resulting error bound in a comment: the 12-term series in
specs/math/radix_economy.t27lines 139-151 is the house example of the naive version, and itsnext omitted term at
x = -2is2^13 / 13! = 1.32e-6, which is already coarser than thetolerance this task asks for.
Body style:
specs/tri/math/bezier.t27lines 28-69 is the house form of a filled-> type { }body in this exact directory. Constructs measured to parse clean there:
if (cond) { },letandlet mutlocals,for (0..n) |i| { },return expr;, field access, indexing, structliterals,
&x, and@as/@floatFromInt/@constCast.3. Replace the five placeholder tests
Delete all five of these blocks. They are generated stubs,
default_input()does not exist in thisfile, and their bodies are discarded by the parser anyway:
(and the identical
binomial_basic_case,poisson_basic_case,normal_basic_case,exponential_basic_case.)Do not rescue them by adding a
default_input()helper the waybezier.t27did. A helper thatfeeds a dead block still produces a test that asserts nothing.
Write brace-form tests in their place. Every float comparison carries an explicit epsilon and uses
@abs, which is the form the two finished siblings in this directory use (@absappears 7 timesin
bezier.t27and 4 times inconstants.t27; the bareabs(spelling also exists in the corpus,at
specs/memory/formula_embed.t27line 180 andspecs/rings/ring_103_phi_sgd.t27line 52 - matchyour directory and use
@abs). Never compare a float with==, not even where the value is exactin binary.
Two tolerances, and each test must state which it is using by writing the literal:
1e-12for every value that is exact in binary floating point - all the Bernoulli andbinomial values, every boundary
0.0, andexponential(2.0, 0.0). No transcendental is involved,so there is nothing to give away.
1e-9for every value that passes through@expor@sqrt. The expected values below aregiven to 15 significant digits, which is why the tolerance is
1e-9and not1e-15.These are the expected values. Each one must appear in a test, with the arithmetic that produces
it reproduced in a comment above the assert so a reviewer can re-derive it.
Constants used below, to 15 significant digits:
e^-1 = 0.367879441171442,e^-2 = 0.135335283236613,e^-0.5 = 0.606530659712633,sqrt(2*pi) = 2.506628274631000,1 / sqrt(2*pi) = 0.398942280401433.bernoulli(0.25, 1)0.25p1e-12bernoulli(0.25, 0)0.751 - 0.251e-12bernoulli(0.25, 0) + bernoulli(0.25, 1)1.00.75 + 0.25, the pmf sums to one1e-12bernoulli(0.25, 2)0.0koutside the support{0, 1}1e-12bernoulli(1.5, 1)0.0poutside[0, 1]; the guard that stops-0.51e-12binomial(5, 2, 0.5)0.3125C(5,2) * 0.5^2 * 0.5^3 = 10 * 0.25 * 0.125 = 10/321e-12binomial(0, 0, 0.5)1.0C(0,0) = 1, empty product; zero trials, zero successes1e-12binomial(0, 1, 0.5)0.01e-12binomial(5, 6, 0.5)0.0k > n1e-12binomial(5, 0..5, 0.5)summed1.0(1+5+10+10+5+1)/32 = 32/321e-12poisson(2.0, 3)0.180447044315484e^-2 * 2^3/3! = 0.135335283236613 * 4/3;0.135335283236613 * 4 = 0.541341132946452,/ 31e-9poisson(1.0, 0)0.367879441171442e^-1 * 1^0/0! = e^-1 * 11e-9poisson(0.0, 0)1.01e-12poisson(0.0, 3)0.0k >= 11e-12poisson(-1.0, 2)0.0lambdaoutside the domain1e-12normal(0.0, 1.0, 0.0)0.3989422804014331 / (1 * sqrt(2*pi));2*pi = 6.28318530717959,sqrt = 2.506628274631000, reciprocal1e-9normal(0.0, 1.0, 1.0)0.2419707245191430.398942280401433 * e^-0.5 = 0.398942280401433 * 0.6065306597126331e-9normal(2.0, 1.0, 2.0)0.398942280401433z = (2-2)/1 = 0, same as the peak above1e-9normal(0.0, 2.0, 0.0)0.1994711402007161 / (2 * sqrt(2*pi)) = 0.398942280401433 / 2; doubling the spread halves the peak1e-9normal(0.0, 0.0, 0.0)0.0infandNaN1e-12exponential(2.0, 0.0)2.0lambda * e^0 = 2 * 1; the density at zero islambda, and it is above one1e-12exponential(1.0, 1.0)0.3678794411714421 * e^-11e-9exponential(2.0, 0.5)0.7357588823428852 * e^(-2*0.5) = 2 * e^-1 = 2 * 0.3678794411714421e-9exponential(2.0, -1.0)0.0xbelow the support[0, inf)1e-12exponential(0.0, 1.0)0.0lambdaoutside the domain1e-12Twenty-four required values. Group them into at least twelve brace-form test blocks - at least two
per function, one for the interior of the domain and one for the boundary, plus the two
sums-to-one invariants. A block looks like this:
Do not rewrite the
module TriProbability;line, the twouselines, the SPDX header, themodule header comment, or the
3. Core Functions/TDD: Testsbanners. The module header commentcarries the family identity line; leave it exactly as it is and do not reproduce it in new code.
The file declares no constant for it and you must not invent a cross-module call to fetch one -
there is no resolver check in this pipeline, so a call to a symbol that does not exist here will
parse happily and mean nothing.
Acceptance criteria
Every criterion below is a command with an expected output, and every one of them
fails on the file as it stands today. The
nowcolumn was produced by running thecommand against this file, except for the two
t27crows, which are markedrecord itbecause the mirror this work order was written against carries only
specs/**. Re-runevery one of them before you start; the measured ones will give you the same numbers,
and that is the point - a criterion that already passes before any work is done proves
nothing.
f64, every function body is non-empty, andt27c parsereports recovery-events 0, declarations-swallowed 0, lexer-discarded-chars 0grep -c 'TODO: Implement' specs/tri/math/probability.t27prints 0grep -c 'void' specs/tri/math/probability.t27prints 0grep -c 'default_input' specs/tri/math/probability.t27prints 0 and the gherkin keyword-line count is 0grep -cE '^[[:space:]]*test "' specs/tri/math/probability.t27prints at least 12 andgrep -cE 'assert\(' specs/tri/math/probability.t27prints at least 24grep -cE '@abs\(.*\) < 1e-' specs/tri/math/probability.t27prints at least 24t27c parse specs/tri/math/probability.t27 | grep -c 'kind: TestBlock'prints at least 12 andt27c count specs/tri/math/probability.t27 | head -1reports at least 11 node types-> f64+ non-empty bodies + parsenames=5, f64=0, fns=5 nonempty=0names=5, f64=5, fns=n nonempty=n (n>=5), clean50voidreturns andvoidcomments100default_input=5, gherkin=150, 00 / 0>= 12 / >= 240>= 24>= 10>= 12 / >= 11The exact commands (run from the repository root)
Criterion 1 is four commands that must all hold at once. It is one criterion because
each part is worthless alone: the name grep passes today, and a file with every function
deleted would pass the body scan.
1a - the 5 names survive. Deleting or renaming a function fails here.
Must print
5. It prints5today - this part is a guard against regression, not a task.1b - all 5 signatures were corrected. This is the part that is a task.
Today it prints
0. It must print5.1c - every body is real. This counts declared functions and how many have a body that is
more than comments. Deleting a
TODOcomment and leaving the braces empty does not move it.Today it prints
fns=5 nonempty=0. The two numbers must be equal, and the first must beat least 5. A private helper function you add is welcome - it raises both numbers together
and 1a still pins the 5 public names. Note that a helper will also have to satisfy 1b's sibling
rule of honesty: give it a real return type, not
void, or criterion 3 will catch it.1d - the file still parses clean.
All three must read
0. Use thisgrep -Eform and never| tail -3: on a file withrecovery events the parser prints
discarded:diagnostic lines after the three metrics,so
tail -3shows diagnostics and makes a broken file look clean.Criterion 2 - the stub marker is gone.
Today it prints
5. It must print0.Criterion 3 - no function returns nothing, and no comment says it does.
Today it prints
10: five-> voidreturn types and five generated comment lines thatrestate them. It must print
0. This single grep is the check that the signatures reallychanged and that the comments changed with them - a corrected
fnline under a stale commentleaves this at
5, not0.Criterion 4 - the five placeholder tests are gone.
Today they print
5and15. Both must print0. The first kills the calls to a functionthis file never declares; the second kills the gherkin form itself, which is the form whose
body the parser throws away. Both halves are needed: a gherkin block that no longer mentions
default_inputis still a block that asserts nothing.Criterion 5 - real test blocks, with real asserts in them.
Today they print
0and0. They must print at least12and at least24. The12is twoblocks per function (interior and boundary) plus the two sums-to-one invariants; the
24isone assert per row of the expected-value table above.
Criterion 6 - every float comparison names its tolerance.
Today it prints
0. It must print at least24- one per row of the table. This is thecriterion that stops
assert(poisson(2.0, 3) == 0.180447044315484);, which is the single mostlikely way to make this file look finished while pinning a value that no correct implementation
can ever hit exactly.
Criterion 7 - real code, not prose. Comment-only bodies produce no statement or
expression nodes at all. This counts the node kinds only a real body can produce:
Record what it prints before you start; every body in the file is comment-only today, so
expect a very small number. It must end at at least
10. The bar is two nodes per stubbedfunction (5 x 2 = 10). For scale, and quoting the measurement made for the sibling work
orders on
polynomial.t27andlinked_list.t27: 88.0% of the stub-free, parse-clean filesin
specs/carry at least 2 such nodes per function and the median is 8.8. This is a floor,not a target - a correct
binomialalone will clear it.Criterion 8 - the tests are counted by the parser, and the file became structurally richer.
Record both before you start. The source holds 5
testblocks today (counted above), so thefirst is unlikely to surprise you. When you are done the first must print at least
12and thesecond must report at least
11node types. Counting throught27c parserather thangrep 'test 'over the source is deliberate: only blocks the parser actually accepted arecounted, so a malformed test block scores nothing. Note the trap this file walks into: the
parser counts a gherkin block too, so criterion 8 alone cannot tell a real test from a dead one.
That is exactly why criteria 4, 5 and 6 exist beside it.
| head -1is not optional. The rest of thet27c counttable is not reproducible:rows with equal counts print in a different order on every run (measured: 3 distinct
orderings in 8 consecutive runs over the same unchanged file), and the table stops after
15 rows with
... and N more. 14 files inspecs/contain test blocks thatt27c count | grep TestBlockcannot see for exactly that reason - and they are therichly-implemented ones, which is the state this task is trying to reach. Never read a
number off that table; the first line is the only stable part of the output.
What is not evidence
t27c typecheckis not a gate. It returns{"ok":true,"errors":0}on 500 words ofEnglish prose and on a Python file. Do not cite it for anything.
executed. Write them - a
testblock beside a function is how this corpus states what afunction must do - but never claim you ran one, that one passed, or that you verified a
numeric value by running the code. The expected values in the table above were computed by
hand and the arithmetic is shown; if you disagree with one of them, say so in your report
and show your own arithmetic. Do not silently substitute a different number.
kind: TestBlockcount is not evidence that the tests assert anything. Fiveblocks in this file already assert nothing while being counted. Criteria 4, 5 and 6 are the
ones that carry the weight here.
cargo buildinbootstrap/throughbuild.rs. English only in comments and test names.Your VERDICT
The first thing in your final message must be the block below, with each criterion in the
issue's own words on one line, ending in exactly
met,unmetorcould-not-check:Scope
That file and nothing else. Every other issue in this set owns a different file, and two
agents editing one file is the exact failure the worktree isolation exists to prevent.
specs/tri/math/statistics.t27is the nearest temptation - it has the identical disease(6 stubs, 6
-> voidsignatures, 6default_inputplaceholders) and it belongs to adifferent issue. Leave it alone. Do not edit any other
.t27file, do not touchcompiler/or
bootstrap/, do not add new files, and do not create any.shor.bashfile.Boundary
specs/tri/math/probability.t27