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.
This commit is contained in:
Yogthos 2026-06-05 14:07:14 -04:00
parent 6544d8ef44
commit 740b50aef3
4 changed files with 35 additions and 6 deletions

View file

@ -1614,10 +1614,20 @@
(def core-subs (def core-subs
(fn [& args] (fn [& args]
(case (length args) (when (not (or (= 2 (length args)) (= 3 (length args))))
2 (string/slice (args 0) (args 1)) (error "Wrong number of args passed to: subs"))
3 (string/slice (args 0) (args 1) (args 2)) (let [s (args 0)
(error "Wrong number of args passed to: subs")))) 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 # I/O — minimal wrappers
@ -3346,7 +3356,10 @@
(defn- tr-assoc! [t k v] (defn- tr-assoc! [t k v]
(tr-check-active! t) (tr-check-active! t)
(case (t :kind) (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]) :map (put (t :tbl) (canon-key k) @[k v])
(error "assoc! expects a transient vector or map")) (error "assoc! expects a transient vector or map"))
t) t)

View file

@ -18,7 +18,7 @@
# Baseline: assertions Jolt currently passes across the suite. Raise as Jolt # Baseline: assertions Jolt currently passes across the suite. Raise as Jolt
# improves so a regression (previously-passing assertion breaking) is caught. # 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. # A file is "clean" when it ran with zero failures AND zero errors.
(def baseline-clean-files 45) (def baseline-clean-files 45)
# Per-file wall-clock budget (seconds). Normal files finish in well under 1s; # Per-file wall-clock budget (seconds). Normal files finish in well under 1s;

View file

@ -33,3 +33,13 @@
["replace" "\"hexxo\"" "(do (require (quote [clojure.string :as s])) (s/replace \"hello\" \"l\" \"x\"))"] ["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\"))"] ["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\"))"]) ["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)"])

View file

@ -90,3 +90,9 @@
["conj! no args" "[]" "(persistent! (conj!))"] ["conj! no args" "[]" "(persistent! (conj!))"]
["conj! identity" "[1 2]" "(conj! [1 2])"] ["conj! identity" "[1 2]" "(conj! [1 2])"]
["conj! map merges map" "{:a 1, :b 2}" "(persistent! (conj! (transient {:a 1}) {:b 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)"])