Skip to main content

engawa_lisp/
lower.rs

1//! Typed lowering — parsed sexprs → `engawa::RenderGraph`.
2//!
3//! The grammar is intentionally small + form-based:
4//!
5//! ```text
6//! (defresource <id> <kind>)
7//!   kind ::= external
8//!         | (texture <width> <height>)
9//!         | (uniform <size_bytes>)
10//!         | (storage <size_bytes>)
11//!         | sampler
12//!
13//! (defmaterial <name>
14//!   (shader (inline <wgsl-string>))     | (shader (path <path-string>))
15//!   (bindings (binding <n> <kind> <resource-id>) …))
16//!
17//! (defgraph <name>
18//!   (input <resource-id>) …
19//!   (output <resource-id>) …
20//!   (node <node-id>
21//!     (kind clear | fullscreen-effect)
22//!     [(material <material-name>)]
23//!     [(input <resource-id>) …]
24//!     [(output <resource-id>) …]))
25//! ```
26//!
27//! Operator errors carry line:col context so the failing form
28//! is locatable in the source file.
29
30use std::collections::BTreeMap;
31
32use engawa::{
33    BindingKind, Material, Node, NodeId, RenderGraph, ResourceId, ResourceKind,
34    ShaderSource, UniformBinding,
35};
36use thiserror::Error;
37
38use crate::parse::Span;
39use crate::sexpr::{Sexpr, SexprKind};
40
41#[derive(Debug, Error, Clone, PartialEq)]
42pub enum LowerError {
43    #[error("expected form, got non-list at line {line}, column {col}")]
44    NotAForm { line: usize, col: usize },
45    #[error("expected form name (symbol) at line {line}, column {col}")]
46    MissingFormName { line: usize, col: usize },
47    #[error("unknown form {form:?} at line {line}, column {col}")]
48    UnknownForm {
49        form: String,
50        line: usize,
51        col: usize,
52    },
53    #[error("form {form:?} expects {expected} arguments, got {got} at line {line}")]
54    Arity {
55        form: String,
56        expected: usize,
57        got: usize,
58        line: usize,
59    },
60    #[error("form {form:?} expects argument {position} to be a {kind} at line {line}")]
61    BadArgKind {
62        form: String,
63        position: usize,
64        kind: &'static str,
65        line: usize,
66    },
67    #[error("could not parse {value:?} as {kind} at line {line}")]
68    BadNumber {
69        value: String,
70        kind: &'static str,
71        line: usize,
72    },
73    #[error("unknown resource kind {found:?} at line {line}")]
74    UnknownResourceKind { found: String, line: usize },
75    #[error("unknown binding kind {found:?} at line {line}")]
76    UnknownBindingKind { found: String, line: usize },
77    #[error("unknown node kind {found:?} at line {line}")]
78    UnknownNodeKind { found: String, line: usize },
79    #[error("material {name:?} referenced but not defined")]
80    UndefinedMaterial { name: String },
81}
82
83/// Lower a parsed source (vec of top-level sexprs) into an
84/// `engawa::RenderGraph`. The forms can appear in any order;
85/// the lowerer collects resources + materials first, then
86/// builds the graph from the `(defgraph …)` form.
87///
88/// Exactly one `(defgraph …)` form is expected per file. Future
89/// extension: multiple graphs in one file would require a name
90/// argument here.
91pub fn lower_to_graph(forms: &[Sexpr]) -> Result<RenderGraph, LowerError> {
92    let mut materials: BTreeMap<String, Material> = BTreeMap::new();
93    let mut resources: BTreeMap<ResourceId, ResourceKind> = BTreeMap::new();
94    let mut graph_form: Option<&Sexpr> = None;
95
96    for form in forms {
97        let items = require_list(form)?;
98        let head_name = require_symbol(&items[0], "form-head", 0, form.span)?;
99        match head_name {
100            "defresource" => {
101                let (id, kind) = lower_resource(items, form.span)?;
102                resources.insert(id, kind);
103            }
104            "defmaterial" => {
105                let mat = lower_material(items, form.span)?;
106                materials.insert(mat.name.clone(), mat);
107            }
108            "defgraph" => {
109                if graph_form.is_some() {
110                    return Err(LowerError::UnknownForm {
111                        form: "defgraph (multiple)".to_string(),
112                        line: form.span.line,
113                        col: form.span.column,
114                    });
115                }
116                graph_form = Some(form);
117            }
118            other => {
119                return Err(LowerError::UnknownForm {
120                    form: other.to_string(),
121                    line: form.span.line,
122                    col: form.span.column,
123                });
124            }
125        }
126    }
127
128    let Some(graph_form) = graph_form else {
129        // Empty file → empty graph. Operator can still build a
130        // graph programmatically; this just isn't authored.
131        let mut g = RenderGraph::default();
132        for (id, kind) in resources {
133            g = g.with_resource(id, kind);
134        }
135        return Ok(g);
136    };
137
138    let items = require_list(graph_form)?;
139    let mut g = RenderGraph::default();
140    for (id, kind) in &resources {
141        g = g.with_resource(id.clone(), kind.clone());
142    }
143
144    // Skip head symbol + name.
145    let body = &items[1..];
146    let _graph_name = require_symbol(&items[1], "defgraph", 1, graph_form.span)?;
147    for clause in &body[1..] {
148        let clause_items = require_list(clause)?;
149        let head = require_symbol(&clause_items[0], "graph-clause", 0, clause.span)?;
150        match head {
151            "input" => {
152                let id = lower_resource_ref(clause_items, "input", clause.span)?;
153                g = g.with_input(id);
154            }
155            "output" => {
156                let id = lower_resource_ref(clause_items, "output", clause.span)?;
157                g = g.with_output(id);
158            }
159            "node" => {
160                let node = lower_node(clause_items, clause.span, &materials)?;
161                g = g.with_node(node);
162            }
163            other => {
164                return Err(LowerError::UnknownForm {
165                    form: other.to_string(),
166                    line: clause.span.line,
167                    col: clause.span.column,
168                });
169            }
170        }
171    }
172    Ok(g)
173}
174
175fn lower_resource(
176    items: &[Sexpr],
177    span: Span,
178) -> Result<(ResourceId, ResourceKind), LowerError> {
179    if items.len() < 3 {
180        return Err(LowerError::Arity {
181            form: "defresource".into(),
182            expected: 3,
183            got: items.len(),
184            line: span.line,
185        });
186    }
187    let id_str = require_symbol(&items[1], "defresource", 1, span)?;
188    let id = ResourceId::new(id_str);
189    let kind_form = &items[2];
190    let kind = match &kind_form.kind {
191        SexprKind::Symbol(s) if s == "external" => ResourceKind::External,
192        SexprKind::Symbol(s) if s == "sampler" => ResourceKind::Sampler,
193        SexprKind::List(inner) => {
194            let head = require_symbol(&inner[0], "resource-kind", 0, kind_form.span)?;
195            match head {
196                "texture" => {
197                    let w = parse_u32(&inner[1], "texture-width", kind_form.span)?;
198                    let h = parse_u32(&inner[2], "texture-height", kind_form.span)?;
199                    ResourceKind::Texture {
200                        width: Some(w),
201                        height: Some(h),
202                    }
203                }
204                "uniform" => {
205                    let n = parse_u32(&inner[1], "uniform-size", kind_form.span)?;
206                    ResourceKind::Uniform { size_bytes: n }
207                }
208                "storage" => {
209                    let n = parse_u32(&inner[1], "storage-size", kind_form.span)?;
210                    ResourceKind::Storage { size_bytes: n }
211                }
212                other => {
213                    return Err(LowerError::UnknownResourceKind {
214                        found: other.to_string(),
215                        line: kind_form.span.line,
216                    });
217                }
218            }
219        }
220        _ => {
221            return Err(LowerError::UnknownResourceKind {
222                found: format!("{:?}", kind_form.kind),
223                line: kind_form.span.line,
224            });
225        }
226    };
227    Ok((id, kind))
228}
229
230fn lower_material(items: &[Sexpr], span: Span) -> Result<Material, LowerError> {
231    if items.len() < 3 {
232        return Err(LowerError::Arity {
233            form: "defmaterial".into(),
234            expected: 3,
235            got: items.len(),
236            line: span.line,
237        });
238    }
239    let name = require_symbol(&items[1], "defmaterial", 1, span)?.to_string();
240    let mut shader: Option<ShaderSource> = None;
241    let mut bindings: Vec<UniformBinding> = Vec::new();
242    for clause in &items[2..] {
243        let inner = require_list(clause)?;
244        let head = require_symbol(&inner[0], "material-clause", 0, clause.span)?;
245        match head {
246            "shader" => {
247                let body = require_list(&inner[1])?;
248                let kind_name = require_symbol(&body[0], "shader-kind", 0, clause.span)?;
249                let payload = require_string(&body[1], "shader-payload", 1, clause.span)?;
250                shader = Some(match kind_name {
251                    "inline" => ShaderSource::inline(payload),
252                    "path" => ShaderSource::path(payload),
253                    other => {
254                        return Err(LowerError::UnknownForm {
255                            form: format!("shader-kind:{other}"),
256                            line: clause.span.line,
257                            col: clause.span.column,
258                        });
259                    }
260                });
261            }
262            "bindings" => {
263                for b in &inner[1..] {
264                    bindings.push(lower_binding(b)?);
265                }
266            }
267            other => {
268                return Err(LowerError::UnknownForm {
269                    form: other.to_string(),
270                    line: clause.span.line,
271                    col: clause.span.column,
272                });
273            }
274        }
275    }
276    Ok(Material {
277        name,
278        shader: shader.unwrap_or_else(|| ShaderSource::inline("")),
279        bindings,
280    })
281}
282
283fn lower_binding(form: &Sexpr) -> Result<UniformBinding, LowerError> {
284    let items = require_list(form)?;
285    let head = require_symbol(&items[0], "binding", 0, form.span)?;
286    if head != "binding" {
287        return Err(LowerError::UnknownForm {
288            form: head.to_string(),
289            line: form.span.line,
290            col: form.span.column,
291        });
292    }
293    let n = parse_u32(&items[1], "binding-index", form.span)?;
294    let kind_str = require_symbol(&items[2], "binding-kind", 2, form.span)?;
295    let kind = match kind_str {
296        "uniform" => BindingKind::Uniform,
297        "storage_read" | "storage-read" => BindingKind::StorageRead,
298        "storage_read_write" | "storage-read-write" => BindingKind::StorageReadWrite,
299        "texture" => BindingKind::Texture,
300        "sampler" => BindingKind::Sampler,
301        other => {
302            return Err(LowerError::UnknownBindingKind {
303                found: other.to_string(),
304                line: form.span.line,
305            });
306        }
307    };
308    let resource_str = require_string(&items[3], "binding-resource", 3, form.span)?;
309    Ok(UniformBinding {
310        binding: n,
311        kind,
312        resource: ResourceId::new(resource_str),
313    })
314}
315
316fn lower_resource_ref(
317    items: &[Sexpr],
318    form: &'static str,
319    span: Span,
320) -> Result<ResourceId, LowerError> {
321    if items.len() != 2 {
322        return Err(LowerError::Arity {
323            form: form.into(),
324            expected: 2,
325            got: items.len(),
326            line: span.line,
327        });
328    }
329    let s = require_symbol(&items[1], form, 1, span)?;
330    Ok(ResourceId::new(s))
331}
332
333fn lower_node(
334    items: &[Sexpr],
335    span: Span,
336    materials: &BTreeMap<String, Material>,
337) -> Result<Node, LowerError> {
338    if items.len() < 3 {
339        return Err(LowerError::Arity {
340            form: "node".into(),
341            expected: 3,
342            got: items.len(),
343            line: span.line,
344        });
345    }
346    let node_id_str = require_symbol(&items[1], "node", 1, span)?;
347    let node_id = NodeId::new(node_id_str);
348    let mut kind: Option<String> = None;
349    let mut material: Option<Material> = None;
350    let mut inputs: Vec<ResourceId> = Vec::new();
351    let mut outputs: Vec<ResourceId> = Vec::new();
352    for clause in &items[2..] {
353        let inner = require_list(clause)?;
354        let head = require_symbol(&inner[0], "node-clause", 0, clause.span)?;
355        match head {
356            "kind" => {
357                let s = require_symbol(&inner[1], "kind", 1, clause.span)?;
358                kind = Some(s.to_string());
359            }
360            "material" => {
361                let s = require_symbol(&inner[1], "material", 1, clause.span)?;
362                let m = materials.get(s).ok_or_else(|| LowerError::UndefinedMaterial {
363                    name: s.to_string(),
364                })?;
365                material = Some(m.clone());
366            }
367            "input" => {
368                let s = require_symbol(&inner[1], "input", 1, clause.span)?;
369                inputs.push(ResourceId::new(s));
370            }
371            "output" => {
372                let s = require_symbol(&inner[1], "output", 1, clause.span)?;
373                outputs.push(ResourceId::new(s));
374            }
375            other => {
376                return Err(LowerError::UnknownForm {
377                    form: other.to_string(),
378                    line: clause.span.line,
379                    col: clause.span.column,
380                });
381            }
382        }
383    }
384    let pass = engawa::PassKind::Render;
385    let _ = kind; // kind clause currently informational; the
386                  // node-kind enum collapses to PassKind::Render
387                  // for both "clear" and "fullscreen-effect" in v0.1.
388    Ok(Node {
389        id: node_id,
390        pass,
391        inputs,
392        outputs,
393        material,
394    })
395}
396
397// ── helpers ────────────────────────────────────────────────────
398
399fn require_list(s: &Sexpr) -> Result<&[Sexpr], LowerError> {
400    s.as_list().ok_or(LowerError::NotAForm {
401        line: s.span.line,
402        col: s.span.column,
403    })
404}
405
406fn require_symbol<'a>(
407    s: &'a Sexpr,
408    form: &'static str,
409    position: usize,
410    span: Span,
411) -> Result<&'a str, LowerError> {
412    s.as_symbol().ok_or(LowerError::BadArgKind {
413        form: form.into(),
414        position,
415        kind: "symbol",
416        line: span.line,
417    })
418}
419
420fn require_string<'a>(
421    s: &'a Sexpr,
422    form: &'static str,
423    position: usize,
424    span: Span,
425) -> Result<&'a str, LowerError> {
426    s.as_string().ok_or(LowerError::BadArgKind {
427        form: form.into(),
428        position,
429        kind: "string",
430        line: span.line,
431    })
432}
433
434fn parse_u32(s: &Sexpr, kind: &'static str, span: Span) -> Result<u32, LowerError> {
435    let txt = s.as_number().ok_or(LowerError::BadArgKind {
436        form: kind.into(),
437        position: 0,
438        kind: "number",
439        line: span.line,
440    })?;
441    txt.parse::<u32>().map_err(|_| LowerError::BadNumber {
442        value: txt.to_string(),
443        kind: "u32",
444        line: span.line,
445    })
446}