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#[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
35pub fn canonical_program_source(program: &Program) -> Result<String, CanonicalSourceError> {
42 SourceFormatter::new(None).program_source(program)
43}
44
45pub 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
54pub fn canonical_process_source(process: &ProcessDecl) -> Result<String, CanonicalSourceError> {
60 SourceFormatter::new(None).process_source(process)
61}
62
63pub 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(¶m.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;