Skip to main content

graphix_package_str/
lib.rs

1#![doc(
2    html_logo_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg",
3    html_favicon_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg"
4)]
5use anyhow::{bail, Context, Result};
6use arcstr::{literal, ArcStr};
7use escaping::Escape;
8use graphix_compiler::{
9    err, errf,
10    expr::ExprId,
11    typ::{FnType, Type},
12    Apply, BuiltIn, Event, ExecCtx, Node, Rt, Scope, TypecheckPhase, UserEvent,
13};
14use graphix_package_core::{extract_cast_type, CachedArgs, CachedVals, EvalCached};
15use netidx::{path::Path, subscriber::Value};
16use netidx_value::ValArray;
17use smallvec::SmallVec;
18use std::cell::RefCell;
19
20#[derive(Debug, Default)]
21struct StartsWithEv;
22
23impl<R: Rt, E: UserEvent> EvalCached<R, E> for StartsWithEv {
24    const NAME: &str = "str_starts_with";
25    const NEEDS_CALLSITE: bool = false;
26
27    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
28        match (&from.0[0], &from.0[1]) {
29            (Some(Value::String(pfx)), Some(Value::String(val))) => {
30                if val.starts_with(&**pfx) {
31                    Some(Value::Bool(true))
32                } else {
33                    Some(Value::Bool(false))
34                }
35            }
36            _ => None,
37        }
38    }
39}
40
41type StartsWith = CachedArgs<StartsWithEv>;
42
43#[derive(Debug, Default)]
44struct EndsWithEv;
45
46impl<R: Rt, E: UserEvent> EvalCached<R, E> for EndsWithEv {
47    const NAME: &str = "str_ends_with";
48    const NEEDS_CALLSITE: bool = false;
49
50    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
51        match (&from.0[0], &from.0[1]) {
52            (Some(Value::String(sfx)), Some(Value::String(val))) => {
53                if val.ends_with(&**sfx) {
54                    Some(Value::Bool(true))
55                } else {
56                    Some(Value::Bool(false))
57                }
58            }
59            _ => None,
60        }
61    }
62}
63
64type EndsWith = CachedArgs<EndsWithEv>;
65
66#[derive(Debug, Default)]
67struct ContainsEv;
68
69impl<R: Rt, E: UserEvent> EvalCached<R, E> for ContainsEv {
70    const NAME: &str = "str_contains";
71    const NEEDS_CALLSITE: bool = false;
72
73    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
74        match (&from.0[0], &from.0[1]) {
75            (Some(Value::String(chs)), Some(Value::String(val))) => {
76                if val.contains(&**chs) {
77                    Some(Value::Bool(true))
78                } else {
79                    Some(Value::Bool(false))
80                }
81            }
82            _ => None,
83        }
84    }
85}
86
87type Contains = CachedArgs<ContainsEv>;
88
89#[derive(Debug, Default)]
90struct StripPrefixEv;
91
92impl<R: Rt, E: UserEvent> EvalCached<R, E> for StripPrefixEv {
93    const NAME: &str = "str_strip_prefix";
94    const NEEDS_CALLSITE: bool = false;
95
96    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
97        match (&from.0[0], &from.0[1]) {
98            (Some(Value::String(pfx)), Some(Value::String(val))) => val
99                .strip_prefix(&**pfx)
100                .map(|s| Value::String(s.into()))
101                .or(Some(Value::Null)),
102            _ => None,
103        }
104    }
105}
106
107type StripPrefix = CachedArgs<StripPrefixEv>;
108
109#[derive(Debug, Default)]
110struct StripSuffixEv;
111
112impl<R: Rt, E: UserEvent> EvalCached<R, E> for StripSuffixEv {
113    const NAME: &str = "str_strip_suffix";
114    const NEEDS_CALLSITE: bool = false;
115
116    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
117        match (&from.0[0], &from.0[1]) {
118            (Some(Value::String(sfx)), Some(Value::String(val))) => val
119                .strip_suffix(&**sfx)
120                .map(|s| Value::String(s.into()))
121                .or(Some(Value::Null)),
122            _ => None,
123        }
124    }
125}
126
127type StripSuffix = CachedArgs<StripSuffixEv>;
128
129#[derive(Debug, Default)]
130struct TrimEv;
131
132impl<R: Rt, E: UserEvent> EvalCached<R, E> for TrimEv {
133    const NAME: &str = "str_trim";
134    const NEEDS_CALLSITE: bool = false;
135
136    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
137        match &from.0[0] {
138            Some(Value::String(val)) => Some(Value::String(val.trim().into())),
139            _ => None,
140        }
141    }
142}
143
144type Trim = CachedArgs<TrimEv>;
145
146#[derive(Debug, Default)]
147struct TrimStartEv;
148
149impl<R: Rt, E: UserEvent> EvalCached<R, E> for TrimStartEv {
150    const NAME: &str = "str_trim_start";
151    const NEEDS_CALLSITE: bool = false;
152
153    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
154        match &from.0[0] {
155            Some(Value::String(val)) => Some(Value::String(val.trim_start().into())),
156            _ => None,
157        }
158    }
159}
160
161type TrimStart = CachedArgs<TrimStartEv>;
162
163#[derive(Debug, Default)]
164struct TrimEndEv;
165
166impl<R: Rt, E: UserEvent> EvalCached<R, E> for TrimEndEv {
167    const NAME: &str = "str_trim_end";
168    const NEEDS_CALLSITE: bool = false;
169
170    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
171        match &from.0[0] {
172            Some(Value::String(val)) => Some(Value::String(val.trim_end().into())),
173            _ => None,
174        }
175    }
176}
177
178type TrimEnd = CachedArgs<TrimEndEv>;
179
180#[derive(Debug, Default)]
181struct ReplaceEv;
182
183impl<R: Rt, E: UserEvent> EvalCached<R, E> for ReplaceEv {
184    const NAME: &str = "str_replace";
185    const NEEDS_CALLSITE: bool = false;
186
187    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
188        match (&from.0[0], &from.0[1], &from.0[2]) {
189            (
190                Some(Value::String(pat)),
191                Some(Value::String(rep)),
192                Some(Value::String(val)),
193            ) => Some(Value::String(val.replace(&**pat, &**rep).into())),
194            _ => None,
195        }
196    }
197}
198
199type Replace = CachedArgs<ReplaceEv>;
200
201#[derive(Debug, Default)]
202struct DirnameEv;
203
204impl<R: Rt, E: UserEvent> EvalCached<R, E> for DirnameEv {
205    const NAME: &str = "str_dirname";
206    const NEEDS_CALLSITE: bool = false;
207
208    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
209        match &from.0[0] {
210            Some(Value::String(path)) => match Path::dirname(path) {
211                None if path != "/" => Some(Value::String(literal!("/"))),
212                None => Some(Value::Null),
213                Some(dn) => Some(Value::String(dn.into())),
214            },
215            _ => None,
216        }
217    }
218}
219
220type Dirname = CachedArgs<DirnameEv>;
221
222#[derive(Debug, Default)]
223struct BasenameEv;
224
225impl<R: Rt, E: UserEvent> EvalCached<R, E> for BasenameEv {
226    const NAME: &str = "str_basename";
227    const NEEDS_CALLSITE: bool = false;
228
229    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
230        match &from.0[0] {
231            Some(Value::String(path)) => match Path::basename(path) {
232                None => Some(Value::Null),
233                Some(dn) => Some(Value::String(dn.into())),
234            },
235            _ => None,
236        }
237    }
238}
239
240type Basename = CachedArgs<BasenameEv>;
241
242#[derive(Debug, Default)]
243struct StringJoinEv;
244
245impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringJoinEv {
246    const NAME: &str = "str_join";
247    const NEEDS_CALLSITE: bool = false;
248
249    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
250        thread_local! {
251            static BUF: RefCell<String> = RefCell::new(String::new());
252        }
253        match &from.0[..] {
254            [_] | [] => None,
255            [None, ..] => None,
256            [Some(sep), parts @ ..] => {
257                for p in parts {
258                    if p.is_none() {
259                        return None;
260                    }
261                }
262                let sep = match sep {
263                    Value::String(c) => c.clone(),
264                    sep => match sep.clone().cast_to::<ArcStr>().ok() {
265                        Some(c) => c,
266                        None => return None,
267                    },
268                };
269                BUF.with_borrow_mut(|buf| {
270                    macro_rules! push {
271                        ($c:expr) => {
272                            if buf.is_empty() {
273                                buf.push_str($c.as_str());
274                            } else {
275                                buf.push_str(sep.as_str());
276                                buf.push_str($c.as_str());
277                            }
278                        };
279                    }
280                    buf.clear();
281                    for p in parts {
282                        match p.as_ref().unwrap() {
283                            Value::String(c) => push!(c),
284                            Value::Array(a) => {
285                                for v in a.iter() {
286                                    if let Value::String(c) = v {
287                                        push!(c)
288                                    }
289                                }
290                            }
291                            _ => return None,
292                        }
293                    }
294                    Some(Value::String(buf.as_str().into()))
295                })
296            }
297        }
298    }
299}
300
301type StringJoin = CachedArgs<StringJoinEv>;
302
303#[derive(Debug, Default)]
304struct StringConcatEv;
305
306impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringConcatEv {
307    const NAME: &str = "str_concat";
308    const NEEDS_CALLSITE: bool = false;
309
310    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
311        thread_local! {
312            static BUF: RefCell<String> = RefCell::new(String::new());
313        }
314        let parts = &from.0[..];
315        for p in parts {
316            if p.is_none() {
317                return None;
318            }
319        }
320        BUF.with_borrow_mut(|buf| {
321            buf.clear();
322            for p in parts {
323                match p.as_ref().unwrap() {
324                    Value::String(c) => buf.push_str(c.as_ref()),
325                    Value::Array(a) => {
326                        for v in a.iter() {
327                            if let Value::String(c) = v {
328                                buf.push_str(c.as_ref())
329                            }
330                        }
331                    }
332                    _ => return None,
333                }
334            }
335            Some(Value::String(buf.as_str().into()))
336        })
337    }
338}
339
340type StringConcat = CachedArgs<StringConcatEv>;
341
342fn build_escape(esc: Value) -> Result<Escape> {
343    fn escape_non_printing(c: char) -> bool {
344        c.is_control()
345    }
346    let [(_, to_escape), (_, escape_char), (_, tr)] =
347        esc.cast_to::<[(ArcStr, Value); 3]>().context("parse escape")?;
348    let escape_char = {
349        let s = escape_char.cast_to::<ArcStr>().context("escape char")?;
350        if s.len() != 1 {
351            bail!("expected a single escape char")
352        }
353        s.chars().next().unwrap()
354    };
355    let to_escape = match to_escape {
356        Value::String(s) => s.chars().collect::<SmallVec<[char; 32]>>(),
357        _ => bail!("escape: expected a string"),
358    };
359    let tr =
360        tr.cast_to::<SmallVec<[(ArcStr, ArcStr); 8]>>().context("escape: parsing tr")?;
361    for (k, _) in &tr {
362        if k.len() != 1 {
363            bail!("escape: tr key {k} is invalid, expected 1 character");
364        }
365    }
366    let tr = tr
367        .into_iter()
368        .map(|(k, v)| (k.chars().next().unwrap(), v))
369        .collect::<SmallVec<[_; 8]>>();
370    let tr = tr.iter().map(|(c, s)| (*c, s.as_str())).collect::<SmallVec<[_; 8]>>();
371    Escape::new(escape_char, &to_escape, &tr, Some(escape_non_printing))
372}
373
374macro_rules! escape_fn {
375    ($name:ident, $builtin_name:literal, $escape:ident) => {
376        #[derive(Debug)]
377        struct $name {
378            escape: Option<Escape>,
379            args: CachedVals,
380        }
381
382        impl<R: Rt, E: UserEvent> BuiltIn<R, E> for $name {
383            const NAME: &str = $builtin_name;
384            const NEEDS_CALLSITE: bool = false;
385
386            fn init<'a, 'b, 'c, 'd>(
387                _ctx: &'a mut ExecCtx<R, E>,
388                _typ: &'a FnType,
389                _resolved: Option<&'d FnType>,
390                _scope: &'b Scope,
391                from: &'c [Node<R, E>],
392                _top_id: ExprId,
393            ) -> Result<Box<dyn Apply<R, E>>> {
394                Ok(Box::new(Self { escape: None, args: CachedVals::new(from) }))
395            }
396        }
397
398        impl<R: Rt, E: UserEvent> Apply<R, E> for $name {
399            fn update(
400                &mut self,
401                ctx: &mut ExecCtx<R, E>,
402                from: &mut [Node<R, E>],
403                event: &mut Event<E>,
404            ) -> Option<Value> {
405                static TAG: ArcStr = literal!("StringError");
406                let mut up = [false; 2];
407                self.args.update_diff(&mut up, ctx, from, event);
408                if up[0] {
409                    match &self.args.0[0] {
410                        Some(esc) => match build_escape(esc.clone()) {
411                            Err(e) => {
412                                return Some(errf!(TAG, "escape: invalid argument {e:?}"))
413                            }
414                            Ok(esc) => self.escape = Some(esc),
415                        },
416                        _ => return None,
417                    };
418                }
419                match (up, &self.escape, &self.args.0[1]) {
420                    ([_, true], Some(esc), Some(Value::String(s))) => {
421                        Some(Value::String(ArcStr::from(esc.$escape(&s))))
422                    }
423                    (_, _, _) => None,
424                }
425            }
426
427            fn sleep(&mut self, _ctx: &mut ExecCtx<R, E>) {
428                self.escape = None;
429                self.args.clear();
430            }
431        }
432    };
433}
434
435escape_fn!(StringEscape, "str_escape", escape);
436escape_fn!(StringUnescape, "str_unescape", unescape);
437
438macro_rules! string_split {
439    ($name:ident, $final_name:ident, $builtin:literal, $fn:ident) => {
440        #[derive(Debug, Default)]
441        struct $name;
442
443        impl<R: Rt, E: UserEvent> EvalCached<R, E> for $name {
444            const NAME: &str = $builtin;
445            const NEEDS_CALLSITE: bool = false;
446
447            fn eval(
448                &mut self,
449                _ctx: &mut ExecCtx<R, E>,
450                from: &CachedVals,
451            ) -> Option<Value> {
452                for p in &from.0[..] {
453                    if p.is_none() {
454                        return None;
455                    }
456                }
457                let pat = match &from.0[0] {
458                    Some(Value::String(s)) => s,
459                    _ => return None,
460                };
461                match &from.0[1] {
462                    Some(Value::String(s)) => Some(Value::Array(ValArray::from_iter(
463                        s.$fn(&**pat).map(|s| Value::String(ArcStr::from(s))),
464                    ))),
465                    _ => None,
466                }
467            }
468        }
469
470        type $final_name = CachedArgs<$name>;
471    };
472}
473
474string_split!(StringSplitEv, StringSplit, "str_split", split);
475string_split!(StringRSplitEv, StringRSplit, "str_rsplit", rsplit);
476
477macro_rules! string_splitn {
478    ($name:ident, $final_name:ident, $builtin:literal, $fn:ident) => {
479        #[derive(Debug, Default)]
480        struct $name;
481
482        impl<R: Rt, E: UserEvent> EvalCached<R, E> for $name {
483            const NAME: &str = $builtin;
484            const NEEDS_CALLSITE: bool = false;
485
486            fn eval(
487                &mut self,
488                _ctx: &mut ExecCtx<R, E>,
489                from: &CachedVals,
490            ) -> Option<Value> {
491                static TAG: ArcStr = literal!("StringSplitError");
492                for p in &from.0[..] {
493                    if p.is_none() {
494                        return None;
495                    }
496                }
497                let pat = match &from.0[0] {
498                    Some(Value::String(s)) => s,
499                    _ => return None,
500                };
501                let n = match &from.0[1] {
502                    Some(Value::I64(n)) if *n > 0 => *n as usize,
503                    Some(v) => {
504                        return Some(errf!(TAG, "splitn: {v} must be a number > 0"))
505                    }
506                    None => return None,
507                };
508                match &from.0[2] {
509                    Some(Value::String(s)) => Some(Value::Array(ValArray::from_iter(
510                        s.$fn(n, &**pat).map(|s| Value::String(ArcStr::from(s))),
511                    ))),
512                    _ => None,
513                }
514            }
515        }
516
517        type $final_name = CachedArgs<$name>;
518    };
519}
520
521string_splitn!(StringSplitNEv, StringSplitN, "str_splitn", splitn);
522string_splitn!(StringRSplitNEv, StringRSplitN, "str_rsplitn", rsplitn);
523
524#[derive(Debug, Default)]
525struct StringSplitEscapedEv;
526
527impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringSplitEscapedEv {
528    const NAME: &str = "str_split_escaped";
529    const NEEDS_CALLSITE: bool = false;
530
531    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
532        static TAG: ArcStr = literal!("SplitEscError");
533        for p in &from.0[..] {
534            if p.is_none() {
535                return None;
536            }
537        }
538        let esc = match &from.0[0] {
539            Some(Value::String(s)) if s.len() == 1 => s.chars().next().unwrap(),
540            _ => return Some(err!(TAG, "split_escaped: invalid escape char")),
541        };
542        let sep = match &from.0[1] {
543            Some(Value::String(s)) if s.len() == 1 => s.chars().next().unwrap(),
544            _ => return Some(err!(TAG, "split_escaped: invalid separator")),
545        };
546        match &from.0[2] {
547            Some(Value::String(s)) => Some(Value::Array(ValArray::from_iter(
548                escaping::split(s, esc, sep).map(|s| Value::String(ArcStr::from(s))),
549            ))),
550            _ => None,
551        }
552    }
553}
554
555type StringSplitEscaped = CachedArgs<StringSplitEscapedEv>;
556
557#[derive(Debug, Default)]
558struct StringSplitNEscapedEv;
559
560impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringSplitNEscapedEv {
561    const NAME: &str = "str_splitn_escaped";
562    const NEEDS_CALLSITE: bool = false;
563
564    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
565        static TAG: ArcStr = literal!("SplitNEscError");
566        for p in &from.0[..] {
567            if p.is_none() {
568                return None;
569            }
570        }
571        let n = match &from.0[0] {
572            Some(Value::I64(n)) if *n > 0 => *n as usize,
573            Some(v) => return Some(errf!(TAG, "splitn_escaped: invalid n {v}")),
574            None => return None,
575        };
576        let esc = match &from.0[1] {
577            Some(Value::String(s)) if s.len() == 1 => s.chars().next().unwrap(),
578            _ => return Some(err!(TAG, "split_escaped: invalid escape char")),
579        };
580        let sep = match &from.0[2] {
581            Some(Value::String(s)) if s.len() == 1 => s.chars().next().unwrap(),
582            _ => return Some(err!(TAG, "split_escaped: invalid separator")),
583        };
584        match &from.0[3] {
585            Some(Value::String(s)) => Some(Value::Array(ValArray::from_iter(
586                escaping::splitn(s, esc, n, sep).map(|s| Value::String(ArcStr::from(s))),
587            ))),
588            _ => None,
589        }
590    }
591}
592
593type StringSplitNEscaped = CachedArgs<StringSplitNEscapedEv>;
594
595#[derive(Debug, Default)]
596struct StringSplitOnceEv;
597
598impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringSplitOnceEv {
599    const NAME: &str = "str_split_once";
600    const NEEDS_CALLSITE: bool = false;
601
602    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
603        for p in &from.0[..] {
604            if p.is_none() {
605                return None;
606            }
607        }
608        let pat = match &from.0[0] {
609            Some(Value::String(s)) => s,
610            _ => return None,
611        };
612        match &from.0[1] {
613            Some(Value::String(s)) => match s.split_once(&**pat) {
614                None => Some(Value::Null),
615                Some((s0, s1)) => Some(Value::Array(ValArray::from([
616                    Value::String(s0.into()),
617                    Value::String(s1.into()),
618                ]))),
619            },
620            _ => None,
621        }
622    }
623}
624
625type StringSplitOnce = CachedArgs<StringSplitOnceEv>;
626
627#[derive(Debug, Default)]
628struct StringRSplitOnceEv;
629
630impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringRSplitOnceEv {
631    const NAME: &str = "str_rsplit_once";
632    const NEEDS_CALLSITE: bool = false;
633
634    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
635        for p in &from.0[..] {
636            if p.is_none() {
637                return None;
638            }
639        }
640        let pat = match &from.0[0] {
641            Some(Value::String(s)) => s,
642            _ => return None,
643        };
644        match &from.0[1] {
645            Some(Value::String(s)) => match s.rsplit_once(&**pat) {
646                None => Some(Value::Null),
647                Some((s0, s1)) => Some(Value::Array(ValArray::from([
648                    Value::String(s0.into()),
649                    Value::String(s1.into()),
650                ]))),
651            },
652            _ => None,
653        }
654    }
655}
656
657type StringRSplitOnce = CachedArgs<StringRSplitOnceEv>;
658
659#[derive(Debug, Default)]
660struct StringToLowerEv;
661
662impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringToLowerEv {
663    const NAME: &str = "str_to_lower";
664    const NEEDS_CALLSITE: bool = false;
665
666    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
667        match &from.0[0] {
668            Some(Value::String(s)) => Some(Value::String(s.to_lowercase().into())),
669            _ => None,
670        }
671    }
672}
673
674type StringToLower = CachedArgs<StringToLowerEv>;
675
676#[derive(Debug, Default)]
677struct StringToUpperEv;
678
679impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringToUpperEv {
680    const NAME: &str = "str_to_upper";
681    const NEEDS_CALLSITE: bool = false;
682
683    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
684        match &from.0[0] {
685            Some(Value::String(s)) => Some(Value::String(s.to_uppercase().into())),
686            _ => None,
687        }
688    }
689}
690
691type StringToUpper = CachedArgs<StringToUpperEv>;
692
693#[derive(Debug, Default)]
694struct SprintfEv {
695    buf: String,
696    args: Vec<Value>,
697}
698
699impl<R: Rt, E: UserEvent> EvalCached<R, E> for SprintfEv {
700    const NAME: &str = "str_sprintf";
701    const NEEDS_CALLSITE: bool = false;
702
703    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
704        match &from.0[..] {
705            [Some(Value::String(fmt)), args @ ..] => {
706                self.buf.clear();
707                self.args.clear();
708                for v in args {
709                    match v {
710                        Some(v) => self.args.push(v.clone()),
711                        None => return None,
712                    }
713                }
714                match netidx_value::printf(&mut self.buf, fmt, &self.args) {
715                    Ok(_) => Some(Value::String(ArcStr::from(&self.buf))),
716                    Err(e) => Some(Value::error(ArcStr::from(e.to_string()))),
717                }
718            }
719            _ => None,
720        }
721    }
722}
723
724type Sprintf = CachedArgs<SprintfEv>;
725
726#[derive(Debug, Default)]
727struct LenEv;
728
729impl<R: Rt, E: UserEvent> EvalCached<R, E> for LenEv {
730    const NAME: &str = "str_len";
731    const NEEDS_CALLSITE: bool = false;
732
733    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
734        match &from.0[0] {
735            Some(Value::String(s)) => Some(Value::I64(s.len() as i64)),
736            _ => None,
737        }
738    }
739}
740
741type Len = CachedArgs<LenEv>;
742
743#[derive(Debug, Default)]
744struct SubEv(String);
745
746impl<R: Rt, E: UserEvent> EvalCached<R, E> for SubEv {
747    const NAME: &str = "str_sub";
748    const NEEDS_CALLSITE: bool = false;
749
750    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
751        match &from.0[..] {
752            [Some(Value::I64(start)), Some(Value::I64(len)), Some(Value::String(s))]
753                if *start >= 0 && *len >= 0 =>
754            {
755                let start = *start as usize;
756                let end = start + *len as usize;
757                self.0.clear();
758                for (i, c) in s.chars().enumerate() {
759                    if i >= start && i < end {
760                        self.0.push(c);
761                    }
762                }
763                Some(Value::String(ArcStr::from(&self.0)))
764            }
765            v => Some(errf!(literal!("SubError"), "sub args must be non negative {v:?}")),
766        }
767    }
768}
769
770type Sub = CachedArgs<SubEv>;
771
772#[derive(Debug, Default)]
773struct ParseEv {
774    cast_typ: Option<Type>,
775}
776
777impl<R: Rt, E: UserEvent> EvalCached<R, E> for ParseEv {
778    const NAME: &str = "str_parse";
779    const NEEDS_CALLSITE: bool = true;
780
781    fn init(
782        _ctx: &mut ExecCtx<R, E>,
783        _typ: &FnType,
784        resolved: Option<&FnType>,
785        _scope: &Scope,
786        _from: &[Node<R, E>],
787        _top_id: ExprId,
788    ) -> Self {
789        Self { cast_typ: extract_cast_type(resolved) }
790    }
791
792    fn typecheck(
793        &mut self,
794        _ctx: &mut ExecCtx<R, E>,
795        _from: &mut [Node<R, E>],
796        phase: TypecheckPhase<'_>,
797    ) -> Result<()> {
798        match phase {
799            TypecheckPhase::Lambda => Ok(()),
800            TypecheckPhase::CallSite(resolved) => {
801                self.cast_typ = extract_cast_type(Some(resolved));
802                if self.cast_typ.is_none() {
803                    bail!("str::parse requires a concrete return type")
804                }
805                Ok(())
806            }
807        }
808    }
809
810    fn eval(&mut self, ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
811        let raw = match &from.0[0] {
812            Some(Value::String(s)) => match s.parse::<Value>() {
813                Ok(v) => match v {
814                    Value::Error(e) => return Some(errf!(literal!("ParseError"), "{e}")),
815                    v => v,
816                },
817                Err(e) => return Some(errf!(literal!("ParseError"), "{e:?}")),
818            },
819            _ => return None,
820        };
821        Some(match &self.cast_typ {
822            Some(typ) => typ.cast_value(&ctx.env, raw),
823            None => errf!("TypeError", "parse requires a concrete type annotation"),
824        })
825    }
826}
827
828type Parse = CachedArgs<ParseEv>;
829
830graphix_derive::defpackage! {
831    builtins => [
832        StartsWith,
833        EndsWith,
834        Contains,
835        StripPrefix,
836        StripSuffix,
837        Trim,
838        TrimStart,
839        TrimEnd,
840        Replace,
841        Dirname,
842        Basename,
843        StringJoin,
844        StringConcat,
845        StringEscape,
846        StringUnescape,
847        StringSplit,
848        StringRSplit,
849        StringSplitN,
850        StringRSplitN,
851        StringSplitOnce,
852        StringRSplitOnce,
853        StringSplitEscaped,
854        StringSplitNEscaped,
855        StringToLower,
856        StringToUpper,
857        Sprintf,
858        Len,
859        Sub,
860        Parse,
861    ],
862}