From b8dcf8ba58b4f7bb37e415d7e4f433296b65b19d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 08:32:05 +0000 Subject: [PATCH] Add pvec-zip-with library function (eigentrust pitfalls #13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds pvec-zip-with: (A -> B -> C) -> PVec A -> PVec B -> PVec C as a library function in lib/prologos/core/pvec.prologos. Truncates to the shorter PVec's length, mirroring List zip-with's semantics. The signature also mirrors List zip-with: spec pvec-zip-with {A B C : Type} [A -> B -> C] [PVec A] [PVec B] -> [PVec C]. Implementation routes through pvec-to-list / zip-with / pvec-from-list rather than threading an index accumulator via pvec-fold + pvec-nth. The list-conversion path is O(n) (same asymptotic cost as a direct index walk) and avoids the closure-multiplicity issues that pitfall #2 identifies for pvec-fold callbacks that capture scalars. The wrapping zip-with's f parameter is itself a function (naturally mw), so it does not trip the QTT path that scalar capture in a pvec-fold closure would. Without this primitive, callers writing elementwise binary operations on two PVecs (e.g. pvec-zip-with int+ a b in eigentrust) had to write explicit index-threaded-accumulator recursion. This restores parity with the List API. Adds tests/test-pvec-zip-with.rkt covering: equal-length elementwise add (length / nth values), truncation when xs is shorter, truncation when ys is shorter, both-empty / xs-empty edge cases, heterogeneous result types (Nat -> Nat -> Bool), and the eigentrust mirror case (elementwise add of two equal-length Nat vectors). Tests do not run under Racket 8.10 due to the pre-existing BSP scheduler issue ((thread #:pool 'own ...) keyword arg unsupported); both this new file and the pre-existing test-pvec-fold.rkt fail with the same test-support.rkt module-load error on 8.10. Test file compiles cleanly via raco make and parses cleanly through prologos-sexp-read. pvec-fold-zip skipped: the user-facing case (Σ aᵢ·bᵢ) decomposes cleanly as pvec-fold add zero (pvec-zip-with mult a b) at one extra allocation. A direct pvec-fold-zip primitive would either add a second native AST node or require building an intermediate Sigma-pair list which itself risks tripping multiplicity inference. Defer until a measured use case justifies the extra surface. https: //claude.ai/code/session_01MbncYJnrvjzhbVWw4xGi5x Co-authored-by: kumavis <1474978+kumavis@users.noreply.github.com> --- .../prologos/lib/prologos/core/pvec.prologos | 14 +- racket/prologos/tests/test-pvec-zip-with.rkt | 179 ++++++++++++++++++ 2 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 racket/prologos/tests/test-pvec-zip-with.rkt diff --git a/racket/prologos/lib/prologos/core/pvec.prologos b/racket/prologos/lib/prologos/core/pvec.prologos index bd0d961a6..2daa22cbc 100644 --- a/racket/prologos/lib/prologos/core/pvec.prologos +++ b/racket/prologos/lib/prologos/core/pvec.prologos @@ -8,7 +8,7 @@ require [prologos::core::collection-traits :refer [Seqable Functor]] require [prologos::data::lseq :refer [LSeq]] require [prologos::data::lseq-ops :refer [list-to-lseq lseq-to-list]] -require [prologos::data::list :refer [List nil cons]] +require [prologos::data::list :refer [List nil cons zip-with]] require [prologos::data::option :refer [Option some none]] require [prologos::data::nat :refer [lt?]] @@ -134,3 +134,15 @@ defn pvec-from-list-fn [xs] spec pvec-to-list-fn {A : Type} [PVec A] -> [List A] defn pvec-to-list-fn [v] pvec-to-list v + +;; ======================================== +;; pvec-zip-with : combine two PVecs element-wise +;; ======================================== +;; Signature mirrors List zip-with; truncates to the shorter PVec's length. +;; Implementation: convert both pvecs to lists, zip-with on lists, convert +;; the result back to a PVec. The asymptotic cost is the same as a direct +;; index walk: O(n) for the dominant length. +spec pvec-zip-with {A B C : Type} [A -> B -> C] [PVec A] [PVec B] -> [PVec C] + :doc "Combine two PVecs element-wise with a function; truncates to shorter." +defn pvec-zip-with [f xs ys] + pvec-from-list [zip-with f [pvec-to-list xs] [pvec-to-list ys]] diff --git a/racket/prologos/tests/test-pvec-zip-with.rkt b/racket/prologos/tests/test-pvec-zip-with.rkt new file mode 100644 index 000000000..34147d7c7 --- /dev/null +++ b/racket/prologos/tests/test-pvec-zip-with.rkt @@ -0,0 +1,179 @@ +#lang racket/base + +;;; +;;; Tests for pvec-zip-with library function. +;;; Addresses eigentrust pitfalls doc #13: provide a zip-with equivalent for +;;; PVec so elementwise operations on two PVecs do not require explicit +;;; index-threaded-accumulator recursion in user code. +;;; +;;; Signature: pvec-zip-with : (A -> B -> C) -> PVec A -> PVec B -> PVec C +;;; Truncates to the length of the shorter input (mirrors List zip-with). +;;; + +(require rackunit + racket/list + racket/string + "test-support.rkt" + "../macros.rkt" + "../syntax.rkt" + "../errors.rkt" + "../metavar-store.rkt" + "../global-env.rkt" + "../driver.rkt" + "../namespace.rkt" + "../multi-dispatch.rkt") + +;; ======================================== +;; Helpers +;; ======================================== + +(define (run-ns s) + (parameterize ([current-prelude-env (hasheq)] + [current-module-definitions-content (hasheq)] + [current-ns-context #f] + [current-module-registry prelude-module-registry] + [current-lib-paths (list prelude-lib-dir)] + [current-mult-meta-store (make-hasheq)] + [current-preparse-registry prelude-preparse-registry] + [current-ctor-registry (current-ctor-registry)] + [current-type-meta (current-type-meta)] + [current-trait-registry prelude-trait-registry] + [current-impl-registry prelude-impl-registry] + [current-param-impl-registry prelude-param-impl-registry] + [current-multi-defn-registry (current-multi-defn-registry)] + [current-spec-store (hasheq)]) + (install-module-loader!) + (process-string s))) + +(define (run-last s) + (last (run-ns s))) + +(define (check-contains actual substr [msg #f]) + (check-true (and (string? actual) (string-contains? actual substr)) + (or msg (format "Expected ~s to contain ~s" actual substr)))) + +(define preamble + (string-append + "(ns pvec-zip-with-test)\n" + "(require [prologos::core::pvec :refer [pvec-zip-with]])\n")) + +;; ======================================== +;; 1. Equal-length pvecs (basic case) +;; ======================================== + +(test-case "pvec-zip-with/equal-length: type-checks against PVec Nat" + (define result + (run-last + (string-append preamble + "(check (pvec-zip-with add @[1N 2N 3N] @[10N 20N 30N]) : (PVec Nat))\n"))) + (check-equal? result "OK")) + +(test-case "pvec-zip-with/equal-length: elementwise add yields expected length" + ;; @[1+10, 2+20, 3+30] = @[11, 22, 33]; length 3 + (define result + (run-last + (string-append preamble + "(eval (pvec-length" + " (pvec-zip-with add @[1N 2N 3N] @[10N 20N 30N])))\n"))) + (check-equal? result "3N : Nat")) + +(test-case "pvec-zip-with/equal-length: elementwise add nth=0 yields 11" + (define result + (run-last + (string-append preamble + "(eval (pvec-nth" + " (pvec-zip-with add @[1N 2N 3N] @[10N 20N 30N])" + " zero))\n"))) + (check-equal? result "11N : Nat")) + +(test-case "pvec-zip-with/equal-length: elementwise add nth=2 yields 33" + (define result + (run-last + (string-append preamble + "(eval (pvec-nth" + " (pvec-zip-with add @[1N 2N 3N] @[10N 20N 30N])" + " (suc (suc zero))))\n"))) + (check-equal? result "33N : Nat")) + +;; ======================================== +;; 2. Truncation: shorter-first (xs shorter) +;; ======================================== + +(test-case "pvec-zip-with/truncate-xs-shorter: length is min(2,4)=2" + (define result + (run-last + (string-append preamble + "(eval (pvec-length" + " (pvec-zip-with add @[1N 2N] @[10N 20N 30N 40N])))\n"))) + (check-equal? result "2N : Nat")) + +;; ======================================== +;; 3. Truncation: shorter-second (ys shorter) +;; ======================================== + +(test-case "pvec-zip-with/truncate-ys-shorter: length is min(4,1)=1" + (define result + (run-last + (string-append preamble + "(eval (pvec-length" + " (pvec-zip-with add @[1N 2N 3N 4N] @[100N])))\n"))) + (check-equal? result "1N : Nat")) + +(test-case "pvec-zip-with/truncate-ys-shorter: nth=0 yields 1+100=101" + (define result + (run-last + (string-append preamble + "(eval (pvec-nth" + " (pvec-zip-with add @[1N 2N 3N 4N] @[100N])" + " zero))\n"))) + (check-equal? result "101N : Nat")) + +;; ======================================== +;; 4. Empty inputs +;; ======================================== + +(test-case "pvec-zip-with/both-empty: yields empty PVec (length 0)" + (define result + (run-last + (string-append preamble + "(check (pvec-zip-with add @[] @[]) : (PVec Nat))\n"))) + (check-equal? result "OK")) + +(test-case "pvec-zip-with/xs-empty: yields empty PVec (length 0)" + (define result + (run-last + (string-append preamble + "(eval (pvec-length" + " (pvec-zip-with add @[] @[1N 2N 3N])))\n"))) + (check-equal? result "0N : Nat")) + +;; ======================================== +;; 5. Heterogeneous: f changes element type (Nat -> Nat -> Bool) +;; ======================================== + +(test-case "pvec-zip-with/result-type: PVec Bool when f returns Bool" + ;; (fn x y -> zero? x) :: Nat -> Nat -> Bool + (define result + (run-last + (string-append preamble + "(check (pvec-zip-with" + " (fn (x : Nat) (y : Nat) (zero? x))" + " @[0N 1N]" + " @[10N 20N]) : (PVec Bool))\n"))) + (check-equal? result "OK")) + +;; ======================================== +;; 6. Eigentrust use case mirror: pvec-zip-with with named binary op +;; ======================================== +;; The eigentrust pitfalls doc's motivating example was elementwise addition over +;; two reputation vectors. Verify that pattern works end-to-end. + +(test-case "pvec-zip-with/eigentrust-mirror: add two equal-length vectors" + (define result + (run-last + (string-append preamble + "(eval (pvec-to-list" + " (pvec-zip-with add @[5N 7N 11N] @[2N 3N 4N])))\n"))) + (check-contains result "7N") ; 5 + 2 + (check-contains result "10N") ; 7 + 3 + (check-contains result "15N")) ; 11 + 4