From d0f0535b68f7430c4a0b4e9c16954e0116832082 Mon Sep 17 00:00:00 2001 From: hyperpolymath Date: Sun, 17 May 2026 00:18:40 +0100 Subject: [PATCH 1/2] playground: implement source so just dev/test/probability work (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- playground/examples/uncertainty.ts | 81 +++++++++++++++++++++ playground/src/main.ts | 22 ++++++ playground/src/probability.ts | 106 ++++++++++++++++++++++++++++ playground/src/ternary.ts | 106 ++++++++++++++++++++++++++++ playground/test/probability_test.ts | 56 +++++++++++++++ playground/test/ternary_test.ts | 60 ++++++++++++++++ 6 files changed, 431 insertions(+) create mode 100644 playground/examples/uncertainty.ts create mode 100644 playground/src/main.ts create mode 100644 playground/src/probability.ts create mode 100644 playground/src/ternary.ts create mode 100644 playground/test/probability_test.ts create mode 100644 playground/test/ternary_test.ts diff --git a/playground/examples/uncertainty.ts b/playground/examples/uncertainty.ts new file mode 100644 index 0000000..2dad43f --- /dev/null +++ b/playground/examples/uncertainty.ts @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2025 hyperpolymath +// +// BetLang playground — uncertainty-aware number tower (sample). +// +// The full language exposes ~14 uncertainty number systems as its type +// system. This example demonstrates two of them — interval arithmetic and +// Gaussian (mean ± sd) propagation — and how the lazy ternary core lets a +// comparison return Unknown when two uncertain quantities overlap. + +import { type Tri } from '../src/ternary.ts'; + +/** Closed real interval [lo, hi]. */ +export interface Interval { + lo: number; + hi: number; +} + +export const iv = (lo: number, hi: number): Interval => ({ + lo: Math.min(lo, hi), + hi: Math.max(lo, hi), +}); + +export function addI(a: Interval, b: Interval): Interval { + return iv(a.lo + b.lo, a.hi + b.hi); +} + +export function mulI(a: Interval, b: Interval): Interval { + const ps = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi]; + return iv(Math.min(...ps), Math.max(...ps)); +} + +/** Ternary comparison: definite when disjoint, Unknown when they overlap. */ +export function ltI(a: Interval, b: Interval): Tri { + if (a.hi < b.lo) return 'T'; + if (a.lo > b.hi) return 'F'; + return 'U'; +} + +/** Gaussian number: mean with standard deviation. */ +export interface Gaussian { + mu: number; + sd: number; +} + +export const gauss = (mu: number, sd: number): Gaussian => ({ mu, sd: Math.abs(sd) }); + +/** First-order (uncorrelated) propagation through sum and product. */ +export function addG(a: Gaussian, b: Gaussian): Gaussian { + return gauss(a.mu + b.mu, Math.hypot(a.sd, b.sd)); +} + +export function mulG(a: Gaussian, b: Gaussian): Gaussian { + const mu = a.mu * b.mu; + const rel = Math.hypot(a.sd / a.mu, b.sd / b.mu); + return gauss(mu, Math.abs(mu) * rel); +} + +function main(): void { + console.log('=== BetLang Uncertainty Modeling ===\n'); + + const a = iv(2, 4); + const b = iv(3, 5); + console.log(`Intervals: a=[${a.lo},${a.hi}] b=[${b.lo},${b.hi}]`); + console.log(` a + b = [${addI(a, b).lo}, ${addI(a, b).hi}]`); + console.log(` a * b = [${mulI(a, b).lo}, ${mulI(a, b).hi}]`); + console.log(` a < b ? -> ${ltI(a, b)} (overlap => Unknown, not a false certainty)`); + console.log(` [0,1] < [5,6] ? -> ${ltI(iv(0, 1), iv(5, 6))}`); + + const g1 = gauss(10, 1); + const g2 = gauss(20, 2); + const s = addG(g1, g2); + const p = mulG(g1, g2); + console.log(`\nGaussians: g1=${g1.mu}±${g1.sd} g2=${g2.mu}±${g2.sd}`); + console.log(` g1 + g2 = ${s.mu}±${s.sd.toFixed(4)}`); + console.log(` g1 * g2 = ${p.mu}±${p.sd.toFixed(4)}`); +} + +if (import.meta.main) { + main(); +} diff --git a/playground/src/main.ts b/playground/src/main.ts new file mode 100644 index 0000000..bdf82a0 --- /dev/null +++ b/playground/src/main.ts @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2025 hyperpolymath +// +// BetLang playground entry point (`deno task dev` / `just dev`). +// +// Runs the ternary core demo and the probabilistic layer demo in sequence, +// giving a contributor a single runnable surface for the sandbox. + +import { main as ternaryDemo } from './ternary.ts'; +import { main as probabilityDemo } from './probability.ts'; + +function main(): void { + console.log('BetLang Playground — Symbolic Probabilistic Metalanguage\n'); + ternaryDemo(); + console.log('\n' + '-'.repeat(60) + '\n'); + probabilityDemo(); + console.log('\nDone. See `just --list` for individual demos.'); +} + +if (import.meta.main) { + main(); +} diff --git a/playground/src/probability.ts b/playground/src/probability.ts new file mode 100644 index 0000000..16fa898 --- /dev/null +++ b/playground/src/probability.ts @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2025 hyperpolymath +// +// BetLang playground — probabilistic layer. +// +// Builds the non-uniform / predicate-driven choice forms on top of the +// lazy ternary core: +// (bet/weighted ...) — non-uniform probabilities +// (bet_conditional ...) — predicate-driven selection +// +// Evaluation stays lazy: a Bet is a *description* of a choice; only the +// branch that wins a draw is forced. + +import { bet, type Tri } from './ternary.ts'; + +/** A lazy, weighted branch: a relative weight and a thunk producing a value. */ +export interface Branch { + weight: number; + value: () => A; +} + +/** Deterministic, seedable PRNG (mulberry32) so demos/tests are reproducible. */ +export function rng(seed: number): () => number { + let s = seed >>> 0; + return () => { + s = (s + 0x6d2b79f5) >>> 0; + let t = s; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +/** Force exactly one branch, chosen with probability proportional to weight. */ +export function betWeighted(branches: Branch[], draw: () => number): A { + const total = branches.reduce((acc, b) => acc + b.weight, 0); + if (total <= 0) throw new Error('betWeighted: weights must sum to a positive number'); + let r = draw() * total; + for (const b of branches) { + r -= b.weight; + if (r <= 0) return b.value(); + } + return branches[branches.length - 1].value(); // float-rounding fallback +} + +/** + * Predicate-driven ternary selection. The predicate yields a Tri; a definite + * answer takes the matching branch, Unknown defers to the `uncertain` branch. + */ +export function betConditional( + predicate: () => Tri, + ifTrue: () => A, + uncertain: () => A, + ifFalse: () => A, +): A { + return bet(predicate, ifTrue, uncertain, ifFalse); +} + +/** Monte-Carlo expectation of a numeric weighted bet over `n` samples. */ +export function expectation(branches: Branch[], n: number, draw: () => number): number { + let sum = 0; + for (let i = 0; i < n; i++) sum += betWeighted(branches, draw); + return sum / n; +} + +export function main(): void { + console.log('=== BetLang Probabilistic Layer ===\n'); + + // A loaded three-sided "coin": 60% True, 30% Unknown, 10% False. + const loaded: Branch[] = [ + { weight: 0.6, value: () => 'T' as Tri }, + { weight: 0.3, value: () => 'U' as Tri }, + { weight: 0.1, value: () => 'F' as Tri }, + ]; + const draw = rng(42); + const counts: Record = { T: 0, U: 0, F: 0 }; + const N = 100_000; + for (let i = 0; i < N; i++) counts[betWeighted(loaded, draw)]++; + console.log(`Empirical distribution over ${N.toLocaleString()} draws (target 0.60/0.30/0.10):`); + console.log( + ` T=${(counts.T / N).toFixed(3)} U=${(counts.U / N).toFixed(3)} ` + + `F=${(counts.F / N).toFixed(3)}`, + ); + + // Expected payout of a weighted numeric bet. + const payout: Branch[] = [ + { weight: 1, value: () => 100 }, + { weight: 2, value: () => 10 }, + { weight: 7, value: () => 0 }, + ]; + const ev = expectation(payout, 200_000, rng(7)); + console.log(`\nExpected payout (analytic = 12.0): ${ev.toFixed(2)}`); + + // Conditional choice that stays total under Unknown. + const choice = betConditional( + () => 'U' as Tri, + () => 'committed', + () => 'hedged (predicate was Unknown)', + () => 'declined', + ); + console.log(`\nbet_conditional under Unknown -> ${choice}`); +} + +if (import.meta.main) { + main(); +} diff --git a/playground/src/ternary.ts b/playground/src/ternary.ts new file mode 100644 index 0000000..1ba6ad9 --- /dev/null +++ b/playground/src/ternary.ts @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2025 hyperpolymath +// +// BetLang playground — ternary core. +// +// BetLang's minimal core is a Kleene strong three-valued logic over +// { True, False, Unknown } plus the lazy ternary choice primitive +// (bet A B C) +// where only the selected branch is ever evaluated. +// +// The AND truth table here is exactly the one documented in the playground +// Justfile (`just ternary-demo`): conjunction is `min`, disjunction is `max` +// under the order F < U < T. + +/** The three logical values of the BetLang core. */ +export type Tri = 'T' | 'F' | 'U'; + +const ORDER: Record = { F: 0, U: 1, T: 2 }; +const BY_RANK: Tri[] = ['F', 'U', 'T']; + +/** Kleene negation: swaps T/F, fixes U. */ +export function not(a: Tri): Tri { + if (a === 'T') return 'F'; + if (a === 'F') return 'T'; + return 'U'; +} + +/** Kleene conjunction = min under F < U < T. */ +export function and(a: Tri, b: Tri): Tri { + return BY_RANK[Math.min(ORDER[a], ORDER[b])]; +} + +/** Kleene disjunction = max under F < U < T. */ +export function or(a: Tri, b: Tri): Tri { + return BY_RANK[Math.max(ORDER[a], ORDER[b])]; +} + +/** Material implication, defined as `or(not(a), b)`. */ +export function implies(a: Tri, b: Tri): Tri { + return or(not(a), b); +} + +/** + * The lazy ternary choice primitive `(bet A B C)`. + * + * Branches are passed as thunks; exactly one is forced. `selector` decides + * which branch wins — when it returns 'U' the middle branch is taken, which + * is what makes the choice *total* even under uncertainty. + */ +export function bet( + selector: () => Tri, + onTrue: () => A, + onUnknown: () => A, + onFalse: () => A, +): A { + switch (selector()) { + case 'T': + return onTrue(); + case 'F': + return onFalse(); + default: + return onUnknown(); + } +} + +/** Render a full binary truth table for a Tri operator. */ +function table(name: string, op: (a: Tri, b: Tri) => Tri): string { + const vals: Tri[] = ['T', 'U', 'F']; + const rows = vals.flatMap((a) => vals.map((b) => ` ${a} ${name} ${b} = ${op(a, b)}`)); + return [`${name} truth table:`, ...rows].join('\n'); +} + +export function main(): void { + console.log('=== BetLang Ternary Core ==='); + console.log('Values: True (T), False (F), Unknown (U)\n'); + console.log(table('AND', and)); + console.log(); + console.log(table('OR', or)); + console.log(); + console.log('NOT: NOT T = ' + not('T') + ', NOT U = ' + not('U') + ', NOT F = ' + not('F')); + console.log(); + + // Laziness demonstration: only the selected thunk runs. + let evaluated = ''; + const result = bet( + () => 'U', + () => { + evaluated = 'true-branch'; + return 1; + }, + () => { + evaluated = 'unknown-branch'; + return 0; + }, + () => { + evaluated = 'false-branch'; + return -1; + }, + ); + console.log(`Lazy (bet ? : :) on Unknown -> result=${result}, evaluated=${evaluated}`); + console.log('(only the Unknown branch ran; True/False thunks were never forced)'); +} + +if (import.meta.main) { + main(); +} diff --git a/playground/test/probability_test.ts b/playground/test/probability_test.ts new file mode 100644 index 0000000..cd0c69c --- /dev/null +++ b/playground/test/probability_test.ts @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2025 hyperpolymath + +import { assert, assertEquals, assertThrows } from '@std/assert'; +import { betConditional, betWeighted, type Branch, expectation, rng } from '../src/probability.ts'; +import { type Tri } from '../src/ternary.ts'; + +Deno.test('rng is deterministic for a fixed seed and stays in [0,1)', () => { + const a = rng(123); + const b = rng(123); + for (let i = 0; i < 50; i++) { + const x = a(); + assertEquals(x, b()); + assert(x >= 0 && x < 1); + } +}); + +Deno.test('betWeighted respects weights within Monte-Carlo tolerance', () => { + const branches: Branch[] = [ + { weight: 0.6, value: () => 'T' }, + { weight: 0.3, value: () => 'U' }, + { weight: 0.1, value: () => 'F' }, + ]; + const draw = rng(42); + const counts: Record = { T: 0, U: 0, F: 0 }; + const N = 50_000; + for (let i = 0; i < N; i++) counts[betWeighted(branches, draw)]++; + assert(Math.abs(counts.T / N - 0.6) < 0.02); + assert(Math.abs(counts.U / N - 0.3) < 0.02); + assert(Math.abs(counts.F / N - 0.1) < 0.02); +}); + +Deno.test('betWeighted rejects non-positive total weight', () => { + assertThrows(() => betWeighted([{ weight: 0, value: () => 1 }], rng(1))); +}); + +Deno.test('expectation converges to the analytic mean', () => { + const payout: Branch[] = [ + { weight: 1, value: () => 100 }, + { weight: 2, value: () => 10 }, + { weight: 7, value: () => 0 }, + ]; + // analytic EV = (1*100 + 2*10 + 7*0) / 10 = 12 + assert(Math.abs(expectation(payout, 100_000, rng(7)) - 12) < 0.5); +}); + +Deno.test('betConditional defers to the uncertain branch on Unknown', () => { + assertEquals( + betConditional(() => 'U', () => 'yes', () => 'maybe', () => 'no'), + 'maybe', + ); + assertEquals( + betConditional(() => 'T', () => 'yes', () => 'maybe', () => 'no'), + 'yes', + ); +}); diff --git a/playground/test/ternary_test.ts b/playground/test/ternary_test.ts new file mode 100644 index 0000000..6de8905 --- /dev/null +++ b/playground/test/ternary_test.ts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2025 hyperpolymath + +import { assertEquals } from '@std/assert'; +import { and, bet, implies, not, or, type Tri } from '../src/ternary.ts'; + +const ALL: Tri[] = ['T', 'U', 'F']; + +Deno.test('negation is involutive and fixes Unknown', () => { + assertEquals(not('T'), 'F'); + assertEquals(not('F'), 'T'); + assertEquals(not('U'), 'U'); + for (const v of ALL) assertEquals(not(not(v)), v); +}); + +Deno.test('AND matches the documented Justfile truth table (min)', () => { + assertEquals(and('T', 'T'), 'T'); + assertEquals(and('T', 'U'), 'U'); + assertEquals(and('T', 'F'), 'F'); + assertEquals(and('U', 'U'), 'U'); + assertEquals(and('U', 'F'), 'F'); + assertEquals(and('F', 'F'), 'F'); +}); + +Deno.test('AND and OR are commutative; De Morgan holds', () => { + for (const a of ALL) { + for (const b of ALL) { + assertEquals(and(a, b), and(b, a)); + assertEquals(or(a, b), or(b, a)); + assertEquals(not(and(a, b)), or(not(a), not(b))); + } + } +}); + +Deno.test('implies(a,b) == or(not a, b)', () => { + for (const a of ALL) { + for (const b of ALL) assertEquals(implies(a, b), or(not(a), b)); + } +}); + +Deno.test('bet is lazy: only the selected branch is forced', () => { + const forced: string[] = []; + const r = bet( + () => 'F', + () => { + forced.push('T'); + return 1; + }, + () => { + forced.push('U'); + return 2; + }, + () => { + forced.push('F'); + return 3; + }, + ); + assertEquals(r, 3); + assertEquals(forced, ['F']); +}); From 919fb5f5f1187fed0fc0ccb348255927a3b525ec Mon Sep 17 00:00:00 2001 From: hyperpolymath Date: Sun, 17 May 2026 00:19:17 +0100 Subject: [PATCH 2/2] 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 --- playground/deno.lock | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 playground/deno.lock diff --git a/playground/deno.lock b/playground/deno.lock new file mode 100644 index 0000000..afe945c --- /dev/null +++ b/playground/deno.lock @@ -0,0 +1,24 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@1": "1.0.19", + "jsr:@std/internal@^1.0.12": "1.0.13" + }, + "jsr": { + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.13": { + "integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@1", + "jsr:@std/testing@1" + ] + } +}