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 RowColEv;
244
245impl<R: Rt, E: UserEvent> EvalCached<R, E> for RowColEv {
246    const NAME: &str = "str_row_col";
247    const NEEDS_CALLSITE: bool = false;
248
249    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
250        match &from.0[0] {
251            Some(Value::String(path)) => {
252                let col = match Path::basename(path) {
253                    Some(s) => s,
254                    None => return Some(Value::Null),
255                };
256                let parent = match Path::dirname(path) {
257                    Some(s) => s,
258                    None => return Some(Value::Null),
259                };
260                let row = match Path::basename(parent) {
261                    Some(s) => s,
262                    None => return Some(Value::Null),
263                };
264                Some(Value::Array(ValArray::from([
265                    Value::String(row.into()),
266                    Value::String(col.into()),
267                ])))
268            }
269            _ => None,
270        }
271    }
272}
273
274type RowCol = CachedArgs<RowColEv>;
275
276#[derive(Debug, Default)]
277struct StringJoinEv;
278
279impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringJoinEv {
280    const NAME: &str = "str_join";
281    const NEEDS_CALLSITE: bool = false;
282
283    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
284        thread_local! {
285            static BUF: RefCell<String> = RefCell::new(String::new());
286        }
287        match &from.0[..] {
288            [_] | [] => None,
289            [None, ..] => None,
290            [Some(sep), parts @ ..] => {
291                for p in parts {
292                    if p.is_none() {
293                        return None;
294                    }
295                }
296                let sep = match sep {
297                    Value::String(c) => c.clone(),
298                    sep => match sep.clone().cast_to::<ArcStr>().ok() {
299                        Some(c) => c,
300                        None => return None,
301                    },
302                };
303                BUF.with_borrow_mut(|buf| {
304                    macro_rules! push {
305                        ($c:expr) => {
306                            if buf.is_empty() {
307                                buf.push_str($c.as_str());
308                            } else {
309                                buf.push_str(sep.as_str());
310                                buf.push_str($c.as_str());
311                            }
312                        };
313                    }
314                    buf.clear();
315                    for p in parts {
316                        match p.as_ref().unwrap() {
317                            Value::String(c) => push!(c),
318                            Value::Array(a) => {
319                                for v in a.iter() {
320                                    if let Value::String(c) = v {
321                                        push!(c)
322                                    }
323                                }
324                            }
325                            _ => return None,
326                        }
327                    }
328                    Some(Value::String(buf.as_str().into()))
329                })
330            }
331        }
332    }
333}
334
335type StringJoin = CachedArgs<StringJoinEv>;
336
337#[derive(Debug, Default)]
338struct StringConcatEv;
339
340impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringConcatEv {
341    const NAME: &str = "str_concat";
342    const NEEDS_CALLSITE: bool = false;
343
344    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
345        thread_local! {
346            static BUF: RefCell<String> = RefCell::new(String::new());
347        }
348        let parts = &from.0[..];
349        for p in parts {
350            if p.is_none() {
351                return None;
352            }
353        }
354        BUF.with_borrow_mut(|buf| {
355            buf.clear();
356            for p in parts {
357                match p.as_ref().unwrap() {
358                    Value::String(c) => buf.push_str(c.as_ref()),
359                    Value::Array(a) => {
360                        for v in a.iter() {
361                            if let Value::String(c) = v {
362                                buf.push_str(c.as_ref())
363                            }
364                        }
365                    }
366                    _ => return None,
367                }
368            }
369            Some(Value::String(buf.as_str().into()))
370        })
371    }
372}
373
374type StringConcat = CachedArgs<StringConcatEv>;
375
376fn build_escape(esc: Value) -> Result<Escape> {
377    fn escape_non_printing(c: char) -> bool {
378        c.is_control()
379    }
380    let [(_, to_escape), (_, escape_char), (_, tr)] =
381        esc.cast_to::<[(ArcStr, Value); 3]>().context("parse escape")?;
382    let escape_char = {
383        let s = escape_char.cast_to::<ArcStr>().context("escape char")?;
384        if s.len() != 1 {
385            bail!("expected a single escape char")
386        }
387        s.chars().next().unwrap()
388    };
389    let to_escape = match to_escape {
390        Value::String(s) => s.chars().collect::<SmallVec<[char; 32]>>(),
391        _ => bail!("escape: expected a string"),
392    };
393    let tr =
394        tr.cast_to::<SmallVec<[(ArcStr, ArcStr); 8]>>().context("escape: parsing tr")?;
395    for (k, _) in &tr {
396        if k.len() != 1 {
397            bail!("escape: tr key {k} is invalid, expected 1 character");
398        }
399    }
400    let tr = tr
401        .into_iter()
402        .map(|(k, v)| (k.chars().next().unwrap(), v))
403        .collect::<SmallVec<[_; 8]>>();
404    let tr = tr.iter().map(|(c, s)| (*c, s.as_str())).collect::<SmallVec<[_; 8]>>();
405    Escape::new(escape_char, &to_escape, &tr, Some(escape_non_printing))
406}
407
408macro_rules! escape_fn {
409    ($name:ident, $builtin_name:literal, $escape:ident) => {
410        #[derive(Debug)]
411        struct $name {
412            escape: Option<Escape>,
413            args: CachedVals,
414        }
415
416        impl<R: Rt, E: UserEvent> BuiltIn<R, E> for $name {
417            const NAME: &str = $builtin_name;
418            const NEEDS_CALLSITE: bool = false;
419
420            fn init<'a, 'b, 'c, 'd>(
421                _ctx: &'a mut ExecCtx<R, E>,
422                _typ: &'a FnType,
423                _resolved: Option<&'d FnType>,
424                _scope: &'b Scope,
425                from: &'c [Node<R, E>],
426                _top_id: ExprId,
427            ) -> Result<Box<dyn Apply<R, E>>> {
428                Ok(Box::new(Self { escape: None, args: CachedVals::new(from) }))
429            }
430        }
431
432        impl<R: Rt, E: UserEvent> Apply<R, E> for $name {
433            fn update(
434                &mut self,
435                ctx: &mut ExecCtx<R, E>,
436                from: &mut [Node<R, E>],
437                event: &mut Event<E>,
438            ) -> Option<Value> {
439                static TAG: ArcStr = literal!("StringError");
440                let mut up = [false; 2];
441                self.args.update_diff(&mut up, ctx, from, event);
442                if up[0] {
443                    match &self.args.0[0] {
444                        Some(esc) => match build_escape(esc.clone()) {
445                            Err(e) => {
446                                return Some(errf!(TAG, "escape: invalid argument {e:?}"))
447                            }
448                            Ok(esc) => self.escape = Some(esc),
449                        },
450                        _ => return None,
451                    };
452                }
453                match (up, &self.escape, &self.args.0[1]) {
454                    ([_, true], Some(esc), Some(Value::String(s))) => {
455                        Some(Value::String(ArcStr::from(esc.$escape(&s))))
456                    }
457                    (_, _, _) => None,
458                }
459            }
460
461            fn sleep(&mut self, _ctx: &mut ExecCtx<R, E>) {
462                self.escape = None;
463                self.args.clear();
464            }
465        }
466    };
467}
468
469escape_fn!(StringEscape, "str_escape", escape);
470escape_fn!(StringUnescape, "str_unescape", unescape);
471
472macro_rules! string_split {
473    ($name:ident, $final_name:ident, $builtin:literal, $fn:ident) => {
474        #[derive(Debug, Default)]
475        struct $name;
476
477        impl<R: Rt, E: UserEvent> EvalCached<R, E> for $name {
478            const NAME: &str = $builtin;
479            const NEEDS_CALLSITE: bool = false;
480
481            fn eval(
482                &mut self,
483                _ctx: &mut ExecCtx<R, E>,
484                from: &CachedVals,
485            ) -> Option<Value> {
486                for p in &from.0[..] {
487                    if p.is_none() {
488                        return None;
489                    }
490                }
491                let pat = match &from.0[0] {
492                    Some(Value::String(s)) => s,
493                    _ => return None,
494                };
495                match &from.0[1] {
496                    Some(Value::String(s)) => Some(Value::Array(ValArray::from_iter(
497                        s.$fn(&**pat).map(|s| Value::String(ArcStr::from(s))),
498                    ))),
499                    _ => None,
500                }
501            }
502        }
503
504        type $final_name = CachedArgs<$name>;
505    };
506}
507
508string_split!(StringSplitEv, StringSplit, "str_split", split);
509string_split!(StringRSplitEv, StringRSplit, "str_rsplit", rsplit);
510
511macro_rules! string_splitn {
512    ($name:ident, $final_name:ident, $builtin:literal, $fn:ident) => {
513        #[derive(Debug, Default)]
514        struct $name;
515
516        impl<R: Rt, E: UserEvent> EvalCached<R, E> for $name {
517            const NAME: &str = $builtin;
518            const NEEDS_CALLSITE: bool = false;
519
520            fn eval(
521                &mut self,
522                _ctx: &mut ExecCtx<R, E>,
523                from: &CachedVals,
524            ) -> Option<Value> {
525                static TAG: ArcStr = literal!("StringSplitError");
526                for p in &from.0[..] {
527                    if p.is_none() {
528                        return None;
529                    }
530                }
531                let pat = match &from.0[0] {
532                    Some(Value::String(s)) => s,
533                    _ => return None,
534                };
535                let n = match &from.0[1] {
536                    Some(Value::I64(n)) if *n > 0 => *n as usize,
537                    Some(v) => {
538                        return Some(errf!(TAG, "splitn: {v} must be a number > 0"))
539                    }
540                    None => return None,
541                };
542                match &from.0[2] {
543                    Some(Value::String(s)) => Some(Value::Array(ValArray::from_iter(
544                        s.$fn(n, &**pat).map(|s| Value::String(ArcStr::from(s))),
545                    ))),
546                    _ => None,
547                }
548            }
549        }
550
551        type $final_name = CachedArgs<$name>;
552    };
553}
554
555string_splitn!(StringSplitNEv, StringSplitN, "str_splitn", splitn);
556string_splitn!(StringRSplitNEv, StringRSplitN, "str_rsplitn", rsplitn);
557
558#[derive(Debug, Default)]
559struct StringSplitEscapedEv;
560
561impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringSplitEscapedEv {
562    const NAME: &str = "str_split_escaped";
563    const NEEDS_CALLSITE: bool = false;
564
565    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
566        static TAG: ArcStr = literal!("SplitEscError");
567        for p in &from.0[..] {
568            if p.is_none() {
569                return None;
570            }
571        }
572        let esc = match &from.0[0] {
573            Some(Value::String(s)) if s.len() == 1 => s.chars().next().unwrap(),
574            _ => return Some(err!(TAG, "split_escaped: invalid escape char")),
575        };
576        let sep = 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 separator")),
579        };
580        match &from.0[2] {
581            Some(Value::String(s)) => Some(Value::Array(ValArray::from_iter(
582                escaping::split(s, esc, sep).map(|s| Value::String(ArcStr::from(s))),
583            ))),
584            _ => None,
585        }
586    }
587}
588
589type StringSplitEscaped = CachedArgs<StringSplitEscapedEv>;
590
591#[derive(Debug, Default)]
592struct StringSplitNEscapedEv;
593
594impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringSplitNEscapedEv {
595    const NAME: &str = "str_splitn_escaped";
596    const NEEDS_CALLSITE: bool = false;
597
598    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
599        static TAG: ArcStr = literal!("SplitNEscError");
600        for p in &from.0[..] {
601            if p.is_none() {
602                return None;
603            }
604        }
605        let n = match &from.0[0] {
606            Some(Value::I64(n)) if *n > 0 => *n as usize,
607            Some(v) => return Some(errf!(TAG, "splitn_escaped: invalid n {v}")),
608            None => return None,
609        };
610        let esc = match &from.0[1] {
611            Some(Value::String(s)) if s.len() == 1 => s.chars().next().unwrap(),
612            _ => return Some(err!(TAG, "split_escaped: invalid escape char")),
613        };
614        let sep = match &from.0[2] {
615            Some(Value::String(s)) if s.len() == 1 => s.chars().next().unwrap(),
616            _ => return Some(err!(TAG, "split_escaped: invalid separator")),
617        };
618        match &from.0[3] {
619            Some(Value::String(s)) => Some(Value::Array(ValArray::from_iter(
620                escaping::splitn(s, esc, n, sep).map(|s| Value::String(ArcStr::from(s))),
621            ))),
622            _ => None,
623        }
624    }
625}
626
627type StringSplitNEscaped = CachedArgs<StringSplitNEscapedEv>;
628
629#[derive(Debug, Default)]
630struct StringSplitOnceEv;
631
632impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringSplitOnceEv {
633    const NAME: &str = "str_split_once";
634    const NEEDS_CALLSITE: bool = false;
635
636    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
637        for p in &from.0[..] {
638            if p.is_none() {
639                return None;
640            }
641        }
642        let pat = match &from.0[0] {
643            Some(Value::String(s)) => s,
644            _ => return None,
645        };
646        match &from.0[1] {
647            Some(Value::String(s)) => match s.split_once(&**pat) {
648                None => Some(Value::Null),
649                Some((s0, s1)) => Some(Value::Array(ValArray::from([
650                    Value::String(s0.into()),
651                    Value::String(s1.into()),
652                ]))),
653            },
654            _ => None,
655        }
656    }
657}
658
659type StringSplitOnce = CachedArgs<StringSplitOnceEv>;
660
661#[derive(Debug, Default)]
662struct StringRSplitOnceEv;
663
664impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringRSplitOnceEv {
665    const NAME: &str = "str_rsplit_once";
666    const NEEDS_CALLSITE: bool = false;
667
668    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
669        for p in &from.0[..] {
670            if p.is_none() {
671                return None;
672            }
673        }
674        let pat = match &from.0[0] {
675            Some(Value::String(s)) => s,
676            _ => return None,
677        };
678        match &from.0[1] {
679            Some(Value::String(s)) => match s.rsplit_once(&**pat) {
680                None => Some(Value::Null),
681                Some((s0, s1)) => Some(Value::Array(ValArray::from([
682                    Value::String(s0.into()),
683                    Value::String(s1.into()),
684                ]))),
685            },
686            _ => None,
687        }
688    }
689}
690
691type StringRSplitOnce = CachedArgs<StringRSplitOnceEv>;
692
693#[derive(Debug, Default)]
694struct StringToLowerEv;
695
696impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringToLowerEv {
697    const NAME: &str = "str_to_lower";
698    const NEEDS_CALLSITE: bool = false;
699
700    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
701        match &from.0[0] {
702            Some(Value::String(s)) => Some(Value::String(s.to_lowercase().into())),
703            _ => None,
704        }
705    }
706}
707
708type StringToLower = CachedArgs<StringToLowerEv>;
709
710#[derive(Debug, Default)]
711struct StringToUpperEv;
712
713impl<R: Rt, E: UserEvent> EvalCached<R, E> for StringToUpperEv {
714    const NAME: &str = "str_to_upper";
715    const NEEDS_CALLSITE: bool = false;
716
717    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
718        match &from.0[0] {
719            Some(Value::String(s)) => Some(Value::String(s.to_uppercase().into())),
720            _ => None,
721        }
722    }
723}
724
725type StringToUpper = CachedArgs<StringToUpperEv>;
726
727#[derive(Debug, Default)]
728struct SprintfEv {
729    buf: String,
730    args: Vec<Value>,
731}
732
733impl<R: Rt, E: UserEvent> EvalCached<R, E> for SprintfEv {
734    const NAME: &str = "str_sprintf";
735    const NEEDS_CALLSITE: bool = false;
736
737    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
738        match &from.0[..] {
739            [Some(Value::String(fmt)), args @ ..] => {
740                self.buf.clear();
741                self.args.clear();
742                for v in args {
743                    match v {
744                        Some(v) => self.args.push(v.clone()),
745                        None => return None,
746                    }
747                }
748                match netidx_value::printf(&mut self.buf, fmt, &self.args) {
749                    Ok(_) => Some(Value::String(ArcStr::from(&self.buf))),
750                    Err(e) => Some(Value::error(ArcStr::from(e.to_string()))),
751                }
752            }
753            _ => None,
754        }
755    }
756}
757
758type Sprintf = CachedArgs<SprintfEv>;
759
760#[derive(Debug, Default)]
761struct LenEv;
762
763impl<R: Rt, E: UserEvent> EvalCached<R, E> for LenEv {
764    const NAME: &str = "str_len";
765    const NEEDS_CALLSITE: bool = false;
766
767    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
768        match &from.0[0] {
769            Some(Value::String(s)) => Some(Value::I64(s.len() as i64)),
770            _ => None,
771        }
772    }
773}
774
775type Len = CachedArgs<LenEv>;
776
777#[derive(Debug, Default)]
778struct SubEv(String);
779
780impl<R: Rt, E: UserEvent> EvalCached<R, E> for SubEv {
781    const NAME: &str = "str_sub";
782    const NEEDS_CALLSITE: bool = false;
783
784    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
785        match &from.0[..] {
786            [Some(Value::I64(start)), Some(Value::I64(len)), Some(Value::String(s))]
787                if *start >= 0 && *len >= 0 =>
788            {
789                let start = *start as usize;
790                let end = start + *len as usize;
791                self.0.clear();
792                for (i, c) in s.chars().enumerate() {
793                    if i >= start && i < end {
794                        self.0.push(c);
795                    }
796                }
797                Some(Value::String(ArcStr::from(&self.0)))
798            }
799            v => Some(errf!(literal!("SubError"), "sub args must be non negative {v:?}")),
800        }
801    }
802}
803
804type Sub = CachedArgs<SubEv>;
805
806#[derive(Debug, Default)]
807struct ParseEv {
808    cast_typ: Option<Type>,
809}
810
811impl<R: Rt, E: UserEvent> EvalCached<R, E> for ParseEv {
812    const NAME: &str = "str_parse";
813    const NEEDS_CALLSITE: bool = true;
814
815    fn init(
816        _ctx: &mut ExecCtx<R, E>,
817        _typ: &FnType,
818        resolved: Option<&FnType>,
819        _scope: &Scope,
820        _from: &[Node<R, E>],
821        _top_id: ExprId,
822    ) -> Self {
823        Self { cast_typ: extract_cast_type(resolved) }
824    }
825
826    fn typecheck(
827        &mut self,
828        _ctx: &mut ExecCtx<R, E>,
829        _from: &mut [Node<R, E>],
830        phase: TypecheckPhase<'_>,
831    ) -> Result<()> {
832        match phase {
833            TypecheckPhase::Lambda => Ok(()),
834            TypecheckPhase::CallSite(resolved) => {
835                self.cast_typ = extract_cast_type(Some(resolved));
836                if self.cast_typ.is_none() {
837                    bail!("str::parse requires a concrete return type")
838                }
839                Ok(())
840            }
841        }
842    }
843
844    fn eval(&mut self, ctx: &mut ExecCtx<R, E>, from: &CachedVals) -> Option<Value> {
845        let raw = match &from.0[0] {
846            Some(Value::String(s)) => match s.parse::<Value>() {
847                Ok(v) => match v {
848                    Value::Error(e) => return Some(errf!(literal!("ParseError"), "{e}")),
849                    v => v,
850                },
851                Err(e) => return Some(errf!(literal!("ParseError"), "{e:?}")),
852            },
853            _ => return None,
854        };
855        Some(match &self.cast_typ {
856            Some(typ) => typ.cast_value(&ctx.env, raw),
857            None => errf!("TypeError", "parse requires a concrete type annotation"),
858        })
859    }
860}
861
862type Parse = CachedArgs<ParseEv>;
863
864graphix_derive::defpackage! {
865    builtins => [
866        StartsWith,
867        EndsWith,
868        Contains,
869        StripPrefix,
870        StripSuffix,
871        Trim,
872        TrimStart,
873        TrimEnd,
874        Replace,
875        Dirname,
876        Basename,
877        RowCol,
878        StringJoin,
879        StringConcat,
880        StringEscape,
881        StringUnescape,
882        StringSplit,
883        StringRSplit,
884        StringSplitN,
885        StringRSplitN,
886        StringSplitOnce,
887        StringRSplitOnce,
888        StringSplitEscaped,
889        StringSplitNEscaped,
890        StringToLower,
891        StringToUpper,
892        Sprintf,
893        Len,
894        Sub,
895        Parse,
896    ],
897}