Regex: accept Java-compatible char-class dash and (X+)* quantifier

irregex rejected two patterns the JVM accepts, which blocked library loads:

- [\w-_] errored with bad char-set because a - after a shorthand class was
  read as a range start. Java reads it as a literal hyphen. Preprocess the
  pattern to escape such a dash.
- (X+)* errored with duplicate repetition because sre-repeater? recurses
  through submatch, treating a quantified group like a dangling a**. Override
  it to a bare leading * / + check, matching the JVM (which only rejects the
  dangling case).

Both in regex.ss (runtime). Unblocks cuerdas (was load-fail, now 292 passing)
and aws-api config-test. Also documents the host/chez/java source-layering rule
in host-interop.md.

jolt-l8so
This commit is contained in:
Yogthos 2026-06-26 17:35:08 -04:00
parent 687dc60af6
commit 8a877662dc
3 changed files with 74 additions and 1 deletions

View file

@ -33,6 +33,14 @@
(apply %chez-error args)))
(load "vendor/irregex/irregex.scm")
;; irregex rejects a quantifier applied to anything that already contains one —
;; including a GROUP like (a+)* — because sre-repeater? recurses through submatch.
;; Java only rejects a DANGLING double quantifier (a**); it allows a quantifier on
;; a group whose body is quantified. Restrict the check to a bare leading * / + so
;; a** still errors but (a+)* parses (cuerdas's format tokenizer needs this).
(set! sre-repeater?
(lambda (sre) (and (pair? sre) (memq (car sre) '(* +)) #t)))
;; Unicode property classes \p{...}: irregex's string syntax has no
;; \p{...}, so translate a fixed set of property names
;; to ASCII char classes before compiling. ASCII-only — \p{L} would need
@ -92,6 +100,36 @@
(write-char c out) (loop (fx+ i 1) #f))
(else (write-char c out) (loop (fx+ i 1) in-class))))))))
;; Inside a [...] class, irregex reads a '-' that follows a shorthand class
;; (\w \d \s \W \D \S) as the start of a range and errors ("bad char-set"); Java
;; reads it as a literal hyphen (a shorthand can't be a range endpoint). Escape
;; such a '-' to \- so the class parses. Only a '-' right after a shorthand and
;; not the class terminator is touched; a '-' after a plain char (a real range
;; like [a-z]) is left alone.
(define (escape-class-shorthand-dash src)
(let ((len (string-length src)) (out (open-output-string)))
(let loop ((i 0) (in-class #f) (after-shorthand #f))
(if (fx>=? i len)
(get-output-string out)
(let ((c (string-ref src i)))
(cond
;; an escape pair: \w-style shorthand sets after-shorthand inside a class
((and (char=? c #\\) (fx<? (fx+ i 1) len))
(let ((n (string-ref src (fx+ i 1))))
(write-char c out) (write-char n out)
(loop (fx+ i 2) in-class
(and in-class (memv n '(#\w #\d #\s #\W #\D #\S)) #t))))
((and (not in-class) (char=? c #\[))
(write-char c out) (loop (fx+ i 1) #t #f))
((and in-class (char=? c #\]))
(write-char c out) (loop (fx+ i 1) #f #f))
;; the case Java reads as a literal hyphen
((and in-class after-shorthand (char=? c #\-)
(fx<? (fx+ i 1) len) (not (char=? (string-ref src (fx+ i 1)) #\])))
(write-char #\\ out) (write-char #\- out)
(loop (fx+ i 1) in-class #f))
(else (write-char c out) (loop (fx+ i 1) in-class #f))))))))
;; Java/Clojure inline flags: a leading (?imsx…) group sets a flag over the whole
;; pattern. irregex has the same semantics but as constructor OPTIONS, not inline
;; syntax (it rejects (?s)/(?s:…)), so peel any leading flag groups off the source
@ -123,7 +161,8 @@
(define-record-type regex-t (fields source irx) (nongenerative jolt-regex-v1))
(define (jolt-regex source)
(let-values (((opts pat) (regex-parse-flags source)))
(make-regex-t source (apply irregex (translate-prop-classes pat) opts))))
(make-regex-t source
(apply irregex (translate-prop-classes (escape-class-shorthand-dash pat)) opts))))
(define (jolt-regex? x) (regex-t? x))
(define (jolt-re-pattern x) (if (regex-t? x) x (jolt-regex x)))