1use 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
14pub trait Texts {
16 fn text(&self, path: &str) -> Result<Option<String>, String>;
18}
19
20pub struct NoFiles;
22
23impl Texts for NoFiles {
24 fn text(&self, _: &str) -> Result<Option<String>, String> {
25 Ok(None)
26 }
27}
28
29pub 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
39pub 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
92fn 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
133fn 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
183fn 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
208fn 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}