Skip to main content

sva_core/
outline.rs

1// Concern: writes an expression's outline, a score's lines or a song's derivation as the `data` object | Non-concern: building any of them (sva-ast) | IO: (text) or (source, target) -> JSON
2
3use sva_ast::outline::{Argument, Form};
4use sva_ast::score::{EXTENSION, Item, spelled};
5use sva_ast::song::{Curve, Held, Home, LaneId, Pos};
6use sva_ast::{Address, BinOp, ByteSpan, Literal, Outline, Refusal, SpanUnit};
7
8use crate::CliError;
9use crate::json::{NONE, escape, list, num};
10
11/// `{ "text", "outline" }`, or the parser's own refusal, located in the text. Score text, a
12/// JSON line, has no one tree: it answers `{ "text", "score" }`, one item per placed item.
13pub fn outline_data(text: &str) -> Result<String, CliError> {
14    if text.trim_start().starts_with('{') {
15        return score_data(text);
16    }
17    let tree = sva_ast::outline(text).map_err(|d| {
18        CliError::Refusals(vec![Refusal::new("expression", d.span, d.code, d.message)])
19    })?;
20    Ok(format!(
21        "{{ \"text\": \"{}\", \"outline\": {} }}",
22        escape(text),
23        node(&tree)
24    ))
25}
26
27fn time(amount: f64, unit: SpanUnit) -> String {
28    format!("\"{}\"", spelled(amount, unit))
29}
30
31fn score_data(text: &str) -> Result<String, CliError> {
32    let score = sva_ast::parse_score(text)
33        .map_err(|d| CliError::Refusals(vec![Refusal::new("score", d.span, d.code, d.message)]))?;
34    let unit = score.unit;
35    let mut items = Vec::new();
36    for line in &score.lines {
37        for item in &line.items {
38            let fields = match item {
39                Item::Event(e) => format!(
40                    ", \"node\": \"@{}\", \"deg\": {}, \"len\": {}, \"args\": {}",
41                    escape(&e.node.name),
42                    e.deg
43                        .map_or_else(|| NONE.to_string(), |(d, _)| d.to_string()),
44                    e.len
45                        .map_or_else(|| NONE.to_string(), |l| time(l.amount.to_f64(), unit)),
46                    list(&e.binds, |b| format!(
47                        "{{ \"name\": \"{}\", \"value\": \"{}\" }}",
48                        escape(&b.name),
49                        escape(&sva_ast::render_expr(&b.value))
50                    ))
51                ),
52                Item::Mark(m) => format!(", \"mark\": \"{}\"", escape(&m.name)),
53                Item::Comment(_) => String::new(),
54                Item::Move(m) => format!(
55                    ", \"node\": {}, \"lane\": \"{}\", \"to\": \"{}\", \"len\": {}, \"shape\": {}",
56                    m.node
57                        .as_ref()
58                        .map_or_else(|| NONE.to_string(), |n| format!("\"@{}\"", escape(&n.name))),
59                    escape(&m.lane.name),
60                    escape(&m.to.text),
61                    m.ramp
62                        .map_or_else(|| NONE.to_string(), |r| time(r.len.amount.to_f64(), unit)),
63                    m.ramp
64                        .map_or_else(|| NONE.to_string(), |r| format!("\"{}\"", r.shape.name())),
65                ),
66            };
67            items.push(format!(
68                "{{ \"kind\": \"{}\", \"line\": {}, \"span\": {}, \"at\": {}{fields} }}",
69                item.kind(),
70                line.number,
71                span(line.span),
72                time(item.at().amount.to_f64(), unit)
73            ));
74        }
75    }
76    Ok(format!(
77        "{{ \"text\": \"{}\", \"score\": {} }}",
78        escape(text),
79        list(&items, String::clone)
80    ))
81}
82
83/// A position as bars and seconds.
84fn placed(at: Pos, curve: &Curve) -> String {
85    format!(
86        "{{ \"bars\": {}, \"seconds\": {} }}",
87        num(at.bars(curve)),
88        num(at.seconds(curve))
89    )
90}
91
92/// `sva-cli outline @score/song`: the score as the song a render of it is, its marks, tempo
93/// curve, lanes, events and size.
94pub fn song_data(source: &dyn sva_ast::Source, target: &str) -> Result<String, CliError> {
95    let (graph, song) = crate::song_of(source, target)?;
96    let curve = song.curve();
97    let marks = list(song.marks(), |m| {
98        format!(
99            "{{ \"name\": \"{}\", \"at\": {}, \"file\": \"{}{EXTENSION}\", \"line\": {} }}",
100            escape(&m.name),
101            placed(m.at, curve),
102            escape(&m.score),
103            m.line
104        )
105    });
106    let tempo = list(&curve.pieces(), |(from, to, start, end, a, b)| {
107        format!(
108            "{{ \"bars\": [{}, {}], \"seconds\": [{}, {}], \"bpm\": [{}, {}] }}",
109            num(*from),
110            num(*to),
111            num(*start),
112            num(*end),
113            num(*a),
114            num(*b)
115        )
116    });
117    let instances = song.instances();
118    let lanes = list(song.lanes(), |lane| {
119        let reaching = instances
120            .iter()
121            .filter(|i| i.lanes.contains(&lane.id))
122            .count();
123        let moving = song.passed().iter().filter(|(_, l)| *l == lane.id).count();
124        let home = match &lane.home {
125            Home::Number(v, _) => num(*v),
126            Home::Scale(_) => "\"a scale\"".to_string(),
127            Home::Default(text) => format!("\"{}\"", escape(text)),
128            Home::None => NONE.to_string(),
129        };
130        let held = |h: &Option<Held>| match h {
131            Some(Held::Number(v)) => num(*v),
132            Some(Held::Scale(s)) => format!("\"{:?}\"", s.cents),
133            None => NONE.to_string(),
134        };
135        let moves = list(&lane.moves, |m| {
136            let r = &m.reached;
137            let line = graph.score(&r.score).map_or(0, |s| s.lines[r.line].number);
138            format!(
139                "{{ \"from\": {}, \"to\": {}, \"start\": {}, \"end\": {}, \"shape\": {}, \"file\": \"{}{EXTENSION}\", \"line\": {line}, \"chain\": \"{}\" }}",
140                held(&m.from),
141                held(&Some(m.to.clone())),
142                placed(r.start, curve),
143                placed(r.end.unwrap_or(r.start), curve),
144                m.shape
145                    .map_or_else(|| NONE.to_string(), |s| format!("\"{}\"", s.name())),
146                escape(&r.score),
147                escape(&r.chain.text(curve))
148            )
149        });
150        format!(
151            "{{ \"lane\": \"{}\", \"home\": {home}, \"moves\": {moves}, \"instances\": {}, \"moving\": {moving} }}",
152            escape(&lane.id.name()),
153            reaching
154        )
155    });
156    let events = events(&graph, &song);
157    let typed = sva_engine::types(&graph, crate::PROBE).map_err(CliError::Engine)?;
158    Ok(format!(
159        "{{ \"song\": \"{}\", \"marks\": {marks}, \"tempo\": {tempo}, \"lanes\": {lanes}, \"events\": {}, \"size\": {{ \"events\": {}, \"instances\": {}, \"limit\": {} }} }}",
160        escape(target),
161        list(&events, String::clone),
162        events.len(),
163        typed.paths().count(),
164        sva_engine::instantiate::MAX_INSTANCES
165    ))
166}
167
168/// Each note `song` places, once however many reads reach it: its song time, when it first
169/// sounds, how long its line holds it, its node, pitch, line and where each parameter comes from.
170fn events(graph: &sva_ast::Graph, song: &sva_ast::song::Song) -> Vec<String> {
171    let curve = song.curve();
172    let source = graph.sourced();
173    song.instances()
174        .iter()
175        .filter_map(|i| Some((i, i.line.as_ref()?, i.at?, i.sounds(curve)?)))
176        .map(|(i, (score, li, ii), at, sounds)| {
177            let Some(Item::Event(e)) = graph.score(score).map(|s| &s.lines[*li].items[*ii]) else {
178                return String::new();
179            };
180            let hz = match e.deg {
181                Some((n, _)) => song
182                    .key_at(at.seconds(curve))
183                    .zip(song.scale_at(at.seconds(curve)).and_then(|s| s.ratio(n)))
184                    .map(|(k, r)| k * r),
185                None => e
186                    .binds
187                    .iter()
188                    .find(|b| b.name == "f0")
189                    .and_then(|b| sva_ast::fold_number(&b.value, &|n| sva_ast::note_hz(n))),
190            };
191            let mut params = sva_ast::song::parameters(&source, &i.node);
192            params.sort();
193            let source = |p: &String| {
194                if i.lanes.contains(&LaneId::Sound(i.node.clone(), p.clone())) {
195                    "lane"
196                } else if i.bound.contains(p) {
197                    "line"
198                } else {
199                    "default"
200                }
201            };
202            let len = song.ends(graph, i).map_or_else(
203                || NONE.to_string(),
204                |end| {
205                    format!(
206                        "{{ \"bars\": {}, \"seconds\": {} }}",
207                        num(end.bars(curve) - at.bars(curve)),
208                        num(end.seconds(curve) - at.seconds(curve))
209                    )
210                },
211            );
212            format!(
213                "{{ \"at\": {}, \"sounds\": {}, \"len\": {len}, \"node\": \"{}\", \"hz\": {}, \"file\": \"{}{EXTENSION}\", \"line\": {}, \"params\": {} }}",
214                placed(at, curve),
215                placed(sounds, curve),
216                escape(&i.node),
217                hz.map_or_else(|| NONE.to_string(), num),
218                escape(score),
219                graph.score(score).map_or(0, |s| s.lines[*li].number),
220                list(&params, |p| format!(
221                    "{{ \"name\": \"{}\", \"from\": \"{}\" }}",
222                    escape(p),
223                    source(p)
224                ))
225            )
226        })
227        .collect()
228}
229
230/// `{ "song", "events" }`: the events `song_data` lists, without its marks, tempo, lanes and
231/// size, whose count types the whole graph on the engine; what a page draws a roll from.
232pub fn events_data(source: &dyn sva_ast::Source, target: &str) -> Result<String, CliError> {
233    let (graph, song) = crate::song_of(source, target)?;
234    Ok(format!(
235        "{{ \"song\": \"{}\", \"events\": {} }}",
236        escape(target),
237        list(&events(&graph, &song), String::clone)
238    ))
239}
240
241fn span(at: ByteSpan) -> String {
242    format!("{{ \"start\": {}, \"end\": {} }}", at.start, at.end)
243}
244
245fn node(o: &Outline) -> String {
246    let (kind, fields) = match &o.form {
247        Form::Literal(l) => ("literal", literal(l)),
248        Form::Name(name) => ("name", format!("\"name\": \"{}\"", escape(name))),
249        Form::Operator {
250            op,
251            at,
252            left,
253            right,
254        } => (
255            "operator",
256            format!(
257                "\"op\": \"{}\", \"at\": {}, \"left\": {}, \"right\": {}",
258                symbol(*op),
259                span(*at),
260                node(left),
261                node(right)
262            ),
263        ),
264        Form::Call { name, at, args } => (
265            "call",
266            format!(
267                "\"name\": \"{}\", \"at\": {}, \"args\": {}",
268                escape(name),
269                span(*at),
270                list(args, argument)
271            ),
272        ),
273        Form::Ref {
274            path,
275            at,
276            arg,
277            binds,
278            address,
279        } => (
280            "ref",
281            format!(
282                "\"path\": \"{}\", \"at\": {}, \"read\": \"{}\", \"arg\": {}, \"binds\": {}",
283                escape(path),
284                span(*at),
285                read(*address),
286                node(arg),
287                list(binds, argument)
288            ),
289        ),
290        Form::SelfRef { at, arg, address } => (
291            "self",
292            format!(
293                "\"at\": {}, \"read\": \"{}\", \"arg\": {}",
294                span(*at),
295                read(*address),
296                node(arg)
297            ),
298        ),
299        Form::Indexed { name, at, arg } => (
300            "parameter",
301            format!(
302                "\"name\": \"{}\", \"at\": {}, \"read\": \"{}\", \"arg\": {}",
303                escape(name),
304                span(*at),
305                read(Address::Index),
306                node(arg)
307            ),
308        ),
309    };
310    format!(
311        "{{ \"kind\": \"{kind}\", \"span\": {}, \"written\": {}, {fields} }}",
312        span(o.span),
313        o.written
314    )
315}
316
317/// `time` for `x(e)`, `index` for `x[i]`.
318fn read(address: Address) -> &'static str {
319    match address {
320        Address::Time => "time",
321        Address::Index => "index",
322    }
323}
324
325fn argument(a: &Argument) -> String {
326    match a {
327        Argument::Positional(value) => {
328            format!("{{ \"kind\": \"positional\", \"value\": {} }}", node(value))
329        }
330        Argument::Named { name, at, value } => format!(
331            "{{ \"kind\": \"named\", \"name\": \"{}\", \"at\": {}, \"value\": {} }}",
332            escape(name),
333            span(*at),
334            node(value)
335        ),
336    }
337}
338
339/// A unit the lexer resolves, `s` or `db`, is a number already; bars and samples wait for a
340/// tempo and a rate.
341fn literal(l: &Literal) -> String {
342    let (unit, value) = match l {
343        Literal::Num(v) => ("number", num(*v)),
344        Literal::Bars(v) => ("bars", num(*v)),
345        Literal::Samples(v) => ("samples", num(*v)),
346        Literal::Str(s) => ("text", format!("\"{}\"", escape(s))),
347        Literal::Path(path) => ("path", format!("\"{}\"", escape(path))),
348        Literal::Recorded(recording) => ("path", format!("\"{}\"", escape(&recording.path))),
349    };
350    format!("\"unit\": \"{unit}\", \"value\": {value}")
351}
352
353fn symbol(op: BinOp) -> &'static str {
354    match op {
355        BinOp::Add => "+",
356        BinOp::Sub => "-",
357        BinOp::Mul => "*",
358        BinOp::Div => "/",
359        BinOp::Mod => "%",
360    }
361}