From 740b50aef3b475b8278882b11fc21469b47d70f2 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 14:07:14 -0400 Subject: [PATCH] feat(strictness): subs validates bounds; assoc! bounds-checks vector index - subs requires a string and validates 0<=start<=end<=count (no Janet from-end/clamping); negative/out-of-range/nil indices throw - assoc! on a transient vector bounds-checks the index (0..count) subs 11-fail -> 24/5 (5 remaining are byte-vs-codepoint Unicode, platform); assoc_bang 32/6 -> 35/3. clojure-test-suite pass 3889->3898. spec: string/subs-strictness (7), transient/assoc!-bounds (4). jpm test green. --- src/jolt/core.janet | 23 +++++++++++++++---- .../integration/clojure-test-suite-test.janet | 2 +- test/spec/strings-spec.janet | 10 ++++++++ test/spec/transients-spec.janet | 6 +++++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 19082ba..828f321 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -1614,10 +1614,20 @@ (def core-subs (fn [& args] - (case (length args) - 2 (string/slice (args 0) (args 1)) - 3 (string/slice (args 0) (args 1) (args 2)) - (error "Wrong number of args passed to: subs")))) + (when (not (or (= 2 (length args)) (= 3 (length args)))) + (error "Wrong number of args passed to: subs")) + (let [s (args 0) + start (get args 1)] + (when (not (string? s)) (error (string "subs requires a string, got " (type s)))) + (let [len (length s) + end (if (= 3 (length args)) (args 2) len)] + # Clojure validates bounds (no negative/from-end/clamping like Janet): + # 0 <= start <= end <= (count s). + (when (not (and (number? start) (number? end) + (= start (math/floor start)) (= end (math/floor end)) + (>= start 0) (<= start end) (<= end len))) + (error "String index out of range")) + (string/slice s start end))))) # ============================================================ # I/O — minimal wrappers @@ -3346,7 +3356,10 @@ (defn- tr-assoc! [t k v] (tr-check-active! t) (case (t :kind) - :vector (let [a (t :arr)] (if (= k (length a)) (array/push a v) (put a k v))) + :vector (let [a (t :arr)] + (when (not (and (number? k) (= k (math/floor k)) (>= k 0) (<= k (length a)))) + (error (string "Index " k " out of bounds for assoc! on a transient vector of length " (length a)))) + (if (= k (length a)) (array/push a v) (put a k v))) :map (put (t :tbl) (canon-key k) @[k v]) (error "assoc! expects a transient vector or map")) t) diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet index 5114cbd..f28e9a0 100644 --- a/test/integration/clojure-test-suite-test.janet +++ b/test/integration/clojure-test-suite-test.janet @@ -18,7 +18,7 @@ # Baseline: assertions Jolt currently passes across the suite. Raise as Jolt # improves so a regression (previously-passing assertion breaking) is caught. -(def baseline-pass 3885) +(def baseline-pass 3895) # A file is "clean" when it ran with zero failures AND zero errors. (def baseline-clean-files 45) # Per-file wall-clock budget (seconds). Normal files finish in well under 1s; diff --git a/test/spec/strings-spec.janet b/test/spec/strings-spec.janet index a1cf844..e3739e1 100644 --- a/test/spec/strings-spec.janet +++ b/test/spec/strings-spec.janet @@ -33,3 +33,13 @@ ["replace" "\"hexxo\"" "(do (require (quote [clojure.string :as s])) (s/replace \"hello\" \"l\" \"x\"))"] ["reverse" "\"cba\"" "(do (require (quote [clojure.string :as s])) (s/reverse \"abc\"))"] ["index-of" "2" "(do (require (quote [clojure.string :as s])) (s/index-of \"hello\" \"l\"))"]) + +# subs validates bounds like Clojure (no Janet from-end/clamping). +(defspec "string / subs strictness" + ["subs basic" "\"bcd\"" "(subs \"abcde\" 1 4)"] + ["subs to end" "\"cde\"" "(subs \"abcde\" 2)"] + ["subs start>end" :throws "(subs \"abcde\" 2 1)"] + ["subs negative" :throws "(subs \"abcde\" -1)"] + ["subs end past len" :throws "(subs \"abcde\" 1 6)"] + ["subs nil start" :throws "(subs \"abcde\" nil 2)"] + ["subs on nil" :throws "(subs nil 1 2)"]) diff --git a/test/spec/transients-spec.janet b/test/spec/transients-spec.janet index 2e9bd4f..be67284 100644 --- a/test/spec/transients-spec.janet +++ b/test/spec/transients-spec.janet @@ -90,3 +90,9 @@ ["conj! no args" "[]" "(persistent! (conj!))"] ["conj! identity" "[1 2]" "(conj! [1 2])"] ["conj! map merges map" "{:a 1, :b 2}" "(persistent! (conj! (transient {:a 1}) {:b 2}))"]) + +(defspec "transient / assoc! bounds" + ["assoc! existing idx" "[1 9 3]" "(persistent! (assoc! (transient [1 2 3]) 1 9))"] + ["assoc! at count grows" "[1 2 3]" "(persistent! (assoc! (transient [1 2]) 2 3))"] + ["assoc! out of bounds" :throws "(assoc! (transient [0 1 2]) 4 4)"] + ["assoc! negative" :throws "(assoc! (transient []) -1 0)"])