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