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