Skip to main content

sva_core/solve/
load.rs

1// Concern: reads each load of a cost once, before any render, checking a reading against its leaf | Non-concern: one reading's JSON (decode.rs), the cost | IO: (Cost, files) -> Vec<Held>
2
3use std::collections::BTreeMap;
4
5use serde_json::Value;
6use sva_ast::Source;
7use sva_engine::fitted_frame;
8
9use super::cost::{Cost, From, Held, Leaf, Load, Step};
10use crate::cli_error::CliError;
11use crate::decode::{reading, shape};
12use crate::query::{Call, Spelled, Taken, asked};
13use crate::{Job, planned};
14
15/// Where `load_json` finds a file's text: in the CLI, relative to the directory it runs in.
16pub trait Texts {
17    /// `Ok(None)` is no file there; `Err` is one that could not be read as text.
18    fn text(&self, path: &str) -> Result<Option<String>, String>;
19}
20
21/// No file at all: a front end with no filesystem hands each value over instead.
22pub struct NoFiles;
23
24impl Texts for NoFiles {
25    fn text(&self, _: &str) -> Result<Option<String>, String> {
26        Ok(None)
27    }
28}
29
30/// What the render side of a reading decides, which a loaded reading must match.
31pub struct Side<'a> {
32    pub source: &'a dyn Source,
33    pub rate: Option<u32>,
34}
35
36fn refused(code: &'static str, message: String) -> CliError {
37    CliError::usage(code, message)
38}
39
40/// Each load's value, every file read and parsed once; a reading decoded by its leaf and
41/// checked against what that leaf renders.
42pub fn loaded(cost: &Cost, texts: &dyn Texts, side: &Side) -> Result<Vec<Held>, CliError> {
43    let mut files: BTreeMap<&str, Value> = BTreeMap::new();
44    let paired = cost.paired();
45    let mut out = Vec::with_capacity(cost.loads.len());
46    for (load, pair) in cost.loads.iter().zip(paired) {
47        let value = match &load.from {
48            From::Value(value) => value,
49            From::File { file, path, .. } => {
50                if !files.contains_key(file.as_str()) {
51                    files.insert(file, parsed(file, texts)?);
52                }
53                walked(&files[file.as_str()], load, path)?
54            }
55        };
56        out.push(held(load, value, pair.map(|at| &cost.leaves[at]), side)?);
57    }
58    Ok(out)
59}
60
61fn parsed(file: &str, texts: &dyn Texts) -> Result<Value, CliError> {
62    let text = texts
63        .text(file)
64        .map_err(|why| {
65            refused(
66                "solve.bad_json",
67                format!("`{file}` could not be read: {why}"),
68            )
69        })?
70        .ok_or_else(|| {
71            CliError::NotFound(format!(
72                "`load_json(\"{file}\", ...)` names no file: a path is relative to the directory \
73                 sva-cli runs in, or absolute"
74            ))
75        })?;
76    serde_json::from_str(&text).map_err(|e| {
77        refused(
78            "solve.bad_json",
79            format!("`{file}` is no JSON: {e}; `load_json` reads one JSON document"),
80        )
81    })
82}
83
84fn where_in(load: &Load) -> String {
85    match &load.from {
86        From::File {
87            file, path_text, ..
88        } => format!("`{file}` at `{path_text}`"),
89        From::Value(_) => format!("`{}`", load.written),
90    }
91}
92
93/// The value `path` names; a step that finds nothing names what is there instead.
94fn walked<'v>(root: &'v Value, load: &Load, path: &[Step]) -> Result<&'v Value, CliError> {
95    let From::File { file, .. } = &load.from else {
96        unreachable!("only a file is walked");
97    };
98    let mut at = root;
99    let mut reached = String::new();
100    for step in path {
101        let next = match (step, at) {
102            (Step::Key(key), Value::Object(map)) => map.get(key),
103            (Step::Index(k), Value::Array(list)) => list.get(*k),
104            _ => None,
105        };
106        let Some(next) = next else {
107            let held = match at {
108                Value::Object(map) => {
109                    let keys: Vec<String> = map.keys().take(24).map(|k| written_key(k)).collect();
110                    format!("holds the keys {}", keys.join(", "))
111                }
112                Value::Array(list) => format!("is a list of {} items, from `[0]`", list.len()),
113                other => format!("is {}", shape(other)),
114            };
115            let step = match step {
116                Step::Key(key) => written_key(key),
117                Step::Index(k) => format!("[{k}]"),
118            };
119            let here = if reached.is_empty() { "." } else { &reached };
120            return Err(refused(
121                "solve.no_such_key",
122                format!("`{file}` has no `{step}` under `{here}`: `{here}` {held}"),
123            ));
124        };
125        reached.push_str(&match step {
126            Step::Key(key) => written_key(key),
127            Step::Index(k) => format!("[{k}]"),
128        });
129        at = next;
130    }
131    Ok(at)
132}
133
134/// A key as a path step spells it: `.name`, else `['any key']`.
135fn written_key(key: &str) -> String {
136    let plain = key.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
137        && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
138    match plain {
139        true => format!(".{key}"),
140        false => format!("['{key}']"),
141    }
142}
143
144fn held(load: &Load, value: &Value, leaf: Option<&Leaf>, side: &Side) -> Result<Held, CliError> {
145    let here = where_in(load);
146    match value {
147        Value::Number(n) => Ok(Held::Number(n.as_f64().unwrap_or(f64::NAN))),
148        Value::Array(list) => list
149            .iter()
150            .map(Value::as_f64)
151            .collect::<Option<Vec<f64>>>()
152            .map(Held::Vector)
153            .ok_or_else(|| {
154                refused(
155                    "solve.bad_load",
156                    format!(
157                        "{here} is a list that holds more than numbers; a list loads as a \
158                         vector of numbers"
159                    ),
160                )
161            }),
162        Value::Object(_) => match leaf {
163            Some(leaf) => decoded(value, leaf, &here, side).map(Held::Reading),
164            None => Err(refused(
165                "solve.unpaired_load",
166                format!(
167                    "{here} holds an object, which loads only as the reading a `repr.` leaf \
168                     subtracts, as `norm(repr.bands(@a) - {})`",
169                    load.written
170                ),
171            )),
172        },
173        other => Err(refused(
174            "solve.bad_load",
175            format!(
176                "{here} is {}; a load holds a number, a list of numbers, or a reading as \
177                 `analyze` prints its `data`",
178                shape(other)
179            ),
180        )),
181    }
182}
183
184/// The first `has_more: true` under `value`, where a list was cut short.
185fn cut_short(value: &Value, path: &str) -> Option<String> {
186    match value {
187        Value::Object(map) => {
188            if map.get("has_more") == Some(&Value::Bool(true)) {
189                return Some(path.to_string());
190            }
191            map.iter()
192                .find_map(|(k, v)| cut_short(v, &format!("{path}{}", written_key(k))))
193        }
194        Value::Array(list) => list
195            .iter()
196            .enumerate()
197            .find_map(|(k, v)| cut_short(v, &format!("{path}[{k}]"))),
198        _ => None,
199    }
200}
201
202fn mismatched(here: &str, leaf: &Leaf, what: String) -> CliError {
203    refused(
204        "solve.mismatched_load",
205        format!("{here} and `{}` differ: {what}", leaf.written),
206    )
207}
208
209/// `value` is analyze's `data`: its interval, rate and profile beside `representations`.
210fn decoded(
211    value: &Value,
212    leaf: &Leaf,
213    here: &str,
214    side: &Side,
215) -> Result<sva_engine::Output, CliError> {
216    let name = &leaf.asked.name;
217    let Some(readings) = value.get("representations").and_then(Value::as_object) else {
218        let keys: Vec<String> = value
219            .as_object()
220            .map(|map| map.keys().take(24).map(|k| written_key(k)).collect())
221            .unwrap_or_default();
222        return Err(refused(
223            "solve.bad_load",
224            format!(
225                "{here} holds {} and no `representations`; a reading loads from `analyze`'s \
226                 `data`, the object holding `interval` and `representations`: from a bench \
227                 download `.readings.{name}`, from a reading `analyze` wrote to a file `.data`",
228                keys.join(", ")
229            ),
230        ));
231    };
232    if let Some(at) = cut_short(value, "") {
233        return Err(refused(
234            "solve.cut_short",
235            format!(
236                "{here} was cut short at `{at}` (`has_more: true`); write the reading whole, \
237                 to a file, as `--representation {name}=ref.json`, or pass `limit: null`"
238            ),
239        ));
240    }
241    let Some(answer) = readings.get(name.as_str()) else {
242        let held: Vec<&str> = readings.keys().map(String::as_str).collect();
243        return Err(mismatched(
244            here,
245            leaf,
246            format!("it holds {} and no `{name}`", held.join(", ")),
247        ));
248    };
249    let field = |key: &str| answer.get(key).or_else(|| value.get(key));
250    let Some((theirs_name, theirs_args)) = answer
251        .get("representation")
252        .and_then(Value::as_str)
253        .zip(answer.get("args").and_then(Value::as_object))
254    else {
255        return Err(refused(
256            "solve.load_without_args",
257            format!(
258                "{here} holds `{name}` with no `representation` and `args`, what it was read \
259                 with, so its arguments cannot be checked; it was printed before readings \
260                 carried them: read it again"
261            ),
262        ));
263    };
264    if theirs_name != name {
265        return Err(mismatched(
266            here,
267            leaf,
268            format!("it holds a `{theirs_name}` reading under `{name}`"),
269        ));
270    }
271    let job = Job {
272        rate: side.rate,
273        ..Job::over(side.source, &leaf.target)
274    };
275    let (render, _, per_bar) = planned(&job)?;
276    let rate = render.config.rate;
277    let profile = render.config.profile.name;
278    if field("source").and_then(Value::as_str) != Some("measured") {
279        return Err(mismatched(
280            here,
281            leaf,
282            "it was not measured off samples (`source` is not `measured`), and a cost reads \
283             every leaf off samples; read it as `sample(<target>)`"
284                .to_string(),
285        ));
286    }
287    for (key, at) in [("sample_rate", value), ("rate", answer)] {
288        let theirs = at.get(key).and_then(Value::as_u64);
289        if theirs != Some(u64::from(rate)) {
290            let theirs = theirs.map_or("no rate".to_string(), |r| format!("{r} Hz"));
291            return Err(mismatched(
292                here,
293                leaf,
294                format!(
295                    "it was read at {theirs}, and the solve renders at {rate} Hz; solve at its \
296                     rate, `--rate` (a page's `rate` option)"
297                ),
298            ));
299        }
300    }
301    for at in [value, answer] {
302        let theirs = at.get("profile").and_then(Value::as_str);
303        if theirs != Some(profile) {
304            return Err(mismatched(
305                here,
306                leaf,
307                format!(
308                    "it was read under the profile `{}`, and the solve renders under `{profile}`",
309                    theirs.unwrap_or("none")
310                ),
311            ));
312        }
313    }
314    let span = |key: &str| {
315        value
316            .get("interval")
317            .and_then(|i| i.get(key))
318            .and_then(Value::as_f64)
319    };
320    let theirs = match (span("start_secs"), span("end_secs")) {
321        (Some(start), Some(end)) => Some(((end - start) * f64::from(rate)).round()),
322        _ => None,
323    };
324    let ours = render.range.map(|r| r.len() as f64);
325    if theirs.is_none() || theirs != ours {
326        let samples = |n: Option<f64>| {
327            n.map_or("no interval".to_string(), |n| {
328                format!("{n} samples ({} s)", n / f64::from(rate))
329            })
330        };
331        return Err(mismatched(
332            here,
333            leaf,
334            format!(
335                "it reads {}, and the leaf's target renders {}; frames past the shorter side \
336                 would count as silence, so crop the target to the same length",
337                samples(theirs),
338                samples(ours)
339            ),
340        ));
341    }
342    let taken = Taken::Measured {
343        fitted_frame: render.range.map(|r| fitted_frame(r.len())),
344    };
345    let mut written = Vec::with_capacity(theirs_args.len());
346    for (key, value) in theirs_args {
347        match value {
348            Value::String(text) => written.push((key.clone(), text.clone())),
349            Value::Number(n) => written.push((key.clone(), n.to_string())),
350            Value::Null => {}
351            other => {
352                return Err(refused(
353                    "solve.bad_reading",
354                    format!(
355                        "{here} holds `{name}` with `args.{key}` {}; an argument is a number or a \
356                         string as `--representation` spells it, or null",
357                        shape(other)
358                    ),
359                ));
360            }
361        }
362    }
363    let theirs = asked(&Call {
364        name: name.clone(),
365        args: written,
366        dest: None,
367    })
368    .and_then(|one| one.args(rate, None, taken))
369    .map_err(|e| {
370        refused(
371            "solve.bad_reading",
372            format!("{here} holds `{name}` with `args` its reading refuses: {e}"),
373        )
374    })?;
375    let ours = leaf.asked.args(rate, per_bar, taken)?;
376    let spelled = |v: &Option<Spelled>| {
377        v.as_ref()
378            .map_or("none".to_string(), |v| format!("`{}`", v.text()))
379    };
380    for ((key, theirs), (_, ours)) in theirs.iter().zip(&ours) {
381        if theirs != ours {
382            return Err(mismatched(
383                here,
384                leaf,
385                format!(
386                    "its `{key}` is {}, and the leaf's is {}",
387                    spelled(theirs),
388                    spelled(ours)
389                ),
390            ));
391        }
392    }
393    let Some(inner) = answer.get("value") else {
394        return Err(refused(
395            "solve.bad_reading",
396            format!("{here} holds `{name}` with no `value`"),
397        ));
398    };
399    let representation = leaf.asked.representation(rate, per_bar)?;
400    let path = format!("representations.{name}.value");
401    reading(representation, inner, &path)
402        .map_err(|why| refused("solve.bad_reading", format!("{here}: {why}")))
403}