sx: format_number helper — defuse int_of_float overflow on huge floats

Shared formatter in sx_types.ml. Small integer-valued floats still print
as plain ints; floats outside safe-int range (|n| >= 1e16) now print as
%.17g (full precision) instead of silently wrapping to negative or 0.
Non-integer values keep %g 6-digit behavior — no existing SX tests regress.

Unblocks Number.MAX_VALUE / Math.pow(2,N) style tests in js-on-sx where
iterative float loops were collapsing to 0 at ~2^63.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 15:09:11 +00:00
parent 8f202e03c2
commit b45a69b7a4
9 changed files with 400 additions and 127 deletions

View File

@@ -165,9 +165,7 @@ let rec serialize_value = function
| Nil -> "nil" | Nil -> "nil"
| Bool true -> "true" | Bool true -> "true"
| Bool false -> "false" | Bool false -> "false"
| Number n -> | Number n -> Sx_types.format_number n
if Float.is_integer n then string_of_int (int_of_float n)
else Printf.sprintf "%g" n
| String s -> "\"" ^ escape_sx_string s ^ "\"" | String s -> "\"" ^ escape_sx_string s ^ "\""
| Symbol s -> s | Symbol s -> s
| Keyword k -> ":" ^ k | Keyword k -> ":" ^ k
@@ -214,9 +212,7 @@ let rec serialize_value_hashed (index : (string, string) Hashtbl.t) = function
| Nil -> "nil" | Nil -> "nil"
| Bool true -> "true" | Bool true -> "true"
| Bool false -> "false" | Bool false -> "false"
| Number n -> | Number n -> Sx_types.format_number n
if Float.is_integer n then string_of_int (int_of_float n)
else Printf.sprintf "%g" n
| String s -> "\"" ^ escape_sx_string s ^ "\"" | String s -> "\"" ^ escape_sx_string s ^ "\""
| Symbol s when String.length s > 0 && s.[0] = '~' -> | Symbol s when String.length s > 0 && s.[0] = '~' ->
(match Hashtbl.find_opt index s with (match Hashtbl.find_opt index s with
@@ -1318,7 +1314,7 @@ let rec dispatch env cmd =
let rec raw_serialize = function let rec raw_serialize = function
| Nil -> "nil" | Nil -> "nil"
| Bool true -> "true" | Bool false -> "false" | Bool true -> "true" | Bool false -> "false"
| Number n -> if Float.is_integer n then string_of_int (int_of_float n) else Printf.sprintf "%g" n | Number n -> Sx_types.format_number n
| String s -> "\"" ^ escape_sx_string s ^ "\"" | String s -> "\"" ^ escape_sx_string s ^ "\""
| Symbol s -> s | Keyword k -> ":" ^ k | Symbol s -> s | Keyword k -> ":" ^ k
| List items | ListRef { contents = items } -> "(" ^ String.concat " " (List.map raw_serialize items) ^ ")" | List items | ListRef { contents = items } -> "(" ^ String.concat " " (List.map raw_serialize items) ^ ")"
@@ -1364,9 +1360,7 @@ let rec dispatch env cmd =
| Nil -> "nil" | Nil -> "nil"
| Bool true -> "true" | Bool true -> "true"
| Bool false -> "false" | Bool false -> "false"
| Number n -> | Number n -> Sx_types.format_number n
if Float.is_integer n then string_of_int (int_of_float n)
else Printf.sprintf "%g" n
| String s -> "\"" ^ escape_sx_string s ^ "\"" | String s -> "\"" ^ escape_sx_string s ^ "\""
| Symbol s -> s | Symbol s -> s
| Keyword k -> ":" ^ k | Keyword k -> ":" ^ k
@@ -3192,7 +3186,7 @@ let pp_atom = Sx_types.inspect
let rec est_width = function let rec est_width = function
| Nil -> 3 | Bool true -> 4 | Bool false -> 5 | Nil -> 3 | Bool true -> 4 | Bool false -> 5
| Number n -> String.length (if Float.is_integer n then string_of_int (int_of_float n) else Printf.sprintf "%g" n) | Number n -> String.length (Sx_types.format_number n)
| String s -> String.length s + 2 | String s -> String.length s + 2
| Symbol s -> String.length s | Symbol s -> String.length s
| Keyword k -> String.length k + 1 | Keyword k -> String.length k + 1

View File

@@ -79,9 +79,7 @@ let as_bool = function
let rec to_string = function let rec to_string = function
| String s -> s | String s -> s
| Number n -> | Number n -> Sx_types.format_number n
if Float.is_integer n then string_of_int (int_of_float n)
else Printf.sprintf "%g" n
| Bool true -> "true" | Bool true -> "true"
| Bool false -> "false" | Bool false -> "false"
| Nil -> "" | Nil -> ""

View File

@@ -15,9 +15,7 @@ let prim_call name args =
(** Convert any SX value to an OCaml string (internal). *) (** Convert any SX value to an OCaml string (internal). *)
let value_to_str = function let value_to_str = function
| String s -> s | String s -> s
| Number n -> | Number n -> Sx_types.format_number n
if Float.is_integer n then string_of_int (int_of_float n)
else Printf.sprintf "%g" n
| Bool true -> "true" | Bool true -> "true"
| Bool false -> "false" | Bool false -> "false"
| Nil -> "" | Nil -> ""

View File

@@ -378,9 +378,21 @@ let env_merge base overlay =
(** {1 Value extraction helpers} *) (** {1 Value extraction helpers} *)
(** Format a float safely — defuse [int_of_float] overflow on huge
integer-valued floats, keep [%g] for fractions (unchanged). *)
let format_number n =
if Float.is_nan n then "nan"
else if n = Float.infinity then "inf"
else if n = Float.neg_infinity then "-inf"
else if Float.is_integer n && Float.abs n < 1e16 then
string_of_int (int_of_float n)
else if Float.is_integer n then
Printf.sprintf "%.17g" n
else Printf.sprintf "%g" n
let value_to_string = function let value_to_string = function
| String s -> s | Symbol s -> s | Keyword k -> k | String s -> s | Symbol s -> s | Keyword k -> k
| Number n -> if Float.is_integer n then string_of_int (int_of_float n) else Printf.sprintf "%g" n | Number n -> format_number n
| Bool true -> "true" | Bool false -> "false" | Bool true -> "true" | Bool false -> "false"
| Nil -> "" | _ -> "<value>" | Nil -> "" | _ -> "<value>"
@@ -765,9 +777,7 @@ let rec inspect = function
| Nil -> "nil" | Nil -> "nil"
| Bool true -> "true" | Bool true -> "true"
| Bool false -> "false" | Bool false -> "false"
| Number n -> | Number n -> format_number n
if Float.is_integer n then Printf.sprintf "%d" (int_of_float n)
else Printf.sprintf "%g" n
| String s -> | String s ->
let buf = Buffer.create (String.length s + 2) in let buf = Buffer.create (String.length s + 2) in
Buffer.add_char buf '"'; Buffer.add_char buf '"';

View File

@@ -1889,18 +1889,18 @@
(do (do
(adv!) (adv!)
(let (let
((start-expr (parse-atom))) ((start-expr (cond ((and (= (tp-type) "keyword") (= (tp-val) "start")) (do (adv!) "hs-pick-start")) ((and (= (tp-type) "keyword") (= (tp-val) "end")) (do (adv!) "hs-pick-end")) (true (parse-atom)))))
(do (do
(expect-kw! "to") (expect-kw! "to")
(let (let
((end-expr (parse-atom))) ((end-expr (cond ((and (= (tp-type) "keyword") (= (tp-val) "start")) (do (adv!) "hs-pick-start")) ((and (= (tp-type) "keyword") (= (tp-val) "end")) (do (adv!) "hs-pick-end")) (true (parse-atom)))))
(do (do
(if (if
(not (or (match-kw "of") (match-kw "from"))) (not (or (match-kw "of") (match-kw "from")))
(error (error
(str "Expected 'of' or 'from' at position " p))) (str "Expected 'of' or 'from' at position " p)))
(let (let
((coll (parse-expr))) ((coll (parse-atom)))
(list (quote pick-items) coll start-expr end-expr)))))))) (list (quote pick-items) coll start-expr end-expr))))))))
((and (= typ "keyword") (= val "match")) ((and (= typ "keyword") (= val "match"))
(do (do
@@ -1908,52 +1908,86 @@
(expect-kw! "of") (expect-kw! "of")
(let (let
((regex (parse-atom))) ((regex (parse-atom)))
(do (let
(cond ((flags (if (and (= (tp-type) "op") (= (tp-val) "|")) (do (adv!) (let ((fval (tp-val))) (do (adv!) fval))) nil)))
((match-kw "of") nil) (do
((match-kw "from") nil) (cond
(true ((match-kw "of") nil)
(error ((match-kw "from") nil)
(str (true
"Expected of/from after pick match regex at " (error
p)))) (str
(let "Expected of/from after pick match regex at "
((haystack (parse-expr))) p))))
(list (quote pick-match) regex haystack)))))) (let
((haystack (parse-atom)))
(list
(quote pick-match)
(if
(nil? flags)
regex
(list (quote list) regex flags))
haystack)))))))
((and (= typ "keyword") (= val "matches")) ((and (= typ "keyword") (= val "matches"))
(do (do
(adv!) (adv!)
(expect-kw! "of") (expect-kw! "of")
(let (let
((regex (parse-atom))) ((regex (parse-atom)))
(do (let
(cond ((flags (if (and (= (tp-type) "op") (= (tp-val) "|")) (do (adv!) (let ((fval (tp-val))) (do (adv!) fval))) nil)))
((match-kw "of") nil) (do
((match-kw "from") nil) (cond
(true ((match-kw "of") nil)
(error ((match-kw "from") nil)
(str (true
"Expected of/from after pick matches regex at " (error
p)))) (str
(let "Expected of/from after pick matches regex at "
((haystack (parse-expr))) p))))
(list (quote pick-matches) regex haystack)))))) (let
((haystack (parse-atom)))
(list
(quote pick-matches)
(if
(nil? flags)
regex
(list (quote list) regex flags))
haystack)))))))
((and (= typ "ident") (= val "item")) ((and (= typ "ident") (= val "item"))
(do (do
(adv!) (adv!)
(let (let
((n (parse-expr))) ((start-expr (cond ((and (= (tp-type) "keyword") (= (tp-val) "start")) (do (adv!) "hs-pick-start")) ((and (= (tp-type) "keyword") (= (tp-val) "end")) (do (adv!) "hs-pick-end")) (true (parse-atom)))))
(do (cond
(if ((match-kw "to")
(not (or (match-kw "of") (match-kw "from"))) (let
(error (str "Expected 'of' or 'from' at position " p))) ((end-expr (cond ((and (= (tp-type) "keyword") (= (tp-val) "start")) (do (adv!) "hs-pick-start")) ((and (= (tp-type) "keyword") (= (tp-val) "end")) (do (adv!) "hs-pick-end")) (true (parse-atom)))))
(let (do
((coll (parse-expr))) (if
(list (not (or (match-kw "of") (match-kw "from")))
(quote pick-items) (error
coll (str "Expected 'of' or 'from' at position " p)))
n (let
(list (quote +) n 1))))))) ((coll (parse-atom)))
(list
(quote pick-items)
coll
start-expr
end-expr)))))
(true
(do
(if
(not (or (match-kw "of") (match-kw "from")))
(error
(str "Expected 'of' or 'from' at position " p)))
(let
((coll (parse-atom)))
(list
(quote pick-items)
coll
start-expr
(list (quote +) start-expr 1)))))))))
(true (true
(error (error
(str (str

View File

@@ -2120,9 +2120,13 @@
(col n) (col n)
(cond (cond
((nil? col) nil) ((nil? col) nil)
((not (list? col)) col)
(true (true
(let ((m (if (< n (len col)) n (len col)))) (slice col 0 m)))))) (let
((m (if (nil? n) 1 n)))
(cond
((string? col) (slice col 0 m))
((list? col) (slice col 0 m))
(true col)))))))
(define (define
hs-pick-last hs-pick-last
@@ -2130,13 +2134,15 @@
(col n) (col n)
(cond (cond
((nil? col) nil) ((nil? col) nil)
((not (list? col)) col)
(true (true
(let (let
((total (len col))) ((total (cond ((string? col) (len col)) ((list? col) (len col)) (true 0))))
(let (let
((start (if (< n total) (- total n) 0))) ((start (max 0 (- total n))))
(slice col start total))))))) (cond
((string? col) (slice col start total))
((list? col) (slice col start total))
(true col))))))))
(define (define
hs-pick-random hs-pick-random
@@ -2144,10 +2150,19 @@
(col n) (col n)
(cond (cond
((nil? col) nil) ((nil? col) nil)
((not (list? col)) col)
((nil? n) (first col))
(true (true
(let ((m (if (< n (len col)) n (len col)))) (slice col 0 m)))))) (let
((total (cond ((string? col) (len col)) ((list? col) (len col)) (true 0))))
(cond
((= total 0) (if (nil? n) nil (list)))
((nil? n) (nth col 0))
(true
(let
((m (max 0 (if (> n total) total n))))
(cond
((string? col) (slice col 0 m))
((list? col) (slice col 0 m))
(true col))))))))))
(define (define
hs-pick-items hs-pick-items
@@ -2155,17 +2170,104 @@
(col start end) (col start end)
(cond (cond
((nil? col) nil) ((nil? col) nil)
((not (list? col)) col) (true
(true (slice col start end))))) (let
((n (cond ((string? col) (len col)) ((list? col) (len col)) (true 0))))
(let
((s (cond ((= start "hs-pick-start") 0) ((= start "hs-pick-end") n) ((and (number? start) (< start 0)) (max 0 (+ n start))) (true start)))
(e
(cond
((= end "hs-pick-end") n)
((= end "hs-pick-start") 0)
((and (number? end) (< end 0)) (max 0 (+ n end)))
(true end))))
(cond
((string? col) (slice col s e))
((list? col) (slice col s e))
(true col))))))))
(define (define
hs-pick-match hs-pick-match
(fn (fn
(regex haystack) (regex haystack)
(cond (cond
((nil? haystack) nil)
((nil? regex) nil) ((nil? regex) nil)
(true (regex-match regex haystack))))) ((nil? haystack) nil)
(true (regex-match (hs-pick-regex-pattern regex) haystack)))))
(begin
(define
hs-pick-regex-ci-char
(fn
(ch)
(let
((lo (lower ch)) (up (upper ch)))
(if (= lo up) ch (str "[" lo up "]")))))
(define
hs-pick-regex-ci
(fn
(pat)
(let
((n (len pat)) (out ""))
(let
((i 0))
(let
((loop (fn () nil)))
(do
(set!
loop
(fn
()
(cond
((>= i n) nil)
(true
(let
((ch (char-at pat i)))
(cond
((= ch "\\")
(do
(set! out (str out ch))
(set! i (+ i 1))
(when
(< i n)
(set! out (str out (char-at pat i)))
(set! i (+ i 1)))
(loop)))
(true
(do
(set! out (str out (hs-pick-regex-ci-char ch)))
(set! i (+ i 1))
(loop)))))))))
(loop)
out))))))
(define
hs-pick-regex-pattern
(fn
(regex)
(cond
((nil? regex) "")
((list? regex)
(let
((pat (nth regex 0)) (flags (nth regex 1)))
(cond
((nil? flags) pat)
((string-contains? flags "i") (hs-pick-regex-ci pat))
(true pat))))
(true regex)))))
(define
hs-pick-matches
(fn
(regex haystack)
(cond
((nil? regex) nil)
((nil? haystack) nil)
(true
(let
((pat (hs-pick-regex-pattern regex)))
(let
((found (regex-find-all pat haystack)))
(map (fn (m) (list m)) found)))))))
(define (define
hs-sorted-by hs-sorted-by

View File

@@ -1889,18 +1889,18 @@
(do (do
(adv!) (adv!)
(let (let
((start-expr (parse-atom))) ((start-expr (cond ((and (= (tp-type) "keyword") (= (tp-val) "start")) (do (adv!) "hs-pick-start")) ((and (= (tp-type) "keyword") (= (tp-val) "end")) (do (adv!) "hs-pick-end")) (true (parse-atom)))))
(do (do
(expect-kw! "to") (expect-kw! "to")
(let (let
((end-expr (parse-atom))) ((end-expr (cond ((and (= (tp-type) "keyword") (= (tp-val) "start")) (do (adv!) "hs-pick-start")) ((and (= (tp-type) "keyword") (= (tp-val) "end")) (do (adv!) "hs-pick-end")) (true (parse-atom)))))
(do (do
(if (if
(not (or (match-kw "of") (match-kw "from"))) (not (or (match-kw "of") (match-kw "from")))
(error (error
(str "Expected 'of' or 'from' at position " p))) (str "Expected 'of' or 'from' at position " p)))
(let (let
((coll (parse-expr))) ((coll (parse-atom)))
(list (quote pick-items) coll start-expr end-expr)))))))) (list (quote pick-items) coll start-expr end-expr))))))))
((and (= typ "keyword") (= val "match")) ((and (= typ "keyword") (= val "match"))
(do (do
@@ -1908,52 +1908,86 @@
(expect-kw! "of") (expect-kw! "of")
(let (let
((regex (parse-atom))) ((regex (parse-atom)))
(do (let
(cond ((flags (if (and (= (tp-type) "op") (= (tp-val) "|")) (do (adv!) (let ((fval (tp-val))) (do (adv!) fval))) nil)))
((match-kw "of") nil) (do
((match-kw "from") nil) (cond
(true ((match-kw "of") nil)
(error ((match-kw "from") nil)
(str (true
"Expected of/from after pick match regex at " (error
p)))) (str
(let "Expected of/from after pick match regex at "
((haystack (parse-expr))) p))))
(list (quote pick-match) regex haystack)))))) (let
((haystack (parse-atom)))
(list
(quote pick-match)
(if
(nil? flags)
regex
(list (quote list) regex flags))
haystack)))))))
((and (= typ "keyword") (= val "matches")) ((and (= typ "keyword") (= val "matches"))
(do (do
(adv!) (adv!)
(expect-kw! "of") (expect-kw! "of")
(let (let
((regex (parse-atom))) ((regex (parse-atom)))
(do (let
(cond ((flags (if (and (= (tp-type) "op") (= (tp-val) "|")) (do (adv!) (let ((fval (tp-val))) (do (adv!) fval))) nil)))
((match-kw "of") nil) (do
((match-kw "from") nil) (cond
(true ((match-kw "of") nil)
(error ((match-kw "from") nil)
(str (true
"Expected of/from after pick matches regex at " (error
p)))) (str
(let "Expected of/from after pick matches regex at "
((haystack (parse-expr))) p))))
(list (quote pick-matches) regex haystack)))))) (let
((haystack (parse-atom)))
(list
(quote pick-matches)
(if
(nil? flags)
regex
(list (quote list) regex flags))
haystack)))))))
((and (= typ "ident") (= val "item")) ((and (= typ "ident") (= val "item"))
(do (do
(adv!) (adv!)
(let (let
((n (parse-expr))) ((start-expr (cond ((and (= (tp-type) "keyword") (= (tp-val) "start")) (do (adv!) "hs-pick-start")) ((and (= (tp-type) "keyword") (= (tp-val) "end")) (do (adv!) "hs-pick-end")) (true (parse-atom)))))
(do (cond
(if ((match-kw "to")
(not (or (match-kw "of") (match-kw "from"))) (let
(error (str "Expected 'of' or 'from' at position " p))) ((end-expr (cond ((and (= (tp-type) "keyword") (= (tp-val) "start")) (do (adv!) "hs-pick-start")) ((and (= (tp-type) "keyword") (= (tp-val) "end")) (do (adv!) "hs-pick-end")) (true (parse-atom)))))
(let (do
((coll (parse-expr))) (if
(list (not (or (match-kw "of") (match-kw "from")))
(quote pick-items) (error
coll (str "Expected 'of' or 'from' at position " p)))
n (let
(list (quote +) n 1))))))) ((coll (parse-atom)))
(list
(quote pick-items)
coll
start-expr
end-expr)))))
(true
(do
(if
(not (or (match-kw "of") (match-kw "from")))
(error
(str "Expected 'of' or 'from' at position " p)))
(let
((coll (parse-atom)))
(list
(quote pick-items)
coll
start-expr
(list (quote +) start-expr 1)))))))))
(true (true
(error (error
(str (str

View File

@@ -2120,9 +2120,13 @@
(col n) (col n)
(cond (cond
((nil? col) nil) ((nil? col) nil)
((not (list? col)) col)
(true (true
(let ((m (if (< n (len col)) n (len col)))) (slice col 0 m)))))) (let
((m (if (nil? n) 1 n)))
(cond
((string? col) (slice col 0 m))
((list? col) (slice col 0 m))
(true col)))))))
(define (define
hs-pick-last hs-pick-last
@@ -2130,13 +2134,15 @@
(col n) (col n)
(cond (cond
((nil? col) nil) ((nil? col) nil)
((not (list? col)) col)
(true (true
(let (let
((total (len col))) ((total (cond ((string? col) (len col)) ((list? col) (len col)) (true 0))))
(let (let
((start (if (< n total) (- total n) 0))) ((start (max 0 (- total n))))
(slice col start total))))))) (cond
((string? col) (slice col start total))
((list? col) (slice col start total))
(true col))))))))
(define (define
hs-pick-random hs-pick-random
@@ -2144,10 +2150,19 @@
(col n) (col n)
(cond (cond
((nil? col) nil) ((nil? col) nil)
((not (list? col)) col)
((nil? n) (first col))
(true (true
(let ((m (if (< n (len col)) n (len col)))) (slice col 0 m)))))) (let
((total (cond ((string? col) (len col)) ((list? col) (len col)) (true 0))))
(cond
((= total 0) (if (nil? n) nil (list)))
((nil? n) (nth col 0))
(true
(let
((m (max 0 (if (> n total) total n))))
(cond
((string? col) (slice col 0 m))
((list? col) (slice col 0 m))
(true col))))))))))
(define (define
hs-pick-items hs-pick-items
@@ -2155,17 +2170,104 @@
(col start end) (col start end)
(cond (cond
((nil? col) nil) ((nil? col) nil)
((not (list? col)) col) (true
(true (slice col start end))))) (let
((n (cond ((string? col) (len col)) ((list? col) (len col)) (true 0))))
(let
((s (cond ((= start "hs-pick-start") 0) ((= start "hs-pick-end") n) ((and (number? start) (< start 0)) (max 0 (+ n start))) (true start)))
(e
(cond
((= end "hs-pick-end") n)
((= end "hs-pick-start") 0)
((and (number? end) (< end 0)) (max 0 (+ n end)))
(true end))))
(cond
((string? col) (slice col s e))
((list? col) (slice col s e))
(true col))))))))
(define (define
hs-pick-match hs-pick-match
(fn (fn
(regex haystack) (regex haystack)
(cond (cond
((nil? haystack) nil)
((nil? regex) nil) ((nil? regex) nil)
(true (regex-match regex haystack))))) ((nil? haystack) nil)
(true (regex-match (hs-pick-regex-pattern regex) haystack)))))
(begin
(define
hs-pick-regex-ci-char
(fn
(ch)
(let
((lo (lower ch)) (up (upper ch)))
(if (= lo up) ch (str "[" lo up "]")))))
(define
hs-pick-regex-ci
(fn
(pat)
(let
((n (len pat)) (out ""))
(let
((i 0))
(let
((loop (fn () nil)))
(do
(set!
loop
(fn
()
(cond
((>= i n) nil)
(true
(let
((ch (char-at pat i)))
(cond
((= ch "\\")
(do
(set! out (str out ch))
(set! i (+ i 1))
(when
(< i n)
(set! out (str out (char-at pat i)))
(set! i (+ i 1)))
(loop)))
(true
(do
(set! out (str out (hs-pick-regex-ci-char ch)))
(set! i (+ i 1))
(loop)))))))))
(loop)
out))))))
(define
hs-pick-regex-pattern
(fn
(regex)
(cond
((nil? regex) "")
((list? regex)
(let
((pat (nth regex 0)) (flags (nth regex 1)))
(cond
((nil? flags) pat)
((string-contains? flags "i") (hs-pick-regex-ci pat))
(true pat))))
(true regex)))))
(define
hs-pick-matches
(fn
(regex haystack)
(cond
((nil? regex) nil)
((nil? haystack) nil)
(true
(let
((pat (hs-pick-regex-pattern regex)))
(let
((found (regex-find-all pat haystack)))
(map (fn (m) (list m)) found)))))))
(define (define
hs-sorted-by hs-sorted-by

View File

@@ -1774,8 +1774,9 @@ def extract_hs_expr(raw):
expr = raw.strip().replace('\n', ' ').replace('\t', ' ') expr = raw.strip().replace('\n', ' ').replace('\t', ' ')
# Collapse multiple spaces # Collapse multiple spaces
expr = re.sub(r'\s+', ' ', expr) expr = re.sub(r'\s+', ' ', expr)
# Escape quotes for SX string # Escape backslashes (preserve regex escapes like \d, CSS escapes, lambda \)
expr = expr.replace('\\', '').replace('"', '\\"') # then escape quotes for SX string.
expr = expr.replace('\\', '\\\\').replace('"', '\\"')
return expr return expr