Skip to content

Commit 646bc29

Browse files
playground: implement source so just dev/test/probability work (#6) (#15)
* playground: implement source so just dev/test/probability work (#6) The playground/ had its toolchain (Justfile, deno.json, rescript.json) but empty src/ test/ — every runnable recipe failed. Add a faithful TypeScript realisation of BetLang's core: - src/ternary.ts — Kleene 3-valued logic (T/F/U) + lazy (bet A B C); AND/OR match the table documented in the Justfile - src/probability.ts — bet/weighted, bet_conditional, seeded RNG, expectation - src/main.ts — dev entry point wiring both demos - examples/uncertainty.ts — interval + Gaussian number-tower sample - test/*_test.ts — 10 Deno tests (truth tables, De Morgan, laziness, weighted-draw tolerance, EV convergence) just dev/test/test-verbose/ternary-demo/probability/uncertainty now all run; deno check + fmt + lint clean; test is no longer a no-op (10 passed). Closes #6 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * playground: commit deno.lock for deterministic CI Matches the convention in the canonical standards repo (tracks deno.lock). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: hyperpolymath <hyperpolymath@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent fe26843 commit 646bc29

7 files changed

Lines changed: 455 additions & 0 deletions

File tree

playground/deno.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

playground/examples/uncertainty.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2025 hyperpolymath
3+
//
4+
// BetLang playground — uncertainty-aware number tower (sample).
5+
//
6+
// The full language exposes ~14 uncertainty number systems as its type
7+
// system. This example demonstrates two of them — interval arithmetic and
8+
// Gaussian (mean ± sd) propagation — and how the lazy ternary core lets a
9+
// comparison return Unknown when two uncertain quantities overlap.
10+
11+
import { type Tri } from '../src/ternary.ts';
12+
13+
/** Closed real interval [lo, hi]. */
14+
export interface Interval {
15+
lo: number;
16+
hi: number;
17+
}
18+
19+
export const iv = (lo: number, hi: number): Interval => ({
20+
lo: Math.min(lo, hi),
21+
hi: Math.max(lo, hi),
22+
});
23+
24+
export function addI(a: Interval, b: Interval): Interval {
25+
return iv(a.lo + b.lo, a.hi + b.hi);
26+
}
27+
28+
export function mulI(a: Interval, b: Interval): Interval {
29+
const ps = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi];
30+
return iv(Math.min(...ps), Math.max(...ps));
31+
}
32+
33+
/** Ternary comparison: definite when disjoint, Unknown when they overlap. */
34+
export function ltI(a: Interval, b: Interval): Tri {
35+
if (a.hi < b.lo) return 'T';
36+
if (a.lo > b.hi) return 'F';
37+
return 'U';
38+
}
39+
40+
/** Gaussian number: mean with standard deviation. */
41+
export interface Gaussian {
42+
mu: number;
43+
sd: number;
44+
}
45+
46+
export const gauss = (mu: number, sd: number): Gaussian => ({ mu, sd: Math.abs(sd) });
47+
48+
/** First-order (uncorrelated) propagation through sum and product. */
49+
export function addG(a: Gaussian, b: Gaussian): Gaussian {
50+
return gauss(a.mu + b.mu, Math.hypot(a.sd, b.sd));
51+
}
52+
53+
export function mulG(a: Gaussian, b: Gaussian): Gaussian {
54+
const mu = a.mu * b.mu;
55+
const rel = Math.hypot(a.sd / a.mu, b.sd / b.mu);
56+
return gauss(mu, Math.abs(mu) * rel);
57+
}
58+
59+
function main(): void {
60+
console.log('=== BetLang Uncertainty Modeling ===\n');
61+
62+
const a = iv(2, 4);
63+
const b = iv(3, 5);
64+
console.log(`Intervals: a=[${a.lo},${a.hi}] b=[${b.lo},${b.hi}]`);
65+
console.log(` a + b = [${addI(a, b).lo}, ${addI(a, b).hi}]`);
66+
console.log(` a * b = [${mulI(a, b).lo}, ${mulI(a, b).hi}]`);
67+
console.log(` a < b ? -> ${ltI(a, b)} (overlap => Unknown, not a false certainty)`);
68+
console.log(` [0,1] < [5,6] ? -> ${ltI(iv(0, 1), iv(5, 6))}`);
69+
70+
const g1 = gauss(10, 1);
71+
const g2 = gauss(20, 2);
72+
const s = addG(g1, g2);
73+
const p = mulG(g1, g2);
74+
console.log(`\nGaussians: g1=${g1.mu}±${g1.sd} g2=${g2.mu}±${g2.sd}`);
75+
console.log(` g1 + g2 = ${s.mu}±${s.sd.toFixed(4)}`);
76+
console.log(` g1 * g2 = ${p.mu}±${p.sd.toFixed(4)}`);
77+
}
78+
79+
if (import.meta.main) {
80+
main();
81+
}

playground/src/main.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2025 hyperpolymath
3+
//
4+
// BetLang playground entry point (`deno task dev` / `just dev`).
5+
//
6+
// Runs the ternary core demo and the probabilistic layer demo in sequence,
7+
// giving a contributor a single runnable surface for the sandbox.
8+
9+
import { main as ternaryDemo } from './ternary.ts';
10+
import { main as probabilityDemo } from './probability.ts';
11+
12+
function main(): void {
13+
console.log('BetLang Playground — Symbolic Probabilistic Metalanguage\n');
14+
ternaryDemo();
15+
console.log('\n' + '-'.repeat(60) + '\n');
16+
probabilityDemo();
17+
console.log('\nDone. See `just --list` for individual demos.');
18+
}
19+
20+
if (import.meta.main) {
21+
main();
22+
}

playground/src/probability.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2025 hyperpolymath
3+
//
4+
// BetLang playground — probabilistic layer.
5+
//
6+
// Builds the non-uniform / predicate-driven choice forms on top of the
7+
// lazy ternary core:
8+
// (bet/weighted ...) — non-uniform probabilities
9+
// (bet_conditional ...) — predicate-driven selection
10+
//
11+
// Evaluation stays lazy: a Bet is a *description* of a choice; only the
12+
// branch that wins a draw is forced.
13+
14+
import { bet, type Tri } from './ternary.ts';
15+
16+
/** A lazy, weighted branch: a relative weight and a thunk producing a value. */
17+
export interface Branch<A> {
18+
weight: number;
19+
value: () => A;
20+
}
21+
22+
/** Deterministic, seedable PRNG (mulberry32) so demos/tests are reproducible. */
23+
export function rng(seed: number): () => number {
24+
let s = seed >>> 0;
25+
return () => {
26+
s = (s + 0x6d2b79f5) >>> 0;
27+
let t = s;
28+
t = Math.imul(t ^ (t >>> 15), t | 1);
29+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
30+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
31+
};
32+
}
33+
34+
/** Force exactly one branch, chosen with probability proportional to weight. */
35+
export function betWeighted<A>(branches: Branch<A>[], draw: () => number): A {
36+
const total = branches.reduce((acc, b) => acc + b.weight, 0);
37+
if (total <= 0) throw new Error('betWeighted: weights must sum to a positive number');
38+
let r = draw() * total;
39+
for (const b of branches) {
40+
r -= b.weight;
41+
if (r <= 0) return b.value();
42+
}
43+
return branches[branches.length - 1].value(); // float-rounding fallback
44+
}
45+
46+
/**
47+
* Predicate-driven ternary selection. The predicate yields a Tri; a definite
48+
* answer takes the matching branch, Unknown defers to the `uncertain` branch.
49+
*/
50+
export function betConditional<A>(
51+
predicate: () => Tri,
52+
ifTrue: () => A,
53+
uncertain: () => A,
54+
ifFalse: () => A,
55+
): A {
56+
return bet(predicate, ifTrue, uncertain, ifFalse);
57+
}
58+
59+
/** Monte-Carlo expectation of a numeric weighted bet over `n` samples. */
60+
export function expectation(branches: Branch<number>[], n: number, draw: () => number): number {
61+
let sum = 0;
62+
for (let i = 0; i < n; i++) sum += betWeighted(branches, draw);
63+
return sum / n;
64+
}
65+
66+
export function main(): void {
67+
console.log('=== BetLang Probabilistic Layer ===\n');
68+
69+
// A loaded three-sided "coin": 60% True, 30% Unknown, 10% False.
70+
const loaded: Branch<Tri>[] = [
71+
{ weight: 0.6, value: () => 'T' as Tri },
72+
{ weight: 0.3, value: () => 'U' as Tri },
73+
{ weight: 0.1, value: () => 'F' as Tri },
74+
];
75+
const draw = rng(42);
76+
const counts: Record<Tri, number> = { T: 0, U: 0, F: 0 };
77+
const N = 100_000;
78+
for (let i = 0; i < N; i++) counts[betWeighted(loaded, draw)]++;
79+
console.log(`Empirical distribution over ${N.toLocaleString()} draws (target 0.60/0.30/0.10):`);
80+
console.log(
81+
` T=${(counts.T / N).toFixed(3)} U=${(counts.U / N).toFixed(3)} ` +
82+
`F=${(counts.F / N).toFixed(3)}`,
83+
);
84+
85+
// Expected payout of a weighted numeric bet.
86+
const payout: Branch<number>[] = [
87+
{ weight: 1, value: () => 100 },
88+
{ weight: 2, value: () => 10 },
89+
{ weight: 7, value: () => 0 },
90+
];
91+
const ev = expectation(payout, 200_000, rng(7));
92+
console.log(`\nExpected payout (analytic = 12.0): ${ev.toFixed(2)}`);
93+
94+
// Conditional choice that stays total under Unknown.
95+
const choice = betConditional(
96+
() => 'U' as Tri,
97+
() => 'committed',
98+
() => 'hedged (predicate was Unknown)',
99+
() => 'declined',
100+
);
101+
console.log(`\nbet_conditional under Unknown -> ${choice}`);
102+
}
103+
104+
if (import.meta.main) {
105+
main();
106+
}

playground/src/ternary.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2025 hyperpolymath
3+
//
4+
// BetLang playground — ternary core.
5+
//
6+
// BetLang's minimal core is a Kleene strong three-valued logic over
7+
// { True, False, Unknown } plus the lazy ternary choice primitive
8+
// (bet A B C)
9+
// where only the selected branch is ever evaluated.
10+
//
11+
// The AND truth table here is exactly the one documented in the playground
12+
// Justfile (`just ternary-demo`): conjunction is `min`, disjunction is `max`
13+
// under the order F < U < T.
14+
15+
/** The three logical values of the BetLang core. */
16+
export type Tri = 'T' | 'F' | 'U';
17+
18+
const ORDER: Record<Tri, number> = { F: 0, U: 1, T: 2 };
19+
const BY_RANK: Tri[] = ['F', 'U', 'T'];
20+
21+
/** Kleene negation: swaps T/F, fixes U. */
22+
export function not(a: Tri): Tri {
23+
if (a === 'T') return 'F';
24+
if (a === 'F') return 'T';
25+
return 'U';
26+
}
27+
28+
/** Kleene conjunction = min under F < U < T. */
29+
export function and(a: Tri, b: Tri): Tri {
30+
return BY_RANK[Math.min(ORDER[a], ORDER[b])];
31+
}
32+
33+
/** Kleene disjunction = max under F < U < T. */
34+
export function or(a: Tri, b: Tri): Tri {
35+
return BY_RANK[Math.max(ORDER[a], ORDER[b])];
36+
}
37+
38+
/** Material implication, defined as `or(not(a), b)`. */
39+
export function implies(a: Tri, b: Tri): Tri {
40+
return or(not(a), b);
41+
}
42+
43+
/**
44+
* The lazy ternary choice primitive `(bet A B C)`.
45+
*
46+
* Branches are passed as thunks; exactly one is forced. `selector` decides
47+
* which branch wins — when it returns 'U' the middle branch is taken, which
48+
* is what makes the choice *total* even under uncertainty.
49+
*/
50+
export function bet<A>(
51+
selector: () => Tri,
52+
onTrue: () => A,
53+
onUnknown: () => A,
54+
onFalse: () => A,
55+
): A {
56+
switch (selector()) {
57+
case 'T':
58+
return onTrue();
59+
case 'F':
60+
return onFalse();
61+
default:
62+
return onUnknown();
63+
}
64+
}
65+
66+
/** Render a full binary truth table for a Tri operator. */
67+
function table(name: string, op: (a: Tri, b: Tri) => Tri): string {
68+
const vals: Tri[] = ['T', 'U', 'F'];
69+
const rows = vals.flatMap((a) => vals.map((b) => ` ${a} ${name} ${b} = ${op(a, b)}`));
70+
return [`${name} truth table:`, ...rows].join('\n');
71+
}
72+
73+
export function main(): void {
74+
console.log('=== BetLang Ternary Core ===');
75+
console.log('Values: True (T), False (F), Unknown (U)\n');
76+
console.log(table('AND', and));
77+
console.log();
78+
console.log(table('OR', or));
79+
console.log();
80+
console.log('NOT: NOT T = ' + not('T') + ', NOT U = ' + not('U') + ', NOT F = ' + not('F'));
81+
console.log();
82+
83+
// Laziness demonstration: only the selected thunk runs.
84+
let evaluated = '';
85+
const result = bet(
86+
() => 'U',
87+
() => {
88+
evaluated = 'true-branch';
89+
return 1;
90+
},
91+
() => {
92+
evaluated = 'unknown-branch';
93+
return 0;
94+
},
95+
() => {
96+
evaluated = 'false-branch';
97+
return -1;
98+
},
99+
);
100+
console.log(`Lazy (bet ? : :) on Unknown -> result=${result}, evaluated=${evaluated}`);
101+
console.log('(only the Unknown branch ran; True/False thunks were never forced)');
102+
}
103+
104+
if (import.meta.main) {
105+
main();
106+
}

0 commit comments

Comments
 (0)