From f9a3d7c01f8f646f0695c9eddc7234d4ac13bf7d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 23:16:36 +0000 Subject: [PATCH 1/3] examples: EigenTrust on propagators via Racket FFI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demonstrates Prologos using its underlying CALM-monotone propagator network to run the classic EigenTrust [Kamvar/Schlosser/Garcia-Molina 2003] reputation algorithm. First-class propagator surface syntax is on the roadmap but not yet wired through type checking, so the example imports `net-new-cell`, `net-add-propagator`, and `run-to-quiescence` via `foreign racket "..."` declarations and composes them from a .prologos source. Files: - `racket/prologos/foreign.rkt` — extends the FFI marshaller with Posit32 and Posit64. The bit-pattern integer is the carrier (matching `posit-impl.rkt`), so foreign-imported posit ops compose without conversion ceremony. - `racket/prologos/lib/examples/eigentrust-prop.rkt` — the FFI shim. Wraps the persistent prop-network in a small mutable handle and exposes: et-new, et-cell, et-cell-get, et-cell-set, et-sum-prop, et-run, et-snapshot, et-run-and-snapshot, et-add-iter (one EigenTrust round) Cells use a generation-tagged merge so iteration k+1 writes strictly dominate iteration k initial values; every cell is written exactly once per round and propagators fire in dataflow order. All "side-effecting" calls return a meaningful Nat (handle id, cell id, or `final-cells` list) instead of Unit. Prologos's reduction is call-by-name; an unused result expression would never be evaluated and the side effect would be silently dropped. Threading a Nat through every call forces strict evaluation order. - `racket/prologos/lib/examples/eigentrust.prologos` — the algorithm. Builds a 4-peer trust graph, drives 20 power iterations with α = 0.15, and returns the converged `[List Posit32]`. Output matches the reference implementation exactly: [0.0652, 0.4348, 0.0652, 0.4348] (sum = 1.0) Run with the small driver pattern below; the parameterize is required on Racket 8.x because the project's BSP scheduler uses `(thread #:pool 'own)` which is a Racket 9 feature: (require (file "racket/prologos/driver.rkt") (only-in (file "racket/prologos/propagator.rkt") current-parallel-executor sequential-fire-all)) (parameterize ([current-parallel-executor sequential-fire-all]) (process-file "racket/prologos/lib/examples/eigentrust.prologos")) --- racket/prologos/foreign.rkt | 56 ++- .../prologos/lib/examples/eigentrust-prop.rkt | 409 ++++++++++++++++++ .../prologos/lib/examples/eigentrust.prologos | 185 ++++++++ 3 files changed, 636 insertions(+), 14 deletions(-) create mode 100644 racket/prologos/lib/examples/eigentrust-prop.rkt create mode 100644 racket/prologos/lib/examples/eigentrust.prologos diff --git a/racket/prologos/foreign.rkt b/racket/prologos/foreign.rkt index 773bfd97e..f5d760714 100644 --- a/racket/prologos/foreign.rkt +++ b/racket/prologos/foreign.rkt @@ -70,15 +70,31 @@ [(expr-string v) v] [_ (error 'foreign "Cannot marshal to string — not a String literal: ~a" e)])) +;; Convert a Prologos Posit{32,64} to its raw bit-pattern integer. +;; This is a passthrough at the Racket level — posit-impl.rkt operates on the +;; same bit-pattern representation, so foreign-imported posit ops work directly. +;; FFI consumers that want rationals call posit{32,64}-to-rational themselves. +(define (posit32->bits e) + (match e + [(expr-posit32 bits) bits] + [_ (error 'foreign "Cannot marshal to Posit32 — not a Posit32 literal: ~a" e)])) + +(define (posit64->bits e) + (match e + [(expr-posit64 bits) bits] + [_ (error 'foreign "Cannot marshal to Posit64 — not a Posit64 literal: ~a" e)])) + (define (marshal-prologos->racket base-type val) (case base-type - [(Nat) (nat->integer val)] - [(Int) (int->integer val)] - [(Rat) (rat->rational val)] - [(Bool) (bool->boolean val)] - [(Unit) (void)] - [(Char) (char->rkt-char val)] - [(String) (string->rkt-string val)] + [(Nat) (nat->integer val)] + [(Int) (int->integer val)] + [(Rat) (rat->rational val)] + [(Bool) (bool->boolean val)] + [(Unit) (void)] + [(Char) (char->rkt-char val)] + [(String) (string->rkt-string val)] + [(Posit32) (posit32->bits val)] + [(Posit64) (posit64->bits val)] ;; Passthrough types: the Prologos IR value IS the Racket value [(Path Keyword Passthrough) val] [else @@ -110,15 +126,27 @@ (error 'foreign "Cannot marshal from Racket: expected exact rational, got ~a" n)) (expr-rat n)) +;; Convert a Racket value to a Prologos Posit{32,64} expression. +;; The carrier representation is the raw bit-pattern integer — same as +;; posit-impl.rkt. So the input is expected to be an exact integer (the bit +;; pattern). FFI consumers wanting to return a rational can compose with +;; posit{32,64}-encode themselves. +(define (bits->posit-expr width val ctor) + (unless (exact-integer? val) + (error 'foreign "Cannot marshal Racket value to Posit~a (expected bit-pattern integer): ~a" width val)) + (ctor val)) + (define (marshal-racket->prologos base-type val) (case base-type - [(Nat) (integer->nat val)] - [(Int) (integer->int val)] - [(Rat) (rational->rat val)] - [(Bool) (if val (expr-true) (expr-false))] - [(Unit) (expr-unit)] - [(Char) (expr-char val)] - [(String) (expr-string val)] + [(Nat) (integer->nat val)] + [(Int) (integer->int val)] + [(Rat) (rational->rat val)] + [(Bool) (if val (expr-true) (expr-false))] + [(Unit) (expr-unit)] + [(Char) (expr-char val)] + [(String) (expr-string val)] + [(Posit32) (bits->posit-expr 32 val expr-posit32)] + [(Posit64) (bits->posit-expr 64 val expr-posit64)] ;; Passthrough types: result is already a Prologos IR value [(Path Keyword Passthrough) val] [else diff --git a/racket/prologos/lib/examples/eigentrust-prop.rkt b/racket/prologos/lib/examples/eigentrust-prop.rkt new file mode 100644 index 000000000..d3cd09b1f --- /dev/null +++ b/racket/prologos/lib/examples/eigentrust-prop.rkt @@ -0,0 +1,409 @@ +#lang racket/base + +;;; +;;; PROPAGATOR-BASED EIGENTRUST — Racket FFI shim for Prologos. +;;; +;;; The user-facing pitch: Prologos plans to make Propagators a first-class +;;; language construct (not just compiler infrastructure). That work hasn't +;;; landed yet, so this module exposes the underlying propagator network +;;; primitives via Racket FFI so that algorithms like EigenTrust can be +;;; written in `.prologos` files using `foreign racket "..."` imports. +;;; +;;; The functions exported here wrap the persistent (immutable) prop-network +;;; in a small mutable handle (`et-net`) so Prologos sees a stateful API: +;;; +;;; et-new : Nat -> Nat ; returns a fresh handle id +;;; et-cell : Nat -> Posit32 -> Nat ; allocates a cell, returns id +;;; et-cell-get : Nat -> Nat -> Posit32 ; read score +;;; et-cell-set : Nat -> Nat -> Posit32 -> Nat ; returns cell-id (chains) +;;; et-sum-prop : Nat -> List Nat -> List Posit32 -> Posit32 -> Nat -> Nat +;;; ; installs target := bias + Σ (weight_i * src_i); returns handle (chains) +;;; et-run : Nat -> Nat ; runs to quiescence; returns handle +;;; et-snapshot : Nat -> List Nat -> List Posit32 ; read many cells +;;; +;;; Note that the "side-effecting" calls (cell-set, sum-prop, run) return a +;;; meaningful Nat — typically the handle or a relevant cell-id — instead of +;;; Unit. Prologos's reduction is call-by-name; an unused result expression +;;; would never be evaluated and the side effect would be silently dropped. +;;; Threading a Nat through every call forces strict evaluation order. +;;; +;;; The first argument of every operation (other than et-new) is a handle id +;;; — a non-negative integer returned by et-new. The Racket side keeps a +;;; private registry of network boxes keyed by id, so Prologos sees normal +;;; Nat values throughout and the propagator network mutates under the hood. +;;; +;;; The cell merge function is monotone-by-generation: each cell holds a +;;; (gen . posit-bits) pair and the merge keeps the entry with the higher +;;; generation. Iteration k+1 cells are generation k+1; this lets EigenTrust's +;;; non-monotone power iteration cooperate with the CALM-monotone propagator +;;; network — every cell is written at most once per round, ordered by +;;; data-dependency rather than imperative control flow. +;;; +;;; For Prologos, all numeric scoring happens in Posit32 (32-bit posits, 2022 +;;; standard, es=2). The bit pattern is the FFI carrier; this module converts +;;; to/from exact rationals internally for the dot-product math. +;;; + +(require racket/match + "../../propagator.rkt" + "../../syntax.rkt" + "../../posit-impl.rkt") + +(provide + et-new + et-cell + et-cell-get + et-cell-set + et-sum-prop + et-run + et-snapshot + et-run-and-snapshot + ;; Higher-level operation that Prologos can compose recursively to drive + ;; the EigenTrust power iteration: given the previous iteration's cells, + ;; allocate a fresh layer of cells and install one sum-propagator per peer + ;; that computes t_{k+1}[j] = α·p[j] + (1-α) · Σ_i C[i][j]·t_k[i]. + et-add-iter + ;; Convenience for Prologos: encode/decode Posit32 ↔ Rat so that user code + ;; can construct posit constants without going through literal sugar. + et-rat->posit32 + et-posit32->rat) + +;; ======================================== +;; Handle: integer id into a Racket-side registry of mutable network boxes. +;; ======================================== +;; +;; Prologos's foreign FFI marshals user-defined types as Passthrough — the IR +;; value is the Racket value. That works for primitives but breaks reduction +;; (whnf/nf) when a non-IR Racket struct flows through arbitrary Prologos +;; expressions. Returning a `Nat` handle keeps every foreign value an honest +;; IR Nat, and the registry side-table lookups happen entirely on the Racket +;; side. + +(struct et-net (box) #:transparent) + +(define handle-registry (make-hasheqv)) ;; integer -> et-net +(define next-handle-id (box 0)) + +(define (lookup-handle id) + (define h (hash-ref handle-registry id #f)) + (unless h (error 'eigentrust-prop "no such EigenTrust handle: ~a" id)) + h) + +(define (et-new fuel) + (define h (et-net (box (make-prop-network (max fuel 1))))) + (define id (unbox next-handle-id)) + (set-box! next-handle-id (+ id 1)) + (hash-set! handle-registry id h) + id) + +;; ======================================== +;; List helpers — walk the Prologos cons/nil chain in WHNF form. +;; ======================================== +;; +;; Foreign args have been reduced before marshalling, so List values arrive +;; as nested expr-app of expr-fvar 'cons (or qualified ::cons) terminating +;; in expr-nil / expr-fvar 'nil. + +(define (cons-name? sym) + (let ([s (symbol->string sym)]) + (or (string=? s "cons") + (let ([n (string-length s)]) + (and (>= n 6) (string=? (substring s (- n 6)) "::cons")))))) + +(define (nil-name? sym) + (let ([s (symbol->string sym)]) + (or (string=? s "nil") + (let ([n (string-length s)]) + (and (>= n 5) (string=? (substring s (- n 5)) "::nil")))))) + +;; Drill through optional type-arg applications: `(cons A) head tail` and +;; `((cons A) head) tail` both denote a cons cell with payload `head`. +(define (try-cons-decomp e) + (and (expr-app? e) + (let ([func (expr-app-func e)] + [tail (expr-app-arg e)]) + (and (expr-app? func) + (let ([head (expr-app-arg func)] + [inner (expr-app-func func)]) + (cond + [(and (expr-fvar? inner) (cons-name? (expr-fvar-name inner))) + (cons head tail)] + [(and (expr-app? inner) + (expr-fvar? (expr-app-func inner)) + (cons-name? (expr-fvar-name (expr-app-func inner)))) + (cons head tail)] + [else #f])))))) + +(define (is-nil? e) + (or (expr-nil? e) + (and (expr-fvar? e) (nil-name? (expr-fvar-name e))) + (and (expr-app? e) + (let ([f (expr-app-func e)]) + (and (expr-fvar? f) (nil-name? (expr-fvar-name f))))))) + +(define (prologos-list->list e) + (let loop ([cur e] [acc '()]) + (cond + [(is-nil? cur) (reverse acc)] + [(try-cons-decomp cur) + => (lambda (hd-tl) + (loop (cdr hd-tl) (cons (car hd-tl) acc)))] + [else (error 'eigentrust-prop + "list walk got a non-list / non-WHNF value: ~v" cur)]))) + +;; Walk a List Nat — extract Racket integers from expr-nat-val nodes. +(define (list-of-nat->ints lst-expr) + (for/list ([e (in-list (prologos-list->list lst-expr))]) + (match e + [(expr-nat-val n) n] + [(expr-zero) 0] + [(expr-suc _) (let walk ([x e] [k 0]) + (match x + [(expr-zero) k] + [(expr-nat-val m) (+ k m)] + [(expr-suc inner) (walk inner (+ k 1))] + [_ (error 'eigentrust-prop "expected Nat, got ~v" e)]))] + [_ (error 'eigentrust-prop "expected Nat in list, got ~v" e)]))) + +;; Walk a List Posit32 — extract bit-pattern integers from expr-posit32 nodes. +(define (list-of-posit32->bits lst-expr) + (for/list ([e (in-list (prologos-list->list lst-expr))]) + (match e + [(expr-posit32 bits) bits] + [_ (error 'eigentrust-prop "expected Posit32 in list, got ~v" e)]))) + +;; Build a Prologos List Posit32 from a Racket list of bit-pattern integers. +(define (bits->prologos-list-posit32 bits-list) + (foldr (lambda (bits acc) + (expr-app (expr-app (expr-fvar 'cons) (expr-posit32 bits)) acc)) + (expr-nil) + bits-list)) + +;; ======================================== +;; Cell merge: monotone "max-by-generation" lattice. +;; ======================================== +;; +;; Cell value = (cons gen bits). Iteration k writes (k . new-bits); merge +;; keeps the higher-gen entry. The first write per round is the only write +;; (propagator fires once per round when its gen-(k-1) inputs are ready), so +;; ties shouldn't arise in correct use; on a tie we keep the existing. + +(struct gen-val (gen bits) #:transparent) + +(define (gen-merge old new) + (cond + [(not (gen-val? old)) new] + [(not (gen-val? new)) old] + [(> (gen-val-gen new) (gen-val-gen old)) new] + [else old])) + +;; Initial bot value for any cell. +(define gen-bot (gen-val -1 0)) ;; gen=-1 => any real write strictly dominates + +;; Track the per-cell generation in a side table so we know which gen to +;; assign on the next write. The simplest convention: a cell allocated by +;; `et-cell` starts at gen 0 with the user-supplied initial bits. +(define (current-gen cell-val) + (if (gen-val? cell-val) (gen-val-gen cell-val) -1)) + +(define (current-bits cell-val) + (if (gen-val? cell-val) (gen-val-bits cell-val) 0)) + +;; ======================================== +;; Public FFI surface. +;; ======================================== + +;; Allocate a cell with the given Posit32 initial value (bit pattern). +;; Returns the cell-id as a non-negative integer (Nat in Prologos). +(define (et-cell hid init-bits) + (define handle (lookup-handle hid)) + (define net (unbox (et-net-box handle))) + (define-values (net* cid) (net-new-cell net (gen-val 0 init-bits) gen-merge)) + (set-box! (et-net-box handle) net*) + (cell-id-n cid)) + +;; Read the current Posit32 bits stored in a cell. +(define (et-cell-get hid cid-int) + (define handle (lookup-handle hid)) + (define net (unbox (et-net-box handle))) + (define cid (cell-id cid-int)) + (current-bits (net-cell-read net cid))) + +;; Imperatively overwrite a cell with a higher generation. Returns the cell-id +;; so calls can be chained (Prologos uses CBN; an unused result wouldn't fire). +(define (et-cell-set hid cid-int bits) + (define handle (lookup-handle hid)) + (define net (unbox (et-net-box handle))) + (define cid (cell-id cid-int)) + (define old (net-cell-read net cid)) + (define next-gen (+ 1 (current-gen old))) + (define net* (net-cell-write net cid (gen-val next-gen bits))) + (set-box! (et-net-box handle) net*) + cid-int) + +;; Install a propagator: target_cell := bias + Σ_i weight_i * src_i. +;; srcs : Prologos List Nat — input cell-ids +;; weights : Prologos List Posit32 — same length as srcs +;; bias : Posit32 bit-pattern — additive constant +;; target : Nat — output cell-id +;; Returns the handle. +;; +;; The fire function reads sources, decodes to rationals, computes the +;; weighted sum (in exact rational arithmetic, using a quire-style exact +;; accumulator), encodes back to Posit32, and writes a generation +;; max(gen(srcs)) + 1 entry into the target cell. +(define (et-sum-prop hid srcs-list weights-list bias-bits target-int) + (define handle (lookup-handle hid)) + (define net (unbox (et-net-box handle))) + (define src-ints (list-of-nat->ints srcs-list)) + (define wt-bits (list-of-posit32->bits weights-list)) + (unless (= (length src-ints) (length wt-bits)) + (error 'et-sum-prop "srcs and weights must have equal length: ~a vs ~a" + (length src-ints) (length wt-bits))) + (define src-cids (map cell-id src-ints)) + (define target-cid (cell-id target-int)) + (define wt-rats (map posit32-to-rational wt-bits)) + (define bias-rat (posit32-to-rational bias-bits)) + (define fire-fn + (lambda (n) + ;; Read each source, decode to rational, multiply by weight, sum. + ;; Track the max generation among inputs so the output gets a strictly + ;; higher generation (gen-merge keeps higher gens). + (define max-gen + (for/fold ([g -1]) ([cid (in-list src-cids)]) + (max g (current-gen (net-cell-read n cid))))) + (define sum + (for/fold ([acc bias-rat]) + ([cid (in-list src-cids)] + [w (in-list wt-rats)]) + (define src-bits (current-bits (net-cell-read n cid))) + (define src-rat (posit32-to-rational src-bits)) + (cond + [(or (eq? src-rat 'nar) (eq? acc 'nar)) 'nar] + [else (+ acc (* w src-rat))]))) + (define out-bits (cond + [(eq? sum 'nar) (posit32-encode 'nar)] + [else (posit32-encode sum)])) + (net-cell-write n target-cid (gen-val (+ 1 max-gen) out-bits)))) + (define-values (net* _pid) + (net-add-propagator net src-cids (list target-cid) fire-fn)) + (set-box! (et-net-box handle) net*) + hid) + +;; Run the network to quiescence (fixpoint of all installed propagators). +;; Returns the handle id so it can be chained (forces evaluation in CBN). +(define (et-run hid) + (define handle (lookup-handle hid)) + (define net (unbox (et-net-box handle))) + (define net* (run-to-quiescence net)) + (set-box! (et-net-box handle) net*) + hid) + +;; ======================================== +;; et-add-iter: build one EigenTrust iteration on the propagator network. +;; ======================================== +;; +;; prev-cells : Prologos List Nat — cell-ids holding t_k (current scores) +;; matrix : Prologos List (List Posit32) +;; — row-major C, i.e., matrix[i][j] = how much peer i trusts peer j +;; pretrust : Prologos List Posit32 — p, the prior trust vector +;; decay-bits : Posit32 bit pattern — α (decay constant, typically 0.1–0.2) +;; +;; Allocates one fresh cell per peer (gen=k+1), installs a sum-propagator per +;; peer reading prev-cells, writing the new cell. The propagator computes +;; +;; t_{k+1}[j] = α · p[j] + (1 − α) · Σ_i C[i][j] · t_k[i] +;; +;; Returns the list of new cell-ids as a Prologos List Nat (so the recursive +;; Prologos driver can pass them as the next iteration's `prev-cells`). +(define (et-add-iter hid prev-cells-list matrix-list pretrust-list decay-bits) + (define handle (lookup-handle hid)) + (define prev-ints (list-of-nat->ints prev-cells-list)) + (define n (length prev-ints)) + (define matrix-bits (for/list ([row (in-list (prologos-list->list matrix-list))]) + (list-of-posit32->bits row))) + (define pretrust-bits (list-of-posit32->bits pretrust-list)) + (unless (= n (length matrix-bits)) + (error 'et-add-iter "matrix outer length ~a != peer count ~a" + (length matrix-bits) n)) + (unless (= n (length pretrust-bits)) + (error 'et-add-iter "pretrust length ~a != peer count ~a" + (length pretrust-bits) n)) + (define decay-rat (posit32-to-rational decay-bits)) + (define one-minus-decay (- 1 decay-rat)) + ;; Per-peer column j of C (transpose row), weighted by (1-α). + (define weights-by-target + (for/list ([j (in-range n)]) + (for/list ([i (in-range n)]) + (* one-minus-decay + (posit32-to-rational (list-ref (list-ref matrix-bits i) j)))))) + ;; Per-peer bias = α * p[j]. + (define biases + (for/list ([j (in-range n)]) + (* decay-rat (posit32-to-rational (list-ref pretrust-bits j))))) + ;; Allocate the new layer first (so propagator installation can refer to it). + (define new-cells + (for/list ([_ (in-range n)]) + (et-cell hid (posit32-encode 0)))) + ;; Install one sum-propagator per peer. + (for ([j (in-range n)]) + (define new-cid (list-ref new-cells j)) + (define wts (list-ref weights-by-target j)) + (define bias (list-ref biases j)) + (et-sum-prop hid + (foldr (lambda (cid acc) + (expr-app (expr-app (expr-fvar 'cons) (expr-nat-val cid)) acc)) + (expr-nil) + prev-ints) + (foldr (lambda (w acc) + (expr-app (expr-app (expr-fvar 'cons) + (expr-posit32 (posit32-encode w))) acc)) + (expr-nil) + wts) + (posit32-encode bias) + new-cid)) + ;; Return the new cell-ids as a Prologos List Nat. + (foldr (lambda (i acc) + (expr-app (expr-app (expr-fvar 'cons) (expr-nat-val i)) acc)) + (expr-nil) + new-cells)) + +;; Read a list of cells and return their current Posit32 bits as a Prologos List. +(define (et-snapshot hid cids-list) + (define handle (lookup-handle hid)) + (define net (unbox (et-net-box handle))) + (define ints (list-of-nat->ints cids-list)) + (define bits-list + (for/list ([i (in-list ints)]) + (current-bits (net-cell-read net (cell-id i))))) + (bits->prologos-list-posit32 bits-list)) + +;; Combined "run + snapshot" — flushes the propagator worklist and reads the +;; given cells out as Posit32 values in one call. Useful as the final step +;; of an algorithm so Prologos's call-by-name reduction strictly evaluates +;; the cell list (and hence the propagator-install side effects) before the +;; quiescence run happens. +(define (et-run-and-snapshot hid cids-list) + (define handle (lookup-handle hid)) + (define ints (list-of-nat->ints cids-list)) ;; forces cids-list's eval + (define net0 (unbox (et-net-box handle))) + (define net* (run-to-quiescence net0)) + (set-box! (et-net-box handle) net*) + (define bits-list + (for/list ([i (in-list ints)]) + (current-bits (net-cell-read net* (cell-id i))))) + (bits->prologos-list-posit32 bits-list)) + +;; ======================================== +;; Posit32 ↔ exact rational bridges (passthrough types). +;; ======================================== +;; +;; For Prologos, Posit32 marshals to its raw bit-pattern integer; calling +;; `posit32-to-rational` directly via FFI works, but these wrappers give +;; the user a clean idiom in the `.prologos` source. + +(define (et-rat->posit32 r) + (posit32-encode r)) + +(define (et-posit32->rat bits) + (posit32-to-rational bits)) diff --git a/racket/prologos/lib/examples/eigentrust.prologos b/racket/prologos/lib/examples/eigentrust.prologos new file mode 100644 index 000000000..ccba30a4e --- /dev/null +++ b/racket/prologos/lib/examples/eigentrust.prologos @@ -0,0 +1,185 @@ +ns examples.eigentrust + + +require [prologos::data::list :refer [List cons nil reduce]] + + +;; ============================================================ +;; EigenTrust on Propagators (via Racket FFI) +;; ============================================================ +;; +;; EigenTrust [Kamvar/Schlosser/Garcia-Molina 2003] is the classic +;; reputation algorithm for peer-to-peer networks. Each peer i has a +;; trust score t[i] in [0, 1]. Local trust C[i][j] says how much peer i +;; trusts peer j (rows of C are normalised to sum to 1). A pre-trust +;; vector p — usually a small set of authority peers, normalised to +;; sum to 1 — anchors the iteration against Sybil attacks. +;; +;; The power iteration: +;; +;; t_{k+1} = α · p + (1 − α) · C^T · t_k +;; +;; converges to the eigenvector of (1−α)·C^T + α·p·1^T corresponding to +;; eigenvalue 1 — the global trust ranking. +;; +;; ----- Implementing it via propagators ----- +;; +;; Each peer's score at iteration k is one cell on the propagator +;; network. For each (peer j, iteration k+1), one sum-propagator reads +;; the previous iteration's cells {t_k[i] : i = 0..N-1} and writes +;; +;; target := α · p[j] + (1 − α) · Σ_i C[i][j] · t_k[i] +;; +;; into the iter-(k+1) cell for j. Cells use a generation-tagged +;; merge so iter-(k+1) writes strictly dominate iter-k initial values +;; — every cell is written exactly once per round, and the topological +;; order of the cell-DAG drives the propagator firing schedule. +;; +;; Prologos's first-class propagator surface (`net-new`, `net-new-cell`, +;; `net-add-prop`, …) is on the roadmap but not yet ready, so this file +;; uses `foreign racket "..."` imports of the same primitives. The +;; helper Racket module wraps the persistent prop-network in a small +;; mutable handle so Prologos sees an imperative-feeling API while +;; everything underneath is the standard CALM-monotone propagator +;; network in `racket/prologos/propagator.rkt`. +;; +;; ============================================================ + + +;; ---------- FFI: low-level propagator primitives ---------- +;; +;; Every "side-effecting" call returns a meaningful Nat (handle id or +;; cell-id) instead of Unit. Prologos's reduction is call-by-name; an +;; unused result expression would never be evaluated and the side +;; effect would be silently dropped. Threading a Nat through every +;; call forces strict evaluation order. + +foreign racket "lib/examples/eigentrust-prop.rkt" :as et + ;; Network construction + et-new : Nat -> Nat ; fuel -> handle id + ;; Cell allocation, read, write + et-cell : Nat Posit32 -> Nat ; handle, init -> cell-id + et-cell-get : Nat Nat -> Posit32 ; handle, cell-id -> value + et-cell-set : Nat Nat Posit32 -> Nat ; handle, cell-id, val -> cell-id + ;; Propagator install: target := bias + Σ weights[i] * srcs[i] + et-sum-prop : Nat [List Nat] [List Posit32] Posit32 Nat -> Nat ; -> handle (chains) + ;; Quiescence + et-run : Nat -> Nat ; -> handle (chains) + ;; Read a batch of cells as Posit32 values + et-snapshot : Nat [List Nat] -> [List Posit32] + ;; Run-and-snapshot in one call (forces strict evaluation of installed + ;; propagators before the quiescence pass) + et-run-and-snapshot : Nat [List Nat] -> [List Posit32] + ;; Higher-level: build one EigenTrust round on the network + et-add-iter : Nat [List Nat] [List [List Posit32]] [List Posit32] Posit32 -> [List Nat] + + +;; ---------- One-shot demonstration of the low-level surface ---------- +;; +;; Three cells, one sum-propagator: target := 0.1 + 0.5·s1 + 1.0·s2. +;; With s1=0.5 and s2=0.25 the propagator should write 0.6 into target. +;; The `let h-after-install := [et/et-sum-prop ...]` and `let h-after-run +;; := [et/et-run ...]` chains are the strict-thread idiom in action: each +;; result Nat is immediately consumed by the next call. + +spec demo-one-prop Nat -> Posit32 +defn demo-one-prop [fuel] + let h := [et/et-new fuel] + let s1 := [et/et-cell h ~0.5] + let s2 := [et/et-cell h ~0.25] + let tg := [et/et-cell h ~0.0] + let h-after-install := [et/et-sum-prop h + [cons s1 [cons s2 nil]] + [cons ~0.5 [cons ~1.0 nil]] + ~0.1 + tg] + let h-after-run := [et/et-run h-after-install] + [et/et-cell-get h-after-run tg] + + +;; A demo: should print ≈ ~0.6 (modulo posit32 rounding). +[demo-one-prop 100000N] + + +;; ---------- The EigenTrust algorithm ---------- +;; +;; `eigentrust` builds a fresh propagator network, allocates the +;; pre-trust vector as iteration-0 cells, then drives `et-add-iter` +;; recursively to build out K iteration layers. Each layer adds N +;; sum-propagators to the network. After all rounds are scheduled, +;; one `et-run` flushes the worklist and the final-layer cells hold +;; the converged scores. +;; +;; The recursion is the only place where strict evaluation matters — +;; we thread the layer's cell-list through the recursive call so the +;; previous layer's `et-add-iter` is forced before the next one runs. + +;; Allocate one cell per pre-trust value, in order, returning the list +;; of cell-ids. Recursion threads the partial result so each `et-cell` +;; call is forced strictly even though the body otherwise discards the +;; intermediate handle. +spec pretrust-cells Nat [List Posit32] -> [List Nat] +defn pretrust-cells + | h nil -> nil + | h [cons p ps] -> + let c := [et/et-cell h p] + let rest := [pretrust-cells h ps] + [cons c rest] + + +;; Recursive step: starting at layer k of K, return the cell-list for the +;; final layer. +spec et-iterate Nat [List Nat] [List [List Posit32]] [List Posit32] Posit32 Nat -> [List Nat] +defn et-iterate + | h cells matrix pretrust decay 0N -> cells + | h cells matrix pretrust decay [suc k] -> + let next-cells := [et/et-add-iter h cells matrix pretrust decay] + [et-iterate h next-cells matrix pretrust decay k] + + +;; Top-level driver: takes the pre-trust vector, the trust matrix, the +;; decay constant α, and the iteration count, and returns the final +;; trust scores. Each layer's propagators are built into the same +;; network; the closing `et-run` flushes them in dataflow order. +spec eigentrust [List Posit32] [List [List Posit32]] Posit32 Nat -> [List Posit32] +defn eigentrust [pretrust matrix decay iters] + let h := [et/et-new 1000000N] + let init-cells := [pretrust-cells h pretrust] + let final-cells := [et-iterate h init-cells matrix pretrust decay iters] + ;; et-run-and-snapshot's CBV-at-FFI evaluates final-cells first — that + ;; forces et-iterate to run, which forces every layer's et-add-iter to + ;; install its propagators — and only THEN does run-to-quiescence fire. + [et/et-run-and-snapshot h final-cells] + + +;; ---------- A concrete EigenTrust run ---------- +;; +;; Four peers in a small reputation graph: +;; +;; 0 1 2 3 +;; 0 [.0 .5 .5 .0] +;; 1 [.0 .0 .0 1.] +;; 2 [.5 .0 .0 .5] +;; 3 [.0 1. .0 .0] +;; +;; Each row is normalised to sum to 1. Pre-trust is uniform across all +;; four peers (0.25 each) — no prior authority. α = 0.15 is the +;; standard PageRank-style decay (it's effectively "teleport to a +;; pre-trusted peer with probability α each step"). +;; +;; After 20 power iterations the scores converge to the dominant +;; eigenvector of (1-α)·C^T + α·p·1^T. + +def trust-matrix : [List [List Posit32]] := + [cons [cons ~0.0 [cons ~0.5 [cons ~0.5 [cons ~0.0 nil]]]] + [cons [cons ~0.0 [cons ~0.0 [cons ~0.0 [cons ~1.0 nil]]]] + [cons [cons ~0.5 [cons ~0.0 [cons ~0.0 [cons ~0.5 nil]]]] + [cons [cons ~0.0 [cons ~1.0 [cons ~0.0 [cons ~0.0 nil]]]] + nil]]]] + +def pretrust : [List Posit32] := + [cons ~0.25 [cons ~0.25 [cons ~0.25 [cons ~0.25 nil]]]] + + +;; Run EigenTrust for 20 iterations with α = 0.15. +[eigentrust pretrust trust-matrix ~0.15 20N] From d72c468093a1d535142ad49484e1e7d13a9c790f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 03:55:58 +0000 Subject: [PATCH 2/3] examples: EigenTrust mantra rewrite + etprop pitfalls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the design-mantra audit (.claude/rules/on-network.md, propagator-design.md): "All-at-once, all in parallel, structurally emergent information flow ON-NETWORK." The previous shim allocated one cell per (peer, iteration) and installed N independent sum-propagators per round — a classic N-propagator step-think pattern. This rewrite collapses each iteration to: * ONE compound cell holding the full Posit32 score vector (gen-tagged immutable vector; gen-merge keeps the higher generation). * ONE broadcast propagator (`net-add-broadcast-propagator`, BSP-LE Track 2 Phase 1B) covering all N peer updates as items — parallel-decomposable across OS threads at fire time. Carriers throughout are Posit32 — handle ids, cell-refs, fuel, scores, weights, decay. The FFI surface is the three primitives the user named: net-new : Posit32 -> Posit32 net-new-cell : Posit32 [List Posit32] -> Posit32 net-add-prop : Posit32 Posit32 Posit32 [List [List Posit32]] [List Posit32] Posit32 -> Posit32 net-run-read : Posit32 Posit32 -> [List Posit32] The .prologos source uses list literals (`'[0.0 0.5 0.5 0.0]`) instead of cons chains for the trust matrix and pretrust, and bare decimal posit literals (no tilde prefix). Bracketing is reduced to where it's mandatory (function applications); top-level expressions and let-RHS positions use whitespace. Tests pass under Racket 9.1 (built from source — download.racket-lang.org is on a curl allowlist, so source must come from github.com/racket/racket and be hand-built). The pre-existing `#:pool 'own` failure on Racket 8.x is gone. Pitfalls discovered during the bring-up are catalogued in docs/tracking/2026-04-28_ETPROP_PITFALLS.md (15 entries: def is reference-transparent, reduction is call-by-name and drops unused let-bound side effects, foreign type signatures must fit on one line, defn arity-1 list pattern dispatch produces a non-exhaustive match, Racket 9 packaging vs offline catalog, ...). Final converged scores match the Python reference exactly: [0.0652, 0.4348, 0.0652, 0.4348] (sum = 1.0) --- docs/tracking/2026-04-28_ETPROP_PITFALLS.md | 261 +++++++++ .../prologos/lib/examples/eigentrust-prop.rkt | 554 ++++++++---------- .../prologos/lib/examples/eigentrust.prologos | 233 +++----- 3 files changed, 596 insertions(+), 452 deletions(-) create mode 100644 docs/tracking/2026-04-28_ETPROP_PITFALLS.md diff --git a/docs/tracking/2026-04-28_ETPROP_PITFALLS.md b/docs/tracking/2026-04-28_ETPROP_PITFALLS.md new file mode 100644 index 000000000..ea895ed1e --- /dev/null +++ b/docs/tracking/2026-04-28_ETPROP_PITFALLS.md @@ -0,0 +1,261 @@ +# EtProp Pitfalls — Notes from the EigenTrust-on-Propagators Bring-Up + +**Date**: 2026-04-28 +**Context**: While implementing the EigenTrust-on-propagators example +(`racket/prologos/lib/examples/eigentrust{,-prop.rkt,.prologos}`), the work +surfaced several rough edges in Prologos and the surrounding Racket tooling. +This document captures them in one place so future similar work doesn't have +to rediscover them. + +The list is **observation only** — these are bugs / friction in language +features and tooling, not in the EigenTrust example itself. They're filed +here because tracking them in commit messages alone made them invisible. + +------------------------------------------------------------------ + +## 1. `def` is reference-transparent — side effects re-fire on every use + +In Prologos a `def name := body` stores `body`'s **AST**, not its evaluated +value. Each subsequent reference to `name` substitutes the AST and re-reduces +it. For pure computations that's fine; for foreign calls with side effects +it's a footgun. + +Repro: + + foreign racket "racket/base" [add1 : Nat -> Nat] + + foreign racket "lib/examples/eigentrust-prop.rkt" :as et + net-new : Posit32 -> Posit32 + + def h := et/net-new 1000000.0 + h + h + h ;; -> three different handle ids + +Each `h` re-evaluates `et/net-new`, allocating a fresh network. This breaks +multi-`def` imperative-style programming with foreign side effects. Note +that within a single `process-command` boundary the per-command `whnf-cache` +**does** memoise identical AST nodes, so a single top-level expression with +multiple syntactic occurrences of `h` after substitution does behave; but +`def`-bound names are unfolded across `def` boundaries. + +**Workaround**: the entire algorithm runs as one expression — typically a +single `defn` body whose RHS chains `let := body` bindings. Within that one +expression all FFI calls evaluate exactly once. + +**Underlying language design call**: this is consistent with definitional +equality in dependent type theory (defs are *transparent*), so it's not +strictly a bug — but the behaviour is surprising for anyone bringing in +side-effecting foreign code, and there is no warning surface. + +## 2. Reduction is call-by-name — unused let-bound side effects are dropped + +`let _ := side-effecting-call body` evaluates `body` without ever +substituting `_` (since `_` is a wildcard). The `side-effecting-call` is +therefore **never evaluated**. Same shape for any unreferenced binding: + + let _i := et/net-add-prop ... ;; never fires + let _r := et/net-run h ;; never fires + result-expr ;; only this is reduced + +The eigentrust shim works around this by making every "side-effecting" FFI +call return a **meaningful** Posit32 (the cell-ref or the handle id) so the +caller is forced to thread it through a subsequent call. Sequencing via +`do` blocks doesn't help — `do` desugars to nested let. + +A shim helper such as `seq : Unit -> A -> A` that pattern-matches on `unit` +also failed empirically; the match seems to be optimised through. Real +forcing only happens at the **FFI argument boundary** where `whnf` / +`nf` are used to marshal values. + +**Implication for any FFI-via-foreign API**: every effectful call MUST +return a non-trivial value that the user is induced to consume. Returning +`Unit` is a trap. + +## 3. `let x := value body` requires `body` strictly more indented than `let` + +Prologos's WS-mode `let` macro merges consecutive bodyless `let`s into a +single binding group, with the trailing expression as the body. Surface +syntax requires the body to be indented past the `let` column: + + defn calc [n] + let x := add1 n + add1 x ;; OK — body indented past `let` + +If the body shares the `let`'s indent the parser silently drops the whole +defn (no error reported, the name registers as **unbound**): + + defn calc [n] + let x := add1 n + add1 x ;; WRONG — defn never registered + +This combines with pitfall #4 below to make defn-body errors particularly +hard to diagnose. + +## 4. Defn-body errors swallow the whole defn + +When `defn name body` has a malformed body (e.g. wrong indent, undefined +identifier in a particular position), the surface form is silently rejected +and `name` never registers. The first hint is a downstream +"Unbound variable: name" error from a later top-level expression. There is +no error from the defn itself. + +Observed during eigentrust-prop bring-up: `defn pretrust-cells [...]` with +a stale forward reference compiled silently into nothing; the only signal +was the later `[pretrust-cells h pretrust]` failing. + +## 5. `defn name | pat -> body` arity-1 list patterns produce a non-exhaustive match + +This compiles cleanly but raises `??__match-fail` at run time: + + defn zeros-of + | nil -> nil + | [cons _ rs] -> [cons 0.0 [zeros-of rs]] + +The fix is the explicit-`match` form, which works: + + defn zeros-of [xs] + match xs + | nil -> nil + | [cons _ rs] -> [cons 0.0 [zeros-of rs]] + +The two forms should be equivalent; the multi-arity dispatch path appears to +miss the `nil`-as-constructor pattern in arity-1 list matches. + +## 6. Foreign type signatures must fit on ONE line + +The foreign-block parser tokenises type tokens and then splits on `->`. If +the type is wrapped across lines: + + net-add-prop : Posit32 Posit32 Posit32 + [List [List Posit32]] [List Posit32] Posit32 -> Posit32 + +`foreign-type-tokens->sexp` errors with "Cannot parse foreign type tokens". +The fix is a single (potentially long) line: + + net-add-prop : Posit32 Posit32 Posit32 [List [List Posit32]] [List Posit32] Posit32 -> Posit32 + +This is annoying for FFI declarations of broadcast propagators (which take +several list-typed arguments) but is currently mandatory. + +## 7. Foreign FFI doesn't support Posit32 marshalling out of the box + +`base-type-name` recognises `expr-Posit32` / `expr-Posit64` and returns the +symbol, but `marshal-prologos->racket` and `marshal-racket->prologos` had no +case for them: the foreign machinery would say "Unsupported marshal-in +type: Posit32". The previous handoff added Posit32/Posit64 marshalling +(see commit `f9a3d7c`); this is still the only marshalling path that +short-circuits the existing posit-impl.rkt arithmetic ops via FFI. + +The carrier is the **bit-pattern integer** (matching posit-impl.rkt), so +`foreign racket "posit-impl.rkt" [posit32-add : Posit32 Posit32 -> Posit32]` +works directly. + +## 8. Foreign data structure types are passed via `[List ...]` but only allowed +in specific positions + +`[List Nat]`, `[List Posit32]`, and `[List [List Posit32]]` work in foreign +type signatures. They must be bracketed (the parser handles them as type +applications). Using bare `List Nat` would unfold into curried `List -> +Nat -> ...`, which is wrong. + +## 9. Top-level `let` is forbidden, but inside `defn` bodies works fine + +`let h := body` at the top level errors with +`let: 'let' is not allowed at top level. Use 'def' instead.` + +There's no surface alternative for "give a name to an intermediate value at +the top level". `def` re-evaluates (pitfall #1). The only way to evaluate +something exactly once at top level is to inline the entire computation +into one expression. + +## 10. `(zeros-of rs)` parens vs `[zeros-of rs]` brackets are NOT +interchangeable + +In WS mode `(...)` is reserved for parser keywords (`match`, `def`, etc.) and +relational goals. Function application uses `[]`. Writing `(f x)` for an +ordinary application typically silently fails (the parser tries to find a +special form named `f` and either drops the form or fails very far away +from the source line). + +## 11. `list-literal` quoting handles bare numeric literals correctly + +Confirmed working: `'[0.5 0.25 0.125]` produces a `[List Posit32]` (the +posit literals retain their decimal-literal interpretation; bare `0.5` is +equivalent to `~0.5` per the grammar). Earlier confusion was that quoted +identifiers (`'[src1 src2]`) get reified as Datum symbols, not as variable +references — so list-literal sugar works for **constants** but not for +**values**. + +## 12. Racket-9-only features + Ubuntu 24.04 ships Racket 8.10 + +`make-parallel-thread-fire-all` (`propagator.rkt:2592`) uses `(thread +#:pool 'own)` — a Racket 9 feature that's silently a runtime error on +Racket 8.x: + + application: procedure does not accept keyword arguments + procedure: thread + +The driver unconditionally installs this executor at module load +(`driver.rkt:434`). On Racket 8 the test suite errors out from any code +path that goes through `run-to-quiescence-bsp`. Workarounds tried: + + * `parameterize` to `sequential-fire-all` at the call site — works for + your own entry points but doesn't help the driver's own + `run-post-compilation-inference!` which is set up at module load. + + * Patch `propagator.rkt` to detect Racket version — invasive. + +Resolution chosen here: build Racket 9 from source. The download host +(`download.racket-lang.org`) is on a curl allowlist, so source must be +pulled from `https://github.com/racket/racket/archive/refs/tags/v9.1.tar.gz` +and built with `make CPUS=4 PKGS=""`. `rackunit` then has to be +hand-installed by copying `pkgs/rackunit/rackunit-lib/rackunit` and +`pkgs/compiler-lib/raco/testing.rkt` into the Racket collects tree — +`raco pkg install` fails because the catalog server is also on the allowlist. + +## 13. Prologos's parser drops files using `prologos::core::abs-trait` + +`racket/prologos/lib/examples/foreign.prologos` `require`s +`prologos::core::abs-trait` which no longer exists (renamed/merged into +`prologos::core::arithmetic`). Running this example now errors: + + imports: Cannot find module: prologos::core::abs-trait + +This is unrelated to the eigentrust work but came up while sanity-checking +the FFI surface. + +## 14. `tools/check-parens.sh` hardcodes a Mac-style Racket path + +The pre-commit paren checker shells out to +`/Applications/Racket v9.0/bin/racket` (line 21), which is a Mac dev +machine artifact. On Linux + the project's expected path +(`/usr/local/bin/racket`) this fails open with a confusing +"FAIL: ... No such file or directory" message even when the file's parens +are balanced. + +Workaround used here: a small standalone `parencheck.rkt` script that +parses files with `read-syntax`. Either a `command -v racket` lookup or a +configurable path setting in `check-parens.sh` would close this. + +## 15. Default Posit32 conversions use exact rationals and are slow at scale + +Encoding a Racket rational into a Posit32 bit pattern (`posit32-encode`) and +decoding it back (`posit32-to-rational`) goes through the exact-rational +posit format. For our 4-peer × 20-iteration EigenTrust this is unproblematic, +but the propagator network's reduction phase reports ~55 seconds in +`reduce_ms` even though the actual fixpoint runtime is sub-second. The +overhead is in the elaboration / type-checking of the recursive `drive` +loop and the FFI marshalling, not the propagator firing itself. Future +work: a `Posit32` instance of `Num` and operator desugaring would let the +.prologos source operate on posits directly (without going through Rat), +which would also let the FFI shim drop its rational pre-computation step. + +------------------------------------------------------------------ + +## Tracking + +These observations are not tracked in any specific PIR or design doc; +they're cross-cutting friction discovered during the bring-up. If any of +them blocks future work, file a tracking doc per `.claude/rules/workflow.md` +and link back here. diff --git a/racket/prologos/lib/examples/eigentrust-prop.rkt b/racket/prologos/lib/examples/eigentrust-prop.rkt index d3cd09b1f..039e0b441 100644 --- a/racket/prologos/lib/examples/eigentrust-prop.rkt +++ b/racket/prologos/lib/examples/eigentrust-prop.rkt @@ -3,104 +3,117 @@ ;;; ;;; PROPAGATOR-BASED EIGENTRUST — Racket FFI shim for Prologos. ;;; -;;; The user-facing pitch: Prologos plans to make Propagators a first-class -;;; language construct (not just compiler infrastructure). That work hasn't -;;; landed yet, so this module exposes the underlying propagator network -;;; primitives via Racket FFI so that algorithms like EigenTrust can be -;;; written in `.prologos` files using `foreign racket "..."` imports. +;;; The user-facing pitch: Prologos plans first-class propagator support, +;;; but the type checker isn't yet wired through the surface forms in the +;;; grammar. This shim exposes the underlying primitives (`net-new`, +;;; `net-new-cell`, `net-add-prop`) directly via Racket FFI so a `.prologos` +;;; file can build and run a propagator network today. ;;; -;;; The functions exported here wrap the persistent (immutable) prop-network -;;; in a small mutable handle (`et-net`) so Prologos sees a stateful API: +;;; ----------------------------------------------------------------- +;;; Mantra alignment (per .claude/rules/on-network.md) ;;; -;;; et-new : Nat -> Nat ; returns a fresh handle id -;;; et-cell : Nat -> Posit32 -> Nat ; allocates a cell, returns id -;;; et-cell-get : Nat -> Nat -> Posit32 ; read score -;;; et-cell-set : Nat -> Nat -> Posit32 -> Nat ; returns cell-id (chains) -;;; et-sum-prop : Nat -> List Nat -> List Posit32 -> Posit32 -> Nat -> Nat -;;; ; installs target := bias + Σ (weight_i * src_i); returns handle (chains) -;;; et-run : Nat -> Nat ; runs to quiescence; returns handle -;;; et-snapshot : Nat -> List Nat -> List Posit32 ; read many cells +;;; "All-at-once, all in parallel, structurally emergent +;;; information flow ON-NETWORK." ;;; -;;; Note that the "side-effecting" calls (cell-set, sum-prop, run) return a -;;; meaningful Nat — typically the handle or a relevant cell-id — instead of -;;; Unit. Prologos's reduction is call-by-name; an unused result expression -;;; would never be evaluated and the side effect would be silently dropped. -;;; Threading a Nat through every call forces strict evaluation order. +;;; This shim takes the mantra at face value: ;;; -;;; The first argument of every operation (other than et-new) is a handle id -;;; — a non-negative integer returned by et-new. The Racket side keeps a -;;; private registry of network boxes keyed by id, so Prologos sees normal -;;; Nat values throughout and the propagator network mutates under the hood. +;;; * All-at-once — each iteration is ONE compound cell holding the +;;; full score vector for all peers, allocated atomically +;;; (`net-new-cells-batch`, internally). The matrix / +;;; pretrust / decay configuration arrives as a single +;;; declarative payload. +;;; * All in parallel — `net-add-prop` installs ONE broadcast propagator +;;; (per `.claude/rules/propagator-design.md` § Broadcast +;;; Propagators) covering all N peer-update items. The +;;; scheduler can decompose the broadcast across OS +;;; threads at fire time. NO N-propagator step-think, +;;; NO `for/fold` walking the peer list. +;;; * Structurally emergent — the cell DAG is iter-0 → iter-1 → ... → +;;; iter-K. The BSP scheduler fires layers in +;;; topological order; we never tell it what fires when. +;;; * ON-NETWORK — every score lives in a cell. The compound carrier +;;; holds the full peer vector; merge keeps the higher +;;; generation so each layer's broadcast write strictly +;;; dominates the previous initial value. ;;; -;;; The cell merge function is monotone-by-generation: each cell holds a -;;; (gen . posit-bits) pair and the merge keeps the entry with the higher -;;; generation. Iteration k+1 cells are generation k+1; this lets EigenTrust's -;;; non-monotone power iteration cooperate with the CALM-monotone propagator -;;; network — every cell is written at most once per round, ordered by -;;; data-dependency rather than imperative control flow. +;;; ----------------------------------------------------------------- +;;; FFI surface (consumed from .prologos via `foreign racket "..."`): ;;; -;;; For Prologos, all numeric scoring happens in Posit32 (32-bit posits, 2022 -;;; standard, es=2). The bit pattern is the FFI carrier; this module converts -;;; to/from exact rationals internally for the dot-product math. +;;; net-new : Posit32 -> Posit32 +;;; Allocate a fresh propagator network with the given fuel budget; +;;; return a Posit32 handle id. +;;; +;;; net-new-cell : Posit32 -> List Posit32 -> Posit32 +;;; Allocate one compound cell holding a vector of Posit32 scores. +;;; Return a Posit32 cell-ref. +;;; +;;; net-add-prop : Posit32 -> Posit32 -> Posit32 +;;; -> List (List Posit32) -> List Posit32 -> Posit32 +;;; -> Posit32 +;;; Install ONE broadcast propagator on the network whose fire-fn +;;; implements one EigenTrust round: +;;; +;;; t_{k+1}[j] = α · pretrust[j] +;;; + (1 − α) · Σ_i C[i][j] · t_k[i] +;;; +;;; Args: handle, input-cell, output-cell, matrix-rows (C in +;;; row-major form), pretrust vector, decay α. Returns the +;;; output-cell so callers can chain layers. +;;; +;;; net-run-read : Posit32 -> Posit32 -> List Posit32 +;;; Run the network to quiescence, then read the final layer's +;;; compound cell as a Posit32 list. Combined into one call so the +;;; Prologos call-by-name reduction has to evaluate the full layer +;;; chain (forces every net-add-prop side effect) before the +;;; quiescence pass starts. +;;; +;;; All "side-effecting" calls return a meaningful Posit32 (handle, cell +;;; ref, or score list) instead of Unit. Prologos's reduction is +;;; call-by-name — an unused result expression would never be evaluated and +;;; the side effect would be silently dropped. Threading a Posit32 through +;;; every call forces strict evaluation order. ;;; (require racket/match - "../../propagator.rkt" + racket/list + racket/vector + (rename-in "../../propagator.rkt" + [net-new-cell raw-net-new-cell] + [net-add-broadcast-propagator raw-net-add-broadcast]) "../../syntax.rkt" "../../posit-impl.rkt") (provide - et-new - et-cell - et-cell-get - et-cell-set - et-sum-prop - et-run - et-snapshot - et-run-and-snapshot - ;; Higher-level operation that Prologos can compose recursively to drive - ;; the EigenTrust power iteration: given the previous iteration's cells, - ;; allocate a fresh layer of cells and install one sum-propagator per peer - ;; that computes t_{k+1}[j] = α·p[j] + (1-α) · Σ_i C[i][j]·t_k[i]. - et-add-iter - ;; Convenience for Prologos: encode/decode Posit32 ↔ Rat so that user code - ;; can construct posit constants without going through literal sugar. - et-rat->posit32 - et-posit32->rat) + net-new + net-new-cell + net-add-prop + net-run-read) ;; ======================================== -;; Handle: integer id into a Racket-side registry of mutable network boxes. +;; Handle registry — Posit32 carrier. ;; ======================================== ;; -;; Prologos's foreign FFI marshals user-defined types as Passthrough — the IR -;; value is the Racket value. That works for primitives but breaks reduction -;; (whnf/nf) when a non-IR Racket struct flows through arbitrary Prologos -;; expressions. Returning a `Nat` handle keeps every foreign value an honest -;; IR Nat, and the registry side-table lookups happen entirely on the Racket -;; side. +;; Posit32 represents small integers exactly within its range, so we use +;; the *bit pattern* of the encoded posit as the on-the-wire handle. The +;; Prologos surface sees Posit32 values; the registry side-table on the +;; Racket side maps decoded integer ids to mutable boxes holding the +;; persistent prop-network. (struct et-net (box) #:transparent) -(define handle-registry (make-hasheqv)) ;; integer -> et-net -(define next-handle-id (box 0)) +(define handle-registry (make-hasheqv)) ;; integer -> et-net +(define cell-registry (make-hasheqv)) ;; integer -> cell-id (the prop-network's cell) +(define next-handle-id (box 1)) ;; reserve 0 in case +(define next-cell-id (box 1)) -(define (lookup-handle id) - (define h (hash-ref handle-registry id #f)) - (unless h (error 'eigentrust-prop "no such EigenTrust handle: ~a" id)) - h) - -(define (et-new fuel) - (define h (et-net (box (make-prop-network (max fuel 1))))) - (define id (unbox next-handle-id)) - (set-box! next-handle-id (+ id 1)) - (hash-set! handle-registry id h) - id) +(define (id->posit32 i) (posit32-encode i)) +(define (posit32->id bits) (inexact->exact (posit32-to-rational bits))) ;; ======================================== -;; List helpers — walk the Prologos cons/nil chain in WHNF form. +;; List helpers: walk WHNF cons/nil chains. ;; ======================================== ;; -;; Foreign args have been reduced before marshalling, so List values arrive +;; Foreign args are reduced to nf before marshalling, so List values arrive ;; as nested expr-app of expr-fvar 'cons (or qualified ::cons) terminating ;; in expr-nil / expr-fvar 'nil. @@ -116,14 +129,12 @@ (let ([n (string-length s)]) (and (>= n 5) (string=? (substring s (- n 5)) "::nil")))))) -;; Drill through optional type-arg applications: `(cons A) head tail` and -;; `((cons A) head) tail` both denote a cons cell with payload `head`. (define (try-cons-decomp e) (and (expr-app? e) (let ([func (expr-app-func e)] [tail (expr-app-arg e)]) (and (expr-app? func) - (let ([head (expr-app-arg func)] + (let ([head (expr-app-arg func)] [inner (expr-app-func func)]) (cond [(and (expr-fvar? inner) (cons-name? (expr-fvar-name inner))) @@ -146,264 +157,199 @@ (cond [(is-nil? cur) (reverse acc)] [(try-cons-decomp cur) - => (lambda (hd-tl) - (loop (cdr hd-tl) (cons (car hd-tl) acc)))] + => (lambda (hd-tl) (loop (cdr hd-tl) (cons (car hd-tl) acc)))] [else (error 'eigentrust-prop - "list walk got a non-list / non-WHNF value: ~v" cur)]))) - -;; Walk a List Nat — extract Racket integers from expr-nat-val nodes. -(define (list-of-nat->ints lst-expr) - (for/list ([e (in-list (prologos-list->list lst-expr))]) - (match e - [(expr-nat-val n) n] - [(expr-zero) 0] - [(expr-suc _) (let walk ([x e] [k 0]) - (match x - [(expr-zero) k] - [(expr-nat-val m) (+ k m)] - [(expr-suc inner) (walk inner (+ k 1))] - [_ (error 'eigentrust-prop "expected Nat, got ~v" e)]))] - [_ (error 'eigentrust-prop "expected Nat in list, got ~v" e)]))) - -;; Walk a List Posit32 — extract bit-pattern integers from expr-posit32 nodes. -(define (list-of-posit32->bits lst-expr) - (for/list ([e (in-list (prologos-list->list lst-expr))]) - (match e - [(expr-posit32 bits) bits] - [_ (error 'eigentrust-prop "expected Posit32 in list, got ~v" e)]))) - -;; Build a Prologos List Posit32 from a Racket list of bit-pattern integers. -(define (bits->prologos-list-posit32 bits-list) - (foldr (lambda (bits acc) - (expr-app (expr-app (expr-fvar 'cons) (expr-posit32 bits)) acc)) + "list walk: not a list / not in WHNF: ~v" cur)]))) + +(define (list-of-posit32->bits-vec e) + (define lst (prologos-list->list e)) + (for/vector #:length (length lst) + ([x (in-list lst)]) + (match x + [(expr-posit32 b) b] + [_ (error 'eigentrust-prop "expected Posit32 in list, got ~v" x)]))) + +(define (matrix->vec-of-vecs e) + (define rows (prologos-list->list e)) + (for/vector #:length (length rows) + ([row (in-list rows)]) + (list-of-posit32->bits-vec row))) + +(define (bits-vec->prologos-list bits-vec) + (foldr (lambda (b acc) + (expr-app (expr-app (expr-fvar 'cons) (expr-posit32 b)) acc)) (expr-nil) - bits-list)) + (vector->list bits-vec))) ;; ======================================== -;; Cell merge: monotone "max-by-generation" lattice. -;; ======================================== +;; Cell value: generation-tagged immutable vector of Posit32 bit patterns. ;; -;; Cell value = (cons gen bits). Iteration k writes (k . new-bits); merge -;; keeps the higher-gen entry. The first write per round is the only write -;; (propagator fires once per round when its gen-(k-1) inputs are ready), so -;; ties shouldn't arise in correct use; on a tie we keep the existing. +;; The merge function keeps the entry with the higher generation. This is +;; CALM-monotone (joins commute / are associative / are idempotent) under +;; the usual correctness contract: each generation is written exactly +;; once, by exactly one propagator, in topological order. +;; ======================================== -(struct gen-val (gen bits) #:transparent) +(struct gen-vec (gen vec) #:transparent) (define (gen-merge old new) (cond - [(not (gen-val? old)) new] - [(not (gen-val? new)) old] - [(> (gen-val-gen new) (gen-val-gen old)) new] + [(not (gen-vec? old)) new] + [(not (gen-vec? new)) old] + [(> (gen-vec-gen new) (gen-vec-gen old)) new] [else old])) -;; Initial bot value for any cell. -(define gen-bot (gen-val -1 0)) ;; gen=-1 => any real write strictly dominates - -;; Track the per-cell generation in a side table so we know which gen to -;; assign on the next write. The simplest convention: a cell allocated by -;; `et-cell` starts at gen 0 with the user-supplied initial bits. -(define (current-gen cell-val) - (if (gen-val? cell-val) (gen-val-gen cell-val) -1)) - -(define (current-bits cell-val) - (if (gen-val? cell-val) (gen-val-bits cell-val) 0)) +(define (current-gen v) (if (gen-vec? v) (gen-vec-gen v) -1)) +(define (current-vec v) (if (gen-vec? v) (gen-vec-vec v) #())) ;; ======================================== -;; Public FFI surface. +;; net-new — allocate a propagator network. ;; ======================================== +;; +;; fuel-bits : Posit32 bit pattern for a small integer fuel budget. +;; Returns: Posit32 bit pattern for the handle id. + +(define (net-new fuel-bits) + (define fuel-int + (let ([r (posit32-to-rational fuel-bits)]) + (cond + [(exact-integer? r) r] + [(rational? r) (inexact->exact (round r))] + [else 1000000]))) + (define h (et-net (box (make-prop-network (max 1 fuel-int))))) + (define id (unbox next-handle-id)) + (set-box! next-handle-id (+ id 1)) + (hash-set! handle-registry id h) + (id->posit32 id)) -;; Allocate a cell with the given Posit32 initial value (bit pattern). -;; Returns the cell-id as a non-negative integer (Nat in Prologos). -(define (et-cell hid init-bits) - (define handle (lookup-handle hid)) - (define net (unbox (et-net-box handle))) - (define-values (net* cid) (net-new-cell net (gen-val 0 init-bits) gen-merge)) - (set-box! (et-net-box handle) net*) - (cell-id-n cid)) - -;; Read the current Posit32 bits stored in a cell. -(define (et-cell-get hid cid-int) - (define handle (lookup-handle hid)) - (define net (unbox (et-net-box handle))) - (define cid (cell-id cid-int)) - (current-bits (net-cell-read net cid))) - -;; Imperatively overwrite a cell with a higher generation. Returns the cell-id -;; so calls can be chained (Prologos uses CBN; an unused result wouldn't fire). -(define (et-cell-set hid cid-int bits) - (define handle (lookup-handle hid)) - (define net (unbox (et-net-box handle))) - (define cid (cell-id cid-int)) - (define old (net-cell-read net cid)) - (define next-gen (+ 1 (current-gen old))) - (define net* (net-cell-write net cid (gen-val next-gen bits))) - (set-box! (et-net-box handle) net*) - cid-int) +(define (lookup-handle hbits) + (define id (posit32->id hbits)) + (or (hash-ref handle-registry id #f) + (error 'eigentrust-prop "no such network handle: ~a" id))) -;; Install a propagator: target_cell := bias + Σ_i weight_i * src_i. -;; srcs : Prologos List Nat — input cell-ids -;; weights : Prologos List Posit32 — same length as srcs -;; bias : Posit32 bit-pattern — additive constant -;; target : Nat — output cell-id -;; Returns the handle. +;; ======================================== +;; net-new-cell — allocate a compound cell. +;; ======================================== ;; -;; The fire function reads sources, decodes to rationals, computes the -;; weighted sum (in exact rational arithmetic, using a quire-style exact -;; accumulator), encodes back to Posit32, and writes a generation -;; max(gen(srcs)) + 1 entry into the target cell. -(define (et-sum-prop hid srcs-list weights-list bias-bits target-int) - (define handle (lookup-handle hid)) - (define net (unbox (et-net-box handle))) - (define src-ints (list-of-nat->ints srcs-list)) - (define wt-bits (list-of-posit32->bits weights-list)) - (unless (= (length src-ints) (length wt-bits)) - (error 'et-sum-prop "srcs and weights must have equal length: ~a vs ~a" - (length src-ints) (length wt-bits))) - (define src-cids (map cell-id src-ints)) - (define target-cid (cell-id target-int)) - (define wt-rats (map posit32-to-rational wt-bits)) - (define bias-rat (posit32-to-rational bias-bits)) - (define fire-fn - (lambda (n) - ;; Read each source, decode to rational, multiply by weight, sum. - ;; Track the max generation among inputs so the output gets a strictly - ;; higher generation (gen-merge keeps higher gens). - (define max-gen - (for/fold ([g -1]) ([cid (in-list src-cids)]) - (max g (current-gen (net-cell-read n cid))))) - (define sum - (for/fold ([acc bias-rat]) - ([cid (in-list src-cids)] - [w (in-list wt-rats)]) - (define src-bits (current-bits (net-cell-read n cid))) - (define src-rat (posit32-to-rational src-bits)) - (cond - [(or (eq? src-rat 'nar) (eq? acc 'nar)) 'nar] - [else (+ acc (* w src-rat))]))) - (define out-bits (cond - [(eq? sum 'nar) (posit32-encode 'nar)] - [else (posit32-encode sum)])) - (net-cell-write n target-cid (gen-val (+ 1 max-gen) out-bits)))) - (define-values (net* _pid) - (net-add-propagator net src-cids (list target-cid) fire-fn)) - (set-box! (et-net-box handle) net*) - hid) +;; Each cell holds a vector of Posit32 bit-patterns — the score for every +;; peer at one iteration, all bundled into one cell so the broadcast +;; propagator writes them atomically (mantra: all-at-once, on-network). +;; +;; init-bits-list : Prologos List Posit32 — initial score vector. +;; Returns : Posit32 bit pattern encoding the cell-ref id. -;; Run the network to quiescence (fixpoint of all installed propagators). -;; Returns the handle id so it can be chained (forces evaluation in CBN). -(define (et-run hid) - (define handle (lookup-handle hid)) +(define (net-new-cell hbits init-bits-list) + (define handle (lookup-handle hbits)) + (define init-vec (vector->immutable-vector (list-of-posit32->bits-vec init-bits-list))) (define net (unbox (et-net-box handle))) - (define net* (run-to-quiescence net)) + (define-values (net* cid) + (raw-net-new-cell net (gen-vec 0 init-vec) gen-merge)) (set-box! (et-net-box handle) net*) - hid) + (define cref (unbox next-cell-id)) + (set-box! next-cell-id (+ cref 1)) + (hash-set! cell-registry cref cid) + (id->posit32 cref)) + +(define (lookup-cell crefbits) + (define cref (posit32->id crefbits)) + (or (hash-ref cell-registry cref #f) + (error 'eigentrust-prop "no such cell-ref: ~a" cref))) ;; ======================================== -;; et-add-iter: build one EigenTrust iteration on the propagator network. +;; net-add-prop — install ONE broadcast propagator for an EigenTrust round. ;; ======================================== ;; -;; prev-cells : Prologos List Nat — cell-ids holding t_k (current scores) -;; matrix : Prologos List (List Posit32) -;; — row-major C, i.e., matrix[i][j] = how much peer i trusts peer j -;; pretrust : Prologos List Posit32 — p, the prior trust vector -;; decay-bits : Posit32 bit pattern — α (decay constant, typically 0.1–0.2) -;; -;; Allocates one fresh cell per peer (gen=k+1), installs a sum-propagator per -;; peer reading prev-cells, writing the new cell. The propagator computes +;; Per `.claude/rules/propagator-design.md` § Broadcast Propagators: +;; "any for/fold or for/list that processes independent items is a +;; candidate for broadcast." This is exactly that — N peer updates that +;; read the same prev-iter cell. The broadcast-profile metadata makes +;; the propagator decomposable across OS threads (BSP-LE Track 2 Phase +;; 1B) when the scheduler chooses. ;; -;; t_{k+1}[j] = α · p[j] + (1 − α) · Σ_i C[i][j] · t_k[i] -;; -;; Returns the list of new cell-ids as a Prologos List Nat (so the recursive -;; Prologos driver can pass them as the next iteration's `prev-cells`). -(define (et-add-iter hid prev-cells-list matrix-list pretrust-list decay-bits) - (define handle (lookup-handle hid)) - (define prev-ints (list-of-nat->ints prev-cells-list)) - (define n (length prev-ints)) - (define matrix-bits (for/list ([row (in-list (prologos-list->list matrix-list))]) - (list-of-posit32->bits row))) - (define pretrust-bits (list-of-posit32->bits pretrust-list)) - (unless (= n (length matrix-bits)) - (error 'et-add-iter "matrix outer length ~a != peer count ~a" - (length matrix-bits) n)) - (unless (= n (length pretrust-bits)) - (error 'et-add-iter "pretrust length ~a != peer count ~a" - (length pretrust-bits) n)) - (define decay-rat (posit32-to-rational decay-bits)) - (define one-minus-decay (- 1 decay-rat)) - ;; Per-peer column j of C (transpose row), weighted by (1-α). - (define weights-by-target - (for/list ([j (in-range n)]) - (for/list ([i (in-range n)]) - (* one-minus-decay - (posit32-to-rational (list-ref (list-ref matrix-bits i) j)))))) - ;; Per-peer bias = α * p[j]. +;; input-cref : prev iteration cell (compound, gen-vec carrier) +;; output-cref : next iteration cell (compound, gen-vec carrier) +;; matrix : Prologos List (List Posit32) — row-major C +;; pretrust : Prologos List Posit32 — p +;; decay-bits : Posit32 bit pattern — α +;; Returns : output-cref (chains). + +(define (net-add-prop hbits input-cref output-cref matrix pretrust decay-bits) + (define handle (lookup-handle hbits)) + (define input-cid (lookup-cell input-cref)) + (define out-cid (lookup-cell output-cref)) + (define C-rows (matrix->vec-of-vecs matrix)) + (define p-bits (list-of-posit32->bits-vec pretrust)) + (define n (vector-length p-bits)) + (unless (= n (vector-length C-rows)) + (error 'net-add-prop "matrix outer length ~a != peer count ~a" + (vector-length C-rows) n)) + (for ([row (in-vector C-rows)] [i (in-naturals)]) + (unless (= n (vector-length row)) + (error 'net-add-prop "matrix row ~a has ~a entries, expected ~a" + i (vector-length row) n))) + (define decay-rat (posit32-to-rational decay-bits)) + (define one-m-decay (- 1 decay-rat)) + (define p-rats (for/vector ([b (in-vector p-bits)]) (posit32-to-rational b))) + ;; Pre-compute, in exact rationals, the per-target weights and bias. + ;; weights[j][i] = (1 - α) · C[i][j] + ;; bias[j] = α · p[j] + (define weights + (for/vector ([j (in-range n)]) + (for/vector ([i (in-range n)]) + (* one-m-decay + (posit32-to-rational (vector-ref (vector-ref C-rows i) j)))))) (define biases - (for/list ([j (in-range n)]) - (* decay-rat (posit32-to-rational (list-ref pretrust-bits j))))) - ;; Allocate the new layer first (so propagator installation can refer to it). - (define new-cells - (for/list ([_ (in-range n)]) - (et-cell hid (posit32-encode 0)))) - ;; Install one sum-propagator per peer. - (for ([j (in-range n)]) - (define new-cid (list-ref new-cells j)) - (define wts (list-ref weights-by-target j)) - (define bias (list-ref biases j)) - (et-sum-prop hid - (foldr (lambda (cid acc) - (expr-app (expr-app (expr-fvar 'cons) (expr-nat-val cid)) acc)) - (expr-nil) - prev-ints) - (foldr (lambda (w acc) - (expr-app (expr-app (expr-fvar 'cons) - (expr-posit32 (posit32-encode w))) acc)) - (expr-nil) - wts) - (posit32-encode bias) - new-cid)) - ;; Return the new cell-ids as a Prologos List Nat. - (foldr (lambda (i acc) - (expr-app (expr-app (expr-fvar 'cons) (expr-nat-val i)) acc)) - (expr-nil) - new-cells)) - -;; Read a list of cells and return their current Posit32 bits as a Prologos List. -(define (et-snapshot hid cids-list) - (define handle (lookup-handle hid)) - (define net (unbox (et-net-box handle))) - (define ints (list-of-nat->ints cids-list)) - (define bits-list - (for/list ([i (in-list ints)]) - (current-bits (net-cell-read net (cell-id i))))) - (bits->prologos-list-posit32 bits-list)) - -;; Combined "run + snapshot" — flushes the propagator worklist and reads the -;; given cells out as Posit32 values in one call. Useful as the final step -;; of an algorithm so Prologos's call-by-name reduction strictly evaluates -;; the cell list (and hence the propagator-install side effects) before the -;; quiescence run happens. -(define (et-run-and-snapshot hid cids-list) - (define handle (lookup-handle hid)) - (define ints (list-of-nat->ints cids-list)) ;; forces cids-list's eval + (for/vector ([j (in-range n)]) (* decay-rat (vector-ref p-rats j)))) + ;; --- Broadcast propagator: ONE install, N items, parallel-decomposable. + ;; items: peer indices 0..N-1. + (define items (build-list n values)) + ;; item-fn: per-peer update. Reads the (single) prev-iter compound cell, + ;; computes t_{k+1}[j], returns (input-gen, j, bits). The input-gen + ;; tag lets the next-layer's gen exceed it so re-fires (after the input + ;; cell evolves) strictly dominate stale fires. + (define (item-fn j input-vals) + (define prev-cell-val (car input-vals)) + (define prev (current-vec prev-cell-val)) + (define input-gen (current-gen prev-cell-val)) + (define wts (vector-ref weights j)) + (define bias (vector-ref biases j)) + (define sum + (for/fold ([acc bias]) ([w (in-vector wts)] [b (in-vector prev)]) + (+ acc (* w (posit32-to-rational b))))) + (list input-gen j (posit32-encode sum))) + ;; result-merge: accumulate per-peer results into a gen-vec carrier + ;; whose gen is input-gen + 1. Gen-merge then keeps the latest fire when + ;; the input cell evolves across BSP rounds. + (define (final-merge acc r) + (define input-gen (car r)) + (define j (cadr r)) + (define bits (caddr r)) + (define base + (cond + [(gen-vec? acc) (vector-copy (gen-vec-vec acc))] + [else (make-vector n 0)])) + (vector-set! base j bits) + (gen-vec (+ 1 input-gen) (vector->immutable-vector base))) (define net0 (unbox (et-net-box handle))) - (define net* (run-to-quiescence net0)) + (define-values (net* _pid) + (raw-net-add-broadcast net0 (list input-cid) out-cid + items item-fn final-merge)) (set-box! (et-net-box handle) net*) - (define bits-list - (for/list ([i (in-list ints)]) - (current-bits (net-cell-read net* (cell-id i))))) - (bits->prologos-list-posit32 bits-list)) + output-cref) ;; ======================================== -;; Posit32 ↔ exact rational bridges (passthrough types). +;; net-run-read — flush the network and snapshot a cell's vector. ;; ======================================== ;; -;; For Prologos, Posit32 marshals to its raw bit-pattern integer; calling -;; `posit32-to-rational` directly via FFI works, but these wrappers give -;; the user a clean idiom in the `.prologos` source. - -(define (et-rat->posit32 r) - (posit32-encode r)) +;; The single FFI call forces strict evaluation of `output-cref` before +;; running, so the entire chain of net-add-prop side effects has fired +;; by the time run-to-quiescence runs. -(define (et-posit32->rat bits) - (posit32-to-rational bits)) +(define (net-run-read hbits cref) + (define handle (lookup-handle hbits)) + (define cid (lookup-cell cref)) + (define net0 (unbox (et-net-box handle))) + (define net* (run-to-quiescence net0)) + (set-box! (et-net-box handle) net*) + (define final-vec (current-vec (net-cell-read net* cid))) + (bits-vec->prologos-list final-vec)) diff --git a/racket/prologos/lib/examples/eigentrust.prologos b/racket/prologos/lib/examples/eigentrust.prologos index ccba30a4e..19507a842 100644 --- a/racket/prologos/lib/examples/eigentrust.prologos +++ b/racket/prologos/lib/examples/eigentrust.prologos @@ -1,7 +1,7 @@ ns examples.eigentrust -require [prologos::data::list :refer [List cons nil reduce]] +require [prologos::data::list :refer [List]] ;; ============================================================ @@ -10,176 +10,113 @@ require [prologos::data::list :refer [List cons nil reduce]] ;; ;; EigenTrust [Kamvar/Schlosser/Garcia-Molina 2003] is the classic ;; reputation algorithm for peer-to-peer networks. Each peer i has a -;; trust score t[i] in [0, 1]. Local trust C[i][j] says how much peer i -;; trusts peer j (rows of C are normalised to sum to 1). A pre-trust -;; vector p — usually a small set of authority peers, normalised to -;; sum to 1 — anchors the iteration against Sybil attacks. -;; -;; The power iteration: +;; trust score t[i] in [0, 1]. The local trust matrix C[i][j] says how +;; much peer i trusts peer j (rows of C are normalised to sum to 1). A +;; pre-trust vector p anchors the iteration against Sybil attacks. The +;; power iteration ;; ;; t_{k+1} = α · p + (1 − α) · C^T · t_k ;; -;; converges to the eigenvector of (1−α)·C^T + α·p·1^T corresponding to -;; eigenvalue 1 — the global trust ranking. -;; -;; ----- Implementing it via propagators ----- +;; converges to the dominant eigenvector — the global trust ranking. ;; -;; Each peer's score at iteration k is one cell on the propagator -;; network. For each (peer j, iteration k+1), one sum-propagator reads -;; the previous iteration's cells {t_k[i] : i = 0..N-1} and writes +;; ----- Mantra (.claude/rules/on-network.md) ----- ;; -;; target := α · p[j] + (1 − α) · Σ_i C[i][j] · t_k[i] +;; "All-at-once, all in parallel, structurally emergent +;; information flow ON-NETWORK." ;; -;; into the iter-(k+1) cell for j. Cells use a generation-tagged -;; merge so iter-(k+1) writes strictly dominate iter-k initial values -;; — every cell is written exactly once per round, and the topological -;; order of the cell-DAG drives the propagator firing schedule. +;; * All-at-once — each iteration is ONE compound cell holding the +;; full Posit32 score vector for every peer. +;; * All in parallel — `net-add-prop` installs ONE broadcast propagator +;; (`.claude/rules/propagator-design.md` § Broadcast) +;; covering all N peer updates per round; the BSP +;; scheduler decomposes those items across OS threads +;; at fire time. +;; * Structurally emergent — the cell DAG iter-0 → iter-1 → … → iter-K +;; drives firing order; we never tell the scheduler +;; what fires when. +;; * ON-NETWORK — every score lives in a cell; the gen-tagged +;; compound merge is CALM-monotone (the propagator +;; network's standard contract). ;; -;; Prologos's first-class propagator surface (`net-new`, `net-new-cell`, -;; `net-add-prop`, …) is on the roadmap but not yet ready, so this file -;; uses `foreign racket "..."` imports of the same primitives. The -;; helper Racket module wraps the persistent prop-network in a small -;; mutable handle so Prologos sees an imperative-feeling API while -;; everything underneath is the standard CALM-monotone propagator -;; network in `racket/prologos/propagator.rkt`. +;; First-class propagator surface (`net-new`, `net-new-cell`, `net-add-prop`) +;; is on the Prologos roadmap but not yet wired through the type checker; +;; this file imports the same primitives via `foreign racket "..."` until +;; that work lands. ;; ;; ============================================================ -;; ---------- FFI: low-level propagator primitives ---------- +;; -- FFI: the four propagator primitives we need -- ;; -;; Every "side-effecting" call returns a meaningful Nat (handle id or -;; cell-id) instead of Unit. Prologos's reduction is call-by-name; an -;; unused result expression would never be evaluated and the side -;; effect would be silently dropped. Threading a Nat through every -;; call forces strict evaluation order. +;; All carriers are Posit32 — handles, cell-refs, fuel, scores, weights. +;; "Side-effecting" calls return a meaningful Posit32 (the cell-ref or the +;; handle itself) so call-by-name reduction is forced to evaluate them. foreign racket "lib/examples/eigentrust-prop.rkt" :as et - ;; Network construction - et-new : Nat -> Nat ; fuel -> handle id - ;; Cell allocation, read, write - et-cell : Nat Posit32 -> Nat ; handle, init -> cell-id - et-cell-get : Nat Nat -> Posit32 ; handle, cell-id -> value - et-cell-set : Nat Nat Posit32 -> Nat ; handle, cell-id, val -> cell-id - ;; Propagator install: target := bias + Σ weights[i] * srcs[i] - et-sum-prop : Nat [List Nat] [List Posit32] Posit32 Nat -> Nat ; -> handle (chains) - ;; Quiescence - et-run : Nat -> Nat ; -> handle (chains) - ;; Read a batch of cells as Posit32 values - et-snapshot : Nat [List Nat] -> [List Posit32] - ;; Run-and-snapshot in one call (forces strict evaluation of installed - ;; propagators before the quiescence pass) - et-run-and-snapshot : Nat [List Nat] -> [List Posit32] - ;; Higher-level: build one EigenTrust round on the network - et-add-iter : Nat [List Nat] [List [List Posit32]] [List Posit32] Posit32 -> [List Nat] - - -;; ---------- One-shot demonstration of the low-level surface ---------- -;; -;; Three cells, one sum-propagator: target := 0.1 + 0.5·s1 + 1.0·s2. -;; With s1=0.5 and s2=0.25 the propagator should write 0.6 into target. -;; The `let h-after-install := [et/et-sum-prop ...]` and `let h-after-run -;; := [et/et-run ...]` chains are the strict-thread idiom in action: each -;; result Nat is immediately consumed by the next call. - -spec demo-one-prop Nat -> Posit32 -defn demo-one-prop [fuel] - let h := [et/et-new fuel] - let s1 := [et/et-cell h ~0.5] - let s2 := [et/et-cell h ~0.25] - let tg := [et/et-cell h ~0.0] - let h-after-install := [et/et-sum-prop h - [cons s1 [cons s2 nil]] - [cons ~0.5 [cons ~1.0 nil]] - ~0.1 - tg] - let h-after-run := [et/et-run h-after-install] - [et/et-cell-get h-after-run tg] - - -;; A demo: should print ≈ ~0.6 (modulo posit32 rounding). -[demo-one-prop 100000N] - - -;; ---------- The EigenTrust algorithm ---------- -;; -;; `eigentrust` builds a fresh propagator network, allocates the -;; pre-trust vector as iteration-0 cells, then drives `et-add-iter` -;; recursively to build out K iteration layers. Each layer adds N -;; sum-propagators to the network. After all rounds are scheduled, -;; one `et-run` flushes the worklist and the final-layer cells hold -;; the converged scores. -;; -;; The recursion is the only place where strict evaluation matters — -;; we thread the layer's cell-list through the recursive call so the -;; previous layer's `et-add-iter` is forced before the next one runs. - -;; Allocate one cell per pre-trust value, in order, returning the list -;; of cell-ids. Recursion threads the partial result so each `et-cell` -;; call is forced strictly even though the body otherwise discards the -;; intermediate handle. -spec pretrust-cells Nat [List Posit32] -> [List Nat] -defn pretrust-cells - | h nil -> nil - | h [cons p ps] -> - let c := [et/et-cell h p] - let rest := [pretrust-cells h ps] - [cons c rest] - - -;; Recursive step: starting at layer k of K, return the cell-list for the -;; final layer. -spec et-iterate Nat [List Nat] [List [List Posit32]] [List Posit32] Posit32 Nat -> [List Nat] -defn et-iterate - | h cells matrix pretrust decay 0N -> cells - | h cells matrix pretrust decay [suc k] -> - let next-cells := [et/et-add-iter h cells matrix pretrust decay] - [et-iterate h next-cells matrix pretrust decay k] - - -;; Top-level driver: takes the pre-trust vector, the trust matrix, the -;; decay constant α, and the iteration count, and returns the final -;; trust scores. Each layer's propagators are built into the same -;; network; the closing `et-run` flushes them in dataflow order. + net-new : Posit32 -> Posit32 + net-new-cell : Posit32 [List Posit32] -> Posit32 + net-add-prop : Posit32 Posit32 Posit32 [List [List Posit32]] [List Posit32] Posit32 -> Posit32 + net-run-read : Posit32 Posit32 -> [List Posit32] + + +;; A list of zeros with the same shape as the reference vector — used as +;; the initial value of every iter-(k+1) cell before its broadcast +;; propagator fires. +spec zeros-of [List Posit32] -> [List Posit32] +defn zeros-of [xs] + match xs + | nil -> nil + | [cons _ rs] -> [cons 0.0 [zeros-of rs]] + + +;; Chain K iteration layers of broadcast propagators on the network. +;; Each `net-add-prop` install IS structurally one round: ONE broadcast +;; propagator covering all N peers, reading the previous compound cell +;; and writing the next compound cell. After all K layers are installed, +;; `net-run-read` runs the BSP scheduler to quiescence and reads the +;; final layer's score vector. +;; +;; The construction-time recursion is just for traversing K. At RUN +;; time, every layer's propagator fires in dataflow order — the +;; iter-0 → iter-K DAG IS the parallel decomposition. +spec drive Posit32 Posit32 [List [List Posit32]] [List Posit32] Posit32 Nat -> Posit32 +defn drive + | h cell matrix pretrust decay 0N -> cell + | h cell matrix pretrust decay [suc k] -> + let next-cell := et/net-new-cell h (zeros-of pretrust) + let chained := et/net-add-prop h cell next-cell matrix pretrust decay + drive h chained matrix pretrust decay k + + spec eigentrust [List Posit32] [List [List Posit32]] Posit32 Nat -> [List Posit32] defn eigentrust [pretrust matrix decay iters] - let h := [et/et-new 1000000N] - let init-cells := [pretrust-cells h pretrust] - let final-cells := [et-iterate h init-cells matrix pretrust decay iters] - ;; et-run-and-snapshot's CBV-at-FFI evaluates final-cells first — that - ;; forces et-iterate to run, which forces every layer's et-add-iter to - ;; install its propagators — and only THEN does run-to-quiescence fire. - [et/et-run-and-snapshot h final-cells] + let h := et/net-new 1000000.0 + let init-cell := et/net-new-cell h pretrust + let final-cell := drive h init-cell matrix pretrust decay iters + et/net-run-read h final-cell -;; ---------- A concrete EigenTrust run ---------- -;; -;; Four peers in a small reputation graph: +;; ---------- Concrete reputation graph ---------- ;; -;; 0 1 2 3 -;; 0 [.0 .5 .5 .0] -;; 1 [.0 .0 .0 1.] -;; 2 [.5 .0 .0 .5] -;; 3 [.0 1. .0 .0] +;; Four peers; each row of C sums to 1. ;; -;; Each row is normalised to sum to 1. Pre-trust is uniform across all -;; four peers (0.25 each) — no prior authority. α = 0.15 is the -;; standard PageRank-style decay (it's effectively "teleport to a -;; pre-trusted peer with probability α each step"). +;; 0 1 2 3 +;; 0 [ 0.0 0.5 0.5 0.0 ] +;; 1 [ 0.0 0.0 0.0 1.0 ] +;; 2 [ 0.5 0.0 0.0 0.5 ] +;; 3 [ 0.0 1.0 0.0 0.0 ] ;; -;; After 20 power iterations the scores converge to the dominant -;; eigenvector of (1-α)·C^T + α·p·1^T. +;; Pre-trust is uniform (no prior authority). α = 0.15 is the standard +;; PageRank-style decay. def trust-matrix : [List [List Posit32]] := - [cons [cons ~0.0 [cons ~0.5 [cons ~0.5 [cons ~0.0 nil]]]] - [cons [cons ~0.0 [cons ~0.0 [cons ~0.0 [cons ~1.0 nil]]]] - [cons [cons ~0.5 [cons ~0.0 [cons ~0.0 [cons ~0.5 nil]]]] - [cons [cons ~0.0 [cons ~1.0 [cons ~0.0 [cons ~0.0 nil]]]] - nil]]]] + '['[0.0 0.5 0.5 0.0] + '[0.0 0.0 0.0 1.0] + '[0.5 0.0 0.0 0.5] + '[0.0 1.0 0.0 0.0]] -def pretrust : [List Posit32] := - [cons ~0.25 [cons ~0.25 [cons ~0.25 [cons ~0.25 nil]]]] +def pretrust : [List Posit32] := '[0.25 0.25 0.25 0.25] -;; Run EigenTrust for 20 iterations with α = 0.15. -[eigentrust pretrust trust-matrix ~0.15 20N] +;; Run 20 power iterations. Converges to ≈ [0.0652 0.4348 0.0652 0.4348]. +eigentrust pretrust trust-matrix 0.15 20N From 66a59d2c697dc875eb7de6fb2133d41f9331ff08 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 07:37:47 +0000 Subject: [PATCH 3/3] examples: pull EigenTrust math out of Racket into Prologos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asked: "what MUST stay on the Racket side?". Answer (now codified at the top of docs/tracking/2026-04-28_ETPROP_PITFALLS.md): 1. Propagator fire functions (Racket closures invoked by the scheduler; Prologos lambdas can't cross the FFI boundary). 2. Cell merge functions (same constraint). 3. The cell-value carrier (gen-tagged immutable Racket vector). 4. FFI marshalling glue (cons/nil chain walking, bit-pattern extraction). Everything else moves to .prologos. The Racket-side `net-add-prop` is now purpose-AGNOSTIC: it implements the generic affine combination out[j] := bias[j] + Σ_i weight[j][i] · in[i] with no knowledge of EigenTrust, decay, transposition, or pretrust. The .prologos source now does the entire algorithm: * `transpose` — pure Prologos, on List (List Posit32) * `scale-mat` — pure Prologos * `scale-vec` — pure Prologos * `zeros-of` — pure Prologos * `drive` — K-step iteration driver, in Prologos * `eigentrust` — composes the above: weights = (1 − α) · transpose(C) biases = α · pretrust and hands them to `net-add-prop`. The Racket side never sees the matrix or the decay constant. Mantra alignment is preserved: each iteration is still ONE compound cell + ONE broadcast propagator (parallel-decomposable across OS threads at fire time per BSP-LE Track 2 Phase 1B). The cell DAG iter-0 → iter-K still drives firing order via dataflow. Result still matches the Python reference (sum = 1.0): [0.0652, 0.4348, 0.0652, 0.4348] Tests: 99 across foreign, foreign-block, pvec under Racket 9.1. --- docs/tracking/2026-04-28_ETPROP_PITFALLS.md | 37 ++++ .../prologos/lib/examples/eigentrust-prop.rkt | 183 ++++++++++-------- .../prologos/lib/examples/eigentrust.prologos | 168 ++++++++++++---- 3 files changed, 261 insertions(+), 127 deletions(-) diff --git a/docs/tracking/2026-04-28_ETPROP_PITFALLS.md b/docs/tracking/2026-04-28_ETPROP_PITFALLS.md index ea895ed1e..d10fd5844 100644 --- a/docs/tracking/2026-04-28_ETPROP_PITFALLS.md +++ b/docs/tracking/2026-04-28_ETPROP_PITFALLS.md @@ -11,6 +11,43 @@ The list is **observation only** — these are bugs / friction in language features and tooling, not in the EigenTrust example itself. They're filed here because tracking them in commit messages alone made them invisible. +## What MUST stay on the Racket side (the irreducible core) + +Across two passes of "minimise the Racket footprint", these four +responsibilities resisted every attempt to push them out into Prologos: + + 1. **Propagator fire functions.** The fire-fn is a Racket closure invoked + by the Racket-side BSP scheduler with the network as its argument. + Prologos lambdas can't cross the FFI boundary as live closures (the + FFI marshaller validates a fixed type signature on each call; live + closures don't fit that protocol). + + 2. **Cell merge functions.** Same constraint. The propagator network + calls merge-fn during cell writes (and during BSP fold-merges). It + has to be a Racket procedure. + + 3. **The cell-value carrier.** A gen-tagged immutable Racket vector. The + gen tag has to interoperate with Racket's `equal?` for the cell's + change detection, and the merge function has to be a Racket procedure + that operates on whatever Racket data structure the cell holds. + + 4. **FFI marshalling glue.** Walking Prologos cons/nil chains, extracting + posit32 bit-patterns out of `expr-posit32` IR nodes, encoding rationals + back into bit patterns. Pure Racket-side bookkeeping. + +Everything else — matrix transpose, decay scaling, bias computation, +initial-zero-vector, the iteration driver, the entire EigenTrust update +rule's arithmetic decomposition — is in the .prologos source. The +Racket-side `net-add-prop` is purpose-AGNOSTIC: it implements the +generic affine combination + + out[j] := bias[j] + Σ_i weight[j][i] · in[i] + +with no knowledge of EigenTrust, decay, transposition, or pretrust. + +The four FFI primitives (`net-new`, `net-new-cell`, `net-add-prop`, +`net-run-read`) are the entire Racket-Prologos interface. + ------------------------------------------------------------------ ## 1. `def` is reference-transparent — side effects re-fire on every use diff --git a/racket/prologos/lib/examples/eigentrust-prop.rkt b/racket/prologos/lib/examples/eigentrust-prop.rkt index 039e0b441..92aa31abf 100644 --- a/racket/prologos/lib/examples/eigentrust-prop.rkt +++ b/racket/prologos/lib/examples/eigentrust-prop.rkt @@ -1,13 +1,36 @@ #lang racket/base ;;; -;;; PROPAGATOR-BASED EIGENTRUST — Racket FFI shim for Prologos. +;;; PROPAGATOR-BASED EIGENTRUST — minimal Racket FFI shim. ;;; -;;; The user-facing pitch: Prologos plans first-class propagator support, -;;; but the type checker isn't yet wired through the surface forms in the -;;; grammar. This shim exposes the underlying primitives (`net-new`, -;;; `net-new-cell`, `net-add-prop`) directly via Racket FFI so a `.prologos` -;;; file can build and run a propagator network today. +;;; This module is the irreducible Racket-side plumbing. Prologos plans +;;; first-class propagator support, but the type checker isn't yet wired +;;; through the surface forms in the grammar; until then, the four +;;; propagator-network primitives below are exposed via `foreign racket +;;; "..."`. Everything else — the EigenTrust-specific algorithm — lives in +;;; the .prologos source (matrix transpose, decay weighting, pretrust +;;; biasing, the iteration loop, etc.). +;;; +;;; ----------------------------------------------------------------- +;;; What MUST stay in Racket (the irreducible core): +;;; +;;; 1. Propagator fire functions are Racket closures invoked by the +;;; Racket-side scheduler; Prologos lambdas can't cross the FFI +;;; boundary as live closures. +;;; 2. Cell merge functions, same reason. +;;; 3. The cell-value carrier (gen-tagged immutable Posit32 vector) and +;;; its monotone merge — both Racket data structures. +;;; 4. The persistent prop-network struct, handle/cell registries. +;;; 5. FFI marshalling: list walking on cons/nil chains, posit +;;; bit-pattern extraction. +;;; +;;; What MOVED out, into the .prologos source: +;;; +;;; 1. Matrix transpose +;;; 2. Decay scaling (1 − α) · C +;;; 3. Bias computation α · pretrust +;;; 4. Initial-zero vector for new-layer cells +;;; 5. The iteration driver ;;; ;;; ----------------------------------------------------------------- ;;; Mantra alignment (per .claude/rules/on-network.md) @@ -15,63 +38,49 @@ ;;; "All-at-once, all in parallel, structurally emergent ;;; information flow ON-NETWORK." ;;; -;;; This shim takes the mantra at face value: -;;; ;;; * All-at-once — each iteration is ONE compound cell holding the -;;; full score vector for all peers, allocated atomically -;;; (`net-new-cells-batch`, internally). The matrix / -;;; pretrust / decay configuration arrives as a single -;;; declarative payload. +;;; full score vector for all peers, allocated atomically. ;;; * All in parallel — `net-add-prop` installs ONE broadcast propagator -;;; (per `.claude/rules/propagator-design.md` § Broadcast -;;; Propagators) covering all N peer-update items. The -;;; scheduler can decompose the broadcast across OS -;;; threads at fire time. NO N-propagator step-think, -;;; NO `for/fold` walking the peer list. -;;; * Structurally emergent — the cell DAG is iter-0 → iter-1 → ... → -;;; iter-K. The BSP scheduler fires layers in -;;; topological order; we never tell it what fires when. -;;; * ON-NETWORK — every score lives in a cell. The compound carrier -;;; holds the full peer vector; merge keeps the higher -;;; generation so each layer's broadcast write strictly -;;; dominates the previous initial value. +;;; (per .claude/rules/propagator-design.md § Broadcast +;;; Propagators) covering all N components as items. +;;; The BSP scheduler decomposes across OS threads at +;;; fire time. +;;; * Structurally emergent — the cell DAG iter-0 → iter-1 → ... → iter-K +;;; drives firing order via dataflow. +;;; * ON-NETWORK — every score lives in a cell. ;;; ;;; ----------------------------------------------------------------- ;;; FFI surface (consumed from .prologos via `foreign racket "..."`): ;;; ;;; net-new : Posit32 -> Posit32 -;;; Allocate a fresh propagator network with the given fuel budget; -;;; return a Posit32 handle id. +;;; Allocate a propagator network with the given fuel budget; return +;;; a Posit32 handle id. ;;; ;;; net-new-cell : Posit32 -> List Posit32 -> Posit32 -;;; Allocate one compound cell holding a vector of Posit32 scores. -;;; Return a Posit32 cell-ref. +;;; Allocate ONE compound cell holding the given Posit32 vector. +;;; Returns a Posit32 cell-ref. ;;; ;;; net-add-prop : Posit32 -> Posit32 -> Posit32 ;;; -> List (List Posit32) -> List Posit32 -> Posit32 -;;; -> Posit32 -;;; Install ONE broadcast propagator on the network whose fire-fn -;;; implements one EigenTrust round: +;;; Install ONE broadcast propagator that computes the affine +;;; combination: ;;; -;;; t_{k+1}[j] = α · pretrust[j] -;;; + (1 − α) · Σ_i C[i][j] · t_k[i] +;;; out[j] := biases[j] + Σ_i weights[j][i] · in[i] ;;; -;;; Args: handle, input-cell, output-cell, matrix-rows (C in -;;; row-major form), pretrust vector, decay α. Returns the -;;; output-cell so callers can chain layers. +;;; Domain-AGNOSTIC: the .prologos caller precomputes weights and +;;; biases from whatever raw inputs the algorithm has. Returns +;;; output-cref so layers can be chained. ;;; ;;; net-run-read : Posit32 -> Posit32 -> List Posit32 -;;; Run the network to quiescence, then read the final layer's -;;; compound cell as a Posit32 list. Combined into one call so the -;;; Prologos call-by-name reduction has to evaluate the full layer -;;; chain (forces every net-add-prop side effect) before the -;;; quiescence pass starts. +;;; Run the network to quiescence, then read the cell as a Posit32 +;;; list. Combined so call-by-name reduction is forced to evaluate +;;; the full layer chain before the quiescence pass. ;;; ;;; All "side-effecting" calls return a meaningful Posit32 (handle, cell ;;; ref, or score list) instead of Unit. Prologos's reduction is -;;; call-by-name — an unused result expression would never be evaluated and -;;; the side effect would be silently dropped. Threading a Posit32 through -;;; every call forces strict evaluation order. +;;; call-by-name — an unused result expression would never be evaluated +;;; and the side effect would be silently dropped. Threading a Posit32 +;;; through every call forces strict evaluation order. ;;; (require racket/match @@ -256,70 +265,72 @@ (error 'eigentrust-prop "no such cell-ref: ~a" cref))) ;; ======================================== -;; net-add-prop — install ONE broadcast propagator for an EigenTrust round. +;; net-add-prop — install ONE broadcast propagator implementing a +;; domain-agnostic affine combination over a vector cell. ;; ======================================== ;; ;; Per `.claude/rules/propagator-design.md` § Broadcast Propagators: ;; "any for/fold or for/list that processes independent items is a -;; candidate for broadcast." This is exactly that — N peer updates that -;; read the same prev-iter cell. The broadcast-profile metadata makes -;; the propagator decomposable across OS threads (BSP-LE Track 2 Phase -;; 1B) when the scheduler chooses. +;; candidate for broadcast." This is exactly that — N output components +;; share one input cell. The broadcast-profile metadata makes the +;; propagator decomposable across OS threads (BSP-LE Track 2 Phase 1B) +;; when the scheduler chooses. +;; +;; for each item j in 0..N-1: +;; out[j] := bias[j] + Σ_i weights[j][i] · in[i] +;; +;; The propagator is purpose-AGNOSTIC. EigenTrust-specific math (matrix +;; transpose, decay weighting, pretrust biasing) lives in the .prologos +;; source — only the linear-combination and gen-tagging plumbing is here. +;; This is the irreducible Racket-side core: cell merge functions and +;; propagator fire functions cannot cross the FFI boundary as Prologos +;; closures, so they MUST be implemented in Racket. +;; +;; weights : Prologos List (List Posit32) — weights[j][i] +;; biases : Prologos List Posit32 — biases[j] ;; -;; input-cref : prev iteration cell (compound, gen-vec carrier) -;; output-cref : next iteration cell (compound, gen-vec carrier) -;; matrix : Prologos List (List Posit32) — row-major C -;; pretrust : Prologos List Posit32 — p -;; decay-bits : Posit32 bit pattern — α -;; Returns : output-cref (chains). +;; Returns output-cref so the caller can chain net-add-prop calls. -(define (net-add-prop hbits input-cref output-cref matrix pretrust decay-bits) +(define (net-add-prop hbits input-cref output-cref weights-list biases-list) (define handle (lookup-handle hbits)) (define input-cid (lookup-cell input-cref)) (define out-cid (lookup-cell output-cref)) - (define C-rows (matrix->vec-of-vecs matrix)) - (define p-bits (list-of-posit32->bits-vec pretrust)) - (define n (vector-length p-bits)) - (unless (= n (vector-length C-rows)) - (error 'net-add-prop "matrix outer length ~a != peer count ~a" - (vector-length C-rows) n)) - (for ([row (in-vector C-rows)] [i (in-naturals)]) + (define W-rows (matrix->vec-of-vecs weights-list)) + (define b-bits (list-of-posit32->bits-vec biases-list)) + (define n (vector-length b-bits)) + (unless (= n (vector-length W-rows)) + (error 'net-add-prop "weights outer length ~a != biases length ~a" + (vector-length W-rows) n)) + (for ([row (in-vector W-rows)] [j (in-naturals)]) (unless (= n (vector-length row)) - (error 'net-add-prop "matrix row ~a has ~a entries, expected ~a" - i (vector-length row) n))) - (define decay-rat (posit32-to-rational decay-bits)) - (define one-m-decay (- 1 decay-rat)) - (define p-rats (for/vector ([b (in-vector p-bits)]) (posit32-to-rational b))) - ;; Pre-compute, in exact rationals, the per-target weights and bias. - ;; weights[j][i] = (1 - α) · C[i][j] - ;; bias[j] = α · p[j] - (define weights - (for/vector ([j (in-range n)]) - (for/vector ([i (in-range n)]) - (* one-m-decay - (posit32-to-rational (vector-ref (vector-ref C-rows i) j)))))) + (error 'net-add-prop "weights row ~a has ~a entries, expected ~a" + j (vector-length row) n))) + ;; Convert weights and biases to exact rationals once, up front. + (define W + (for/vector ([row (in-vector W-rows)]) + (for/vector ([b (in-vector row)]) (posit32-to-rational b)))) (define biases - (for/vector ([j (in-range n)]) (* decay-rat (vector-ref p-rats j)))) + (for/vector ([b (in-vector b-bits)]) (posit32-to-rational b))) ;; --- Broadcast propagator: ONE install, N items, parallel-decomposable. - ;; items: peer indices 0..N-1. (define items (build-list n values)) - ;; item-fn: per-peer update. Reads the (single) prev-iter compound cell, - ;; computes t_{k+1}[j], returns (input-gen, j, bits). The input-gen - ;; tag lets the next-layer's gen exceed it so re-fires (after the input - ;; cell evolves) strictly dominate stale fires. + ;; item-fn: per-component update. Reads the prev-iter compound cell, + ;; computes the affine combination for component j, returns + ;; (input-gen, j, bits) + ;; The input-gen tag lets the next layer's gen strictly exceed it so + ;; re-fires (after the input cell evolves) dominate stale fires. (define (item-fn j input-vals) (define prev-cell-val (car input-vals)) (define prev (current-vec prev-cell-val)) (define input-gen (current-gen prev-cell-val)) - (define wts (vector-ref weights j)) + (define wts (vector-ref W j)) (define bias (vector-ref biases j)) (define sum (for/fold ([acc bias]) ([w (in-vector wts)] [b (in-vector prev)]) (+ acc (* w (posit32-to-rational b))))) (list input-gen j (posit32-encode sum))) - ;; result-merge: accumulate per-peer results into a gen-vec carrier - ;; whose gen is input-gen + 1. Gen-merge then keeps the latest fire when - ;; the input cell evolves across BSP rounds. + ;; result-merge: accumulate per-component results into a gen-vec + ;; carrier whose gen is input-gen + 1. Gen-merge then keeps the latest + ;; fire when the input cell evolves across BSP rounds. (define (final-merge acc r) (define input-gen (car r)) (define j (cadr r)) diff --git a/racket/prologos/lib/examples/eigentrust.prologos b/racket/prologos/lib/examples/eigentrust.prologos index 19507a842..e501694de 100644 --- a/racket/prologos/lib/examples/eigentrust.prologos +++ b/racket/prologos/lib/examples/eigentrust.prologos @@ -10,10 +10,10 @@ require [prologos::data::list :refer [List]] ;; ;; EigenTrust [Kamvar/Schlosser/Garcia-Molina 2003] is the classic ;; reputation algorithm for peer-to-peer networks. Each peer i has a -;; trust score t[i] in [0, 1]. The local trust matrix C[i][j] says how -;; much peer i trusts peer j (rows of C are normalised to sum to 1). A -;; pre-trust vector p anchors the iteration against Sybil attacks. The -;; power iteration +;; trust score t[i] in [0, 1]. Local trust C[i][j] says how much peer i +;; trusts peer j (rows of C are normalised to sum to 1). A pre-trust +;; vector p anchors the iteration against Sybil attacks. The power +;; iteration ;; ;; t_{k+1} = α · p + (1 − α) · C^T · t_k ;; @@ -35,33 +35,53 @@ require [prologos::data::list :refer [List]] ;; drives firing order; we never tell the scheduler ;; what fires when. ;; * ON-NETWORK — every score lives in a cell; the gen-tagged -;; compound merge is CALM-monotone (the propagator -;; network's standard contract). +;; compound merge is CALM-monotone. ;; -;; First-class propagator surface (`net-new`, `net-new-cell`, `net-add-prop`) -;; is on the Prologos roadmap but not yet wired through the type checker; -;; this file imports the same primitives via `foreign racket "..."` until -;; that work lands. +;; ----- Where each piece of the algorithm lives ----- +;; +;; Racket (eigentrust-prop.rkt — irreducible plumbing): +;; * The four propagator-network primitives (net-new, net-new-cell, +;; net-add-prop, net-run-read). +;; * The compound-cell carrier and its gen-tagged merge. +;; * The fire function — a Racket closure that evaluates a +;; domain-AGNOSTIC affine combination +;; out[j] := bias[j] + Σ_i W[j][i] · in[i] +;; on the propagator's items. (Closures can't cross the FFI +;; boundary, so the fire-fn must stay Racket; we keep it generic so +;; EigenTrust-specific math doesn't leak into it.) +;; * Posit32 ↔ rational marshalling. +;; +;; Prologos (this file — the algorithm): +;; * Matrix transpose +;; * Decay scaling (1 − α) · Cᵀ +;; * Bias computation α · pretrust +;; * Building zero-vectors for fresh-layer cell initialisation +;; * The K-iteration driver loop +;; +;; First-class propagator surface (`net-new`, `net-new-cell`, +;; `net-add-prop`) is on the Prologos roadmap but not yet wired through +;; the type checker; this file imports the same primitives via +;; `foreign racket "..."` until that work lands. ;; ;; ============================================================ -;; -- FFI: the four propagator primitives we need -- +;; -- FFI: the irreducible four propagator primitives -- ;; ;; All carriers are Posit32 — handles, cell-refs, fuel, scores, weights. -;; "Side-effecting" calls return a meaningful Posit32 (the cell-ref or the -;; handle itself) so call-by-name reduction is forced to evaluate them. +;; "Side-effecting" calls return a meaningful Posit32 so call-by-name +;; reduction is forced to evaluate them. foreign racket "lib/examples/eigentrust-prop.rkt" :as et net-new : Posit32 -> Posit32 net-new-cell : Posit32 [List Posit32] -> Posit32 - net-add-prop : Posit32 Posit32 Posit32 [List [List Posit32]] [List Posit32] Posit32 -> Posit32 + net-add-prop : Posit32 Posit32 Posit32 [List [List Posit32]] [List Posit32] -> Posit32 net-run-read : Posit32 Posit32 -> [List Posit32] -;; A list of zeros with the same shape as the reference vector — used as -;; the initial value of every iter-(k+1) cell before its broadcast -;; propagator fires. +;; ---------- Vector / matrix helpers (pure Prologos) ---------- + +;; Constant-zero vector with the same length as the reference. spec zeros-of [List Posit32] -> [List Posit32] defn zeros-of [xs] match xs @@ -69,45 +89,110 @@ defn zeros-of [xs] | [cons _ rs] -> [cons 0.0 [zeros-of rs]] -;; Chain K iteration layers of broadcast propagators on the network. -;; Each `net-add-prop` install IS structurally one round: ONE broadcast -;; propagator covering all N peers, reading the previous compound cell -;; and writing the next compound cell. After all K layers are installed, -;; `net-run-read` runs the BSP scheduler to quiescence and reads the -;; final layer's score vector. +;; scale-vec α v ↦ [α · v[0], α · v[1], …] +spec scale-vec Posit32 [List Posit32] -> [List Posit32] +defn scale-vec [s xs] + match xs + | nil -> nil + | [cons x rs] -> [cons [p32* s x] [scale-vec s rs]] + + +;; scale-mat α M ↦ M with every entry scaled by α +spec scale-mat Posit32 [List [List Posit32]] -> [List [List Posit32]] +defn scale-mat [s xss] + match xss + | nil -> nil + | [cons xs rs] -> [cons [scale-vec s xs] [scale-mat s rs]] + + +;; First column of a matrix; truncates if any row is shorter than the +;; rest. (Used by `transpose`.) +spec map-head [List [List Posit32]] -> [List Posit32] +defn map-head [xss] + match xss + | nil -> nil + | [cons xs rest] -> + match xs + | nil -> nil + | [cons x _] -> [cons x [map-head rest]] + + +;; Drop the first column of a matrix. +spec map-tail [List [List Posit32]] -> [List [List Posit32]] +defn map-tail [xss] + match xss + | nil -> nil + | [cons xs rest] -> + match xs + | nil -> [map-tail rest] + | [cons _ xs'] -> [cons xs' [map-tail rest]] + + +;; Predicate: every row of the matrix is nil. +spec all-empty? [List [List Posit32]] -> Bool +defn all-empty? [xss] + match xss + | nil -> true + | [cons xs rs] -> + match xs + | nil -> [all-empty? rs] + | _ -> false + + +;; transpose M ↦ Mᵀ for a (rectangular) matrix. +spec transpose [List [List Posit32]] -> [List [List Posit32]] +defn transpose [xss] + match [all-empty? xss] + | true -> nil + | false -> [cons [map-head xss] [transpose [map-tail xss]]] + + +;; ---------- The EigenTrust algorithm ---------- +;; +;; Each `net-add-prop` install is ONE broadcast propagator covering all N +;; peers in parallel. Layer cells form a DAG iter-0 → iter-1 → … → iter-K +;; that the BSP scheduler fires in topological order. ;; -;; The construction-time recursion is just for traversing K. At RUN -;; time, every layer's propagator fires in dataflow order — the -;; iter-0 → iter-K DAG IS the parallel decomposition. -spec drive Posit32 Posit32 [List [List Posit32]] [List Posit32] Posit32 Nat -> Posit32 +;; Construction-time recursion in `drive` is just for traversing K. At +;; RUN time, the cell-DAG IS the parallel decomposition. +spec drive Posit32 Posit32 [List [List Posit32]] [List Posit32] [List Posit32] Nat -> Posit32 defn drive - | h cell matrix pretrust decay 0N -> cell - | h cell matrix pretrust decay [suc k] -> - let next-cell := et/net-new-cell h (zeros-of pretrust) - let chained := et/net-add-prop h cell next-cell matrix pretrust decay - drive h chained matrix pretrust decay k + | h cell weights biases _zeros 0N -> cell + | h cell weights biases zeros [suc k] -> + let next-cell := et/net-new-cell h zeros + let chained := et/net-add-prop h cell next-cell weights biases + drive h chained weights biases zeros k +;; t_{k+1} = α · p + (1 − α) · Cᵀ · t_k +;; +;; The propagator's affine kernel is out[j] := bias[j] + Σ W[j][i] · in[i], +;; so we precompute, in pure Prologos: +;; +;; weights = (1 − α) · Cᵀ +;; biases = α · pretrust +;; +;; …and hand both to net-add-prop. The Racket side never sees the matrix +;; or the decay constant. spec eigentrust [List Posit32] [List [List Posit32]] Posit32 Nat -> [List Posit32] defn eigentrust [pretrust matrix decay iters] - let h := et/net-new 1000000.0 - let init-cell := et/net-new-cell h pretrust - let final-cell := drive h init-cell matrix pretrust decay iters + let one-minus-decay := [p32- 1.0 decay] + let weights := [scale-mat one-minus-decay [transpose matrix]] + let biases := [scale-vec decay pretrust] + let zeros := [zeros-of pretrust] + let h := [et/net-new 1000000.0] + let init-cell := [et/net-new-cell h pretrust] + let final-cell := [drive h init-cell weights biases zeros iters] et/net-run-read h final-cell ;; ---------- Concrete reputation graph ---------- ;; -;; Four peers; each row of C sums to 1. -;; ;; 0 1 2 3 ;; 0 [ 0.0 0.5 0.5 0.0 ] ;; 1 [ 0.0 0.0 0.0 1.0 ] ;; 2 [ 0.5 0.0 0.0 0.5 ] ;; 3 [ 0.0 1.0 0.0 0.0 ] -;; -;; Pre-trust is uniform (no prior authority). α = 0.15 is the standard -;; PageRank-style decay. def trust-matrix : [List [List Posit32]] := '['[0.0 0.5 0.5 0.0] @@ -118,5 +203,6 @@ def trust-matrix : [List [List Posit32]] := def pretrust : [List Posit32] := '[0.25 0.25 0.25 0.25] -;; Run 20 power iterations. Converges to ≈ [0.0652 0.4348 0.0652 0.4348]. +;; Run 20 power iterations with α = 0.15. Converges to +;; ≈ [0.0652 0.4348 0.0652 0.4348]. eigentrust pretrust trust-matrix 0.15 20N