Skip to content

Commit e7b9349

Browse files
authored
Merge pull request #119 from CertainLach/feature/std-regex
feat: add std regex builtins
2 parents 11193ce + 8ec1af0 commit e7b9349

File tree

8 files changed

+414
-89
lines changed

8 files changed

+414
-89
lines changed

Cargo.lock

+212-87
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ clap_complete = "4.4"
6464
lsp-server = "0.7.4"
6565
lsp-types = "0.94.1"
6666

67+
regex = "1.8.4"
68+
lru = "0.10.0"
69+
6770
#[profile.test]
6871
#opt-level = 1
6972

cmds/jrsonnet/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ exp-destruct = ["jrsonnet-evaluator/exp-destruct"]
2929
exp-object-iteration = ["jrsonnet-evaluator/exp-object-iteration"]
3030
# Bigint type
3131
exp-bigint = ["jrsonnet-evaluator/exp-bigint", "jrsonnet-cli/exp-bigint"]
32+
# std.regex and co.
33+
exp-regex = ["jrsonnet-cli/exp-regex"]
3234
# obj?.field, obj?.['field']
3335
exp-null-coaelse = [
3436
"jrsonnet-evaluator/exp-null-coaelse",

crates/jrsonnet-cli/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ exp-null-coaelse = [
2020
"jrsonnet-evaluator/exp-null-coaelse",
2121
"jrsonnet-stdlib/exp-null-coaelse",
2222
]
23+
exp-regex = [
24+
"jrsonnet-stdlib/exp-regex",
25+
]
2326
legacy-this-file = ["jrsonnet-stdlib/legacy-this-file"]
2427

2528
[dependencies]

crates/jrsonnet-evaluator/src/typed/conversions.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
bail,
1111
function::{native::NativeDesc, FuncDesc, FuncVal},
1212
typed::CheckType,
13-
val::{IndexableVal, ThunkMapper},
13+
val::{IndexableVal, StrValue, ThunkMapper},
1414
ObjValue, ObjValueBuilder, Result, Thunk, Val,
1515
};
1616

@@ -304,6 +304,22 @@ impl Typed for String {
304304
}
305305
}
306306

307+
impl Typed for StrValue {
308+
const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);
309+
310+
fn into_untyped(value: Self) -> Result<Val> {
311+
Ok(Val::Str(value))
312+
}
313+
314+
fn from_untyped(value: Val) -> Result<Self> {
315+
<Self as Typed>::TYPE.check(&value)?;
316+
match value {
317+
Val::Str(s) => Ok(s),
318+
_ => unreachable!(),
319+
}
320+
}
321+
}
322+
307323
impl Typed for char {
308324
const TYPE: &'static ComplexValType = &ComplexValType::Char;
309325

crates/jrsonnet-stdlib/Cargo.toml

+7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ exp-preserve-order = ["jrsonnet-evaluator/exp-preserve-order"]
2020
exp-bigint = ["num-bigint", "jrsonnet-evaluator/exp-bigint"]
2121

2222
exp-null-coaelse = ["jrsonnet-parser/exp-null-coaelse", "jrsonnet-evaluator/exp-null-coaelse"]
23+
# std.regexMatch and other helpers
24+
exp-regex = ["regex", "lru", "rustc-hash"]
2325

2426
[dependencies]
2527
jrsonnet-evaluator.workspace = true
@@ -49,6 +51,11 @@ serde_yaml_with_quirks.workspace = true
4951

5052
num-bigint = { workspace = true, optional = true }
5153

54+
# regex
55+
regex = { workspace = true, optional = true }
56+
lru = { workspace = true, optional = true }
57+
rustc-hash = { workspace = true, optional = true }
58+
5259
[build-dependencies]
5360
jrsonnet-parser.workspace = true
5461
structdump = { workspace = true, features = ["derive"] }

crates/jrsonnet-stdlib/src/lib.rs

+37-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ mod sets;
4343
pub use sets::*;
4444
mod compat;
4545
pub use compat::*;
46+
#[cfg(feature = "exp-regex")]
47+
mod regex;
48+
#[cfg(feature = "exp-regex")]
49+
pub use crate::regex::*;
4650

4751
pub fn stdlib_uncached(settings: Rc<RefCell<Settings>>) -> ObjValue {
4852
let mut builder = ObjValueBuilder::new();
@@ -185,6 +189,9 @@ pub fn stdlib_uncached(settings: Rc<RefCell<Settings>>) -> ObjValue {
185189
("setInter", builtin_set_inter::INST),
186190
("setDiff", builtin_set_diff::INST),
187191
("setUnion", builtin_set_union::INST),
192+
// Regex
193+
#[cfg(feature = "exp-regex")]
194+
("regexQuoteMeta", builtin_regex_quote_meta::INST),
188195
// Compat
189196
("__compare", builtin___compare::INST),
190197
]
@@ -207,9 +214,38 @@ pub fn stdlib_uncached(settings: Rc<RefCell<Settings>>) -> ObjValue {
207214
},
208215
);
209216
builder.method("trace", builtin_trace { settings });
210-
211217
builder.method("id", FuncVal::Id);
212218

219+
#[cfg(feature = "exp-regex")]
220+
{
221+
// Regex
222+
let regex_cache = RegexCache::default();
223+
builder.method(
224+
"regexFullMatch",
225+
builtin_regex_full_match {
226+
cache: regex_cache.clone(),
227+
},
228+
);
229+
builder.method(
230+
"regexPartialMatch",
231+
builtin_regex_partial_match {
232+
cache: regex_cache.clone(),
233+
},
234+
);
235+
builder.method(
236+
"regexReplace",
237+
builtin_regex_replace {
238+
cache: regex_cache.clone(),
239+
},
240+
);
241+
builder.method(
242+
"regexGlobalReplace",
243+
builtin_regex_global_replace {
244+
cache: regex_cache.clone(),
245+
},
246+
);
247+
};
248+
213249
builder.build()
214250
}
215251

crates/jrsonnet-stdlib/src/regex.rs

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use std::{cell::RefCell, hash::BuildHasherDefault, num::NonZeroUsize, rc::Rc};
2+
3+
use ::regex::Regex;
4+
use jrsonnet_evaluator::{
5+
error::{ErrorKind::*, Result},
6+
val::StrValue,
7+
IStr, ObjValueBuilder, Val,
8+
};
9+
use jrsonnet_macros::builtin;
10+
use lru::LruCache;
11+
use rustc_hash::FxHasher;
12+
13+
pub struct RegexCacheInner {
14+
cache: RefCell<LruCache<IStr, Rc<Regex>, BuildHasherDefault<FxHasher>>>,
15+
}
16+
impl Default for RegexCacheInner {
17+
fn default() -> Self {
18+
Self {
19+
cache: RefCell::new(LruCache::with_hasher(
20+
NonZeroUsize::new(20).unwrap(),
21+
BuildHasherDefault::default(),
22+
)),
23+
}
24+
}
25+
}
26+
pub type RegexCache = Rc<RegexCacheInner>;
27+
impl RegexCacheInner {
28+
fn parse(&self, pattern: IStr) -> Result<Rc<Regex>> {
29+
let mut cache = self.cache.borrow_mut();
30+
if let Some(found) = cache.get(&pattern) {
31+
return Ok(found.clone());
32+
}
33+
let regex = Regex::new(&pattern)
34+
.map_err(|e| RuntimeError(format!("regex parse failed: {e}").into()))?;
35+
let regex = Rc::new(regex);
36+
cache.push(pattern, regex.clone());
37+
Ok(regex)
38+
}
39+
}
40+
41+
pub fn regex_match_inner(regex: &Regex, str: String) -> Result<Val> {
42+
let mut out = ObjValueBuilder::with_capacity(3);
43+
44+
let mut captures = Vec::with_capacity(regex.captures_len());
45+
let mut named_captures = ObjValueBuilder::with_capacity(regex.capture_names().len());
46+
47+
let Some(captured) = regex.captures(&str) else {
48+
return Ok(Val::Null);
49+
};
50+
51+
for ele in captured.iter().skip(1) {
52+
if let Some(ele) = ele {
53+
captures.push(Val::Str(StrValue::Flat(ele.as_str().into())))
54+
} else {
55+
captures.push(Val::Str(StrValue::Flat(IStr::empty())))
56+
}
57+
}
58+
for (i, name) in regex
59+
.capture_names()
60+
.skip(1)
61+
.enumerate()
62+
.flat_map(|(i, v)| Some((i, v?)))
63+
{
64+
let capture = captures[i].clone();
65+
named_captures.field(name).try_value(capture)?;
66+
}
67+
68+
out.field("string")
69+
.value(Val::Str(captured.get(0).unwrap().as_str().into()));
70+
out.field("captures").value(Val::Arr(captures.into()));
71+
out.field("namedCaptures")
72+
.value(Val::Obj(named_captures.build()));
73+
74+
Ok(Val::Obj(out.build()))
75+
}
76+
77+
#[builtin(fields(
78+
cache: RegexCache,
79+
))]
80+
pub fn builtin_regex_partial_match(
81+
this: &builtin_regex_partial_match,
82+
pattern: IStr,
83+
str: String,
84+
) -> Result<Val> {
85+
let regex = this.cache.parse(pattern)?;
86+
regex_match_inner(&regex, str)
87+
}
88+
89+
#[builtin(fields(
90+
cache: RegexCache,
91+
))]
92+
pub fn builtin_regex_full_match(
93+
this: &builtin_regex_full_match,
94+
pattern: StrValue,
95+
str: String,
96+
) -> Result<Val> {
97+
let pattern = format!("^{pattern}$").into();
98+
let regex = this.cache.parse(pattern)?;
99+
regex_match_inner(&regex, str)
100+
}
101+
102+
#[builtin]
103+
pub fn builtin_regex_quote_meta(pattern: String) -> String {
104+
regex::escape(&pattern)
105+
}
106+
107+
#[builtin(fields(
108+
cache: RegexCache,
109+
))]
110+
pub fn builtin_regex_replace(
111+
this: &builtin_regex_replace,
112+
str: String,
113+
pattern: IStr,
114+
to: String,
115+
) -> Result<String> {
116+
let regex = this.cache.parse(pattern)?;
117+
let replaced = regex.replace(&str, to);
118+
Ok(replaced.to_string())
119+
}
120+
121+
#[builtin(fields(
122+
cache: RegexCache,
123+
))]
124+
pub fn builtin_regex_global_replace(
125+
this: &builtin_regex_global_replace,
126+
str: String,
127+
pattern: IStr,
128+
to: String,
129+
) -> Result<String> {
130+
let regex = this.cache.parse(pattern)?;
131+
let replaced = regex.replace_all(&str, to);
132+
Ok(replaced.to_string())
133+
}

0 commit comments

Comments
 (0)