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