jsecmon: port dga::shannon_entropy (nested for/fold + f64 math)

Jaime Fournier <jaimef@linbsd.org>

1e0c552d59bf60b4f1749ece6d173dffcbaffb49

diff --git a/README.md b/README.md
index 0df11c5..291e3eb 100644
--- a/README.md
+++ b/README.md
@@ -34,8 +34,8 @@ then crypto orchestration, then I/O / async / FFI (monitors, server, storage).
 | secmon module            | jsecmon            | status                          |
 |--------------------------|--------------------|---------------------------------|
 | `dga::max_consonant_run` | `typed/dga.ss`     | ✅ ported, vectors pass         |
-| `dga::shannon_entropy`   | `typed/dga.ss`     | ⏳ needs backend f64 `log2`     |
-| `dga::score_domain`      | `typed/dga.ss`     | ⏳ after entropy + string ops   |
+| `dga::shannon_entropy`   | `typed/dga.ss`     | ✅ ported, vectors pass         |
+| `dga::score_domain`      | `typed/dga.ss`     | ⏳ needs string ops (lowercase, split, ends_with, char classes) |
 | `lolbin`, `sigma`, `triage` | —               | ⏳ pure logic, queued           |
 | `crypto::{psk,ecies}`    | —                  | ⏳ FFI-delegated; orchestration only |
 | monitors / server / storage / ebpf / dtrace | —  | ⏳ I/O+async+FFI, last           |
diff --git a/tests/dga_vectors.rs b/tests/dga_vectors.rs
index 9ca9141..27cc18a 100644
--- a/tests/dga_vectors.rs
+++ b/tests/dga_vectors.rs
@@ -1,7 +1,7 @@
 // Verifies the Typed-Jerboa-generated dga kernel against secmon's own
 // dga::tests vectors (secmon/src/dga.rs). `make rust` copies this file into
 // the generated crate's tests/ dir before `cargo test`.
-use jerboa_typed_generated::jsecmon_dga::max_consonant_run;
+use jerboa_typed_generated::jsecmon_dga::{max_consonant_run, shannon_entropy};
 
 #[test]
 fn consonant_run_matches_secmon_vectors() {
@@ -15,3 +15,14 @@ fn consonant_run_matches_secmon_vectors() {
     // uppercase folds the same as lowercase
     assert_eq!(max_consonant_run("STRENGTHS".to_string()), 5);
 }
+
+#[test]
+fn shannon_entropy_matches_secmon_vectors() {
+    // secmon's dga::tests::shannon_entropy_baseline
+    assert!(shannon_entropy("aaaaaa".to_string()) < 0.1);
+    assert!(shannon_entropy("abcdef".to_string()) > 2.5);
+    // empty string is 0.0 (no special-casing needed)
+    assert_eq!(shannon_entropy("".to_string()), 0.0);
+    // 6 distinct symbols, each once → exactly log2(6) ≈ 2.585
+    assert!((shannon_entropy("abcdef".to_string()) - 6f64.log2()).abs() < 1e-9);
+}
diff --git a/typed/dga.ss b/typed/dga.ss
index c399170..44fc2da 100644
--- a/typed/dga.ss
+++ b/typed/dga.ss
@@ -4,7 +4,7 @@
 ;;; to Rust by the jerboa typed→rust backend. Domains are ASCII, so we score
 ;;; over UTF-8 bytes (string->utf8 + bytevector-u8-ref) rather than chars.
 (typed-library (jsecmon dga)
-  (export max-consonant-run)
+  (export max-consonant-run shannon-entropy)
 
   ;; Running state for the consonant-run fold: the current run length and the
   ;; longest run seen so far. A record accumulator lets a single-accumulator
@@ -38,4 +38,27 @@
           (if (ascii-consonant? (bytevector-u8-ref bs i))
               (let ((r (+ (CrState-run st) 1)))
                 (make-CrState r (if (> r (CrState-best st)) r (CrState-best st))))
-              (make-CrState 0 (CrState-best st))))))))
+              (make-CrState 0 (CrState-best st)))))))
+
+  ;; How many of the first n bytes of bs equal the byte value v. The inner
+  ;; loop of the entropy histogram, written as its own for/fold so the outer
+  ;; loop stays a single-accumulator fold.
+  (def (count-byte (bs : Bytes) (v : Nat) (n : Nat)) : Nat
+    (for/fold ((c 0)) ((i (in-range n)))
+      (if (= (bytevector-u8-ref bs i) v)
+          (+ c 1)
+          c)))
+
+  ;; Shannon entropy in bits/char: -Σ p·log2(p) over the byte-value histogram.
+  ;; Rather than a mutable 256-entry counts array, we sweep each possible byte
+  ;; value and count its occurrences (labels are short, so 256·n is trivial).
+  ;; The empty string yields 0.0 for free: every count is 0, so no term fires.
+  (def (shannon-entropy (s : String)) : Float
+    (let ((bs (string->utf8 s)))
+      (let ((n (bytevector-length bs)))
+        (for/fold ((ent 0.0)) ((v (in-range 256)))
+          (let ((c (count-byte bs v n)))
+            (if (> c 0)
+                (let ((p (/ (exact->inexact c) (exact->inexact n))))
+                  (- ent (* p (log2 p))))
+                ent)))))))