1use 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
15pub trait Texts {
17 fn text(&self, path: &str) -> Result<Option<String>, String>;
19}
20
21pub struct NoFiles;
23
24impl Texts for NoFiles {
25 fn text(&self, _: &str) -> Result<Option<String>, String> {
26 Ok(None)
27 }
28}
29
30pub 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
40pub 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
93fn 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
134fn 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
184fn 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
209fn 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}