Skip to main content

sva_core/solve/
cost.rs

1// Concern: parses a cost's text, or builds its tree, into leaves, loads and terms, and types it | Non-concern: evaluating it (mod.rs), reading a file (load.rs) | IO: (text or Tree) -> Cost
2
3use sva_ast::{Token, TokenKind, tokenize};
4
5use crate::cli_error::CliError;
6use crate::query::{Asked, asked, call};
7
8/// The readings a cost subtracts, each by the rule `residual.rs` states for it.
9pub const SUBTRACTS: [&str; 7] = [
10    "envelope", "loudness", "bands", "crest", "spectrum", "onsets", "pitch",
11];
12
13pub const REDUCERS: [&str; 5] = ["norm", "sum", "dot", "max", "abs"];
14
15/// How many arguments a reducer takes: `dot` a weight vector and a vector, the rest one.
16pub fn takes(reducer: &str) -> usize {
17    match reducer {
18        "dot" => 2,
19        _ => 1,
20    }
21}
22
23/// The name a leaf of a reading is written under, `repr.bands(...)`.
24pub const REPR: &str = "repr";
25
26/// Each arithmetic operator by name, as a tree names it.
27pub const OPERATORS: [(&str, Op); 4] = [
28    ("add", Op::Add),
29    ("sub", Op::Sub),
30    ("mul", Op::Mul),
31    ("div", Op::Div),
32];
33
34/// A leaf that reads a JSON file instead of rendering: the cost's one file read.
35pub const LOAD_JSON: &str = "load_json";
36
37#[derive(Clone, Debug, PartialEq)]
38pub struct Leaf {
39    /// The render expression, as `sva-cli render` takes it.
40    pub target: String,
41    /// A recording handed over as a value, which `target` reads as `sample("<its key>")`.
42    pub recording: Option<std::sync::Arc<sva_ast::Recording>>,
43    pub asked: Asked,
44    pub written: String,
45}
46
47/// One step of a jq path: `.name` or `['any key']`, or `[3]`.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum Step {
50    Key(String),
51    Index(usize),
52}
53
54/// Where a load's JSON comes from: a file and a path into it, as the CLI writes
55/// `load_json("<file>", "<jq path>")`, or the value itself, as a page hands it over.
56#[derive(Clone, Debug, PartialEq)]
57pub enum From {
58    File {
59        file: String,
60        path: Vec<Step>,
61        path_text: String,
62    },
63    Value(serde_json::Value),
64}
65
66/// A value the cost reads instead of rendering: read and checked once, before any render.
67#[derive(Clone, Debug, PartialEq)]
68pub struct Load {
69    pub from: From,
70    pub written: String,
71}
72
73/// What a load held once read: a number, a vector, or a reading the leaf it subtracts from
74/// decoded.
75#[derive(Clone, Debug, PartialEq)]
76pub enum Held {
77    Number(f64),
78    Vector(Vec<f64>),
79    Reading(sva_engine::Output),
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum Op {
84    Add,
85    Sub,
86    Mul,
87    Div,
88}
89
90#[derive(Clone, Debug, PartialEq)]
91pub enum Node {
92    Leaf(usize),
93    Loaded(usize),
94    Num(f64),
95    Vector(Vec<f64>),
96    Neg(Box<Node>),
97    Bin(Op, Box<Node>, Box<Node>),
98    Reduce(&'static str, Vec<Node>),
99}
100
101#[derive(Clone, Debug, PartialEq)]
102pub struct Term {
103    pub text: String,
104    pub sign: f64,
105    pub node: Node,
106}
107
108#[derive(Clone, Debug, PartialEq)]
109pub struct Cost {
110    pub text: String,
111    pub leaves: Vec<Leaf>,
112    pub loads: Vec<Load>,
113    /// Each top-level `+` or `-` term, which the answer reports on its own.
114    pub terms: Vec<Term>,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq)]
118enum Kind {
119    Reading(usize),
120    /// A load holding a reading, which only a subtraction from a leaf reads.
121    Object(usize),
122    /// A load not read yet: a number, a vector or a reading.
123    Unknown,
124    Vector,
125    Scalar,
126}
127
128fn refused(code: &'static str, message: String) -> CliError {
129    CliError::usage(code, message)
130}
131
132/// Where a cost's text was written, so a refusal can point into it.
133#[derive(Clone, Copy)]
134enum Whence<'a> {
135    Argv,
136    /// A cost file's path; the text is the file's, its comments blanked, so bytes line up.
137    File(&'a str),
138}
139
140impl Whence<'_> {
141    fn refused(self, text: &str, code: &'static str, byte: usize, what: String) -> CliError {
142        match self {
143            Whence::Argv => refused(code, format!("{what}, at byte {byte} of the cost")),
144            Whence::File(file) => {
145                let before = &text[..byte.min(text.len())];
146                let line = before.matches('\n').count() + 1;
147                let column = byte - before.rfind('\n').map_or(0, |at| at + 1) + 1;
148                CliError::Located {
149                    code,
150                    message: format!("{what}, at line {line}, byte {column} of `{file}`"),
151                    file: file.to_string(),
152                    byte,
153                    line,
154                    column,
155                }
156            }
157        }
158    }
159}
160
161pub fn cost(text: &str) -> Result<Cost, CliError> {
162    parsed(text, Whence::Argv)
163}
164
165/// The cost a file holds, `file` its path in the composition: every `;` comment line and
166/// trailing comment is no part of it, and it may run over many lines.
167pub fn cost_file(file: &str, content: &str) -> Result<Cost, CliError> {
168    let blanked: Vec<String> = content
169        .split('\n')
170        .map(|line| {
171            let code = sva_ast::strip_line_comment(line);
172            format!("{code}{}", " ".repeat(line.len() - code.len()))
173        })
174        .collect();
175    let text = blanked.join("\n");
176    if text.trim().is_empty() {
177        return Err(refused(
178            "solve.bad_cost",
179            format!("`{file}` holds no cost, only comments; write one under its annotation line"),
180        ));
181    }
182    let mut cost = parsed(&text, Whence::File(file))?;
183    let spaced = |t: &str| t.split_whitespace().collect::<Vec<_>>().join(" ");
184    cost.text = spaced(&cost.text);
185    for term in &mut cost.terms {
186        term.text = spaced(&term.text);
187    }
188    Ok(cost)
189}
190
191fn parsed(text: &str, whence: Whence) -> Result<Cost, CliError> {
192    let tokens = tokenize(text).map_err(|d| {
193        whence.refused(
194            text,
195            "solve.bad_cost",
196            d.span.start,
197            format!("the cost does not read: {}", d.message),
198        )
199    })?;
200    let mut parser = Parser {
201        whence,
202        text,
203        tokens: &tokens,
204        at: 0,
205        leaves: Vec::new(),
206        loads: Vec::new(),
207        terms: Vec::new(),
208    };
209    let (node, ..) = parser.sum(true)?;
210    if let Some(extra) = tokens.get(parser.at) {
211        return Err(parser.unexpected(extra, "the cost ends here"));
212    }
213    let Parser {
214        leaves,
215        loads,
216        terms,
217        ..
218    } = parser;
219    assembled(text.trim().to_string(), node, terms, leaves, loads)
220}
221
222/// One cost: each top-level term reported apart where every term types to one number, else
223/// the whole as one term, then typed. The text and tree forms both end here.
224fn assembled(
225    text: String,
226    node: Node,
227    terms: Vec<Term>,
228    leaves: Vec<Leaf>,
229    loads: Vec<Load>,
230) -> Result<Cost, CliError> {
231    let mut cost = Cost {
232        text: text.clone(),
233        leaves,
234        loads,
235        terms,
236    };
237    let typing = Typing {
238        leaves: &cost.leaves,
239        loads: &cost.loads,
240        held: None,
241    };
242    let split = cost.terms.iter().all(|t| {
243        typing
244            .kind(&t.node)
245            .is_ok_and(|k| matches!(k, Kind::Scalar | Kind::Unknown))
246    });
247    if !split || cost.terms.is_empty() {
248        cost.terms = vec![Term {
249            text,
250            sign: 1.0,
251            node,
252        }];
253    }
254    cost.typed(None)?;
255    Ok(cost)
256}
257
258impl Cost {
259    /// Every term one number: before the loads are read, `held` is `None` and a load may be
260    /// anything; once read, each is what it held.
261    pub fn typed(&self, held: Option<&[Held]>) -> Result<(), CliError> {
262        let typing = Typing {
263            leaves: &self.leaves,
264            loads: &self.loads,
265            held,
266        };
267        for term in &self.terms {
268            typing.term(term)?;
269        }
270        Ok(())
271    }
272
273    /// The leaf each load subtracts from or is subtracted from, where it is one.
274    pub fn paired(&self) -> Vec<Option<usize>> {
275        let mut out = vec![None; self.loads.len()];
276        for term in &self.terms {
277            pairs(&term.node, &mut out);
278        }
279        out
280    }
281}
282
283fn pairs(node: &Node, out: &mut [Option<usize>]) {
284    match node {
285        Node::Bin(Op::Sub, a, b) => match (&**a, &**b) {
286            (Node::Leaf(x), Node::Loaded(j)) | (Node::Loaded(j), Node::Leaf(x)) => {
287                out[*j] = Some(*x);
288            }
289            _ => {
290                pairs(a, out);
291                pairs(b, out);
292            }
293        },
294        Node::Bin(_, a, b) => {
295            pairs(a, out);
296            pairs(b, out);
297        }
298        Node::Neg(x) => pairs(x, out),
299        Node::Reduce(_, args) => args.iter().for_each(|a| pairs(a, out)),
300        Node::Leaf(_) | Node::Loaded(_) | Node::Num(_) | Node::Vector(_) => {}
301    }
302}
303
304struct Typing<'c> {
305    leaves: &'c [Leaf],
306    loads: &'c [Load],
307    held: Option<&'c [Held]>,
308}
309
310impl Typing<'_> {
311    fn term(&self, term: &Term) -> Result<(), CliError> {
312        match self.kind(&term.node)? {
313            Kind::Scalar | Kind::Unknown => {}
314            Kind::Vector => {
315                return Err(refused(
316                    "solve.vector_cost",
317                    format!(
318                        "`{}` is a vector, and a cost is one number; reduce it, as \
319                         `norm(...)`, `sum(abs(...))`, `max(abs(...))` or `dot([w1, w2], ...)`",
320                        term.text
321                    ),
322                ));
323            }
324            Kind::Reading(leaf) => return Err(unpaired(&self.leaves[leaf])),
325            Kind::Object(load) => return Err(unpaired_load(&self.loads[load])),
326        }
327        Ok(())
328    }
329
330    fn kind(&self, node: &Node) -> Result<Kind, CliError> {
331        let leaves = self.leaves;
332        let loaded = |k: Kind| match k {
333            Kind::Reading(at) => Err(unpaired(&leaves[at])),
334            Kind::Object(at) => Err(unpaired_load(&self.loads[at])),
335            other => Ok(other),
336        };
337        Ok(match node {
338            Node::Leaf(at) => Kind::Reading(*at),
339            Node::Loaded(at) => match self.held.map(|held| &held[*at]) {
340                None => Kind::Unknown,
341                Some(Held::Number(_)) => Kind::Scalar,
342                Some(Held::Vector(_)) => Kind::Vector,
343                Some(Held::Reading(_)) => Kind::Object(*at),
344            },
345            Node::Num(_) => Kind::Scalar,
346            Node::Vector(_) => Kind::Vector,
347            Node::Neg(x) => loaded(self.kind(x)?)?,
348            Node::Bin(op, a, b) => match (self.kind(a)?, self.kind(b)?) {
349                (Kind::Reading(x), Kind::Reading(y)) if *op == Op::Sub => {
350                    let (l, r) = (&leaves[x], &leaves[y]);
351                    if l.asked != r.asked {
352                        return Err(refused(
353                            "solve.mismatched_readings",
354                            format!(
355                                "`{}` and `{}` are not one reading with one set of arguments, \
356                                 so they do not subtract; read both sides alike, as `repr.{}(@a) \
357                                 - repr.{}(sample(\"../ref.wav\"))`",
358                                l.written, r.written, l.asked.name, l.asked.name
359                            ),
360                        ));
361                    }
362                    Kind::Vector
363                }
364                (Kind::Reading(_), Kind::Object(_) | Kind::Unknown)
365                | (Kind::Object(_) | Kind::Unknown, Kind::Reading(_))
366                    if *op == Op::Sub =>
367                {
368                    Kind::Vector
369                }
370                (Kind::Reading(at), _) | (_, Kind::Reading(at)) => {
371                    return Err(unpaired(&leaves[at]));
372                }
373                (Kind::Object(at), _) | (_, Kind::Object(at)) => {
374                    return Err(unpaired_load(&self.loads[at]));
375                }
376                (Kind::Unknown, _) | (_, Kind::Unknown) => Kind::Unknown,
377                (Kind::Scalar, Kind::Scalar) => Kind::Scalar,
378                (Kind::Vector, Kind::Vector) if matches!(op, Op::Add | Op::Sub) => Kind::Vector,
379                (Kind::Vector, Kind::Scalar) | (Kind::Scalar, Kind::Vector)
380                    if matches!(op, Op::Mul) =>
381                {
382                    Kind::Vector
383                }
384                (Kind::Vector, Kind::Scalar) if *op == Op::Div => Kind::Vector,
385                _ => {
386                    return Err(refused(
387                        "solve.bad_cost",
388                        "a vector adds or subtracts only a vector, and scales only by a number; \
389                         reduce it first, as `norm(...)`"
390                            .to_string(),
391                    ));
392                }
393            },
394            Node::Reduce(name, args) => {
395                let kinds = args
396                    .iter()
397                    .map(|a| self.kind(a).and_then(loaded))
398                    .collect::<Result<Vec<_>, _>>()?;
399                let vector = |k: &Kind| matches!(k, Kind::Vector | Kind::Unknown);
400                match (*name, kinds.as_slice()) {
401                    ("abs", [one]) => *one,
402                    ("dot", [w, v]) if vector(w) && vector(v) => Kind::Scalar,
403                    ("norm" | "sum" | "max", [v]) if vector(v) => Kind::Scalar,
404                    _ => {
405                        return Err(refused(
406                            "solve.bad_cost",
407                            format!(
408                                "`{name}` takes {}",
409                                match *name {
410                                    "dot" => "two vectors, `dot([1, 0.5], v)`",
411                                    "abs" => "one number or vector",
412                                    _ => "one vector, as a subtraction of two readings",
413                                }
414                            ),
415                        ));
416                    }
417                }
418            }
419        })
420    }
421}
422
423fn unpaired(leaf: &Leaf) -> CliError {
424    refused(
425        "solve.unpaired_reading",
426        format!(
427            "`{}` is a reading, which a cost subtracts from another of its kind before reducing, \
428             as `norm({} - repr.{}(sample(\"../ref.wav\")))`",
429            leaf.written, leaf.written, leaf.asked.name
430        ),
431    )
432}
433
434/// A leaf's reading, `call` as `--representation` spells it, with only its own arguments.
435fn own_arguments(call_text: &str, written: &str) -> Result<Asked, CliError> {
436    let asked = asked(&call(call_text)?)?;
437    if !SUBTRACTS.contains(&asked.name.as_str()) {
438        return Err(refused(
439            "solve.unsupported_reading",
440            format!(
441                "`{}` is no reading a cost subtracts; it subtracts {}",
442                asked.name,
443                SUBTRACTS.join(", ")
444            ),
445        ));
446    }
447    if asked.node.is_some() || asked.dest.is_some() || asked.brief || asked.skim {
448        return Err(refused(
449            "solve.bad_cost",
450            format!("`{written}` takes only its reading's own arguments"),
451        ));
452    }
453    Ok(asked)
454}
455
456fn unpaired_load(load: &Load) -> CliError {
457    refused(
458        "solve.unpaired_load",
459        format!(
460            "`{}` holds a reading, which a cost subtracts from a `repr.` leaf of that reading \
461             before reducing, as `norm(repr.bands(@a) - {})`; a number or a list of numbers \
462             loads as a weight",
463            load.written, load.written
464        ),
465    )
466}
467
468struct Parser<'a> {
469    whence: Whence<'a>,
470    text: &'a str,
471    tokens: &'a [Token],
472    at: usize,
473    leaves: Vec<Leaf>,
474    loads: Vec<Load>,
475    terms: Vec<Term>,
476}
477
478type Spanned = (Node, usize, usize);
479
480impl Parser<'_> {
481    fn peek(&self) -> Option<&TokenKind> {
482        self.tokens.get(self.at).map(|t| &t.kind)
483    }
484
485    fn end(&self) -> usize {
486        self.at
487            .checked_sub(1)
488            .map_or(0, |at| self.tokens[at].span.end)
489    }
490
491    fn unexpected(&self, token: &Token, wanted: &str) -> CliError {
492        self.whence.refused(
493            self.text,
494            "solve.bad_cost",
495            token.span.start,
496            format!(
497                "`{}`: {wanted}; a cost is `repr.<reading>(<target>, <args>)` and \
498                 `load_json(\"<file>\", \"<jq path>\")` leaves, subtracted in pairs, reduced \
499                 by {} and weighted by numbers",
500                &self.text[token.span.start..token.span.end],
501                REDUCERS.join(", ")
502            ),
503        )
504    }
505
506    fn ended(&self, wanted: &str) -> CliError {
507        self.whence.refused(
508            self.text,
509            "solve.bad_cost",
510            self.text.trim_end().len(),
511            format!("the cost ends where {wanted}"),
512        )
513    }
514
515    fn expect(&mut self, kind: &TokenKind, wanted: &str) -> Result<(), CliError> {
516        match self.tokens.get(self.at) {
517            Some(t) if &t.kind == kind => {
518                self.at += 1;
519                Ok(())
520            }
521            Some(t) => Err(self.unexpected(t, wanted)),
522            None => Err(self.ended(wanted)),
523        }
524    }
525
526    /// `top` records each term, for the answer to report one by one.
527    fn sum(&mut self, top: bool) -> Result<Spanned, CliError> {
528        let first = self.product()?;
529        let start = first.1;
530        let mut parts = vec![(1.0, first)];
531        while let Some(sign) = self.peek().and_then(|k| match k {
532            TokenKind::Plus => Some(1.0),
533            TokenKind::Minus => Some(-1.0),
534            _ => None,
535        }) {
536            self.at += 1;
537            parts.push((sign, self.product()?));
538        }
539        let end = parts.last().map_or(start, |(_, (_, _, e))| *e);
540        if top && parts.len() > 1 {
541            self.terms = parts
542                .iter()
543                .map(|(sign, (node, s, e))| Term {
544                    text: self.text[*s..*e].trim().to_string(),
545                    sign: *sign,
546                    node: node.clone(),
547                })
548                .collect();
549        }
550        let mut parts = parts.into_iter();
551        let (_, (mut node, ..)) = parts.next().expect("a first operand");
552        for (sign, (part, ..)) in parts {
553            let op = if sign > 0.0 { Op::Add } else { Op::Sub };
554            node = Node::Bin(op, Box::new(node), Box::new(part));
555        }
556        Ok((node, start, end))
557    }
558
559    fn product(&mut self) -> Result<Spanned, CliError> {
560        let (mut node, start, mut end) = self.unary()?;
561        while let Some(op) = self.peek().and_then(|k| match k {
562            TokenKind::Star => Some(Op::Mul),
563            TokenKind::Slash => Some(Op::Div),
564            _ => None,
565        }) {
566            self.at += 1;
567            let (rhs, _, e) = self.unary()?;
568            end = e;
569            node = Node::Bin(op, Box::new(node), Box::new(rhs));
570        }
571        Ok((node, start, end))
572    }
573
574    fn unary(&mut self) -> Result<Spanned, CliError> {
575        let start = self.tokens.get(self.at).map_or(0, |t| t.span.start);
576        if self.peek() == Some(&TokenKind::Minus) {
577            self.at += 1;
578            let (x, _, end) = self.unary()?;
579            return Ok((Node::Neg(Box::new(x)), start, end));
580        }
581        let node = self.primary()?;
582        Ok((node, start, self.end()))
583    }
584
585    fn primary(&mut self) -> Result<Node, CliError> {
586        let Some(token) = self.tokens.get(self.at) else {
587            return Err(self.ended("a value was expected"));
588        };
589        self.at += 1;
590        match &token.kind {
591            TokenKind::Num(n) => Ok(Node::Num(*n)),
592            TokenKind::LParen => {
593                let (inner, ..) = self.sum(false)?;
594                self.expect(&TokenKind::RParen, "a `(` closes here")?;
595                Ok(inner)
596            }
597            TokenKind::LBracket => self.vector(),
598            TokenKind::Ident(name) if name == REPR => self.leaf(token.span.start),
599            TokenKind::Ident(name) if name == LOAD_JSON => self.load(token.span.start),
600            TokenKind::Ident(name) => match REDUCERS.iter().find(|r| **r == name) {
601                Some(reducer) => {
602                    self.expect(&TokenKind::LParen, "a reducer is called, as `norm(...)`")?;
603                    let mut args = vec![self.sum(false)?.0];
604                    while self.peek() == Some(&TokenKind::Comma) {
605                        self.at += 1;
606                        args.push(self.sum(false)?.0);
607                    }
608                    self.expect(&TokenKind::RParen, "its `)` closes here")?;
609                    Ok(Node::Reduce(reducer, args))
610                }
611                None => Err(self.unexpected(token, "no such reducer or leaf")),
612            },
613            _ => Err(self.unexpected(token, "a value was expected")),
614        }
615    }
616
617    fn vector(&mut self) -> Result<Node, CliError> {
618        let mut out = Vec::new();
619        loop {
620            let sign = match self.peek() {
621                Some(TokenKind::Minus) => {
622                    self.at += 1;
623                    -1.0
624                }
625                _ => 1.0,
626            };
627            match self.tokens.get(self.at) {
628                Some(Token {
629                    kind: TokenKind::Num(n),
630                    ..
631                }) => out.push(sign * n),
632                Some(t) => return Err(self.unexpected(t, "a weight vector holds numbers")),
633                None => return Err(self.ended("a weight vector's `]` was expected")),
634            }
635            self.at += 1;
636            match self.peek() {
637                Some(TokenKind::Comma) => self.at += 1,
638                Some(TokenKind::RBracket) => {
639                    self.at += 1;
640                    return Ok(Node::Vector(out));
641                }
642                _ => return Err(self.ended("a weight vector's `,` or `]` was expected")),
643            }
644        }
645    }
646
647    fn path_text(&mut self, wanted: &str) -> Result<(String, usize), CliError> {
648        match self.tokens.get(self.at) {
649            Some(Token {
650                kind: TokenKind::Str(text),
651                span,
652            }) => {
653                self.at += 1;
654                Ok((text.clone(), span.start))
655            }
656            Some(t) => Err(self.unexpected(t, wanted)),
657            None => Err(self.ended(wanted)),
658        }
659    }
660
661    /// `load_json("<file>", "<jq path>")`.
662    fn load(&mut self, start: usize) -> Result<Node, CliError> {
663        let wanted = "`load_json` takes a file and a jq path, as `load_json(\"crop.json\", \
664                      \".readings['envelope(frame=10ms)']\")`";
665        self.expect(&TokenKind::LParen, wanted)?;
666        let (file, _) = self.path_text(wanted)?;
667        self.expect(&TokenKind::Comma, wanted)?;
668        let (path_text, at) = self.path_text(wanted)?;
669        self.expect(&TokenKind::RParen, wanted)?;
670        let written = self.text[start..self.end()].to_string();
671        let path = jq_path(&path_text).map_err(|why| {
672            self.whence.refused(
673                self.text,
674                "solve.bad_path",
675                at,
676                format!(
677                    "`{path_text}` is no jq path: {why}; a path is only `.name` steps, `[3]` \
678                     indices and `['any key']` for a key that is not a plain name, as \
679                     `.readings['envelope(frame=10ms)']`, and none of jq's filters"
680                ),
681            )
682        })?;
683        self.loads.push(Load {
684            from: From::File {
685                file,
686                path,
687                path_text,
688            },
689            written,
690        });
691        Ok(Node::Loaded(self.loads.len() - 1))
692    }
693
694    /// `repr.<reading>(<target>, <k>=<v>...)`: the target is the text up to the first comma
695    /// outside every bracket, so an interval `[0, 2s)` stays in it.
696    fn leaf(&mut self, start: usize) -> Result<Node, CliError> {
697        self.expect(&TokenKind::Dot, "`repr` is followed by `.<reading>`")?;
698        let name = match self.tokens.get(self.at).map(|t| &t.kind) {
699            Some(TokenKind::Ident(name)) => name.clone(),
700            _ => return Err(self.ended("a reading's name follows `repr.`")),
701        };
702        self.at += 1;
703        if !SUBTRACTS.contains(&name.as_str()) {
704            return Err(refused(
705                "solve.unsupported_reading",
706                format!(
707                    "`repr.{name}` is no reading a cost subtracts; it subtracts {}",
708                    SUBTRACTS.join(", ")
709                ),
710            ));
711        }
712        let open = self.at;
713        self.expect(&TokenKind::LParen, "a reading is called on its target")?;
714        let mut depth = 1;
715        let mut pieces = Vec::new();
716        let mut from = self.tokens[open].span.end;
717        while depth > 0 {
718            let Some(token) = self.tokens.get(self.at) else {
719                return Err(self.ended(&format!("`repr.{name}(`'s `)` was expected")));
720            };
721            match token.kind {
722                TokenKind::LParen | TokenKind::LBracket => depth += 1,
723                TokenKind::RParen | TokenKind::RBracket => depth -= 1,
724                TokenKind::Comma if depth == 1 => {
725                    pieces.push(self.text[from..token.span.start].trim().to_string());
726                    from = token.span.end;
727                }
728                _ => {}
729            }
730            if depth == 0 {
731                pieces.push(self.text[from..token.span.start].trim().to_string());
732            }
733            self.at += 1;
734        }
735        let written = self.text[start..self.end()].to_string();
736        let mut pieces = pieces.into_iter();
737        let target = pieces.next().filter(|t| !t.is_empty()).ok_or_else(|| {
738            refused(
739                "solve.bad_cost",
740                format!("`{written}` reads no target; write one, as `repr.{name}(@master)`"),
741            )
742        })?;
743        let args: Vec<String> = pieces.collect();
744        let asked = own_arguments(&format!("{name}({})", args.join(", ")), &written)?;
745        self.leaves.push(Leaf {
746            target,
747            recording: None,
748            asked,
749            written,
750        });
751        Ok(Node::Leaf(self.leaves.len() - 1))
752    }
753}
754
755/// `.a.b`, `[3]`, `['any key']` or `.['any key']`; `.` alone is the whole document.
756pub fn jq_path(text: &str) -> Result<Vec<Step>, String> {
757    let mut steps = Vec::new();
758    let mut rest = text.trim();
759    if rest == "." {
760        return Ok(steps);
761    }
762    if !rest.starts_with(['.', '[']) {
763        return Err("it does not start with `.` or `[`".to_string());
764    }
765    while !rest.is_empty() {
766        if let Some(after) = rest.strip_prefix('.') {
767            if after.starts_with('[') {
768                rest = after;
769                continue;
770            }
771            let end = after
772                .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
773                .unwrap_or(after.len());
774            let name = &after[..end];
775            if name.is_empty() || name.starts_with(|c: char| c.is_ascii_digit()) {
776                return Err(format!(
777                    "`.{}` names no key; write one that is not a plain name in brackets, \
778                     `['{}']`",
779                    after.split(['.', '[']).next().unwrap_or(after),
780                    after.split(['.', '[']).next().unwrap_or(after)
781                ));
782            }
783            steps.push(Step::Key(name.to_string()));
784            rest = &after[end..];
785        } else if let Some(after) = rest.strip_prefix("['") {
786            let Some(close) = after.find("']") else {
787                return Err(format!("`['{after}` is never closed by `']`"));
788            };
789            steps.push(Step::Key(after[..close].to_string()));
790            rest = &after[close + 2..];
791        } else if let Some(after) = rest.strip_prefix('[') {
792            let Some(close) = after.find(']') else {
793                return Err(format!("`[{after}` is never closed by `]`"));
794            };
795            let index = after[..close].trim().parse::<usize>().map_err(|_| {
796                format!(
797                    "`[{}]` is no index from 0; a key goes in single quotes, `['{}']`",
798                    &after[..close],
799                    &after[..close]
800                )
801            })?;
802            steps.push(Step::Index(index));
803            rest = &after[close + 1..];
804        } else {
805            return Err(format!("`{rest}` is no `.name`, `[3]` or `['key']` step"));
806        }
807    }
808    Ok(steps)
809}
810
811/// What a leaf reads: a render expression, or a recording handed over as its samples.
812#[derive(Clone, Debug, PartialEq)]
813pub enum Target {
814    Text(String),
815    Samples { rate: u32, planes: Vec<Vec<f32>> },
816}
817
818/// A cost as a tree, as a front end with no text to parse builds one: each node one form,
819/// every reference a value. [`built`] makes the same [`Cost`] the text grammar does.
820#[derive(Clone, Debug, PartialEq)]
821pub enum Tree {
822    Num(f64),
823    Weights(Vec<f64>),
824    /// `call` as `--representation` spells it, `envelope(frame=10ms)`.
825    Repr {
826        call: String,
827        target: Target,
828    },
829    Value(serde_json::Value),
830    Neg(Box<Tree>),
831    Bin(Op, Box<Tree>, Box<Tree>),
832    /// One of [`REDUCERS`], with the arguments [`takes`] says.
833    Reduce(&'static str, Vec<Tree>),
834}
835
836/// `node` as the text grammar writes it, an operand of a sum or product in parentheses.
837fn printed(node: &Node, leaves: &[Leaf], loads: &[Load]) -> String {
838    let whole = |n: &Node| printed(n, leaves, loads);
839    let each = |n: &Node| match n {
840        Node::Bin(..) => format!("({})", whole(n)),
841        _ => whole(n),
842    };
843    match node {
844        Node::Leaf(at) => leaves[*at].written.clone(),
845        Node::Loaded(at) => loads[*at].written.clone(),
846        Node::Num(n) => format!("{n}"),
847        Node::Vector(v) => {
848            let v: Vec<String> = v.iter().map(|x| format!("{x}")).collect();
849            format!("[{}]", v.join(", "))
850        }
851        Node::Neg(x) => format!("-({})", each(x)),
852        Node::Bin(op, a, b) => {
853            let op = match op {
854                Op::Add => "+",
855                Op::Sub => "-",
856                Op::Mul => "*",
857                Op::Div => "/",
858            };
859            format!("{} {op} {}", each(a), each(b))
860        }
861        Node::Reduce(name, args) => {
862            let args: Vec<String> = args.iter().map(whole).collect();
863            format!("{name}({})", args.join(", "))
864        }
865    }
866}
867
868struct Building {
869    leaves: Vec<Leaf>,
870    loads: Vec<Load>,
871}
872
873impl Building {
874    fn node(&mut self, tree: Tree) -> Result<Node, CliError> {
875        Ok(match tree {
876            Tree::Num(n) => Node::Num(n),
877            Tree::Weights(v) => Node::Vector(v),
878            Tree::Neg(x) => Node::Neg(Box::new(self.node(*x)?)),
879            Tree::Bin(op, a, b) => {
880                Node::Bin(op, Box::new(self.node(*a)?), Box::new(self.node(*b)?))
881            }
882            Tree::Reduce(reducer, args) => {
883                let args = args
884                    .into_iter()
885                    .map(|a| self.node(a))
886                    .collect::<Result<Vec<_>, _>>()?;
887                Node::Reduce(reducer, args)
888            }
889            Tree::Value(value) => {
890                let shape = crate::decode::shape(&value);
891                self.loads.push(Load {
892                    written: format!("value {} ({shape})", self.loads.len()),
893                    from: From::Value(value),
894                });
895                Node::Loaded(self.loads.len() - 1)
896            }
897            Tree::Repr { call, target } => {
898                let (target, recording) = match target {
899                    Target::Text(text) if text.trim().is_empty() => {
900                        return Err(refused(
901                            "solve.bad_cost",
902                            format!("`{call}` reads no target; give it one, as `@master`"),
903                        ));
904                    }
905                    Target::Text(text) => (text, None),
906                    Target::Samples { rate, planes } => {
907                        let key = format!("#recording-{}", self.leaves.len());
908                        let recording = sva_ast::Recording {
909                            path: key.clone(),
910                            rate,
911                            planes: std::sync::Arc::new(planes),
912                        };
913                        (
914                            format!("sample(\"{key}\")"),
915                            Some(std::sync::Arc::new(recording)),
916                        )
917                    }
918                };
919                let shown = match &recording {
920                    Some(r) => format!("<recording at {} Hz>", r.rate),
921                    None => target.clone(),
922                };
923                let probe = self::call(&call)?;
924                asked(&probe)?;
925                let args: Vec<String> =
926                    probe.args.iter().map(|(k, v)| format!("{k}={v}")).collect();
927                let args = args.join(", ");
928                let written = match args.is_empty() {
929                    true => format!("repr.{}({shown})", probe.name),
930                    false => format!("repr.{}({shown}, {args})", probe.name),
931                };
932                let asked = own_arguments(&call, &written)?;
933                self.leaves.push(Leaf {
934                    target,
935                    recording,
936                    asked,
937                    written,
938                });
939                Node::Leaf(self.leaves.len() - 1)
940            }
941        })
942    }
943}
944
945/// Each top-level `+` or `-` operand, with its sign, as the text grammar splits them.
946fn split(node: Node, out: &mut Vec<(f64, Node)>) {
947    match node {
948        Node::Bin(Op::Add, a, b) => {
949            split(*a, out);
950            out.push((1.0, *b));
951        }
952        Node::Bin(Op::Sub, a, b) => {
953            split(*a, out);
954            out.push((-1.0, *b));
955        }
956        other => out.push((1.0, other)),
957    }
958}
959
960/// `tree` as the [`Cost`] its text would parse to, typed by the same rules.
961pub fn built(tree: Tree) -> Result<Cost, CliError> {
962    let mut building = Building {
963        leaves: Vec::new(),
964        loads: Vec::new(),
965    };
966    let node = building.node(tree)?;
967    let Building { leaves, loads } = building;
968    let text = printed(&node, &leaves, &loads);
969    let mut parts = Vec::new();
970    split(node.clone(), &mut parts);
971    let terms = match parts.len() {
972        1 => Vec::new(),
973        _ => parts
974            .into_iter()
975            .map(|(sign, node)| Term {
976                text: printed(&node, &leaves, &loads),
977                sign,
978                node,
979            })
980            .collect(),
981    };
982    assembled(text, node, terms, leaves, loads)
983}