1mod analyze;
4mod answer;
5mod builtins;
6mod cli_error;
7mod decode;
8pub mod encode;
9pub mod json;
10mod lint_code;
11mod outline;
12mod output;
13mod query;
14mod set;
15pub mod solve;
16mod song;
17mod target;
18mod until;
19
20pub use analyze::{analyzable, analyzed};
21pub use answer::{
22 Played, Printed, Report, SAMPLE_LIMIT, answer_json, counters_json, engine_stats_json,
23 label_json, query_data, resampled_json, stats_json, stream_stats_json, value_json, work_json,
24};
25pub use builtins::{Builtins, Callable, Crossing, builtins, builtins_data};
26pub use cli_error::{CliError, LintViolation, lint_diagnostic, warning_diagnostic};
27pub use lint_code::LintCode;
28pub use outline::{events_data, outline_data, song_data};
29pub use output::{
30 Diagnostic, Severity, diagnostics_json, error_envelope, error_envelope_with, success_envelope,
31};
32pub use query::{
33 Asked, Call, Destination, RETIRED, Spelled, Taken, asked, asked_all, call, calls, is_wav,
34 retired, seconds,
35};
36pub use set::{overlaid, set};
37pub use song::{as_lane, sung};
38pub use sva_ast::RESERVED_VARIABLES;
39pub use target::{Edge, Target, target};
40pub use until::until;
41
42use std::cell::RefCell;
43use std::num::NonZeroUsize;
44use std::path::Path;
45
46use sva_ast::song::Song;
47use sva_ast::{Dir, Graph, Refusal, Source};
48use sva_engine::{
49 Ask, Change, Changed, DEFAULT_SAMPLE_RATE, EngineError, Kind, PSYCHOACOUSTIC_V1, Phase, Range,
50 RenderConfig, StreamConfig, render, render_called,
51};
52
53pub use sva_engine::{Abandon, Handle, Hear, Never, Out, Placed, Session, Stream, Until};
54
55pub use sva_engine::{Answer, Extent, Label, Output, Representation};
56pub use sva_engine::{
57 Backend, CacheStats, Counters, DEFAULT_CACHE_BYTES, DEFAULT_STORE_BYTES, EngineStats,
58 FETCH_READS, Nothing, Persisted, Store, Tier,
59};
60
61pub const ROOT: &str = "master";
62pub const PROBE: &str = "probe";
63pub const RECORDING_UNREADABLE: &str = "recording.unreadable";
64
65pub struct Rendered {
66 pub config: RenderConfig,
67 pub warnings: Vec<sva_ast::song::Warning>,
69 pub expression: String,
70 pub target: String,
71 pub render: sva_engine::Render,
72 pub graph: Graph,
74}
75
76impl Rendered {
77 pub fn answer(&self, node: &str, representation: Representation) -> Result<Answer, CliError> {
78 let written = |e| CliError::Engine(as_written(e, &self.expression));
79 let id = self.render.node(node).map_err(written)?;
80 let mut answer = sva_engine::answer(&self.render, id, representation).map_err(written)?;
81 self.attribute(&mut answer)?;
82 Ok(answer)
83 }
84
85 pub fn seconds_per_bar(&self) -> Option<f64> {
87 self.graph.tempo().map(|tempo| tempo.seconds_per_bar())
88 }
89
90 pub fn answered(&self, asked: &Asked) -> Result<Answer, CliError> {
92 let representation = asked.representation(self.config.rate, self.seconds_per_bar())?;
93 self.answer(
94 asked.node.as_deref().unwrap_or(&self.target),
95 representation,
96 )
97 }
98
99 fn attribute(&self, answer: &mut Answer) -> Result<(), CliError> {
101 let Output::Alias(alias) = &mut answer.value else {
102 return Ok(());
103 };
104 alias.instances = self.render.tys.paths().count();
105 Ok(())
106 }
107
108 pub fn label(&self) -> Option<&sva_engine::Label> {
109 self.render.labels.get(&self.render.root)
110 }
111}
112
113pub struct Job<'a> {
116 pub source: &'a dyn Source,
117 pub target: &'a str,
118 pub until: Option<&'a str>,
120 pub rate: Option<u32>,
121 pub bits: Option<i32>,
122 pub asked: &'a [Asked],
123 pub volatile: &'a [String],
124 pub out: Out,
125 pub abandon: &'a dyn Abandon,
126 pub hear: Option<&'a dyn Hear>,
127 pub threads: Option<NonZeroUsize>,
129}
130
131impl<'a> Job<'a> {
132 pub fn over(source: &'a dyn Source, target: &'a str) -> Job<'a> {
133 Job {
134 source,
135 target,
136 until: None,
137 rate: None,
138 bits: None,
139 asked: &[],
140 volatile: &[],
141 out: Out::Kept,
142 abandon: &Never,
143 hear: None,
144 threads: None,
145 }
146 }
147}
148
149fn settle(job: &Job) -> Result<(Graph, RenderConfig, Song), CliError> {
150 let Target { expr, interval } = target(job.target)?;
151 let roots = roots_of(job.source, &expr)?;
152 let hinted = |e| solve::hinted(e, job.source, Some(job.target));
153 let mut graph = settled(sva_ast::load_reaching(
154 job.source,
155 &roots.iter().map(String::as_str).collect::<Vec<_>>(),
156 ))
157 .map_err(hinted)?;
158 let parsed = sva_ast::parse_expr(&expr)
159 .map_err(|d| {
160 CliError::BadProbe(format!(
161 "`{}` does not parse as an expression: {}",
162 job.target, d.message
163 ))
164 })
165 .map_err(hinted)?;
166 let parsed =
167 sva_ast::resolve_recordings(&parsed, job.source).map_err(|unheld| match unheld.why {
168 None => CliError::NotFound(unheld.message()),
169 Some(_) => CliError::usage(RECORDING_UNREADABLE, unheld.message()),
170 })?;
171 define_probe(&mut graph, parsed)?;
172 let rate = job.rate.unwrap_or(DEFAULT_SAMPLE_RATE);
173 let per_bar = graph.tempo().map(|tempo| tempo.seconds_per_bar());
174 let range = match interval {
175 None => Range::default(),
176 Some((start, end)) => Range {
177 start: start.sample(rate, per_bar)?,
178 end: end.sample(rate, per_bar)?,
179 },
180 };
181 if let Range {
182 start: Some(start),
183 end: Some(end),
184 } = range
185 && end <= start
186 {
187 return Err(CliError::usage(
188 "render.empty_interval",
189 format!(
190 "`{}` reads an interval from sample {start} to {end}, which holds none",
191 job.target
192 ),
193 ));
194 }
195 let until = match job.until {
196 Some(text) => Some(until(text, rate, per_bar)?),
197 None => None,
198 };
199 let mut config = RenderConfig {
200 range,
201 until,
202 ..RenderConfig::at(rate)
203 };
204 config.profile.precision_bits = precision_bits(job.bits)?;
205 config.volatile = job.volatile.to_vec();
206 config.out = job.out;
207 if let Some(threads) = job.threads {
208 config.threads = threads;
209 }
210 config.asks = job
211 .asked
212 .iter()
213 .map(|asked| {
214 Ok(Ask {
215 node: asked.node.clone().unwrap_or_else(|| PROBE.to_string()),
216 representation: asked.representation(rate, per_bar)?,
217 })
218 })
219 .collect::<Result<_, CliError>>()?;
220 let song = sung(&mut graph, PROBE, rate)?;
221 Ok((graph, config, song))
222}
223
224fn refused(song: &Song, graph: &Graph, e: EngineError, target: &str) -> CliError {
226 match as_lane(song, graph, e) {
227 CliError::Engine(e) => CliError::Engine(as_written(e, target)),
228 other => other,
229 }
230}
231
232fn instance_call(text: &str) -> Option<(&str, &str)> {
234 let (path, rest) = text.split_once('(')?;
235 let binds = rest.strip_suffix(')')?.trim();
236 (!binds.is_empty() && all_named(binds)).then_some((path, binds))
237}
238
239fn all_named(binds: &str) -> bool {
241 let mut depth = 0i32;
242 let mut named = false;
243 for c in binds.chars() {
244 match c {
245 '(' => depth += 1,
246 ')' if depth == 0 => return false,
247 ')' => depth -= 1,
248 '=' if depth == 0 => named = true,
249 ',' if depth == 0 && !std::mem::take(&mut named) => return false,
250 _ => {}
251 }
252 }
253 named && depth == 0
254}
255
256fn instance_read(graph: &Graph, text: &str) -> Option<sva_ast::Expr> {
258 let (path, binds) = instance_call(text)?;
259 if !graph.defines(path) {
260 return None;
261 }
262 sva_ast::parse_expr(&format!("@{path}(t, {binds})")).ok()
263}
264
265pub fn execute(job: Job, tier: &Tier) -> Result<Rendered, CliError> {
266 let (graph, config, song) = settle(&job)?;
267 let render = render(&graph, PROBE, config, tier);
268 rendered(&job, (graph, song), render)
269}
270
271pub async fn execute_over<B: Backend>(
274 job: Job<'_>,
275 tier: &Tier<B>,
276 session: &mut Session,
277) -> Result<Rendered, CliError> {
278 let call = tier.call(Kind::Render, job.target, Phase::Parsing);
279 let (graph, config, song) = settle(&job)?;
280 let hooks = (job.abandon, job.hear);
281 let render = render_called(session, &graph, PROBE, config, tier, hooks, call).await;
282 rendered(&job, (graph, song), render)
283}
284
285fn rendered(
286 job: &Job,
287 (graph, song): (Graph, Song),
288 render: Result<sva_engine::Render, EngineError>,
289) -> Result<Rendered, CliError> {
290 let render = render.map_err(|e| refused(&song, &graph, e, job.target))?;
291 Ok(Rendered {
292 warnings: song.warnings().to_vec(),
293 config: render.config.clone(),
294 expression: job.target.to_string(),
295 target: PROBE.to_string(),
296 render,
297 graph,
298 })
299}
300
301pub fn plan(job: &Job) -> Result<(sva_engine::Render, Vec<sva_ast::song::Warning>), CliError> {
303 let (planned, song, _) = planned(job)?;
304 Ok((planned, song.warnings().to_vec()))
305}
306
307pub(crate) fn planned(job: &Job) -> Result<(sva_engine::Render, Song, Option<f64>), CliError> {
309 let (graph, config, song) = settle(job)?;
310 let per_bar = graph.tempo().map(|tempo| tempo.seconds_per_bar());
311 let planned = sva_engine::plan(&graph, PROBE, config);
312 let planned = planned.map_err(|e| refused(&song, &graph, e, job.target))?;
313 Ok((planned, song, per_bar))
314}
315
316pub fn types(job: &Job) -> Result<Vec<sva_ast::song::Warning>, CliError> {
318 let (graph, config, song) = settle(job)?;
319 sva_engine::types_at(&graph, PROBE, config.rate)
320 .map_err(|e| refused(&song, &graph, e, job.target))?;
321 Ok(song.warnings().to_vec())
322}
323
324pub fn song_of(source: &dyn Source, target: &str) -> Result<(Graph, Song), CliError> {
326 let job = Job::over(source, target);
327 let (graph, _, song) = settle(&job)?;
328 Ok((graph, song))
329}
330
331pub async fn stream<B: Backend>(
333 job: &Job<'_>,
334 (block, channels): (usize, Option<usize>),
335 tier: &Tier<B>,
336) -> Result<Stream, CliError> {
337 let call = tier.call(Kind::Stream, job.target, Phase::Parsing);
338 let (graph, config, _) = settle(job)?;
339 let target = graph
340 .expr(PROBE)
341 .cloned()
342 .expect("the target was defined as the probe");
343 let config = StreamConfig {
344 block,
345 channels,
346 render: config,
347 };
348 Stream::open_called(&graph, &target, config, tier, call)
349 .await
350 .map_err(|e| CliError::Engine(as_written(e, job.target)))
351}
352
353pub async fn edit<B: Backend>(
356 stream: &RefCell<Stream>,
357 source: &dyn Source,
358 target: &str,
359 tier: &Tier<B>,
360) -> Result<(), CliError> {
361 let build = |s: &Stream| {
362 let (graph, expr) = streamed(s, source, target, None)?;
363 Ok(Change::Target(graph, expr))
364 };
365 changed(stream, build, Some(target), tier).await.map(|_| ())
366}
367
368pub async fn add<B: Backend>(
370 stream: &RefCell<Stream>,
371 source: &dyn Source,
372 (term, at): (&str, Placed),
373 tier: &Tier<B>,
374) -> Result<Handle, CliError> {
375 let build = |s: &Stream| {
376 let (graph, expr) = streamed(s, source, term, Some(at))?;
377 Ok(Change::Add(graph, expr, at))
378 };
379 match changed(stream, build, Some(term), tier).await? {
380 Changed::Added(handle) => Ok(handle),
381 _ => unreachable!("an add answers its handle"),
382 }
383}
384
385pub async fn replace<B: Backend>(
387 stream: &RefCell<Stream>,
388 source: &dyn Source,
389 (handle, term, at): (Handle, &str, Placed),
390 tier: &Tier<B>,
391) -> Result<bool, CliError> {
392 let build = |s: &Stream| {
393 let (graph, expr) = streamed(s, source, term, Some(at))?;
394 Ok(Change::Replace(handle, graph, expr, at))
395 };
396 Ok(changed(stream, build, Some(term), tier).await? == Changed::Held(true))
397}
398
399pub async fn remove<B: Backend>(
401 stream: &RefCell<Stream>,
402 handle: Handle,
403 tier: &Tier<B>,
404) -> Result<bool, CliError> {
405 let build = |_: &Stream| Ok(Change::Remove(handle));
406 Ok(changed(stream, build, None, tier).await? == Changed::Held(true))
407}
408
409async fn changed<B: Backend>(
410 stream: &RefCell<Stream>,
411 build: impl FnMut(&Stream) -> Result<Change, CliError>,
412 text: Option<&str>,
413 tier: &Tier<B>,
414) -> Result<Changed, CliError> {
415 let changed = sva_engine::change(stream, build, tier).await;
416 changed.map_err(|e| match (e, text) {
417 (CliError::Engine(e), Some(text)) => CliError::Engine(as_written(e, text)),
418 (e, _) => e,
419 })
420}
421
422pub async fn fetch<B: Backend>(stream: &RefCell<Stream>, tier: &Tier<B>) {
423 sva_engine::fetch(stream, tier).await;
424}
425
426fn streamed(
429 stream: &Stream,
430 source: &dyn Source,
431 text: &str,
432 term: Option<Placed>,
433) -> Result<(Graph, sva_ast::Expr), CliError> {
434 let Target { expr, interval } = target(text)?;
435 if interval.is_some() {
436 return Err(CliError::usage(
437 "stream.interval",
438 format!("`{text}` reads an interval, and a stream keeps its own"),
439 ));
440 }
441 let held = stream.graph();
442 let roots = roots_of(source, &expr)?;
443 let roots: Vec<&str> = roots.iter().map(String::as_str).collect();
444 let mut delta = sva_ast::load_beside(source, &roots, held).map_err(CliError::Refusals)?;
445 delta
446 .lower_reserved(Some(held))
447 .map_err(CliError::Refusals)?;
448 let parsed = sva_ast::parse_expr(&expr).map_err(|d| {
449 CliError::BadProbe(format!(
450 "`{text}` does not parse as an expression: {}",
451 d.message
452 ))
453 })?;
454 let mut view = held.sourced();
455 for path in delta.paths() {
456 view.set(path, delta.held(path));
457 }
458 let rate = stream.config().render.rate;
459 let lowered = match term {
460 None => {
461 view.set(PROBE, None);
462 define_probe(&mut view, parsed)?;
463 sung(&mut view, PROBE, rate)?;
464 view.expr(PROBE)
465 .cloned()
466 .expect("the target was defined as the probe")
467 }
468 Some(placed) => termed(&mut view, parsed, placed, rate)?,
469 };
470 let mut out = delta;
471 let changed: Vec<String> = view
472 .paths()
473 .filter(|path| !held.holds_as(path, &view) || !held.read_as(path, &view))
474 .map(str::to_string)
475 .collect();
476 for path in changed {
477 out.set(&path, view.held(&path));
478 }
479 Ok((out, lowered))
480}
481
482fn termed(
486 view: &mut Graph,
487 term: sva_ast::Expr,
488 placed: Placed,
489 rate: u32,
490) -> Result<sva_ast::Expr, CliError> {
491 const ALONE: &str = "#term";
492 const BESIDE: &str = "#song";
493 let mut alone = view.clone();
494 alone
495 .define_settled(None, ALONE, term)
496 .map_err(CliError::Refusals)?;
497 let written = alone.expr(ALONE).cloned().expect("just defined");
498 let own = alone.song(ALONE).map_err(CliError::Refusals)?;
499 if let Some(lane) = own.lanes().first() {
500 let first = &lane.moves[0].reached;
501 return Err(CliError::Refusals(vec![Refusal::new(
502 format!("{}{}", first.score, sva_ast::score::EXTENSION),
503 sva_ast::ByteSpan::at(0),
504 sva_ast::DiagCode::BadLane,
505 format!(
506 "a stream term reaches this move on `{}`; a term adds sound and never moves, \
507 since lanes derive from the song's files: move it in one",
508 lane.id.name()
509 ),
510 )]));
511 }
512 let target = view.source_expr(PROBE).expect("a stream holds its target");
513 let both = sva_ast::Expr::Bin(
514 sva_ast::BinOp::Add,
515 Box::new(target),
516 Box::new(written.clone()),
517 );
518 view.define_settled(None, BESIDE, both)
519 .map_err(CliError::Refusals)?;
520 sung(view, BESIDE, rate)?;
521 let Some(sva_ast::Expr::Bin(_, _, lowered)) = view.expr(BESIDE).cloned() else {
522 unreachable!("a sum lowers to a sum")
523 };
524 view.set(BESIDE, None);
525 if placed == Placed::Landing && *lowered != written {
526 return Err(CliError::Refusals(vec![Refusal::new(
527 ALONE,
528 sva_ast::ByteSpan::at(0),
529 sva_ast::DiagCode::BadLane,
530 "this term lands where the stream is when it arrives, so it has no song time, and a \
531 lane of the song moves what it plays; a lane reaches an instance at its song \
532 position, so add it at a written time"
533 .to_string(),
534 )]));
535 }
536 Ok(*lowered)
537}
538
539pub fn precision_bits(bits: Option<i32>) -> Result<i32, CliError> {
541 bits.map_or(Ok(PSYCHOACOUSTIC_V1.precision_bits), precision)
542}
543
544fn precision(bits: i32) -> Result<i32, CliError> {
546 match (2..=52).contains(&bits) {
547 true => Ok(bits),
548 false => Err(CliError::usage(
549 "cli.bad_value",
550 format!("`--bits` takes a precision from 2 to 52 bits, not {bits}"),
551 )),
552 }
553}
554
555fn as_written(refused: EngineError, target: &str) -> EngineError {
558 match refused {
559 EngineError::Refused(mut d) => {
560 if d.location.node == PROBE {
561 d.location.node = target.to_string();
562 }
563 d.message = d
564 .message
565 .replace(&format!("`{PROBE}`"), &format!("`{target}`"));
566 EngineError::Refused(d)
567 }
568 EngineError::Binding { node, span, fault } if node == PROBE => EngineError::Binding {
569 node: target.to_string(),
570 span,
571 fault,
572 },
573 other => other,
574 }
575}
576
577pub fn probe(dir: &Path, expression: &str) -> Result<Rendered, CliError> {
578 execute(Job::over(&Dir::at(dir), expression), &Tier::default())
579}
580
581pub fn cwd() -> Result<std::path::PathBuf, CliError> {
582 std::env::current_dir()
583 .map_err(|e| CliError::Io(format!("could not read the current directory: {e}")))
584}
585
586pub fn prepared(source: &dyn Source) -> Result<Graph, CliError> {
588 settled(sva_ast::load(&Unfitted(source))).map_err(|e| solve::hinted(e, source, None))
589}
590
591struct Unfitted<'a>(&'a dyn Source);
592
593impl Source for Unfitted<'_> {
594 fn paths(&self) -> Result<sva_ast::Listing, String> {
595 let fits = format!("{}/", solve::FITS);
596 let mut listing = self.0.paths()?;
597 listing.found.retain(|path| !path.starts_with(&fits));
598 listing.unreadable.retain(|path| !path.starts_with(&fits));
599 Ok(listing)
600 }
601
602 fn get(&self, path: &str) -> Result<Option<std::borrow::Cow<'_, str>>, String> {
603 self.0.get(path)
604 }
605
606 fn generation(&self, path: &str) -> Option<u64> {
607 self.0.generation(path)
608 }
609
610 fn name(&self) -> String {
611 self.0.name()
612 }
613
614 fn recording(&self, path: &str) -> Result<Option<std::sync::Arc<sva_ast::Recording>>, String> {
615 self.0.recording(path)
616 }
617}
618
619pub fn define_probe_for(graph: &mut Graph, text: &str) -> Result<(), CliError> {
622 let expr = match instance_read(graph, text) {
623 Some(read) => read,
624 None if names_a_missing_node(text) => {
625 return Err(CliError::NotFound(format!(
626 "`{text}` is not a node this composition defines"
627 )));
628 }
629 None => sva_ast::parse_expr(text).map_err(|d| {
630 CliError::BadProbe(format!(
631 "`{text}` is not a node in this composition, and does not parse as an \
632 expression: {}",
633 d.message
634 ))
635 })?,
636 };
637 define_probe(graph, expr)
638}
639
640fn names_a_missing_node(text: &str) -> bool {
643 match instance_call(text) {
644 Some((path, _)) => sva_ast::names_a_node(path),
645 None => !text.contains('(') && sva_ast::names_a_node(text) && !reads_as_a_value(text),
646 }
647}
648
649fn reads_as_a_value(text: &str) -> bool {
650 match sva_ast::parse_expr(text) {
651 Ok(sva_ast::Expr::Lit(_)) => true,
652 Ok(sva_ast::Expr::Var(name)) => sva_ast::is_reserved(&name),
653 _ => false,
654 }
655}
656
657fn define_probe(graph: &mut Graph, expr: sva_ast::Expr) -> Result<(), CliError> {
659 let defined = graph
660 .define_settled(None, PROBE, expr)
661 .map_err(CliError::Refusals)?;
662 if !defined {
663 return Err(CliError::BadProbe(format!(
664 "this composition already has a node named `{PROBE}`"
665 )));
666 }
667 Ok(())
668}
669
670pub fn settled(loaded: Result<Graph, Vec<Refusal>>) -> Result<Graph, CliError> {
672 let mut graph = loaded.map_err(CliError::Refusals)?;
673 graph.lower_reserved(None).map_err(CliError::Refusals)?;
674 Ok(graph)
675}
676
677pub fn roots_of(source: &dyn Source, target: &str) -> Result<Vec<String>, CliError> {
680 let mut roots: Vec<String> = RESERVED_VARIABLES.map(sva_ast::variable).to_vec();
681 let listed = source.paths().map_err(CliError::Io)?.found;
682 let variables = format!("{}/", sva_ast::VARIABLES);
683 roots.extend(listed.into_iter().filter(|p| p.starts_with(&variables)));
684 roots.sort();
685 roots.dedup();
686 let held = |path: &str| -> Result<bool, CliError> {
687 let file = format!("{path}{}", sva_ast::score::EXTENSION);
688 let get = |at: &str| {
689 source
690 .get(at)
691 .map(|text| text.is_some())
692 .map_err(CliError::Io)
693 };
694 Ok(sva_ast::whole_ref_path(path) && (get(path)? || get(&file)?))
695 };
696 match target {
697 target if held(target)? => {
698 roots.push(target.to_string());
699 }
700 text => match instance_call(text).map(|(path, _)| path) {
701 Some(path) if held(path)? => {
702 roots.push(path.to_string());
703 }
704 _ => {
705 if let Ok(expr) = sva_ast::parse_expr(text) {
706 roots.extend(sva_ast::reads_of(PROBE, &expr));
707 }
708 }
709 },
710 }
711 Ok(roots)
712}