1use std::collections::{HashMap, HashSet};
12use std::fmt::Write;
13use std::path::PathBuf;
14
15use bock_air::{AIRNode, AirInterpolationPart, EnumVariantPayload, NodeKind, ResultVariant};
16use bock_ast::{AssignOp, BinOp, ImportItems, Literal, UnaryOp, Visibility};
17use bock_errors::Span;
18use bock_types::AIRModule;
19
20use crate::error::CodegenError;
21use crate::generator::{CodeGenerator, GeneratedCode, OutputFile, SourceMap, SourceMapping};
22use crate::profile::TargetProfile;
23
24const CONCURRENCY_RUNTIME_JS: &str = "\
28// ── Bock concurrency runtime ──
29const __bockChannelNew = () => {
30 const queue = [];
31 const waiters = [];
32 const ch = {
33 send(v) {
34 if (waiters.length > 0) { waiters.shift()(v); } else { queue.push(v); }
35 },
36 recv() {
37 return new Promise((resolve) => {
38 if (queue.length > 0) { resolve(queue.shift()); }
39 else { waiters.push(resolve); }
40 });
41 },
42 close() {}
43 };
44 return [ch, ch];
45};
46const __bockSpawn = (x) => x;
47";
48
49#[derive(Debug)]
51pub struct JsGenerator {
52 profile: TargetProfile,
53}
54
55impl JsGenerator {
56 #[must_use]
58 pub fn new() -> Self {
59 Self {
60 profile: TargetProfile::javascript(),
61 }
62 }
63}
64
65impl Default for JsGenerator {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl CodeGenerator for JsGenerator {
72 fn target(&self) -> &TargetProfile {
73 &self.profile
74 }
75
76 fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
77 let mut ctx = EmitCtx::new();
78 ctx.emit_node(module)?;
79 let (content, mappings) = ctx.finish();
80 let source_map = SourceMap {
81 generated_file: "output.js".to_string(),
82 mappings,
83 ..Default::default()
84 };
85 Ok(GeneratedCode {
86 files: vec![OutputFile {
87 path: PathBuf::from("output.js"),
88 content,
89 }],
90 source_map: Some(source_map),
91 })
92 }
93
94 fn entry_invocation(&self, main_is_async: bool) -> Option<String> {
95 if main_is_async {
96 Some("(async () => { await main(); })();\n".to_string())
99 } else {
100 Some("main();\n".to_string())
101 }
102 }
103}
104
105struct EmitCtx {
109 buf: String,
110 indent: usize,
111 effect_ops: HashMap<String, String>,
113 current_handler_vars: HashMap<String, String>,
115 fn_effects: HashMap<String, Vec<String>>,
117 composite_effects: HashMap<String, Vec<String>>,
119 record_names: HashSet<String>,
121 cur_line: u32,
123 cur_col: u32,
125 scan_pos: usize,
127 last_marked: Option<(u32, u32)>,
130 mappings: Vec<SourceMapping>,
132}
133
134impl EmitCtx {
135 fn new() -> Self {
136 Self {
137 buf: String::with_capacity(4096),
138 indent: 0,
139 effect_ops: HashMap::new(),
140 current_handler_vars: HashMap::new(),
141 fn_effects: HashMap::new(),
142 composite_effects: HashMap::new(),
143 record_names: HashSet::new(),
144 cur_line: 1,
145 cur_col: 1,
146 scan_pos: 0,
147 last_marked: None,
148 mappings: Vec::new(),
149 }
150 }
151
152 fn finish(self) -> (String, Vec<SourceMapping>) {
153 (self.buf, self.mappings)
154 }
155
156 fn sync_pos(&mut self) {
159 if self.scan_pos >= self.buf.len() {
160 return;
161 }
162 let slice = &self.buf[self.scan_pos..];
163 for ch in slice.chars() {
164 if ch == '\n' {
165 self.cur_line += 1;
166 self.cur_col = 1;
167 } else {
168 self.cur_col += 1;
169 }
170 }
171 self.scan_pos = self.buf.len();
172 }
173
174 fn mark_span(&mut self, span: Span) {
177 if span.start == 0 && span.end == 0 {
178 return;
179 }
180 self.sync_pos();
181 let key = (self.cur_line, self.cur_col);
182 if self.last_marked == Some(key) {
183 return;
184 }
185 self.last_marked = Some(key);
186 self.mappings.push(SourceMapping {
187 gen_line: self.cur_line,
188 gen_col: self.cur_col,
189 src_line: 0,
190 src_col: 0,
191 src_offset: span.start as u32,
192 src_file_id: span.file.0,
193 });
194 }
195
196 fn indent_str(&self) -> String {
197 " ".repeat(self.indent)
198 }
199
200 fn write_indent(&mut self) {
201 let indent = self.indent_str();
202 self.buf.push_str(&indent);
203 }
204
205 fn writeln(&mut self, s: &str) {
206 self.write_indent();
207 self.buf.push_str(s);
208 self.buf.push('\n');
209 }
210
211 fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
215 let start = self.buf.len();
216 let saved_line = self.cur_line;
220 let saved_col = self.cur_col;
221 let saved_scan = self.scan_pos;
222 let saved_marked = self.last_marked;
223 let mappings_len = self.mappings.len();
224 self.emit_expr(node)?;
225 let s = self.buf[start..].to_string();
226 self.buf.truncate(start);
227 self.cur_line = saved_line;
228 self.cur_col = saved_col;
229 self.scan_pos = saved_scan;
230 self.last_marked = saved_marked;
231 self.mappings.truncate(mappings_len);
232 Ok(s)
233 }
234
235 fn map_prelude_call(
238 &mut self,
239 callee: &AIRNode,
240 args: &[bock_air::AirArg],
241 ) -> Result<Option<String>, CodegenError> {
242 let name = match &callee.kind {
243 NodeKind::Identifier { name } => name.name.as_str(),
244 _ => return Ok(None),
245 };
246 let arg_strs: Vec<String> = args
247 .iter()
248 .map(|a| self.expr_to_string(&a.value))
249 .collect::<Result<_, _>>()?;
250 let code = match name {
251 "println" => {
252 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
253 format!("console.log({a})")
254 }
255 "print" => {
256 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
257 format!("process.stdout.write(String({a}))")
258 }
259 "debug" => {
260 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
261 format!("console.debug({a})")
262 }
263 "assert" => {
264 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
265 format!("if (!{a}) throw new Error(\"assertion failed\")")
266 }
267 "todo" => "throw new Error(\"not implemented\")".to_string(),
268 "unreachable" => "throw new Error(\"unreachable\")".to_string(),
269 "sleep" => {
270 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
272 format!("new Promise((__r) => setTimeout(__r, Math.floor(({a}) / 1e6)))")
273 }
274 _ => return Ok(None),
275 };
276 Ok(Some(code))
277 }
278
279 fn module_uses_concurrency(&self, items: &[AIRNode]) -> bool {
284 items.iter().any(Self::node_uses_concurrency)
285 }
286
287 fn node_uses_concurrency(node: &AIRNode) -> bool {
288 let serialized = format!("{node:?}");
289 serialized.contains("\"Channel\"") || serialized.contains("\"spawn\"")
290 }
291
292 fn try_emit_concurrency_call(
296 &mut self,
297 callee: &AIRNode,
298 args: &[bock_air::AirArg],
299 ) -> Result<bool, CodegenError> {
300 if let NodeKind::Identifier { name } = &callee.kind {
302 if name.name == "spawn" {
303 self.buf.push_str("__bockSpawn(");
304 for (i, arg) in args.iter().enumerate() {
305 if i > 0 {
306 self.buf.push_str(", ");
307 }
308 self.emit_expr(&arg.value)?;
309 }
310 self.buf.push(')');
311 return Ok(true);
312 }
313 }
314 let NodeKind::FieldAccess { object, field } = &callee.kind else {
315 return Ok(false);
316 };
317 if let NodeKind::Identifier { name: type_name } = &object.kind {
319 if type_name.name == "Channel" && field.name == "new" {
320 self.buf.push_str("__bockChannelNew()");
321 return Ok(true);
322 }
323 }
324 if matches!(field.name.as_str(), "send" | "recv" | "close") {
328 self.emit_expr(object)?;
329 let _ = write!(self.buf, ".{}", field.name);
330 self.buf.push('(');
331 for (i, arg) in args.iter().skip(1).enumerate() {
332 if i > 0 {
333 self.buf.push_str(", ");
334 }
335 self.emit_expr(&arg.value)?;
336 }
337 self.buf.push(')');
338 return Ok(true);
339 }
340 Ok(false)
341 }
342
343 fn try_emit_time_assoc_call(
349 &mut self,
350 callee: &AIRNode,
351 args: &[bock_air::AirArg],
352 ) -> Result<bool, CodegenError> {
353 let NodeKind::FieldAccess { object, field } = &callee.kind else {
354 return Ok(false);
355 };
356 let NodeKind::Identifier { name: type_name } = &object.kind else {
357 return Ok(false);
358 };
359 let arg_strs: Vec<String> = args
360 .iter()
361 .map(|a| self.expr_to_string(&a.value))
362 .collect::<Result<_, _>>()?;
363 let arg0 = || arg_strs.first().cloned().unwrap_or_default();
364 let code = match (type_name.name.as_str(), field.name.as_str()) {
365 ("Duration", "zero") => "0".to_string(),
366 ("Duration", "nanos") => arg0(),
367 ("Duration", "micros") => format!("(({}) * 1000)", arg0()),
368 ("Duration", "millis") => format!("(({}) * 1000000)", arg0()),
369 ("Duration", "seconds") => format!("(({}) * 1000000000)", arg0()),
370 ("Duration", "minutes") => format!("(({}) * 60000000000)", arg0()),
371 ("Duration", "hours") => format!("(({}) * 3600000000000)", arg0()),
372 ("Instant", "now") => {
373 "(performance.now() * 1000000)".to_string()
374 }
375 _ => return Ok(false),
376 };
377 self.buf.push_str(&code);
378 Ok(true)
379 }
380
381 fn try_emit_time_desugared_method(
385 &mut self,
386 callee: &AIRNode,
387 args: &[bock_air::AirArg],
388 ) -> Result<bool, CodegenError> {
389 let NodeKind::FieldAccess { object, field } = &callee.kind else {
390 return Ok(false);
391 };
392 if let NodeKind::Identifier { name } = &object.kind {
394 if matches!(name.name.as_str(), "Duration" | "Instant") {
395 return Ok(false);
396 }
397 }
398 if !is_time_method_name(&field.name) {
399 return Ok(false);
400 }
401 let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
402 self.try_emit_time_method(object, &field.name, &remaining)
403 }
404
405 fn try_emit_time_method(
408 &mut self,
409 receiver: &AIRNode,
410 method: &str,
411 args: &[bock_air::AirArg],
412 ) -> Result<bool, CodegenError> {
413 let recv_str = self.expr_to_string(receiver)?;
414 let arg_strs: Vec<String> = args
415 .iter()
416 .map(|a| self.expr_to_string(&a.value))
417 .collect::<Result<_, _>>()?;
418 let code = match method {
419 "as_nanos" => format!("({recv_str})"),
420 "as_millis" => format!("Math.floor(({recv_str}) / 1000000)"),
421 "as_seconds" => format!("Math.floor(({recv_str}) / 1000000000)"),
422 "is_zero" => format!("(({recv_str}) === 0)"),
423 "is_negative" => format!("(({recv_str}) < 0)"),
424 "abs" => format!("Math.abs({recv_str})"),
425 "elapsed" => format!("((performance.now() * 1000000) - ({recv_str}))"),
426 "duration_since" => {
427 let other = arg_strs.first().cloned().unwrap_or_default();
428 format!("(({recv_str}) - ({other}))")
429 }
430 _ => return Ok(false),
431 };
432 self.buf.push_str(&code);
433 Ok(true)
434 }
435
436 fn try_emit_prelude_ctor(
440 &mut self,
441 callee: &AIRNode,
442 args: &[bock_air::AirArg],
443 ) -> Result<bool, CodegenError> {
444 let name = match &callee.kind {
445 NodeKind::Identifier { name } => name.name.as_str(),
446 _ => return Ok(false),
447 };
448 if !matches!(name, "Some" | "Ok" | "Err") {
449 return Ok(false);
450 }
451 let _ = write!(self.buf, "{{ _tag: \"{name}\"");
452 if let Some(arg) = args.first() {
453 self.buf.push_str(", _0: ");
454 self.emit_expr(&arg.value)?;
455 }
456 self.buf.push_str(" }");
457 Ok(true)
458 }
459
460 fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
463 self.mark_span(node.span);
464 match &node.kind {
465 NodeKind::Module { imports, items, .. } => {
466 if self.module_uses_concurrency(items) {
467 self.buf.push_str(CONCURRENCY_RUNTIME_JS);
468 self.buf.push('\n');
469 }
470 for imp in imports {
471 self.emit_node(imp)?;
472 }
473 if !imports.is_empty() && !items.is_empty() {
474 self.buf.push('\n');
475 }
476 for (i, item) in items.iter().enumerate() {
477 if i > 0 {
478 self.buf.push('\n');
479 }
480 self.emit_node(item)?;
481 }
482 Ok(())
483 }
484 NodeKind::ImportDecl { path, items } => {
485 let path_str = path
487 .segments
488 .iter()
489 .map(|s| s.name.as_str())
490 .collect::<Vec<_>>()
491 .join(".");
492 match items {
493 ImportItems::Module => {
494 self.writeln(&format!("// import {path_str}"));
495 }
496 ImportItems::Named(names) => {
497 let names_str = names
498 .iter()
499 .map(|n| n.name.name.as_str())
500 .collect::<Vec<_>>()
501 .join(", ");
502 self.writeln(&format!("// import {{ {names_str} }} from {path_str}"));
503 }
504 ImportItems::Glob => {
505 self.writeln(&format!("// import * from {path_str}"));
506 }
507 }
508 Ok(())
509 }
510 NodeKind::FnDecl {
511 visibility,
512 is_async,
513 name,
514 params,
515 effect_clause,
516 body,
517 ..
518 } => self.emit_fn_decl(
519 *visibility,
520 *is_async,
521 &name.name,
522 params,
523 effect_clause,
524 body,
525 false,
526 ),
527 NodeKind::RecordDecl { name, fields, .. } => {
528 self.record_names.insert(name.name.clone());
530 if fields.is_empty() {
531 self.writeln(&format!("class {} {{}}", name.name));
532 } else {
533 let field_names: Vec<&str> =
534 fields.iter().map(|f| f.name.name.as_str()).collect();
535 self.writeln(&format!("class {} {{", name.name));
536 self.indent += 1;
537 self.writeln(&format!(
538 "constructor({{ {} }}) {{",
539 field_names.join(", "),
540 ));
541 self.indent += 1;
542 for f in &field_names {
543 self.writeln(&format!("this.{f} = {f};"));
544 }
545 self.indent -= 1;
546 self.writeln("}");
547 self.indent -= 1;
548 self.writeln("}");
549 }
550 Ok(())
551 }
552 NodeKind::EnumDecl { name, variants, .. } => {
553 for variant in variants {
555 self.emit_enum_variant(&name.name, variant)?;
556 }
557 Ok(())
558 }
559 NodeKind::ClassDecl {
560 name,
561 fields,
562 methods,
563 ..
564 } => {
565 self.writeln(&format!("class {} {{", name.name));
566 self.indent += 1;
567 let field_names: Vec<&str> = fields.iter().map(|f| f.name.name.as_str()).collect();
569 self.writeln(&format!("constructor({}) {{", field_names.join(", ")));
570 self.indent += 1;
571 for f in &field_names {
572 self.writeln(&format!("this.{f} = {f};"));
573 }
574 self.indent -= 1;
575 self.writeln("}");
576 for method in methods {
578 self.buf.push('\n');
579 self.emit_class_method(method)?;
580 }
581 self.indent -= 1;
582 self.writeln("}");
583 Ok(())
584 }
585 NodeKind::TraitDecl { name, methods, .. } => {
586 self.writeln(&format!("// trait {}", name.name));
588 self.writeln(&format!("const {} = {{", name.name));
589 self.indent += 1;
590 for (i, method) in methods.iter().enumerate() {
591 if i > 0 {
592 self.buf.push('\n');
593 }
594 if let NodeKind::FnDecl {
595 name, params, body, ..
596 } = &method.kind
597 {
598 let param_names = self.collect_param_names(params);
599 self.writeln(&format!("{}({}) {{", name.name, param_names.join(", ")));
600 self.indent += 1;
601 self.emit_block_body(body)?;
602 self.indent -= 1;
603 self.writeln("},");
604 }
605 }
606 self.indent -= 1;
607 self.writeln("};");
608 Ok(())
609 }
610 NodeKind::ImplBlock {
611 trait_path,
612 target,
613 methods,
614 ..
615 } => {
616 let target_name = self.type_expr_to_string(target);
618 if let Some(tp) = trait_path {
619 let trait_name = tp
620 .segments
621 .iter()
622 .map(|s| s.name.as_str())
623 .collect::<Vec<_>>()
624 .join(".");
625 self.writeln(&format!("// impl {trait_name} for {target_name}"));
626 } else {
627 self.writeln(&format!("// impl {target_name}"));
628 }
629 for method in methods {
630 if let NodeKind::FnDecl {
631 is_async,
632 name,
633 params,
634 effect_clause,
635 body,
636 ..
637 } = &method.kind
638 {
639 let async_kw = if *is_async { "async " } else { "" };
640 let param_names = self.collect_param_names(params);
641 let effects_param = self.effects_param(effect_clause);
642 let mut all_params = param_names;
643 if let Some(ep) = effects_param {
644 all_params.push(ep);
645 }
646 self.writeln(&format!(
647 "{target_name}.prototype.{} = {async_kw}function({}) {{",
648 name.name,
649 all_params.join(", "),
650 ));
651 self.indent += 1;
652 let old_handler_vars = self.current_handler_vars.clone();
653 let expanded = self.expand_effect_names(effect_clause);
654 for ename in &expanded {
655 self.current_handler_vars
656 .insert(ename.clone(), to_camel_case(ename));
657 }
658 self.emit_block_body(body)?;
659 self.current_handler_vars = old_handler_vars;
660 self.indent -= 1;
661 self.writeln("};");
662 }
663 }
664 Ok(())
665 }
666 NodeKind::EffectDecl {
667 name,
668 components,
669 operations,
670 ..
671 } => {
672 if !components.is_empty() {
674 let comp_names: Vec<String> = components
675 .iter()
676 .map(|tp| {
677 tp.segments
678 .last()
679 .map_or("effect".to_string(), |s| s.name.clone())
680 })
681 .collect();
682 self.writeln(&format!(
683 "// composite effect {} = {}",
684 name.name,
685 comp_names.join(" + ")
686 ));
687 self.composite_effects
688 .insert(name.name.clone(), comp_names);
689 return Ok(());
690 }
691 for op in operations {
693 if let NodeKind::FnDecl {
694 name: op_name, ..
695 } = &op.kind
696 {
697 self.effect_ops
698 .insert(op_name.name.clone(), name.name.clone());
699 }
700 }
701 self.writeln(&format!("class {} {{", name.name));
703 self.indent += 1;
704 for op in operations {
705 if let NodeKind::FnDecl {
706 name, params, ..
707 } = &op.kind
708 {
709 let param_names = self.collect_param_names(params);
710 self.writeln(&format!(
711 "{}({}) {{",
712 to_camel_case(&name.name),
713 param_names.join(", "),
714 ));
715 self.indent += 1;
716 self.writeln("throw new Error(\"not implemented\");");
717 self.indent -= 1;
718 self.writeln("}");
719 }
720 }
721 self.indent -= 1;
722 self.writeln("}");
723 Ok(())
724 }
725 NodeKind::TypeAlias { name, .. } => {
726 self.writeln(&format!("// type {} = ...", name.name));
728 Ok(())
729 }
730 NodeKind::ConstDecl { name, value, .. } => {
731 let ind = self.indent_str();
732 let _ = write!(self.buf, "{ind}const {} = ", name.name);
733 self.emit_expr(value)?;
734 self.buf.push_str(";\n");
735 Ok(())
736 }
737 NodeKind::ModuleHandle { effect, handler } => {
738 let effect_name =
739 effect.segments.last().map_or("effect", |s| s.name.as_str());
740 let var_name = format!("__{}", to_camel_case(effect_name));
741 let ind = self.indent_str();
742 let _ = write!(self.buf, "{ind}const {var_name} = ");
743 self.emit_expr(handler)?;
744 self.buf.push_str(";\n");
745 self.current_handler_vars
747 .insert(effect_name.to_string(), var_name);
748 Ok(())
749 }
750 NodeKind::PropertyTest { name, body, .. } => {
751 self.writeln(&format!("// property test: {name}"));
752 self.writeln("// (property tests are not emitted in JS output)");
753 let _ = body;
754 Ok(())
755 }
756 NodeKind::LetBinding { .. }
758 | NodeKind::If { .. }
759 | NodeKind::For { .. }
760 | NodeKind::While { .. }
761 | NodeKind::Loop { .. }
762 | NodeKind::Return { .. }
763 | NodeKind::Break { .. }
764 | NodeKind::Continue
765 | NodeKind::Guard { .. }
766 | NodeKind::Match { .. }
767 | NodeKind::Block { .. }
768 | NodeKind::HandlingBlock { .. }
769 | NodeKind::Assign { .. } => self.emit_stmt(node),
770 _ => {
772 self.write_indent();
773 self.emit_expr(node)?;
774 self.buf.push_str(";\n");
775 Ok(())
776 }
777 }
778 }
779
780 #[allow(clippy::too_many_arguments)]
783 fn emit_fn_decl(
784 &mut self,
785 visibility: Visibility,
786 is_async: bool,
787 name: &str,
788 params: &[AIRNode],
789 effect_clause: &[bock_ast::TypePath],
790 body: &AIRNode,
791 _is_method: bool,
792 ) -> Result<(), CodegenError> {
793 let export = if matches!(visibility, Visibility::Public) {
794 "export "
795 } else {
796 ""
797 };
798 let async_kw = if is_async { "async " } else { "" };
799 let param_names = self.collect_param_names(params);
800 let effects_param = self.effects_param(effect_clause);
801 let mut all_params = param_names;
802 if let Some(ep) = effects_param {
803 all_params.push(ep);
804 }
805 if !effect_clause.is_empty() {
806 let effect_names = self.expand_effect_names(effect_clause);
807 self.fn_effects.insert(name.to_string(), effect_names);
808 }
809 let js_name = to_camel_case(name);
810 self.writeln(&format!(
811 "{export}{async_kw}function {js_name}({}) {{",
812 all_params.join(", "),
813 ));
814 self.indent += 1;
815 let old_handler_vars = self.current_handler_vars.clone();
816 let expanded = self.expand_effect_names(effect_clause);
817 for ename in &expanded {
818 self.current_handler_vars
819 .insert(ename.clone(), to_camel_case(ename));
820 }
821 self.emit_block_body(body)?;
822 self.current_handler_vars = old_handler_vars;
823 self.indent -= 1;
824 self.writeln("}");
825 Ok(())
826 }
827
828 fn emit_class_method(&mut self, method: &AIRNode) -> Result<(), CodegenError> {
829 if let NodeKind::FnDecl {
830 is_async,
831 name,
832 params,
833 effect_clause,
834 body,
835 ..
836 } = &method.kind
837 {
838 let async_kw = if *is_async { "async " } else { "" };
839 let param_names = self.collect_param_names(params);
840 let effects_param = self.effects_param(effect_clause);
841 let mut all_params = param_names;
842 if let Some(ep) = effects_param {
843 all_params.push(ep);
844 }
845 let method_name = to_camel_case(&name.name);
846 self.writeln(&format!(
847 "{async_kw}{method_name}({}) {{",
848 all_params.join(", "),
849 ));
850 self.indent += 1;
851 let old_handler_vars = self.current_handler_vars.clone();
852 let expanded = self.expand_effect_names(effect_clause);
853 for ename in &expanded {
854 self.current_handler_vars
855 .insert(ename.clone(), to_camel_case(ename));
856 }
857 self.emit_block_body(body)?;
858 self.current_handler_vars = old_handler_vars;
859 self.indent -= 1;
860 self.writeln("}");
861 }
862 Ok(())
863 }
864
865 fn collect_param_names(&self, params: &[AIRNode]) -> Vec<String> {
866 params
867 .iter()
868 .filter_map(|p| {
869 if let NodeKind::Param {
870 pattern, default, ..
871 } = &p.kind
872 {
873 let name = self.pattern_to_binding_name(pattern);
874 if let Some(def) = default {
875 let mut ctx = EmitCtx::new();
876 ctx.indent = self.indent;
877 if ctx.emit_expr_to_string(def).is_ok() {
878 let (def_str, _) = ctx.finish();
879 return Some(format!("{name} = {def_str}"));
880 }
881 }
882 Some(name)
883 } else {
884 None
885 }
886 })
887 .collect()
888 }
889
890 fn emit_expr_to_string(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
891 self.emit_expr(node)
892 }
893
894 fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
896 let mut result = Vec::new();
897 for tp in effects {
898 let name = tp
899 .segments
900 .last()
901 .map_or("effect".to_string(), |s| s.name.clone());
902 if let Some(components) = self.composite_effects.get(&name) {
903 result.extend(components.iter().cloned());
904 } else {
905 result.push(name);
906 }
907 }
908 result
909 }
910
911 fn effects_param(&self, effects: &[bock_ast::TypePath]) -> Option<String> {
913 if effects.is_empty() {
914 return None;
915 }
916 let expanded = self.expand_effect_names(effects);
917 if expanded.is_empty() {
918 return None;
919 }
920 let names: Vec<String> = expanded.iter().map(|n| to_camel_case(n)).collect();
921 Some(format!("{{ {} }}", names.join(", ")))
922 }
923
924 fn build_effects_call_arg_js(&self, fn_name: &str) -> Option<String> {
927 let effects = self.fn_effects.get(fn_name)?;
928 let entries: Vec<String> = effects
929 .iter()
930 .filter_map(|e| {
931 let handler_var = self.current_handler_vars.get(e)?;
932 let param_name = to_camel_case(e);
933 Some(format!("{param_name}: {handler_var}"))
934 })
935 .collect();
936 if entries.is_empty() {
937 return None;
938 }
939 Some(format!("{{ {} }}", entries.join(", ")))
940 }
941
942 fn emit_enum_variant(
945 &mut self,
946 enum_name: &str,
947 variant: &AIRNode,
948 ) -> Result<(), CodegenError> {
949 if let NodeKind::EnumVariant { name, payload } = &variant.kind {
950 let vname = &name.name;
951 match payload {
952 EnumVariantPayload::Unit => {
953 self.writeln(&format!(
954 "const {enum_name}_{vname} = Object.freeze({{ _tag: \"{vname}\" }});"
955 ));
956 }
957 EnumVariantPayload::Struct(fields) => {
958 let field_names: Vec<&str> =
959 fields.iter().map(|f| f.name.name.as_str()).collect();
960 self.writeln(&format!(
961 "function {enum_name}_{vname}({}) {{",
962 field_names.join(", ")
963 ));
964 self.indent += 1;
965 self.writeln(&format!(
966 "return {{ _tag: \"{vname}\", {} }};",
967 field_names.join(", ")
968 ));
969 self.indent -= 1;
970 self.writeln("}");
971 }
972 EnumVariantPayload::Tuple(elems) => {
973 let param_names: Vec<String> =
974 (0..elems.len()).map(|i| format!("_{i}")).collect();
975 self.writeln(&format!(
976 "function {enum_name}_{vname}({}) {{",
977 param_names.join(", ")
978 ));
979 self.indent += 1;
980 self.writeln(&format!(
981 "return {{ _tag: \"{vname}\", {} }};",
982 param_names
983 .iter()
984 .enumerate()
985 .map(|(i, p)| format!("_{i}: {p}"))
986 .collect::<Vec<_>>()
987 .join(", ")
988 ));
989 self.indent -= 1;
990 self.writeln("}");
991 }
992 }
993 }
994 Ok(())
995 }
996
997 fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1000 self.mark_span(node.span);
1001 match &node.kind {
1002 NodeKind::LetBinding {
1003 is_mut,
1004 pattern,
1005 value,
1006 ..
1007 } => {
1008 let kw = if *is_mut { "let" } else { "const" };
1009 let binding = self.pattern_to_js_destructure(pattern);
1010 let ind = self.indent_str();
1011 let _ = write!(self.buf, "{ind}{kw} {binding} = ");
1012 self.emit_expr(value)?;
1013 self.buf.push_str(";\n");
1014 Ok(())
1015 }
1016 NodeKind::If {
1017 let_pattern,
1018 condition,
1019 then_block,
1020 else_block,
1021 } => {
1022 if let Some(pat) = let_pattern {
1023 let ind = self.indent_str();
1025 let _ = write!(self.buf, "{ind}if (");
1026 self.emit_expr(condition)?;
1027 self.buf.push_str(" != null) {\n");
1028 self.indent += 1;
1029 let binding = self.pattern_to_js_destructure(pat);
1030 self.writeln(&format!("const {binding} = "));
1031 self.emit_block_body(then_block)?;
1035 self.indent -= 1;
1036 } else {
1037 let ind = self.indent_str();
1038 let _ = write!(self.buf, "{ind}if (");
1039 self.emit_expr(condition)?;
1040 self.buf.push_str(") {\n");
1041 self.indent += 1;
1042 self.emit_block_body(then_block)?;
1043 self.indent -= 1;
1044 }
1045 if let Some(else_b) = else_block {
1046 if matches!(else_b.kind, NodeKind::If { .. }) {
1047 let ind = self.indent_str();
1048 let _ = write!(self.buf, "{ind}}} else ");
1049 self.emit_stmt(else_b)?;
1051 return Ok(());
1052 }
1053 self.writeln("} else {");
1054 self.indent += 1;
1055 self.emit_block_body(else_b)?;
1056 self.indent -= 1;
1057 }
1058 self.writeln("}");
1059 Ok(())
1060 }
1061 NodeKind::For {
1062 pattern,
1063 iterable,
1064 body,
1065 } => {
1066 let binding = self.pattern_to_js_destructure(pattern);
1067 let ind = self.indent_str();
1068 let _ = write!(self.buf, "{ind}for (const {binding} of ");
1069 self.emit_expr(iterable)?;
1070 self.buf.push_str(") {\n");
1071 self.indent += 1;
1072 self.emit_block_body(body)?;
1073 self.indent -= 1;
1074 self.writeln("}");
1075 Ok(())
1076 }
1077 NodeKind::While { condition, body } => {
1078 let ind = self.indent_str();
1079 let _ = write!(self.buf, "{ind}while (");
1080 self.emit_expr(condition)?;
1081 self.buf.push_str(") {\n");
1082 self.indent += 1;
1083 self.emit_block_body(body)?;
1084 self.indent -= 1;
1085 self.writeln("}");
1086 Ok(())
1087 }
1088 NodeKind::Loop { body } => {
1089 self.writeln("while (true) {");
1090 self.indent += 1;
1091 self.emit_block_body(body)?;
1092 self.indent -= 1;
1093 self.writeln("}");
1094 Ok(())
1095 }
1096 NodeKind::Return { value } => {
1097 if let Some(val) = value {
1098 let ind = self.indent_str();
1099 let _ = write!(self.buf, "{ind}return ");
1100 self.emit_expr(val)?;
1101 self.buf.push_str(";\n");
1102 } else {
1103 self.writeln("return;");
1104 }
1105 Ok(())
1106 }
1107 NodeKind::Break { value } => {
1108 if let Some(val) = value {
1109 let ind = self.indent_str();
1111 let _ = write!(self.buf, "{ind}/* break value: ");
1112 self.emit_expr(val)?;
1113 self.buf.push_str(" */ break;\n");
1114 } else {
1115 self.writeln("break;");
1116 }
1117 Ok(())
1118 }
1119 NodeKind::Continue => {
1120 self.writeln("continue;");
1121 Ok(())
1122 }
1123 NodeKind::Guard {
1124 condition,
1125 else_block,
1126 ..
1127 } => {
1128 let ind = self.indent_str();
1129 let _ = write!(self.buf, "{ind}if (!(");
1130 self.emit_expr(condition)?;
1131 self.buf.push_str(")) {\n");
1132 self.indent += 1;
1133 self.emit_block_body(else_block)?;
1134 self.indent -= 1;
1135 self.writeln("}");
1136 Ok(())
1137 }
1138 NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms),
1139 NodeKind::Block { stmts, tail } => {
1140 self.writeln("{");
1141 self.indent += 1;
1142 for s in stmts {
1143 self.emit_node(s)?;
1144 }
1145 if let Some(t) = tail {
1146 self.write_indent();
1147 self.emit_expr(t)?;
1148 self.buf.push_str(";\n");
1149 }
1150 self.indent -= 1;
1151 self.writeln("}");
1152 Ok(())
1153 }
1154 NodeKind::HandlingBlock { handlers, body } => {
1155 self.writeln("{");
1157 self.indent += 1;
1158 let old_handler_vars = self.current_handler_vars.clone();
1159 for h in handlers {
1160 let effect_name =
1161 h.effect.segments.last().map_or("effect", |s| s.name.as_str());
1162 let var_name = format!("__{}", to_camel_case(effect_name));
1163 let ind = self.indent_str();
1164 let _ = write!(self.buf, "{ind}const {var_name} = ");
1165 self.emit_expr(&h.handler)?;
1166 self.buf.push_str(";\n");
1167 self.current_handler_vars
1168 .insert(effect_name.to_string(), var_name);
1169 }
1170 if let NodeKind::Block { stmts, tail } = &body.kind {
1171 for s in stmts {
1172 self.emit_node(s)?;
1173 }
1174 if let Some(t) = tail {
1175 self.write_indent();
1176 self.emit_expr(t)?;
1177 self.buf.push_str(";\n");
1178 }
1179 } else {
1180 self.emit_stmt(body)?;
1181 }
1182 self.current_handler_vars = old_handler_vars;
1183 self.indent -= 1;
1184 self.writeln("}");
1185 Ok(())
1186 }
1187 NodeKind::Assign { op, target, value } => {
1188 let ind = self.indent_str();
1189 let _ = write!(self.buf, "{ind}");
1190 self.emit_expr(target)?;
1191 let op_str = match op {
1192 AssignOp::Assign => "=",
1193 AssignOp::AddAssign => "+=",
1194 AssignOp::SubAssign => "-=",
1195 AssignOp::MulAssign => "*=",
1196 AssignOp::DivAssign => "/=",
1197 AssignOp::RemAssign => "%=",
1198 };
1199 let _ = write!(self.buf, " {op_str} ");
1200 self.emit_expr(value)?;
1201 self.buf.push_str(";\n");
1202 Ok(())
1203 }
1204 _ => {
1205 self.write_indent();
1207 self.emit_expr(node)?;
1208 self.buf.push_str(";\n");
1209 Ok(())
1210 }
1211 }
1212 }
1213
1214 fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1217 self.mark_span(node.span);
1218 match &node.kind {
1219 NodeKind::Literal { lit } => {
1220 match lit {
1221 Literal::Int(s) => self.buf.push_str(s),
1222 Literal::Float(s) => self.buf.push_str(s),
1223 Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
1224 Literal::Char(s) => {
1225 self.buf.push('\'');
1226 self.buf.push_str(s);
1227 self.buf.push('\'');
1228 }
1229 Literal::String(s) => {
1230 self.buf.push('"');
1231 self.buf.push_str(&escape_js_string(s));
1232 self.buf.push('"');
1233 }
1234 Literal::Unit => self.buf.push_str("undefined"),
1235 }
1236 Ok(())
1237 }
1238 NodeKind::Identifier { name } => {
1239 if name.name == "None" {
1240 self.buf.push_str("{ _tag: \"None\" }");
1241 } else {
1242 self.buf.push_str(&to_camel_case(&name.name));
1243 }
1244 Ok(())
1245 }
1246 NodeKind::BinaryOp { op, left, right } => {
1247 self.buf.push('(');
1248 self.emit_expr(left)?;
1249 let op_str = match op {
1250 BinOp::Add => " + ",
1251 BinOp::Sub => " - ",
1252 BinOp::Mul => " * ",
1253 BinOp::Div => " / ",
1254 BinOp::Rem => " % ",
1255 BinOp::Pow => " ** ",
1256 BinOp::Eq => " === ",
1257 BinOp::Ne => " !== ",
1258 BinOp::Lt => " < ",
1259 BinOp::Le => " <= ",
1260 BinOp::Gt => " > ",
1261 BinOp::Ge => " >= ",
1262 BinOp::And => " && ",
1263 BinOp::Or => " || ",
1264 BinOp::BitAnd => " & ",
1265 BinOp::BitOr => " | ",
1266 BinOp::BitXor => " ^ ",
1267 BinOp::Compose => " /* >> */ ",
1268 BinOp::Is => " instanceof ",
1269 };
1270 self.buf.push_str(op_str);
1271 self.emit_expr(right)?;
1272 self.buf.push(')');
1273 Ok(())
1274 }
1275 NodeKind::UnaryOp { op, operand } => {
1276 let op_str = match op {
1277 UnaryOp::Neg => "-",
1278 UnaryOp::Not => "!",
1279 UnaryOp::BitNot => "~",
1280 };
1281 self.buf.push_str(op_str);
1282 self.emit_expr(operand)?;
1283 Ok(())
1284 }
1285 NodeKind::Call { callee, args, .. } => {
1286 if let Some(code) = self.map_prelude_call(callee, args)? {
1287 self.buf.push_str(&code);
1288 return Ok(());
1289 }
1290 if self.try_emit_prelude_ctor(callee, args)? {
1291 return Ok(());
1292 }
1293 if self.try_emit_time_assoc_call(callee, args)? {
1294 return Ok(());
1295 }
1296 if self.try_emit_time_desugared_method(callee, args)? {
1297 return Ok(());
1298 }
1299 if self.try_emit_concurrency_call(callee, args)? {
1300 return Ok(());
1301 }
1302 if let NodeKind::Identifier { name } = &callee.kind {
1304 if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
1305 if let Some(handler_var) =
1306 self.current_handler_vars.get(&effect_name).cloned()
1307 {
1308 let _ = write!(self.buf, "{}.{}", handler_var, name.name);
1309 self.buf.push('(');
1310 for (i, arg) in args.iter().enumerate() {
1311 if i > 0 {
1312 self.buf.push_str(", ");
1313 }
1314 self.emit_expr(&arg.value)?;
1315 }
1316 self.buf.push(')');
1317 return Ok(());
1318 }
1319 }
1320 }
1321 let effects_arg = if let NodeKind::Identifier { name } = &callee.kind {
1323 self.build_effects_call_arg_js(&name.name)
1324 } else {
1325 None
1326 };
1327 self.emit_expr(callee)?;
1328 self.buf.push('(');
1329 for (i, arg) in args.iter().enumerate() {
1330 if i > 0 {
1331 self.buf.push_str(", ");
1332 }
1333 self.emit_expr(&arg.value)?;
1334 }
1335 if let Some(ea) = effects_arg {
1336 if !args.is_empty() {
1337 self.buf.push_str(", ");
1338 }
1339 self.buf.push_str(&ea);
1340 }
1341 self.buf.push(')');
1342 Ok(())
1343 }
1344 NodeKind::MethodCall {
1345 receiver,
1346 method,
1347 args,
1348 ..
1349 } => {
1350 if self.try_emit_time_method(receiver, &method.name, args)? {
1351 return Ok(());
1352 }
1353 self.emit_expr(receiver)?;
1354 let _ = write!(self.buf, ".{}", to_camel_case(&method.name));
1355 self.buf.push('(');
1356 for (i, arg) in args.iter().enumerate() {
1357 if i > 0 {
1358 self.buf.push_str(", ");
1359 }
1360 self.emit_expr(&arg.value)?;
1361 }
1362 self.buf.push(')');
1363 Ok(())
1364 }
1365 NodeKind::FieldAccess { object, field } => {
1366 self.emit_expr(object)?;
1367 let _ = write!(self.buf, ".{}", field.name);
1368 Ok(())
1369 }
1370 NodeKind::Index { object, index } => {
1371 self.emit_expr(object)?;
1372 self.buf.push('[');
1373 self.emit_expr(index)?;
1374 self.buf.push(']');
1375 Ok(())
1376 }
1377 NodeKind::Lambda { params, body } => {
1378 let param_names = self.collect_param_names(params);
1379 let _ = write!(self.buf, "({}) => ", param_names.join(", "));
1380 if matches!(body.kind, NodeKind::Block { .. }) {
1382 self.buf.push_str("{\n");
1383 self.indent += 1;
1384 self.emit_block_body(body)?;
1385 self.indent -= 1;
1386 self.write_indent();
1387 self.buf.push('}');
1388 } else {
1389 self.emit_expr(body)?;
1390 }
1391 Ok(())
1392 }
1393 NodeKind::Pipe { left, right } => {
1394 self.emit_pipe(left, right)
1397 }
1398 NodeKind::Compose { left, right } => {
1399 let _ = write!(self.buf, "((x) => ");
1401 self.emit_expr(right)?;
1402 self.buf.push('(');
1403 self.emit_expr(left)?;
1404 self.buf.push_str("(x)))");
1405 Ok(())
1406 }
1407 NodeKind::Await { expr } => {
1408 self.buf.push_str("(await ");
1409 self.emit_expr(expr)?;
1410 self.buf.push(')');
1411 Ok(())
1412 }
1413 NodeKind::Propagate { expr } => {
1414 self.emit_expr(expr)?;
1417 Ok(())
1418 }
1419 NodeKind::Range { lo, hi, inclusive } => {
1420 if *inclusive {
1422 self.buf.push_str("rangeInclusive(");
1423 } else {
1424 self.buf.push_str("range(");
1425 }
1426 self.emit_expr(lo)?;
1427 self.buf.push_str(", ");
1428 self.emit_expr(hi)?;
1429 self.buf.push(')');
1430 Ok(())
1431 }
1432 NodeKind::RecordConstruct {
1433 path,
1434 fields,
1435 spread,
1436 } => {
1437 let type_name = path
1438 .segments
1439 .last()
1440 .map(|s| s.name.as_str())
1441 .unwrap_or("");
1442 let is_class = self.record_names.contains(type_name);
1443 if is_class {
1444 let _ = write!(self.buf, "new {type_name}(");
1445 if fields.is_empty() && spread.is_none() {
1446 self.buf.push(')');
1447 return Ok(());
1448 }
1449 }
1450 if let Some(sp) = spread {
1451 self.buf.push_str("{ ...");
1452 self.emit_expr(sp)?;
1453 if !fields.is_empty() {
1454 self.buf.push_str(", ");
1455 }
1456 } else {
1457 self.buf.push_str("{ ");
1458 }
1459 for (i, f) in fields.iter().enumerate() {
1460 if i > 0 {
1461 self.buf.push_str(", ");
1462 }
1463 if let Some(val) = &f.value {
1464 let _ = write!(self.buf, "{}: ", f.name.name);
1465 self.emit_expr(val)?;
1466 } else {
1467 self.buf.push_str(&f.name.name);
1469 }
1470 }
1471 self.buf.push_str(" }");
1472 if is_class {
1473 self.buf.push(')');
1474 }
1475 Ok(())
1476 }
1477 NodeKind::ListLiteral { elems } => {
1478 self.buf.push('[');
1479 for (i, e) in elems.iter().enumerate() {
1480 if i > 0 {
1481 self.buf.push_str(", ");
1482 }
1483 self.emit_expr(e)?;
1484 }
1485 self.buf.push(']');
1486 Ok(())
1487 }
1488 NodeKind::MapLiteral { entries } => {
1489 self.buf.push_str("new Map([");
1490 for (i, entry) in entries.iter().enumerate() {
1491 if i > 0 {
1492 self.buf.push_str(", ");
1493 }
1494 self.buf.push('[');
1495 self.emit_expr(&entry.key)?;
1496 self.buf.push_str(", ");
1497 self.emit_expr(&entry.value)?;
1498 self.buf.push(']');
1499 }
1500 self.buf.push_str("])");
1501 Ok(())
1502 }
1503 NodeKind::SetLiteral { elems } => {
1504 self.buf.push_str("new Set([");
1505 for (i, e) in elems.iter().enumerate() {
1506 if i > 0 {
1507 self.buf.push_str(", ");
1508 }
1509 self.emit_expr(e)?;
1510 }
1511 self.buf.push_str("])");
1512 Ok(())
1513 }
1514 NodeKind::TupleLiteral { elems } => {
1515 self.buf.push('[');
1517 for (i, e) in elems.iter().enumerate() {
1518 if i > 0 {
1519 self.buf.push_str(", ");
1520 }
1521 self.emit_expr(e)?;
1522 }
1523 self.buf.push(']');
1524 Ok(())
1525 }
1526 NodeKind::Interpolation { parts } => {
1527 self.buf.push('`');
1528 for part in parts {
1529 match part {
1530 AirInterpolationPart::Literal(s) => {
1531 self.buf.push_str(&escape_template_literal(s));
1532 }
1533 AirInterpolationPart::Expr(expr) => {
1534 self.buf.push_str("${");
1535 self.emit_expr(expr)?;
1536 self.buf.push('}');
1537 }
1538 }
1539 }
1540 self.buf.push('`');
1541 Ok(())
1542 }
1543 NodeKind::Placeholder => {
1544 self.buf.push('_');
1545 Ok(())
1546 }
1547 NodeKind::Unreachable => {
1548 self.buf
1549 .push_str("(() => { throw new Error(\"unreachable\"); })()");
1550 Ok(())
1551 }
1552 NodeKind::ResultConstruct { variant, value } => {
1553 match variant {
1554 ResultVariant::Ok => {
1555 self.buf.push_str("{ _tag: \"Ok\", value: ");
1556 if let Some(v) = value {
1557 self.emit_expr(v)?;
1558 } else {
1559 self.buf.push_str("undefined");
1560 }
1561 self.buf.push_str(" }");
1562 }
1563 ResultVariant::Err => {
1564 self.buf.push_str("{ _tag: \"Err\", error: ");
1565 if let Some(v) = value {
1566 self.emit_expr(v)?;
1567 } else {
1568 self.buf.push_str("undefined");
1569 }
1570 self.buf.push_str(" }");
1571 }
1572 }
1573 Ok(())
1574 }
1575 NodeKind::Assign { op, target, value } => {
1576 self.emit_expr(target)?;
1577 let op_str = match op {
1578 AssignOp::Assign => " = ",
1579 AssignOp::AddAssign => " += ",
1580 AssignOp::SubAssign => " -= ",
1581 AssignOp::MulAssign => " *= ",
1582 AssignOp::DivAssign => " /= ",
1583 AssignOp::RemAssign => " %= ",
1584 };
1585 self.buf.push_str(op_str);
1586 self.emit_expr(value)?;
1587 Ok(())
1588 }
1589 NodeKind::If {
1590 condition,
1591 then_block,
1592 else_block,
1593 ..
1594 } => {
1595 self.buf.push('(');
1597 self.emit_expr(condition)?;
1598 self.buf.push_str(" ? ");
1599 self.emit_block_as_expr(then_block)?;
1600 self.buf.push_str(" : ");
1601 if let Some(eb) = else_block {
1602 self.emit_block_as_expr(eb)?;
1603 } else {
1604 self.buf.push_str("undefined");
1605 }
1606 self.buf.push(')');
1607 Ok(())
1608 }
1609 NodeKind::Block { stmts, tail } => {
1610 self.buf.push_str("(() => {\n");
1612 self.indent += 1;
1613 for s in stmts {
1614 self.emit_node(s)?;
1615 }
1616 if let Some(t) = tail {
1617 let ind = self.indent_str();
1618 let _ = write!(self.buf, "{ind}return ");
1619 self.emit_expr(t)?;
1620 self.buf.push_str(";\n");
1621 }
1622 self.indent -= 1;
1623 self.write_indent();
1624 self.buf.push_str("})()");
1625 Ok(())
1626 }
1627 NodeKind::Match { scrutinee, arms } => {
1628 self.buf.push_str("(() => {\n");
1630 self.indent += 1;
1631 self.emit_match(scrutinee, arms)?;
1632 self.indent -= 1;
1633 self.write_indent();
1634 self.buf.push_str("})()");
1635 Ok(())
1636 }
1637 NodeKind::Move { expr }
1639 | NodeKind::Borrow { expr }
1640 | NodeKind::MutableBorrow { expr } => self.emit_expr(expr),
1641 NodeKind::EffectOp {
1643 effect,
1644 operation,
1645 args,
1646 } => {
1647 let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
1648 let _ = write!(
1649 self.buf,
1650 "{}.{}",
1651 to_camel_case(effect_name),
1652 operation.name
1653 );
1654 self.buf.push('(');
1655 for (i, arg) in args.iter().enumerate() {
1656 if i > 0 {
1657 self.buf.push_str(", ");
1658 }
1659 self.emit_expr(&arg.value)?;
1660 }
1661 self.buf.push(')');
1662 Ok(())
1663 }
1664 NodeKind::TypeNamed { .. }
1666 | NodeKind::TypeTuple { .. }
1667 | NodeKind::TypeFunction { .. }
1668 | NodeKind::TypeOptional { .. }
1669 | NodeKind::TypeSelf => {
1670 self.buf.push_str("/* type */");
1671 Ok(())
1672 }
1673 NodeKind::EffectRef { path } => {
1675 let name = path
1676 .segments
1677 .iter()
1678 .map(|s| s.name.as_str())
1679 .collect::<Vec<_>>()
1680 .join(".");
1681 self.buf.push_str(&name);
1682 Ok(())
1683 }
1684 NodeKind::Error => {
1686 self.buf.push_str("/* error */");
1687 Ok(())
1688 }
1689 _ => {
1691 self.buf.push_str("/* unsupported */");
1692 Ok(())
1693 }
1694 }
1695 }
1696
1697 fn emit_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<(), CodegenError> {
1700 let is_adt = arms.iter().any(|arm| {
1702 if let NodeKind::MatchArm { pattern, .. } = &arm.kind {
1703 matches!(pattern.kind, NodeKind::ConstructorPat { .. })
1704 } else {
1705 false
1706 }
1707 });
1708
1709 if is_adt {
1710 let ind = self.indent_str();
1711 let _ = write!(self.buf, "{ind}switch (");
1712 self.emit_expr(scrutinee)?;
1713 self.buf.push_str("._tag) {\n");
1714 } else {
1715 let ind = self.indent_str();
1716 let _ = write!(self.buf, "{ind}switch (");
1717 self.emit_expr(scrutinee)?;
1718 self.buf.push_str(") {\n");
1719 }
1720 self.indent += 1;
1721 for arm in arms {
1722 self.emit_match_arm(arm, is_adt, scrutinee)?;
1723 }
1724 self.indent -= 1;
1725 self.writeln("}");
1726 Ok(())
1727 }
1728
1729 fn emit_match_arm(
1730 &mut self,
1731 arm: &AIRNode,
1732 is_adt: bool,
1733 scrutinee: &AIRNode,
1734 ) -> Result<(), CodegenError> {
1735 if let NodeKind::MatchArm {
1736 pattern,
1737 guard,
1738 body,
1739 } = &arm.kind
1740 {
1741 match &pattern.kind {
1742 NodeKind::WildcardPat => {
1743 self.writeln("default: {");
1744 }
1745 NodeKind::BindPat { name, .. } if !is_adt => {
1746 self.writeln("default: {");
1748 self.indent += 1;
1749 let ind = self.indent_str();
1750 let _ = write!(self.buf, "{ind}const {} = ", name.name);
1751 self.emit_expr(scrutinee)?;
1752 self.buf.push_str(";\n");
1753 self.indent -= 1;
1754 }
1755 NodeKind::LiteralPat { lit } => {
1756 let ind = self.indent_str();
1757 let _ = write!(self.buf, "{ind}case ");
1758 match lit {
1759 Literal::Int(s) => self.buf.push_str(s),
1760 Literal::Float(s) => self.buf.push_str(s),
1761 Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
1762 Literal::Char(s) => {
1763 self.buf.push('\'');
1764 self.buf.push_str(s);
1765 self.buf.push('\'');
1766 }
1767 Literal::String(s) => {
1768 self.buf.push('"');
1769 self.buf.push_str(&escape_js_string(s));
1770 self.buf.push('"');
1771 }
1772 Literal::Unit => self.buf.push_str("undefined"),
1773 }
1774 self.buf.push_str(": {\n");
1775 }
1776 NodeKind::ConstructorPat { path, fields } => {
1777 let variant_name = path.segments.last().map_or("_", |s| s.name.as_str());
1778 self.writeln(&format!("case \"{variant_name}\": {{"));
1779 if !fields.is_empty() {
1781 self.indent += 1;
1782 for (i, field) in fields.iter().enumerate() {
1783 let binding = self.pattern_to_binding_name(field);
1784 let ind = self.indent_str();
1785 let _ = write!(self.buf, "{ind}const {binding} = ");
1786 self.emit_expr(scrutinee)?;
1787 let _ = writeln!(self.buf, "._{i};");
1788 }
1789 self.indent -= 1;
1790 }
1791 }
1792 NodeKind::RecordPat { path, fields, .. } => {
1793 let variant_name = path.segments.last().map_or("_", |s| s.name.as_str());
1794 if is_adt {
1795 self.writeln(&format!("case \"{variant_name}\": {{"));
1796 } else {
1797 self.writeln("default: {");
1798 }
1799 if !fields.is_empty() {
1800 self.indent += 1;
1801 for f in fields {
1802 let field_name = &f.name.name;
1803 if let Some(pat) = &f.pattern {
1804 let binding = self.pattern_to_binding_name(pat);
1805 let ind = self.indent_str();
1806 let _ = write!(self.buf, "{ind}const {binding} = ");
1807 self.emit_expr(scrutinee)?;
1808 let _ = writeln!(self.buf, ".{field_name};");
1809 } else {
1810 let ind = self.indent_str();
1811 let _ = write!(self.buf, "{ind}const {field_name} = ");
1812 self.emit_expr(scrutinee)?;
1813 let _ = writeln!(self.buf, ".{field_name};");
1814 }
1815 }
1816 self.indent -= 1;
1817 }
1818 }
1819 _ => {
1820 self.writeln("default: {");
1822 }
1823 }
1824
1825 self.indent += 1;
1826 if let Some(g) = guard {
1827 let ind = self.indent_str();
1828 let _ = write!(self.buf, "{ind}if (!(");
1829 self.emit_expr(g)?;
1830 self.buf.push_str(")) break;\n");
1831 }
1832 self.emit_block_body(body)?;
1833 self.writeln("break;");
1834 self.indent -= 1;
1835 self.writeln("}");
1836 }
1837 Ok(())
1838 }
1839
1840 fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
1843 if let NodeKind::Call { callee, args, .. } = &right.kind {
1846 let has_placeholder = args
1847 .iter()
1848 .any(|a| matches!(a.value.kind, NodeKind::Placeholder));
1849 if has_placeholder {
1850 self.emit_expr(callee)?;
1851 self.buf.push('(');
1852 for (i, arg) in args.iter().enumerate() {
1853 if i > 0 {
1854 self.buf.push_str(", ");
1855 }
1856 if matches!(arg.value.kind, NodeKind::Placeholder) {
1857 self.emit_expr(left)?;
1858 } else {
1859 self.emit_expr(&arg.value)?;
1860 }
1861 }
1862 self.buf.push(')');
1863 return Ok(());
1864 }
1865 }
1866 self.emit_expr(right)?;
1868 self.buf.push('(');
1869 self.emit_expr(left)?;
1870 self.buf.push(')');
1871 Ok(())
1872 }
1873
1874 fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1877 if let NodeKind::Block { stmts, tail } = &node.kind {
1878 for s in stmts {
1879 self.emit_node(s)?;
1880 }
1881 if let Some(t) = tail {
1882 let ind = self.indent_str();
1883 let _ = write!(self.buf, "{ind}return ");
1884 self.emit_expr(t)?;
1885 self.buf.push_str(";\n");
1886 }
1887 } else {
1888 let ind = self.indent_str();
1890 let _ = write!(self.buf, "{ind}return ");
1891 self.emit_expr(node)?;
1892 self.buf.push_str(";\n");
1893 }
1894 Ok(())
1895 }
1896
1897 fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1898 if let NodeKind::Block { stmts, tail } = &node.kind {
1899 if stmts.is_empty() {
1900 if let Some(t) = tail {
1901 return self.emit_expr(t);
1902 }
1903 }
1904 }
1905 self.emit_expr(node)
1907 }
1908
1909 fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
1910 match &pat.kind {
1911 NodeKind::BindPat { name, .. } => to_camel_case(&name.name),
1912 NodeKind::WildcardPat => "_".into(),
1913 NodeKind::TuplePat { elems } => {
1914 format!(
1915 "[{}]",
1916 elems
1917 .iter()
1918 .map(|e| self.pattern_to_binding_name(e))
1919 .collect::<Vec<_>>()
1920 .join(", ")
1921 )
1922 }
1923 NodeKind::RecordPat { fields, .. } => {
1924 format!(
1925 "{{ {} }}",
1926 fields
1927 .iter()
1928 .map(|f| to_camel_case(&f.name.name).to_string())
1929 .collect::<Vec<_>>()
1930 .join(", ")
1931 )
1932 }
1933 _ => "_".into(),
1934 }
1935 }
1936
1937 fn pattern_to_js_destructure(&self, pat: &AIRNode) -> String {
1938 self.pattern_to_binding_name(pat)
1939 }
1940
1941 fn type_expr_to_string(&self, node: &AIRNode) -> String {
1942 match &node.kind {
1943 NodeKind::TypeNamed { path, .. } => path
1944 .segments
1945 .iter()
1946 .map(|s| s.name.as_str())
1947 .collect::<Vec<_>>()
1948 .join("."),
1949 NodeKind::Identifier { name } => name.name.clone(),
1950 _ => "Unknown".into(),
1951 }
1952 }
1953}
1954
1955fn is_time_method_name(name: &str) -> bool {
1960 matches!(
1961 name,
1962 "as_nanos"
1963 | "as_millis"
1964 | "as_seconds"
1965 | "is_zero"
1966 | "is_negative"
1967 | "abs"
1968 | "elapsed"
1969 | "duration_since"
1970 )
1971}
1972
1973fn to_camel_case(s: &str) -> String {
1975 if s.is_empty() || s == "_" {
1976 return s.to_string();
1977 }
1978 if !s.contains('_') && s.starts_with(|c: char| c.is_lowercase()) {
1980 return s.to_string();
1981 }
1982 if s.contains('_') {
1984 let parts: Vec<&str> = s.split('_').filter(|p| !p.is_empty()).collect();
1985 if parts.is_empty() {
1986 return s.to_string();
1987 }
1988 let mut result = parts[0].to_lowercase();
1989 for part in &parts[1..] {
1990 let mut chars = part.chars();
1991 if let Some(first) = chars.next() {
1992 result.push(
1993 first
1994 .to_uppercase()
1995 .next()
1996 .expect("uppercase yields at least one char"),
1997 );
1998 result.extend(chars);
1999 }
2000 }
2001 return result;
2002 }
2003 let mut chars = s.chars();
2005 let first = chars.next().expect("non-empty string guaranteed by caller");
2006 let mut result = first.to_lowercase().to_string();
2007 result.extend(chars);
2008 result
2009}
2010
2011fn escape_js_string(s: &str) -> String {
2013 let mut out = String::with_capacity(s.len());
2014 for ch in s.chars() {
2015 match ch {
2016 '"' => out.push_str("\\\""),
2017 '\\' => out.push_str("\\\\"),
2018 '\n' => out.push_str("\\n"),
2019 '\r' => out.push_str("\\r"),
2020 '\t' => out.push_str("\\t"),
2021 _ => out.push(ch),
2022 }
2023 }
2024 out
2025}
2026
2027fn escape_template_literal(s: &str) -> String {
2029 let mut out = String::with_capacity(s.len());
2030 for ch in s.chars() {
2031 match ch {
2032 '`' => out.push_str("\\`"),
2033 '\\' => out.push_str("\\\\"),
2034 '$' => out.push_str("\\$"),
2035 _ => out.push(ch),
2036 }
2037 }
2038 out
2039}
2040
2041#[cfg(test)]
2044mod tests {
2045 use super::*;
2046 use bock_air::{AirArg, AirRecordField};
2047 use bock_ast::{Ident, TypePath};
2048 use bock_errors::{FileId, Span};
2049
2050 fn span() -> Span {
2051 Span {
2052 file: FileId(0),
2053 start: 0,
2054 end: 0,
2055 }
2056 }
2057
2058 fn ident(name: &str) -> Ident {
2059 Ident {
2060 name: name.to_string(),
2061 span: span(),
2062 }
2063 }
2064
2065 fn type_path(segments: &[&str]) -> TypePath {
2066 TypePath {
2067 segments: segments.iter().map(|s| ident(s)).collect(),
2068 span: span(),
2069 }
2070 }
2071
2072 fn node(id: u32, kind: NodeKind) -> AIRNode {
2073 AIRNode::new(id, span(), kind)
2074 }
2075
2076 fn int_lit(id: u32, val: &str) -> AIRNode {
2077 node(
2078 id,
2079 NodeKind::Literal {
2080 lit: Literal::Int(val.into()),
2081 },
2082 )
2083 }
2084
2085 fn str_lit(id: u32, val: &str) -> AIRNode {
2086 node(
2087 id,
2088 NodeKind::Literal {
2089 lit: Literal::String(val.into()),
2090 },
2091 )
2092 }
2093
2094 fn bool_lit(id: u32, val: bool) -> AIRNode {
2095 node(
2096 id,
2097 NodeKind::Literal {
2098 lit: Literal::Bool(val),
2099 },
2100 )
2101 }
2102
2103 fn id_node(id: u32, name: &str) -> AIRNode {
2104 node(id, NodeKind::Identifier { name: ident(name) })
2105 }
2106
2107 fn bind_pat(id: u32, name: &str) -> AIRNode {
2108 node(
2109 id,
2110 NodeKind::BindPat {
2111 name: ident(name),
2112 is_mut: false,
2113 },
2114 )
2115 }
2116
2117 fn param_node(id: u32, name: &str) -> AIRNode {
2118 node(
2119 id,
2120 NodeKind::Param {
2121 pattern: Box::new(bind_pat(id + 100, name)),
2122 ty: None,
2123 default: None,
2124 },
2125 )
2126 }
2127
2128 fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
2129 node(
2130 id,
2131 NodeKind::Block {
2132 stmts,
2133 tail: tail.map(Box::new),
2134 },
2135 )
2136 }
2137
2138 fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
2139 node(
2140 0,
2141 NodeKind::Module {
2142 path: None,
2143 annotations: vec![],
2144 imports,
2145 items,
2146 },
2147 )
2148 }
2149
2150 fn gen(module: &AIRNode) -> String {
2151 let gen = JsGenerator::new();
2152 let result = gen.generate_module(module).unwrap();
2153 result.files[0].content.clone()
2154 }
2155
2156 #[test]
2159 fn implements_code_generator_trait() {
2160 let gen = JsGenerator::new();
2161 assert_eq!(gen.target().id, "js");
2162 }
2163
2164 #[test]
2165 fn empty_module() {
2166 let m = module(vec![], vec![]);
2167 let out = gen(&m);
2168 assert_eq!(out, "");
2169 }
2170
2171 #[test]
2172 fn simple_function() {
2173 let body = block(2, vec![], Some(int_lit(3, "42")));
2174 let f = node(
2175 1,
2176 NodeKind::FnDecl {
2177 annotations: vec![],
2178 visibility: Visibility::Private,
2179 is_async: false,
2180 name: ident("answer"),
2181 generic_params: vec![],
2182 params: vec![],
2183 return_type: None,
2184 effect_clause: vec![],
2185 where_clause: vec![],
2186 body: Box::new(body),
2187 },
2188 );
2189 let out = gen(&module(vec![], vec![f]));
2190 assert!(out.contains("function answer()"));
2191 assert!(out.contains("return 42;"));
2192 }
2193
2194 #[test]
2195 fn function_with_params() {
2196 let body = block(
2197 5,
2198 vec![],
2199 Some(node(
2200 6,
2201 NodeKind::BinaryOp {
2202 op: BinOp::Add,
2203 left: Box::new(id_node(7, "a")),
2204 right: Box::new(id_node(8, "b")),
2205 },
2206 )),
2207 );
2208 let f = node(
2209 1,
2210 NodeKind::FnDecl {
2211 annotations: vec![],
2212 visibility: Visibility::Public,
2213 is_async: false,
2214 name: ident("add"),
2215 generic_params: vec![],
2216 params: vec![param_node(2, "a"), param_node(3, "b")],
2217 return_type: None,
2218 effect_clause: vec![],
2219 where_clause: vec![],
2220 body: Box::new(body),
2221 },
2222 );
2223 let out = gen(&module(vec![], vec![f]));
2224 assert!(out.contains("export function add(a, b)"));
2225 assert!(out.contains("(a + b)"));
2226 }
2227
2228 #[test]
2229 fn async_function() {
2230 let body = block(
2231 3,
2232 vec![],
2233 Some(node(
2234 4,
2235 NodeKind::Await {
2236 expr: Box::new(node(
2237 5,
2238 NodeKind::Call {
2239 callee: Box::new(id_node(6, "fetch")),
2240 args: vec![AirArg {
2241 label: None,
2242 value: str_lit(7, "https://example.com"),
2243 }],
2244 type_args: vec![],
2245 },
2246 )),
2247 },
2248 )),
2249 );
2250 let f = node(
2251 1,
2252 NodeKind::FnDecl {
2253 annotations: vec![],
2254 visibility: Visibility::Private,
2255 is_async: true,
2256 name: ident("fetchData"),
2257 generic_params: vec![],
2258 params: vec![],
2259 return_type: None,
2260 effect_clause: vec![],
2261 where_clause: vec![],
2262 body: Box::new(body),
2263 },
2264 );
2265 let out = gen(&module(vec![], vec![f]));
2266 assert!(out.contains("async function fetchData()"));
2267 assert!(out.contains("await fetch"));
2268 }
2269
2270 #[test]
2271 fn effects_as_destructured_params() {
2272 let body = block(
2273 3,
2274 vec![node(
2275 4,
2276 NodeKind::LetBinding {
2277 is_mut: false,
2278 pattern: Box::new(bind_pat(5, "msg")),
2279 ty: None,
2280 value: Box::new(str_lit(6, "hello")),
2281 },
2282 )],
2283 Some(node(
2284 7,
2285 NodeKind::EffectOp {
2286 effect: type_path(&["Log"]),
2287 operation: ident("info"),
2288 args: vec![AirArg {
2289 label: None,
2290 value: id_node(8, "msg"),
2291 }],
2292 },
2293 )),
2294 );
2295 let f = node(
2296 1,
2297 NodeKind::FnDecl {
2298 annotations: vec![],
2299 visibility: Visibility::Private,
2300 is_async: false,
2301 name: ident("process"),
2302 generic_params: vec![],
2303 params: vec![param_node(2, "data")],
2304 return_type: None,
2305 effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
2306 where_clause: vec![],
2307 body: Box::new(body),
2308 },
2309 );
2310 let out = gen(&module(vec![], vec![f]));
2311 assert!(out.contains("function process(data, { log, clock })"));
2312 assert!(out.contains("log.info(msg)"));
2313 }
2314
2315 #[test]
2316 fn enum_to_tagged_objects() {
2317 let enum_decl = node(
2318 1,
2319 NodeKind::EnumDecl {
2320 annotations: vec![],
2321 visibility: Visibility::Public,
2322 name: ident("Shape"),
2323 generic_params: vec![],
2324 variants: vec![
2325 node(
2326 2,
2327 NodeKind::EnumVariant {
2328 name: ident("Circle"),
2329 payload: EnumVariantPayload::Struct(vec![bock_ast::RecordDeclField {
2330 id: 0,
2331 span: span(),
2332 name: ident("radius"),
2333 ty: bock_ast::TypeExpr::Named {
2334 id: 0,
2335 span: span(),
2336 path: type_path(&["Float"]),
2337 args: vec![],
2338 },
2339 default: None,
2340 }]),
2341 },
2342 ),
2343 node(
2344 3,
2345 NodeKind::EnumVariant {
2346 name: ident("None"),
2347 payload: EnumVariantPayload::Unit,
2348 },
2349 ),
2350 ],
2351 },
2352 );
2353 let out = gen(&module(vec![], vec![enum_decl]));
2354 assert!(out.contains("function Shape_Circle(radius)"));
2355 assert!(out.contains("_tag: \"Circle\""));
2356 assert!(out.contains("Shape_None = Object.freeze({ _tag: \"None\" })"));
2357 }
2358
2359 #[test]
2360 fn match_on_tagged_objects() {
2361 let scrutinee = id_node(10, "shape");
2362 let arms = vec![
2363 node(
2364 11,
2365 NodeKind::MatchArm {
2366 pattern: Box::new(node(
2367 12,
2368 NodeKind::ConstructorPat {
2369 path: type_path(&["Shape", "Circle"]),
2370 fields: vec![bind_pat(13, "r")],
2371 },
2372 )),
2373 guard: None,
2374 body: Box::new(block(
2375 14,
2376 vec![],
2377 Some(node(
2378 15,
2379 NodeKind::BinaryOp {
2380 op: BinOp::Mul,
2381 left: Box::new(id_node(16, "r")),
2382 right: Box::new(id_node(17, "r")),
2383 },
2384 )),
2385 )),
2386 },
2387 ),
2388 node(
2389 18,
2390 NodeKind::MatchArm {
2391 pattern: Box::new(node(19, NodeKind::WildcardPat)),
2392 guard: None,
2393 body: Box::new(block(20, vec![], Some(int_lit(21, "0")))),
2394 },
2395 ),
2396 ];
2397 let match_stmt = node(
2398 9,
2399 NodeKind::Match {
2400 scrutinee: Box::new(scrutinee),
2401 arms,
2402 },
2403 );
2404 let f = node(
2406 1,
2407 NodeKind::FnDecl {
2408 annotations: vec![],
2409 visibility: Visibility::Private,
2410 is_async: false,
2411 name: ident("area"),
2412 generic_params: vec![],
2413 params: vec![param_node(2, "shape")],
2414 return_type: None,
2415 effect_clause: vec![],
2416 where_clause: vec![],
2417 body: Box::new(block(3, vec![match_stmt], None)),
2418 },
2419 );
2420 let out = gen(&module(vec![], vec![f]));
2421 assert!(out.contains("switch (shape._tag)"));
2422 assert!(out.contains("case \"Circle\""));
2423 assert!(out.contains("const r = shape._0;"));
2424 assert!(out.contains("default:"));
2425 }
2426
2427 #[test]
2428 fn ownership_erased() {
2429 let move_expr = node(
2430 1,
2431 NodeKind::Move {
2432 expr: Box::new(id_node(2, "x")),
2433 },
2434 );
2435 let borrow_expr = node(
2436 3,
2437 NodeKind::Borrow {
2438 expr: Box::new(id_node(4, "y")),
2439 },
2440 );
2441 let mut_borrow_expr = node(
2442 5,
2443 NodeKind::MutableBorrow {
2444 expr: Box::new(id_node(6, "z")),
2445 },
2446 );
2447 let body = block(
2448 7,
2449 vec![
2450 node(
2451 8,
2452 NodeKind::LetBinding {
2453 is_mut: false,
2454 pattern: Box::new(bind_pat(9, "a")),
2455 ty: None,
2456 value: Box::new(move_expr),
2457 },
2458 ),
2459 node(
2460 10,
2461 NodeKind::LetBinding {
2462 is_mut: false,
2463 pattern: Box::new(bind_pat(11, "b")),
2464 ty: None,
2465 value: Box::new(borrow_expr),
2466 },
2467 ),
2468 node(
2469 12,
2470 NodeKind::LetBinding {
2471 is_mut: false,
2472 pattern: Box::new(bind_pat(13, "c")),
2473 ty: None,
2474 value: Box::new(mut_borrow_expr),
2475 },
2476 ),
2477 ],
2478 None,
2479 );
2480 let f = node(
2481 0,
2482 NodeKind::FnDecl {
2483 annotations: vec![],
2484 visibility: Visibility::Private,
2485 is_async: false,
2486 name: ident("test"),
2487 generic_params: vec![],
2488 params: vec![],
2489 return_type: None,
2490 effect_clause: vec![],
2491 where_clause: vec![],
2492 body: Box::new(body),
2493 },
2494 );
2495 let out = gen(&module(vec![], vec![f]));
2496 assert!(out.contains("const a = x;"));
2498 assert!(out.contains("const b = y;"));
2499 assert!(out.contains("const c = z;"));
2500 }
2501
2502 #[test]
2503 fn let_binding_mut_uses_let() {
2504 let binding = node(
2505 1,
2506 NodeKind::LetBinding {
2507 is_mut: true,
2508 pattern: Box::new(bind_pat(2, "count")),
2509 ty: None,
2510 value: Box::new(int_lit(3, "0")),
2511 },
2512 );
2513 let f = node(
2514 0,
2515 NodeKind::FnDecl {
2516 annotations: vec![],
2517 visibility: Visibility::Private,
2518 is_async: false,
2519 name: ident("test"),
2520 generic_params: vec![],
2521 params: vec![],
2522 return_type: None,
2523 effect_clause: vec![],
2524 where_clause: vec![],
2525 body: Box::new(block(4, vec![binding], None)),
2526 },
2527 );
2528 let out = gen(&module(vec![], vec![f]));
2529 assert!(out.contains("let count = 0;"));
2530 }
2531
2532 #[test]
2533 fn string_interpolation() {
2534 let interp = node(
2535 1,
2536 NodeKind::Interpolation {
2537 parts: vec![
2538 AirInterpolationPart::Literal("Hello, ".into()),
2539 AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
2540 AirInterpolationPart::Literal("!".into()),
2541 ],
2542 },
2543 );
2544 let binding = node(
2545 3,
2546 NodeKind::LetBinding {
2547 is_mut: false,
2548 pattern: Box::new(bind_pat(4, "msg")),
2549 ty: None,
2550 value: Box::new(interp),
2551 },
2552 );
2553 let f = node(
2554 0,
2555 NodeKind::FnDecl {
2556 annotations: vec![],
2557 visibility: Visibility::Private,
2558 is_async: false,
2559 name: ident("greet"),
2560 generic_params: vec![],
2561 params: vec![param_node(5, "name")],
2562 return_type: None,
2563 effect_clause: vec![],
2564 where_clause: vec![],
2565 body: Box::new(block(6, vec![binding], Some(id_node(7, "msg")))),
2566 },
2567 );
2568 let out = gen(&module(vec![], vec![f]));
2569 assert!(out.contains("`Hello, ${name}!`"));
2570 }
2571
2572 #[test]
2573 fn list_map_set_literals() {
2574 let list = node(
2575 1,
2576 NodeKind::ListLiteral {
2577 elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
2578 },
2579 );
2580 let map = node(
2581 5,
2582 NodeKind::MapLiteral {
2583 entries: vec![bock_air::AirMapEntry {
2584 key: str_lit(6, "a"),
2585 value: int_lit(7, "1"),
2586 }],
2587 },
2588 );
2589 let set = node(
2590 8,
2591 NodeKind::SetLiteral {
2592 elems: vec![int_lit(9, "1"), int_lit(10, "2")],
2593 },
2594 );
2595 let body = block(
2596 11,
2597 vec![
2598 node(
2599 12,
2600 NodeKind::LetBinding {
2601 is_mut: false,
2602 pattern: Box::new(bind_pat(13, "xs")),
2603 ty: None,
2604 value: Box::new(list),
2605 },
2606 ),
2607 node(
2608 14,
2609 NodeKind::LetBinding {
2610 is_mut: false,
2611 pattern: Box::new(bind_pat(15, "m")),
2612 ty: None,
2613 value: Box::new(map),
2614 },
2615 ),
2616 node(
2617 16,
2618 NodeKind::LetBinding {
2619 is_mut: false,
2620 pattern: Box::new(bind_pat(17, "s")),
2621 ty: None,
2622 value: Box::new(set),
2623 },
2624 ),
2625 ],
2626 None,
2627 );
2628 let f = node(
2629 0,
2630 NodeKind::FnDecl {
2631 annotations: vec![],
2632 visibility: Visibility::Private,
2633 is_async: false,
2634 name: ident("collections"),
2635 generic_params: vec![],
2636 params: vec![],
2637 return_type: None,
2638 effect_clause: vec![],
2639 where_clause: vec![],
2640 body: Box::new(body),
2641 },
2642 );
2643 let out = gen(&module(vec![], vec![f]));
2644 assert!(out.contains("[1, 2, 3]"));
2645 assert!(out.contains("new Map([[\"a\", 1]])"));
2646 assert!(out.contains("new Set([1, 2])"));
2647 }
2648
2649 #[test]
2650 fn record_construction() {
2651 let rec = node(
2652 1,
2653 NodeKind::RecordConstruct {
2654 path: type_path(&["User"]),
2655 fields: vec![
2656 AirRecordField {
2657 name: ident("name"),
2658 value: Some(Box::new(str_lit(2, "Alice"))),
2659 },
2660 AirRecordField {
2661 name: ident("age"),
2662 value: Some(Box::new(int_lit(3, "30"))),
2663 },
2664 ],
2665 spread: None,
2666 },
2667 );
2668 let binding = node(
2669 4,
2670 NodeKind::LetBinding {
2671 is_mut: false,
2672 pattern: Box::new(bind_pat(5, "user")),
2673 ty: None,
2674 value: Box::new(rec),
2675 },
2676 );
2677 let f = node(
2678 0,
2679 NodeKind::FnDecl {
2680 annotations: vec![],
2681 visibility: Visibility::Private,
2682 is_async: false,
2683 name: ident("test"),
2684 generic_params: vec![],
2685 params: vec![],
2686 return_type: None,
2687 effect_clause: vec![],
2688 where_clause: vec![],
2689 body: Box::new(block(6, vec![binding], None)),
2690 },
2691 );
2692 let out = gen(&module(vec![], vec![f]));
2693 assert!(out.contains("{ name: \"Alice\", age: 30 }"));
2694 }
2695
2696 #[test]
2697 fn control_flow() {
2698 let if_stmt = node(
2699 1,
2700 NodeKind::If {
2701 let_pattern: None,
2702 condition: Box::new(bool_lit(2, true)),
2703 then_block: Box::new(block(3, vec![], Some(int_lit(4, "1")))),
2704 else_block: Some(Box::new(block(5, vec![], Some(int_lit(6, "2"))))),
2705 },
2706 );
2707 let for_stmt = node(
2708 7,
2709 NodeKind::For {
2710 pattern: Box::new(bind_pat(8, "x")),
2711 iterable: Box::new(id_node(9, "items")),
2712 body: Box::new(block(10, vec![], None)),
2713 },
2714 );
2715 let while_stmt = node(
2716 11,
2717 NodeKind::While {
2718 condition: Box::new(bool_lit(12, true)),
2719 body: Box::new(block(
2720 13,
2721 vec![node(14, NodeKind::Break { value: None })],
2722 None,
2723 )),
2724 },
2725 );
2726 let body = block(15, vec![if_stmt, for_stmt, while_stmt], None);
2727 let f = node(
2728 0,
2729 NodeKind::FnDecl {
2730 annotations: vec![],
2731 visibility: Visibility::Private,
2732 is_async: false,
2733 name: ident("flow"),
2734 generic_params: vec![],
2735 params: vec![param_node(16, "items")],
2736 return_type: None,
2737 effect_clause: vec![],
2738 where_clause: vec![],
2739 body: Box::new(body),
2740 },
2741 );
2742 let out = gen(&module(vec![], vec![f]));
2743 assert!(out.contains("if (true)"));
2744 assert!(out.contains("} else {"));
2745 assert!(out.contains("for (const x of items)"));
2746 assert!(out.contains("while (true)"));
2747 assert!(out.contains("break;"));
2748 }
2749
2750 #[test]
2751 fn lambda_and_pipe() {
2752 let lambda = node(
2753 1,
2754 NodeKind::Lambda {
2755 params: vec![param_node(2, "x")],
2756 body: Box::new(node(
2757 3,
2758 NodeKind::BinaryOp {
2759 op: BinOp::Mul,
2760 left: Box::new(id_node(4, "x")),
2761 right: Box::new(int_lit(5, "2")),
2762 },
2763 )),
2764 },
2765 );
2766 let pipe = node(
2767 6,
2768 NodeKind::Pipe {
2769 left: Box::new(int_lit(7, "5")),
2770 right: Box::new(id_node(8, "double")),
2771 },
2772 );
2773 let body = block(
2774 9,
2775 vec![
2776 node(
2777 10,
2778 NodeKind::LetBinding {
2779 is_mut: false,
2780 pattern: Box::new(bind_pat(11, "double")),
2781 ty: None,
2782 value: Box::new(lambda),
2783 },
2784 ),
2785 node(
2786 12,
2787 NodeKind::LetBinding {
2788 is_mut: false,
2789 pattern: Box::new(bind_pat(13, "result")),
2790 ty: None,
2791 value: Box::new(pipe),
2792 },
2793 ),
2794 ],
2795 None,
2796 );
2797 let f = node(
2798 0,
2799 NodeKind::FnDecl {
2800 annotations: vec![],
2801 visibility: Visibility::Private,
2802 is_async: false,
2803 name: ident("test"),
2804 generic_params: vec![],
2805 params: vec![],
2806 return_type: None,
2807 effect_clause: vec![],
2808 where_clause: vec![],
2809 body: Box::new(body),
2810 },
2811 );
2812 let out = gen(&module(vec![], vec![f]));
2813 assert!(out.contains("(x) => (x * 2)"));
2814 assert!(out.contains("double(5)"));
2815 }
2816
2817 #[test]
2818 fn result_construct() {
2819 let ok = node(
2820 1,
2821 NodeKind::ResultConstruct {
2822 variant: ResultVariant::Ok,
2823 value: Some(Box::new(int_lit(2, "42"))),
2824 },
2825 );
2826 let err = node(
2827 3,
2828 NodeKind::ResultConstruct {
2829 variant: ResultVariant::Err,
2830 value: Some(Box::new(str_lit(4, "failed"))),
2831 },
2832 );
2833 let body = block(
2834 5,
2835 vec![
2836 node(
2837 6,
2838 NodeKind::LetBinding {
2839 is_mut: false,
2840 pattern: Box::new(bind_pat(7, "good")),
2841 ty: None,
2842 value: Box::new(ok),
2843 },
2844 ),
2845 node(
2846 8,
2847 NodeKind::LetBinding {
2848 is_mut: false,
2849 pattern: Box::new(bind_pat(9, "bad")),
2850 ty: None,
2851 value: Box::new(err),
2852 },
2853 ),
2854 ],
2855 None,
2856 );
2857 let f = node(
2858 0,
2859 NodeKind::FnDecl {
2860 annotations: vec![],
2861 visibility: Visibility::Private,
2862 is_async: false,
2863 name: ident("test"),
2864 generic_params: vec![],
2865 params: vec![],
2866 return_type: None,
2867 effect_clause: vec![],
2868 where_clause: vec![],
2869 body: Box::new(body),
2870 },
2871 );
2872 let out = gen(&module(vec![], vec![f]));
2873 assert!(out.contains("{ _tag: \"Ok\", value: 42 }"));
2874 assert!(out.contains("{ _tag: \"Err\", error: \"failed\" }"));
2875 }
2876
2877 #[test]
2878 fn class_declaration() {
2879 let method_body = block(10, vec![], Some(id_node(11, "undefined")));
2880 let method = node(
2881 5,
2882 NodeKind::FnDecl {
2883 annotations: vec![],
2884 visibility: Visibility::Public,
2885 is_async: false,
2886 name: ident("greet"),
2887 generic_params: vec![],
2888 params: vec![],
2889 return_type: None,
2890 effect_clause: vec![],
2891 where_clause: vec![],
2892 body: Box::new(method_body),
2893 },
2894 );
2895 let cls = node(
2896 1,
2897 NodeKind::ClassDecl {
2898 annotations: vec![],
2899 visibility: Visibility::Public,
2900 name: ident("Person"),
2901 generic_params: vec![],
2902 base: None,
2903 traits: vec![],
2904 fields: vec![bock_ast::RecordDeclField {
2905 id: 0,
2906 span: span(),
2907 name: ident("name"),
2908 ty: bock_ast::TypeExpr::Named {
2909 id: 0,
2910 span: span(),
2911 path: type_path(&["String"]),
2912 args: vec![],
2913 },
2914 default: None,
2915 }],
2916 methods: vec![method],
2917 },
2918 );
2919 let out = gen(&module(vec![], vec![cls]));
2920 assert!(out.contains("class Person {"));
2921 assert!(out.contains("constructor(name)"));
2922 assert!(out.contains("this.name = name;"));
2923 assert!(out.contains("greet()"));
2924 }
2925
2926 #[test]
2927 fn const_declaration() {
2928 let c = node(
2929 1,
2930 NodeKind::ConstDecl {
2931 annotations: vec![],
2932 visibility: Visibility::Public,
2933 name: ident("PI"),
2934 ty: Box::new(node(
2935 2,
2936 NodeKind::TypeNamed {
2937 path: type_path(&["Float"]),
2938 args: vec![],
2939 },
2940 )),
2941 value: Box::new(node(
2942 3,
2943 NodeKind::Literal {
2944 lit: Literal::Float("3.14159".into()),
2945 },
2946 )),
2947 },
2948 );
2949 let out = gen(&module(vec![], vec![c]));
2950 assert!(out.contains("const PI = 3.14159;"));
2951 }
2952
2953 #[test]
2954 fn record_declaration() {
2955 let rec = node(
2956 1,
2957 NodeKind::RecordDecl {
2958 annotations: vec![],
2959 visibility: Visibility::Public,
2960 name: ident("Point"),
2961 generic_params: vec![],
2962 fields: vec![
2963 bock_ast::RecordDeclField {
2964 id: 0,
2965 span: span(),
2966 name: ident("x"),
2967 ty: bock_ast::TypeExpr::Named {
2968 id: 0,
2969 span: span(),
2970 path: type_path(&["Float"]),
2971 args: vec![],
2972 },
2973 default: None,
2974 },
2975 bock_ast::RecordDeclField {
2976 id: 0,
2977 span: span(),
2978 name: ident("y"),
2979 ty: bock_ast::TypeExpr::Named {
2980 id: 0,
2981 span: span(),
2982 path: type_path(&["Float"]),
2983 args: vec![],
2984 },
2985 default: None,
2986 },
2987 ],
2988 },
2989 );
2990 let out = gen(&module(vec![], vec![rec]));
2991 assert!(out.contains("class Point {"));
2992 assert!(out.contains("constructor({ x, y })"));
2993 assert!(out.contains("this.x = x;"));
2994 assert!(out.contains("this.y = y;"));
2995 }
2996
2997 fn has_node() -> bool {
3000 std::process::Command::new("which")
3001 .arg("node")
3002 .output()
3003 .map(|o| o.status.success())
3004 .unwrap_or(false)
3005 }
3006
3007 fn check_js_syntax(code: &str) -> bool {
3009 use std::io::Write;
3010 let mut child = std::process::Command::new("node")
3011 .arg("--check")
3012 .stdin(std::process::Stdio::piped())
3013 .stdout(std::process::Stdio::null())
3014 .stderr(std::process::Stdio::null())
3015 .spawn()
3016 .expect("failed to spawn node");
3017 child
3018 .stdin
3019 .as_mut()
3020 .unwrap()
3021 .write_all(code.as_bytes())
3022 .unwrap();
3023 child.wait().unwrap().success()
3024 }
3025
3026 fn run_js(code: &str) -> String {
3028 let output = std::process::Command::new("node")
3029 .arg("-e")
3030 .arg(code)
3031 .output()
3032 .expect("failed to run node");
3033 String::from_utf8(output.stdout).unwrap().trim().to_string()
3034 }
3035
3036 #[test]
3037 #[ignore]
3038 fn e2e_hello_world() {
3039 if !has_node() {
3040 return;
3041 }
3042 let body = block(
3044 2,
3045 vec![],
3046 Some(node(
3047 3,
3048 NodeKind::Call {
3049 callee: Box::new(node(
3050 4,
3051 NodeKind::FieldAccess {
3052 object: Box::new(id_node(5, "console")),
3053 field: ident("log"),
3054 },
3055 )),
3056 args: vec![AirArg {
3057 label: None,
3058 value: str_lit(6, "Hello, World!"),
3059 }],
3060 type_args: vec![],
3061 },
3062 )),
3063 );
3064 let f = node(
3065 1,
3066 NodeKind::FnDecl {
3067 annotations: vec![],
3068 visibility: Visibility::Private,
3069 is_async: false,
3070 name: ident("main"),
3071 generic_params: vec![],
3072 params: vec![],
3073 return_type: None,
3074 effect_clause: vec![],
3075 where_clause: vec![],
3076 body: Box::new(body),
3077 },
3078 );
3079 let code = gen(&module(vec![], vec![f]));
3080 let full = format!("{code}\nmain();\n");
3081 assert!(check_js_syntax(&full));
3082 assert_eq!(run_js(&full), "Hello, World!");
3083 }
3084
3085 #[test]
3086 #[ignore]
3087 fn e2e_arithmetic() {
3088 if !has_node() {
3089 return;
3090 }
3091 let body = block(
3092 2,
3093 vec![],
3094 Some(node(
3095 3,
3096 NodeKind::BinaryOp {
3097 op: BinOp::Add,
3098 left: Box::new(int_lit(4, "10")),
3099 right: Box::new(int_lit(5, "32")),
3100 },
3101 )),
3102 );
3103 let f = node(
3104 1,
3105 NodeKind::FnDecl {
3106 annotations: vec![],
3107 visibility: Visibility::Private,
3108 is_async: false,
3109 name: ident("calc"),
3110 generic_params: vec![],
3111 params: vec![],
3112 return_type: None,
3113 effect_clause: vec![],
3114 where_clause: vec![],
3115 body: Box::new(body),
3116 },
3117 );
3118 let code = gen(&module(vec![], vec![f]));
3119 let full = format!("{code}\nconsole.log(calc());\n");
3120 assert!(check_js_syntax(&full));
3121 assert_eq!(run_js(&full), "42");
3122 }
3123
3124 #[test]
3125 #[ignore]
3126 fn e2e_if_else() {
3127 if !has_node() {
3128 return;
3129 }
3130 let if_stmt = node(
3131 3,
3132 NodeKind::If {
3133 let_pattern: None,
3134 condition: Box::new(node(
3135 4,
3136 NodeKind::BinaryOp {
3137 op: BinOp::Gt,
3138 left: Box::new(id_node(5, "x")),
3139 right: Box::new(int_lit(6, "0")),
3140 },
3141 )),
3142 then_block: Box::new(block(7, vec![], Some(str_lit(8, "positive")))),
3143 else_block: Some(Box::new(block(
3144 9,
3145 vec![],
3146 Some(str_lit(10, "non-positive")),
3147 ))),
3148 },
3149 );
3150 let body = block(2, vec![if_stmt], None);
3151 let f = node(
3152 1,
3153 NodeKind::FnDecl {
3154 annotations: vec![],
3155 visibility: Visibility::Private,
3156 is_async: false,
3157 name: ident("classify"),
3158 generic_params: vec![],
3159 params: vec![param_node(11, "x")],
3160 return_type: None,
3161 effect_clause: vec![],
3162 where_clause: vec![],
3163 body: Box::new(body),
3164 },
3165 );
3166 let code = gen(&module(vec![], vec![f]));
3167 let full = format!("{code}\nconsole.log(classify(5));\nconsole.log(classify(-1));\n");
3168 assert!(check_js_syntax(&full));
3169 let output = run_js(&full);
3170 assert!(output.contains("positive"));
3171 assert!(output.contains("non-positive"));
3172 }
3173
3174 #[test]
3175 #[ignore]
3176 fn e2e_for_loop() {
3177 if !has_node() {
3178 return;
3179 }
3180 let body = block(
3181 2,
3182 vec![
3183 node(
3184 3,
3185 NodeKind::LetBinding {
3186 is_mut: true,
3187 pattern: Box::new(bind_pat(4, "sum")),
3188 ty: None,
3189 value: Box::new(int_lit(5, "0")),
3190 },
3191 ),
3192 node(
3193 6,
3194 NodeKind::For {
3195 pattern: Box::new(bind_pat(7, "x")),
3196 iterable: Box::new(node(
3197 8,
3198 NodeKind::ListLiteral {
3199 elems: vec![int_lit(9, "1"), int_lit(10, "2"), int_lit(11, "3")],
3200 },
3201 )),
3202 body: Box::new(block(
3203 12,
3204 vec![node(
3205 13,
3206 NodeKind::Assign {
3207 op: AssignOp::AddAssign,
3208 target: Box::new(id_node(14, "sum")),
3209 value: Box::new(id_node(15, "x")),
3210 },
3211 )],
3212 None,
3213 )),
3214 },
3215 ),
3216 ],
3217 Some(id_node(16, "sum")),
3218 );
3219 let f = node(
3220 1,
3221 NodeKind::FnDecl {
3222 annotations: vec![],
3223 visibility: Visibility::Private,
3224 is_async: false,
3225 name: ident("total"),
3226 generic_params: vec![],
3227 params: vec![],
3228 return_type: None,
3229 effect_clause: vec![],
3230 where_clause: vec![],
3231 body: Box::new(body),
3232 },
3233 );
3234 let code = gen(&module(vec![], vec![f]));
3235 let full = format!("{code}\nconsole.log(total());\n");
3236 assert!(check_js_syntax(&full));
3237 assert_eq!(run_js(&full), "6");
3238 }
3239
3240 #[test]
3241 #[ignore]
3242 fn e2e_tagged_objects() {
3243 if !has_node() {
3244 return;
3245 }
3246 let enum_decl = node(
3248 1,
3249 NodeKind::EnumDecl {
3250 annotations: vec![],
3251 visibility: Visibility::Public,
3252 name: ident("Color"),
3253 generic_params: vec![],
3254 variants: vec![
3255 node(
3256 2,
3257 NodeKind::EnumVariant {
3258 name: ident("Red"),
3259 payload: EnumVariantPayload::Unit,
3260 },
3261 ),
3262 node(
3263 3,
3264 NodeKind::EnumVariant {
3265 name: ident("Green"),
3266 payload: EnumVariantPayload::Unit,
3267 },
3268 ),
3269 node(
3270 4,
3271 NodeKind::EnumVariant {
3272 name: ident("Blue"),
3273 payload: EnumVariantPayload::Unit,
3274 },
3275 ),
3276 ],
3277 },
3278 );
3279 let code = gen(&module(vec![], vec![enum_decl]));
3280 let full =
3281 format!("{code}\nconsole.log(Color_Red._tag);\nconsole.log(Color_Green._tag);\n");
3282 assert!(check_js_syntax(&full));
3283 let output = run_js(&full);
3284 assert!(output.contains("Red"));
3285 assert!(output.contains("Green"));
3286 }
3287
3288 #[test]
3289 #[ignore]
3290 fn e2e_match_switch() {
3291 if !has_node() {
3292 return;
3293 }
3294 let match_node = node(
3296 3,
3297 NodeKind::Match {
3298 scrutinee: Box::new(id_node(4, "n")),
3299 arms: vec![
3300 node(
3301 5,
3302 NodeKind::MatchArm {
3303 pattern: Box::new(node(
3304 6,
3305 NodeKind::LiteralPat {
3306 lit: Literal::Int("1".into()),
3307 },
3308 )),
3309 guard: None,
3310 body: Box::new(block(7, vec![], Some(str_lit(8, "one")))),
3311 },
3312 ),
3313 node(
3314 9,
3315 NodeKind::MatchArm {
3316 pattern: Box::new(node(
3317 10,
3318 NodeKind::LiteralPat {
3319 lit: Literal::Int("2".into()),
3320 },
3321 )),
3322 guard: None,
3323 body: Box::new(block(11, vec![], Some(str_lit(12, "two")))),
3324 },
3325 ),
3326 node(
3327 13,
3328 NodeKind::MatchArm {
3329 pattern: Box::new(node(14, NodeKind::WildcardPat)),
3330 guard: None,
3331 body: Box::new(block(15, vec![], Some(str_lit(16, "other")))),
3332 },
3333 ),
3334 ],
3335 },
3336 );
3337 let f = node(
3338 1,
3339 NodeKind::FnDecl {
3340 annotations: vec![],
3341 visibility: Visibility::Private,
3342 is_async: false,
3343 name: ident("describe"),
3344 generic_params: vec![],
3345 params: vec![param_node(2, "n")],
3346 return_type: None,
3347 effect_clause: vec![],
3348 where_clause: vec![],
3349 body: Box::new(block(17, vec![match_node], None)),
3350 },
3351 );
3352 let code = gen(&module(vec![], vec![f]));
3353 let full = format!("{code}\nconsole.log(describe(1));\nconsole.log(describe(2));\nconsole.log(describe(99));\n");
3354 assert!(check_js_syntax(&full));
3355 let output = run_js(&full);
3356 assert!(output.contains("one"));
3357 assert!(output.contains("two"));
3358 assert!(output.contains("other"));
3359 }
3360
3361 #[test]
3362 #[ignore]
3363 fn e2e_string_interpolation() {
3364 if !has_node() {
3365 return;
3366 }
3367 let body = block(
3368 2,
3369 vec![],
3370 Some(node(
3371 3,
3372 NodeKind::Interpolation {
3373 parts: vec![
3374 AirInterpolationPart::Literal("Hello, ".into()),
3375 AirInterpolationPart::Expr(Box::new(id_node(4, "name"))),
3376 AirInterpolationPart::Literal("! You are ".into()),
3377 AirInterpolationPart::Expr(Box::new(id_node(5, "age"))),
3378 AirInterpolationPart::Literal(" years old.".into()),
3379 ],
3380 },
3381 )),
3382 );
3383 let f = node(
3384 1,
3385 NodeKind::FnDecl {
3386 annotations: vec![],
3387 visibility: Visibility::Private,
3388 is_async: false,
3389 name: ident("greet"),
3390 generic_params: vec![],
3391 params: vec![param_node(6, "name"), param_node(7, "age")],
3392 return_type: None,
3393 effect_clause: vec![],
3394 where_clause: vec![],
3395 body: Box::new(body),
3396 },
3397 );
3398 let code = gen(&module(vec![], vec![f]));
3399 let full = format!("{code}\nconsole.log(greet(\"Alice\", 30));\n");
3400 assert!(check_js_syntax(&full));
3401 assert_eq!(run_js(&full), "Hello, Alice! You are 30 years old.");
3402 }
3403
3404 #[test]
3405 #[ignore]
3406 fn e2e_lambda_and_method_call() {
3407 if !has_node() {
3408 return;
3409 }
3410 let body = block(
3411 2,
3412 vec![node(
3413 3,
3414 NodeKind::LetBinding {
3415 is_mut: false,
3416 pattern: Box::new(bind_pat(4, "nums")),
3417 ty: None,
3418 value: Box::new(node(
3419 5,
3420 NodeKind::ListLiteral {
3421 elems: vec![int_lit(6, "1"), int_lit(7, "2"), int_lit(8, "3")],
3422 },
3423 )),
3424 },
3425 )],
3426 Some(node(
3427 9,
3428 NodeKind::MethodCall {
3429 receiver: Box::new(node(
3430 10,
3431 NodeKind::MethodCall {
3432 receiver: Box::new(id_node(11, "nums")),
3433 method: ident("map"),
3434 type_args: vec![],
3435 args: vec![AirArg {
3436 label: None,
3437 value: node(
3438 12,
3439 NodeKind::Lambda {
3440 params: vec![param_node(13, "x")],
3441 body: Box::new(node(
3442 14,
3443 NodeKind::BinaryOp {
3444 op: BinOp::Mul,
3445 left: Box::new(id_node(15, "x")),
3446 right: Box::new(int_lit(16, "2")),
3447 },
3448 )),
3449 },
3450 ),
3451 }],
3452 },
3453 )),
3454 method: ident("join"),
3455 type_args: vec![],
3456 args: vec![AirArg {
3457 label: None,
3458 value: str_lit(17, ", "),
3459 }],
3460 },
3461 )),
3462 );
3463 let f = node(
3464 1,
3465 NodeKind::FnDecl {
3466 annotations: vec![],
3467 visibility: Visibility::Private,
3468 is_async: false,
3469 name: ident("transform"),
3470 generic_params: vec![],
3471 params: vec![],
3472 return_type: None,
3473 effect_clause: vec![],
3474 where_clause: vec![],
3475 body: Box::new(body),
3476 },
3477 );
3478 let code = gen(&module(vec![], vec![f]));
3479 let full = format!("{code}\nconsole.log(transform());\n");
3480 assert!(check_js_syntax(&full));
3481 assert_eq!(run_js(&full), "2, 4, 6");
3482 }
3483
3484 #[test]
3485 #[ignore]
3486 fn e2e_while_loop() {
3487 if !has_node() {
3488 return;
3489 }
3490 let body = block(
3491 2,
3492 vec![
3493 node(
3494 3,
3495 NodeKind::LetBinding {
3496 is_mut: true,
3497 pattern: Box::new(bind_pat(4, "i")),
3498 ty: None,
3499 value: Box::new(int_lit(5, "0")),
3500 },
3501 ),
3502 node(
3503 6,
3504 NodeKind::LetBinding {
3505 is_mut: true,
3506 pattern: Box::new(bind_pat(7, "result")),
3507 ty: None,
3508 value: Box::new(int_lit(8, "1")),
3509 },
3510 ),
3511 node(
3512 9,
3513 NodeKind::While {
3514 condition: Box::new(node(
3515 10,
3516 NodeKind::BinaryOp {
3517 op: BinOp::Lt,
3518 left: Box::new(id_node(11, "i")),
3519 right: Box::new(id_node(12, "n")),
3520 },
3521 )),
3522 body: Box::new(block(
3523 13,
3524 vec![
3525 node(
3526 14,
3527 NodeKind::Assign {
3528 op: AssignOp::MulAssign,
3529 target: Box::new(id_node(15, "result")),
3530 value: Box::new(int_lit(16, "2")),
3531 },
3532 ),
3533 node(
3534 17,
3535 NodeKind::Assign {
3536 op: AssignOp::AddAssign,
3537 target: Box::new(id_node(18, "i")),
3538 value: Box::new(int_lit(19, "1")),
3539 },
3540 ),
3541 ],
3542 None,
3543 )),
3544 },
3545 ),
3546 ],
3547 Some(id_node(20, "result")),
3548 );
3549 let f = node(
3550 1,
3551 NodeKind::FnDecl {
3552 annotations: vec![],
3553 visibility: Visibility::Private,
3554 is_async: false,
3555 name: ident("pow2"),
3556 generic_params: vec![],
3557 params: vec![param_node(21, "n")],
3558 return_type: None,
3559 effect_clause: vec![],
3560 where_clause: vec![],
3561 body: Box::new(body),
3562 },
3563 );
3564 let code = gen(&module(vec![], vec![f]));
3565 let full = format!("{code}\nconsole.log(pow2(10));\n");
3566 assert!(check_js_syntax(&full));
3567 assert_eq!(run_js(&full), "1024");
3568 }
3569
3570 #[test]
3571 #[ignore]
3572 fn e2e_async_await() {
3573 if !has_node() {
3574 return;
3575 }
3576 let body = block(
3578 2,
3579 vec![],
3580 Some(node(
3581 3,
3582 NodeKind::Await {
3583 expr: Box::new(node(
3584 4,
3585 NodeKind::Call {
3586 callee: Box::new(node(
3587 5,
3588 NodeKind::FieldAccess {
3589 object: Box::new(id_node(6, "Promise")),
3590 field: ident("resolve"),
3591 },
3592 )),
3593 args: vec![AirArg {
3594 label: None,
3595 value: int_lit(7, "42"),
3596 }],
3597 type_args: vec![],
3598 },
3599 )),
3600 },
3601 )),
3602 );
3603 let f = node(
3604 1,
3605 NodeKind::FnDecl {
3606 annotations: vec![],
3607 visibility: Visibility::Private,
3608 is_async: true,
3609 name: ident("delayed"),
3610 generic_params: vec![],
3611 params: vec![],
3612 return_type: None,
3613 effect_clause: vec![],
3614 where_clause: vec![],
3615 body: Box::new(body),
3616 },
3617 );
3618 let code = gen(&module(vec![], vec![f]));
3619 let full = format!("{code}\ndelayed().then(v => console.log(v));\n");
3620 assert!(check_js_syntax(&full));
3621 assert_eq!(run_js(&full), "42");
3622 }
3623
3624 #[test]
3625 fn to_camel_case_converts() {
3626 assert_eq!(to_camel_case("Log"), "log");
3628 assert_eq!(to_camel_case("Clock"), "clock");
3629 assert_eq!(to_camel_case("IO"), "iO");
3630 assert_eq!(to_camel_case(""), "");
3631 assert_eq!(to_camel_case("create_user"), "createUser");
3633 assert_eq!(to_camel_case("get_all_items"), "getAllItems");
3634 assert_eq!(to_camel_case("createUser"), "createUser");
3636 assert_eq!(to_camel_case("x"), "x");
3637 assert_eq!(to_camel_case("_"), "_");
3639 }
3640
3641 #[test]
3642 fn snake_case_fn_becomes_camel_case() {
3643 let body = block(2, vec![], Some(int_lit(3, "42")));
3644 let f = node(
3645 1,
3646 NodeKind::FnDecl {
3647 annotations: vec![],
3648 visibility: Visibility::Private,
3649 is_async: false,
3650 name: ident("create_user"),
3651 generic_params: vec![],
3652 params: vec![],
3653 return_type: None,
3654 effect_clause: vec![],
3655 where_clause: vec![],
3656 body: Box::new(body),
3657 },
3658 );
3659 let out = gen(&module(vec![], vec![f]));
3660 assert!(
3661 out.contains("function createUser()"),
3662 "expected camelCase function name, got: {out}"
3663 );
3664 }
3665
3666 #[test]
3667 fn escape_js_string_works() {
3668 assert_eq!(escape_js_string("hello"), "hello");
3669 assert_eq!(escape_js_string("he\"llo"), "he\\\"llo");
3670 assert_eq!(escape_js_string("line\nbreak"), "line\\nbreak");
3671 }
3672
3673 #[test]
3674 fn escape_template_literal_works() {
3675 assert_eq!(escape_template_literal("hello"), "hello");
3676 assert_eq!(escape_template_literal("cost: $5"), "cost: \\$5");
3677 assert_eq!(escape_template_literal("back`tick"), "back\\`tick");
3678 }
3679
3680 fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
3684 let call = node(
3685 10,
3686 NodeKind::Call {
3687 callee: Box::new(id_node(11, func_name)),
3688 args: vec![AirArg {
3689 label: None,
3690 value: arg,
3691 }],
3692 type_args: vec![],
3693 },
3694 );
3695 let body = block(2, vec![call], None);
3696 let f = node(
3697 1,
3698 NodeKind::FnDecl {
3699 name: ident("main"),
3700 params: vec![],
3701 return_type: None,
3702 body: Box::new(body),
3703 generic_params: vec![],
3704 visibility: Visibility::Private,
3705 annotations: vec![],
3706 effect_clause: vec![],
3707 where_clause: vec![],
3708 is_async: false,
3709 },
3710 );
3711 gen(&module(vec![], vec![f]))
3712 }
3713
3714 fn gen_prelude_call_no_args(func_name: &str) -> String {
3716 let call = node(
3717 10,
3718 NodeKind::Call {
3719 callee: Box::new(id_node(11, func_name)),
3720 args: vec![],
3721 type_args: vec![],
3722 },
3723 );
3724 let body = block(2, vec![call], None);
3725 let f = node(
3726 1,
3727 NodeKind::FnDecl {
3728 name: ident("main"),
3729 params: vec![],
3730 return_type: None,
3731 body: Box::new(body),
3732 generic_params: vec![],
3733 visibility: Visibility::Private,
3734 annotations: vec![],
3735 effect_clause: vec![],
3736 where_clause: vec![],
3737 is_async: false,
3738 },
3739 );
3740 gen(&module(vec![], vec![f]))
3741 }
3742
3743 #[test]
3744 fn prelude_println_maps_to_console_log() {
3745 let code = gen_prelude_call("println", str_lit(12, "Hello"));
3746 assert!(
3747 code.contains("console.log("),
3748 "expected console.log, got: {code}"
3749 );
3750 assert!(
3751 !code.contains("println("),
3752 "should not contain bare println, got: {code}"
3753 );
3754 }
3755
3756 #[test]
3757 fn prelude_print_maps_to_process_stdout_write() {
3758 let code = gen_prelude_call("print", str_lit(12, "no newline"));
3759 assert!(
3760 code.contains("process.stdout.write(String("),
3761 "expected process.stdout.write, got: {code}"
3762 );
3763 }
3764
3765 #[test]
3766 fn prelude_debug_maps_to_console_debug() {
3767 let code = gen_prelude_call("debug", str_lit(12, "val"));
3768 assert!(
3769 code.contains("console.debug("),
3770 "expected console.debug, got: {code}"
3771 );
3772 }
3773
3774 #[test]
3775 fn prelude_assert_maps_to_throw() {
3776 let code = gen_prelude_call("assert", bool_lit(12, true));
3777 assert!(
3778 code.contains("if (!true) throw new Error(\"assertion failed\")"),
3779 "expected assert mapping, got: {code}"
3780 );
3781 }
3782
3783 #[test]
3784 fn prelude_todo_maps_to_throw_not_implemented() {
3785 let code = gen_prelude_call_no_args("todo");
3786 assert!(
3787 code.contains("throw new Error(\"not implemented\")"),
3788 "expected todo mapping, got: {code}"
3789 );
3790 }
3791
3792 #[test]
3793 fn prelude_unreachable_maps_to_throw_unreachable() {
3794 let code = gen_prelude_call_no_args("unreachable");
3795 assert!(
3796 code.contains("throw new Error(\"unreachable\")"),
3797 "expected unreachable mapping, got: {code}"
3798 );
3799 }
3800
3801 #[test]
3802 fn non_prelude_call_unaffected() {
3803 let code = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
3804 assert!(
3805 code.contains("myCustomFunc("),
3806 "expected normal call emission, got: {code}"
3807 );
3808 }
3809
3810 #[test]
3813 fn effect_decl_becomes_class() {
3814 let effect = node(
3815 1,
3816 NodeKind::EffectDecl {
3817 annotations: vec![],
3818 visibility: Visibility::Public,
3819 name: ident("Logger"),
3820 generic_params: vec![],
3821 components: vec![],
3822 operations: vec![
3823 node(
3824 2,
3825 NodeKind::FnDecl {
3826 annotations: vec![],
3827 visibility: Visibility::Public,
3828 is_async: false,
3829 name: ident("log"),
3830 generic_params: vec![],
3831 params: vec![
3832 param_node(3, "level"),
3833 param_node(4, "msg"),
3834 ],
3835 return_type: None,
3836 effect_clause: vec![],
3837 where_clause: vec![],
3838 body: Box::new(block(5, vec![], None)),
3839 },
3840 ),
3841 node(
3842 6,
3843 NodeKind::FnDecl {
3844 annotations: vec![],
3845 visibility: Visibility::Public,
3846 is_async: false,
3847 name: ident("flush"),
3848 generic_params: vec![],
3849 params: vec![],
3850 return_type: None,
3851 effect_clause: vec![],
3852 where_clause: vec![],
3853 body: Box::new(block(7, vec![], None)),
3854 },
3855 ),
3856 ],
3857 },
3858 );
3859 let out = gen(&module(vec![], vec![effect]));
3860 assert!(out.contains("class Logger {"), "got: {out}");
3861 assert!(out.contains("log(level, msg) {"), "got: {out}");
3862 assert!(out.contains("flush() {"), "got: {out}");
3863 assert!(
3864 out.contains("throw new Error(\"not implemented\");"),
3865 "got: {out}"
3866 );
3867 }
3868
3869 #[test]
3870 fn effect_decl_empty_operations() {
3871 let effect = node(
3872 1,
3873 NodeKind::EffectDecl {
3874 annotations: vec![],
3875 visibility: Visibility::Public,
3876 name: ident("Empty"),
3877 generic_params: vec![],
3878 components: vec![],
3879 operations: vec![],
3880 },
3881 );
3882 let out = gen(&module(vec![], vec![effect]));
3883 assert!(out.contains("class Empty {"), "got: {out}");
3884 assert!(out.contains("}"), "got: {out}");
3885 }
3886
3887 #[test]
3888 fn handling_block_passes_handlers_to_effectful_call() {
3889 use bock_air::AirHandlerPair;
3890
3891 let effect_decl = node(
3892 1,
3893 NodeKind::EffectDecl {
3894 annotations: vec![],
3895 visibility: Visibility::Public,
3896 name: ident("Logger"),
3897 generic_params: vec![],
3898 components: vec![],
3899 operations: vec![node(
3900 2,
3901 NodeKind::FnDecl {
3902 annotations: vec![],
3903 visibility: Visibility::Public,
3904 is_async: false,
3905 name: ident("log"),
3906 generic_params: vec![],
3907 params: vec![param_node(3, "msg")],
3908 return_type: None,
3909 effect_clause: vec![],
3910 where_clause: vec![],
3911 body: Box::new(block(4, vec![], None)),
3912 },
3913 )],
3914 },
3915 );
3916
3917 let inner_fn = node(
3919 10,
3920 NodeKind::FnDecl {
3921 annotations: vec![],
3922 visibility: Visibility::Private,
3923 is_async: false,
3924 name: ident("inner"),
3925 generic_params: vec![],
3926 params: vec![],
3927 return_type: None,
3928 effect_clause: vec![type_path(&["Logger"])],
3929 where_clause: vec![],
3930 body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
3931 },
3932 );
3933
3934 let call_inner = node(
3935 20,
3936 NodeKind::Call {
3937 callee: Box::new(id_node(21, "inner")),
3938 args: vec![],
3939 type_args: vec![],
3940 },
3941 );
3942 let handling = node(
3943 30,
3944 NodeKind::HandlingBlock {
3945 handlers: vec![AirHandlerPair {
3946 effect: type_path(&["Logger"]),
3947 handler: Box::new(node(
3948 31,
3949 NodeKind::Call {
3950 callee: Box::new(id_node(32, "StdoutLogger")),
3951 args: vec![],
3952 type_args: vec![],
3953 },
3954 )),
3955 }],
3956 body: Box::new(block(33, vec![], Some(call_inner))),
3957 },
3958 );
3959 let main_fn = node(
3960 40,
3961 NodeKind::FnDecl {
3962 annotations: vec![],
3963 visibility: Visibility::Private,
3964 is_async: false,
3965 name: ident("main"),
3966 generic_params: vec![],
3967 params: vec![],
3968 return_type: None,
3969 effect_clause: vec![],
3970 where_clause: vec![],
3971 body: Box::new(block(41, vec![handling], None)),
3972 },
3973 );
3974
3975 let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
3976 assert!(
3978 out.contains("inner({ logger: __logger })"),
3979 "handling block should pass handler to effectful call, got: {out}"
3980 );
3981 assert!(
3982 out.contains("const __logger = stdoutLogger()"),
3983 "handling block should instantiate handler, got: {out}"
3984 );
3985 }
3986
3987 #[test]
3988 fn composite_effect_expands_to_components() {
3989 use bock_air::AirHandlerPair;
3990
3991 let logger_decl = node(
3993 1,
3994 NodeKind::EffectDecl {
3995 annotations: vec![],
3996 visibility: Visibility::Public,
3997 name: ident("Logger"),
3998 generic_params: vec![],
3999 components: vec![],
4000 operations: vec![node(
4001 2,
4002 NodeKind::FnDecl {
4003 annotations: vec![],
4004 visibility: Visibility::Public,
4005 is_async: false,
4006 name: ident("log"),
4007 generic_params: vec![],
4008 params: vec![param_node(3, "msg")],
4009 return_type: None,
4010 effect_clause: vec![],
4011 where_clause: vec![],
4012 body: Box::new(block(4, vec![], None)),
4013 },
4014 )],
4015 },
4016 );
4017
4018 let clock_decl = node(
4020 5,
4021 NodeKind::EffectDecl {
4022 annotations: vec![],
4023 visibility: Visibility::Public,
4024 name: ident("Clock"),
4025 generic_params: vec![],
4026 components: vec![],
4027 operations: vec![node(
4028 6,
4029 NodeKind::FnDecl {
4030 annotations: vec![],
4031 visibility: Visibility::Public,
4032 is_async: false,
4033 name: ident("now"),
4034 generic_params: vec![],
4035 params: vec![],
4036 return_type: None,
4037 effect_clause: vec![],
4038 where_clause: vec![],
4039 body: Box::new(block(7, vec![], None)),
4040 },
4041 )],
4042 },
4043 );
4044
4045 let composite_decl = node(
4047 8,
4048 NodeKind::EffectDecl {
4049 annotations: vec![],
4050 visibility: Visibility::Public,
4051 name: ident("ServiceStack"),
4052 generic_params: vec![],
4053 components: vec![type_path(&["Logger"]), type_path(&["Clock"])],
4054 operations: vec![],
4055 },
4056 );
4057
4058 let serve_fn = node(
4060 10,
4061 NodeKind::FnDecl {
4062 annotations: vec![],
4063 visibility: Visibility::Private,
4064 is_async: false,
4065 name: ident("serve"),
4066 generic_params: vec![],
4067 params: vec![param_node(11, "request")],
4068 return_type: None,
4069 effect_clause: vec![type_path(&["ServiceStack"])],
4070 where_clause: vec![],
4071 body: Box::new(block(12, vec![], Some(str_lit(13, "ok")))),
4072 },
4073 );
4074
4075 let call_serve = node(
4077 20,
4078 NodeKind::Call {
4079 callee: Box::new(id_node(21, "serve")),
4080 args: vec![bock_air::AirArg {
4081 label: None,
4082 value: str_lit(22, "GET /"),
4083 }],
4084 type_args: vec![],
4085 },
4086 );
4087 let handling = node(
4088 30,
4089 NodeKind::HandlingBlock {
4090 handlers: vec![
4091 AirHandlerPair {
4092 effect: type_path(&["Logger"]),
4093 handler: Box::new(node(
4094 31,
4095 NodeKind::Call {
4096 callee: Box::new(id_node(32, "StdLogger")),
4097 args: vec![],
4098 type_args: vec![],
4099 },
4100 )),
4101 },
4102 AirHandlerPair {
4103 effect: type_path(&["Clock"]),
4104 handler: Box::new(node(
4105 33,
4106 NodeKind::Call {
4107 callee: Box::new(id_node(34, "StdClock")),
4108 args: vec![],
4109 type_args: vec![],
4110 },
4111 )),
4112 },
4113 ],
4114 body: Box::new(block(35, vec![], Some(call_serve))),
4115 },
4116 );
4117 let main_fn = node(
4118 40,
4119 NodeKind::FnDecl {
4120 annotations: vec![],
4121 visibility: Visibility::Private,
4122 is_async: false,
4123 name: ident("main"),
4124 generic_params: vec![],
4125 params: vec![],
4126 return_type: None,
4127 effect_clause: vec![],
4128 where_clause: vec![],
4129 body: Box::new(block(41, vec![handling], None)),
4130 },
4131 );
4132
4133 let out = gen(&module(
4134 vec![],
4135 vec![logger_decl, clock_decl, composite_decl, serve_fn, main_fn],
4136 ));
4137
4138 assert!(
4140 out.contains("// composite effect ServiceStack = Logger + Clock"),
4141 "composite effect should be a comment, got: {out}"
4142 );
4143 assert!(
4144 !out.contains("class ServiceStack"),
4145 "composite effect should NOT generate a class, got: {out}"
4146 );
4147
4148 assert!(
4150 out.contains("function serve(request, { logger, clock })"),
4151 "serve should have expanded effect params, got: {out}"
4152 );
4153
4154 assert!(
4156 out.contains("logger: __logger") && out.contains("clock: __clock"),
4157 "call should pass expanded handler args, got: {out}"
4158 );
4159 }
4160
4161 #[test]
4162 fn record_becomes_class_for_prototype_impls() {
4163 use bock_air::AirHandlerPair;
4164
4165 let rec = node(
4166 1,
4167 NodeKind::RecordDecl {
4168 annotations: vec![],
4169 visibility: Visibility::Public,
4170 name: ident("ConsoleLogger"),
4171 generic_params: vec![],
4172 fields: vec![],
4173 },
4174 );
4175 let out = gen(&module(vec![], vec![rec]));
4176 assert!(
4177 out.contains("class ConsoleLogger {}"),
4178 "empty record should be an empty class, got: {out}"
4179 );
4180 let _ = AirHandlerPair { effect: type_path(&["X"]),
4182 handler: Box::new(id_node(0, "x")),
4183 };
4184 }
4185
4186 #[test]
4187 fn record_construct_of_declared_record_uses_new() {
4188 let rec = node(
4189 1,
4190 NodeKind::RecordDecl {
4191 annotations: vec![],
4192 visibility: Visibility::Public,
4193 name: ident("ConsoleLogger"),
4194 generic_params: vec![],
4195 fields: vec![],
4196 },
4197 );
4198 let construct = node(
4199 2,
4200 NodeKind::RecordConstruct {
4201 path: type_path(&["ConsoleLogger"]),
4202 fields: vec![],
4203 spread: None,
4204 },
4205 );
4206 let let_stmt = node(
4207 3,
4208 NodeKind::LetBinding {
4209 is_mut: false,
4210 pattern: Box::new(bind_pat(4, "x")),
4211 ty: None,
4212 value: Box::new(construct),
4213 },
4214 );
4215 let f = node(
4216 5,
4217 NodeKind::FnDecl {
4218 annotations: vec![],
4219 visibility: Visibility::Private,
4220 is_async: false,
4221 name: ident("test"),
4222 generic_params: vec![],
4223 params: vec![],
4224 return_type: None,
4225 effect_clause: vec![],
4226 where_clause: vec![],
4227 body: Box::new(block(6, vec![let_stmt], None)),
4228 },
4229 );
4230 let out = gen(&module(vec![], vec![rec, f]));
4231 assert!(
4232 out.contains("new ConsoleLogger()"),
4233 "declared record construct should use `new`, got: {out}"
4234 );
4235 }
4236
4237 #[test]
4238 fn module_handle_registers_handler_for_same_module_calls() {
4239 use bock_air::AirHandlerPair;
4240 let _ = AirHandlerPair {
4241 effect: type_path(&["X"]),
4242 handler: Box::new(id_node(0, "x")),
4243 };
4244
4245 let effect_decl = node(
4247 1,
4248 NodeKind::EffectDecl {
4249 annotations: vec![],
4250 visibility: Visibility::Public,
4251 name: ident("Logger"),
4252 generic_params: vec![],
4253 components: vec![],
4254 operations: vec![node(
4255 2,
4256 NodeKind::FnDecl {
4257 annotations: vec![],
4258 visibility: Visibility::Public,
4259 is_async: false,
4260 name: ident("log"),
4261 generic_params: vec![],
4262 params: vec![param_node(3, "msg")],
4263 return_type: None,
4264 effect_clause: vec![],
4265 where_clause: vec![],
4266 body: Box::new(block(4, vec![], None)),
4267 },
4268 )],
4269 },
4270 );
4271
4272 let rec = node(
4274 5,
4275 NodeKind::RecordDecl {
4276 annotations: vec![],
4277 visibility: Visibility::Public,
4278 name: ident("StdoutLogger"),
4279 generic_params: vec![],
4280 fields: vec![],
4281 },
4282 );
4283
4284 let greet = node(
4286 10,
4287 NodeKind::FnDecl {
4288 annotations: vec![],
4289 visibility: Visibility::Public,
4290 is_async: false,
4291 name: ident("greet"),
4292 generic_params: vec![],
4293 params: vec![],
4294 return_type: None,
4295 effect_clause: vec![type_path(&["Logger"])],
4296 where_clause: vec![],
4297 body: Box::new(block(11, vec![], Some(str_lit(12, "hi")))),
4298 },
4299 );
4300
4301 let module_handle = node(
4303 20,
4304 NodeKind::ModuleHandle {
4305 effect: type_path(&["Logger"]),
4306 handler: Box::new(node(
4307 21,
4308 NodeKind::RecordConstruct {
4309 path: type_path(&["StdoutLogger"]),
4310 fields: vec![],
4311 spread: None,
4312 },
4313 )),
4314 },
4315 );
4316
4317 let call_greet = node(
4319 30,
4320 NodeKind::Call {
4321 callee: Box::new(id_node(31, "greet")),
4322 args: vec![],
4323 type_args: vec![],
4324 },
4325 );
4326 let main_fn = node(
4327 32,
4328 NodeKind::FnDecl {
4329 annotations: vec![],
4330 visibility: Visibility::Private,
4331 is_async: false,
4332 name: ident("main"),
4333 generic_params: vec![],
4334 params: vec![],
4335 return_type: None,
4336 effect_clause: vec![],
4337 where_clause: vec![],
4338 body: Box::new(block(33, vec![], Some(call_greet))),
4339 },
4340 );
4341
4342 let out = gen(&module(
4343 vec![],
4344 vec![effect_decl, rec, greet, module_handle, main_fn],
4345 ));
4346
4347 assert!(
4349 out.contains("const __logger = new StdoutLogger()"),
4350 "module handle should use `new` on declared record, got: {out}"
4351 );
4352 assert!(
4354 out.contains("greet({ logger: __logger })"),
4355 "module handle should be threaded into effectful calls, got: {out}"
4356 );
4357 }
4358
4359 #[test]
4362 fn entry_invocation_sync_main() {
4363 let inv = JsGenerator::new().entry_invocation(false).unwrap();
4364 assert_eq!(inv, "main();\n");
4365 }
4366
4367 #[test]
4368 fn entry_invocation_async_main() {
4369 let inv = JsGenerator::new().entry_invocation(true).unwrap();
4370 assert!(inv.contains("async () =>"));
4371 assert!(inv.contains("await main()"));
4372 }
4373
4374 #[test]
4375 fn generate_project_async_main_wraps_entry() {
4376 let main_fn = node(
4377 1,
4378 NodeKind::FnDecl {
4379 annotations: vec![],
4380 visibility: Visibility::Private,
4381 is_async: true,
4382 name: ident("main"),
4383 generic_params: vec![],
4384 params: vec![],
4385 return_type: None,
4386 effect_clause: vec![],
4387 where_clause: vec![],
4388 body: Box::new(block(2, vec![], None)),
4389 },
4390 );
4391 let m = module(vec![], vec![main_fn]);
4392 let gen = JsGenerator::new();
4393 let out = gen.generate_project(&[&m]).unwrap();
4394 let src = &out.files[0].content;
4395 assert!(src.contains("async function main()"), "got: {src}");
4396 assert!(
4397 src.contains("(async () => { await main(); })();"),
4398 "async entry wrapper missing, got: {src}"
4399 );
4400 }
4401}