Skip to main content

lashlang/
source.rs

1use thiserror::Error;
2
3use crate::HostRequirements;
4use crate::ast::{
5    AssignPathStep, AssignTarget, BinaryOp, Declaration, Expr, LabelMetadata,
6    ListComprehensionClause, ProcessDecl, Program, ResourceRefExpr, TypeExpr, TypeField, UnaryOp,
7};
8
9/// Error returned when canonical IR cannot be represented as Lashlang source.
10#[derive(Clone, Debug, Error, PartialEq, Eq)]
11pub enum CanonicalSourceError {
12    #[error("invalid {context} identifier `{name}`")]
13    InvalidIdentifier { context: &'static str, name: String },
14    #[error("invalid {context} path `{path}`")]
15    InvalidPath { context: &'static str, path: String },
16    #[error("cannot render non-sourceable {kind} expression")]
17    NonSourceableExpression { kind: &'static str },
18    #[error("cannot render non-sourceable {kind} type")]
19    NonSourceableType { kind: &'static str },
20    #[error("cannot render number literal `{value}` as canonical Lashlang source")]
21    UnsupportedNumber { value: String },
22    #[error(
23        "cannot render host descriptor constructor `{type_name}` without an unambiguous constructor path"
24    )]
25    UnknownHostDescriptorConstructor { type_name: String },
26    #[error(
27        "cannot render host descriptor constructor `{type_name}` because multiple constructor paths match: {paths:?}"
28    )]
29    AmbiguousHostDescriptorConstructor {
30        type_name: String,
31        paths: Vec<String>,
32    },
33}
34
35/// Pretty-print a canonical Lashlang program as source.
36///
37/// This is a view of the IR, not the original authored text: comments and
38/// formatting are not preserved. If the program was linked and contains host
39/// descriptor constructors, use [`canonical_program_source_with_requirements`]
40/// so constructor paths can be recovered from the saved host requirements.
41pub fn canonical_program_source(program: &Program) -> Result<String, CanonicalSourceError> {
42    SourceFormatter::new(None).program_source(program)
43}
44
45/// Pretty-print a canonical Lashlang program using the host requirements saved
46/// with the artifact.
47pub fn canonical_program_source_with_requirements(
48    program: &Program,
49    requirements: &HostRequirements,
50) -> Result<String, CanonicalSourceError> {
51    SourceFormatter::new(Some(requirements)).program_source(program)
52}
53
54/// Pretty-print one process definition as a focused source fragment.
55///
56/// The returned fragment is the process declaration itself. It may reference
57/// type declarations, host constructors, resources, or other processes that are
58/// only self-contained in the full module source.
59pub fn canonical_process_source(process: &ProcessDecl) -> Result<String, CanonicalSourceError> {
60    SourceFormatter::new(None).process_source(process)
61}
62
63/// Pretty-print one process definition using the host requirements saved with
64/// the artifact.
65pub fn canonical_process_source_with_requirements(
66    process: &ProcessDecl,
67    requirements: &HostRequirements,
68) -> Result<String, CanonicalSourceError> {
69    SourceFormatter::new(Some(requirements)).process_source(process)
70}
71
72struct SourceFormatter<'a> {
73    requirements: Option<&'a HostRequirements>,
74}
75
76impl<'a> SourceFormatter<'a> {
77    fn new(requirements: Option<&'a HostRequirements>) -> Self {
78        Self { requirements }
79    }
80
81    fn program_source(&self, program: &Program) -> Result<String, CanonicalSourceError> {
82        let mut sections = Vec::new();
83        for declaration in &program.declarations {
84            sections.push(self.declaration_source(declaration)?);
85        }
86
87        let main = self.main_source(&program.main)?;
88        if !main.is_empty() {
89            sections.push(main);
90        }
91
92        Ok(finish_source(sections))
93    }
94
95    fn process_source(&self, process: &ProcessDecl) -> Result<String, CanonicalSourceError> {
96        let mut out = String::new();
97        self.write_process(&mut out, process)?;
98        if !out.is_empty() {
99            out.push('\n');
100        }
101        Ok(out)
102    }
103
104    fn declaration_source(
105        &self,
106        declaration: &Declaration,
107    ) -> Result<String, CanonicalSourceError> {
108        match declaration {
109            Declaration::Type(type_decl) => {
110                let mut out = String::new();
111                out.push_str("type ");
112                out.push_str(&format_identifier("type name", type_decl.name.as_str())?);
113                out.push_str(" = ");
114                out.push_str(&self.type_source(&type_decl.ty)?);
115                Ok(out)
116            }
117            Declaration::Process(process) => {
118                let mut out = String::new();
119                self.write_process(&mut out, process)?;
120                Ok(out)
121            }
122        }
123    }
124
125    fn write_process(
126        &self,
127        out: &mut String,
128        process: &ProcessDecl,
129    ) -> Result<(), CanonicalSourceError> {
130        if let Some(label) = &process.label {
131            out.push_str(&label_source(label));
132            out.push('\n');
133        }
134        out.push_str("process ");
135        out.push_str(&format_identifier("process name", process.name.as_str())?);
136        out.push('(');
137        for (index, param) in process.params.iter().enumerate() {
138            if index > 0 {
139                out.push_str(", ");
140            }
141            out.push_str(&format_identifier(
142                "process parameter",
143                param.name.as_str(),
144            )?);
145            out.push_str(": ");
146            out.push_str(&self.type_source(&param.ty)?);
147        }
148        out.push(')');
149        if !process.signals.is_empty() {
150            out.push_str(" signals { ");
151            for (index, signal) in process.signals.iter().enumerate() {
152                if index > 0 {
153                    out.push_str(", ");
154                }
155                out.push_str(&format_key_name(signal.name.as_str()));
156                out.push_str(": ");
157                out.push_str(&self.type_source(&signal.ty)?);
158            }
159            out.push_str(" }");
160        }
161        if let Some(return_ty) = &process.return_ty {
162            out.push_str(" -> ");
163            out.push_str(&self.type_source(return_ty)?);
164        }
165        out.push(' ');
166        out.push_str(&self.block_source(&process.body, 0)?);
167        Ok(())
168    }
169
170    fn main_source(&self, main: &Expr) -> Result<String, CanonicalSourceError> {
171        match main {
172            Expr::Block(expressions) => {
173                let mut out = String::new();
174                self.write_statements(&mut out, expressions, 0)?;
175                Ok(trim_trailing_newline(out))
176            }
177            expr => self.statement_line(expr, 0).map(trim_trailing_newline),
178        }
179    }
180
181    fn block_source(&self, expr: &Expr, indent: usize) -> Result<String, CanonicalSourceError> {
182        let Expr::Block(expressions) = expr else {
183            let mut out = String::new();
184            out.push_str("{\n");
185            out.push_str(&self.statement_line(expr, indent + 1)?);
186            out.push_str(&indent_string(indent));
187            out.push('}');
188            return Ok(out);
189        };
190        if expressions.is_empty() {
191            return Ok("{}".to_string());
192        }
193        let mut out = String::new();
194        out.push_str("{\n");
195        self.write_statements(&mut out, expressions, indent + 1)?;
196        out.push_str(&indent_string(indent));
197        out.push('}');
198        Ok(out)
199    }
200
201    fn write_statements(
202        &self,
203        out: &mut String,
204        expressions: &[Expr],
205        indent: usize,
206    ) -> Result<(), CanonicalSourceError> {
207        for expr in expressions {
208            out.push_str(&self.statement_line(expr, indent)?);
209        }
210        Ok(())
211    }
212
213    fn statement_line(&self, expr: &Expr, indent: usize) -> Result<String, CanonicalSourceError> {
214        let prefix = indent_string(indent);
215        let mut out = String::new();
216        match expr {
217            Expr::LabelAnnotated { label, expr } => {
218                out.push_str(&prefix);
219                out.push_str(&label_source(label));
220                out.push('\n');
221                out.push_str(&self.statement_line(expr, indent)?);
222            }
223            Expr::If {
224                condition,
225                then_block,
226                else_block,
227            } if is_statement_block(then_block) => {
228                out.push_str(&prefix);
229                out.push_str(&self.if_statement_source(condition, then_block, else_block, indent)?);
230                out.push('\n');
231            }
232            Expr::For {
233                binding,
234                iterable,
235                body,
236            } => {
237                out.push_str(&prefix);
238                out.push_str("for ");
239                out.push_str(&format_identifier("for binding", binding.as_str())?);
240                out.push_str(" in ");
241                out.push_str(&self.expr_source(iterable)?);
242                out.push(' ');
243                out.push_str(&self.block_source(body, indent)?);
244                out.push('\n');
245            }
246            Expr::While { condition, body } => {
247                out.push_str(&prefix);
248                out.push_str("while ");
249                out.push_str(&self.expr_source(condition)?);
250                out.push(' ');
251                out.push_str(&self.block_source(body, indent)?);
252                out.push('\n');
253            }
254            _ => {
255                out.push_str(&prefix);
256                out.push_str(&self.statement_expr_source(expr)?);
257                out.push('\n');
258            }
259        }
260        Ok(out)
261    }
262
263    fn if_statement_source(
264        &self,
265        condition: &Expr,
266        then_block: &Expr,
267        else_block: &Expr,
268        indent: usize,
269    ) -> Result<String, CanonicalSourceError> {
270        let mut out = String::new();
271        out.push_str("if ");
272        out.push_str(&self.expr_source(condition)?);
273        out.push(' ');
274        out.push_str(&self.block_source(then_block, indent)?);
275        match else_block {
276            Expr::Block(expressions) if expressions.is_empty() => {}
277            Expr::If {
278                condition,
279                then_block,
280                else_block,
281            } if is_statement_block(then_block) => {
282                out.push_str(" else ");
283                out.push_str(&self.if_statement_source(condition, then_block, else_block, indent)?);
284            }
285            _ => {
286                out.push_str(" else ");
287                out.push_str(&self.block_source(else_block, indent)?);
288            }
289        }
290        Ok(out)
291    }
292
293    fn statement_expr_source(&self, expr: &Expr) -> Result<String, CanonicalSourceError> {
294        match expr {
295            Expr::Assign { target, expr } => {
296                let mut out = self.assign_target_source(target)?;
297                out.push_str(" = ");
298                out.push_str(&self.expr_source(expr)?);
299                Ok(out)
300            }
301            Expr::Break => Ok("break".to_string()),
302            Expr::Continue => Ok("continue".to_string()),
303            Expr::Cancel(expr) => Ok(format!("cancel {}", self.expr_source(expr)?)),
304            Expr::Print(expr) => Ok(format!("print {}", self.expr_source(expr)?)),
305            Expr::Yield(expr) => Ok(format!("yield {}", self.expr_source(expr)?)),
306            Expr::Wake(expr) => Ok(format!("wake {}", self.expr_source(expr)?)),
307            Expr::Finish(expr) => Ok(format!("finish {}", self.expr_source(expr)?)),
308            Expr::Fail(expr) => Ok(format!("fail {}", self.expr_source(expr)?)),
309            Expr::Block(_) => {
310                Err(CanonicalSourceError::NonSourceableExpression { kind: "bare block" })
311            }
312            expr => self.expr_source(expr),
313        }
314    }
315
316    fn expr_source(&self, expr: &Expr) -> Result<String, CanonicalSourceError> {
317        match expr {
318            Expr::Block(_) => Err(CanonicalSourceError::NonSourceableExpression { kind: "block" }),
319            Expr::LabelAnnotated { .. } => Err(CanonicalSourceError::NonSourceableExpression {
320                kind: "label-annotated expression",
321            }),
322            Expr::Null => Ok("null".to_string()),
323            Expr::Bool(value) => Ok(value.to_string()),
324            Expr::Number(value) => format_number(*value),
325            Expr::String(value) => Ok(format_string(value.as_str())),
326            Expr::Variable(name) => format_identifier("variable", name.as_str()),
327            Expr::Tuple(items) => Ok(self.tuple_source(items)?),
328            Expr::List(items) => {
329                let items = items
330                    .iter()
331                    .map(|item| self.expr_source(item))
332                    .collect::<Result<Vec<_>, _>>()?;
333                Ok(format!("[{}]", items.join(", ")))
334            }
335            Expr::ListComprehension { element, clauses } => {
336                let mut out = String::new();
337                out.push('[');
338                out.push_str(&self.expr_source(element)?);
339                for clause in clauses {
340                    match clause {
341                        ListComprehensionClause::For { binding, iterable } => {
342                            out.push_str(" for ");
343                            out.push_str(&format_identifier("comprehension binding", binding)?);
344                            out.push_str(" in ");
345                            out.push_str(&self.expr_source(iterable)?);
346                        }
347                        ListComprehensionClause::If { condition } => {
348                            out.push_str(" if ");
349                            out.push_str(&self.expr_source(condition)?);
350                        }
351                    }
352                }
353                out.push(']');
354                Ok(out)
355            }
356            Expr::Record(entries) if is_trigger_event_placeholder(entries) => {
357                Ok("trigger.event".to_string())
358            }
359            Expr::Record(entries) => {
360                let entries = entries
361                    .iter()
362                    .map(|(key, value)| {
363                        Ok(format!(
364                            "{}: {}",
365                            format_key_name(key.as_str()),
366                            self.expr_source(value)?
367                        ))
368                    })
369                    .collect::<Result<Vec<_>, CanonicalSourceError>>()?;
370                Ok(format!("{{ {} }}", entries.join(", ")))
371            }
372            Expr::Assign { .. } => {
373                Err(CanonicalSourceError::NonSourceableExpression { kind: "assignment" })
374            }
375            Expr::If {
376                condition,
377                then_block,
378                else_block,
379            } => {
380                if is_statement_block(then_block) || is_statement_block(else_block) {
381                    return Err(CanonicalSourceError::NonSourceableExpression {
382                        kind: "statement if",
383                    });
384                }
385                Ok(format!(
386                    "({} ? {} : {})",
387                    self.expr_source(condition)?,
388                    self.expr_source(then_block)?,
389                    self.expr_source(else_block)?
390                ))
391            }
392            Expr::For { .. } => Err(CanonicalSourceError::NonSourceableExpression { kind: "for" }),
393            Expr::While { .. } => {
394                Err(CanonicalSourceError::NonSourceableExpression { kind: "while" })
395            }
396            Expr::Break => Err(CanonicalSourceError::NonSourceableExpression { kind: "break" }),
397            Expr::Continue => {
398                Err(CanonicalSourceError::NonSourceableExpression { kind: "continue" })
399            }
400            Expr::StartProcess(start) => {
401                let args = start
402                    .args
403                    .iter()
404                    .map(|(key, value)| {
405                        Ok(format!(
406                            "{}: {}",
407                            format_key_name(key.as_str()),
408                            self.expr_source(value)?
409                        ))
410                    })
411                    .collect::<Result<Vec<_>, CanonicalSourceError>>()?;
412                Ok(format!(
413                    "start {}({})",
414                    format_identifier("process name", start.process.as_str())?,
415                    args.join(", ")
416                ))
417            }
418            Expr::ProcessRef { process } => format_identifier("process name", process.as_str()),
419            Expr::HostDescriptorConstructor { type_name, input } => {
420                let path = self.constructor_path(type_name.as_str())?;
421                Ok(format!(
422                    "{}({})",
423                    format_receiver_path("constructor", &path)?,
424                    self.expr_source(input)?
425                ))
426            }
427            Expr::ResourceRef(resource) => self.resource_ref_source(resource),
428            Expr::ReceiverCall {
429                receiver,
430                operation,
431                args,
432            } => {
433                let args = args
434                    .iter()
435                    .map(|arg| self.expr_source(arg))
436                    .collect::<Result<Vec<_>, _>>()?;
437                Ok(format!(
438                    "{}.{}({})",
439                    self.postfix_target_source(receiver)?,
440                    format_key_name(operation.as_str()),
441                    args.join(", ")
442                ))
443            }
444            Expr::Await(expr) => Ok(format!("await {}", self.unary_operand_source(expr)?)),
445            Expr::SleepFor(expr) => Ok(format!("sleep for {}", self.expr_source(expr)?)),
446            Expr::SleepUntil(expr) => Ok(format!("sleep until {}", self.expr_source(expr)?)),
447            Expr::WaitSignal { name } => Ok(format!("wait_signal({})", format_string(name))),
448            Expr::SignalRun { run, name, payload } => Ok(format!(
449                "signal_run({}, {}, {})",
450                self.expr_source(run)?,
451                format_string(name),
452                self.expr_source(payload)?
453            )),
454            Expr::ResultUnwrap(expr) => Ok(format!("{}?", self.postfix_target_source(expr)?)),
455            Expr::Cancel(expr) => Ok(format!("cancel {}", self.expr_source(expr)?)),
456            Expr::Print(expr) => Ok(format!("print {}", self.expr_source(expr)?)),
457            Expr::Yield(expr) => Ok(format!("yield {}", self.expr_source(expr)?)),
458            Expr::Wake(expr) => Ok(format!("wake {}", self.expr_source(expr)?)),
459            Expr::Finish(expr) => Ok(format!("finish {}", self.expr_source(expr)?)),
460            Expr::Fail(expr) => Ok(format!("fail {}", self.expr_source(expr)?)),
461            Expr::BuiltinCall { name, args } => {
462                let args = args
463                    .iter()
464                    .map(|arg| self.expr_source(arg))
465                    .collect::<Result<Vec<_>, _>>()?;
466                Ok(format!(
467                    "{}({})",
468                    format_identifier("builtin", name.as_str())?,
469                    args.join(", ")
470                ))
471            }
472            Expr::Field { target, field } => Ok(format!(
473                "{}.{}",
474                self.postfix_target_source(target)?,
475                format_key_name(field.as_str())
476            )),
477            Expr::Index { target, index } => Ok(format!(
478                "{}[{}]",
479                self.postfix_target_source(target)?,
480                self.expr_source(index)?
481            )),
482            Expr::Unary { op, expr } => {
483                let op = match op {
484                    UnaryOp::Negate => "-",
485                    UnaryOp::Not => "not ",
486                };
487                Ok(format!("{op}{}", self.unary_operand_source(expr)?))
488            }
489            Expr::Binary { left, op, right } => Ok(format!(
490                "({} {} {})",
491                self.expr_source(left)?,
492                binary_op_source(*op),
493                self.expr_source(right)?
494            )),
495            Expr::TypeLiteral(ty) => Ok(format!("Type {}", self.type_source(ty)?)),
496        }
497    }
498
499    fn postfix_target_source(&self, expr: &Expr) -> Result<String, CanonicalSourceError> {
500        match expr {
501            Expr::Null
502            | Expr::Bool(_)
503            | Expr::Number(_)
504            | Expr::String(_)
505            | Expr::Variable(_)
506            | Expr::Tuple(_)
507            | Expr::List(_)
508            | Expr::ListComprehension { .. }
509            | Expr::Record(_)
510            | Expr::StartProcess(_)
511            | Expr::ProcessRef { .. }
512            | Expr::HostDescriptorConstructor { .. }
513            | Expr::ResourceRef(_)
514            | Expr::ReceiverCall { .. }
515            | Expr::BuiltinCall { .. }
516            | Expr::Field { .. }
517            | Expr::Index { .. }
518            | Expr::ResultUnwrap(_)
519            | Expr::TypeLiteral(_) => self.expr_source(expr),
520            _ => Ok(format!("({})", self.expr_source(expr)?)),
521        }
522    }
523
524    fn unary_operand_source(&self, expr: &Expr) -> Result<String, CanonicalSourceError> {
525        match expr {
526            Expr::Null
527            | Expr::Bool(_)
528            | Expr::Number(_)
529            | Expr::String(_)
530            | Expr::Variable(_)
531            | Expr::Tuple(_)
532            | Expr::List(_)
533            | Expr::ListComprehension { .. }
534            | Expr::Record(_)
535            | Expr::StartProcess(_)
536            | Expr::ProcessRef { .. }
537            | Expr::HostDescriptorConstructor { .. }
538            | Expr::ResourceRef(_)
539            | Expr::ReceiverCall { .. }
540            | Expr::BuiltinCall { .. }
541            | Expr::Field { .. }
542            | Expr::Index { .. }
543            | Expr::ResultUnwrap(_)
544            | Expr::Unary { .. }
545            | Expr::TypeLiteral(_) => self.expr_source(expr),
546            _ => Ok(format!("({})", self.expr_source(expr)?)),
547        }
548    }
549
550    fn assign_target_source(&self, target: &AssignTarget) -> Result<String, CanonicalSourceError> {
551        let mut out = format_identifier("assignment target", target.root.as_str())?;
552        for step in &target.steps {
553            match step {
554                AssignPathStep::Field(field) => {
555                    out.push('.');
556                    out.push_str(&format_key_name(field.as_str()));
557                }
558                AssignPathStep::Index(index) => {
559                    out.push('[');
560                    out.push_str(&self.expr_source(index)?);
561                    out.push(']');
562                }
563            }
564        }
565        Ok(out)
566    }
567
568    fn tuple_source(&self, items: &[Expr]) -> Result<String, CanonicalSourceError> {
569        let items = items
570            .iter()
571            .map(|item| self.expr_source(item))
572            .collect::<Result<Vec<_>, _>>()?;
573        Ok(match items.as_slice() {
574            [] => "()".to_string(),
575            [item] => format!("({item},)"),
576            items => format!("({})", items.join(", ")),
577        })
578    }
579
580    fn type_source(&self, ty: &TypeExpr) -> Result<String, CanonicalSourceError> {
581        match ty {
582            TypeExpr::Any => Ok("any".to_string()),
583            TypeExpr::Str => Ok("str".to_string()),
584            TypeExpr::Int => Ok("int".to_string()),
585            TypeExpr::Float => Ok("float".to_string()),
586            TypeExpr::Bool => Ok("bool".to_string()),
587            TypeExpr::Dict => Ok("dict".to_string()),
588            TypeExpr::Null => Ok("null".to_string()),
589            TypeExpr::Enum(values) if values.is_empty() => {
590                Err(CanonicalSourceError::NonSourceableType { kind: "empty enum" })
591            }
592            TypeExpr::Enum(values) => Ok(format!(
593                "enum[{}]",
594                values
595                    .iter()
596                    .map(|value| format_string(value.as_str()))
597                    .collect::<Vec<_>>()
598                    .join(", ")
599            )),
600            TypeExpr::List(item) => Ok(format!("list[{}]", self.type_source(item)?)),
601            TypeExpr::Object(fields) => self.object_type_source(fields),
602            TypeExpr::Ref(name) => format_type_ref(name.as_str()),
603            TypeExpr::Process { input_count, .. } if *input_count != 1 => {
604                Err(CanonicalSourceError::NonSourceableType {
605                    kind: "multi-input process",
606                })
607            }
608            TypeExpr::Process { input, output, .. } => Ok(format!(
609                "Process<{}, {}>",
610                self.type_source(input)?,
611                self.type_source(output)?
612            )),
613            TypeExpr::TriggerHandle(event) => {
614                Ok(format!("TriggerHandle<{}>", self.type_source(event)?))
615            }
616            TypeExpr::Union(items) if items.len() < 2 => {
617                Err(CanonicalSourceError::NonSourceableType {
618                    kind: "single-variant union",
619                })
620            }
621            TypeExpr::Union(items) => {
622                let items = items
623                    .iter()
624                    .map(|item| self.type_source(item))
625                    .collect::<Result<Vec<_>, _>>()?;
626                Ok(items.join(" | "))
627            }
628        }
629    }
630
631    fn object_type_source(&self, fields: &[TypeField]) -> Result<String, CanonicalSourceError> {
632        let fields = fields
633            .iter()
634            .map(|field| {
635                let optional = if field.optional { "?" } else { "" };
636                Ok(format!(
637                    "{}: {}{}",
638                    format_key_name(field.name.as_str()),
639                    self.type_source(&field.ty)?,
640                    optional
641                ))
642            })
643            .collect::<Result<Vec<_>, CanonicalSourceError>>()?;
644        Ok(format!("{{ {} }}", fields.join(", ")))
645    }
646
647    fn resource_ref_source(
648        &self,
649        resource: &ResourceRefExpr,
650    ) -> Result<String, CanonicalSourceError> {
651        if !resource.path.is_empty() {
652            return format_receiver_path(
653                "resource",
654                &resource
655                    .path
656                    .iter()
657                    .map(|segment| segment.to_string())
658                    .collect::<Vec<_>>(),
659            );
660        }
661        if resource.alias.is_empty() {
662            return Err(CanonicalSourceError::InvalidPath {
663                context: "resource",
664                path: String::new(),
665            });
666        }
667        format_receiver_path(
668            "resource",
669            &resource
670                .alias
671                .split('.')
672                .map(ToString::to_string)
673                .collect::<Vec<_>>(),
674        )
675    }
676
677    fn constructor_path(&self, type_name: &str) -> Result<Vec<String>, CanonicalSourceError> {
678        let Some(requirements) = self.requirements else {
679            return Err(CanonicalSourceError::UnknownHostDescriptorConstructor {
680                type_name: type_name.to_string(),
681            });
682        };
683        let paths = requirements
684            .resources
685            .value_constructors()
686            .filter(|(_, constructor)| constructor.type_name == type_name)
687            .map(|(_, constructor)| constructor.path.clone())
688            .collect::<Vec<_>>();
689        match paths.as_slice() {
690            [path] => Ok(path.clone()),
691            [] => Err(CanonicalSourceError::UnknownHostDescriptorConstructor {
692                type_name: type_name.to_string(),
693            }),
694            paths => Err(CanonicalSourceError::AmbiguousHostDescriptorConstructor {
695                type_name: type_name.to_string(),
696                paths: paths.iter().map(|path| path.join(".")).collect::<Vec<_>>(),
697            }),
698        }
699    }
700}
701
702fn finish_source(sections: Vec<String>) -> String {
703    let mut out = sections.join("\n\n");
704    if !out.is_empty() {
705        out.push('\n');
706    }
707    out
708}
709
710fn trim_trailing_newline(mut out: String) -> String {
711    if out.ends_with('\n') {
712        out.pop();
713    }
714    out
715}
716
717fn indent_string(indent: usize) -> String {
718    "  ".repeat(indent)
719}
720
721fn is_statement_block(expr: &Expr) -> bool {
722    matches!(expr, Expr::Block(_))
723}
724
725fn is_trigger_event_placeholder(entries: &[(crate::ast::AstString, Expr)]) -> bool {
726    matches!(
727        entries,
728        [(key, Expr::Bool(true))] if key.as_str() == crate::trigger::LASH_TRIGGER_EVENT_KEY
729    )
730}
731
732fn label_source(label: &LabelMetadata) -> String {
733    let mut out = String::from("@label(title: ");
734    out.push_str(&format_string(label.title.as_str()));
735    if let Some(description) = &label.description {
736        out.push_str(", description: ");
737        out.push_str(&format_string(description.as_str()));
738    }
739    out.push(')');
740    out
741}
742
743fn format_number(value: f64) -> Result<String, CanonicalSourceError> {
744    if !value.is_finite() || value.is_sign_negative() {
745        return Err(CanonicalSourceError::UnsupportedNumber {
746            value: value.to_string(),
747        });
748    }
749    Ok(value.to_string())
750}
751
752fn format_string(value: &str) -> String {
753    let mut out = String::with_capacity(value.len() + 2);
754    out.push('"');
755    for ch in value.chars() {
756        match ch {
757            '"' => out.push_str("\\\""),
758            '\\' => out.push_str("\\\\"),
759            '\n' => out.push_str("\\n"),
760            '\r' => out.push_str("\\r"),
761            '\t' => out.push_str("\\t"),
762            other => out.push(other),
763        }
764    }
765    out.push('"');
766    out
767}
768
769fn format_identifier(context: &'static str, name: &str) -> Result<String, CanonicalSourceError> {
770    if is_identifier(name) {
771        return Ok(name.to_string());
772    }
773    Err(CanonicalSourceError::InvalidIdentifier {
774        context,
775        name: name.to_string(),
776    })
777}
778
779fn format_key_name(name: &str) -> String {
780    if is_bare_key(name) {
781        name.to_string()
782    } else {
783        format_string(name)
784    }
785}
786
787fn format_type_ref(name: &str) -> Result<String, CanonicalSourceError> {
788    let segments = name.split('.').collect::<Vec<_>>();
789    if segments.iter().all(|segment| is_identifier(segment)) {
790        return Ok(name.to_string());
791    }
792    Err(CanonicalSourceError::InvalidPath {
793        context: "type reference",
794        path: name.to_string(),
795    })
796}
797
798fn format_receiver_path(
799    context: &'static str,
800    path: &[String],
801) -> Result<String, CanonicalSourceError> {
802    let Some((root, rest)) = path.split_first() else {
803        return Err(CanonicalSourceError::InvalidPath {
804            context,
805            path: String::new(),
806        });
807    };
808    let mut out = format_identifier(context, root)?;
809    for segment in rest {
810        out.push('.');
811        out.push_str(&format_key_name(segment));
812    }
813    Ok(out)
814}
815
816fn is_identifier(name: &str) -> bool {
817    let mut chars = name.chars();
818    let Some(first) = chars.next() else {
819        return false;
820    };
821    if !(first == '_' || first.is_ascii_alphabetic()) {
822        return false;
823    }
824    chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) && !is_hard_keyword(name)
825}
826
827fn is_bare_key(name: &str) -> bool {
828    let mut chars = name.chars();
829    let Some(first) = chars.next() else {
830        return false;
831    };
832    (first == '_' || first.is_ascii_alphabetic())
833        && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
834}
835
836fn is_hard_keyword(name: &str) -> bool {
837    matches!(
838        name,
839        "if" | "else"
840            | "for"
841            | "in"
842            | "await"
843            | "cancel"
844            | "submit"
845            | "print"
846            | "call"
847            | "and"
848            | "or"
849            | "not"
850            | "true"
851            | "false"
852            | "null"
853    )
854}
855
856fn binary_op_source(op: BinaryOp) -> &'static str {
857    match op {
858        BinaryOp::Add => "+",
859        BinaryOp::Subtract => "-",
860        BinaryOp::Multiply => "*",
861        BinaryOp::Divide => "/",
862        BinaryOp::Modulo => "%",
863        BinaryOp::Equal => "==",
864        BinaryOp::NotEqual => "!=",
865        BinaryOp::Less => "<",
866        BinaryOp::LessEqual => "<=",
867        BinaryOp::Greater => ">",
868        BinaryOp::GreaterEqual => ">=",
869        BinaryOp::And => "and",
870        BinaryOp::Or => "or",
871    }
872}
873
874#[cfg(test)]
875mod tests;