Skip to main content

sva_core/
lib.rs

1// Concern: the parse->tempo->render pipeline shared by both front ends | Non-concern: argv (sva-cli), JS bindings (sva-wasm) | IO: (a Source, a target) -> Rendered, a Stream or CliError
2
3mod analyze;
4mod answer;
5mod builtins;
6mod cli_error;
7pub mod encode;
8pub mod json;
9mod lint_code;
10mod outline;
11mod output;
12mod query;
13mod set;
14mod song;
15mod target;
16mod until;
17
18pub use analyze::{analyzable, analyzed};
19pub use answer::{
20    Played, Printed, Report, SAMPLE_LIMIT, answer_json, counters_json, engine_stats_json,
21    label_json, query_data, stats_json, stream_stats_json, value_json, work_json,
22};
23pub use builtins::{Builtins, Callable, Crossing, builtins, builtins_data};
24pub use cli_error::{CliError, LintViolation, lint_diagnostic, warning_diagnostic};
25pub use lint_code::LintCode;
26pub use outline::{events_data, outline_data, song_data};
27pub use output::{Diagnostic, Severity, diagnostics_json, error_envelope, success_envelope};
28pub use query::{
29    Asked, Call, Destination, RETIRED, asked, asked_all, call, calls, is_wav, retired, seconds,
30    wav_path,
31};
32pub use set::{overlaid, set};
33pub use song::{as_lane, sung};
34pub use sva_ast::RESERVED_VARIABLES;
35pub use target::{Edge, Target, target};
36pub use until::until;
37
38use std::cell::RefCell;
39use std::num::NonZeroUsize;
40use std::path::Path;
41
42use sva_ast::song::Song;
43use sva_ast::{Dir, Graph, Refusal, Source};
44use sva_engine::{
45    Ask, Change, Changed, DEFAULT_SAMPLE_RATE, EngineError, Kind, PSYCHOACOUSTIC_V1, Phase, Range,
46    RenderConfig, StreamConfig, render, render_called,
47};
48
49pub use sva_engine::{Abandon, Handle, Hear, Never, Out, Placed, Session, Stream, Until};
50
51pub use sva_engine::{Answer, Extent, Label, Output, Representation};
52pub use sva_engine::{
53    Backend, CacheStats, Counters, DEFAULT_CACHE_BYTES, DEFAULT_STORE_BYTES, EngineStats,
54    FETCH_READS, Nothing, Persisted, Store, Tier,
55};
56
57pub const ROOT: &str = "master";
58pub const PROBE: &str = "probe";
59
60pub struct Rendered {
61    pub config: RenderConfig,
62    /// What the song's files earn: each prints, and the render goes on.
63    pub warnings: Vec<sva_ast::song::Warning>,
64    pub expression: String,
65    pub target: String,
66    pub render: sva_engine::Render,
67    /// So a structural check needs no second parse, and an ad-hoc target is the same `probe`.
68    pub graph: Graph,
69}
70
71impl Rendered {
72    pub fn answer(&self, node: &str, representation: Representation) -> Result<Answer, CliError> {
73        let written = |e| CliError::Engine(as_written(e, &self.expression));
74        let id = self.render.node(node).map_err(written)?;
75        let mut answer = sva_engine::answer(&self.render, id, representation).map_err(written)?;
76        self.attribute(&mut answer)?;
77        Ok(answer)
78    }
79
80    /// `asked`'s reading, its bars in this render's tempo.
81    pub fn answered(&self, asked: &Asked) -> Result<Answer, CliError> {
82        let per_bar = self.graph.tempo().map(|tempo| tempo.seconds_per_bar());
83        let representation = asked.representation(self.config.rate, per_bar)?;
84        self.answer(
85            asked.node.as_deref().unwrap_or(&self.target),
86            representation,
87        )
88    }
89
90    /// An alias score reads beside how many instances the render holds.
91    fn attribute(&self, answer: &mut Answer) -> Result<(), CliError> {
92        let Output::Alias(alias) = &mut answer.value else {
93            return Ok(());
94        };
95        alias.instances = self.render.tys.paths().count();
96        Ok(())
97    }
98
99    pub fn label(&self) -> Option<&sva_engine::Label> {
100        self.render.labels.get(&self.render.root)
101    }
102}
103
104/// One render: `target` an expression over the composition `source` holds, its own ref
105/// read over an interval where it writes one.
106pub struct Job<'a> {
107    pub source: &'a dyn Source,
108    pub target: &'a str,
109    /// `None` ends the render where the interval or the target's support ends.
110    pub until: Option<&'a str>,
111    pub rate: Option<u32>,
112    pub bits: Option<i32>,
113    pub asked: &'a [Asked],
114    pub volatile: &'a [String],
115    pub out: Out,
116    pub abandon: &'a dyn Abandon,
117    pub hear: Option<&'a dyn Hear>,
118    /// The most threads it computes on; `None` takes every core the platform reports.
119    pub threads: Option<NonZeroUsize>,
120}
121
122impl<'a> Job<'a> {
123    pub fn over(source: &'a dyn Source, target: &'a str) -> Job<'a> {
124        Job {
125            source,
126            target,
127            until: None,
128            rate: None,
129            bits: None,
130            asked: &[],
131            volatile: &[],
132            out: Out::Kept,
133            abandon: &Never,
134            hear: None,
135            threads: None,
136        }
137    }
138}
139
140fn settle(job: &Job) -> Result<(Graph, RenderConfig, Song), CliError> {
141    let Target { expr, interval } = target(job.target)?;
142    let roots = roots_of(job.source, &expr)?;
143    let mut graph = settled(sva_ast::load_reaching(
144        job.source,
145        &roots.iter().map(String::as_str).collect::<Vec<_>>(),
146    ))?;
147    let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
148        CliError::BadProbe(format!(
149            "`{}` does not parse as an expression: {}",
150            job.target, d.message
151        ))
152    })?;
153    define_probe(&mut graph, parsed)?;
154    let rate = job.rate.unwrap_or(DEFAULT_SAMPLE_RATE);
155    let per_bar = graph.tempo().map(|tempo| tempo.seconds_per_bar());
156    let range = match interval {
157        None => Range::default(),
158        Some((start, end)) => Range {
159            start: start.sample(rate, per_bar)?,
160            end: end.sample(rate, per_bar)?,
161        },
162    };
163    if let Range {
164        start: Some(start),
165        end: Some(end),
166    } = range
167        && end <= start
168    {
169        return Err(CliError::usage(
170            "render.empty_interval",
171            format!(
172                "`{}` reads an interval from sample {start} to {end}, which holds none",
173                job.target
174            ),
175        ));
176    }
177    let until = match job.until {
178        Some(text) => Some(until(text, rate, per_bar)?),
179        None => None,
180    };
181    let mut config = RenderConfig {
182        range,
183        until,
184        ..RenderConfig::at(rate)
185    };
186    config.profile.precision_bits = precision_bits(job.bits)?;
187    config.volatile = job.volatile.to_vec();
188    config.out = job.out;
189    if let Some(threads) = job.threads {
190        config.threads = threads;
191    }
192    config.asks = job
193        .asked
194        .iter()
195        .map(|asked| {
196            Ok(Ask {
197                node: asked.node.clone().unwrap_or_else(|| PROBE.to_string()),
198                representation: asked.representation(rate, per_bar)?,
199            })
200        })
201        .collect::<Result<_, CliError>>()?;
202    let song = sung(&mut graph, PROBE, rate)?;
203    Ok((graph, config, song))
204}
205
206/// An engine refusal as the song names it where a moving lane reaches it, else as written.
207fn refused(song: &Song, graph: &Graph, e: EngineError, target: &str) -> CliError {
208    match as_lane(song, graph, e) {
209        CliError::Engine(e) => CliError::Engine(as_written(e, target)),
210        other => other,
211    }
212}
213
214/// `trace` names one instance of a parameterized file as `<path>(<name>=<value>, ..)`.
215fn instance_call(text: &str) -> Option<(&str, &str)> {
216    let (path, rest) = text.split_once('(')?;
217    let binds = rest.strip_suffix(')')?.trim();
218    (!binds.is_empty() && all_named(binds)).then_some((path, binds))
219}
220
221/// Only commas and equals outside a bind's own parens count: a bind's value may be a call.
222fn all_named(binds: &str) -> bool {
223    let mut depth = 0i32;
224    let mut named = false;
225    for c in binds.chars() {
226        match c {
227            '(' => depth += 1,
228            ')' if depth == 0 => return false,
229            ')' => depth -= 1,
230            '=' if depth == 0 => named = true,
231            ',' if depth == 0 && !std::mem::take(&mut named) => return false,
232            _ => {}
233        }
234    }
235    named && depth == 0
236}
237
238/// The ref an instance name stands for, so `lint` and `trace` answer for the same node.
239fn instance_read(graph: &Graph, text: &str) -> Option<sva_ast::Expr> {
240    let (path, binds) = instance_call(text)?;
241    if !graph.defines(path) {
242        return None;
243    }
244    sva_ast::parse_expr(&format!("@{path}(t, {binds})")).ok()
245}
246
247pub fn execute(job: Job, tier: &Tier) -> Result<Rendered, CliError> {
248    let (graph, config, song) = settle(&job)?;
249    let render = render(&graph, PROBE, config, tier);
250    rendered(&job, (graph, song), render)
251}
252
253/// `job` over `tier`, typing anew only what `session` typed otherwise; only `persist` commits
254/// what it offers a disk beneath.
255pub async fn execute_over<B: Backend>(
256    job: Job<'_>,
257    tier: &Tier<B>,
258    session: &mut Session,
259) -> Result<Rendered, CliError> {
260    let call = tier.call(Kind::Render, job.target, Phase::Parsing);
261    let (graph, config, song) = settle(&job)?;
262    let hooks = (job.abandon, job.hear);
263    let render = render_called(session, &graph, PROBE, config, tier, hooks, call).await;
264    rendered(&job, (graph, song), render)
265}
266
267fn rendered(
268    job: &Job,
269    (graph, song): (Graph, Song),
270    render: Result<sva_engine::Render, EngineError>,
271) -> Result<Rendered, CliError> {
272    let render = render.map_err(|e| refused(&song, &graph, e, job.target))?;
273    Ok(Rendered {
274        warnings: song.warnings().to_vec(),
275        config: render.config.clone(),
276        expression: job.target.to_string(),
277        target: PROBE.to_string(),
278        render,
279        graph,
280    })
281}
282
283/// The render a job would run, and the warnings its song earns.
284pub fn plan(job: &Job) -> Result<(sva_engine::Render, Vec<sva_ast::song::Warning>), CliError> {
285    let (graph, config, song) = settle(job)?;
286    let planned = sva_engine::plan(&graph, PROBE, config);
287    let planned = planned.map_err(|e| refused(&song, &graph, e, job.target))?;
288    Ok((planned, song.warnings().to_vec()))
289}
290
291/// Every refusal typing the target reaches, and no render's; the warnings its song earns.
292pub fn types(job: &Job) -> Result<Vec<sva_ast::song::Warning>, CliError> {
293    let (graph, config, song) = settle(job)?;
294    sva_engine::types_at(&graph, PROBE, config.rate)
295        .map_err(|e| refused(&song, &graph, e, job.target))?;
296    Ok(song.warnings().to_vec())
297}
298
299/// `root`'s song derived and laid over `graph`, as `outline` reads it.
300pub fn song_of(source: &dyn Source, target: &str) -> Result<(Graph, Song), CliError> {
301    let job = Job::over(source, target);
302    let (graph, _, song) = settle(&job)?;
303    Ok((graph, song))
304}
305
306/// `channels`: what it plays, the target's own where `None`.
307pub async fn stream<B: Backend>(
308    job: &Job<'_>,
309    (block, channels): (usize, Option<usize>),
310    tier: &Tier<B>,
311) -> Result<Stream, CliError> {
312    let call = tier.call(Kind::Stream, job.target, Phase::Parsing);
313    let (graph, config, _) = settle(job)?;
314    let target = graph
315        .expr(PROBE)
316        .cloned()
317        .expect("the target was defined as the probe");
318    let config = StreamConfig {
319        block,
320        channels,
321        render: config,
322    };
323    Stream::open_called(&graph, &target, config, tier, call)
324        .await
325        .map_err(|e| CliError::Engine(as_written(e, job.target)))
326}
327
328/// `target`, an expression over `source` with no interval of its own, in place of what
329/// `stream` plays. Each edit here is `sva_engine::change`'s: the stream plays on meanwhile.
330pub async fn edit<B: Backend>(
331    stream: &RefCell<Stream>,
332    source: &dyn Source,
333    target: &str,
334    tier: &Tier<B>,
335) -> Result<(), CliError> {
336    let build = |s: &Stream| {
337        let (graph, expr) = streamed(s, source, target, None)?;
338        Ok(Change::Target(graph, expr))
339    };
340    changed(stream, build, Some(target), tier).await.map(|_| ())
341}
342
343/// `term` summed into the stream's `@notes`, its sample 0 placed `at`.
344pub async fn add<B: Backend>(
345    stream: &RefCell<Stream>,
346    source: &dyn Source,
347    (term, at): (&str, Placed),
348    tier: &Tier<B>,
349) -> Result<Handle, CliError> {
350    let build = |s: &Stream| {
351        let (graph, expr) = streamed(s, source, term, Some(at))?;
352        Ok(Change::Add(graph, expr, at))
353    };
354    match changed(stream, build, Some(term), tier).await? {
355        Changed::Added(handle) => Ok(handle),
356        _ => unreachable!("an add answers its handle"),
357    }
358}
359
360/// False where the stream no longer holds `handle`.
361pub async fn replace<B: Backend>(
362    stream: &RefCell<Stream>,
363    source: &dyn Source,
364    (handle, term, at): (Handle, &str, Placed),
365    tier: &Tier<B>,
366) -> Result<bool, CliError> {
367    let build = |s: &Stream| {
368        let (graph, expr) = streamed(s, source, term, Some(at))?;
369        Ok(Change::Replace(handle, graph, expr, at))
370    };
371    Ok(changed(stream, build, Some(term), tier).await? == Changed::Held(true))
372}
373
374/// False where the stream no longer holds `handle`.
375pub async fn remove<B: Backend>(
376    stream: &RefCell<Stream>,
377    handle: Handle,
378    tier: &Tier<B>,
379) -> Result<bool, CliError> {
380    let build = |_: &Stream| Ok(Change::Remove(handle));
381    Ok(changed(stream, build, None, tier).await? == Changed::Held(true))
382}
383
384async fn changed<B: Backend>(
385    stream: &RefCell<Stream>,
386    build: impl FnMut(&Stream) -> Result<Change, CliError>,
387    text: Option<&str>,
388    tier: &Tier<B>,
389) -> Result<Changed, CliError> {
390    let changed = sva_engine::change(stream, build, tier).await;
391    changed.map_err(|e| match (e, text) {
392        (CliError::Engine(e), Some(text)) => CliError::Engine(as_written(e, text)),
393        (e, _) => e,
394    })
395}
396
397pub async fn fetch<B: Backend>(stream: &RefCell<Stream>, tier: &Tier<B>) {
398    sva_engine::fetch(stream, tier).await;
399}
400
401/// `text` with no interval, and each node it reads that `stream` does not hold, or holds at an
402/// earlier edit of `source`, read and parsed off `source`: an edit is a version the stream plays.
403fn streamed(
404    stream: &Stream,
405    source: &dyn Source,
406    text: &str,
407    term: Option<Placed>,
408) -> Result<(Graph, sva_ast::Expr), CliError> {
409    let Target { expr, interval } = target(text)?;
410    if interval.is_some() {
411        return Err(CliError::usage(
412            "stream.interval",
413            format!("`{text}` reads an interval, and a stream keeps its own"),
414        ));
415    }
416    let held = stream.graph();
417    let roots = roots_of(source, &expr)?;
418    let roots: Vec<&str> = roots.iter().map(String::as_str).collect();
419    let mut delta = sva_ast::load_beside(source, &roots, held).map_err(CliError::Refusals)?;
420    delta
421        .lower_reserved(Some(held))
422        .map_err(CliError::Refusals)?;
423    let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
424        CliError::BadProbe(format!(
425            "`{text}` does not parse as an expression: {}",
426            d.message
427        ))
428    })?;
429    let mut view = held.sourced();
430    for path in delta.paths() {
431        view.set(path, delta.held(path));
432    }
433    let rate = stream.config().render.rate;
434    let lowered = match term {
435        None => {
436            view.set(PROBE, None);
437            define_probe(&mut view, parsed)?;
438            sung(&mut view, PROBE, rate)?;
439            view.expr(PROBE)
440                .cloned()
441                .expect("the target was defined as the probe")
442        }
443        Some(placed) => termed(&mut view, parsed, placed, rate)?,
444    };
445    let mut out = delta;
446    let changed: Vec<String> = view
447        .paths()
448        .filter(|path| !held.holds_as(path, &view) || !held.read_as(path, &view))
449        .map(str::to_string)
450        .collect();
451    for path in changed {
452        out.set(&path, view.held(&path));
453    }
454    Ok((out, lowered))
455}
456
457/// A stream term lowered beside the stream's song: its instances read the song's lanes at
458/// their position, and a term reaching a move of its own refuses, since lanes derive from the
459/// song's files.
460fn termed(
461    view: &mut Graph,
462    term: sva_ast::Expr,
463    placed: Placed,
464    rate: u32,
465) -> Result<sva_ast::Expr, CliError> {
466    const ALONE: &str = "#term";
467    const BESIDE: &str = "#song";
468    let mut alone = view.clone();
469    alone
470        .define_settled(None, ALONE, term)
471        .map_err(CliError::Refusals)?;
472    let written = alone.expr(ALONE).cloned().expect("just defined");
473    let own = alone.song(ALONE).map_err(CliError::Refusals)?;
474    if let Some(lane) = own.lanes().first() {
475        let first = &lane.moves[0].reached;
476        return Err(CliError::Refusals(vec![Refusal::new(
477            format!("{}{}", first.score, sva_ast::score::EXTENSION),
478            sva_ast::ByteSpan::at(0),
479            sva_ast::DiagCode::BadLane,
480            format!(
481                "a stream term reaches this move on `{}`; a term adds sound and never moves, \
482                 since lanes derive from the song's files: move it in one",
483                lane.id.name()
484            ),
485        )]));
486    }
487    let target = view.source_expr(PROBE).expect("a stream holds its target");
488    let both = sva_ast::Expr::Bin(
489        sva_ast::BinOp::Add,
490        Box::new(target),
491        Box::new(written.clone()),
492    );
493    view.define_settled(None, BESIDE, both)
494        .map_err(CliError::Refusals)?;
495    sung(view, BESIDE, rate)?;
496    let Some(sva_ast::Expr::Bin(_, _, lowered)) = view.expr(BESIDE).cloned() else {
497        unreachable!("a sum lowers to a sum")
498    };
499    view.set(BESIDE, None);
500    if placed == Placed::Landing && *lowered != written {
501        return Err(CliError::Refusals(vec![Refusal::new(
502            ALONE,
503            sva_ast::ByteSpan::at(0),
504            sva_ast::DiagCode::BadLane,
505            "this term lands where the stream is when it arrives, so it has no song time, and a \
506             lane of the song moves what it plays; a lane reaches an instance at its song \
507             position, so add it at a written time"
508                .to_string(),
509        )]));
510    }
511    Ok(*lowered)
512}
513
514/// The precision a render writes every sample to: `bits`, else the profile's own.
515pub fn precision_bits(bits: Option<i32>) -> Result<i32, CliError> {
516    bits.map_or(Ok(PSYCHOACOUSTIC_V1.precision_bits), precision)
517}
518
519/// A double holds no bit past its own mantissa, and one bit writes only zero.
520fn precision(bits: i32) -> Result<i32, CliError> {
521    match (2..=52).contains(&bits) {
522        true => Ok(bits),
523        false => Err(CliError::usage(
524            "cli.bad_value",
525            format!("`--bits` takes a precision from 2 to 52 bits, not {bits}"),
526        )),
527    }
528}
529
530/// The engine knows a target only as the node it was defined as, so a refusal that names
531/// that node names the target as the caller wrote it instead.
532fn as_written(refused: EngineError, target: &str) -> EngineError {
533    match refused {
534        EngineError::Refused(mut d) => {
535            if d.location.node == PROBE {
536                d.location.node = target.to_string();
537            }
538            d.message = d
539                .message
540                .replace(&format!("`{PROBE}`"), &format!("`{target}`"));
541            EngineError::Refused(d)
542        }
543        EngineError::Binding { node, span, fault } if node == PROBE => EngineError::Binding {
544            node: target.to_string(),
545            span,
546            fault,
547        },
548        other => other,
549    }
550}
551
552pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
553    execute(Job::over(&Dir::at(dir), expression), &Tier::default())
554}
555
556pub fn cwd() -> Result<std::path::PathBuf, CliError> {
557    std::env::current_dir()
558        .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
559}
560
561pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
562    settled(sva_ast::load(source))
563}
564
565/// Argv math: `trace` and `lint` each refuse a target no node answers for here, under one
566/// code and one message.
567pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
568    let expr = match instance_read(graph, text) {
569        Some(read) => read,
570        None if names_a_missing_node(text) => {
571            return Err(CliError::NotFound(format!(
572                "`{text}` is not a node this composition defines"
573            )));
574        }
575        None => sva_ast::parse_expr(text).map_err(|d| {
576            CliError::BadProbe(format!(
577                "`{text}` is not a node in this composition, and does not parse as an \
578                 expression: {}",
579                d.message
580            ))
581        })?,
582    };
583    define_probe(graph, expr)
584}
585
586/// Only a path, or instance syntax, is a node; the rest is math the parser and engine answer
587/// for. `t` and `A4` are spelled like paths and read as values.
588fn names_a_missing_node(text: &str) -> bool {
589    match instance_call(text) {
590        Some((path, _)) => sva_ast::names_a_node(path),
591        None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
592    }
593}
594
595fn reads_as_a_value(text: &str) -> bool {
596    match sva_ast::parse_expr(text) {
597        Ok(sva_ast::Expr::Lit(_)) => true,
598        Ok(sva_ast::Expr::Var(name)) => sva_ast::is_reserved(&name),
599        _ => false,
600    }
601}
602
603/// Argv math settled the way a file is; `concat` is no file-only dialect.
604fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
605    let defined = graph
606        .define_settled(None, PROBE, expr)
607        .map_err(CliError::Refusals)?;
608    if !defined {
609        return Err(CliError::BadProbe(format!(
610            "this composition already has a node named `{PROBE}`"
611        )));
612    }
613    Ok(())
614}
615
616/// Loaded with its reserved variables lowered; a song lowers its scores once a root is named.
617pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
618    let mut graph = loaded.map_err(CliError::Refusals)?;
619    graph.lower_reserved(None).map_err(CliError::Refusals)?;
620    Ok(graph)
621}
622
623/// A node path the source answers for is a node; anything else is math, and its reads are
624/// roots. Only a node path is asked of the source: math is never looked up as a file.
625pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
626    let mut roots: Vec<String> = RESERVED_VARIABLES.map(sva_ast::variable).to_vec();
627    let listed = source.paths().map_err(CliError::Io)?.found;
628    let variables = format!("{}/", sva_ast::VARIABLES);
629    roots.extend(listed.into_iter().filter(|p| p.starts_with(&variables)));
630    roots.sort();
631    roots.dedup();
632    let held = |path: &str| -> Result<bool, CliError> {
633        let file = format!("{path}{}", sva_ast::score::EXTENSION);
634        let get = |at: &str| {
635            source
636                .get(at)
637                .map(|text| text.is_some())
638                .map_err(CliError::Io)
639        };
640        Ok(sva_ast::whole_ref_path(path) && (get(path)? || get(&file)?))
641    };
642    match target {
643        target if held(target)? => {
644            roots.push(target.to_string());
645        }
646        text => match instance_call(text).map(|(path, _)| path) {
647            Some(path) if held(path)? => {
648                roots.push(path.to_string());
649            }
650            _ => {
651                if let Ok(expr) = sva_ast::parse_expr(text) {
652                    roots.extend(sva_ast::reads_of(PROBE, &expr));
653                }
654            }
655        },
656    }
657    Ok(roots)
658}