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, 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
49const RANGE_RUNTIME_JS: &str = "\
59// ── Bock range runtime ──
60const range = (lo, hi) => { const r = []; for (let i = lo; i < hi; i++) r.push(i); return r; };
61const rangeInclusive = (lo, hi) => { const r = []; for (let i = lo; i <= hi; i++) r.push(i); return r; };
62";
63
64fn js_module_uses_range(items: &[AIRNode]) -> bool {
70 items.iter().any(|n| format!("{n:?}").contains("Range {"))
71}
72
73const EQ_RUNTIME_JS: &str = "\
102// ── Bock structural equality runtime ──
103const __bockEq = (a, b) => {
104 if (a === b) return true;
105 if (typeof a !== \"object\" || typeof b !== \"object\" || a === null || b === null) {
106 return a === b;
107 }
108 if (typeof a.eq === \"function\" && typeof b.eq === \"function\") return a.eq(a, b);
109 if (Array.isArray(a)) {
110 if (!Array.isArray(b) || a.length !== b.length) return false;
111 for (let i = 0; i < a.length; i++) { if (!__bockEq(a[i], b[i])) return false; }
112 return true;
113 }
114 if (a instanceof Map) {
115 if (!(b instanceof Map) || a.size !== b.size) return false;
116 for (const [k, v] of a) {
117 if (b.has(k)) { if (!__bockEq(b.get(k), v)) return false; continue; }
118 let found = false;
119 for (const [bk, bv] of b) { if (__bockEq(k, bk) && __bockEq(v, bv)) { found = true; break; } }
120 if (!found) return false;
121 }
122 return true;
123 }
124 if (a instanceof Set) {
125 if (!(b instanceof Set) || a.size !== b.size) return false;
126 for (const x of a) {
127 if (b.has(x)) continue;
128 let found = false;
129 for (const y of b) { if (__bockEq(x, y)) { found = true; break; } }
130 if (!found) return false;
131 }
132 return true;
133 }
134 const ka = Object.keys(a);
135 const kb = Object.keys(b);
136 if (ka.length !== kb.length) return false;
137 for (const k of ka) { if (!__bockEq(a[k], b[k])) return false; }
138 return true;
139};
140";
141
142fn js_module_uses_eq(items: &[AIRNode]) -> bool {
150 items.iter().any(|n| {
151 let dbg = format!("{n:?}");
152 dbg.contains("\"user_eq\": String(\"structural\")")
153 || dbg.contains("\"user_eq\": String(\"deep\")")
154 || dbg.contains("\"user_eq\": String(\"deep_custom\")")
155 || dbg.contains("\"user_eq\": String(\"generic\")")
156 || dbg.contains("TraitBound:Equatable")
157 })
158}
159
160const COMPARE_RUNTIME_JS: &str = "\
173// ── Bock structural comparison runtime ──
174const __bockCompare = (a, b) => {
175 if (a !== null && typeof a === \"object\" && typeof a.compare === \"function\") {
176 return a.compare(a, b);
177 }
178 return (a < b ? { _tag: \"Less\" } : (a === b ? { _tag: \"Equal\" } : { _tag: \"Greater\" }));
179};
180";
181
182fn js_module_uses_compare(items: &[AIRNode]) -> bool {
189 items.iter().any(|n| {
190 let dbg = format!("{n:?}");
191 dbg.contains("TraitBound:Comparable")
192 })
193}
194
195const STR_RUNTIME_JS: &str = "// ── Bock display-string runtime ──
206const __bockStr = (x) => {
207 if (x !== null && typeof x === \"object\" && typeof x.to_string === \"function\") {
208 return x.to_string(x);
209 }
210 return String(x);
211};
212";
213
214fn js_module_uses_str(items: &[AIRNode]) -> bool {
220 items.iter().any(|n| {
221 let dbg = format!("{n:?}");
222 dbg.contains("Interpolation")
223 })
224}
225
226const RUNTIME_MODULE_JS: &str = "_bock_runtime";
234
235#[derive(Debug)]
237pub struct JsGenerator {
238 profile: TargetProfile,
239}
240
241impl JsGenerator {
242 #[must_use]
244 pub fn new() -> Self {
245 Self {
246 profile: TargetProfile::javascript(),
247 }
248 }
249}
250
251impl Default for JsGenerator {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257impl CodeGenerator for JsGenerator {
258 fn target(&self) -> &TargetProfile {
259 &self.profile
260 }
261
262 fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
263 let module =
267 &crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(module.clone()));
268 let mut ctx = EmitCtx::new();
269 ctx.enum_variants =
270 crate::generator::collect_enum_variants(&[(module, std::path::Path::new(""))]);
271 ctx.trait_decls =
272 crate::generator::collect_trait_decls(&[(module, std::path::Path::new(""))]);
273 ctx.class_fields =
274 crate::generator::collect_class_fields(&[(module, std::path::Path::new(""))]);
275 ctx.const_names =
276 crate::generator::collect_const_names(&[(module, std::path::Path::new(""))]);
277 ctx.emit_node(module)?;
278 let (content, mappings) = ctx.finish();
279 let source_map = SourceMap {
280 generated_file: String::new(),
281 mappings,
282 ..Default::default()
283 };
284 Ok(GeneratedCode {
285 files: vec![OutputFile {
286 path: PathBuf::new(),
287 content,
288 source_map: Some(source_map),
289 }],
290 })
291 }
292
293 fn entry_invocation(&self, main_is_async: bool) -> Option<String> {
294 if main_is_async {
295 Some("(async () => { await main(); })();\n".to_string())
298 } else {
299 Some("main();\n".to_string())
300 }
301 }
302
303 fn generate_project(
326 &self,
327 modules: &[(&AIRModule, &std::path::Path)],
328 ) -> Result<GeneratedCode, CodegenError> {
329 let hoisted: Vec<(AIRModule, &std::path::Path)> = modules
333 .iter()
334 .map(|(m, p)| {
335 (
336 crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(
337 (*m).clone(),
338 )),
339 *p,
340 )
341 })
342 .collect();
343 let modules: Vec<(&AIRModule, &std::path::Path)> =
344 hoisted.iter().map(|(m, p)| (m, *p)).collect();
345 let modules = modules.as_slice();
346 let reachable = crate::generator::reachable_modules(modules);
349 let modules = reachable.as_slice();
350 if modules.is_empty() {
351 return Ok(GeneratedCode { files: vec![] });
352 }
353
354 let entry_idx = modules
357 .iter()
358 .position(|(m, _)| crate::generator::module_declares_main_fn(m))
359 .unwrap_or(modules.len() - 1);
360
361 let enum_variants = crate::generator::collect_enum_variants(modules);
364 let trait_decls = crate::generator::collect_trait_decls(modules);
365 let record_names = crate::generator::collect_record_names(modules);
366 let class_fields = crate::generator::collect_class_fields(modules);
367 let const_names = crate::generator::collect_const_names(modules);
368 let public_symbols = crate::generator::collect_public_symbols_for_esm(modules);
369 let mut field_method_collisions = HashSet::new();
374 for (module, _) in modules {
375 field_method_collisions.extend(crate::generator::collect_record_field_names(
376 module,
377 to_camel_case,
378 ));
379 }
380
381 let main_is_async = modules
382 .iter()
383 .any(|(m, _)| crate::generator::module_main_fn_is_async(m));
384 let invocation = self.entry_invocation(main_is_async);
385
386 let mut files: Vec<OutputFile> = Vec::with_capacity(modules.len() + 2);
387 let mut runtime_concurrency = false;
388 let mut runtime_range = false;
389 let mut runtime_eq = false;
390 let mut runtime_compare = false;
391 let mut runtime_str = false;
392
393 for (i, (module, source_path)) in modules.iter().enumerate() {
394 let own_path = crate::generator::module_path_string(module).unwrap_or_default();
395 let mut ctx = EmitCtx::new();
396 ctx.per_module = true;
397 ctx.enum_variants = enum_variants.clone();
398 ctx.trait_decls = trait_decls.clone();
399 ctx.record_names = record_names.clone();
403 ctx.class_fields = class_fields.clone();
406 ctx.const_names = const_names.clone();
407 ctx.field_method_collisions = field_method_collisions.clone();
410 ctx.seed_effect_registries(modules);
413 ctx.self_module_path = if i == entry_idx {
417 String::new()
418 } else {
419 own_path.clone()
420 };
421 ctx.implicit_imports =
422 crate::generator::implicit_esm_imports_for(module, &public_symbols, &own_path);
423 ctx.public_symbols = public_symbols.clone();
424 ctx.export_names = crate::generator::exportable_value_names(module);
425 ctx.emit_node(module)?;
426 runtime_concurrency |= ctx.needs_runtime_concurrency;
427 runtime_range |= ctx.needs_runtime_range;
428 runtime_eq |= ctx.needs_runtime_eq;
429 runtime_compare |= ctx.needs_runtime_compare;
430 runtime_str |= ctx.needs_runtime_str;
431 let (mut content, mappings) = ctx.finish();
432
433 if i == entry_idx && crate::generator::module_declares_main_fn(module) {
435 if let Some(invoc) = invocation.as_ref() {
436 if !content.is_empty() && !content.ends_with('\n') {
437 content.push('\n');
438 }
439 content.push_str(invoc);
440 }
441 }
442
443 let out_path = self.module_output_path(module, source_path, i == entry_idx);
444 let generated_file = out_path
445 .file_name()
446 .and_then(|s| s.to_str())
447 .unwrap_or("")
448 .to_string();
449 files.push(OutputFile {
450 path: out_path,
451 content,
452 source_map: Some(SourceMap {
453 generated_file,
454 mappings,
455 ..Default::default()
456 }),
457 });
458 }
459
460 if runtime_concurrency || runtime_range || runtime_eq || runtime_compare || runtime_str {
463 let mut content = String::new();
464 if runtime_concurrency {
465 content.push_str(&export_runtime_consts(CONCURRENCY_RUNTIME_JS));
466 content.push('\n');
467 }
468 if runtime_range {
469 content.push_str(&export_runtime_consts(RANGE_RUNTIME_JS));
470 content.push('\n');
471 }
472 if runtime_eq {
473 content.push_str(&export_runtime_consts(EQ_RUNTIME_JS));
474 content.push('\n');
475 }
476 if runtime_compare {
477 content.push_str(&export_runtime_consts(COMPARE_RUNTIME_JS));
478 content.push('\n');
479 }
480 if runtime_str {
481 content.push_str(&export_runtime_consts(STR_RUNTIME_JS));
482 content.push('\n');
483 }
484 files.push(OutputFile {
485 path: PathBuf::from(format!("{RUNTIME_MODULE_JS}.js")),
486 content,
487 source_map: Some(SourceMap {
488 generated_file: format!("{RUNTIME_MODULE_JS}.js"),
489 ..Default::default()
490 }),
491 });
492 }
493
494 Ok(GeneratedCode { files })
501 }
502
503 fn generate_tests(
511 &self,
512 modules: &[(&AIRModule, &std::path::Path)],
513 framework: &str,
514 ) -> Result<crate::generator::TestArtifacts, CodegenError> {
515 crate::generator::js_ts_generate_tests(
516 modules,
517 framework,
518 &self.target().conventions.file_extension,
519 "js",
521 |module, source_path, is_entry| self.module_output_path(module, source_path, is_entry),
522 |ctx_modules| {
523 let enum_variants = crate::generator::collect_enum_variants(ctx_modules);
524 let trait_decls = crate::generator::collect_trait_decls(ctx_modules);
525 let record_names = crate::generator::collect_record_names(ctx_modules);
526 let class_fields = crate::generator::collect_class_fields(ctx_modules);
527 let const_names = crate::generator::collect_const_names(ctx_modules);
528 let mut field_method_collisions = HashSet::new();
529 for (module, _) in ctx_modules {
530 field_method_collisions.extend(crate::generator::collect_record_field_names(
531 module,
532 to_camel_case,
533 ));
534 }
535 let mut ctx = EmitCtx::new();
536 ctx.per_module = true;
537 ctx.enum_variants = enum_variants;
538 ctx.trait_decls = trait_decls;
539 ctx.record_names = record_names;
540 ctx.class_fields = class_fields;
541 ctx.const_names = const_names;
542 ctx.field_method_collisions = field_method_collisions;
543 ctx.seed_effect_registries(ctx_modules);
544 Box::new(JsTestEmitter { ctx })
545 },
546 )
547 }
548}
549
550struct JsTestEmitter {
554 ctx: EmitCtx,
555}
556
557impl crate::generator::JsTsExprEmitter for JsTestEmitter {
558 fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
559 self.ctx.expr_to_string(node)
560 }
561}
562
563impl JsGenerator {
564 fn module_output_path(
573 &self,
574 module: &AIRModule,
575 source_path: &std::path::Path,
576 is_entry: bool,
577 ) -> PathBuf {
578 if is_entry {
579 return crate::generator::derive_output_path(source_path, self.target());
580 }
581 match crate::generator::module_path_string(module) {
582 Some(path) if !path.is_empty() => {
583 let rel: PathBuf = path.split('.').collect();
584 rel.with_extension(&self.target().conventions.file_extension)
585 }
586 _ => crate::generator::derive_output_path(source_path, self.target()),
587 }
588 }
589}
590
591fn export_runtime_consts(runtime: &str) -> String {
598 runtime
599 .lines()
600 .map(|line| {
601 if let Some(rest) = line.strip_prefix("const ") {
602 format!("export const {rest}")
603 } else {
604 line.to_string()
605 }
606 })
607 .collect::<Vec<_>>()
608 .join("\n")
609}
610
611struct EmitCtx {
615 buf: String,
616 indent: usize,
617 effect_ops: HashMap<String, String>,
619 current_handler_vars: HashMap<String, String>,
621 fn_effects: HashMap<String, Vec<String>>,
623 composite_effects: HashMap<String, Vec<String>>,
625 record_names: HashSet<String>,
627 class_fields: HashMap<String, Vec<String>>,
636 const_names: HashSet<String>,
642 cur_line: u32,
644 cur_col: u32,
646 scan_pos: usize,
648 last_marked: Option<(u32, u32)>,
651 mappings: Vec<SourceMapping>,
653 loop_labels: Vec<Option<String>>,
659 switch_label_depth: usize,
662 loop_label_counter: usize,
664 match_temp_counter: usize,
670 concurrency_runtime_emitted: bool,
676 range_runtime_emitted: bool,
682 eq_runtime_emitted: bool,
686 compare_runtime_emitted: bool,
690 str_runtime_emitted: bool,
694 enum_variants: crate::generator::EnumVariantRegistry,
701 trait_decls: crate::generator::TraitDeclRegistry,
708 per_module: bool,
718 needs_runtime_concurrency: bool,
724 needs_runtime_range: bool,
727 needs_runtime_eq: bool,
730 needs_runtime_compare: bool,
735 needs_runtime_str: bool,
739 implicit_imports: Vec<crate::generator::ImplicitEsmImport>,
744 public_symbols: HashMap<String, crate::generator::EsmSymbol>,
748 self_module_path: String,
753 export_names: Vec<crate::generator::EsmExport>,
759 field_method_collisions: HashSet<String>,
768 let_scopes: Vec<LetScope>,
777 propagate_temp_counter: usize,
780 propagate_temps: HashMap<usize, String>,
787 discard_tail: bool,
800}
801
802#[derive(Default)]
805struct LetScope {
806 declared: HashSet<String>,
808 needs_let: HashSet<String>,
811}
812
813impl EmitCtx {
814 fn new() -> Self {
815 Self {
816 buf: String::with_capacity(4096),
817 indent: 0,
818 effect_ops: HashMap::new(),
819 current_handler_vars: HashMap::new(),
820 fn_effects: HashMap::new(),
821 composite_effects: HashMap::new(),
822 record_names: HashSet::new(),
823 class_fields: HashMap::new(),
824 const_names: HashSet::new(),
825 cur_line: 1,
826 cur_col: 1,
827 scan_pos: 0,
828 last_marked: None,
829 mappings: Vec::new(),
830 loop_labels: Vec::new(),
831 switch_label_depth: 0,
832 loop_label_counter: 0,
833 match_temp_counter: 0,
834 concurrency_runtime_emitted: false,
835 range_runtime_emitted: false,
836 eq_runtime_emitted: false,
837 compare_runtime_emitted: false,
838 str_runtime_emitted: false,
839 enum_variants: crate::generator::EnumVariantRegistry::new(),
840 trait_decls: crate::generator::TraitDeclRegistry::new(),
841 per_module: false,
842 needs_runtime_concurrency: false,
843 needs_runtime_range: false,
844 needs_runtime_eq: false,
845 needs_runtime_compare: false,
846 needs_runtime_str: false,
847 implicit_imports: Vec::new(),
848 public_symbols: HashMap::new(),
849 self_module_path: String::new(),
850 export_names: Vec::new(),
851 field_method_collisions: HashSet::new(),
852 let_scopes: Vec::new(),
853 propagate_temp_counter: 0,
854 propagate_temps: HashMap::new(),
855 discard_tail: false,
856 }
857 }
858
859 fn js_method_name(&self, rendered: &str) -> String {
874 crate::generator::disambiguate_method_name(
875 rendered.to_string(),
876 &self.field_method_collisions,
877 "Method",
878 )
879 }
880
881 fn user_variant_for_path(
887 &self,
888 path: &bock_ast::TypePath,
889 ) -> Option<&crate::generator::EnumVariantInfo> {
890 let info = crate::generator::registered_variant(&self.enum_variants, path)?;
891 if matches!(info.enum_name.as_str(), "Optional" | "Result") {
892 return None;
893 }
894 Some(info)
895 }
896
897 fn user_variant_for_name(&self, name: &str) -> Option<&crate::generator::EnumVariantInfo> {
899 let info = self.enum_variants.get(name)?;
900 if matches!(info.enum_name.as_str(), "Optional" | "Result") {
901 return None;
902 }
903 Some(info)
904 }
905
906 fn finish(self) -> (String, Vec<SourceMapping>) {
907 (self.buf, self.mappings)
908 }
909
910 fn seed_effect_registries(&mut self, modules: &[(&AIRModule, &std::path::Path)]) {
919 for (module, _) in modules {
920 let NodeKind::Module { items, .. } = &module.kind else {
921 continue;
922 };
923 for item in items {
924 let NodeKind::EffectDecl {
925 name,
926 components,
927 operations,
928 ..
929 } = &item.kind
930 else {
931 continue;
932 };
933 if !components.is_empty() {
934 let comp_names: Vec<String> = components
935 .iter()
936 .map(|tp| {
937 tp.segments
938 .last()
939 .map_or("effect".to_string(), |s| s.name.clone())
940 })
941 .collect();
942 self.composite_effects.insert(name.name.clone(), comp_names);
943 continue;
944 }
945 for op in operations {
946 if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
947 self.effect_ops
948 .insert(op_name.name.clone(), name.name.clone());
949 }
950 }
951 }
952 }
953 }
954
955 fn emit_esm_imports(&mut self, imports: &[AIRNode]) -> Result<(), CodegenError> {
963 let ext = "js";
966
967 if self.needs_runtime_concurrency
969 || self.needs_runtime_range
970 || self.needs_runtime_eq
971 || self.needs_runtime_compare
972 || self.needs_runtime_str
973 {
974 let mut names: Vec<&str> = Vec::new();
975 if self.needs_runtime_concurrency {
976 names.extend(["__bockChannelNew", "__bockSpawn"]);
977 }
978 if self.needs_runtime_range {
979 names.extend(["range", "rangeInclusive"]);
980 }
981 if self.needs_runtime_eq {
982 names.push("__bockEq");
983 }
984 if self.needs_runtime_compare {
985 names.push("__bockCompare");
986 }
987 if self.needs_runtime_str {
988 names.push("__bockStr");
989 }
990 let spec = crate::generator::esm_relative_specifier(
991 &self.self_module_path,
992 RUNTIME_MODULE_JS,
993 ext,
994 );
995 self.writeln(&format!(
996 "import {{ {} }} from \"{spec}\";",
997 names.join(", ")
998 ));
999 }
1000
1001 for import in imports {
1006 if let NodeKind::ImportDecl { path, items } = &import.kind {
1007 let target_path = path
1008 .segments
1009 .iter()
1010 .map(|s| s.name.as_str())
1011 .collect::<Vec<_>>()
1012 .join(".");
1013 if target_path.is_empty() {
1014 continue;
1015 }
1016 let spec = crate::generator::esm_relative_specifier(
1017 &self.self_module_path,
1018 &target_path,
1019 ext,
1020 );
1021 match items {
1022 bock_ast::ImportItems::Named(named) => {
1023 let rendered: Vec<String> = named
1024 .iter()
1025 .filter(|n| {
1031 if crate::generator::ESM_RUNTIME_PRELUDE_NAMES
1032 .contains(&n.name.name.as_str())
1033 {
1034 return false;
1035 }
1036 match self.public_symbols.get(&n.name.name) {
1037 Some(s) => s.kind.is_js_value(),
1038 None => true,
1041 }
1042 })
1043 .map(|n| {
1044 let is_fn = self
1045 .public_symbols
1046 .get(&n.name.name)
1047 .map(|s| s.is_fn())
1048 .unwrap_or(false);
1049 let src = self.esm_emit_name(&n.name.name, is_fn);
1050 match &n.alias {
1051 Some(alias) => {
1052 let dst = self.esm_emit_name(&alias.name, is_fn);
1053 if dst == src {
1054 src
1055 } else {
1056 format!("{src} as {dst}")
1057 }
1058 }
1059 None => src,
1060 }
1061 })
1062 .collect();
1063 if !rendered.is_empty() {
1064 self.writeln(&format!(
1065 "import {{ {} }} from \"{spec}\";",
1066 rendered.join(", ")
1067 ));
1068 }
1069 }
1070 bock_ast::ImportItems::Module | bock_ast::ImportItems::Glob => {}
1076 }
1077 }
1078 }
1079
1080 let mut by_module: std::collections::BTreeMap<String, Vec<String>> =
1085 std::collections::BTreeMap::new();
1086 for imp in &self.implicit_imports {
1087 if !imp.kind.is_js_value() {
1088 continue;
1089 }
1090 by_module
1091 .entry(imp.module_path.clone())
1092 .or_default()
1093 .push(esm_emit_name_static(&imp.name, imp.is_fn()));
1094 }
1095 for (module_path, mut names) in by_module {
1096 names.sort_unstable();
1097 names.dedup();
1098 let spec =
1099 crate::generator::esm_relative_specifier(&self.self_module_path, &module_path, ext);
1100 self.writeln(&format!(
1101 "import {{ {} }} from \"{spec}\";",
1102 names.join(", ")
1103 ));
1104 }
1105 Ok(())
1106 }
1107
1108 fn esm_emit_name(&self, name: &str, is_fn: bool) -> String {
1113 esm_emit_name_static(name, is_fn)
1114 }
1115
1116 fn emit_trailing_exports(&mut self) {
1122 let mut names: Vec<String> = self
1123 .export_names
1124 .iter()
1125 .filter(|e| !e.is_fn)
1126 .map(|e| esm_emit_name_static(&e.name, e.is_fn))
1127 .collect();
1128 if names.is_empty() {
1129 return;
1130 }
1131 names.sort_unstable();
1132 names.dedup();
1133 if !self.buf.is_empty() && !self.buf.ends_with('\n') {
1134 self.buf.push('\n');
1135 }
1136 self.writeln(&format!("export {{ {} }};", names.join(", ")));
1137 }
1138
1139 fn sync_pos(&mut self) {
1142 if self.scan_pos >= self.buf.len() {
1143 return;
1144 }
1145 let slice = &self.buf[self.scan_pos..];
1146 for ch in slice.chars() {
1147 if ch == '\n' {
1148 self.cur_line += 1;
1149 self.cur_col = 1;
1150 } else {
1151 self.cur_col += 1;
1152 }
1153 }
1154 self.scan_pos = self.buf.len();
1155 }
1156
1157 fn mark_span(&mut self, span: Span) {
1160 if span.start == 0 && span.end == 0 {
1161 return;
1162 }
1163 self.sync_pos();
1164 let key = (self.cur_line, self.cur_col);
1165 if self.last_marked == Some(key) {
1166 return;
1167 }
1168 self.last_marked = Some(key);
1169 self.mappings.push(SourceMapping {
1170 gen_line: self.cur_line,
1171 gen_col: self.cur_col,
1172 src_line: 0,
1173 src_col: 0,
1174 src_offset: span.start as u32,
1175 src_file_id: span.file.0,
1176 });
1177 }
1178
1179 fn indent_str(&self) -> String {
1180 " ".repeat(self.indent)
1181 }
1182
1183 fn write_indent(&mut self) {
1184 let indent = self.indent_str();
1185 self.buf.push_str(&indent);
1186 }
1187
1188 fn writeln(&mut self, s: &str) {
1189 self.write_indent();
1190 self.buf.push_str(s);
1191 self.buf.push('\n');
1192 }
1193
1194 fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
1198 let start = self.buf.len();
1199 let saved_line = self.cur_line;
1203 let saved_col = self.cur_col;
1204 let saved_scan = self.scan_pos;
1205 let saved_marked = self.last_marked;
1206 let mappings_len = self.mappings.len();
1207 self.emit_expr(node)?;
1208 let s = self.buf[start..].to_string();
1209 self.buf.truncate(start);
1210 self.cur_line = saved_line;
1211 self.cur_col = saved_col;
1212 self.scan_pos = saved_scan;
1213 self.last_marked = saved_marked;
1214 self.mappings.truncate(mappings_len);
1215 Ok(s)
1216 }
1217
1218 fn map_prelude_call(
1221 &mut self,
1222 callee: &AIRNode,
1223 args: &[bock_air::AirArg],
1224 ) -> Result<Option<String>, CodegenError> {
1225 let name = match &callee.kind {
1226 NodeKind::Identifier { name } => name.name.as_str(),
1227 _ => return Ok(None),
1228 };
1229 let arg_strs: Vec<String> = args
1230 .iter()
1231 .map(|a| self.expr_to_string(&a.value))
1232 .collect::<Result<_, _>>()?;
1233 let code = match name {
1234 "println" => {
1235 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1236 format!("console.log({a})")
1237 }
1238 "print" => {
1239 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1240 format!("process.stdout.write(String({a}))")
1241 }
1242 "debug" => {
1243 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1244 format!("console.debug({a})")
1245 }
1246 "assert" => {
1247 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1248 format!("if (!{a}) throw new Error(\"assertion failed\")")
1249 }
1250 "todo" => "throw new Error(\"not implemented\")".to_string(),
1251 "unreachable" => "throw new Error(\"unreachable\")".to_string(),
1252 "sleep" => {
1253 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1254 if let Some(handler) = self.clock_handler_var() {
1257 format!("{handler}.sleep({a})")
1258 } else {
1259 format!("new Promise((__r) => setTimeout(__r, Math.floor(({a}) / 1e6)))")
1261 }
1262 }
1263 _ => return Ok(None),
1264 };
1265 Ok(Some(code))
1266 }
1267
1268 fn module_uses_concurrency(&self, items: &[AIRNode]) -> bool {
1273 items.iter().any(Self::node_uses_concurrency)
1274 }
1275
1276 fn node_uses_concurrency(node: &AIRNode) -> bool {
1277 let serialized = format!("{node:?}");
1278 serialized.contains("\"Channel\"") || serialized.contains("\"spawn\"")
1279 }
1280
1281 fn try_emit_concurrency_call(
1285 &mut self,
1286 callee: &AIRNode,
1287 args: &[bock_air::AirArg],
1288 ) -> Result<bool, CodegenError> {
1289 if let NodeKind::Identifier { name } = &callee.kind {
1291 if name.name == "spawn" {
1292 self.buf.push_str("__bockSpawn(");
1293 for (i, arg) in args.iter().enumerate() {
1294 if i > 0 {
1295 self.buf.push_str(", ");
1296 }
1297 self.emit_expr(&arg.value)?;
1298 }
1299 self.buf.push(')');
1300 return Ok(true);
1301 }
1302 }
1303 let NodeKind::FieldAccess { object, field } = &callee.kind else {
1304 return Ok(false);
1305 };
1306 if let NodeKind::Identifier { name: type_name } = &object.kind {
1308 if type_name.name == "Channel" && field.name == "new" {
1309 self.buf.push_str("__bockChannelNew()");
1310 return Ok(true);
1311 }
1312 }
1313 if matches!(field.name.as_str(), "send" | "recv" | "close") {
1317 self.emit_expr(object)?;
1318 let _ = write!(self.buf, ".{}", field.name);
1319 self.buf.push('(');
1320 for (i, arg) in args.iter().skip(1).enumerate() {
1321 if i > 0 {
1322 self.buf.push_str(", ");
1323 }
1324 self.emit_expr(&arg.value)?;
1325 }
1326 self.buf.push(')');
1327 return Ok(true);
1328 }
1329 Ok(false)
1330 }
1331
1332 fn try_emit_time_assoc_call(
1338 &mut self,
1339 callee: &AIRNode,
1340 args: &[bock_air::AirArg],
1341 ) -> Result<bool, CodegenError> {
1342 let NodeKind::FieldAccess { object, field } = &callee.kind else {
1343 return Ok(false);
1344 };
1345 let NodeKind::Identifier { name: type_name } = &object.kind else {
1346 return Ok(false);
1347 };
1348 let arg_strs: Vec<String> = args
1349 .iter()
1350 .map(|a| self.expr_to_string(&a.value))
1351 .collect::<Result<_, _>>()?;
1352 let arg0 = || arg_strs.first().cloned().unwrap_or_default();
1353 let code = match (type_name.name.as_str(), field.name.as_str()) {
1354 ("Duration", "zero") => "0".to_string(),
1355 ("Duration", "nanos") => arg0(),
1356 ("Duration", "micros") => format!("(({}) * 1000)", arg0()),
1357 ("Duration", "millis") => format!("(({}) * 1000000)", arg0()),
1358 ("Duration", "seconds") => format!("(({}) * 1000000000)", arg0()),
1359 ("Duration", "minutes") => format!("(({}) * 60000000000)", arg0()),
1360 ("Duration", "hours") => format!("(({}) * 3600000000000)", arg0()),
1361 ("Instant", "now") => {
1362 if let Some(handler) = self.clock_handler_var() {
1365 format!("{handler}.now_monotonic()")
1366 } else {
1367 "(performance.now() * 1000000)".to_string()
1368 }
1369 }
1370 _ => return Ok(false),
1371 };
1372 self.buf.push_str(&code);
1373 Ok(true)
1374 }
1375
1376 fn try_emit_time_desugared_method(
1386 &mut self,
1387 node: &AIRNode,
1388 callee: &AIRNode,
1389 args: &[bock_air::AirArg],
1390 ) -> Result<bool, CodegenError> {
1391 if crate::generator::primitive_recv_kind(node).is_some() {
1392 return Ok(false);
1393 }
1394 let NodeKind::FieldAccess { object, field } = &callee.kind else {
1395 return Ok(false);
1396 };
1397 if let NodeKind::Identifier { name } = &object.kind {
1399 if matches!(name.name.as_str(), "Duration" | "Instant") {
1400 return Ok(false);
1401 }
1402 }
1403 if !is_time_method_name(&field.name) {
1404 return Ok(false);
1405 }
1406 let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
1407 self.try_emit_time_method(object, &field.name, &remaining)
1408 }
1409
1410 fn try_emit_time_method(
1413 &mut self,
1414 receiver: &AIRNode,
1415 method: &str,
1416 args: &[bock_air::AirArg],
1417 ) -> Result<bool, CodegenError> {
1418 let recv_str = self.expr_to_string(receiver)?;
1419 let arg_strs: Vec<String> = args
1420 .iter()
1421 .map(|a| self.expr_to_string(&a.value))
1422 .collect::<Result<_, _>>()?;
1423 let code = match method {
1424 "as_nanos" => format!("({recv_str})"),
1425 "as_millis" => format!("Math.floor(({recv_str}) / 1000000)"),
1426 "as_seconds" => format!("Math.floor(({recv_str}) / 1000000000)"),
1427 "is_zero" => format!("(({recv_str}) === 0)"),
1428 "is_negative" => format!("(({recv_str}) < 0)"),
1429 "abs" => format!("Math.abs({recv_str})"),
1430 "elapsed" => {
1431 if let Some(handler) = self.clock_handler_var() {
1435 format!("({handler}.now_monotonic() - ({recv_str}))")
1436 } else {
1437 format!("((performance.now() * 1000000) - ({recv_str}))")
1438 }
1439 }
1440 "duration_since" => {
1441 let other = arg_strs.first().cloned().unwrap_or_default();
1442 format!("(({recv_str}) - ({other}))")
1443 }
1444 _ => return Ok(false),
1445 };
1446 self.buf.push_str(&code);
1447 Ok(true)
1448 }
1449
1450 fn try_emit_prelude_ctor(
1454 &mut self,
1455 callee: &AIRNode,
1456 args: &[bock_air::AirArg],
1457 ) -> Result<bool, CodegenError> {
1458 let name = match &callee.kind {
1459 NodeKind::Identifier { name } => name.name.as_str(),
1460 _ => return Ok(false),
1461 };
1462 if !matches!(name, "Some" | "Ok" | "Err") {
1463 return Ok(false);
1464 }
1465 let _ = write!(self.buf, "{{ _tag: \"{name}\"");
1466 if let Some(arg) = args.first() {
1467 self.buf.push_str(", _0: ");
1468 self.emit_expr(&arg.value)?;
1469 }
1470 self.buf.push_str(" }");
1471 Ok(true)
1472 }
1473
1474 fn try_emit_primitive_conversion(
1482 &mut self,
1483 node: &AIRNode,
1484 callee: &AIRNode,
1485 args: &[bock_air::AirArg],
1486 ) -> Result<bool, CodegenError> {
1487 let Some((target, method, arg)) =
1488 crate::generator::primitive_conversion_call(node, callee, args)
1489 else {
1490 return Ok(false);
1491 };
1492 let arg_str = self.expr_to_string(arg)?;
1493 let code = match (target, method) {
1494 ("Float" | "Int" | "String", "from") => format!("({arg_str})"),
1498 ("Int", "try_from") => format!(
1501 "((__s) => /^[+-]?[0-9]+$/.test(__s.trim()) \
1502 ? {{ _tag: \"Ok\", _0: Number.parseInt(__s.trim(), 10) }} \
1503 : {{ _tag: \"Err\", _0: new ConvertError({{ message: \
1504 `cannot parse '${{__s}}' as Int` }}) }})({arg_str})"
1505 ),
1506 ("Float", "try_from") => format!(
1509 "((__s) => {{ const __t = __s.trim(); const __n = Number(__t); \
1510 return (__t.length > 0 && !Number.isNaN(__n)) \
1511 ? {{ _tag: \"Ok\", _0: __n }} \
1512 : {{ _tag: \"Err\", _0: new ConvertError({{ message: \
1513 `cannot parse '${{__s}}' as Float` }}) }}; }})({arg_str})"
1514 ),
1515 _ => return Ok(false),
1516 };
1517 self.buf.push_str(&code);
1518 Ok(true)
1519 }
1520
1521 fn try_emit_container_method(
1533 &mut self,
1534 node: &AIRNode,
1535 callee: &AIRNode,
1536 args: &[bock_air::AirArg],
1537 ) -> Result<bool, CodegenError> {
1538 if let Some((recv, method, rest)) =
1539 crate::generator::desugared_optional_method(node, callee, args)
1540 {
1541 self.emit_tagged_container_method(recv, method, rest, "Some")?;
1542 return Ok(true);
1543 }
1544 if let Some((recv, method, rest)) =
1545 crate::generator::desugared_result_method(node, callee, args)
1546 {
1547 self.emit_tagged_container_method(recv, method, rest, "Ok")?;
1548 return Ok(true);
1549 }
1550 Ok(false)
1551 }
1552
1553 fn emit_tagged_container_method(
1558 &mut self,
1559 recv: &AIRNode,
1560 method: &str,
1561 rest: &[bock_air::AirArg],
1562 present_tag: &str,
1563 ) -> Result<(), CodegenError> {
1564 match method {
1567 "is_some" | "is_ok" => {
1568 self.buf.push('(');
1569 self.emit_expr(recv)?;
1570 let _ = write!(self.buf, "._tag === \"{present_tag}\")");
1571 return Ok(());
1572 }
1573 "is_none" | "is_err" => {
1574 self.buf.push('(');
1575 self.emit_expr(recv)?;
1576 let _ = write!(self.buf, "._tag !== \"{present_tag}\")");
1577 return Ok(());
1578 }
1579 _ => {}
1580 }
1581 self.buf.push_str("((__c) => ");
1584 match method {
1585 "unwrap" => {
1586 let _ = write!(
1587 self.buf,
1588 "__c._tag === \"{present_tag}\" ? __c._0 : undefined"
1589 );
1590 }
1591 "unwrap_or" => {
1592 let _ = write!(self.buf, "__c._tag === \"{present_tag}\" ? __c._0 : ");
1593 if let Some(d) = rest.first() {
1594 self.emit_expr(&d.value)?;
1595 } else {
1596 self.buf.push_str("undefined");
1597 }
1598 }
1599 "map" => {
1600 let _ = write!(
1601 self.buf,
1602 "__c._tag === \"{present_tag}\" ? {{ _tag: \"{present_tag}\", _0: ("
1603 );
1604 if let Some(f) = rest.first() {
1605 self.emit_expr(&f.value)?;
1606 } else {
1607 self.buf.push_str("(x) => x");
1608 }
1609 self.buf.push_str(")(__c._0) } : __c");
1610 }
1611 "flat_map" => {
1612 let _ = write!(self.buf, "__c._tag === \"{present_tag}\" ? (");
1613 if let Some(f) = rest.first() {
1614 self.emit_expr(&f.value)?;
1615 } else {
1616 self.buf.push_str("(x) => x");
1617 }
1618 self.buf.push_str(")(__c._0) : __c");
1619 }
1620 "map_err" => {
1621 let _ = write!(
1623 self.buf,
1624 "__c._tag === \"{present_tag}\" ? __c : {{ _tag: \"Err\", _0: ("
1625 );
1626 if let Some(f) = rest.first() {
1627 self.emit_expr(&f.value)?;
1628 } else {
1629 self.buf.push_str("(x) => x");
1630 }
1631 self.buf.push_str(")(__c._0) }");
1632 }
1633 _ => {
1634 self.buf.push_str("undefined");
1636 }
1637 }
1638 self.buf.push_str(")(");
1639 self.emit_expr(recv)?;
1640 self.buf.push(')');
1641 Ok(())
1642 }
1643
1644 fn try_emit_list_method(
1653 &mut self,
1654 node: &AIRNode,
1655 callee: &AIRNode,
1656 args: &[bock_air::AirArg],
1657 ) -> Result<bool, CodegenError> {
1658 let Some((recv, method, rest)) =
1659 crate::generator::desugared_list_method(node, callee, args)
1660 else {
1661 return Ok(false);
1662 };
1663 match method {
1664 "len" | "length" | "count" => {
1665 self.buf.push('(');
1666 self.emit_expr(recv)?;
1667 self.buf.push_str(").length");
1668 }
1669 "is_empty" => {
1670 self.buf.push_str("((");
1671 self.emit_expr(recv)?;
1672 self.buf.push_str(").length === 0)");
1673 }
1674 "get" => {
1675 let Some(idx) = rest.first() else {
1676 return Ok(false);
1677 };
1678 self.buf
1679 .push_str("((__r, __i) => (__i >= 0 && __i < __r.length) ? ");
1680 self.buf
1681 .push_str("{ _tag: \"Some\", _0: __r[__i] } : { _tag: \"None\" })(");
1682 self.emit_expr(recv)?;
1683 self.buf.push_str(", ");
1684 self.emit_expr(&idx.value)?;
1685 self.buf.push(')');
1686 }
1687 "first" => {
1688 self.buf.push_str("((__r) => __r.length > 0 ? ");
1689 self.buf
1690 .push_str("{ _tag: \"Some\", _0: __r[0] } : { _tag: \"None\" })(");
1691 self.emit_expr(recv)?;
1692 self.buf.push(')');
1693 }
1694 "last" => {
1695 self.buf.push_str("((__r) => __r.length > 0 ? ");
1696 self.buf
1697 .push_str("{ _tag: \"Some\", _0: __r[__r.length - 1] } : { _tag: \"None\" })(");
1698 self.emit_expr(recv)?;
1699 self.buf.push(')');
1700 }
1701 "contains" => {
1702 let Some(x) = rest.first() else {
1703 return Ok(false);
1704 };
1705 self.buf.push('(');
1706 self.emit_expr(recv)?;
1707 self.buf.push_str(").includes(");
1708 self.emit_expr(&x.value)?;
1709 self.buf.push(')');
1710 }
1711 "index_of" => {
1712 let Some(x) = rest.first() else {
1713 return Ok(false);
1714 };
1715 self.buf
1716 .push_str("((__r, __x) => { const __i = __r.indexOf(__x); ");
1717 self.buf.push_str(
1718 "return __i >= 0 ? { _tag: \"Some\", _0: __i } : { _tag: \"None\" }; })(",
1719 );
1720 self.emit_expr(recv)?;
1721 self.buf.push_str(", ");
1722 self.emit_expr(&x.value)?;
1723 self.buf.push(')');
1724 }
1725 "concat" => {
1726 let Some(o) = rest.first() else {
1727 return Ok(false);
1728 };
1729 self.buf.push('(');
1730 self.emit_expr(recv)?;
1731 self.buf.push_str(").concat(");
1732 self.emit_expr(&o.value)?;
1733 self.buf.push(')');
1734 }
1735 "join" => {
1736 let Some(sep) = rest.first() else {
1737 return Ok(false);
1738 };
1739 self.buf.push('(');
1740 self.emit_expr(recv)?;
1741 self.buf.push_str(").join(");
1742 self.emit_expr(&sep.value)?;
1743 self.buf.push(')');
1744 }
1745 _ => return Ok(false),
1746 }
1747 Ok(true)
1748 }
1749
1750 fn try_emit_list_mutating_method(
1758 &mut self,
1759 node: &AIRNode,
1760 callee: &AIRNode,
1761 args: &[bock_air::AirArg],
1762 ) -> Result<bool, CodegenError> {
1763 let Some((recv, _method, rest)) =
1764 crate::generator::desugared_list_mutating_method(node, callee, args)
1765 else {
1766 return Ok(false);
1767 };
1768 let Some(x) = rest.first() else {
1769 return Ok(false);
1770 };
1771 self.buf.push('(');
1772 self.emit_expr(recv)?;
1773 self.buf.push_str(").push(");
1774 self.emit_expr(&x.value)?;
1775 self.buf.push(')');
1776 Ok(true)
1777 }
1778
1779 fn try_emit_list_inplace_mutator(
1800 &mut self,
1801 node: &AIRNode,
1802 callee: &AIRNode,
1803 args: &[bock_air::AirArg],
1804 ) -> Result<bool, CodegenError> {
1805 let Some((recv, method, rest)) =
1806 crate::generator::desugared_list_inplace_mutator(node, callee, args)
1807 else {
1808 return Ok(false);
1809 };
1810 match method {
1811 "pop" => {
1812 self.buf.push_str(
1813 "((__r) => __r.length > 0 ? { _tag: \"Some\", _0: __r.pop() } : \
1814 { _tag: \"None\" })(",
1815 );
1816 self.emit_expr(recv)?;
1817 self.buf.push(')');
1818 }
1819 "remove_at" => {
1820 let Some(idx) = rest.first() else {
1821 return Ok(false);
1822 };
1823 self.buf.push_str(
1824 "((__r, __i) => { if (__i < 0 || __i >= __r.length) { throw new Error(\
1825 \"List.remove_at: index \" + __i + \" out of bounds (len \" + __r.length + \")\"); } \
1826 return __r.splice(__i, 1)[0]; })(",
1827 );
1828 self.emit_expr(recv)?;
1829 self.buf.push_str(", ");
1830 self.emit_expr(&idx.value)?;
1831 self.buf.push(')');
1832 }
1833 "insert" => {
1834 let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
1835 return Ok(false);
1836 };
1837 self.buf.push_str(
1838 "((__r, __i, __x) => { if (__i < 0 || __i > __r.length) { throw new Error(\
1839 \"List.insert: index \" + __i + \" out of bounds (len \" + __r.length + \")\"); } \
1840 __r.splice(__i, 0, __x); })(",
1841 );
1842 self.emit_expr(recv)?;
1843 self.buf.push_str(", ");
1844 self.emit_expr(&idx.value)?;
1845 self.buf.push_str(", ");
1846 self.emit_expr(&x.value)?;
1847 self.buf.push(')');
1848 }
1849 "reverse" => {
1850 self.buf.push('(');
1851 self.emit_expr(recv)?;
1852 self.buf.push_str(").reverse()");
1853 }
1854 "set" => {
1855 let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
1856 return Ok(false);
1857 };
1858 self.buf.push_str(
1859 "((__r, __i, __x) => { if (__i < 0 || __i >= __r.length) { throw new Error(\
1860 \"List.set: index \" + __i + \" out of bounds (len \" + __r.length + \")\"); } \
1861 __r[__i] = __x; })(",
1862 );
1863 self.emit_expr(recv)?;
1864 self.buf.push_str(", ");
1865 self.emit_expr(&idx.value)?;
1866 self.buf.push_str(", ");
1867 self.emit_expr(&x.value)?;
1868 self.buf.push(')');
1869 }
1870 _ => return Ok(false),
1871 }
1872 Ok(true)
1873 }
1874
1875 fn try_emit_list_functional_method(
1887 &mut self,
1888 node: &AIRNode,
1889 callee: &AIRNode,
1890 args: &[bock_air::AirArg],
1891 ) -> Result<bool, CodegenError> {
1892 let Some((recv, method, rest)) =
1893 crate::generator::desugared_list_functional_method(node, callee, args)
1894 else {
1895 return Ok(false);
1896 };
1897 match method {
1898 "map" | "filter" | "for_each" | "any" | "all" | "flat_map" => {
1899 let Some(cb) = rest.first() else {
1900 return Ok(false);
1901 };
1902 let native = match method {
1903 "map" => "map",
1904 "filter" => "filter",
1905 "for_each" => "forEach",
1906 "any" => "some",
1907 "all" => "every",
1908 "flat_map" => "flatMap",
1909 _ => unreachable!(),
1910 };
1911 self.buf.push('(');
1912 self.emit_expr(recv)?;
1913 let _ = write!(self.buf, ").{native}(");
1914 self.emit_expr(&cb.value)?;
1915 self.buf.push(')');
1916 }
1917 "reduce" => {
1918 let Some(cb) = rest.first() else {
1921 return Ok(false);
1922 };
1923 self.buf.push('(');
1924 self.emit_expr(recv)?;
1925 self.buf.push_str(").reduce(");
1926 self.emit_expr(&cb.value)?;
1927 self.buf.push(')');
1928 }
1929 "fold" => {
1930 let (Some(init), Some(cb)) = (rest.first(), rest.get(1)) else {
1932 return Ok(false);
1933 };
1934 self.buf.push('(');
1935 self.emit_expr(recv)?;
1936 self.buf.push_str(").reduce(");
1937 self.emit_expr(&cb.value)?;
1938 self.buf.push_str(", ");
1939 self.emit_expr(&init.value)?;
1940 self.buf.push(')');
1941 }
1942 "find" => {
1943 let Some(cb) = rest.first() else {
1946 return Ok(false);
1947 };
1948 self.buf.push_str("((__r) => { const __m = __r.find(");
1949 self.emit_expr(&cb.value)?;
1950 self.buf.push_str(
1951 "); return __m === undefined ? { _tag: \"None\" } : { _tag: \"Some\", _0: __m }; })(",
1952 );
1953 self.emit_expr(recv)?;
1954 self.buf.push(')');
1955 }
1956 _ => return Ok(false),
1957 }
1958 Ok(true)
1959 }
1960
1961 fn try_emit_map_method(
1973 &mut self,
1974 node: &AIRNode,
1975 callee: &AIRNode,
1976 args: &[bock_air::AirArg],
1977 ) -> Result<bool, CodegenError> {
1978 let Some((recv, method, rest)) = crate::generator::desugared_map_method(node, callee, args)
1979 else {
1980 return Ok(false);
1981 };
1982 match method {
1983 "len" | "length" | "count" => {
1984 self.buf.push('(');
1985 self.emit_expr(recv)?;
1986 self.buf.push_str(").size");
1987 }
1988 "is_empty" => {
1989 self.buf.push_str("((");
1990 self.emit_expr(recv)?;
1991 self.buf.push_str(").size === 0)");
1992 }
1993 "contains_key" => {
1994 let Some(k) = rest.first() else {
1995 return Ok(false);
1996 };
1997 self.buf.push('(');
1998 self.emit_expr(recv)?;
1999 self.buf.push_str(").has(");
2000 self.emit_expr(&k.value)?;
2001 self.buf.push(')');
2002 }
2003 "get" => {
2004 let Some(k) = rest.first() else {
2005 return Ok(false);
2006 };
2007 self.buf.push_str(
2008 "((__m, __k) => __m.has(__k) ? { _tag: \"Some\", _0: __m.get(__k) } : \
2009 { _tag: \"None\" })(",
2010 );
2011 self.emit_expr(recv)?;
2012 self.buf.push_str(", ");
2013 self.emit_expr(&k.value)?;
2014 self.buf.push(')');
2015 }
2016 "set" => {
2017 let (Some(k), Some(v)) = (rest.first(), rest.get(1)) else {
2018 return Ok(false);
2019 };
2020 self.buf
2021 .push_str("((__m, __k, __v) => { __m.set(__k, __v); return __m; })(");
2022 self.emit_expr(recv)?;
2023 self.buf.push_str(", ");
2024 self.emit_expr(&k.value)?;
2025 self.buf.push_str(", ");
2026 self.emit_expr(&v.value)?;
2027 self.buf.push(')');
2028 }
2029 "delete" => {
2030 let Some(k) = rest.first() else {
2031 return Ok(false);
2032 };
2033 self.buf
2034 .push_str("((__m, __k) => { __m.delete(__k); return __m; })(");
2035 self.emit_expr(recv)?;
2036 self.buf.push_str(", ");
2037 self.emit_expr(&k.value)?;
2038 self.buf.push(')');
2039 }
2040 "merge" => {
2041 let Some(o) = rest.first() else {
2042 return Ok(false);
2043 };
2044 self.buf.push_str(
2045 "((__m, __o) => { for (const [__k, __v] of __o) __m.set(__k, __v); \
2046 return __m; })(",
2047 );
2048 self.emit_expr(recv)?;
2049 self.buf.push_str(", ");
2050 self.emit_expr(&o.value)?;
2051 self.buf.push(')');
2052 }
2053 "filter" => {
2054 let Some(f) = rest.first() else {
2055 return Ok(false);
2056 };
2057 self.buf.push_str(
2058 "((__m, __f) => { const __r = new Map(); \
2059 for (const [__k, __v] of __m) if (__f(__k, __v)) __r.set(__k, __v); \
2060 return __r; })(",
2061 );
2062 self.emit_expr(recv)?;
2063 self.buf.push_str(", ");
2064 self.emit_expr(&f.value)?;
2065 self.buf.push(')');
2066 }
2067 "keys" => {
2068 self.buf.push_str("[...(");
2069 self.emit_expr(recv)?;
2070 self.buf.push_str(").keys()]");
2071 }
2072 "values" => {
2073 self.buf.push_str("[...(");
2074 self.emit_expr(recv)?;
2075 self.buf.push_str(").values()]");
2076 }
2077 "entries" | "to_list" => {
2078 self.buf.push_str("[...(");
2079 self.emit_expr(recv)?;
2080 self.buf.push_str(").entries()]");
2081 }
2082 "for_each" => {
2083 let Some(f) = rest.first() else {
2084 return Ok(false);
2085 };
2086 self.buf
2087 .push_str("((__m, __f) => { for (const [__k, __v] of __m) __f(__k, __v); })(");
2088 self.emit_expr(recv)?;
2089 self.buf.push_str(", ");
2090 self.emit_expr(&f.value)?;
2091 self.buf.push(')');
2092 }
2093 _ => return Ok(false),
2094 }
2095 Ok(true)
2096 }
2097
2098 fn try_emit_set_method(
2106 &mut self,
2107 node: &AIRNode,
2108 callee: &AIRNode,
2109 args: &[bock_air::AirArg],
2110 ) -> Result<bool, CodegenError> {
2111 let Some((recv, method, rest)) = crate::generator::desugared_set_method(node, callee, args)
2112 else {
2113 return Ok(false);
2114 };
2115 match method {
2116 "len" | "length" | "count" => {
2117 self.buf.push('(');
2118 self.emit_expr(recv)?;
2119 self.buf.push_str(").size");
2120 }
2121 "is_empty" => {
2122 self.buf.push_str("((");
2123 self.emit_expr(recv)?;
2124 self.buf.push_str(").size === 0)");
2125 }
2126 "contains" => {
2127 let Some(x) = rest.first() else {
2128 return Ok(false);
2129 };
2130 self.buf.push('(');
2131 self.emit_expr(recv)?;
2132 self.buf.push_str(").has(");
2133 self.emit_expr(&x.value)?;
2134 self.buf.push(')');
2135 }
2136 "add" => {
2137 let Some(x) = rest.first() else {
2138 return Ok(false);
2139 };
2140 self.buf
2141 .push_str("((__s, __x) => { __s.add(__x); return __s; })(");
2142 self.emit_expr(recv)?;
2143 self.buf.push_str(", ");
2144 self.emit_expr(&x.value)?;
2145 self.buf.push(')');
2146 }
2147 "remove" => {
2148 let Some(x) = rest.first() else {
2149 return Ok(false);
2150 };
2151 self.buf
2152 .push_str("((__s, __x) => { __s.delete(__x); return __s; })(");
2153 self.emit_expr(recv)?;
2154 self.buf.push_str(", ");
2155 self.emit_expr(&x.value)?;
2156 self.buf.push(')');
2157 }
2158 "union" => {
2159 let Some(o) = rest.first() else {
2160 return Ok(false);
2161 };
2162 self.buf
2163 .push_str("((__a, __b) => new Set([...__a, ...__b]))(");
2164 self.emit_expr(recv)?;
2165 self.buf.push_str(", ");
2166 self.emit_expr(&o.value)?;
2167 self.buf.push(')');
2168 }
2169 "intersection" => {
2170 let Some(o) = rest.first() else {
2171 return Ok(false);
2172 };
2173 self.buf
2174 .push_str("((__a, __b) => new Set([...__a].filter((__x) => __b.has(__x))))(");
2175 self.emit_expr(recv)?;
2176 self.buf.push_str(", ");
2177 self.emit_expr(&o.value)?;
2178 self.buf.push(')');
2179 }
2180 "difference" => {
2181 let Some(o) = rest.first() else {
2182 return Ok(false);
2183 };
2184 self.buf
2185 .push_str("((__a, __b) => new Set([...__a].filter((__x) => !__b.has(__x))))(");
2186 self.emit_expr(recv)?;
2187 self.buf.push_str(", ");
2188 self.emit_expr(&o.value)?;
2189 self.buf.push(')');
2190 }
2191 "is_subset" => {
2192 let Some(o) = rest.first() else {
2193 return Ok(false);
2194 };
2195 self.buf
2196 .push_str("((__a, __b) => [...__a].every((__x) => __b.has(__x)))(");
2197 self.emit_expr(recv)?;
2198 self.buf.push_str(", ");
2199 self.emit_expr(&o.value)?;
2200 self.buf.push(')');
2201 }
2202 "is_superset" => {
2203 let Some(o) = rest.first() else {
2204 return Ok(false);
2205 };
2206 self.buf
2207 .push_str("((__a, __b) => [...__b].every((__x) => __a.has(__x)))(");
2208 self.emit_expr(recv)?;
2209 self.buf.push_str(", ");
2210 self.emit_expr(&o.value)?;
2211 self.buf.push(')');
2212 }
2213 "filter" => {
2214 let Some(f) = rest.first() else {
2215 return Ok(false);
2216 };
2217 self.buf
2218 .push_str("((__s, __f) => new Set([...__s].filter(__f)))(");
2219 self.emit_expr(recv)?;
2220 self.buf.push_str(", ");
2221 self.emit_expr(&f.value)?;
2222 self.buf.push(')');
2223 }
2224 "map" => {
2225 let Some(f) = rest.first() else {
2226 return Ok(false);
2227 };
2228 self.buf
2229 .push_str("((__s, __f) => new Set([...__s].map(__f)))(");
2230 self.emit_expr(recv)?;
2231 self.buf.push_str(", ");
2232 self.emit_expr(&f.value)?;
2233 self.buf.push(')');
2234 }
2235 "to_list" => {
2236 self.buf.push_str("[...(");
2237 self.emit_expr(recv)?;
2238 self.buf.push_str(")]");
2239 }
2240 "for_each" => {
2241 let Some(f) = rest.first() else {
2242 return Ok(false);
2243 };
2244 self.buf
2245 .push_str("((__s, __f) => { for (const __x of __s) __f(__x); })(");
2246 self.emit_expr(recv)?;
2247 self.buf.push_str(", ");
2248 self.emit_expr(&f.value)?;
2249 self.buf.push(')');
2250 }
2251 _ => return Ok(false),
2252 }
2253 Ok(true)
2254 }
2255
2256 fn try_emit_string_method(
2284 &mut self,
2285 node: &AIRNode,
2286 callee: &AIRNode,
2287 args: &[bock_air::AirArg],
2288 ) -> Result<bool, CodegenError> {
2289 if crate::generator::primitive_recv_kind(node) != Some("String") {
2290 return Ok(false);
2291 }
2292 let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
2293 return Ok(false);
2294 };
2295 let method = field.name.as_str();
2296 let recv_str = self.expr_to_string(recv)?;
2297 let arg0 = |this: &mut Self| -> Result<Option<String>, CodegenError> {
2298 rest.first()
2299 .map(|a| this.expr_to_string(&a.value))
2300 .transpose()
2301 };
2302 let code = match method {
2303 "len" | "length" | "count" => format!("[...({recv_str})].length"),
2304 "byte_len" => format!("new TextEncoder().encode({recv_str}).length"),
2305 "is_empty" => format!("(({recv_str}).length === 0)"),
2306 "to_upper" => format!("({recv_str}).toUpperCase()"),
2307 "to_lower" => format!("({recv_str}).toLowerCase()"),
2308 "trim" => format!("({recv_str}).trim()"),
2309 "trim_start" => format!("({recv_str}).trimStart()"),
2310 "trim_end" => format!("({recv_str}).trimEnd()"),
2311 "reverse" => format!("[...({recv_str})].reverse().join('')"),
2312 "to_string" | "display" => format!("String({recv_str})"),
2313 "repeat" => {
2314 let Some(n) = arg0(self)? else {
2315 return Ok(false);
2316 };
2317 format!("({recv_str}).repeat({n})")
2318 }
2319 "contains" => {
2320 let Some(p) = arg0(self)? else {
2321 return Ok(false);
2322 };
2323 format!("({recv_str}).includes({p})")
2324 }
2325 "starts_with" => {
2326 let Some(p) = arg0(self)? else {
2327 return Ok(false);
2328 };
2329 format!("({recv_str}).startsWith({p})")
2330 }
2331 "ends_with" => {
2332 let Some(p) = arg0(self)? else {
2333 return Ok(false);
2334 };
2335 format!("({recv_str}).endsWith({p})")
2336 }
2337 "replace" => {
2338 let Some(from) = arg0(self)? else {
2339 return Ok(false);
2340 };
2341 let Some(to) = rest
2342 .get(1)
2343 .map(|a| self.expr_to_string(&a.value))
2344 .transpose()?
2345 else {
2346 return Ok(false);
2347 };
2348 format!("({recv_str}).replaceAll({from}, {to})")
2349 }
2350 "split" => {
2351 let Some(sep) = arg0(self)? else {
2352 return Ok(false);
2353 };
2354 format!("({recv_str}).split({sep})")
2355 }
2356 "slice" | "substring" => {
2361 let Some(start) = arg0(self)? else {
2362 return Ok(false);
2363 };
2364 let Some(end) = rest
2365 .get(1)
2366 .map(|a| self.expr_to_string(&a.value))
2367 .transpose()?
2368 else {
2369 return Ok(false);
2370 };
2371 format!("[...({recv_str})].slice({start}, {end}).join('')")
2372 }
2373 "char_at" => {
2375 let Some(i) = arg0(self)? else {
2376 return Ok(false);
2377 };
2378 format!(
2379 "((__s, __i) => __i >= 0 && __i < __s.length ? {{ _tag: \"Some\", _0: __s[__i] }} : {{ _tag: \"None\" }})([...({recv_str})], {i})"
2380 )
2381 }
2382 "index_of" => {
2386 let Some(p) = arg0(self)? else {
2387 return Ok(false);
2388 };
2389 format!(
2390 "((__s, __p) => {{ const __b = __s.indexOf(__p); return __b >= 0 ? {{ _tag: \"Some\", _0: [...__s.slice(0, __b)].length }} : {{ _tag: \"None\" }}; }})({recv_str}, {p})"
2391 )
2392 }
2393 _ => return Ok(false),
2394 };
2395 self.buf.push_str(&code);
2396 Ok(true)
2397 }
2398
2399 fn try_emit_numeric_method(
2410 &mut self,
2411 node: &AIRNode,
2412 callee: &AIRNode,
2413 args: &[bock_air::AirArg],
2414 ) -> Result<bool, CodegenError> {
2415 let prim = match crate::generator::primitive_recv_kind(node) {
2416 Some(p @ ("Int" | "Float" | "Char" | "Bool")) => p,
2417 _ => return Ok(false),
2418 };
2419 let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
2420 return Ok(false);
2421 };
2422 let method = field.name.as_str();
2423 let recv_str = self.expr_to_string(recv)?;
2424 let arg = |this: &mut Self, i: usize| -> Result<Option<String>, CodegenError> {
2425 rest.get(i)
2426 .map(|a| this.expr_to_string(&a.value))
2427 .transpose()
2428 };
2429 let code = match (prim, method) {
2430 ("Int", "to_float") => format!("({recv_str})"),
2433 ("Float", "to_int") => format!("Math.trunc({recv_str})"),
2434 ("Char", "to_int") => format!("(({recv_str}).codePointAt(0))"),
2435 ("Bool", "to_int") => format!("(({recv_str}) ? 1 : 0)"),
2436 ("Int", "abs") => format!("Math.abs({recv_str})"),
2438 ("Int" | "Float", "min") => {
2439 let Some(o) = arg(self, 0)? else {
2440 return Ok(false);
2441 };
2442 format!("Math.min({recv_str}, {o})")
2443 }
2444 ("Int" | "Float", "max") => {
2445 let Some(o) = arg(self, 0)? else {
2446 return Ok(false);
2447 };
2448 format!("Math.max({recv_str}, {o})")
2449 }
2450 ("Int" | "Float", "clamp") => {
2451 let (Some(lo), Some(hi)) = (arg(self, 0)?, arg(self, 1)?) else {
2452 return Ok(false);
2453 };
2454 format!("Math.min(Math.max({recv_str}, {lo}), {hi})")
2455 }
2456 ("Int", "shift_left") => {
2457 let Some(o) = arg(self, 0)? else {
2458 return Ok(false);
2459 };
2460 format!("(({recv_str}) << ({o}))")
2461 }
2462 ("Int", "shift_right") => {
2463 let Some(o) = arg(self, 0)? else {
2464 return Ok(false);
2465 };
2466 format!("(({recv_str}) >> ({o}))")
2467 }
2468 ("Float", "abs") => format!("Math.abs({recv_str})"),
2470 ("Float", "floor") => format!("Math.floor({recv_str})"),
2471 ("Float", "ceil") => format!("Math.ceil({recv_str})"),
2472 ("Float", "round") => format!("Math.round({recv_str})"),
2473 ("Float", "sqrt") => format!("Math.sqrt({recv_str})"),
2474 ("Float", "is_nan") => format!("Number.isNaN({recv_str})"),
2475 ("Float", "is_infinite") => format!("(!Number.isFinite({recv_str}))"),
2476 ("Bool", "negate") => format!("(!({recv_str}))"),
2478 ("Char", "to_upper") => format!("({recv_str}).toUpperCase()"),
2480 ("Char", "to_lower") => format!("({recv_str}).toLowerCase()"),
2481 ("Char", "is_alpha") => format!("(/\\p{{L}}/u.test({recv_str}))"),
2482 ("Char", "is_digit") => format!("(/[0-9]/.test({recv_str}))"),
2483 ("Char", "is_whitespace") => format!("(/\\s/.test({recv_str}))"),
2484 _ => return Ok(false),
2485 };
2486 self.buf.push_str(&code);
2487 Ok(true)
2488 }
2489
2490 fn try_emit_primitive_bridge(
2491 &mut self,
2492 node: &AIRNode,
2493 callee: &AIRNode,
2494 args: &[bock_air::AirArg],
2495 ) -> Result<bool, CodegenError> {
2496 let Some((recv, method, rest, _prim)) =
2497 crate::generator::primitive_bridge_call(node, callee, args)
2498 else {
2499 return Ok(false);
2500 };
2501 self.emit_bridge_method(recv, method, rest)
2502 }
2503
2504 fn try_emit_trait_bound_bridge(
2511 &mut self,
2512 node: &AIRNode,
2513 callee: &AIRNode,
2514 args: &[bock_air::AirArg],
2515 ) -> Result<bool, CodegenError> {
2516 let Some((recv, method, rest, _tr)) =
2517 crate::generator::trait_bound_bridge_call(node, callee, args, &self.trait_decls)
2518 else {
2519 return Ok(false);
2520 };
2521 if method == "eq" {
2527 let Some(other) = rest.first() else {
2528 return Ok(false);
2529 };
2530 let recv_str = self.expr_to_string(recv)?;
2531 let other = self.expr_to_string(&other.value)?;
2532 let _ = write!(self.buf, "__bockEq({recv_str}, {other})");
2533 return Ok(true);
2534 }
2535 if method == "compare" {
2543 let Some(other) = rest.first() else {
2544 return Ok(false);
2545 };
2546 let recv_str = self.expr_to_string(recv)?;
2547 let other = self.expr_to_string(&other.value)?;
2548 self.needs_runtime_compare = true;
2549 let _ = write!(self.buf, "__bockCompare({recv_str}, {other})");
2550 return Ok(true);
2551 }
2552 self.emit_bridge_method(recv, method, rest)
2553 }
2554
2555 fn emit_bridge_method(
2559 &mut self,
2560 recv: &AIRNode,
2561 method: &str,
2562 rest: &[bock_air::AirArg],
2563 ) -> Result<bool, CodegenError> {
2564 let recv_str = self.expr_to_string(recv)?;
2565 match method {
2566 "compare" => {
2567 let Some(other) = rest.first() else {
2568 return Ok(false);
2569 };
2570 let other = self.expr_to_string(&other.value)?;
2571 let _ = write!(
2572 self.buf,
2573 "(({recv_str}) < ({other}) ? {{ _tag: \"Less\" }} : \
2574 (({recv_str}) === ({other}) ? {{ _tag: \"Equal\" }} : {{ _tag: \"Greater\" }}))"
2575 );
2576 }
2577 "eq" => {
2578 let Some(other) = rest.first() else {
2579 return Ok(false);
2580 };
2581 let other = self.expr_to_string(&other.value)?;
2582 let _ = write!(self.buf, "(({recv_str}) === ({other}))");
2583 }
2584 "to_string" | "display" => {
2585 let _ = write!(self.buf, "String({recv_str})");
2586 }
2587 _ => return Ok(false),
2588 }
2589 Ok(true)
2590 }
2591
2592 fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
2595 self.mark_span(node.span);
2596 match &node.kind {
2597 NodeKind::Module { items, imports, .. } => {
2598 self.field_method_collisions
2606 .extend(crate::generator::collect_record_field_names(
2607 node,
2608 to_camel_case,
2609 ));
2610 if self.per_module {
2611 if self.module_uses_concurrency(items) {
2618 self.needs_runtime_concurrency = true;
2619 }
2620 if js_module_uses_range(items) {
2621 self.needs_runtime_range = true;
2622 }
2623 if js_module_uses_eq(items) {
2624 self.needs_runtime_eq = true;
2625 }
2626 if js_module_uses_compare(items) {
2627 self.needs_runtime_compare = true;
2628 }
2629 if js_module_uses_str(items) {
2630 self.needs_runtime_str = true;
2631 }
2632 self.emit_esm_imports(imports)?;
2635 } else {
2636 if !self.concurrency_runtime_emitted && self.module_uses_concurrency(items) {
2642 self.buf.push_str(CONCURRENCY_RUNTIME_JS);
2643 self.buf.push('\n');
2644 self.concurrency_runtime_emitted = true;
2645 }
2646 if !self.range_runtime_emitted && js_module_uses_range(items) {
2647 self.buf.push_str(RANGE_RUNTIME_JS);
2648 self.buf.push('\n');
2649 self.range_runtime_emitted = true;
2650 }
2651 if !self.eq_runtime_emitted && js_module_uses_eq(items) {
2652 self.buf.push_str(EQ_RUNTIME_JS);
2653 self.buf.push('\n');
2654 self.eq_runtime_emitted = true;
2655 }
2656 if !self.compare_runtime_emitted && js_module_uses_compare(items) {
2657 self.buf.push_str(COMPARE_RUNTIME_JS);
2658 self.buf.push('\n');
2659 self.compare_runtime_emitted = true;
2660 }
2661 if !self.str_runtime_emitted && js_module_uses_str(items) {
2662 self.buf.push_str(STR_RUNTIME_JS);
2663 self.buf.push('\n');
2664 self.str_runtime_emitted = true;
2665 }
2666 }
2667 let mut first = true;
2672 for item in items.iter() {
2673 if crate::generator::fn_is_test(item) {
2674 continue;
2675 }
2676 if !first {
2677 self.buf.push('\n');
2678 }
2679 first = false;
2680 self.emit_node(item)?;
2681 }
2682 if self.per_module {
2685 self.emit_trailing_exports();
2686 }
2687 Ok(())
2688 }
2689 NodeKind::ImportDecl { .. } => {
2690 Ok(())
2695 }
2696 NodeKind::FnDecl {
2697 visibility,
2698 is_async,
2699 name,
2700 params,
2701 effect_clause,
2702 body,
2703 ..
2704 } => self.emit_fn_decl(
2705 *visibility,
2706 *is_async,
2707 &name.name,
2708 params,
2709 effect_clause,
2710 body,
2711 false,
2712 ),
2713 NodeKind::RecordDecl { name, fields, .. } => {
2714 self.record_names.insert(name.name.clone());
2716 if fields.is_empty() {
2717 self.writeln(&format!("class {} {{}}", name.name));
2718 } else {
2719 let field_names: Vec<&str> =
2720 fields.iter().map(|f| f.name.name.as_str()).collect();
2721 self.writeln(&format!("class {} {{", name.name));
2722 self.indent += 1;
2723 self.writeln(&format!("constructor({{ {} }}) {{", field_names.join(", "),));
2724 self.indent += 1;
2725 for f in &field_names {
2726 self.writeln(&format!("this.{f} = {f};"));
2727 }
2728 self.indent -= 1;
2729 self.writeln("}");
2730 self.indent -= 1;
2731 self.writeln("}");
2732 }
2733 Ok(())
2734 }
2735 NodeKind::EnumDecl { name, variants, .. } => {
2736 for variant in variants {
2738 self.emit_enum_variant(&name.name, variant)?;
2739 }
2740 Ok(())
2741 }
2742 NodeKind::ClassDecl {
2743 name,
2744 fields,
2745 methods,
2746 ..
2747 } => {
2748 self.class_fields.insert(
2754 name.name.clone(),
2755 fields.iter().map(|f| f.name.name.clone()).collect(),
2756 );
2757 self.writeln(&format!("class {} {{", name.name));
2758 self.indent += 1;
2759 let field_names: Vec<&str> = fields.iter().map(|f| f.name.name.as_str()).collect();
2761 self.writeln(&format!("constructor({}) {{", field_names.join(", ")));
2762 self.indent += 1;
2763 for f in &field_names {
2764 self.writeln(&format!("this.{f} = {f};"));
2765 }
2766 self.indent -= 1;
2767 self.writeln("}");
2768 for method in methods {
2770 self.buf.push('\n');
2771 self.emit_class_method(method)?;
2772 }
2773 self.indent -= 1;
2774 self.writeln("}");
2775 Ok(())
2776 }
2777 NodeKind::TraitDecl { name, methods, .. } => {
2778 self.writeln(&format!("// trait {}", name.name));
2780 self.writeln(&format!("const {} = {{", name.name));
2781 self.indent += 1;
2782 for (i, method) in methods.iter().enumerate() {
2783 if i > 0 {
2784 self.buf.push('\n');
2785 }
2786 if let NodeKind::FnDecl {
2787 name, params, body, ..
2788 } = &method.kind
2789 {
2790 let param_names = self.collect_param_names(params);
2791 self.writeln(&format!("{}({}) {{", name.name, param_names.join(", ")));
2792 self.indent += 1;
2793 self.emit_block_body(body)?;
2794 self.indent -= 1;
2795 self.writeln("},");
2796 }
2797 }
2798 self.indent -= 1;
2799 self.writeln("};");
2800 Ok(())
2801 }
2802 NodeKind::ImplBlock {
2803 trait_path,
2804 target,
2805 methods,
2806 ..
2807 } => {
2808 let target_name = self.type_expr_to_string(target);
2810 if let Some(tp) = trait_path {
2811 let trait_name = tp
2812 .segments
2813 .iter()
2814 .map(|s| s.name.as_str())
2815 .collect::<Vec<_>>()
2816 .join(".");
2817 self.writeln(&format!("// impl {trait_name} for {target_name}"));
2818 } else {
2819 self.writeln(&format!("// impl {target_name}"));
2820 }
2821 let default_methods: Vec<AIRNode> = trait_path
2828 .as_ref()
2829 .map(|tp| {
2830 crate::generator::inherited_default_methods(&self.trait_decls, tp, methods)
2831 })
2832 .unwrap_or_default();
2833 for method in methods.iter().chain(default_methods.iter()) {
2834 if let NodeKind::FnDecl {
2835 is_async,
2836 name,
2837 params,
2838 effect_clause,
2839 body,
2840 ..
2841 } = &method.kind
2842 {
2843 let async_kw = if *is_async { "async " } else { "" };
2844 let param_names = self.collect_param_names(params);
2845 let effects_param = self.effects_param(effect_clause);
2846 let mut all_params = param_names;
2847 if let Some(ep) = effects_param {
2848 all_params.push(ep);
2849 }
2850 let attach = if crate::generator::is_associated_impl_method(
2857 method,
2858 &self.effect_ops,
2859 ) {
2860 ""
2861 } else {
2862 ".prototype"
2863 };
2864 self.writeln(&format!(
2865 "{target_name}{attach}.{} = {async_kw}function({}) {{",
2866 self.js_method_name(&name.name),
2867 all_params.join(", "),
2868 ));
2869 self.indent += 1;
2870 let old_handler_vars = self.current_handler_vars.clone();
2871 let expanded = self.expand_effect_names(effect_clause);
2872 for ename in &expanded {
2873 self.current_handler_vars
2874 .insert(ename.clone(), to_camel_case(ename));
2875 }
2876 self.emit_block_body(body)?;
2877 self.current_handler_vars = old_handler_vars;
2878 self.indent -= 1;
2879 self.writeln("};");
2880 }
2881 }
2882 Ok(())
2883 }
2884 NodeKind::EffectDecl {
2885 name,
2886 components,
2887 operations,
2888 ..
2889 } => {
2890 if !components.is_empty() {
2892 let comp_names: Vec<String> = components
2893 .iter()
2894 .map(|tp| {
2895 tp.segments
2896 .last()
2897 .map_or("effect".to_string(), |s| s.name.clone())
2898 })
2899 .collect();
2900 self.writeln(&format!(
2901 "// composite effect {} = {}",
2902 name.name,
2903 comp_names.join(" + ")
2904 ));
2905 let marker = comp_names
2913 .iter()
2914 .map(|c| format!("\"{c}\""))
2915 .collect::<Vec<_>>()
2916 .join(", ");
2917 self.writeln(&format!(
2918 "const {} = Object.freeze({{ __composite: [{}] }});",
2919 name.name, marker
2920 ));
2921 self.composite_effects.insert(name.name.clone(), comp_names);
2922 return Ok(());
2923 }
2924 for op in operations {
2926 if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
2927 self.effect_ops
2928 .insert(op_name.name.clone(), name.name.clone());
2929 }
2930 }
2931 self.writeln(&format!("class {} {{", name.name));
2933 self.indent += 1;
2934 for op in operations {
2935 if let NodeKind::FnDecl { name, params, .. } = &op.kind {
2936 let param_names = self.collect_param_names(params);
2937 self.writeln(&format!(
2938 "{}({}) {{",
2939 to_camel_case(&name.name),
2940 param_names.join(", "),
2941 ));
2942 self.indent += 1;
2943 self.writeln("throw new Error(\"not implemented\");");
2944 self.indent -= 1;
2945 self.writeln("}");
2946 }
2947 }
2948 self.indent -= 1;
2949 self.writeln("}");
2950 Ok(())
2951 }
2952 NodeKind::TypeAlias { name, .. } => {
2953 self.writeln(&format!("// type {} = ...", name.name));
2955 Ok(())
2956 }
2957 NodeKind::ConstDecl { name, value, .. } => {
2958 let ind = self.indent_str();
2959 let _ = write!(self.buf, "{ind}const {} = ", name.name);
2960 self.emit_expr(value)?;
2961 self.buf.push_str(";\n");
2962 Ok(())
2963 }
2964 NodeKind::ModuleHandle { effect, handler } => {
2965 let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
2966 let var_name = format!("__{}", to_camel_case(effect_name));
2967 let ind = self.indent_str();
2968 let _ = write!(self.buf, "{ind}const {var_name} = ");
2969 self.emit_expr(handler)?;
2970 self.buf.push_str(";\n");
2971 self.current_handler_vars
2973 .insert(effect_name.to_string(), var_name);
2974 Ok(())
2975 }
2976 NodeKind::PropertyTest { name, body, .. } => {
2977 self.writeln(&format!("// property test: {name}"));
2978 self.writeln("// (property tests are not emitted in JS output)");
2979 let _ = body;
2980 Ok(())
2981 }
2982 NodeKind::LetBinding { .. }
2984 | NodeKind::If { .. }
2985 | NodeKind::For { .. }
2986 | NodeKind::While { .. }
2987 | NodeKind::Loop { .. }
2988 | NodeKind::Return { .. }
2989 | NodeKind::Break { .. }
2990 | NodeKind::Continue
2991 | NodeKind::Guard { .. }
2992 | NodeKind::Match { .. }
2993 | NodeKind::Block { .. }
2994 | NodeKind::HandlingBlock { .. }
2995 | NodeKind::Assign { .. } => self.emit_stmt(node),
2996 _ => {
2998 let only_propagate = self.hoist_propagates(node)?
3001 && matches!(&node.kind, NodeKind::Propagate { .. });
3002 if only_propagate {
3006 return Ok(());
3007 }
3008 self.write_indent();
3009 self.emit_expr(node)?;
3010 self.buf.push_str(";\n");
3011 Ok(())
3012 }
3013 }
3014 }
3015
3016 #[allow(clippy::too_many_arguments)]
3019 fn emit_fn_decl(
3020 &mut self,
3021 visibility: Visibility,
3022 is_async: bool,
3023 name: &str,
3024 params: &[AIRNode],
3025 effect_clause: &[bock_ast::TypePath],
3026 body: &AIRNode,
3027 _is_method: bool,
3028 ) -> Result<(), CodegenError> {
3029 let export = if matches!(visibility, Visibility::Public) {
3030 "export "
3031 } else {
3032 ""
3033 };
3034 let async_kw = if is_async { "async " } else { "" };
3035 let param_names = self.collect_param_names(params);
3036 let effects_param = self.effects_param(effect_clause);
3037 let mut all_params = param_names;
3038 if let Some(ep) = effects_param {
3039 all_params.push(ep);
3040 }
3041 if !effect_clause.is_empty() {
3042 let effect_names = self.expand_effect_names(effect_clause);
3043 self.fn_effects.insert(name.to_string(), effect_names);
3044 }
3045 let js_name = js_value_ident(name);
3046 self.writeln(&format!(
3047 "{export}{async_kw}function {js_name}({}) {{",
3048 all_params.join(", "),
3049 ));
3050 self.indent += 1;
3051 let old_handler_vars = self.current_handler_vars.clone();
3052 let expanded = self.expand_effect_names(effect_clause);
3053 for ename in &expanded {
3054 self.current_handler_vars
3055 .insert(ename.clone(), to_camel_case(ename));
3056 }
3057 self.emit_fn_body_seeded(params, body)?;
3058 self.current_handler_vars = old_handler_vars;
3059 self.indent -= 1;
3060 self.writeln("}");
3061 Ok(())
3062 }
3063
3064 fn emit_class_method(&mut self, method: &AIRNode) -> Result<(), CodegenError> {
3065 if let NodeKind::FnDecl {
3066 is_async,
3067 name,
3068 params,
3069 effect_clause,
3070 body,
3071 ..
3072 } = &method.kind
3073 {
3074 let async_kw = if *is_async { "async " } else { "" };
3075 let param_names = self.collect_param_names(params);
3076 let effects_param = self.effects_param(effect_clause);
3077 let mut all_params = param_names;
3078 if let Some(ep) = effects_param {
3079 all_params.push(ep);
3080 }
3081 let method_name = self.js_method_name(&to_camel_case(&name.name));
3082 self.writeln(&format!(
3083 "{async_kw}{method_name}({}) {{",
3084 all_params.join(", "),
3085 ));
3086 self.indent += 1;
3087 let old_handler_vars = self.current_handler_vars.clone();
3088 let expanded = self.expand_effect_names(effect_clause);
3089 for ename in &expanded {
3090 self.current_handler_vars
3091 .insert(ename.clone(), to_camel_case(ename));
3092 }
3093 self.emit_fn_body_seeded(params, body)?;
3094 self.current_handler_vars = old_handler_vars;
3095 self.indent -= 1;
3096 self.writeln("}");
3097 }
3098 Ok(())
3099 }
3100
3101 fn collect_param_names(&self, params: &[AIRNode]) -> Vec<String> {
3102 params
3103 .iter()
3104 .filter_map(|p| {
3105 if let NodeKind::Param {
3106 pattern, default, ..
3107 } = &p.kind
3108 {
3109 let name = self.pattern_to_binding_name(pattern);
3110 if let Some(def) = default {
3111 let mut ctx = EmitCtx::new();
3112 ctx.indent = self.indent;
3113 ctx.enum_variants = self.enum_variants.clone();
3114 if ctx.emit_expr_to_string(def).is_ok() {
3115 let (def_str, _) = ctx.finish();
3116 return Some(format!("{name} = {def_str}"));
3117 }
3118 }
3119 Some(name)
3120 } else {
3121 None
3122 }
3123 })
3124 .collect()
3125 }
3126
3127 fn emit_expr_to_string(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
3128 self.emit_expr(node)
3129 }
3130
3131 fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
3133 let mut result = Vec::new();
3134 for tp in effects {
3135 let name = tp
3136 .segments
3137 .last()
3138 .map_or("effect".to_string(), |s| s.name.clone());
3139 if let Some(components) = self.composite_effects.get(&name) {
3140 result.extend(components.iter().cloned());
3141 } else {
3142 result.push(name);
3143 }
3144 }
3145 result
3146 }
3147
3148 fn clock_handler_var(&self) -> Option<&str> {
3155 self.current_handler_vars.get("Clock").map(String::as_str)
3156 }
3157
3158 fn effects_param(&self, effects: &[bock_ast::TypePath]) -> Option<String> {
3160 if effects.is_empty() {
3161 return None;
3162 }
3163 let expanded = self.expand_effect_names(effects);
3164 if expanded.is_empty() {
3165 return None;
3166 }
3167 let names: Vec<String> = expanded.iter().map(|n| to_camel_case(n)).collect();
3168 Some(format!("{{ {} }}", names.join(", ")))
3169 }
3170
3171 fn build_effects_call_arg_js(&self, fn_name: &str) -> Option<String> {
3174 let effects = self.fn_effects.get(fn_name)?;
3175 let entries: Vec<String> = effects
3176 .iter()
3177 .filter_map(|e| {
3178 let handler_var = self.current_handler_vars.get(e)?;
3179 let param_name = to_camel_case(e);
3180 Some(format!("{param_name}: {handler_var}"))
3181 })
3182 .collect();
3183 if entries.is_empty() {
3184 return None;
3185 }
3186 Some(format!("{{ {} }}", entries.join(", ")))
3187 }
3188
3189 fn emit_enum_variant(
3192 &mut self,
3193 enum_name: &str,
3194 variant: &AIRNode,
3195 ) -> Result<(), CodegenError> {
3196 if let NodeKind::EnumVariant { name, payload } = &variant.kind {
3197 let vname = &name.name;
3198 match payload {
3199 EnumVariantPayload::Unit => {
3200 self.writeln(&format!(
3201 "const {enum_name}_{vname} = Object.freeze({{ _tag: \"{vname}\" }});"
3202 ));
3203 }
3204 EnumVariantPayload::Struct(fields) => {
3205 let field_names: Vec<&str> =
3206 fields.iter().map(|f| f.name.name.as_str()).collect();
3207 self.writeln(&format!(
3208 "function {enum_name}_{vname}({}) {{",
3209 field_names.join(", ")
3210 ));
3211 self.indent += 1;
3212 self.writeln(&format!(
3213 "return {{ _tag: \"{vname}\", {} }};",
3214 field_names.join(", ")
3215 ));
3216 self.indent -= 1;
3217 self.writeln("}");
3218 }
3219 EnumVariantPayload::Tuple(elems) => {
3220 let param_names: Vec<String> =
3221 (0..elems.len()).map(|i| format!("_{i}")).collect();
3222 self.writeln(&format!(
3223 "function {enum_name}_{vname}({}) {{",
3224 param_names.join(", ")
3225 ));
3226 self.indent += 1;
3227 self.writeln(&format!(
3228 "return {{ _tag: \"{vname}\", {} }};",
3229 param_names
3230 .iter()
3231 .enumerate()
3232 .map(|(i, p)| format!("_{i}: {p}"))
3233 .collect::<Vec<_>>()
3234 .join(", ")
3235 ));
3236 self.indent -= 1;
3237 self.writeln("}");
3238 }
3239 }
3240 }
3241 Ok(())
3242 }
3243
3244 fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
3247 self.mark_span(node.span);
3248 self.hoist_propagates(node)?;
3252 match &node.kind {
3253 NodeKind::LetBinding {
3254 is_mut,
3255 pattern,
3256 value,
3257 ..
3258 } => {
3259 if node.metadata.contains_key(crate::generator::DECL_ONLY_META) {
3263 if let NodeKind::BindPat { name, .. } = &pattern.kind {
3264 let ind = self.indent_str();
3265 let js_name = js_value_ident(&name.name);
3266 self.mark_simple_let_declared(&js_name);
3267 let _ = writeln!(self.buf, "{ind}let {js_name};");
3268 return Ok(());
3269 }
3270 }
3271 let ind = self.indent_str();
3272 if let NodeKind::BindPat { name, .. } = &pattern.kind {
3278 let js_name = js_value_ident(&name.name);
3279 if self.simple_let_redeclared(&js_name) {
3280 let _ = write!(self.buf, "{ind}{js_name} = ");
3281 self.emit_expr(value)?;
3282 self.buf.push_str(";\n");
3283 return Ok(());
3284 }
3285 let needs_let = *is_mut || self.simple_let_needs_let(&js_name);
3286 let kw = if needs_let { "let" } else { "const" };
3287 self.mark_simple_let_declared(&js_name);
3288 let _ = write!(self.buf, "{ind}{kw} {js_name} = ");
3289 self.emit_expr(value)?;
3290 self.buf.push_str(";\n");
3291 return Ok(());
3292 }
3293 let kw = if *is_mut { "let" } else { "const" };
3294 let binding = self.pattern_to_js_destructure(pattern);
3295 let _ = write!(self.buf, "{ind}{kw} {binding} = ");
3296 self.emit_expr(value)?;
3297 self.buf.push_str(";\n");
3298 Ok(())
3299 }
3300 NodeKind::If {
3301 let_pattern,
3302 condition,
3303 then_block,
3304 else_block,
3305 } => {
3306 if let Some(pat) = let_pattern {
3307 let ind = self.indent_str();
3309 let _ = write!(self.buf, "{ind}if (");
3310 self.emit_expr(condition)?;
3311 self.buf.push_str(" != null) {\n");
3312 self.indent += 1;
3313 let binding = self.pattern_to_js_destructure(pat);
3314 self.writeln(&format!("const {binding} = "));
3315 self.emit_block_body(then_block)?;
3319 self.indent -= 1;
3320 } else {
3321 let ind = self.indent_str();
3322 let _ = write!(self.buf, "{ind}if (");
3323 self.emit_expr(condition)?;
3324 self.buf.push_str(") {\n");
3325 self.indent += 1;
3326 self.emit_block_body(then_block)?;
3327 self.indent -= 1;
3328 }
3329 if let Some(else_b) = else_block {
3330 if matches!(else_b.kind, NodeKind::If { .. }) {
3331 let ind = self.indent_str();
3332 let _ = write!(self.buf, "{ind}}} else ");
3333 self.emit_stmt(else_b)?;
3335 return Ok(());
3336 }
3337 self.writeln("} else {");
3338 self.indent += 1;
3339 self.emit_block_body(else_b)?;
3340 self.indent -= 1;
3341 }
3342 self.writeln("}");
3343 Ok(())
3344 }
3345 NodeKind::For {
3346 pattern,
3347 iterable,
3348 body,
3349 } => {
3350 let binding = self.pattern_to_js_destructure(pattern);
3351 self.emit_loop_label_prefix(body);
3352 let ind = self.indent_str();
3353 let _ = write!(self.buf, "{ind}for (const {binding} of ");
3354 self.emit_expr(iterable)?;
3355 self.buf.push_str(") {\n");
3356 self.indent += 1;
3357 self.emit_loop_body(body)?;
3358 self.indent -= 1;
3359 self.writeln("}");
3360 self.loop_labels.pop();
3361 Ok(())
3362 }
3363 NodeKind::While { condition, body } => {
3364 self.emit_loop_label_prefix(body);
3365 let ind = self.indent_str();
3366 let _ = write!(self.buf, "{ind}while (");
3367 self.emit_expr(condition)?;
3368 self.buf.push_str(") {\n");
3369 self.indent += 1;
3370 self.emit_loop_body(body)?;
3371 self.indent -= 1;
3372 self.writeln("}");
3373 self.loop_labels.pop();
3374 Ok(())
3375 }
3376 NodeKind::Loop { body } => {
3377 self.emit_loop_label_prefix(body);
3378 self.writeln("while (true) {");
3379 self.indent += 1;
3380 self.emit_loop_body(body)?;
3381 self.indent -= 1;
3382 self.writeln("}");
3383 self.loop_labels.pop();
3384 Ok(())
3385 }
3386 NodeKind::Return { value } => {
3387 if let Some(val) = value {
3388 let ind = self.indent_str();
3389 let _ = write!(self.buf, "{ind}return ");
3390 self.emit_expr(val)?;
3391 self.buf.push_str(";\n");
3392 } else {
3393 self.writeln("return;");
3394 }
3395 Ok(())
3396 }
3397 NodeKind::Break { value } => {
3398 if let Some(val) = value {
3399 let ind = self.indent_str();
3401 let _ = write!(self.buf, "{ind}/* break value: ");
3402 self.emit_expr(val)?;
3403 self.buf.push_str(" */\n");
3404 }
3405 if self.switch_label_depth > 0 {
3408 if let Some(label) = self.innermost_loop_label() {
3409 self.writeln(&format!("break {label};"));
3410 return Ok(());
3411 }
3412 }
3413 self.writeln("break;");
3414 Ok(())
3415 }
3416 NodeKind::Continue => {
3417 if self.switch_label_depth > 0 {
3418 if let Some(label) = self.innermost_loop_label() {
3419 self.writeln(&format!("continue {label};"));
3420 return Ok(());
3421 }
3422 }
3423 self.writeln("continue;");
3424 Ok(())
3425 }
3426 NodeKind::Guard {
3427 let_pattern,
3428 condition,
3429 else_block,
3430 } => {
3431 if let Some(pat) = let_pattern {
3432 self.match_temp_counter += 1;
3437 let tmp = format!("__guard{}", self.match_temp_counter);
3438 let ind = self.indent_str();
3439 let _ = write!(self.buf, "{ind}const {tmp} = ");
3440 self.emit_expr(condition)?;
3441 self.buf.push_str(";\n");
3442 let test = self.pattern_test_js(pat, &tmp);
3443 if !test.is_empty() {
3445 let ind = self.indent_str();
3446 let _ = writeln!(self.buf, "{ind}if (!({test})) {{");
3447 self.indent += 1;
3448 self.emit_block_body(else_block)?;
3449 self.indent -= 1;
3450 self.writeln("}");
3451 }
3452 self.pattern_binds_js(pat, &tmp)?;
3454 } else {
3455 let ind = self.indent_str();
3456 let _ = write!(self.buf, "{ind}if (!(");
3457 self.emit_expr(condition)?;
3458 self.buf.push_str(")) {\n");
3459 self.indent += 1;
3460 self.emit_block_body(else_block)?;
3461 self.indent -= 1;
3462 self.writeln("}");
3463 }
3464 Ok(())
3465 }
3466 NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms),
3467 NodeKind::Block { stmts, tail } => {
3468 self.writeln("{");
3472 self.indent += 1;
3473 self.enter_let_scope(node);
3474 for s in stmts {
3475 self.emit_node(s)?;
3476 }
3477 if let Some(t) = tail {
3478 self.write_indent();
3479 self.emit_expr(t)?;
3480 self.buf.push_str(";\n");
3481 }
3482 self.leave_let_scope();
3483 self.indent -= 1;
3484 self.writeln("}");
3485 Ok(())
3486 }
3487 NodeKind::HandlingBlock { handlers, body } => {
3488 self.writeln("{");
3499 self.indent += 1;
3500 self.enter_let_scope(body);
3501 let old_handler_vars = self.current_handler_vars.clone();
3502 for h in handlers {
3503 let effect_name = h
3504 .effect
3505 .segments
3506 .last()
3507 .map_or("effect", |s| s.name.as_str());
3508 let var_name = format!("__{}", to_camel_case(effect_name));
3509 let ind = self.indent_str();
3510 let _ = write!(self.buf, "{ind}const {var_name} = ");
3511 self.emit_expr(&h.handler)?;
3512 self.buf.push_str(";\n");
3513 self.current_handler_vars
3514 .insert(effect_name.to_string(), var_name);
3515 }
3516 if let NodeKind::Block { stmts, tail } = &body.kind {
3517 for s in stmts {
3518 self.emit_node(s)?;
3519 }
3520 if let Some(t) = tail {
3521 self.write_indent();
3522 self.emit_expr(t)?;
3523 self.buf.push_str(";\n");
3524 }
3525 } else {
3526 self.emit_stmt(body)?;
3527 }
3528 self.current_handler_vars = old_handler_vars;
3529 self.leave_let_scope();
3530 self.indent -= 1;
3531 self.writeln("}");
3532 Ok(())
3533 }
3534 NodeKind::Assign { op, target, value } => {
3535 let ind = self.indent_str();
3536 let _ = write!(self.buf, "{ind}");
3537 self.emit_expr(target)?;
3538 let op_str = match op {
3539 AssignOp::Assign => "=",
3540 AssignOp::AddAssign => "+=",
3541 AssignOp::SubAssign => "-=",
3542 AssignOp::MulAssign => "*=",
3543 AssignOp::DivAssign => "/=",
3544 AssignOp::RemAssign => "%=",
3545 };
3546 let _ = write!(self.buf, " {op_str} ");
3547 self.emit_expr(value)?;
3548 self.buf.push_str(";\n");
3549 Ok(())
3550 }
3551 _ => {
3552 self.write_indent();
3554 self.emit_expr(node)?;
3555 self.buf.push_str(";\n");
3556 Ok(())
3557 }
3558 }
3559 }
3560
3561 fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
3564 self.mark_span(node.span);
3565 match &node.kind {
3566 NodeKind::Literal { lit } => {
3567 match lit {
3568 Literal::Int(s) => self.buf.push_str(s),
3569 Literal::Float(s) => self.buf.push_str(s),
3570 Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
3571 Literal::Char(s) => {
3572 self.buf.push('\'');
3573 self.buf.push_str(s);
3574 self.buf.push('\'');
3575 }
3576 Literal::String(s) => {
3577 self.buf.push('"');
3578 self.buf.push_str(&escape_js_string(s));
3579 self.buf.push('"');
3580 }
3581 Literal::Unit => self.buf.push_str("undefined"),
3582 }
3583 Ok(())
3584 }
3585 NodeKind::Identifier { name } => {
3586 if name.name == "None" {
3587 self.buf.push_str("{ _tag: \"None\" }");
3588 } else if let Some(variant) = crate::generator::ordering_variant(&name.name) {
3589 let _ = write!(self.buf, "{{ _tag: \"{variant}\" }}");
3594 } else if let Some(enum_name) = self
3595 .user_variant_for_name(&name.name)
3596 .map(|i| i.enum_name.clone())
3597 {
3598 let _ = write!(self.buf, "{enum_name}_{}", name.name);
3601 } else if self.const_names.contains(&name.name) {
3602 self.buf.push_str(&name.name);
3607 } else {
3608 self.buf.push_str(&js_value_ident(&name.name));
3609 }
3610 Ok(())
3611 }
3612 NodeKind::BinaryOp { op, left, right } => {
3613 if matches!(op, BinOp::Add) && crate::generator::is_list_concat(node, left, right) {
3617 self.buf.push_str("[...");
3618 self.emit_expr(left)?;
3619 self.buf.push_str(", ...");
3620 self.emit_expr(right)?;
3621 self.buf.push(']');
3622 return Ok(());
3623 }
3624 if matches!(op, BinOp::Div | BinOp::Rem) && crate::generator::is_int_arith(node) {
3631 let body = if matches!(op, BinOp::Div) {
3632 "Math.trunc(__a / __b)"
3633 } else {
3634 "__a % __b"
3635 };
3636 self.buf.push_str("((__a, __b) => { if (__b === 0) { throw new Error(\"integer division or modulo by zero\"); } return ");
3637 self.buf.push_str(body);
3638 self.buf.push_str("; })(");
3639 self.emit_expr(left)?;
3640 self.buf.push_str(", ");
3641 self.emit_expr(right)?;
3642 self.buf.push(')');
3643 return Ok(());
3644 }
3645 if crate::generator::is_user_compare(node) {
3651 if let Some((tag, is_eq)) = crate::generator::user_compare_variant(*op) {
3652 let recv = self.expr_to_string(left)?;
3653 let other = self.expr_to_string(right)?;
3654 let eq = if is_eq { "===" } else { "!==" };
3655 let _ = write!(
3656 self.buf,
3657 "(({recv}).compare({recv}, {other})._tag {eq} \"{tag}\")"
3658 );
3659 return Ok(());
3660 }
3661 }
3662 if matches!(op, BinOp::Eq | BinOp::Ne) {
3671 if let Some(kind) = crate::generator::user_eq_kind(node) {
3672 let recv = self.expr_to_string(left)?;
3673 let other = self.expr_to_string(right)?;
3674 let neg = if *op == BinOp::Ne { "!" } else { "" };
3675 if kind == "impl" {
3676 let _ = write!(self.buf, "{neg}(({recv}).eq({recv}, {other}))");
3677 } else {
3678 let _ = write!(self.buf, "{neg}__bockEq({recv}, {other})");
3679 }
3680 return Ok(());
3681 }
3682 }
3683 self.buf.push('(');
3684 self.emit_expr(left)?;
3685 let op_str = match op {
3686 BinOp::Add => " + ",
3687 BinOp::Sub => " - ",
3688 BinOp::Mul => " * ",
3689 BinOp::Div => " / ",
3690 BinOp::Rem => " % ",
3691 BinOp::Pow => " ** ",
3692 BinOp::Eq => " === ",
3693 BinOp::Ne => " !== ",
3694 BinOp::Lt => " < ",
3695 BinOp::Le => " <= ",
3696 BinOp::Gt => " > ",
3697 BinOp::Ge => " >= ",
3698 BinOp::And => " && ",
3699 BinOp::Or => " || ",
3700 BinOp::BitAnd => " & ",
3701 BinOp::BitOr => " | ",
3702 BinOp::BitXor => " ^ ",
3703 BinOp::Compose => " /* >> */ ",
3704 BinOp::Is => " instanceof ",
3705 };
3706 self.buf.push_str(op_str);
3707 self.emit_expr(right)?;
3708 self.buf.push(')');
3709 Ok(())
3710 }
3711 NodeKind::UnaryOp { op, operand } => {
3712 let op_str = match op {
3713 UnaryOp::Neg => "-",
3714 UnaryOp::Not => "!",
3715 UnaryOp::BitNot => "~",
3716 };
3717 self.buf.push_str(op_str);
3718 self.emit_expr(operand)?;
3719 Ok(())
3720 }
3721 NodeKind::Call { callee, args, .. } => {
3722 if let Some(code) = self.map_prelude_call(callee, args)? {
3723 self.buf.push_str(&code);
3724 return Ok(());
3725 }
3726 if self.try_emit_prelude_ctor(callee, args)? {
3727 return Ok(());
3728 }
3729 if self.try_emit_time_assoc_call(callee, args)? {
3730 return Ok(());
3731 }
3732 if self.try_emit_time_desugared_method(node, callee, args)? {
3733 return Ok(());
3734 }
3735 if self.try_emit_concurrency_call(callee, args)? {
3736 return Ok(());
3737 }
3738 if self.try_emit_map_method(node, callee, args)? {
3743 return Ok(());
3744 }
3745 if self.try_emit_set_method(node, callee, args)? {
3746 return Ok(());
3747 }
3748 if self.try_emit_string_method(node, callee, args)? {
3752 return Ok(());
3753 }
3754 if self.try_emit_numeric_method(node, callee, args)? {
3758 return Ok(());
3759 }
3760 if self.try_emit_list_mutating_method(node, callee, args)? {
3761 return Ok(());
3762 }
3763 if self.try_emit_list_inplace_mutator(node, callee, args)? {
3764 return Ok(());
3765 }
3766 if self.try_emit_list_method(node, callee, args)? {
3767 return Ok(());
3768 }
3769 if self.try_emit_list_functional_method(node, callee, args)? {
3770 return Ok(());
3771 }
3772 if self.try_emit_primitive_bridge(node, callee, args)? {
3773 return Ok(());
3774 }
3775 if self.try_emit_trait_bound_bridge(node, callee, args)? {
3776 return Ok(());
3777 }
3778 if self.try_emit_container_method(node, callee, args)? {
3779 return Ok(());
3780 }
3781 if let NodeKind::Identifier { name } = &callee.kind {
3783 if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
3784 if let Some(handler_var) =
3785 self.current_handler_vars.get(&effect_name).cloned()
3786 {
3787 let _ = write!(self.buf, "{}.{}", handler_var, name.name);
3788 self.buf.push('(');
3789 for (i, arg) in args.iter().enumerate() {
3790 if i > 0 {
3791 self.buf.push_str(", ");
3792 }
3793 self.emit_expr(&arg.value)?;
3794 }
3795 self.buf.push(')');
3796 return Ok(());
3797 }
3798 }
3799 }
3800 if self.try_emit_primitive_conversion(node, callee, args)? {
3805 return Ok(());
3806 }
3807 if crate::generator::is_associated_call(node) {
3814 if let NodeKind::FieldAccess { object, field } = &callee.kind {
3815 if let NodeKind::Identifier { name: type_name } = &object.kind {
3816 let _ = write!(
3817 self.buf,
3818 "{}.{}",
3819 type_name.name,
3820 self.js_method_name(&field.name)
3821 );
3822 self.buf.push('(');
3823 for (i, arg) in args.iter().enumerate() {
3824 if i > 0 {
3825 self.buf.push_str(", ");
3826 }
3827 self.emit_expr(&arg.value)?;
3828 }
3829 self.buf.push(')');
3830 return Ok(());
3831 }
3832 }
3833 }
3834 if let NodeKind::FieldAccess { object, field } = &callee.kind {
3846 if crate::generator::desugared_self_call(callee, args).is_some() {
3847 let renamed = self.js_method_name(&field.name);
3851 if renamed != field.name {
3852 self.emit_expr(object)?;
3853 let _ = write!(self.buf, ".{renamed}");
3854 self.buf.push('(');
3855 for (i, arg) in args.iter().enumerate() {
3856 if i > 0 {
3857 self.buf.push_str(", ");
3858 }
3859 self.emit_expr(&arg.value)?;
3860 }
3861 self.buf.push(')');
3862 return Ok(());
3863 }
3864 }
3865 }
3866 let effects_arg = if let NodeKind::Identifier { name } = &callee.kind {
3868 self.build_effects_call_arg_js(&name.name)
3869 } else {
3870 None
3871 };
3872 self.emit_callee(callee)?;
3873 self.buf.push('(');
3874 for (i, arg) in args.iter().enumerate() {
3875 if i > 0 {
3876 self.buf.push_str(", ");
3877 }
3878 self.emit_expr(&arg.value)?;
3879 }
3880 if let Some(ea) = effects_arg {
3881 if !args.is_empty() {
3882 self.buf.push_str(", ");
3883 }
3884 self.buf.push_str(&ea);
3885 }
3886 self.buf.push(')');
3887 Ok(())
3888 }
3889 NodeKind::MethodCall {
3890 receiver,
3891 method,
3892 args,
3893 ..
3894 } => {
3895 if self.try_emit_time_method(receiver, &method.name, args)? {
3896 return Ok(());
3897 }
3898 self.emit_expr(receiver)?;
3899 let _ = write!(
3900 self.buf,
3901 ".{}",
3902 self.js_method_name(&to_camel_case(&method.name))
3903 );
3904 self.buf.push('(');
3905 for (i, arg) in args.iter().enumerate() {
3906 if i > 0 {
3907 self.buf.push_str(", ");
3908 }
3909 self.emit_expr(&arg.value)?;
3910 }
3911 self.buf.push(')');
3912 Ok(())
3913 }
3914 NodeKind::FieldAccess { object, field } => {
3915 self.emit_expr(object)?;
3916 let _ = write!(self.buf, ".{}", field.name);
3917 Ok(())
3918 }
3919 NodeKind::Index { object, index } => {
3920 self.emit_expr(object)?;
3921 self.buf.push('[');
3922 self.emit_expr(index)?;
3923 self.buf.push(']');
3924 Ok(())
3925 }
3926 NodeKind::Lambda { params, body } => {
3927 let param_names = self.collect_param_names(params);
3928 let _ = write!(self.buf, "({}) => ", param_names.join(", "));
3929 if matches!(body.kind, NodeKind::Block { .. }) {
3931 self.buf.push_str("{\n");
3932 self.indent += 1;
3933 let prev = std::mem::replace(&mut self.discard_tail, false);
3938 let r = self.emit_block_body(body);
3939 self.discard_tail = prev;
3940 r?;
3941 self.indent -= 1;
3942 self.write_indent();
3943 self.buf.push('}');
3944 } else {
3945 self.emit_expr(body)?;
3946 }
3947 Ok(())
3948 }
3949 NodeKind::Pipe { left, right } => {
3950 self.emit_pipe(left, right)
3953 }
3954 NodeKind::Compose { left, right } => {
3955 let _ = write!(self.buf, "((x) => ");
3963 self.emit_callee(right)?;
3964 self.buf.push('(');
3965 self.emit_callee(left)?;
3966 self.buf.push_str("(x)))");
3967 Ok(())
3968 }
3969 NodeKind::Await { expr } => {
3970 self.buf.push_str("(await ");
3971 self.emit_expr(expr)?;
3972 self.buf.push(')');
3973 Ok(())
3974 }
3975 NodeKind::Propagate { expr } => {
3976 if let Some(tmp) = self.propagate_temps.get(&(node as *const AIRNode as usize)) {
3985 let _ = write!(self.buf, "{tmp}._0");
3986 Ok(())
3987 } else {
3988 self.emit_expr(expr)
3989 }
3990 }
3991 NodeKind::Range { lo, hi, inclusive } => {
3992 if *inclusive {
3994 self.buf.push_str("rangeInclusive(");
3995 } else {
3996 self.buf.push_str("range(");
3997 }
3998 self.emit_expr(lo)?;
3999 self.buf.push_str(", ");
4000 self.emit_expr(hi)?;
4001 self.buf.push(')');
4002 Ok(())
4003 }
4004 NodeKind::RecordConstruct {
4005 path,
4006 fields,
4007 spread,
4008 } => {
4009 let struct_variant = if spread.is_none() {
4014 self.user_variant_for_path(path).and_then(|info| {
4015 if let crate::generator::VariantPayloadKind::Struct(field_order) =
4016 &info.payload
4017 {
4018 Some((info.enum_name.clone(), field_order.clone()))
4019 } else {
4020 None
4021 }
4022 })
4023 } else {
4024 None
4025 };
4026 if let Some((enum_name, field_order)) = struct_variant {
4027 let variant = path.segments.last().map_or("", |s| s.name.as_str());
4028 let _ = write!(self.buf, "{enum_name}_{variant}(");
4029 for (i, fname) in field_order.iter().enumerate() {
4030 if i > 0 {
4031 self.buf.push_str(", ");
4032 }
4033 let supplied = fields.iter().find(|f| &f.name.name == fname);
4036 match supplied.and_then(|f| f.value.as_ref()) {
4037 Some(val) => self.emit_expr(val)?,
4038 None => self.buf.push_str(&js_value_ident(fname)),
4041 }
4042 }
4043 self.buf.push(')');
4044 return Ok(());
4045 }
4046 let type_name = path.segments.last().map(|s| s.name.as_str()).unwrap_or("");
4047 if let Some(field_order) = self.class_fields.get(type_name).cloned() {
4055 let _ = write!(self.buf, "new {type_name}(");
4056 for (i, fname) in field_order.iter().enumerate() {
4057 if i > 0 {
4058 self.buf.push_str(", ");
4059 }
4060 let supplied = fields.iter().find(|f| &f.name.name == fname);
4061 match supplied.and_then(|f| f.value.as_ref()) {
4062 Some(val) => self.emit_expr(val)?,
4063 None if supplied.is_some() => {
4068 self.buf.push_str(&js_value_ident(fname));
4069 }
4070 None => match spread {
4071 Some(sp) => {
4072 self.emit_expr(sp)?;
4073 let _ = write!(self.buf, ".{}", js_value_ident(fname));
4074 }
4075 None => self.buf.push_str("undefined"),
4076 },
4077 }
4078 }
4079 self.buf.push(')');
4080 return Ok(());
4081 }
4082 let is_class = self.record_names.contains(type_name);
4083 if is_class {
4084 let _ = write!(self.buf, "new {type_name}(");
4085 if fields.is_empty() && spread.is_none() {
4086 self.buf.push(')');
4087 return Ok(());
4088 }
4089 }
4090 if let Some(sp) = spread {
4091 self.buf.push_str("{ ...");
4092 self.emit_expr(sp)?;
4093 if !fields.is_empty() {
4094 self.buf.push_str(", ");
4095 }
4096 } else {
4097 self.buf.push_str("{ ");
4098 }
4099 for (i, f) in fields.iter().enumerate() {
4100 if i > 0 {
4101 self.buf.push_str(", ");
4102 }
4103 if let Some(val) = &f.value {
4104 let _ = write!(self.buf, "{}: ", f.name.name);
4105 self.emit_expr(val)?;
4106 } else {
4107 self.buf.push_str(&f.name.name);
4109 }
4110 }
4111 self.buf.push_str(" }");
4112 if is_class {
4113 self.buf.push(')');
4114 }
4115 Ok(())
4116 }
4117 NodeKind::ListLiteral { elems } => {
4118 self.buf.push('[');
4119 for (i, e) in elems.iter().enumerate() {
4120 if i > 0 {
4121 self.buf.push_str(", ");
4122 }
4123 self.emit_expr(e)?;
4124 }
4125 self.buf.push(']');
4126 Ok(())
4127 }
4128 NodeKind::MapLiteral { entries } => {
4129 self.buf.push_str("new Map([");
4130 for (i, entry) in entries.iter().enumerate() {
4131 if i > 0 {
4132 self.buf.push_str(", ");
4133 }
4134 self.buf.push('[');
4135 self.emit_expr(&entry.key)?;
4136 self.buf.push_str(", ");
4137 self.emit_expr(&entry.value)?;
4138 self.buf.push(']');
4139 }
4140 self.buf.push_str("])");
4141 Ok(())
4142 }
4143 NodeKind::SetLiteral { elems } => {
4144 self.buf.push_str("new Set([");
4145 for (i, e) in elems.iter().enumerate() {
4146 if i > 0 {
4147 self.buf.push_str(", ");
4148 }
4149 self.emit_expr(e)?;
4150 }
4151 self.buf.push_str("])");
4152 Ok(())
4153 }
4154 NodeKind::TupleLiteral { elems } => {
4155 self.buf.push('[');
4157 for (i, e) in elems.iter().enumerate() {
4158 if i > 0 {
4159 self.buf.push_str(", ");
4160 }
4161 self.emit_expr(e)?;
4162 }
4163 self.buf.push(']');
4164 Ok(())
4165 }
4166 NodeKind::Interpolation { parts } => {
4167 self.buf.push('`');
4168 for part in parts {
4169 match part {
4170 AirInterpolationPart::Literal(s) => {
4171 self.buf.push_str(&escape_template_literal(s));
4172 }
4173 AirInterpolationPart::Expr(expr) => {
4174 self.needs_runtime_str = true;
4181 self.buf.push_str("${__bockStr(");
4182 self.emit_expr(expr)?;
4183 self.buf.push_str(")}");
4184 }
4185 }
4186 }
4187 self.buf.push('`');
4188 Ok(())
4189 }
4190 NodeKind::Placeholder => {
4191 self.buf.push('_');
4192 Ok(())
4193 }
4194 NodeKind::Unreachable => {
4195 self.buf
4196 .push_str("(() => { throw new Error(\"unreachable\"); })()");
4197 Ok(())
4198 }
4199 NodeKind::ResultConstruct { variant, value } => {
4200 let tag = match variant {
4205 ResultVariant::Ok => "Ok",
4206 ResultVariant::Err => "Err",
4207 };
4208 let _ = write!(self.buf, "{{ _tag: \"{tag}\", _0: ");
4209 if let Some(v) = value {
4210 self.emit_expr(v)?;
4211 } else {
4212 self.buf.push_str("undefined");
4213 }
4214 self.buf.push_str(" }");
4215 Ok(())
4216 }
4217 NodeKind::Assign { op, target, value } => {
4218 self.emit_expr(target)?;
4219 let op_str = match op {
4220 AssignOp::Assign => " = ",
4221 AssignOp::AddAssign => " += ",
4222 AssignOp::SubAssign => " -= ",
4223 AssignOp::MulAssign => " *= ",
4224 AssignOp::DivAssign => " /= ",
4225 AssignOp::RemAssign => " %= ",
4226 };
4227 self.buf.push_str(op_str);
4228 self.emit_expr(value)?;
4229 Ok(())
4230 }
4231 NodeKind::If {
4232 condition,
4233 then_block,
4234 else_block,
4235 ..
4236 } => {
4237 self.buf.push('(');
4239 self.emit_expr(condition)?;
4240 self.buf.push_str(" ? ");
4241 self.emit_block_as_expr(then_block)?;
4242 self.buf.push_str(" : ");
4243 if let Some(eb) = else_block {
4244 self.emit_block_as_expr(eb)?;
4245 } else {
4246 self.buf.push_str("undefined");
4247 }
4248 self.buf.push(')');
4249 Ok(())
4250 }
4251 NodeKind::Block { stmts, tail } => {
4252 self.buf.push_str("(() => {\n");
4258 self.indent += 1;
4259 self.enter_let_scope(node);
4260 for s in stmts {
4261 self.emit_node(s)?;
4262 }
4263 if let Some(t) = tail {
4264 let ind = self.indent_str();
4265 let _ = write!(self.buf, "{ind}return ");
4266 self.emit_expr(t)?;
4267 self.buf.push_str(";\n");
4268 }
4269 self.leave_let_scope();
4270 self.indent -= 1;
4271 self.write_indent();
4272 self.buf.push_str("})()");
4273 Ok(())
4274 }
4275 NodeKind::Match { scrutinee, arms } => {
4276 self.buf.push_str("(() => {\n");
4282 self.indent += 1;
4283 let prev = std::mem::replace(&mut self.discard_tail, false);
4284 let r = self.emit_match(scrutinee, arms);
4285 self.discard_tail = prev;
4286 r?;
4287 self.indent -= 1;
4288 self.write_indent();
4289 self.buf.push_str("})()");
4290 Ok(())
4291 }
4292 NodeKind::Move { expr }
4294 | NodeKind::Borrow { expr }
4295 | NodeKind::MutableBorrow { expr } => self.emit_expr(expr),
4296 NodeKind::EffectOp {
4298 effect,
4299 operation,
4300 args,
4301 } => {
4302 let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
4303 let _ = write!(
4304 self.buf,
4305 "{}.{}",
4306 to_camel_case(effect_name),
4307 operation.name
4308 );
4309 self.buf.push('(');
4310 for (i, arg) in args.iter().enumerate() {
4311 if i > 0 {
4312 self.buf.push_str(", ");
4313 }
4314 self.emit_expr(&arg.value)?;
4315 }
4316 self.buf.push(')');
4317 Ok(())
4318 }
4319 NodeKind::TypeNamed { .. }
4321 | NodeKind::TypeTuple { .. }
4322 | NodeKind::TypeFunction { .. }
4323 | NodeKind::TypeOptional { .. }
4324 | NodeKind::TypeSelf => {
4325 self.buf.push_str("/* type */");
4326 Ok(())
4327 }
4328 NodeKind::EffectRef { path } => {
4330 let name = path
4331 .segments
4332 .iter()
4333 .map(|s| s.name.as_str())
4334 .collect::<Vec<_>>()
4335 .join(".");
4336 self.buf.push_str(&name);
4337 Ok(())
4338 }
4339 NodeKind::Error => {
4341 self.buf.push_str("/* error */");
4342 Ok(())
4343 }
4344 _ => {
4346 self.buf.push_str("/* unsupported */");
4347 Ok(())
4348 }
4349 }
4350 }
4351
4352 fn emit_loop_label_prefix(&mut self, body: &AIRNode) {
4358 if crate::generator::loop_needs_break_label(body) {
4359 self.loop_label_counter += 1;
4360 let label = format!("__bockLoop{}", self.loop_label_counter);
4361 self.writeln(&format!("{label}:"));
4362 self.loop_labels.push(Some(label));
4363 } else {
4364 self.loop_labels.push(None);
4365 }
4366 }
4367
4368 fn innermost_loop_label(&self) -> Option<&str> {
4370 self.loop_labels.last().and_then(|l| l.as_deref())
4371 }
4372
4373 fn emit_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<(), CodegenError> {
4374 if crate::generator::match_needs_ifchain(arms) || match_has_unswitchable_pattern(arms) {
4383 return self.emit_match_ifchain(scrutinee, arms);
4391 }
4392
4393 let is_adt = arms.iter().any(|arm| {
4400 let NodeKind::MatchArm { pattern, .. } = &arm.kind else {
4401 return false;
4402 };
4403 match &pattern.kind {
4404 NodeKind::ConstructorPat { .. } => true,
4405 NodeKind::RecordPat { path, .. } => self.user_variant_for_path(path).is_some(),
4406 _ => false,
4407 }
4408 });
4409
4410 let temp = if matches!(scrutinee.kind, NodeKind::Identifier { .. }) {
4415 None
4416 } else {
4417 self.match_temp_counter += 1;
4418 let name = format!("__match{}", self.match_temp_counter);
4419 let ind = self.indent_str();
4420 let _ = write!(self.buf, "{ind}const {name} = ");
4421 self.emit_expr(scrutinee)?;
4422 self.buf.push_str(";\n");
4423 Some(name)
4424 };
4425
4426 let ind = self.indent_str();
4427 let _ = write!(self.buf, "{ind}switch (");
4428 self.emit_scrutinee_ref(scrutinee, temp.as_deref())?;
4429 if is_adt {
4430 self.buf.push_str("._tag) {\n");
4431 } else {
4432 self.buf.push_str(") {\n");
4433 }
4434 self.indent += 1;
4435 self.switch_label_depth += 1;
4436 for arm in arms {
4437 self.emit_match_arm(arm, is_adt, scrutinee, temp.as_deref())?;
4438 }
4439 self.switch_label_depth -= 1;
4440 self.indent -= 1;
4441 self.writeln("}");
4442 Ok(())
4443 }
4444
4445 fn emit_scrutinee_ref(
4448 &mut self,
4449 scrutinee: &AIRNode,
4450 temp: Option<&str>,
4451 ) -> Result<(), CodegenError> {
4452 match temp {
4453 Some(name) => {
4454 self.buf.push_str(name);
4455 Ok(())
4456 }
4457 None => self.emit_expr(scrutinee),
4458 }
4459 }
4460
4461 fn emit_match_arm(
4462 &mut self,
4463 arm: &AIRNode,
4464 is_adt: bool,
4465 scrutinee: &AIRNode,
4466 temp: Option<&str>,
4467 ) -> Result<(), CodegenError> {
4468 if let NodeKind::MatchArm {
4469 pattern,
4470 guard,
4471 body,
4472 } = &arm.kind
4473 {
4474 match &pattern.kind {
4475 NodeKind::WildcardPat => {
4476 self.writeln("default: {");
4477 }
4478 NodeKind::BindPat { name, is_mut } if !is_adt => {
4479 self.writeln("default: {");
4483 self.indent += 1;
4484 let kw = if *is_mut { "let" } else { "const" };
4485 let ind = self.indent_str();
4486 let _ = write!(self.buf, "{ind}{kw} {} = ", js_value_ident(&name.name));
4487 self.emit_scrutinee_ref(scrutinee, temp)?;
4488 self.buf.push_str(";\n");
4489 self.indent -= 1;
4490 }
4491 NodeKind::LiteralPat { lit } => {
4492 let ind = self.indent_str();
4493 let _ = write!(self.buf, "{ind}case ");
4494 match lit {
4495 Literal::Int(s) => self.buf.push_str(s),
4496 Literal::Float(s) => self.buf.push_str(s),
4497 Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
4498 Literal::Char(s) => {
4499 self.buf.push('\'');
4500 self.buf.push_str(s);
4501 self.buf.push('\'');
4502 }
4503 Literal::String(s) => {
4504 self.buf.push('"');
4505 self.buf.push_str(&escape_js_string(s));
4506 self.buf.push('"');
4507 }
4508 Literal::Unit => self.buf.push_str("undefined"),
4509 }
4510 self.buf.push_str(": {\n");
4511 }
4512 NodeKind::ConstructorPat { path, fields } => {
4513 let variant_name = path.segments.last().map_or("_", |s| s.name.as_str());
4514 self.writeln(&format!("case \"{variant_name}\": {{"));
4515 if !fields.is_empty() {
4517 self.indent += 1;
4518 for (i, field) in fields.iter().enumerate() {
4519 let binding = self.pattern_to_binding_name(field);
4520 let ind = self.indent_str();
4521 let _ = write!(self.buf, "{ind}const {binding} = ");
4522 self.emit_scrutinee_ref(scrutinee, temp)?;
4523 let _ = writeln!(self.buf, "._{i};");
4524 }
4525 self.indent -= 1;
4526 }
4527 }
4528 NodeKind::RecordPat { path, fields, .. } => {
4529 let variant_name = path.segments.last().map_or("_", |s| s.name.as_str());
4530 if is_adt {
4531 self.writeln(&format!("case \"{variant_name}\": {{"));
4532 } else {
4533 self.writeln("default: {");
4534 }
4535 if !fields.is_empty() {
4536 self.indent += 1;
4537 for f in fields {
4538 let field_name = &f.name.name;
4539 if let Some(pat) = &f.pattern {
4540 let binding = self.pattern_to_binding_name(pat);
4541 let ind = self.indent_str();
4542 let _ = write!(self.buf, "{ind}const {binding} = ");
4543 self.emit_scrutinee_ref(scrutinee, temp)?;
4544 let _ = writeln!(self.buf, ".{field_name};");
4545 } else {
4546 let ind = self.indent_str();
4547 let _ = write!(self.buf, "{ind}const {field_name} = ");
4548 self.emit_scrutinee_ref(scrutinee, temp)?;
4549 let _ = writeln!(self.buf, ".{field_name};");
4550 }
4551 }
4552 self.indent -= 1;
4553 }
4554 }
4555 _ => {
4556 self.writeln("default: {");
4558 }
4559 }
4560
4561 self.indent += 1;
4562 if let Some(g) = guard {
4563 let ind = self.indent_str();
4564 let _ = write!(self.buf, "{ind}if (!(");
4565 self.emit_expr(g)?;
4566 self.buf.push_str(")) break;\n");
4567 }
4568 self.emit_block_body(body)?;
4569 self.writeln("break;");
4570 self.indent -= 1;
4571 self.writeln("}");
4572 }
4573 Ok(())
4574 }
4575
4576 fn emit_match_ifchain(
4590 &mut self,
4591 scrutinee: &AIRNode,
4592 arms: &[AIRNode],
4593 ) -> Result<(), CodegenError> {
4594 let root: String = if let NodeKind::Identifier { name } = &scrutinee.kind {
4597 name.name.clone()
4598 } else {
4599 self.match_temp_counter += 1;
4600 let name = format!("__match{}", self.match_temp_counter);
4601 let ind = self.indent_str();
4602 let _ = write!(self.buf, "{ind}const {name} = ");
4603 self.emit_expr(scrutinee)?;
4604 self.buf.push_str(";\n");
4605 name
4606 };
4607
4608 let mut first = true;
4609 let mut closed = false; let arm_count = arms.len();
4611 for (idx, arm) in arms.iter().enumerate() {
4612 let NodeKind::MatchArm {
4613 pattern,
4614 guard,
4615 body,
4616 } = &arm.kind
4617 else {
4618 continue;
4619 };
4620 let test = self.pattern_test_js(pattern, &root);
4621 let is_catch_all = matches!(
4622 pattern.kind,
4623 NodeKind::WildcardPat | NodeKind::BindPat { .. }
4624 );
4625 let is_last = idx + 1 == arm_count;
4626 let unconditional = guard.is_none() && (is_catch_all || is_last);
4632 let ind = self.indent_str();
4633 if unconditional {
4634 if first {
4635 let _ = writeln!(self.buf, "{ind}{{");
4637 } else {
4638 let _ = writeln!(self.buf, "{ind}else {{");
4639 }
4640 closed = true;
4641 } else {
4642 let mut cond = if test.is_empty() {
4643 "true".to_string()
4644 } else {
4645 test
4646 };
4647 if let Some(g) = guard {
4648 let g_str = self.expr_to_string(g)?;
4656 let binds = self.pattern_binds_to_string_js(pattern, &root);
4657 let guard_test = if binds.is_empty() {
4658 format!("({g_str})")
4659 } else {
4660 format!("(() => {{ {binds}return ({g_str}); }})()")
4661 };
4662 if cond == "true" {
4663 cond = guard_test;
4664 } else {
4665 cond = format!("{cond} && {guard_test}");
4666 }
4667 }
4668 if first {
4669 let _ = writeln!(self.buf, "{ind}if ({cond}) {{");
4670 } else {
4671 let _ = writeln!(self.buf, "{ind}else if ({cond}) {{");
4672 }
4673 }
4674 first = false;
4675 self.indent += 1;
4676 self.pattern_binds_js(pattern, &root)?;
4677 self.emit_block_body(body)?;
4678 self.indent -= 1;
4679 self.writeln("}");
4680 }
4681 if !closed && !first {
4685 self.writeln("else { throw new Error(\"non-exhaustive match\"); }");
4686 }
4687 Ok(())
4688 }
4689
4690 fn pattern_test_js(&self, pat: &AIRNode, access: &str) -> String {
4694 match &pat.kind {
4695 NodeKind::WildcardPat | NodeKind::BindPat { .. } => String::new(),
4696 NodeKind::LiteralPat { lit } => {
4697 format!("{access} === {}", js_literal(lit))
4698 }
4699 NodeKind::ConstructorPat { path, fields } => {
4700 let variant = path.segments.last().map_or("_", |s| s.name.as_str());
4701 let mut tests = vec![format!("{access}._tag === \"{variant}\"")];
4702 for (i, field) in fields.iter().enumerate() {
4703 let sub = self.pattern_test_js(field, &format!("{access}._{i}"));
4704 if !sub.is_empty() {
4705 tests.push(sub);
4706 }
4707 }
4708 tests.join(" && ")
4709 }
4710 NodeKind::RecordPat { path, fields, .. } => {
4711 let variant = path.segments.last().map_or("_", |s| s.name.as_str());
4712 let mut tests = Vec::new();
4716 if self.user_variant_for_path(path).is_some() {
4717 tests.push(format!("{access}._tag === \"{variant}\""));
4718 }
4719 for f in fields {
4720 if let Some(p) = &f.pattern {
4721 let sub = self.pattern_test_js(p, &format!("{access}.{}", f.name.name));
4722 if !sub.is_empty() {
4723 tests.push(sub);
4724 }
4725 }
4726 }
4727 if tests.is_empty() {
4728 String::new()
4729 } else {
4730 tests.join(" && ")
4731 }
4732 }
4733 NodeKind::TuplePat { elems } => {
4734 let mut tests = vec![format!("Array.isArray({access})")];
4735 for (i, e) in elems.iter().enumerate() {
4736 let sub = self.pattern_test_js(e, &format!("{access}[{i}]"));
4737 if !sub.is_empty() {
4738 tests.push(sub);
4739 }
4740 }
4741 tests.join(" && ")
4742 }
4743 NodeKind::ListPat { elems, rest } => {
4744 let n = elems.len();
4748 let len_test = if rest.is_some() {
4749 format!("{access}.length >= {n}")
4750 } else {
4751 format!("{access}.length === {n}")
4752 };
4753 let mut tests = vec![format!("Array.isArray({access})"), len_test];
4754 for (i, e) in elems.iter().enumerate() {
4755 let sub = self.pattern_test_js(e, &format!("{access}[{i}]"));
4756 if !sub.is_empty() {
4757 tests.push(sub);
4758 }
4759 }
4760 tests.join(" && ")
4761 }
4762 NodeKind::RangePat { lo, hi, inclusive } => {
4763 let lo_s = range_bound_to_js(lo);
4765 let hi_s = range_bound_to_js(hi);
4766 let upper = if *inclusive { "<=" } else { "<" };
4767 format!("{access} >= {lo_s} && {access} {upper} {hi_s}")
4768 }
4769 NodeKind::OrPat { alternatives } => {
4770 let alts: Vec<String> = alternatives
4771 .iter()
4772 .map(|a| {
4773 let t = self.pattern_test_js(a, access);
4774 if t.is_empty() {
4775 "true".to_string()
4776 } else {
4777 format!("({t})")
4778 }
4779 })
4780 .collect();
4781 alts.join(" || ")
4782 }
4783 _ => String::new(),
4784 }
4785 }
4786
4787 fn pattern_binds_js(&mut self, pat: &AIRNode, access: &str) -> Result<(), CodegenError> {
4792 match &pat.kind {
4793 NodeKind::BindPat { name, .. } => {
4794 let js = js_value_ident(&name.name);
4795 if js != access {
4800 let ind = self.indent_str();
4801 let _ = writeln!(self.buf, "{ind}const {js} = {access};");
4802 }
4803 }
4804 NodeKind::ConstructorPat { fields, .. } => {
4805 for (i, field) in fields.iter().enumerate() {
4806 self.pattern_binds_js(field, &format!("{access}._{i}"))?;
4807 }
4808 }
4809 NodeKind::RecordPat { fields, .. } => {
4810 for f in fields {
4811 let field_access = format!("{access}.{}", f.name.name);
4812 match &f.pattern {
4813 Some(p) => self.pattern_binds_js(p, &field_access)?,
4814 None => {
4816 let ind = self.indent_str();
4817 let _ =
4818 writeln!(self.buf, "{ind}const {} = {field_access};", f.name.name);
4819 }
4820 }
4821 }
4822 }
4823 NodeKind::TuplePat { elems } => {
4824 for (i, e) in elems.iter().enumerate() {
4825 self.pattern_binds_js(e, &format!("{access}[{i}]"))?;
4826 }
4827 }
4828 NodeKind::ListPat { elems, rest } => {
4829 for (i, e) in elems.iter().enumerate() {
4830 self.pattern_binds_js(e, &format!("{access}[{i}]"))?;
4831 }
4832 if let Some(r) = rest {
4835 if let NodeKind::BindPat { name, .. } = &r.kind {
4836 let ind = self.indent_str();
4837 let _ = writeln!(
4838 self.buf,
4839 "{ind}const {} = {access}.slice({});",
4840 js_value_ident(&name.name),
4841 elems.len()
4842 );
4843 }
4844 }
4845 }
4846 NodeKind::OrPat { alternatives } => {
4847 if let Some(first) = alternatives.first() {
4848 self.pattern_binds_js(first, access)?;
4849 }
4850 }
4851 _ => {}
4853 }
4854 Ok(())
4855 }
4856
4857 fn pattern_binds_to_string_js(&self, pat: &AIRNode, access: &str) -> String {
4861 let mut out = String::new();
4862 self.collect_binds_js(pat, access, &mut out);
4863 out
4864 }
4865
4866 fn collect_binds_js(&self, pat: &AIRNode, access: &str, out: &mut String) {
4867 match &pat.kind {
4868 NodeKind::BindPat { name, .. } => {
4869 let js = js_value_ident(&name.name);
4870 if js != access {
4873 let _ = write!(out, "const {js} = {access}; ");
4874 }
4875 }
4876 NodeKind::ConstructorPat { fields, .. } => {
4877 for (i, field) in fields.iter().enumerate() {
4878 self.collect_binds_js(field, &format!("{access}._{i}"), out);
4879 }
4880 }
4881 NodeKind::RecordPat { fields, .. } => {
4882 for f in fields {
4883 let field_access = format!("{access}.{}", f.name.name);
4884 match &f.pattern {
4885 Some(p) => self.collect_binds_js(p, &field_access, out),
4886 None => {
4887 let _ = write!(out, "const {} = {field_access}; ", f.name.name);
4888 }
4889 }
4890 }
4891 }
4892 NodeKind::TuplePat { elems } => {
4893 for (i, e) in elems.iter().enumerate() {
4894 self.collect_binds_js(e, &format!("{access}[{i}]"), out);
4895 }
4896 }
4897 NodeKind::ListPat { elems, rest } => {
4898 for (i, e) in elems.iter().enumerate() {
4899 self.collect_binds_js(e, &format!("{access}[{i}]"), out);
4900 }
4901 if let Some(r) = rest {
4902 if let NodeKind::BindPat { name, .. } = &r.kind {
4903 let _ = write!(
4904 out,
4905 "const {} = {access}.slice({}); ",
4906 js_value_ident(&name.name),
4907 elems.len()
4908 );
4909 }
4910 }
4911 }
4912 NodeKind::OrPat { alternatives } => {
4913 if let Some(first) = alternatives.first() {
4914 self.collect_binds_js(first, access, out);
4915 }
4916 }
4917 _ => {}
4918 }
4919 }
4920
4921 fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
4924 if let NodeKind::Call { callee, args, .. } = &right.kind {
4927 let has_placeholder = args
4928 .iter()
4929 .any(|a| matches!(a.value.kind, NodeKind::Placeholder));
4930 if has_placeholder {
4931 self.emit_expr(callee)?;
4932 self.buf.push('(');
4933 for (i, arg) in args.iter().enumerate() {
4934 if i > 0 {
4935 self.buf.push_str(", ");
4936 }
4937 if matches!(arg.value.kind, NodeKind::Placeholder) {
4938 self.emit_expr(left)?;
4939 } else {
4940 self.emit_expr(&arg.value)?;
4941 }
4942 }
4943 self.buf.push(')');
4944 return Ok(());
4945 }
4946 }
4947 self.emit_callee(right)?;
4951 self.buf.push('(');
4952 self.emit_expr(left)?;
4953 self.buf.push(')');
4954 Ok(())
4955 }
4956
4957 fn emit_callee(&mut self, callee: &AIRNode) -> Result<(), CodegenError> {
4969 if matches!(
4970 callee.kind,
4971 NodeKind::Lambda { .. } | NodeKind::Compose { .. }
4972 ) {
4973 self.buf.push('(');
4974 self.emit_expr(callee)?;
4975 self.buf.push(')');
4976 Ok(())
4977 } else {
4978 self.emit_expr(callee)
4979 }
4980 }
4981
4982 fn simple_let_redeclared(&self, js_name: &str) -> bool {
4988 self.let_scopes
4989 .last()
4990 .is_some_and(|s| s.declared.contains(js_name))
4991 }
4992
4993 fn simple_let_needs_let(&self, js_name: &str) -> bool {
4996 self.let_scopes
4997 .last()
4998 .is_some_and(|s| s.needs_let.contains(js_name))
4999 }
5000
5001 fn mark_simple_let_declared(&mut self, js_name: &str) {
5003 if let Some(s) = self.let_scopes.last_mut() {
5004 s.declared.insert(js_name.to_string());
5005 }
5006 }
5007
5008 fn enter_let_scope(&mut self, block: &AIRNode) {
5015 let mut needs_let = HashSet::new();
5016 if let NodeKind::Block { stmts, tail } = &block.kind {
5017 let mut seen: HashSet<String> = HashSet::new();
5018 let mut visit = |n: &AIRNode, needs_let: &mut HashSet<String>| {
5019 match &n.kind {
5020 NodeKind::LetBinding { pattern, .. } => {
5021 if let NodeKind::BindPat { name, .. } = &pattern.kind {
5022 let js = js_value_ident(&name.name);
5023 if !seen.insert(js.clone()) {
5025 needs_let.insert(js);
5026 }
5027 }
5028 }
5029 NodeKind::Assign { target, .. } => {
5030 if let NodeKind::Identifier { name } = &target.kind {
5031 needs_let.insert(js_value_ident(&name.name));
5032 }
5033 }
5034 _ => {}
5035 }
5036 };
5037 for s in stmts {
5038 visit(s, &mut needs_let);
5039 }
5040 if let Some(t) = tail {
5041 visit(t, &mut needs_let);
5042 }
5043 }
5044 self.let_scopes.push(LetScope {
5045 declared: HashSet::new(),
5046 needs_let,
5047 });
5048 }
5049
5050 fn leave_let_scope(&mut self) {
5052 self.let_scopes.pop();
5053 }
5054
5055 fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
5056 self.enter_let_scope(node);
5057 let r = self.emit_block_body_inner(node);
5058 self.leave_let_scope();
5059 r
5060 }
5061
5062 fn emit_loop_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
5074 let prev = std::mem::replace(&mut self.discard_tail, true);
5075 let r = self.emit_block_body(node);
5076 self.discard_tail = prev;
5077 r
5078 }
5079
5080 fn hoist_propagates(&mut self, stmt: &AIRNode) -> Result<bool, CodegenError> {
5106 let mut found: Vec<&AIRNode> = Vec::new();
5107 collect_propagates_in_expr(stmt, &mut found);
5108 let any = !found.is_empty();
5109 for prop in found {
5110 if let NodeKind::Propagate { expr } = &prop.kind {
5111 let n = self.propagate_temp_counter;
5112 self.propagate_temp_counter += 1;
5113 let tmp = format!("__try{n}");
5114 let ind = self.indent_str();
5115 let _ = write!(self.buf, "{ind}const {tmp} = ");
5116 self.emit_expr(expr)?;
5119 self.buf.push_str(";\n");
5120 let _ = writeln!(
5121 self.buf,
5122 "{ind}if ({tmp}._tag === \"Err\" || {tmp}._tag === \"None\") return {tmp};"
5123 );
5124 self.propagate_temps
5125 .insert(prop as *const AIRNode as usize, tmp);
5126 }
5127 }
5128 Ok(any)
5129 }
5130
5131 fn emit_fn_body_seeded(
5138 &mut self,
5139 params: &[AIRNode],
5140 body: &AIRNode,
5141 ) -> Result<(), CodegenError> {
5142 self.enter_let_scope(body);
5143 if let Some(scope) = self.let_scopes.last_mut() {
5144 for p in params {
5145 if let NodeKind::Param { pattern, .. } = &p.kind {
5146 if let NodeKind::BindPat { name, .. } = &pattern.kind {
5147 let js = js_value_ident(&name.name);
5148 scope.needs_let.insert(js.clone());
5149 scope.declared.insert(js);
5150 }
5151 }
5152 }
5153 }
5154 let r = self.emit_block_body_inner(body);
5155 self.leave_let_scope();
5156 r
5157 }
5158
5159 fn emit_block_body_inner(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
5160 if let NodeKind::Block { stmts, tail } = &node.kind {
5161 let prev_discard = std::mem::replace(&mut self.discard_tail, true);
5171 let mut stmt_res = Ok(());
5172 for s in stmts {
5173 stmt_res = self.emit_node(s);
5174 if stmt_res.is_err() {
5175 break;
5176 }
5177 }
5178 self.discard_tail = prev_discard;
5179 stmt_res?;
5180 if let Some(t) = tail {
5181 if crate::generator::node_is_statement(t) {
5184 self.emit_node(t)?;
5185 return Ok(());
5186 }
5187 if tail_is_statement_form(t) {
5194 self.emit_node(t)?;
5195 return Ok(());
5196 }
5197 if let NodeKind::Match { scrutinee, arms } = &t.kind {
5200 if crate::generator::match_has_statement_arm(arms) {
5201 self.emit_match(scrutinee, arms)?;
5202 return Ok(());
5203 }
5204 }
5205 if js_call_is_diverging(t) {
5209 self.write_indent();
5210 self.emit_expr(t)?;
5211 self.buf.push_str(";\n");
5212 return Ok(());
5213 }
5214 self.hoist_propagates(t)?;
5217 self.emit_tail_value(t)?;
5218 }
5219 } else if crate::generator::node_is_statement(node) || tail_is_statement_form(node) {
5220 self.emit_node(node)?;
5221 } else if js_call_is_diverging(node) {
5222 self.write_indent();
5223 self.emit_expr(node)?;
5224 self.buf.push_str(";\n");
5225 } else if let NodeKind::Match { scrutinee, arms } = &node.kind {
5226 if crate::generator::match_has_statement_arm(arms) {
5227 self.emit_match(scrutinee, arms)?;
5228 } else {
5229 self.hoist_propagates(node)?;
5230 self.emit_tail_value(node)?;
5231 }
5232 } else {
5233 self.hoist_propagates(node)?;
5235 self.emit_tail_value(node)?;
5236 }
5237 Ok(())
5238 }
5239
5240 fn emit_tail_value(&mut self, value: &AIRNode) -> Result<(), CodegenError> {
5249 let ind = self.indent_str();
5250 if self.discard_tail {
5251 self.buf.push_str(&ind);
5252 self.emit_expr(value)?;
5253 self.buf.push_str(";\n");
5254 } else {
5255 let _ = write!(self.buf, "{ind}return ");
5256 self.emit_expr(value)?;
5257 self.buf.push_str(";\n");
5258 }
5259 Ok(())
5260 }
5261
5262 fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
5263 if let NodeKind::Block { stmts, tail } = &node.kind {
5264 if stmts.is_empty() {
5265 if let Some(t) = tail {
5266 return self.emit_expr(t);
5267 }
5268 }
5269 }
5270 self.emit_expr(node)
5272 }
5273
5274 fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
5275 match &pat.kind {
5276 NodeKind::BindPat { name, .. } => js_value_ident(&name.name),
5277 NodeKind::WildcardPat => "_".into(),
5278 NodeKind::TuplePat { elems } => {
5279 format!(
5280 "[{}]",
5281 elems
5282 .iter()
5283 .map(|e| self.pattern_to_binding_name(e))
5284 .collect::<Vec<_>>()
5285 .join(", ")
5286 )
5287 }
5288 NodeKind::RecordPat { fields, .. } => {
5289 format!(
5290 "{{ {} }}",
5291 fields
5292 .iter()
5293 .map(|f| to_camel_case(&f.name.name).to_string())
5294 .collect::<Vec<_>>()
5295 .join(", ")
5296 )
5297 }
5298 _ => "_".into(),
5299 }
5300 }
5301
5302 fn pattern_to_js_destructure(&self, pat: &AIRNode) -> String {
5303 self.pattern_to_binding_name(pat)
5304 }
5305
5306 fn type_expr_to_string(&self, node: &AIRNode) -> String {
5307 match &node.kind {
5308 NodeKind::TypeNamed { path, .. } => path
5309 .segments
5310 .iter()
5311 .map(|s| s.name.as_str())
5312 .collect::<Vec<_>>()
5313 .join("."),
5314 NodeKind::Identifier { name } => name.name.clone(),
5315 _ => "Unknown".into(),
5316 }
5317 }
5318}
5319
5320fn is_time_method_name(name: &str) -> bool {
5325 matches!(
5326 name,
5327 "as_nanos"
5328 | "as_millis"
5329 | "as_seconds"
5330 | "is_zero"
5331 | "is_negative"
5332 | "abs"
5333 | "elapsed"
5334 | "duration_since"
5335 )
5336}
5337
5338fn tail_is_statement_form(node: &AIRNode) -> bool {
5356 matches!(
5357 node.kind,
5358 NodeKind::Loop { .. }
5359 | NodeKind::While { .. }
5360 | NodeKind::For { .. }
5361 | NodeKind::Guard { .. }
5362 | NodeKind::HandlingBlock { .. }
5363 )
5364}
5365
5366fn collect_propagates_in_expr<'a>(node: &'a AIRNode, out: &mut Vec<&'a AIRNode>) {
5381 match &node.kind {
5382 NodeKind::Propagate { expr } => {
5383 collect_propagates_in_expr(expr, out);
5385 out.push(node);
5386 }
5387 NodeKind::Call { callee, args, .. } => {
5388 collect_propagates_in_expr(callee, out);
5389 for a in args {
5390 collect_propagates_in_expr(&a.value, out);
5391 }
5392 }
5393 NodeKind::MethodCall { receiver, args, .. } => {
5394 collect_propagates_in_expr(receiver, out);
5395 for a in args {
5396 collect_propagates_in_expr(&a.value, out);
5397 }
5398 }
5399 NodeKind::BinaryOp { op, left, right } => {
5400 collect_propagates_in_expr(left, out);
5401 if !matches!(op, BinOp::And | BinOp::Or) {
5404 collect_propagates_in_expr(right, out);
5405 }
5406 }
5407 NodeKind::UnaryOp { operand, .. } => collect_propagates_in_expr(operand, out),
5408 NodeKind::FieldAccess { object, .. } => collect_propagates_in_expr(object, out),
5409 NodeKind::Index { object, index } => {
5410 collect_propagates_in_expr(object, out);
5411 collect_propagates_in_expr(index, out);
5412 }
5413 NodeKind::Await { expr } => collect_propagates_in_expr(expr, out),
5414 NodeKind::Pipe { left, right } | NodeKind::Compose { left, right } => {
5415 collect_propagates_in_expr(left, out);
5416 collect_propagates_in_expr(right, out);
5417 }
5418 NodeKind::Range { lo, hi, .. } => {
5419 collect_propagates_in_expr(lo, out);
5420 collect_propagates_in_expr(hi, out);
5421 }
5422 NodeKind::RecordConstruct { fields, spread, .. } => {
5423 for f in fields {
5424 if let Some(v) = &f.value {
5425 collect_propagates_in_expr(v, out);
5426 }
5427 }
5428 if let Some(s) = spread {
5429 collect_propagates_in_expr(s, out);
5430 }
5431 }
5432 NodeKind::ListLiteral { elems }
5433 | NodeKind::SetLiteral { elems }
5434 | NodeKind::TupleLiteral { elems } => {
5435 for e in elems {
5436 collect_propagates_in_expr(e, out);
5437 }
5438 }
5439 NodeKind::MapLiteral { entries } => {
5440 for e in entries {
5441 collect_propagates_in_expr(&e.key, out);
5442 collect_propagates_in_expr(&e.value, out);
5443 }
5444 }
5445 NodeKind::Interpolation { parts } => {
5446 for p in parts {
5447 if let AirInterpolationPart::Expr(e) = p {
5448 collect_propagates_in_expr(e, out);
5449 }
5450 }
5451 }
5452 NodeKind::Assign { value, .. } => collect_propagates_in_expr(value, out),
5453 NodeKind::LetBinding { value, .. } => collect_propagates_in_expr(value, out),
5454 NodeKind::Return { value: Some(v) } => collect_propagates_in_expr(v, out),
5455 _ => {}
5458 }
5459}
5460
5461fn js_call_is_diverging(node: &AIRNode) -> bool {
5466 if let NodeKind::Call { callee, .. } = &node.kind {
5467 if let NodeKind::Identifier { name } = &callee.kind {
5468 return matches!(name.name.as_str(), "todo" | "unreachable");
5469 }
5470 }
5471 false
5472}
5473
5474fn js_value_ident(name: &str) -> String {
5483 let escaped = crate::generator::escape_target_keyword(
5484 &to_camel_case(name),
5485 crate::generator::KeywordTarget::Js,
5486 );
5487 if matches!(escaped.as_str(), "eval" | "arguments") {
5488 format!("{escaped}_")
5489 } else {
5490 escaped
5491 }
5492}
5493
5494fn match_has_unswitchable_pattern(arms: &[AIRNode]) -> bool {
5506 arms.iter().any(|arm| {
5507 matches!(
5508 &arm.kind,
5509 NodeKind::MatchArm { pattern, .. }
5510 if matches!(pattern.kind, NodeKind::ListPat { .. } | NodeKind::RangePat { .. })
5511 )
5512 })
5513}
5514
5515fn esm_emit_name_static(name: &str, is_fn: bool) -> String {
5516 if is_fn {
5517 js_value_ident(name)
5518 } else {
5519 name.to_string()
5520 }
5521}
5522
5523fn to_camel_case(s: &str) -> String {
5524 if s.is_empty() || s == "_" {
5525 return s.to_string();
5526 }
5527 if !s.contains('_') && s.starts_with(|c: char| c.is_lowercase()) {
5529 return s.to_string();
5530 }
5531 if s.contains('_') {
5533 let parts: Vec<&str> = s.split('_').filter(|p| !p.is_empty()).collect();
5534 if parts.is_empty() {
5535 return s.to_string();
5536 }
5537 let mut result = parts[0].to_lowercase();
5538 for part in &parts[1..] {
5539 let mut chars = part.chars();
5540 if let Some(first) = chars.next() {
5541 result.push(
5542 first
5543 .to_uppercase()
5544 .next()
5545 .expect("uppercase yields at least one char"),
5546 );
5547 result.extend(chars);
5548 }
5549 }
5550 return result;
5551 }
5552 let mut chars = s.chars();
5554 let first = chars.next().expect("non-empty string guaranteed by caller");
5555 let mut result = first.to_lowercase().to_string();
5556 result.extend(chars);
5557 result
5558}
5559
5560fn escape_js_string(s: &str) -> String {
5562 let mut out = String::with_capacity(s.len());
5563 for ch in s.chars() {
5564 match ch {
5565 '"' => out.push_str("\\\""),
5566 '\\' => out.push_str("\\\\"),
5567 '\n' => out.push_str("\\n"),
5568 '\r' => out.push_str("\\r"),
5569 '\t' => out.push_str("\\t"),
5570 _ => out.push(ch),
5571 }
5572 }
5573 out
5574}
5575
5576fn range_bound_to_js(node: &AIRNode) -> String {
5582 match &node.kind {
5583 NodeKind::LiteralPat { lit } => js_literal(lit),
5584 NodeKind::Literal { lit } => js_literal(lit),
5585 NodeKind::Identifier { name } => js_value_ident(&name.name),
5586 _ => "0".to_string(),
5587 }
5588}
5589
5590fn js_literal(lit: &Literal) -> String {
5591 match lit {
5592 Literal::Int(s) | Literal::Float(s) => s.clone(),
5593 Literal::Bool(b) => {
5594 if *b {
5595 "true".to_string()
5596 } else {
5597 "false".to_string()
5598 }
5599 }
5600 Literal::Char(s) => format!("'{s}'"),
5601 Literal::String(s) => format!("\"{}\"", escape_js_string(s)),
5602 Literal::Unit => "undefined".to_string(),
5603 }
5604}
5605
5606fn escape_template_literal(s: &str) -> String {
5608 let mut out = String::with_capacity(s.len());
5609 for ch in s.chars() {
5610 match ch {
5611 '`' => out.push_str("\\`"),
5612 '\\' => out.push_str("\\\\"),
5613 '$' => out.push_str("\\$"),
5614 _ => out.push(ch),
5615 }
5616 }
5617 out
5618}
5619
5620#[cfg(test)]
5623mod tests {
5624 use super::*;
5625 use bock_air::{AirArg, AirRecordField};
5626 use bock_ast::{Ident, TypePath};
5627 use bock_errors::{FileId, Span};
5628
5629 fn span() -> Span {
5630 Span {
5631 file: FileId(0),
5632 start: 0,
5633 end: 0,
5634 }
5635 }
5636
5637 fn ident(name: &str) -> Ident {
5638 Ident {
5639 name: name.to_string(),
5640 span: span(),
5641 }
5642 }
5643
5644 fn type_path(segments: &[&str]) -> TypePath {
5645 TypePath {
5646 segments: segments.iter().map(|s| ident(s)).collect(),
5647 span: span(),
5648 }
5649 }
5650
5651 fn node(id: u32, kind: NodeKind) -> AIRNode {
5652 AIRNode::new(id, span(), kind)
5653 }
5654
5655 fn int_lit(id: u32, val: &str) -> AIRNode {
5656 node(
5657 id,
5658 NodeKind::Literal {
5659 lit: Literal::Int(val.into()),
5660 },
5661 )
5662 }
5663
5664 fn str_lit(id: u32, val: &str) -> AIRNode {
5665 node(
5666 id,
5667 NodeKind::Literal {
5668 lit: Literal::String(val.into()),
5669 },
5670 )
5671 }
5672
5673 fn bool_lit(id: u32, val: bool) -> AIRNode {
5674 node(
5675 id,
5676 NodeKind::Literal {
5677 lit: Literal::Bool(val),
5678 },
5679 )
5680 }
5681
5682 fn id_node(id: u32, name: &str) -> AIRNode {
5683 node(id, NodeKind::Identifier { name: ident(name) })
5684 }
5685
5686 fn bind_pat(id: u32, name: &str) -> AIRNode {
5687 node(
5688 id,
5689 NodeKind::BindPat {
5690 name: ident(name),
5691 is_mut: false,
5692 },
5693 )
5694 }
5695
5696 fn param_node(id: u32, name: &str) -> AIRNode {
5697 node(
5698 id,
5699 NodeKind::Param {
5700 pattern: Box::new(bind_pat(id + 100, name)),
5701 ty: None,
5702 default: None,
5703 },
5704 )
5705 }
5706
5707 fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
5708 node(
5709 id,
5710 NodeKind::Block {
5711 stmts,
5712 tail: tail.map(Box::new),
5713 },
5714 )
5715 }
5716
5717 fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
5718 node(
5719 0,
5720 NodeKind::Module {
5721 path: None,
5722 annotations: vec![],
5723 imports,
5724 items,
5725 },
5726 )
5727 }
5728
5729 fn gen(module: &AIRNode) -> String {
5730 let gen = JsGenerator::new();
5731 let result = gen.generate_module(module).unwrap();
5732 result.files[0].content.clone()
5733 }
5734
5735 #[test]
5738 fn implements_code_generator_trait() {
5739 let gen = JsGenerator::new();
5740 assert_eq!(gen.target().id, "js");
5741 }
5742
5743 #[test]
5744 fn empty_module() {
5745 let m = module(vec![], vec![]);
5746 let out = gen(&m);
5747 assert_eq!(out, "");
5748 }
5749
5750 #[test]
5751 fn simple_function() {
5752 let body = block(2, vec![], Some(int_lit(3, "42")));
5753 let f = node(
5754 1,
5755 NodeKind::FnDecl {
5756 annotations: vec![],
5757 visibility: Visibility::Private,
5758 is_async: false,
5759 name: ident("answer"),
5760 generic_params: vec![],
5761 params: vec![],
5762 return_type: None,
5763 effect_clause: vec![],
5764 where_clause: vec![],
5765 body: Box::new(body),
5766 },
5767 );
5768 let out = gen(&module(vec![], vec![f]));
5769 assert!(out.contains("function answer()"));
5770 assert!(out.contains("return 42;"));
5771 }
5772
5773 fn list_method_call(method: &str, extra: Vec<AIRNode>) -> AIRNode {
5777 let recv = id_node(5, "nums");
5778 let callee = node(
5779 6,
5780 NodeKind::FieldAccess {
5781 object: Box::new(recv.clone()),
5782 field: ident(method),
5783 },
5784 );
5785 let mut args = vec![AirArg {
5786 label: None,
5787 value: recv,
5788 }];
5789 args.extend(extra.into_iter().map(|value| AirArg { label: None, value }));
5790 node(
5791 7,
5792 NodeKind::Call {
5793 callee: Box::new(callee),
5794 args,
5795 type_args: vec![],
5796 },
5797 )
5798 }
5799
5800 #[test]
5801 fn list_len_emits_length_property() {
5802 let body = block(2, vec![], Some(list_method_call("len", vec![])));
5803 let f = node(
5804 1,
5805 NodeKind::FnDecl {
5806 annotations: vec![],
5807 visibility: Visibility::Private,
5808 is_async: false,
5809 name: ident("f"),
5810 generic_params: vec![],
5811 params: vec![],
5812 return_type: None,
5813 effect_clause: vec![],
5814 where_clause: vec![],
5815 body: Box::new(body),
5816 },
5817 );
5818 let out = gen(&module(vec![], vec![f]));
5819 assert!(out.contains("(nums).length"), "got: {out}");
5820 assert!(!out.contains("nums.len("), "got: {out}");
5822 }
5823
5824 #[test]
5825 fn list_get_emits_tagged_optional_with_bounds_check() {
5826 let body = block(
5827 2,
5828 vec![],
5829 Some(list_method_call("get", vec![int_lit(8, "1")])),
5830 );
5831 let f = node(
5832 1,
5833 NodeKind::FnDecl {
5834 annotations: vec![],
5835 visibility: Visibility::Private,
5836 is_async: false,
5837 name: ident("f"),
5838 generic_params: vec![],
5839 params: vec![],
5840 return_type: None,
5841 effect_clause: vec![],
5842 where_clause: vec![],
5843 body: Box::new(body),
5844 },
5845 );
5846 let out = gen(&module(vec![], vec![f]));
5847 assert!(out.contains("_tag: \"Some\""), "got: {out}");
5848 assert!(out.contains("_tag: \"None\""), "got: {out}");
5849 assert!(
5850 out.contains("__i < __r.length"),
5851 "bounds check missing, got: {out}"
5852 );
5853 }
5854
5855 #[test]
5856 fn function_with_params() {
5857 let body = block(
5858 5,
5859 vec![],
5860 Some(node(
5861 6,
5862 NodeKind::BinaryOp {
5863 op: BinOp::Add,
5864 left: Box::new(id_node(7, "a")),
5865 right: Box::new(id_node(8, "b")),
5866 },
5867 )),
5868 );
5869 let f = node(
5870 1,
5871 NodeKind::FnDecl {
5872 annotations: vec![],
5873 visibility: Visibility::Public,
5874 is_async: false,
5875 name: ident("add"),
5876 generic_params: vec![],
5877 params: vec![param_node(2, "a"), param_node(3, "b")],
5878 return_type: None,
5879 effect_clause: vec![],
5880 where_clause: vec![],
5881 body: Box::new(body),
5882 },
5883 );
5884 let out = gen(&module(vec![], vec![f]));
5885 assert!(out.contains("export function add(a, b)"));
5886 assert!(out.contains("(a + b)"));
5887 }
5888
5889 #[test]
5890 fn async_function() {
5891 let body = block(
5892 3,
5893 vec![],
5894 Some(node(
5895 4,
5896 NodeKind::Await {
5897 expr: Box::new(node(
5898 5,
5899 NodeKind::Call {
5900 callee: Box::new(id_node(6, "fetch")),
5901 args: vec![AirArg {
5902 label: None,
5903 value: str_lit(7, "https://example.com"),
5904 }],
5905 type_args: vec![],
5906 },
5907 )),
5908 },
5909 )),
5910 );
5911 let f = node(
5912 1,
5913 NodeKind::FnDecl {
5914 annotations: vec![],
5915 visibility: Visibility::Private,
5916 is_async: true,
5917 name: ident("fetchData"),
5918 generic_params: vec![],
5919 params: vec![],
5920 return_type: None,
5921 effect_clause: vec![],
5922 where_clause: vec![],
5923 body: Box::new(body),
5924 },
5925 );
5926 let out = gen(&module(vec![], vec![f]));
5927 assert!(out.contains("async function fetchData()"));
5928 assert!(out.contains("await fetch"));
5929 }
5930
5931 #[test]
5932 fn effects_as_destructured_params() {
5933 let body = block(
5934 3,
5935 vec![node(
5936 4,
5937 NodeKind::LetBinding {
5938 is_mut: false,
5939 pattern: Box::new(bind_pat(5, "msg")),
5940 ty: None,
5941 value: Box::new(str_lit(6, "hello")),
5942 },
5943 )],
5944 Some(node(
5945 7,
5946 NodeKind::EffectOp {
5947 effect: type_path(&["Log"]),
5948 operation: ident("info"),
5949 args: vec![AirArg {
5950 label: None,
5951 value: id_node(8, "msg"),
5952 }],
5953 },
5954 )),
5955 );
5956 let f = node(
5957 1,
5958 NodeKind::FnDecl {
5959 annotations: vec![],
5960 visibility: Visibility::Private,
5961 is_async: false,
5962 name: ident("process"),
5963 generic_params: vec![],
5964 params: vec![param_node(2, "data")],
5965 return_type: None,
5966 effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
5967 where_clause: vec![],
5968 body: Box::new(body),
5969 },
5970 );
5971 let out = gen(&module(vec![], vec![f]));
5972 assert!(out.contains("function process(data, { log, clock })"));
5973 assert!(out.contains("log.info(msg)"));
5974 }
5975
5976 #[test]
5982 fn clock_time_ops_route_through_handler() {
5983 let out = gen(&module(vec![], vec![clock_timed_fn()]));
5984 assert!(out.contains("clock.now_monotonic()"), "got: {out}");
5985 assert!(out.contains("clock.sleep("), "got: {out}");
5986 assert!(
5987 !out.contains("performance.now()"),
5988 "host clock primitive leaked past the handler: {out}"
5989 );
5990 assert!(
5991 !out.contains("setTimeout"),
5992 "host sleep primitive leaked past the handler: {out}"
5993 );
5994 }
5995
5996 fn clock_timed_fn() -> AIRNode {
6000 let instant_now = node(
6001 40,
6002 NodeKind::Call {
6003 callee: Box::new(node(
6004 41,
6005 NodeKind::FieldAccess {
6006 object: Box::new(id_node(42, "Instant")),
6007 field: ident("now"),
6008 },
6009 )),
6010 args: vec![],
6011 type_args: vec![],
6012 },
6013 );
6014 let duration_millis = node(
6015 50,
6016 NodeKind::Call {
6017 callee: Box::new(node(
6018 51,
6019 NodeKind::FieldAccess {
6020 object: Box::new(id_node(52, "Duration")),
6021 field: ident("millis"),
6022 },
6023 )),
6024 args: vec![AirArg {
6025 label: None,
6026 value: int_lit(53, "1"),
6027 }],
6028 type_args: vec![],
6029 },
6030 );
6031 let sleep_call = node(
6032 60,
6033 NodeKind::Call {
6034 callee: Box::new(id_node(61, "sleep")),
6035 args: vec![AirArg {
6036 label: None,
6037 value: duration_millis,
6038 }],
6039 type_args: vec![],
6040 },
6041 );
6042 let elapsed_call = node(
6043 70,
6044 NodeKind::MethodCall {
6045 receiver: Box::new(id_node(71, "start")),
6046 method: ident("elapsed"),
6047 type_args: vec![],
6048 args: vec![],
6049 },
6050 );
6051 let body = block(
6052 30,
6053 vec![
6054 node(
6055 31,
6056 NodeKind::LetBinding {
6057 is_mut: false,
6058 pattern: Box::new(bind_pat(32, "start")),
6059 ty: None,
6060 value: Box::new(instant_now),
6061 },
6062 ),
6063 sleep_call,
6064 node(
6065 33,
6066 NodeKind::LetBinding {
6067 is_mut: false,
6068 pattern: Box::new(bind_pat(34, "d")),
6069 ty: None,
6070 value: Box::new(elapsed_call),
6071 },
6072 ),
6073 ],
6074 None,
6075 );
6076 node(
6077 1,
6078 NodeKind::FnDecl {
6079 annotations: vec![],
6080 visibility: Visibility::Private,
6081 is_async: false,
6082 name: ident("timed"),
6083 generic_params: vec![],
6084 params: vec![],
6085 return_type: None,
6086 effect_clause: vec![type_path(&["Clock"])],
6087 where_clause: vec![],
6088 body: Box::new(body),
6089 },
6090 )
6091 }
6092
6093 #[test]
6094 fn enum_to_tagged_objects() {
6095 let enum_decl = node(
6096 1,
6097 NodeKind::EnumDecl {
6098 annotations: vec![],
6099 visibility: Visibility::Public,
6100 name: ident("Shape"),
6101 generic_params: vec![],
6102 variants: vec![
6103 node(
6104 2,
6105 NodeKind::EnumVariant {
6106 name: ident("Circle"),
6107 payload: EnumVariantPayload::Struct(vec![bock_ast::RecordDeclField {
6108 id: 0,
6109 span: span(),
6110 name: ident("radius"),
6111 ty: bock_ast::TypeExpr::Named {
6112 id: 0,
6113 span: span(),
6114 path: type_path(&["Float"]),
6115 args: vec![],
6116 },
6117 default: None,
6118 }]),
6119 },
6120 ),
6121 node(
6122 3,
6123 NodeKind::EnumVariant {
6124 name: ident("None"),
6125 payload: EnumVariantPayload::Unit,
6126 },
6127 ),
6128 ],
6129 },
6130 );
6131 let out = gen(&module(vec![], vec![enum_decl]));
6132 assert!(out.contains("function Shape_Circle(radius)"));
6133 assert!(out.contains("_tag: \"Circle\""));
6134 assert!(out.contains("Shape_None = Object.freeze({ _tag: \"None\" })"));
6135 }
6136
6137 #[test]
6138 fn match_on_tagged_objects() {
6139 let scrutinee = id_node(10, "shape");
6140 let arms = vec![
6141 node(
6142 11,
6143 NodeKind::MatchArm {
6144 pattern: Box::new(node(
6145 12,
6146 NodeKind::ConstructorPat {
6147 path: type_path(&["Shape", "Circle"]),
6148 fields: vec![bind_pat(13, "r")],
6149 },
6150 )),
6151 guard: None,
6152 body: Box::new(block(
6153 14,
6154 vec![],
6155 Some(node(
6156 15,
6157 NodeKind::BinaryOp {
6158 op: BinOp::Mul,
6159 left: Box::new(id_node(16, "r")),
6160 right: Box::new(id_node(17, "r")),
6161 },
6162 )),
6163 )),
6164 },
6165 ),
6166 node(
6167 18,
6168 NodeKind::MatchArm {
6169 pattern: Box::new(node(19, NodeKind::WildcardPat)),
6170 guard: None,
6171 body: Box::new(block(20, vec![], Some(int_lit(21, "0")))),
6172 },
6173 ),
6174 ];
6175 let match_stmt = node(
6176 9,
6177 NodeKind::Match {
6178 scrutinee: Box::new(scrutinee),
6179 arms,
6180 },
6181 );
6182 let f = node(
6184 1,
6185 NodeKind::FnDecl {
6186 annotations: vec![],
6187 visibility: Visibility::Private,
6188 is_async: false,
6189 name: ident("area"),
6190 generic_params: vec![],
6191 params: vec![param_node(2, "shape")],
6192 return_type: None,
6193 effect_clause: vec![],
6194 where_clause: vec![],
6195 body: Box::new(block(3, vec![match_stmt], None)),
6196 },
6197 );
6198 let out = gen(&module(vec![], vec![f]));
6199 assert!(out.contains("switch (shape._tag)"));
6200 assert!(out.contains("case \"Circle\""));
6201 assert!(out.contains("const r = shape._0;"));
6202 assert!(out.contains("default:"));
6203 }
6204
6205 #[test]
6206 fn ownership_erased() {
6207 let move_expr = node(
6208 1,
6209 NodeKind::Move {
6210 expr: Box::new(id_node(2, "x")),
6211 },
6212 );
6213 let borrow_expr = node(
6214 3,
6215 NodeKind::Borrow {
6216 expr: Box::new(id_node(4, "y")),
6217 },
6218 );
6219 let mut_borrow_expr = node(
6220 5,
6221 NodeKind::MutableBorrow {
6222 expr: Box::new(id_node(6, "z")),
6223 },
6224 );
6225 let body = block(
6226 7,
6227 vec![
6228 node(
6229 8,
6230 NodeKind::LetBinding {
6231 is_mut: false,
6232 pattern: Box::new(bind_pat(9, "a")),
6233 ty: None,
6234 value: Box::new(move_expr),
6235 },
6236 ),
6237 node(
6238 10,
6239 NodeKind::LetBinding {
6240 is_mut: false,
6241 pattern: Box::new(bind_pat(11, "b")),
6242 ty: None,
6243 value: Box::new(borrow_expr),
6244 },
6245 ),
6246 node(
6247 12,
6248 NodeKind::LetBinding {
6249 is_mut: false,
6250 pattern: Box::new(bind_pat(13, "c")),
6251 ty: None,
6252 value: Box::new(mut_borrow_expr),
6253 },
6254 ),
6255 ],
6256 None,
6257 );
6258 let f = node(
6259 0,
6260 NodeKind::FnDecl {
6261 annotations: vec![],
6262 visibility: Visibility::Private,
6263 is_async: false,
6264 name: ident("test"),
6265 generic_params: vec![],
6266 params: vec![],
6267 return_type: None,
6268 effect_clause: vec![],
6269 where_clause: vec![],
6270 body: Box::new(body),
6271 },
6272 );
6273 let out = gen(&module(vec![], vec![f]));
6274 assert!(out.contains("const a = x;"));
6276 assert!(out.contains("const b = y;"));
6277 assert!(out.contains("const c = z;"));
6278 }
6279
6280 #[test]
6281 fn let_binding_mut_uses_let() {
6282 let binding = node(
6283 1,
6284 NodeKind::LetBinding {
6285 is_mut: true,
6286 pattern: Box::new(bind_pat(2, "count")),
6287 ty: None,
6288 value: Box::new(int_lit(3, "0")),
6289 },
6290 );
6291 let f = node(
6292 0,
6293 NodeKind::FnDecl {
6294 annotations: vec![],
6295 visibility: Visibility::Private,
6296 is_async: false,
6297 name: ident("test"),
6298 generic_params: vec![],
6299 params: vec![],
6300 return_type: None,
6301 effect_clause: vec![],
6302 where_clause: vec![],
6303 body: Box::new(block(4, vec![binding], None)),
6304 },
6305 );
6306 let out = gen(&module(vec![], vec![f]));
6307 assert!(out.contains("let count = 0;"));
6308 }
6309
6310 #[test]
6311 fn string_interpolation() {
6312 let interp = node(
6313 1,
6314 NodeKind::Interpolation {
6315 parts: vec![
6316 AirInterpolationPart::Literal("Hello, ".into()),
6317 AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
6318 AirInterpolationPart::Literal("!".into()),
6319 ],
6320 },
6321 );
6322 let binding = node(
6323 3,
6324 NodeKind::LetBinding {
6325 is_mut: false,
6326 pattern: Box::new(bind_pat(4, "msg")),
6327 ty: None,
6328 value: Box::new(interp),
6329 },
6330 );
6331 let f = node(
6332 0,
6333 NodeKind::FnDecl {
6334 annotations: vec![],
6335 visibility: Visibility::Private,
6336 is_async: false,
6337 name: ident("greet"),
6338 generic_params: vec![],
6339 params: vec![param_node(5, "name")],
6340 return_type: None,
6341 effect_clause: vec![],
6342 where_clause: vec![],
6343 body: Box::new(block(6, vec![binding], Some(id_node(7, "msg")))),
6344 },
6345 );
6346 let out = gen(&module(vec![], vec![f]));
6347 assert!(out.contains("`Hello, ${__bockStr(name)}!`"));
6352 }
6353
6354 #[test]
6355 fn list_map_set_literals() {
6356 let list = node(
6357 1,
6358 NodeKind::ListLiteral {
6359 elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
6360 },
6361 );
6362 let map = node(
6363 5,
6364 NodeKind::MapLiteral {
6365 entries: vec![bock_air::AirMapEntry {
6366 key: str_lit(6, "a"),
6367 value: int_lit(7, "1"),
6368 }],
6369 },
6370 );
6371 let set = node(
6372 8,
6373 NodeKind::SetLiteral {
6374 elems: vec![int_lit(9, "1"), int_lit(10, "2")],
6375 },
6376 );
6377 let body = block(
6378 11,
6379 vec![
6380 node(
6381 12,
6382 NodeKind::LetBinding {
6383 is_mut: false,
6384 pattern: Box::new(bind_pat(13, "xs")),
6385 ty: None,
6386 value: Box::new(list),
6387 },
6388 ),
6389 node(
6390 14,
6391 NodeKind::LetBinding {
6392 is_mut: false,
6393 pattern: Box::new(bind_pat(15, "m")),
6394 ty: None,
6395 value: Box::new(map),
6396 },
6397 ),
6398 node(
6399 16,
6400 NodeKind::LetBinding {
6401 is_mut: false,
6402 pattern: Box::new(bind_pat(17, "s")),
6403 ty: None,
6404 value: Box::new(set),
6405 },
6406 ),
6407 ],
6408 None,
6409 );
6410 let f = node(
6411 0,
6412 NodeKind::FnDecl {
6413 annotations: vec![],
6414 visibility: Visibility::Private,
6415 is_async: false,
6416 name: ident("collections"),
6417 generic_params: vec![],
6418 params: vec![],
6419 return_type: None,
6420 effect_clause: vec![],
6421 where_clause: vec![],
6422 body: Box::new(body),
6423 },
6424 );
6425 let out = gen(&module(vec![], vec![f]));
6426 assert!(out.contains("[1, 2, 3]"));
6427 assert!(out.contains("new Map([[\"a\", 1]])"));
6428 assert!(out.contains("new Set([1, 2])"));
6429 }
6430
6431 #[test]
6432 fn record_construction() {
6433 let rec = node(
6434 1,
6435 NodeKind::RecordConstruct {
6436 path: type_path(&["User"]),
6437 fields: vec![
6438 AirRecordField {
6439 name: ident("name"),
6440 value: Some(Box::new(str_lit(2, "Alice"))),
6441 },
6442 AirRecordField {
6443 name: ident("age"),
6444 value: Some(Box::new(int_lit(3, "30"))),
6445 },
6446 ],
6447 spread: None,
6448 },
6449 );
6450 let binding = node(
6451 4,
6452 NodeKind::LetBinding {
6453 is_mut: false,
6454 pattern: Box::new(bind_pat(5, "user")),
6455 ty: None,
6456 value: Box::new(rec),
6457 },
6458 );
6459 let f = node(
6460 0,
6461 NodeKind::FnDecl {
6462 annotations: vec![],
6463 visibility: Visibility::Private,
6464 is_async: false,
6465 name: ident("test"),
6466 generic_params: vec![],
6467 params: vec![],
6468 return_type: None,
6469 effect_clause: vec![],
6470 where_clause: vec![],
6471 body: Box::new(block(6, vec![binding], None)),
6472 },
6473 );
6474 let out = gen(&module(vec![], vec![f]));
6475 assert!(out.contains("{ name: \"Alice\", age: 30 }"));
6476 }
6477
6478 #[test]
6479 fn control_flow() {
6480 let if_stmt = node(
6481 1,
6482 NodeKind::If {
6483 let_pattern: None,
6484 condition: Box::new(bool_lit(2, true)),
6485 then_block: Box::new(block(3, vec![], Some(int_lit(4, "1")))),
6486 else_block: Some(Box::new(block(5, vec![], Some(int_lit(6, "2"))))),
6487 },
6488 );
6489 let for_stmt = node(
6490 7,
6491 NodeKind::For {
6492 pattern: Box::new(bind_pat(8, "x")),
6493 iterable: Box::new(id_node(9, "items")),
6494 body: Box::new(block(10, vec![], None)),
6495 },
6496 );
6497 let while_stmt = node(
6498 11,
6499 NodeKind::While {
6500 condition: Box::new(bool_lit(12, true)),
6501 body: Box::new(block(
6502 13,
6503 vec![node(14, NodeKind::Break { value: None })],
6504 None,
6505 )),
6506 },
6507 );
6508 let body = block(15, vec![if_stmt, for_stmt, while_stmt], None);
6509 let f = node(
6510 0,
6511 NodeKind::FnDecl {
6512 annotations: vec![],
6513 visibility: Visibility::Private,
6514 is_async: false,
6515 name: ident("flow"),
6516 generic_params: vec![],
6517 params: vec![param_node(16, "items")],
6518 return_type: None,
6519 effect_clause: vec![],
6520 where_clause: vec![],
6521 body: Box::new(body),
6522 },
6523 );
6524 let out = gen(&module(vec![], vec![f]));
6525 assert!(out.contains("if (true)"));
6526 assert!(out.contains("} else {"));
6527 assert!(out.contains("for (const x of items)"));
6528 assert!(out.contains("while (true)"));
6529 assert!(out.contains("break;"));
6530 }
6531
6532 #[test]
6533 fn lambda_and_pipe() {
6534 let lambda = node(
6535 1,
6536 NodeKind::Lambda {
6537 params: vec![param_node(2, "x")],
6538 body: Box::new(node(
6539 3,
6540 NodeKind::BinaryOp {
6541 op: BinOp::Mul,
6542 left: Box::new(id_node(4, "x")),
6543 right: Box::new(int_lit(5, "2")),
6544 },
6545 )),
6546 },
6547 );
6548 let pipe = node(
6549 6,
6550 NodeKind::Pipe {
6551 left: Box::new(int_lit(7, "5")),
6552 right: Box::new(id_node(8, "double")),
6553 },
6554 );
6555 let body = block(
6556 9,
6557 vec![
6558 node(
6559 10,
6560 NodeKind::LetBinding {
6561 is_mut: false,
6562 pattern: Box::new(bind_pat(11, "double")),
6563 ty: None,
6564 value: Box::new(lambda),
6565 },
6566 ),
6567 node(
6568 12,
6569 NodeKind::LetBinding {
6570 is_mut: false,
6571 pattern: Box::new(bind_pat(13, "result")),
6572 ty: None,
6573 value: Box::new(pipe),
6574 },
6575 ),
6576 ],
6577 None,
6578 );
6579 let f = node(
6580 0,
6581 NodeKind::FnDecl {
6582 annotations: vec![],
6583 visibility: Visibility::Private,
6584 is_async: false,
6585 name: ident("test"),
6586 generic_params: vec![],
6587 params: vec![],
6588 return_type: None,
6589 effect_clause: vec![],
6590 where_clause: vec![],
6591 body: Box::new(body),
6592 },
6593 );
6594 let out = gen(&module(vec![], vec![f]));
6595 assert!(out.contains("(x) => (x * 2)"));
6596 assert!(out.contains("double(5)"));
6597 }
6598
6599 #[test]
6600 fn result_construct() {
6601 let ok = node(
6602 1,
6603 NodeKind::ResultConstruct {
6604 variant: ResultVariant::Ok,
6605 value: Some(Box::new(int_lit(2, "42"))),
6606 },
6607 );
6608 let err = node(
6609 3,
6610 NodeKind::ResultConstruct {
6611 variant: ResultVariant::Err,
6612 value: Some(Box::new(str_lit(4, "failed"))),
6613 },
6614 );
6615 let body = block(
6616 5,
6617 vec![
6618 node(
6619 6,
6620 NodeKind::LetBinding {
6621 is_mut: false,
6622 pattern: Box::new(bind_pat(7, "good")),
6623 ty: None,
6624 value: Box::new(ok),
6625 },
6626 ),
6627 node(
6628 8,
6629 NodeKind::LetBinding {
6630 is_mut: false,
6631 pattern: Box::new(bind_pat(9, "bad")),
6632 ty: None,
6633 value: Box::new(err),
6634 },
6635 ),
6636 ],
6637 None,
6638 );
6639 let f = node(
6640 0,
6641 NodeKind::FnDecl {
6642 annotations: vec![],
6643 visibility: Visibility::Private,
6644 is_async: false,
6645 name: ident("test"),
6646 generic_params: vec![],
6647 params: vec![],
6648 return_type: None,
6649 effect_clause: vec![],
6650 where_clause: vec![],
6651 body: Box::new(body),
6652 },
6653 );
6654 let out = gen(&module(vec![], vec![f]));
6655 assert!(out.contains("{ _tag: \"Ok\", _0: 42 }"), "got: {out}");
6657 assert!(
6658 out.contains("{ _tag: \"Err\", _0: \"failed\" }"),
6659 "got: {out}"
6660 );
6661 }
6662
6663 #[test]
6664 fn class_declaration() {
6665 let method_body = block(10, vec![], Some(id_node(11, "undefined")));
6666 let method = node(
6667 5,
6668 NodeKind::FnDecl {
6669 annotations: vec![],
6670 visibility: Visibility::Public,
6671 is_async: false,
6672 name: ident("greet"),
6673 generic_params: vec![],
6674 params: vec![],
6675 return_type: None,
6676 effect_clause: vec![],
6677 where_clause: vec![],
6678 body: Box::new(method_body),
6679 },
6680 );
6681 let cls = node(
6682 1,
6683 NodeKind::ClassDecl {
6684 annotations: vec![],
6685 visibility: Visibility::Public,
6686 name: ident("Person"),
6687 generic_params: vec![],
6688 base: None,
6689 traits: vec![],
6690 fields: vec![bock_ast::RecordDeclField {
6691 id: 0,
6692 span: span(),
6693 name: ident("name"),
6694 ty: bock_ast::TypeExpr::Named {
6695 id: 0,
6696 span: span(),
6697 path: type_path(&["String"]),
6698 args: vec![],
6699 },
6700 default: None,
6701 }],
6702 methods: vec![method],
6703 },
6704 );
6705 let out = gen(&module(vec![], vec![cls]));
6706 assert!(out.contains("class Person {"));
6707 assert!(out.contains("constructor(name)"));
6708 assert!(out.contains("this.name = name;"));
6709 assert!(out.contains("greet()"));
6710 }
6711
6712 #[test]
6717 fn class_literal_constructs_positionally() {
6718 fn class_field(name: &str) -> bock_ast::RecordDeclField {
6719 bock_ast::RecordDeclField {
6720 id: 0,
6721 span: span(),
6722 name: ident(name),
6723 ty: bock_ast::TypeExpr::Named {
6724 id: 0,
6725 span: span(),
6726 path: type_path(&["String"]),
6727 args: vec![],
6728 },
6729 default: None,
6730 }
6731 }
6732 let cls = node(
6733 1,
6734 NodeKind::ClassDecl {
6735 annotations: vec![],
6736 visibility: Visibility::Public,
6737 name: ident("Button"),
6738 generic_params: vec![],
6739 base: None,
6740 traits: vec![],
6741 fields: vec![class_field("label"), class_field("on_click")],
6742 methods: vec![],
6743 },
6744 );
6745 let construct = node(
6749 10,
6750 NodeKind::RecordConstruct {
6751 path: type_path(&["Button"]),
6752 fields: vec![
6753 AirRecordField {
6754 name: ident("on_click"),
6755 value: Some(Box::new(str_lit(11, "click"))),
6756 },
6757 AirRecordField {
6758 name: ident("label"),
6759 value: Some(Box::new(str_lit(12, "Submit"))),
6760 },
6761 ],
6762 spread: None,
6763 },
6764 );
6765 let main_fn = node(
6766 20,
6767 NodeKind::FnDecl {
6768 annotations: vec![],
6769 visibility: Visibility::Private,
6770 is_async: false,
6771 name: ident("main"),
6772 generic_params: vec![],
6773 params: vec![],
6774 return_type: None,
6775 effect_clause: vec![],
6776 where_clause: vec![],
6777 body: Box::new(block(
6778 21,
6779 vec![node(
6780 22,
6781 NodeKind::LetBinding {
6782 is_mut: false,
6783 pattern: Box::new(bind_pat(23, "b")),
6784 ty: None,
6785 value: Box::new(construct),
6786 },
6787 )],
6788 None,
6789 )),
6790 },
6791 );
6792 let out = gen(&module(vec![], vec![cls, main_fn]));
6793 assert!(
6795 out.contains(r#"new Button("Submit", "click")"#),
6796 "expected positional `new Button(...)` in declaration order, got:\n{out}"
6797 );
6798 assert!(
6799 !out.contains("{ label:"),
6800 "class literal must not emit a bare object literal:\n{out}"
6801 );
6802 }
6803
6804 #[test]
6805 fn const_declaration() {
6806 let c = node(
6807 1,
6808 NodeKind::ConstDecl {
6809 annotations: vec![],
6810 visibility: Visibility::Public,
6811 name: ident("PI"),
6812 ty: Box::new(node(
6813 2,
6814 NodeKind::TypeNamed {
6815 path: type_path(&["Float"]),
6816 args: vec![],
6817 },
6818 )),
6819 value: Box::new(node(
6820 3,
6821 NodeKind::Literal {
6822 lit: Literal::Float("3.14159".into()),
6823 },
6824 )),
6825 },
6826 );
6827 let out = gen(&module(vec![], vec![c]));
6828 assert!(out.contains("const PI = 3.14159;"));
6829 }
6830
6831 #[test]
6832 fn record_declaration() {
6833 let rec = node(
6834 1,
6835 NodeKind::RecordDecl {
6836 annotations: vec![],
6837 visibility: Visibility::Public,
6838 name: ident("Point"),
6839 generic_params: vec![],
6840 fields: vec![
6841 bock_ast::RecordDeclField {
6842 id: 0,
6843 span: span(),
6844 name: ident("x"),
6845 ty: bock_ast::TypeExpr::Named {
6846 id: 0,
6847 span: span(),
6848 path: type_path(&["Float"]),
6849 args: vec![],
6850 },
6851 default: None,
6852 },
6853 bock_ast::RecordDeclField {
6854 id: 0,
6855 span: span(),
6856 name: ident("y"),
6857 ty: bock_ast::TypeExpr::Named {
6858 id: 0,
6859 span: span(),
6860 path: type_path(&["Float"]),
6861 args: vec![],
6862 },
6863 default: None,
6864 },
6865 ],
6866 },
6867 );
6868 let out = gen(&module(vec![], vec![rec]));
6869 assert!(out.contains("class Point {"));
6870 assert!(out.contains("constructor({ x, y })"));
6871 assert!(out.contains("this.x = x;"));
6872 assert!(out.contains("this.y = y;"));
6873 }
6874
6875 fn has_node() -> bool {
6878 std::process::Command::new("which")
6879 .arg("node")
6880 .output()
6881 .map(|o| o.status.success())
6882 .unwrap_or(false)
6883 }
6884
6885 fn check_js_syntax(code: &str) -> bool {
6887 use std::io::Write;
6888 let mut child = std::process::Command::new("node")
6889 .arg("--check")
6890 .stdin(std::process::Stdio::piped())
6891 .stdout(std::process::Stdio::null())
6892 .stderr(std::process::Stdio::null())
6893 .spawn()
6894 .expect("failed to spawn node");
6895 child
6896 .stdin
6897 .as_mut()
6898 .unwrap()
6899 .write_all(code.as_bytes())
6900 .unwrap();
6901 child.wait().unwrap().success()
6902 }
6903
6904 fn run_js(code: &str) -> String {
6906 let output = std::process::Command::new("node")
6907 .arg("-e")
6908 .arg(code)
6909 .output()
6910 .expect("failed to run node");
6911 String::from_utf8(output.stdout).unwrap().trim().to_string()
6912 }
6913
6914 #[test]
6915 #[ignore]
6916 fn e2e_hello_world() {
6917 if !has_node() {
6918 return;
6919 }
6920 let body = block(
6922 2,
6923 vec![],
6924 Some(node(
6925 3,
6926 NodeKind::Call {
6927 callee: Box::new(node(
6928 4,
6929 NodeKind::FieldAccess {
6930 object: Box::new(id_node(5, "console")),
6931 field: ident("log"),
6932 },
6933 )),
6934 args: vec![AirArg {
6935 label: None,
6936 value: str_lit(6, "Hello, World!"),
6937 }],
6938 type_args: vec![],
6939 },
6940 )),
6941 );
6942 let f = node(
6943 1,
6944 NodeKind::FnDecl {
6945 annotations: vec![],
6946 visibility: Visibility::Private,
6947 is_async: false,
6948 name: ident("main"),
6949 generic_params: vec![],
6950 params: vec![],
6951 return_type: None,
6952 effect_clause: vec![],
6953 where_clause: vec![],
6954 body: Box::new(body),
6955 },
6956 );
6957 let code = gen(&module(vec![], vec![f]));
6958 let full = format!("{code}\nmain();\n");
6959 assert!(check_js_syntax(&full));
6960 assert_eq!(run_js(&full), "Hello, World!");
6961 }
6962
6963 #[test]
6964 #[ignore]
6965 fn e2e_arithmetic() {
6966 if !has_node() {
6967 return;
6968 }
6969 let body = block(
6970 2,
6971 vec![],
6972 Some(node(
6973 3,
6974 NodeKind::BinaryOp {
6975 op: BinOp::Add,
6976 left: Box::new(int_lit(4, "10")),
6977 right: Box::new(int_lit(5, "32")),
6978 },
6979 )),
6980 );
6981 let f = node(
6982 1,
6983 NodeKind::FnDecl {
6984 annotations: vec![],
6985 visibility: Visibility::Private,
6986 is_async: false,
6987 name: ident("calc"),
6988 generic_params: vec![],
6989 params: vec![],
6990 return_type: None,
6991 effect_clause: vec![],
6992 where_clause: vec![],
6993 body: Box::new(body),
6994 },
6995 );
6996 let code = gen(&module(vec![], vec![f]));
6997 let full = format!("{code}\nconsole.log(calc());\n");
6998 assert!(check_js_syntax(&full));
6999 assert_eq!(run_js(&full), "42");
7000 }
7001
7002 #[test]
7003 #[ignore]
7004 fn e2e_if_else() {
7005 if !has_node() {
7006 return;
7007 }
7008 let if_stmt = node(
7009 3,
7010 NodeKind::If {
7011 let_pattern: None,
7012 condition: Box::new(node(
7013 4,
7014 NodeKind::BinaryOp {
7015 op: BinOp::Gt,
7016 left: Box::new(id_node(5, "x")),
7017 right: Box::new(int_lit(6, "0")),
7018 },
7019 )),
7020 then_block: Box::new(block(7, vec![], Some(str_lit(8, "positive")))),
7021 else_block: Some(Box::new(block(
7022 9,
7023 vec![],
7024 Some(str_lit(10, "non-positive")),
7025 ))),
7026 },
7027 );
7028 let body = block(2, vec![if_stmt], None);
7029 let f = node(
7030 1,
7031 NodeKind::FnDecl {
7032 annotations: vec![],
7033 visibility: Visibility::Private,
7034 is_async: false,
7035 name: ident("classify"),
7036 generic_params: vec![],
7037 params: vec![param_node(11, "x")],
7038 return_type: None,
7039 effect_clause: vec![],
7040 where_clause: vec![],
7041 body: Box::new(body),
7042 },
7043 );
7044 let code = gen(&module(vec![], vec![f]));
7045 let full = format!("{code}\nconsole.log(classify(5));\nconsole.log(classify(-1));\n");
7046 assert!(check_js_syntax(&full));
7047 let output = run_js(&full);
7048 assert!(output.contains("positive"));
7049 assert!(output.contains("non-positive"));
7050 }
7051
7052 #[test]
7053 #[ignore]
7054 fn e2e_for_loop() {
7055 if !has_node() {
7056 return;
7057 }
7058 let body = block(
7059 2,
7060 vec![
7061 node(
7062 3,
7063 NodeKind::LetBinding {
7064 is_mut: true,
7065 pattern: Box::new(bind_pat(4, "sum")),
7066 ty: None,
7067 value: Box::new(int_lit(5, "0")),
7068 },
7069 ),
7070 node(
7071 6,
7072 NodeKind::For {
7073 pattern: Box::new(bind_pat(7, "x")),
7074 iterable: Box::new(node(
7075 8,
7076 NodeKind::ListLiteral {
7077 elems: vec![int_lit(9, "1"), int_lit(10, "2"), int_lit(11, "3")],
7078 },
7079 )),
7080 body: Box::new(block(
7081 12,
7082 vec![node(
7083 13,
7084 NodeKind::Assign {
7085 op: AssignOp::AddAssign,
7086 target: Box::new(id_node(14, "sum")),
7087 value: Box::new(id_node(15, "x")),
7088 },
7089 )],
7090 None,
7091 )),
7092 },
7093 ),
7094 ],
7095 Some(id_node(16, "sum")),
7096 );
7097 let f = node(
7098 1,
7099 NodeKind::FnDecl {
7100 annotations: vec![],
7101 visibility: Visibility::Private,
7102 is_async: false,
7103 name: ident("total"),
7104 generic_params: vec![],
7105 params: vec![],
7106 return_type: None,
7107 effect_clause: vec![],
7108 where_clause: vec![],
7109 body: Box::new(body),
7110 },
7111 );
7112 let code = gen(&module(vec![], vec![f]));
7113 let full = format!("{code}\nconsole.log(total());\n");
7114 assert!(check_js_syntax(&full));
7115 assert_eq!(run_js(&full), "6");
7116 }
7117
7118 #[test]
7119 #[ignore]
7120 fn e2e_tagged_objects() {
7121 if !has_node() {
7122 return;
7123 }
7124 let enum_decl = node(
7126 1,
7127 NodeKind::EnumDecl {
7128 annotations: vec![],
7129 visibility: Visibility::Public,
7130 name: ident("Color"),
7131 generic_params: vec![],
7132 variants: vec![
7133 node(
7134 2,
7135 NodeKind::EnumVariant {
7136 name: ident("Red"),
7137 payload: EnumVariantPayload::Unit,
7138 },
7139 ),
7140 node(
7141 3,
7142 NodeKind::EnumVariant {
7143 name: ident("Green"),
7144 payload: EnumVariantPayload::Unit,
7145 },
7146 ),
7147 node(
7148 4,
7149 NodeKind::EnumVariant {
7150 name: ident("Blue"),
7151 payload: EnumVariantPayload::Unit,
7152 },
7153 ),
7154 ],
7155 },
7156 );
7157 let code = gen(&module(vec![], vec![enum_decl]));
7158 let full =
7159 format!("{code}\nconsole.log(Color_Red._tag);\nconsole.log(Color_Green._tag);\n");
7160 assert!(check_js_syntax(&full));
7161 let output = run_js(&full);
7162 assert!(output.contains("Red"));
7163 assert!(output.contains("Green"));
7164 }
7165
7166 #[test]
7167 #[ignore]
7168 fn e2e_match_switch() {
7169 if !has_node() {
7170 return;
7171 }
7172 let match_node = node(
7174 3,
7175 NodeKind::Match {
7176 scrutinee: Box::new(id_node(4, "n")),
7177 arms: vec![
7178 node(
7179 5,
7180 NodeKind::MatchArm {
7181 pattern: Box::new(node(
7182 6,
7183 NodeKind::LiteralPat {
7184 lit: Literal::Int("1".into()),
7185 },
7186 )),
7187 guard: None,
7188 body: Box::new(block(7, vec![], Some(str_lit(8, "one")))),
7189 },
7190 ),
7191 node(
7192 9,
7193 NodeKind::MatchArm {
7194 pattern: Box::new(node(
7195 10,
7196 NodeKind::LiteralPat {
7197 lit: Literal::Int("2".into()),
7198 },
7199 )),
7200 guard: None,
7201 body: Box::new(block(11, vec![], Some(str_lit(12, "two")))),
7202 },
7203 ),
7204 node(
7205 13,
7206 NodeKind::MatchArm {
7207 pattern: Box::new(node(14, NodeKind::WildcardPat)),
7208 guard: None,
7209 body: Box::new(block(15, vec![], Some(str_lit(16, "other")))),
7210 },
7211 ),
7212 ],
7213 },
7214 );
7215 let f = node(
7216 1,
7217 NodeKind::FnDecl {
7218 annotations: vec![],
7219 visibility: Visibility::Private,
7220 is_async: false,
7221 name: ident("describe"),
7222 generic_params: vec![],
7223 params: vec![param_node(2, "n")],
7224 return_type: None,
7225 effect_clause: vec![],
7226 where_clause: vec![],
7227 body: Box::new(block(17, vec![match_node], None)),
7228 },
7229 );
7230 let code = gen(&module(vec![], vec![f]));
7231 let full = format!("{code}\nconsole.log(describe(1));\nconsole.log(describe(2));\nconsole.log(describe(99));\n");
7232 assert!(check_js_syntax(&full));
7233 let output = run_js(&full);
7234 assert!(output.contains("one"));
7235 assert!(output.contains("two"));
7236 assert!(output.contains("other"));
7237 }
7238
7239 #[test]
7241 fn match_literal_bind_stays_switch() {
7242 let arms = vec![
7243 node(
7244 5,
7245 NodeKind::MatchArm {
7246 pattern: Box::new(node(
7247 6,
7248 NodeKind::LiteralPat {
7249 lit: Literal::Int("0".into()),
7250 },
7251 )),
7252 guard: None,
7253 body: Box::new(block(7, vec![], Some(str_lit(8, "zero")))),
7254 },
7255 ),
7256 node(
7257 9,
7258 NodeKind::MatchArm {
7259 pattern: Box::new(node(
7260 10,
7261 NodeKind::BindPat {
7262 name: ident("x"),
7263 is_mut: false,
7264 },
7265 )),
7266 guard: None,
7267 body: Box::new(block(11, vec![], Some(id_node(12, "x")))),
7268 },
7269 ),
7270 ];
7271 let m = node(
7272 3,
7273 NodeKind::Match {
7274 scrutinee: Box::new(id_node(4, "n")),
7275 arms,
7276 },
7277 );
7278 let f = node(
7279 1,
7280 NodeKind::FnDecl {
7281 annotations: vec![],
7282 visibility: Visibility::Private,
7283 is_async: false,
7284 name: ident("label"),
7285 generic_params: vec![],
7286 params: vec![param_node(2, "n")],
7287 return_type: None,
7288 effect_clause: vec![],
7289 where_clause: vec![],
7290 body: Box::new(block(13, vec![m], None)),
7291 },
7292 );
7293 let code = gen(&module(vec![], vec![f]));
7294 assert!(
7295 code.contains("switch (n)"),
7296 "expected switch fast-path, got:\n{code}"
7297 );
7298 assert!(
7299 !code.contains("else if"),
7300 "should not use if-chain, got:\n{code}"
7301 );
7302 }
7303
7304 #[test]
7307 fn match_guard_lowers_to_ifchain() {
7308 let guarded = |id: u32, label: &str| {
7309 node(
7310 id,
7311 NodeKind::MatchArm {
7312 pattern: Box::new(node(
7313 id + 1,
7314 NodeKind::BindPat {
7315 name: ident("x"),
7316 is_mut: false,
7317 },
7318 )),
7319 guard: Some(Box::new(node(
7320 id + 2,
7321 NodeKind::BinaryOp {
7322 op: BinOp::Gt,
7323 left: Box::new(id_node(id + 3, "x")),
7324 right: Box::new(node(
7325 id + 4,
7326 NodeKind::Literal {
7327 lit: Literal::Int("0".into()),
7328 },
7329 )),
7330 },
7331 ))),
7332 body: Box::new(block(id + 5, vec![], Some(str_lit(id + 6, label)))),
7333 },
7334 )
7335 };
7336 let arms = vec![
7337 guarded(5, "pos"),
7338 node(
7339 20,
7340 NodeKind::MatchArm {
7341 pattern: Box::new(node(21, NodeKind::WildcardPat)),
7342 guard: None,
7343 body: Box::new(block(22, vec![], Some(str_lit(23, "other")))),
7344 },
7345 ),
7346 ];
7347 let m = node(
7348 3,
7349 NodeKind::Match {
7350 scrutinee: Box::new(id_node(4, "n")),
7351 arms,
7352 },
7353 );
7354 let f = node(
7355 1,
7356 NodeKind::FnDecl {
7357 annotations: vec![],
7358 visibility: Visibility::Private,
7359 is_async: false,
7360 name: ident("classify"),
7361 generic_params: vec![],
7362 params: vec![param_node(2, "n")],
7363 return_type: None,
7364 effect_clause: vec![],
7365 where_clause: vec![],
7366 body: Box::new(block(30, vec![m], None)),
7367 },
7368 );
7369 let code = gen(&module(vec![], vec![f]));
7370 assert!(
7371 !code.contains("switch"),
7372 "guard match must not use switch, got:\n{code}"
7373 );
7374 assert!(
7375 code.contains("else"),
7376 "guard match must chain to a fallthrough, got:\n{code}"
7377 );
7378 assert!(
7380 code.contains("const x = n;"),
7381 "guard must bind x, got:\n{code}"
7382 );
7383 }
7384
7385 #[test]
7386 #[ignore]
7387 fn e2e_string_interpolation() {
7388 if !has_node() {
7389 return;
7390 }
7391 let body = block(
7392 2,
7393 vec![],
7394 Some(node(
7395 3,
7396 NodeKind::Interpolation {
7397 parts: vec![
7398 AirInterpolationPart::Literal("Hello, ".into()),
7399 AirInterpolationPart::Expr(Box::new(id_node(4, "name"))),
7400 AirInterpolationPart::Literal("! You are ".into()),
7401 AirInterpolationPart::Expr(Box::new(id_node(5, "age"))),
7402 AirInterpolationPart::Literal(" years old.".into()),
7403 ],
7404 },
7405 )),
7406 );
7407 let f = node(
7408 1,
7409 NodeKind::FnDecl {
7410 annotations: vec![],
7411 visibility: Visibility::Private,
7412 is_async: false,
7413 name: ident("greet"),
7414 generic_params: vec![],
7415 params: vec![param_node(6, "name"), param_node(7, "age")],
7416 return_type: None,
7417 effect_clause: vec![],
7418 where_clause: vec![],
7419 body: Box::new(body),
7420 },
7421 );
7422 let code = gen(&module(vec![], vec![f]));
7423 let full = format!("{code}\nconsole.log(greet(\"Alice\", 30));\n");
7424 assert!(check_js_syntax(&full));
7425 assert_eq!(run_js(&full), "Hello, Alice! You are 30 years old.");
7426 }
7427
7428 #[test]
7429 #[ignore]
7430 fn e2e_lambda_and_method_call() {
7431 if !has_node() {
7432 return;
7433 }
7434 let body = block(
7435 2,
7436 vec![node(
7437 3,
7438 NodeKind::LetBinding {
7439 is_mut: false,
7440 pattern: Box::new(bind_pat(4, "nums")),
7441 ty: None,
7442 value: Box::new(node(
7443 5,
7444 NodeKind::ListLiteral {
7445 elems: vec![int_lit(6, "1"), int_lit(7, "2"), int_lit(8, "3")],
7446 },
7447 )),
7448 },
7449 )],
7450 Some(node(
7451 9,
7452 NodeKind::MethodCall {
7453 receiver: Box::new(node(
7454 10,
7455 NodeKind::MethodCall {
7456 receiver: Box::new(id_node(11, "nums")),
7457 method: ident("map"),
7458 type_args: vec![],
7459 args: vec![AirArg {
7460 label: None,
7461 value: node(
7462 12,
7463 NodeKind::Lambda {
7464 params: vec![param_node(13, "x")],
7465 body: Box::new(node(
7466 14,
7467 NodeKind::BinaryOp {
7468 op: BinOp::Mul,
7469 left: Box::new(id_node(15, "x")),
7470 right: Box::new(int_lit(16, "2")),
7471 },
7472 )),
7473 },
7474 ),
7475 }],
7476 },
7477 )),
7478 method: ident("join"),
7479 type_args: vec![],
7480 args: vec![AirArg {
7481 label: None,
7482 value: str_lit(17, ", "),
7483 }],
7484 },
7485 )),
7486 );
7487 let f = node(
7488 1,
7489 NodeKind::FnDecl {
7490 annotations: vec![],
7491 visibility: Visibility::Private,
7492 is_async: false,
7493 name: ident("transform"),
7494 generic_params: vec![],
7495 params: vec![],
7496 return_type: None,
7497 effect_clause: vec![],
7498 where_clause: vec![],
7499 body: Box::new(body),
7500 },
7501 );
7502 let code = gen(&module(vec![], vec![f]));
7503 let full = format!("{code}\nconsole.log(transform());\n");
7504 assert!(check_js_syntax(&full));
7505 assert_eq!(run_js(&full), "2, 4, 6");
7506 }
7507
7508 #[test]
7509 #[ignore]
7510 fn e2e_while_loop() {
7511 if !has_node() {
7512 return;
7513 }
7514 let body = block(
7515 2,
7516 vec![
7517 node(
7518 3,
7519 NodeKind::LetBinding {
7520 is_mut: true,
7521 pattern: Box::new(bind_pat(4, "i")),
7522 ty: None,
7523 value: Box::new(int_lit(5, "0")),
7524 },
7525 ),
7526 node(
7527 6,
7528 NodeKind::LetBinding {
7529 is_mut: true,
7530 pattern: Box::new(bind_pat(7, "result")),
7531 ty: None,
7532 value: Box::new(int_lit(8, "1")),
7533 },
7534 ),
7535 node(
7536 9,
7537 NodeKind::While {
7538 condition: Box::new(node(
7539 10,
7540 NodeKind::BinaryOp {
7541 op: BinOp::Lt,
7542 left: Box::new(id_node(11, "i")),
7543 right: Box::new(id_node(12, "n")),
7544 },
7545 )),
7546 body: Box::new(block(
7547 13,
7548 vec![
7549 node(
7550 14,
7551 NodeKind::Assign {
7552 op: AssignOp::MulAssign,
7553 target: Box::new(id_node(15, "result")),
7554 value: Box::new(int_lit(16, "2")),
7555 },
7556 ),
7557 node(
7558 17,
7559 NodeKind::Assign {
7560 op: AssignOp::AddAssign,
7561 target: Box::new(id_node(18, "i")),
7562 value: Box::new(int_lit(19, "1")),
7563 },
7564 ),
7565 ],
7566 None,
7567 )),
7568 },
7569 ),
7570 ],
7571 Some(id_node(20, "result")),
7572 );
7573 let f = node(
7574 1,
7575 NodeKind::FnDecl {
7576 annotations: vec![],
7577 visibility: Visibility::Private,
7578 is_async: false,
7579 name: ident("pow2"),
7580 generic_params: vec![],
7581 params: vec![param_node(21, "n")],
7582 return_type: None,
7583 effect_clause: vec![],
7584 where_clause: vec![],
7585 body: Box::new(body),
7586 },
7587 );
7588 let code = gen(&module(vec![], vec![f]));
7589 let full = format!("{code}\nconsole.log(pow2(10));\n");
7590 assert!(check_js_syntax(&full));
7591 assert_eq!(run_js(&full), "1024");
7592 }
7593
7594 #[test]
7595 #[ignore]
7596 fn e2e_async_await() {
7597 if !has_node() {
7598 return;
7599 }
7600 let body = block(
7602 2,
7603 vec![],
7604 Some(node(
7605 3,
7606 NodeKind::Await {
7607 expr: Box::new(node(
7608 4,
7609 NodeKind::Call {
7610 callee: Box::new(node(
7611 5,
7612 NodeKind::FieldAccess {
7613 object: Box::new(id_node(6, "Promise")),
7614 field: ident("resolve"),
7615 },
7616 )),
7617 args: vec![AirArg {
7618 label: None,
7619 value: int_lit(7, "42"),
7620 }],
7621 type_args: vec![],
7622 },
7623 )),
7624 },
7625 )),
7626 );
7627 let f = node(
7628 1,
7629 NodeKind::FnDecl {
7630 annotations: vec![],
7631 visibility: Visibility::Private,
7632 is_async: true,
7633 name: ident("delayed"),
7634 generic_params: vec![],
7635 params: vec![],
7636 return_type: None,
7637 effect_clause: vec![],
7638 where_clause: vec![],
7639 body: Box::new(body),
7640 },
7641 );
7642 let code = gen(&module(vec![], vec![f]));
7643 let full = format!("{code}\ndelayed().then(v => console.log(v));\n");
7644 assert!(check_js_syntax(&full));
7645 assert_eq!(run_js(&full), "42");
7646 }
7647
7648 #[test]
7649 fn to_camel_case_converts() {
7650 assert_eq!(to_camel_case("Log"), "log");
7652 assert_eq!(to_camel_case("Clock"), "clock");
7653 assert_eq!(to_camel_case("IO"), "iO");
7654 assert_eq!(to_camel_case(""), "");
7655 assert_eq!(to_camel_case("create_user"), "createUser");
7657 assert_eq!(to_camel_case("get_all_items"), "getAllItems");
7658 assert_eq!(to_camel_case("createUser"), "createUser");
7660 assert_eq!(to_camel_case("x"), "x");
7661 assert_eq!(to_camel_case("_"), "_");
7663 }
7664
7665 #[test]
7666 fn snake_case_fn_becomes_camel_case() {
7667 let body = block(2, vec![], Some(int_lit(3, "42")));
7668 let f = node(
7669 1,
7670 NodeKind::FnDecl {
7671 annotations: vec![],
7672 visibility: Visibility::Private,
7673 is_async: false,
7674 name: ident("create_user"),
7675 generic_params: vec![],
7676 params: vec![],
7677 return_type: None,
7678 effect_clause: vec![],
7679 where_clause: vec![],
7680 body: Box::new(body),
7681 },
7682 );
7683 let out = gen(&module(vec![], vec![f]));
7684 assert!(
7685 out.contains("function createUser()"),
7686 "expected camelCase function name, got: {out}"
7687 );
7688 }
7689
7690 #[test]
7691 fn escape_js_string_works() {
7692 assert_eq!(escape_js_string("hello"), "hello");
7693 assert_eq!(escape_js_string("he\"llo"), "he\\\"llo");
7694 assert_eq!(escape_js_string("line\nbreak"), "line\\nbreak");
7695 }
7696
7697 #[test]
7698 fn escape_template_literal_works() {
7699 assert_eq!(escape_template_literal("hello"), "hello");
7700 assert_eq!(escape_template_literal("cost: $5"), "cost: \\$5");
7701 assert_eq!(escape_template_literal("back`tick"), "back\\`tick");
7702 }
7703
7704 fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
7708 let call = node(
7709 10,
7710 NodeKind::Call {
7711 callee: Box::new(id_node(11, func_name)),
7712 args: vec![AirArg {
7713 label: None,
7714 value: arg,
7715 }],
7716 type_args: vec![],
7717 },
7718 );
7719 let body = block(2, vec![call], None);
7720 let f = node(
7721 1,
7722 NodeKind::FnDecl {
7723 name: ident("main"),
7724 params: vec![],
7725 return_type: None,
7726 body: Box::new(body),
7727 generic_params: vec![],
7728 visibility: Visibility::Private,
7729 annotations: vec![],
7730 effect_clause: vec![],
7731 where_clause: vec![],
7732 is_async: false,
7733 },
7734 );
7735 gen(&module(vec![], vec![f]))
7736 }
7737
7738 fn gen_prelude_call_no_args(func_name: &str) -> String {
7740 let call = node(
7741 10,
7742 NodeKind::Call {
7743 callee: Box::new(id_node(11, func_name)),
7744 args: vec![],
7745 type_args: vec![],
7746 },
7747 );
7748 let body = block(2, vec![call], None);
7749 let f = node(
7750 1,
7751 NodeKind::FnDecl {
7752 name: ident("main"),
7753 params: vec![],
7754 return_type: None,
7755 body: Box::new(body),
7756 generic_params: vec![],
7757 visibility: Visibility::Private,
7758 annotations: vec![],
7759 effect_clause: vec![],
7760 where_clause: vec![],
7761 is_async: false,
7762 },
7763 );
7764 gen(&module(vec![], vec![f]))
7765 }
7766
7767 #[test]
7768 fn prelude_println_maps_to_console_log() {
7769 let code = gen_prelude_call("println", str_lit(12, "Hello"));
7770 assert!(
7771 code.contains("console.log("),
7772 "expected console.log, got: {code}"
7773 );
7774 assert!(
7775 !code.contains("println("),
7776 "should not contain bare println, got: {code}"
7777 );
7778 }
7779
7780 #[test]
7781 fn prelude_print_maps_to_process_stdout_write() {
7782 let code = gen_prelude_call("print", str_lit(12, "no newline"));
7783 assert!(
7784 code.contains("process.stdout.write(String("),
7785 "expected process.stdout.write, got: {code}"
7786 );
7787 }
7788
7789 #[test]
7790 fn prelude_debug_maps_to_console_debug() {
7791 let code = gen_prelude_call("debug", str_lit(12, "val"));
7792 assert!(
7793 code.contains("console.debug("),
7794 "expected console.debug, got: {code}"
7795 );
7796 }
7797
7798 #[test]
7799 fn prelude_assert_maps_to_throw() {
7800 let code = gen_prelude_call("assert", bool_lit(12, true));
7801 assert!(
7802 code.contains("if (!true) throw new Error(\"assertion failed\")"),
7803 "expected assert mapping, got: {code}"
7804 );
7805 }
7806
7807 #[test]
7808 fn prelude_todo_maps_to_throw_not_implemented() {
7809 let code = gen_prelude_call_no_args("todo");
7810 assert!(
7811 code.contains("throw new Error(\"not implemented\")"),
7812 "expected todo mapping, got: {code}"
7813 );
7814 }
7815
7816 #[test]
7817 fn prelude_unreachable_maps_to_throw_unreachable() {
7818 let code = gen_prelude_call_no_args("unreachable");
7819 assert!(
7820 code.contains("throw new Error(\"unreachable\")"),
7821 "expected unreachable mapping, got: {code}"
7822 );
7823 }
7824
7825 #[test]
7826 fn non_prelude_call_unaffected() {
7827 let code = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
7828 assert!(
7829 code.contains("myCustomFunc("),
7830 "expected normal call emission, got: {code}"
7831 );
7832 }
7833
7834 #[test]
7837 fn effect_decl_becomes_class() {
7838 let effect = node(
7839 1,
7840 NodeKind::EffectDecl {
7841 annotations: vec![],
7842 visibility: Visibility::Public,
7843 name: ident("Logger"),
7844 generic_params: vec![],
7845 components: vec![],
7846 operations: vec![
7847 node(
7848 2,
7849 NodeKind::FnDecl {
7850 annotations: vec![],
7851 visibility: Visibility::Public,
7852 is_async: false,
7853 name: ident("log"),
7854 generic_params: vec![],
7855 params: vec![param_node(3, "level"), param_node(4, "msg")],
7856 return_type: None,
7857 effect_clause: vec![],
7858 where_clause: vec![],
7859 body: Box::new(block(5, vec![], None)),
7860 },
7861 ),
7862 node(
7863 6,
7864 NodeKind::FnDecl {
7865 annotations: vec![],
7866 visibility: Visibility::Public,
7867 is_async: false,
7868 name: ident("flush"),
7869 generic_params: vec![],
7870 params: vec![],
7871 return_type: None,
7872 effect_clause: vec![],
7873 where_clause: vec![],
7874 body: Box::new(block(7, vec![], None)),
7875 },
7876 ),
7877 ],
7878 },
7879 );
7880 let out = gen(&module(vec![], vec![effect]));
7881 assert!(out.contains("class Logger {"), "got: {out}");
7882 assert!(out.contains("log(level, msg) {"), "got: {out}");
7883 assert!(out.contains("flush() {"), "got: {out}");
7884 assert!(
7885 out.contains("throw new Error(\"not implemented\");"),
7886 "got: {out}"
7887 );
7888 }
7889
7890 #[test]
7891 fn effect_decl_empty_operations() {
7892 let effect = node(
7893 1,
7894 NodeKind::EffectDecl {
7895 annotations: vec![],
7896 visibility: Visibility::Public,
7897 name: ident("Empty"),
7898 generic_params: vec![],
7899 components: vec![],
7900 operations: vec![],
7901 },
7902 );
7903 let out = gen(&module(vec![], vec![effect]));
7904 assert!(out.contains("class Empty {"), "got: {out}");
7905 assert!(out.contains("}"), "got: {out}");
7906 }
7907
7908 #[test]
7909 fn handling_block_passes_handlers_to_effectful_call() {
7910 use bock_air::AirHandlerPair;
7911
7912 let effect_decl = node(
7913 1,
7914 NodeKind::EffectDecl {
7915 annotations: vec![],
7916 visibility: Visibility::Public,
7917 name: ident("Logger"),
7918 generic_params: vec![],
7919 components: vec![],
7920 operations: vec![node(
7921 2,
7922 NodeKind::FnDecl {
7923 annotations: vec![],
7924 visibility: Visibility::Public,
7925 is_async: false,
7926 name: ident("log"),
7927 generic_params: vec![],
7928 params: vec![param_node(3, "msg")],
7929 return_type: None,
7930 effect_clause: vec![],
7931 where_clause: vec![],
7932 body: Box::new(block(4, vec![], None)),
7933 },
7934 )],
7935 },
7936 );
7937
7938 let inner_fn = node(
7940 10,
7941 NodeKind::FnDecl {
7942 annotations: vec![],
7943 visibility: Visibility::Private,
7944 is_async: false,
7945 name: ident("inner"),
7946 generic_params: vec![],
7947 params: vec![],
7948 return_type: None,
7949 effect_clause: vec![type_path(&["Logger"])],
7950 where_clause: vec![],
7951 body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
7952 },
7953 );
7954
7955 let call_inner = node(
7956 20,
7957 NodeKind::Call {
7958 callee: Box::new(id_node(21, "inner")),
7959 args: vec![],
7960 type_args: vec![],
7961 },
7962 );
7963 let handling = node(
7964 30,
7965 NodeKind::HandlingBlock {
7966 handlers: vec![AirHandlerPair {
7967 effect: type_path(&["Logger"]),
7968 handler: Box::new(node(
7969 31,
7970 NodeKind::Call {
7971 callee: Box::new(id_node(32, "StdoutLogger")),
7972 args: vec![],
7973 type_args: vec![],
7974 },
7975 )),
7976 }],
7977 body: Box::new(block(33, vec![], Some(call_inner))),
7978 },
7979 );
7980 let main_fn = node(
7981 40,
7982 NodeKind::FnDecl {
7983 annotations: vec![],
7984 visibility: Visibility::Private,
7985 is_async: false,
7986 name: ident("main"),
7987 generic_params: vec![],
7988 params: vec![],
7989 return_type: None,
7990 effect_clause: vec![],
7991 where_clause: vec![],
7992 body: Box::new(block(41, vec![handling], None)),
7993 },
7994 );
7995
7996 let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
7997 assert!(
7999 out.contains("inner({ logger: __logger })"),
8000 "handling block should pass handler to effectful call, got: {out}"
8001 );
8002 assert!(
8003 out.contains("const __logger = stdoutLogger()"),
8004 "handling block should instantiate handler, got: {out}"
8005 );
8006 }
8007
8008 #[test]
8009 fn sibling_handling_blocks_do_not_share_let_scope() {
8010 use bock_air::AirHandlerPair;
8011
8012 let make_handling = |id: u32, val: &str| {
8019 node(
8020 id,
8021 NodeKind::HandlingBlock {
8022 handlers: vec![AirHandlerPair {
8023 effect: type_path(&["Logger"]),
8024 handler: Box::new(node(
8025 id + 1,
8026 NodeKind::Call {
8027 callee: Box::new(id_node(id + 2, "StdoutLogger")),
8028 args: vec![],
8029 type_args: vec![],
8030 },
8031 )),
8032 }],
8033 body: Box::new(block(
8034 id + 3,
8035 vec![let_binding(id + 4, "part", false, str_lit(id + 6, val))],
8036 Some(id_node(id + 7, "part")),
8037 )),
8038 },
8039 )
8040 };
8041 let main_fn = node(
8042 40,
8043 NodeKind::FnDecl {
8044 annotations: vec![],
8045 visibility: Visibility::Private,
8046 is_async: false,
8047 name: ident("main"),
8048 generic_params: vec![],
8049 params: vec![],
8050 return_type: None,
8051 effect_clause: vec![],
8052 where_clause: vec![],
8053 body: Box::new(block(
8054 41,
8055 vec![make_handling(50, "first"), make_handling(70, "second")],
8056 None,
8057 )),
8058 },
8059 );
8060
8061 let out = gen(&module(vec![], vec![main_fn]));
8062 assert_eq!(
8063 out.matches("const part = ").count(),
8064 2,
8065 "each sibling handling block should declare its own `const part`, got: {out}"
8066 );
8067 assert!(
8068 !out.contains("\n part = "),
8069 "no sibling handling block may rewrite its `let part` into a bare \
8070 assignment, got: {out}"
8071 );
8072 }
8073
8074 #[test]
8075 fn composite_effect_expands_to_components() {
8076 use bock_air::AirHandlerPair;
8077
8078 let logger_decl = node(
8080 1,
8081 NodeKind::EffectDecl {
8082 annotations: vec![],
8083 visibility: Visibility::Public,
8084 name: ident("Logger"),
8085 generic_params: vec![],
8086 components: vec![],
8087 operations: vec![node(
8088 2,
8089 NodeKind::FnDecl {
8090 annotations: vec![],
8091 visibility: Visibility::Public,
8092 is_async: false,
8093 name: ident("log"),
8094 generic_params: vec![],
8095 params: vec![param_node(3, "msg")],
8096 return_type: None,
8097 effect_clause: vec![],
8098 where_clause: vec![],
8099 body: Box::new(block(4, vec![], None)),
8100 },
8101 )],
8102 },
8103 );
8104
8105 let clock_decl = node(
8107 5,
8108 NodeKind::EffectDecl {
8109 annotations: vec![],
8110 visibility: Visibility::Public,
8111 name: ident("Clock"),
8112 generic_params: vec![],
8113 components: vec![],
8114 operations: vec![node(
8115 6,
8116 NodeKind::FnDecl {
8117 annotations: vec![],
8118 visibility: Visibility::Public,
8119 is_async: false,
8120 name: ident("now"),
8121 generic_params: vec![],
8122 params: vec![],
8123 return_type: None,
8124 effect_clause: vec![],
8125 where_clause: vec![],
8126 body: Box::new(block(7, vec![], None)),
8127 },
8128 )],
8129 },
8130 );
8131
8132 let composite_decl = node(
8134 8,
8135 NodeKind::EffectDecl {
8136 annotations: vec![],
8137 visibility: Visibility::Public,
8138 name: ident("ServiceStack"),
8139 generic_params: vec![],
8140 components: vec![type_path(&["Logger"]), type_path(&["Clock"])],
8141 operations: vec![],
8142 },
8143 );
8144
8145 let serve_fn = node(
8147 10,
8148 NodeKind::FnDecl {
8149 annotations: vec![],
8150 visibility: Visibility::Private,
8151 is_async: false,
8152 name: ident("serve"),
8153 generic_params: vec![],
8154 params: vec![param_node(11, "request")],
8155 return_type: None,
8156 effect_clause: vec![type_path(&["ServiceStack"])],
8157 where_clause: vec![],
8158 body: Box::new(block(12, vec![], Some(str_lit(13, "ok")))),
8159 },
8160 );
8161
8162 let call_serve = node(
8164 20,
8165 NodeKind::Call {
8166 callee: Box::new(id_node(21, "serve")),
8167 args: vec![bock_air::AirArg {
8168 label: None,
8169 value: str_lit(22, "GET /"),
8170 }],
8171 type_args: vec![],
8172 },
8173 );
8174 let handling = node(
8175 30,
8176 NodeKind::HandlingBlock {
8177 handlers: vec![
8178 AirHandlerPair {
8179 effect: type_path(&["Logger"]),
8180 handler: Box::new(node(
8181 31,
8182 NodeKind::Call {
8183 callee: Box::new(id_node(32, "StdLogger")),
8184 args: vec![],
8185 type_args: vec![],
8186 },
8187 )),
8188 },
8189 AirHandlerPair {
8190 effect: type_path(&["Clock"]),
8191 handler: Box::new(node(
8192 33,
8193 NodeKind::Call {
8194 callee: Box::new(id_node(34, "StdClock")),
8195 args: vec![],
8196 type_args: vec![],
8197 },
8198 )),
8199 },
8200 ],
8201 body: Box::new(block(35, vec![], Some(call_serve))),
8202 },
8203 );
8204 let main_fn = node(
8205 40,
8206 NodeKind::FnDecl {
8207 annotations: vec![],
8208 visibility: Visibility::Private,
8209 is_async: false,
8210 name: ident("main"),
8211 generic_params: vec![],
8212 params: vec![],
8213 return_type: None,
8214 effect_clause: vec![],
8215 where_clause: vec![],
8216 body: Box::new(block(41, vec![handling], None)),
8217 },
8218 );
8219
8220 let out = gen(&module(
8221 vec![],
8222 vec![logger_decl, clock_decl, composite_decl, serve_fn, main_fn],
8223 ));
8224
8225 assert!(
8227 out.contains("// composite effect ServiceStack = Logger + Clock"),
8228 "composite effect should be a comment, got: {out}"
8229 );
8230 assert!(
8231 !out.contains("class ServiceStack"),
8232 "composite effect should NOT generate a class, got: {out}"
8233 );
8234 assert!(
8239 out.contains(
8240 "const ServiceStack = Object.freeze({ __composite: [\"Logger\", \"Clock\"] });"
8241 ),
8242 "composite effect should emit an exportable binding, got: {out}"
8243 );
8244
8245 assert!(
8247 out.contains("function serve(request, { logger, clock })"),
8248 "serve should have expanded effect params, got: {out}"
8249 );
8250
8251 assert!(
8253 out.contains("logger: __logger") && out.contains("clock: __clock"),
8254 "call should pass expanded handler args, got: {out}"
8255 );
8256 }
8257
8258 #[test]
8259 fn record_becomes_class_for_prototype_impls() {
8260 use bock_air::AirHandlerPair;
8261
8262 let rec = node(
8263 1,
8264 NodeKind::RecordDecl {
8265 annotations: vec![],
8266 visibility: Visibility::Public,
8267 name: ident("ConsoleLogger"),
8268 generic_params: vec![],
8269 fields: vec![],
8270 },
8271 );
8272 let out = gen(&module(vec![], vec![rec]));
8273 assert!(
8274 out.contains("class ConsoleLogger {}"),
8275 "empty record should be an empty class, got: {out}"
8276 );
8277 let _ = AirHandlerPair {
8278 effect: type_path(&["X"]),
8280 handler: Box::new(id_node(0, "x")),
8281 };
8282 }
8283
8284 #[test]
8285 fn record_construct_of_declared_record_uses_new() {
8286 let rec = node(
8287 1,
8288 NodeKind::RecordDecl {
8289 annotations: vec![],
8290 visibility: Visibility::Public,
8291 name: ident("ConsoleLogger"),
8292 generic_params: vec![],
8293 fields: vec![],
8294 },
8295 );
8296 let construct = node(
8297 2,
8298 NodeKind::RecordConstruct {
8299 path: type_path(&["ConsoleLogger"]),
8300 fields: vec![],
8301 spread: None,
8302 },
8303 );
8304 let let_stmt = node(
8305 3,
8306 NodeKind::LetBinding {
8307 is_mut: false,
8308 pattern: Box::new(bind_pat(4, "x")),
8309 ty: None,
8310 value: Box::new(construct),
8311 },
8312 );
8313 let f = node(
8314 5,
8315 NodeKind::FnDecl {
8316 annotations: vec![],
8317 visibility: Visibility::Private,
8318 is_async: false,
8319 name: ident("test"),
8320 generic_params: vec![],
8321 params: vec![],
8322 return_type: None,
8323 effect_clause: vec![],
8324 where_clause: vec![],
8325 body: Box::new(block(6, vec![let_stmt], None)),
8326 },
8327 );
8328 let out = gen(&module(vec![], vec![rec, f]));
8329 assert!(
8330 out.contains("new ConsoleLogger()"),
8331 "declared record construct should use `new`, got: {out}"
8332 );
8333 }
8334
8335 #[test]
8336 fn module_handle_registers_handler_for_same_module_calls() {
8337 use bock_air::AirHandlerPair;
8338 let _ = AirHandlerPair {
8339 effect: type_path(&["X"]),
8340 handler: Box::new(id_node(0, "x")),
8341 };
8342
8343 let effect_decl = node(
8345 1,
8346 NodeKind::EffectDecl {
8347 annotations: vec![],
8348 visibility: Visibility::Public,
8349 name: ident("Logger"),
8350 generic_params: vec![],
8351 components: vec![],
8352 operations: vec![node(
8353 2,
8354 NodeKind::FnDecl {
8355 annotations: vec![],
8356 visibility: Visibility::Public,
8357 is_async: false,
8358 name: ident("log"),
8359 generic_params: vec![],
8360 params: vec![param_node(3, "msg")],
8361 return_type: None,
8362 effect_clause: vec![],
8363 where_clause: vec![],
8364 body: Box::new(block(4, vec![], None)),
8365 },
8366 )],
8367 },
8368 );
8369
8370 let rec = node(
8372 5,
8373 NodeKind::RecordDecl {
8374 annotations: vec![],
8375 visibility: Visibility::Public,
8376 name: ident("StdoutLogger"),
8377 generic_params: vec![],
8378 fields: vec![],
8379 },
8380 );
8381
8382 let greet = node(
8384 10,
8385 NodeKind::FnDecl {
8386 annotations: vec![],
8387 visibility: Visibility::Public,
8388 is_async: false,
8389 name: ident("greet"),
8390 generic_params: vec![],
8391 params: vec![],
8392 return_type: None,
8393 effect_clause: vec![type_path(&["Logger"])],
8394 where_clause: vec![],
8395 body: Box::new(block(11, vec![], Some(str_lit(12, "hi")))),
8396 },
8397 );
8398
8399 let module_handle = node(
8401 20,
8402 NodeKind::ModuleHandle {
8403 effect: type_path(&["Logger"]),
8404 handler: Box::new(node(
8405 21,
8406 NodeKind::RecordConstruct {
8407 path: type_path(&["StdoutLogger"]),
8408 fields: vec![],
8409 spread: None,
8410 },
8411 )),
8412 },
8413 );
8414
8415 let call_greet = node(
8417 30,
8418 NodeKind::Call {
8419 callee: Box::new(id_node(31, "greet")),
8420 args: vec![],
8421 type_args: vec![],
8422 },
8423 );
8424 let main_fn = node(
8425 32,
8426 NodeKind::FnDecl {
8427 annotations: vec![],
8428 visibility: Visibility::Private,
8429 is_async: false,
8430 name: ident("main"),
8431 generic_params: vec![],
8432 params: vec![],
8433 return_type: None,
8434 effect_clause: vec![],
8435 where_clause: vec![],
8436 body: Box::new(block(33, vec![], Some(call_greet))),
8437 },
8438 );
8439
8440 let out = gen(&module(
8441 vec![],
8442 vec![effect_decl, rec, greet, module_handle, main_fn],
8443 ));
8444
8445 assert!(
8447 out.contains("const __logger = new StdoutLogger()"),
8448 "module handle should use `new` on declared record, got: {out}"
8449 );
8450 assert!(
8452 out.contains("greet({ logger: __logger })"),
8453 "module handle should be threaded into effectful calls, got: {out}"
8454 );
8455 }
8456
8457 #[test]
8460 fn entry_invocation_sync_main() {
8461 let inv = JsGenerator::new().entry_invocation(false).unwrap();
8462 assert_eq!(inv, "main();\n");
8463 }
8464
8465 #[test]
8466 fn entry_invocation_async_main() {
8467 let inv = JsGenerator::new().entry_invocation(true).unwrap();
8468 assert!(inv.contains("async () =>"));
8469 assert!(inv.contains("await main()"));
8470 }
8471
8472 #[test]
8473 fn generate_project_async_main_wraps_entry() {
8474 let main_fn = node(
8475 1,
8476 NodeKind::FnDecl {
8477 annotations: vec![],
8478 visibility: Visibility::Private,
8479 is_async: true,
8480 name: ident("main"),
8481 generic_params: vec![],
8482 params: vec![],
8483 return_type: None,
8484 effect_clause: vec![],
8485 where_clause: vec![],
8486 body: Box::new(block(2, vec![], None)),
8487 },
8488 );
8489 let m = module(vec![], vec![main_fn]);
8490 let gen = JsGenerator::new();
8491 let src_path = std::path::Path::new("src/main.bock");
8492 let out = gen.generate_project(&[(&m, src_path)]).unwrap();
8493 let src = &out.files[0].content;
8494 assert_eq!(out.files[0].path, std::path::PathBuf::from("main.js"));
8495 assert!(src.contains("async function main()"), "got: {src}");
8496 assert!(
8497 src.contains("(async () => { await main(); })();"),
8498 "async entry wrapper missing, got: {src}"
8499 );
8500 }
8501
8502 fn module_with_path(path: &[&str], imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
8506 node(
8507 0,
8508 NodeKind::Module {
8509 path: Some(bock_ast::ModulePath {
8510 segments: path.iter().map(|s| ident(s)).collect(),
8511 span: span(),
8512 }),
8513 annotations: vec![],
8514 imports,
8515 items,
8516 },
8517 )
8518 }
8519
8520 fn import_named(id: u32, path: &[&str], name: &str) -> AIRNode {
8522 node(
8523 id,
8524 NodeKind::ImportDecl {
8525 path: bock_ast::ModulePath {
8526 segments: path.iter().map(|s| ident(s)).collect(),
8527 span: span(),
8528 },
8529 items: bock_ast::ImportItems::Named(vec![bock_ast::ImportedName {
8530 span: span(),
8531 name: ident(name),
8532 alias: None,
8533 }]),
8534 },
8535 )
8536 }
8537
8538 fn fn_decl_tail(id: u32, vis: Visibility, name: &str, tail: AIRNode) -> AIRNode {
8541 node(
8542 id,
8543 NodeKind::FnDecl {
8544 annotations: vec![],
8545 visibility: vis,
8546 is_async: false,
8547 name: ident(name),
8548 generic_params: vec![],
8549 params: vec![],
8550 return_type: None,
8551 effect_clause: vec![],
8552 where_clause: vec![],
8553 body: Box::new(block(id + 1, vec![], Some(tail))),
8554 },
8555 )
8556 }
8557
8558 #[test]
8559 fn per_module_emits_native_esm_import_tree() {
8560 let call = node(
8567 10,
8568 NodeKind::Call {
8569 callee: Box::new(id_node(11, "add_one")),
8570 args: vec![bock_air::AirArg {
8571 label: None,
8572 value: int_lit(12, "6"),
8573 }],
8574 type_args: vec![],
8575 },
8576 );
8577 let main_mod = module_with_path(
8578 &["main"],
8579 vec![import_named(5, &["mathutil"], "add_one")],
8580 vec![fn_decl_tail(1, Visibility::Private, "main", call)],
8581 );
8582 let util_mod = module_with_path(
8583 &["mathutil"],
8584 vec![],
8585 vec![fn_decl_tail(
8586 20,
8587 Visibility::Public,
8588 "add_one",
8589 int_lit(22, "7"),
8590 )],
8591 );
8592
8593 let gen = JsGenerator::new();
8594 let out = gen
8595 .generate_project(&[
8596 (&main_mod, std::path::Path::new("src/main.bock")),
8597 (&util_mod, std::path::Path::new("src/mathutil.bock")),
8598 ])
8599 .unwrap();
8600
8601 let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
8602 let main_file = by_name("main.js").expect("main.js emitted");
8603 let util_file = by_name("mathutil.js").expect("mathutil.js emitted");
8604 assert!(
8607 by_name("package.json").is_none(),
8608 "codegen must NOT emit package.json — the scaffolder owns it (S6a)"
8609 );
8610
8611 assert!(
8612 main_file
8613 .content
8614 .contains("import { addOne } from \"./mathutil.js\";"),
8615 "main.js must import the camelCased fn from the sibling; got:\n{}",
8616 main_file.content
8617 );
8618 assert!(
8619 main_file.content.contains("main();"),
8620 "main.js must carry the entry invocation; got:\n{}",
8621 main_file.content
8622 );
8623 assert!(
8624 util_file.content.contains("export function addOne("),
8625 "mathutil.js must export the fn inline; got:\n{}",
8626 util_file.content
8627 );
8628 }
8629
8630 #[test]
8631 fn per_module_reexports_record_and_constructs_cross_module() {
8632 let point = node(
8636 30,
8637 NodeKind::RecordDecl {
8638 annotations: vec![],
8639 visibility: Visibility::Public,
8640 name: ident("Point"),
8641 generic_params: vec![],
8642 fields: vec![],
8643 },
8644 );
8645 let shapes_mod = module_with_path(&["shapes"], vec![], vec![point]);
8646 let ctor = node(
8647 10,
8648 NodeKind::RecordConstruct {
8649 path: type_path(&["Point"]),
8650 fields: vec![],
8651 spread: None,
8652 },
8653 );
8654 let main_mod = module_with_path(
8655 &["main"],
8656 vec![import_named(5, &["shapes"], "Point")],
8657 vec![fn_decl_tail(1, Visibility::Private, "main", ctor)],
8658 );
8659
8660 let gen = JsGenerator::new();
8661 let out = gen
8662 .generate_project(&[
8663 (&main_mod, std::path::Path::new("src/main.bock")),
8664 (&shapes_mod, std::path::Path::new("src/shapes.bock")),
8665 ])
8666 .unwrap();
8667 let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
8668 let main_file = by_name("main.js").expect("main.js emitted");
8669 let shapes_file = by_name("shapes.js").expect("shapes.js emitted");
8670 assert!(
8671 shapes_file.content.contains("export { Point };"),
8672 "shapes.js must re-export the record; got:\n{}",
8673 shapes_file.content
8674 );
8675 assert!(
8676 main_file
8677 .content
8678 .contains("import { Point } from \"./shapes.js\";"),
8679 "main.js must import the record; got:\n{}",
8680 main_file.content
8681 );
8682 assert!(
8685 main_file.content.contains("new Point("),
8686 "cross-module record construction must use `new`; got:\n{}",
8687 main_file.content
8688 );
8689 }
8690
8691 #[test]
8696 fn match_call_scrutinee_hoisted_to_temp() {
8697 let scrutinee = node(
8699 10,
8700 NodeKind::Call {
8701 callee: Box::new(id_node(11, "f")),
8702 args: vec![],
8703 type_args: vec![],
8704 },
8705 );
8706 let some_arm = node(
8707 20,
8708 NodeKind::MatchArm {
8709 pattern: Box::new(node(
8710 21,
8711 NodeKind::ConstructorPat {
8712 path: type_path(&["Some"]),
8713 fields: vec![bind_pat(22, "x")],
8714 },
8715 )),
8716 guard: None,
8717 body: Box::new(block(23, vec![], Some(id_node(24, "x")))),
8718 },
8719 );
8720 let none_arm = node(
8721 30,
8722 NodeKind::MatchArm {
8723 pattern: Box::new(node(
8724 31,
8725 NodeKind::ConstructorPat {
8726 path: type_path(&["None"]),
8727 fields: vec![],
8728 },
8729 )),
8730 guard: None,
8731 body: Box::new(block(32, vec![], Some(int_lit(33, "0")))),
8732 },
8733 );
8734 let match_stmt = node(
8735 40,
8736 NodeKind::Match {
8737 scrutinee: Box::new(scrutinee),
8738 arms: vec![some_arm, none_arm],
8739 },
8740 );
8741 let f = node(
8742 1,
8743 NodeKind::FnDecl {
8744 annotations: vec![],
8745 visibility: Visibility::Private,
8746 is_async: false,
8747 name: ident("run"),
8748 generic_params: vec![],
8749 params: vec![],
8750 return_type: None,
8751 effect_clause: vec![],
8752 where_clause: vec![],
8753 body: Box::new(block(2, vec![match_stmt], None)),
8754 },
8755 );
8756 let out = gen(&module(vec![], vec![f]));
8757 assert!(
8758 out.contains("const __match1 = f();"),
8759 "call scrutinee should be hoisted to a temp, got: {out}"
8760 );
8761 assert!(
8762 out.contains("switch (__match1._tag)"),
8763 "switch should dispatch on the hoisted temp, got: {out}"
8764 );
8765 assert!(
8766 out.contains("const x = __match1._0;"),
8767 "payload binding should read the hoisted temp, got: {out}"
8768 );
8769 assert!(
8770 !out.contains("f()._tag") && !out.contains("f()._0"),
8771 "call scrutinee must not be re-emitted inline, got: {out}"
8772 );
8773 }
8774
8775 #[test]
8776 fn method_colliding_with_field_is_disambiguated() {
8777 let record_decl = node(
8779 1,
8780 NodeKind::RecordDecl {
8781 annotations: vec![],
8782 visibility: Visibility::Public,
8783 name: ident("SimpleError"),
8784 generic_params: vec![],
8785 fields: vec![bock_ast::RecordDeclField {
8786 id: 0,
8787 span: span(),
8788 name: ident("message"),
8789 ty: bock_ast::TypeExpr::Named {
8790 id: 0,
8791 span: span(),
8792 path: type_path(&["String"]),
8793 args: vec![],
8794 },
8795 default: None,
8796 }],
8797 },
8798 );
8799 let method = node(
8801 10,
8802 NodeKind::FnDecl {
8803 annotations: vec![],
8804 visibility: Visibility::Public,
8805 is_async: false,
8806 name: ident("message"),
8807 generic_params: vec![],
8808 params: vec![param_node(11, "self")],
8809 return_type: None,
8810 effect_clause: vec![],
8811 where_clause: vec![],
8812 body: Box::new(block(
8813 12,
8814 vec![],
8815 Some(node(
8816 13,
8817 NodeKind::FieldAccess {
8818 object: Box::new(id_node(14, "self")),
8819 field: ident("message"),
8820 },
8821 )),
8822 )),
8823 },
8824 );
8825 let impl_block = node(
8826 20,
8827 NodeKind::ImplBlock {
8828 annotations: vec![],
8829 target: Box::new(node(
8830 21,
8831 NodeKind::TypeNamed {
8832 path: type_path(&["SimpleError"]),
8833 args: vec![],
8834 },
8835 )),
8836 trait_path: Some(type_path(&["Error"])),
8837 trait_args: vec![],
8838 generic_params: vec![],
8839 where_clause: vec![],
8840 methods: vec![method],
8841 },
8842 );
8843 let read_fn = node(
8845 30,
8846 NodeKind::FnDecl {
8847 annotations: vec![],
8848 visibility: Visibility::Public,
8849 is_async: false,
8850 name: ident("read"),
8851 generic_params: vec![],
8852 params: vec![param_node(31, "e")],
8853 return_type: None,
8854 effect_clause: vec![],
8855 where_clause: vec![],
8856 body: Box::new(block(
8857 32,
8858 vec![],
8859 Some(node(
8860 33,
8861 NodeKind::Call {
8862 callee: Box::new(node(
8863 34,
8864 NodeKind::FieldAccess {
8865 object: Box::new(id_node(35, "e")),
8870 field: ident("message"),
8871 },
8872 )),
8873 type_args: vec![],
8874 args: vec![AirArg {
8875 label: None,
8876 value: id_node(35, "e"),
8877 }],
8878 },
8879 )),
8880 )),
8881 },
8882 );
8883 let out = gen(&module(vec![], vec![record_decl, impl_block, read_fn]));
8884 assert!(
8886 out.contains("this.message = message"),
8887 "field should remain `message`, got: {out}"
8888 );
8889 assert!(
8891 out.contains("SimpleError.prototype.messageMethod = "),
8892 "prototype method should be `messageMethod`, got: {out}"
8893 );
8894 assert!(
8895 out.contains(".messageMethod(e)"),
8896 "call site should be `.messageMethod(e)`, got: {out}"
8897 );
8898 assert!(
8900 out.contains("return self.message;"),
8901 "method body should read the field `self.message`, got: {out}"
8902 );
8903 }
8904
8905 fn let_binding(id: u32, name: &str, is_mut: bool, value: AIRNode) -> AIRNode {
8909 node(
8910 id,
8911 NodeKind::LetBinding {
8912 is_mut,
8913 pattern: Box::new(node(
8914 id + 1,
8915 NodeKind::BindPat {
8916 name: ident(name),
8917 is_mut,
8918 },
8919 )),
8920 ty: None,
8921 value: Box::new(value),
8922 },
8923 )
8924 }
8925
8926 fn fn_decl(id: u32, name: &str, params: Vec<AIRNode>, body: AIRNode) -> AIRNode {
8927 node(
8928 id,
8929 NodeKind::FnDecl {
8930 annotations: vec![],
8931 visibility: Visibility::Private,
8932 is_async: false,
8933 name: ident(name),
8934 generic_params: vec![],
8935 params,
8936 return_type: None,
8937 effect_clause: vec![],
8938 where_clause: vec![],
8939 body: Box::new(body),
8940 },
8941 )
8942 }
8943
8944 fn match_arm(id: u32, pattern: AIRNode, guard: Option<AIRNode>, body: AIRNode) -> AIRNode {
8945 node(
8946 id,
8947 NodeKind::MatchArm {
8948 pattern: Box::new(pattern),
8949 guard: guard.map(Box::new),
8950 body: Box::new(body),
8951 },
8952 )
8953 }
8954
8955 #[test]
8956 fn rebound_let_lowers_to_assignment_not_redeclaration() {
8957 let body = block(
8959 1,
8960 vec![
8961 let_binding(10, "acc", false, int_lit(11, "1")),
8962 let_binding(20, "acc", false, int_lit(21, "2")),
8963 let_binding(30, "acc", false, int_lit(31, "3")),
8964 ],
8965 None,
8966 );
8967 let out = gen(&module(vec![], vec![fn_decl(2, "f", vec![], body)]));
8968 assert!(
8970 out.contains("let acc = 1;"),
8971 "first re-bound `let` should declare with `let`, got: {out}"
8972 );
8973 assert_eq!(
8974 out.matches("acc = ").count(),
8975 3,
8976 "all three bindings should reference `acc`, got: {out}"
8977 );
8978 assert!(
8979 !out.contains("const acc"),
8980 "a re-bound binding must not emit `const acc`, got: {out}"
8981 );
8982 }
8983
8984 #[test]
8985 fn let_shadowing_a_param_lowers_to_assignment() {
8986 let rebind = let_binding(
8989 10,
8990 "x",
8991 false,
8992 node(
8993 12,
8994 NodeKind::BinaryOp {
8995 op: BinOp::Add,
8996 left: Box::new(id_node(13, "x")),
8997 right: Box::new(int_lit(14, "1")),
8998 },
8999 ),
9000 );
9001 let body = block(1, vec![rebind], Some(id_node(15, "x")));
9002 let out = gen(&module(
9003 vec![],
9004 vec![fn_decl(2, "f", vec![param_node(3, "x")], body)],
9005 ));
9006 assert!(
9007 out.contains("x = (x + 1);"),
9008 "let shadowing a param should assign, got: {out}"
9009 );
9010 assert!(
9011 !out.contains("let x = (x + 1)") && !out.contains("const x = (x + 1)"),
9012 "let shadowing a param must not redeclare `x`, got: {out}"
9013 );
9014 }
9015
9016 #[test]
9017 fn sibling_iife_blocks_do_not_share_let_scope() {
9018 let arm1 = match_arm(
9021 40,
9022 node(
9023 41,
9024 NodeKind::LiteralPat {
9025 lit: Literal::Int("0".into()),
9026 },
9027 ),
9028 None,
9029 block(
9030 42,
9031 vec![let_binding(43, "x", false, int_lit(44, "1"))],
9032 Some(id_node(45, "x")),
9033 ),
9034 );
9035 let arm2 = match_arm(
9036 50,
9037 node(51, NodeKind::WildcardPat),
9038 None,
9039 block(
9040 52,
9041 vec![let_binding(53, "x", false, int_lit(54, "2"))],
9042 Some(id_node(55, "x")),
9043 ),
9044 );
9045 let m = node(
9046 60,
9047 NodeKind::Match {
9048 scrutinee: Box::new(id_node(61, "n")),
9049 arms: vec![arm1, arm2],
9050 },
9051 );
9052 let body = block(1, vec![], Some(m));
9053 let out = gen(&module(
9054 vec![],
9055 vec![fn_decl(2, "f", vec![param_node(3, "n")], body)],
9056 ));
9057 assert_eq!(
9060 out.matches("const x = ").count(),
9061 2,
9062 "sibling IIFE arms should each declare their own `const x`, got: {out}"
9063 );
9064 }
9065
9066 #[test]
9067 fn list_pattern_match_routes_to_ifchain() {
9068 let empty_arm = match_arm(
9070 10,
9071 node(
9072 11,
9073 NodeKind::ListPat {
9074 elems: vec![],
9075 rest: None,
9076 },
9077 ),
9078 None,
9079 block(12, vec![], Some(int_lit(13, "0"))),
9080 );
9081 let cons_arm = match_arm(
9082 20,
9083 node(
9084 21,
9085 NodeKind::ListPat {
9086 elems: vec![bind_pat(22, "first")],
9087 rest: Some(Box::new(bind_pat(23, "rest"))),
9088 },
9089 ),
9090 None,
9091 block(24, vec![], Some(id_node(25, "first"))),
9092 );
9093 let m = node(
9094 30,
9095 NodeKind::Match {
9096 scrutinee: Box::new(id_node(31, "xs")),
9097 arms: vec![empty_arm, cons_arm],
9098 },
9099 );
9100 let body = block(1, vec![], Some(m));
9101 let out = gen(&module(
9102 vec![],
9103 vec![fn_decl(2, "f", vec![param_node(3, "xs")], body)],
9104 ));
9105 assert!(
9109 !out.contains("switch"),
9110 "list-pattern match must not lower to a switch, got: {out}"
9111 );
9112 assert!(
9113 out.contains("xs.length === 0"),
9114 "empty-list arm should test `length === 0`, got: {out}"
9115 );
9116 assert!(
9120 out.contains("const first = xs[0];"),
9121 "cons arm should bind the head element, got: {out}"
9122 );
9123 assert!(
9124 out.contains("const rest = xs.slice(1);"),
9125 "`..rest` should bind the trailing slice, got: {out}"
9126 );
9127 }
9128
9129 #[test]
9130 fn range_pattern_match_routes_to_ifchain() {
9131 let range_arm = match_arm(
9133 10,
9134 node(
9135 11,
9136 NodeKind::RangePat {
9137 lo: Box::new(int_lit(12, "1")),
9138 hi: Box::new(int_lit(13, "10")),
9139 inclusive: false,
9140 },
9141 ),
9142 None,
9143 block(14, vec![], Some(str_lit(15, "lo"))),
9144 );
9145 let wild_arm = match_arm(
9146 20,
9147 node(21, NodeKind::WildcardPat),
9148 None,
9149 block(22, vec![], Some(str_lit(23, "hi"))),
9150 );
9151 let m = node(
9152 30,
9153 NodeKind::Match {
9154 scrutinee: Box::new(id_node(31, "n")),
9155 arms: vec![range_arm, wild_arm],
9156 },
9157 );
9158 let body = block(1, vec![], Some(m));
9159 let out = gen(&module(
9160 vec![],
9161 vec![fn_decl(2, "f", vec![param_node(3, "n")], body)],
9162 ));
9163 assert!(
9164 !out.contains("switch"),
9165 "range-pattern match must not lower to a switch, got: {out}"
9166 );
9167 assert!(
9168 out.contains("n >= 1 && n < 10"),
9169 "exclusive range should test `>= lo && < hi`, got: {out}"
9170 );
9171 }
9172
9173 #[test]
9174 fn mut_bind_arm_emits_let_not_const() {
9175 let mut_pat = node(
9177 11,
9178 NodeKind::BindPat {
9179 name: ident("x"),
9180 is_mut: true,
9181 },
9182 );
9183 let assign = node(
9184 12,
9185 NodeKind::Assign {
9186 op: AssignOp::Assign,
9187 target: Box::new(id_node(13, "x")),
9188 value: Box::new(node(
9189 14,
9190 NodeKind::BinaryOp {
9191 op: BinOp::Add,
9192 left: Box::new(id_node(15, "x")),
9193 right: Box::new(int_lit(16, "1")),
9194 },
9195 )),
9196 },
9197 );
9198 let arm = match_arm(
9199 10,
9200 mut_pat,
9201 None,
9202 block(17, vec![assign], Some(id_node(18, "x"))),
9203 );
9204 let m = node(
9205 30,
9206 NodeKind::Match {
9207 scrutinee: Box::new(id_node(31, "n")),
9208 arms: vec![arm],
9209 },
9210 );
9211 let body = block(1, vec![], Some(m));
9212 let out = gen(&module(
9213 vec![],
9214 vec![fn_decl(2, "f", vec![param_node(3, "n")], body)],
9215 ));
9216 assert!(
9217 out.contains("let x = n;"),
9218 "a `mut` arm binding should declare with `let`, got: {out}"
9219 );
9220 assert!(
9221 !out.contains("const x = n;"),
9222 "a `mut` arm binding must not be `const`, got: {out}"
9223 );
9224 }
9225
9226 #[test]
9227 fn guard_let_binds_pattern_into_enclosing_scope() {
9228 let guard = node(
9230 10,
9231 NodeKind::Guard {
9232 let_pattern: Some(Box::new(node(
9233 11,
9234 NodeKind::ConstructorPat {
9235 path: type_path(&["Ok"]),
9236 fields: vec![bind_pat(12, "v")],
9237 },
9238 ))),
9239 condition: Box::new(node(
9240 13,
9241 NodeKind::Call {
9242 callee: Box::new(id_node(14, "parse")),
9243 type_args: vec![],
9244 args: vec![AirArg {
9245 label: None,
9246 value: id_node(15, "s"),
9247 }],
9248 },
9249 )),
9250 else_block: Box::new(block(
9251 16,
9252 vec![node(17, NodeKind::Return { value: None })],
9253 None,
9254 )),
9255 },
9256 );
9257 let body = block(1, vec![guard], Some(id_node(18, "v")));
9258 let out = gen(&module(
9259 vec![],
9260 vec![fn_decl(2, "f", vec![param_node(3, "s")], body)],
9261 ));
9262 assert!(
9264 out.contains("._tag === \"Ok\""),
9265 "guard should test the constructor tag, got: {out}"
9266 );
9267 assert!(
9268 out.contains("const v = "),
9269 "guard should bind `v` into the enclosing scope, got: {out}"
9270 );
9271 }
9272
9273 #[test]
9274 fn eval_identifier_is_escaped_for_strict_mode() {
9275 let body = block(1, vec![], Some(int_lit(11, "0")));
9277 let out = gen(&module(vec![], vec![fn_decl(2, "eval", vec![], body)]));
9278 assert!(
9279 out.contains("function eval_("),
9280 "a fn named `eval` must be escaped to `eval_` (strict mode), got: {out}"
9281 );
9282 assert!(
9283 !out.contains("function eval("),
9284 "bare `function eval(` is a strict-mode SyntaxError, got: {out}"
9285 );
9286 }
9287
9288 fn diverging_value_if_fn() -> AIRNode {
9293 let then_b = block(2, vec![], Some(int_lit(3, "1")));
9294 let ret = node(
9295 5,
9296 NodeKind::Return {
9297 value: Some(Box::new(int_lit(6, "0"))),
9298 },
9299 );
9300 let else_b = block(4, vec![], Some(ret));
9301 let if_node = node(
9302 1,
9303 NodeKind::If {
9304 let_pattern: None,
9305 condition: Box::new(id_node(7, "c")),
9306 then_block: Box::new(then_b),
9307 else_block: Some(Box::new(else_b)),
9308 },
9309 );
9310 let let_x = node(
9311 10,
9312 NodeKind::LetBinding {
9313 is_mut: false,
9314 pattern: Box::new(bind_pat(11, "x")),
9315 ty: None,
9316 value: Box::new(if_node),
9317 },
9318 );
9319 let body = block(20, vec![let_x], Some(id_node(21, "x")));
9320 let f = node(
9321 30,
9322 NodeKind::FnDecl {
9323 annotations: vec![],
9324 visibility: Visibility::Private,
9325 is_async: false,
9326 name: ident("f"),
9327 generic_params: vec![],
9328 params: vec![],
9329 return_type: None,
9330 effect_clause: vec![],
9331 where_clause: vec![],
9332 body: Box::new(body),
9333 },
9334 );
9335 module(vec![], vec![f])
9336 }
9337
9338 #[test]
9339 fn diverging_value_if_hoists_to_stmt_form_no_iife() {
9340 let out = gen(&diverging_value_if_fn());
9341 assert!(
9342 !out.contains("/* unsupported */"),
9343 "diverging value-if must not emit `/* unsupported */`, got: {out}"
9344 );
9345 assert!(
9347 out.contains("bockCf0 = MessageType") || out.contains("bockCf0 = 1"),
9348 "value arm must assign the temp, got: {out}"
9349 );
9350 assert!(
9351 out.contains("return 0"),
9352 "diverging arm must keep its return (not wrapped in an IIFE), got: {out}"
9353 );
9354 }
9355
9356 fn call_node(id: u32, callee: &str, args: Vec<AIRNode>) -> AIRNode {
9359 node(
9360 id,
9361 NodeKind::Call {
9362 callee: Box::new(id_node(id + 1, callee)),
9363 type_args: vec![],
9364 args: args
9365 .into_iter()
9366 .map(|value| AirArg { label: None, value })
9367 .collect(),
9368 },
9369 )
9370 }
9371
9372 fn propagate(id: u32, expr: AIRNode) -> AIRNode {
9373 node(
9374 id,
9375 NodeKind::Propagate {
9376 expr: Box::new(expr),
9377 },
9378 )
9379 }
9380
9381 fn ctor_call(id: u32, name: &str, arg: AIRNode) -> AIRNode {
9382 node(
9383 id,
9384 NodeKind::Call {
9385 callee: Box::new(id_node(id + 1, name)),
9386 type_args: vec![],
9387 args: vec![AirArg {
9388 label: None,
9389 value: arg,
9390 }],
9391 },
9392 )
9393 }
9394
9395 #[test]
9396 fn propagate_in_let_rhs_unwraps_and_early_returns() {
9397 let let_v = node(
9399 10,
9400 NodeKind::LetBinding {
9401 is_mut: false,
9402 pattern: Box::new(bind_pat(11, "v")),
9403 ty: None,
9404 value: Box::new(propagate(12, call_node(13, "g", vec![id_node(15, "x")]))),
9405 },
9406 );
9407 let tail = ctor_call(20, "Ok", id_node(22, "v"));
9408 let body = block(1, vec![let_v], Some(tail));
9409 let out = gen(&module(
9410 vec![],
9411 vec![fn_decl(2, "f", vec![param_node(3, "x")], body)],
9412 ));
9413 assert!(
9416 out.contains("g(x)"),
9417 "the propagated expr must be evaluated, got: {out}"
9418 );
9419 assert!(
9420 out.contains("_tag === \"Err\"") || out.contains("_tag === \"None\""),
9421 "`?` must early-return on the failure tag, got: {out}"
9422 );
9423 assert!(
9424 out.contains("return __try"),
9425 "`?` must early-return the wrapped failure value, got: {out}"
9426 );
9427 assert!(
9428 out.contains("const v = __try0._0") || out.contains("const v = __try1._0"),
9429 "`?` must bind the unwrapped payload (`._0`), got: {out}"
9430 );
9431 }
9432
9433 #[test]
9434 fn propagate_in_statement_position_early_returns() {
9435 let stmt = propagate(10, call_node(11, "save", vec![id_node(13, "x")]));
9437 let tail = ctor_call(20, "Ok", int_lit(22, "0"));
9438 let body = block(1, vec![stmt], Some(tail));
9439 let out = gen(&module(
9440 vec![],
9441 vec![fn_decl(2, "f", vec![param_node(3, "x")], body)],
9442 ));
9443 assert!(
9444 out.contains("save(x)"),
9445 "the propagated call must be evaluated, got: {out}"
9446 );
9447 assert!(
9448 out.contains("_tag === \"Err\"") || out.contains("_tag === \"None\""),
9449 "statement-position `?` must early-return on the failure tag, got: {out}"
9450 );
9451 assert!(
9452 !out.contains("/* unsupported */"),
9453 "statement-position `?` must not emit `/* unsupported */`, got: {out}"
9454 );
9455 }
9456
9457 #[test]
9460 fn tail_position_loop_emits_as_statement_not_unsupported() {
9461 let brk = node(40, NodeKind::Break { value: None });
9464 let loop_body = block(41, vec![brk], None);
9465 let lp = node(
9466 42,
9467 NodeKind::Loop {
9468 body: Box::new(loop_body),
9469 },
9470 );
9471 let body = block(1, vec![], Some(lp));
9472 let out = gen(&module(vec![], vec![fn_decl(2, "f", vec![], body)]));
9473 assert!(
9474 !out.contains("/* unsupported */"),
9475 "a tail-position value-less loop must not emit `/* unsupported */`, got: {out}"
9476 );
9477 assert!(
9478 out.contains("while (true)"),
9479 "a tail-position loop must lower to `while (true)`, got: {out}"
9480 );
9481 assert!(
9482 !out.contains("return while"),
9483 "a loop must not be wrapped in `return`, got: {out}"
9484 );
9485 }
9486
9487 #[test]
9488 fn tail_position_guard_emits_as_statement() {
9489 let guard = node(
9492 10,
9493 NodeKind::Guard {
9494 let_pattern: Some(Box::new(node(
9495 11,
9496 NodeKind::ConstructorPat {
9497 path: type_path(&["Ok"]),
9498 fields: vec![bind_pat(12, "v")],
9499 },
9500 ))),
9501 condition: Box::new(call_node(13, "parse", vec![id_node(16, "s")])),
9502 else_block: Box::new(block(
9503 17,
9504 vec![node(18, NodeKind::Return { value: None })],
9505 None,
9506 )),
9507 },
9508 );
9509 let body = block(1, vec![], Some(guard));
9510 let out = gen(&module(
9511 vec![],
9512 vec![fn_decl(2, "f", vec![param_node(3, "s")], body)],
9513 ));
9514 assert!(
9515 !out.contains("/* unsupported */"),
9516 "a tail-position guard must not emit `/* unsupported */`, got: {out}"
9517 );
9518 assert!(
9519 out.contains("._tag === \"Ok\""),
9520 "the guard's pattern test must survive, got: {out}"
9521 );
9522 }
9523
9524 #[test]
9525 fn diverging_intrinsic_tail_is_a_bare_statement_not_return_throw() {
9526 let body = block(1, vec![], Some(call_node(10, "todo", vec![])));
9529 let out = gen(&module(vec![], vec![fn_decl(2, "f", vec![], body)]));
9530 assert!(
9531 out.contains("throw new Error(\"not implemented\")"),
9532 "`todo()` must lower to a throw, got: {out}"
9533 );
9534 assert!(
9535 !out.contains("return throw"),
9536 "`return throw …` is invalid JS; the throw must be a bare statement, got: {out}"
9537 );
9538 }
9539
9540 fn incl_range(id: u32, lo: &str, hi: &str) -> AIRNode {
9556 node(
9557 id,
9558 NodeKind::Range {
9559 lo: Box::new(int_lit(id + 1, lo)),
9560 hi: Box::new(int_lit(id + 2, hi)),
9561 inclusive: true,
9562 },
9563 )
9564 }
9565
9566 #[test]
9567 fn for_loop_body_tail_call_is_discarded_not_returned() {
9568 let loop_body = block(
9570 30,
9571 vec![],
9572 Some(call_node(31, "println", vec![id_node(33, "i")])),
9573 );
9574 let for_loop = node(
9575 10,
9576 NodeKind::For {
9577 pattern: Box::new(bind_pat(11, "i")),
9578 iterable: Box::new(incl_range(20, "1", "3")),
9579 body: Box::new(loop_body),
9580 },
9581 );
9582 let out = gen(&module(
9583 vec![],
9584 vec![fn_decl(1, "main", vec![], block(2, vec![for_loop], None))],
9585 ));
9586 assert!(
9587 !out.contains("return console.log"),
9588 "a for-loop body's tail call must be a discarded statement, not a \
9589 `return` (which aborts the loop after one iteration); got:\n{out}"
9590 );
9591 assert!(
9592 out.contains("console.log(i);"),
9593 "the loop body should still emit the call as a statement; got:\n{out}"
9594 );
9595 }
9596
9597 #[test]
9598 fn while_loop_body_tail_call_is_discarded_not_returned() {
9599 let loop_body = block(
9601 30,
9602 vec![],
9603 Some(call_node(31, "println", vec![str_lit(33, "x")])),
9604 );
9605 let while_loop = node(
9606 10,
9607 NodeKind::While {
9608 condition: Box::new(bool_lit(20, true)),
9609 body: Box::new(loop_body),
9610 },
9611 );
9612 let out = gen(&module(
9613 vec![],
9614 vec![fn_decl(1, "main", vec![], block(2, vec![while_loop], None))],
9615 ));
9616 assert!(
9617 !out.contains("return console.log"),
9618 "a while-loop body's tail call must be a discarded statement, not a \
9619 `return`; got:\n{out}"
9620 );
9621 assert!(
9622 out.contains("console.log(\"x\");"),
9623 "the loop body should still emit the call as a statement; got:\n{out}"
9624 );
9625 }
9626
9627 #[test]
9628 fn infinite_loop_body_tail_call_is_discarded_not_returned() {
9629 let loop_body = block(
9631 30,
9632 vec![],
9633 Some(call_node(31, "println", vec![str_lit(33, "x")])),
9634 );
9635 let inf_loop = node(
9636 10,
9637 NodeKind::Loop {
9638 body: Box::new(loop_body),
9639 },
9640 );
9641 let out = gen(&module(
9642 vec![],
9643 vec![fn_decl(1, "main", vec![], block(2, vec![inf_loop], None))],
9644 ));
9645 assert!(
9646 !out.contains("return console.log"),
9647 "a `loop` body's tail call must be a discarded statement, not a \
9648 `return`; got:\n{out}"
9649 );
9650 }
9651
9652 #[test]
9653 fn statement_match_arm_tail_call_is_discarded_not_returned() {
9654 let ok_arm = match_arm(
9659 20,
9660 node(
9661 21,
9662 NodeKind::ConstructorPat {
9663 path: type_path(&["Ok"]),
9664 fields: vec![bind_pat(22, "v")],
9665 },
9666 ),
9667 None,
9668 block(
9669 23,
9670 vec![],
9671 Some(call_node(24, "println", vec![id_node(26, "v")])),
9672 ),
9673 );
9674 let err_arm = match_arm(
9675 30,
9676 node(
9677 31,
9678 NodeKind::ConstructorPat {
9679 path: type_path(&["Err"]),
9680 fields: vec![bind_pat(32, "e")],
9681 },
9682 ),
9683 None,
9684 block(
9685 33,
9686 vec![],
9687 Some(call_node(34, "println", vec![id_node(36, "e")])),
9688 ),
9689 );
9690 let match_stmt = node(
9691 40,
9692 NodeKind::Match {
9693 scrutinee: Box::new(id_node(41, "r")),
9694 arms: vec![ok_arm, err_arm],
9695 },
9696 );
9697 let trailer = call_node(50, "println", vec![str_lit(52, "done")]);
9698 let body = block(3, vec![match_stmt], Some(trailer));
9699 let out = gen(&module(
9700 vec![],
9701 vec![fn_decl(1, "run", vec![param_node(2, "r")], body)],
9702 ));
9703 assert!(
9704 out.contains("console.log(v);") && out.contains("console.log(e);"),
9705 "statement-position match arms should emit their tail call as a \
9706 discarded statement; got:\n{out}"
9707 );
9708 assert!(
9709 !out.contains("return console.log(v);") && !out.contains("return console.log(e);"),
9710 "a statement-position match arm's tail call must be a discarded \
9711 statement, not a `return` (which would skip the code after the \
9712 match); got:\n{out}"
9713 );
9714 }
9715
9716 #[test]
9717 fn loop_in_function_body_does_not_discard_the_function_tail() {
9718 let assign = node(
9722 40,
9723 NodeKind::Assign {
9724 op: AssignOp::Assign,
9725 target: Box::new(id_node(41, "sum")),
9726 value: Box::new(node(
9727 42,
9728 NodeKind::BinaryOp {
9729 op: BinOp::Add,
9730 left: Box::new(id_node(43, "sum")),
9731 right: Box::new(id_node(44, "i")),
9732 },
9733 )),
9734 },
9735 );
9736 let for_loop = node(
9737 10,
9738 NodeKind::For {
9739 pattern: Box::new(bind_pat(11, "i")),
9740 iterable: Box::new(incl_range(20, "1", "3")),
9741 body: Box::new(block(30, vec![assign], None)),
9742 },
9743 );
9744 let body = block(
9745 2,
9746 vec![let_binding(5, "sum", true, int_lit(6, "0")), for_loop],
9747 Some(id_node(7, "sum")),
9748 );
9749 let out = gen(&module(vec![], vec![fn_decl(1, "total", vec![], body)]));
9750 assert!(
9751 out.contains("return sum;"),
9752 "the function-body tail after a loop must still `return`; got:\n{out}"
9753 );
9754 }
9755
9756 #[test]
9757 fn value_position_match_in_loop_body_still_returns_arm_values() {
9758 let arm1 = match_arm(
9763 60,
9764 node(
9765 61,
9766 NodeKind::LiteralPat {
9767 lit: Literal::Int("1".into()),
9768 },
9769 ),
9770 None,
9771 str_lit(62, "one"),
9772 );
9773 let arm_def = match_arm(
9774 70,
9775 node(71, NodeKind::WildcardPat),
9776 None,
9777 str_lit(72, "many"),
9778 );
9779 let match_expr = node(
9780 50,
9781 NodeKind::Match {
9782 scrutinee: Box::new(id_node(51, "i")),
9783 arms: vec![arm1, arm_def],
9784 },
9785 );
9786 let let_s = let_binding(40, "s", false, match_expr);
9787 let for_loop = node(
9788 10,
9789 NodeKind::For {
9790 pattern: Box::new(bind_pat(11, "i")),
9791 iterable: Box::new(incl_range(20, "1", "3")),
9792 body: Box::new(block(30, vec![let_s], None)),
9793 },
9794 );
9795 let out = gen(&module(
9796 vec![],
9797 vec![fn_decl(1, "main", vec![], block(2, vec![for_loop], None))],
9798 ));
9799 assert!(
9800 out.contains("return \"one\";") && out.contains("return \"many\";"),
9801 "a value-position `match` IIFE inside a loop body must still `return` \
9802 its arm values; got:\n{out}"
9803 );
9804 }
9805
9806 #[test]
9807 fn lambda_in_loop_body_still_returns_its_tail() {
9808 let lambda = node(
9812 50,
9813 NodeKind::Lambda {
9814 params: vec![param_node(51, "x")],
9815 body: Box::new(block(52, vec![], Some(id_node(53, "x")))),
9816 },
9817 );
9818 let let_f = let_binding(40, "f", false, lambda);
9819 let for_loop = node(
9820 10,
9821 NodeKind::For {
9822 pattern: Box::new(bind_pat(11, "i")),
9823 iterable: Box::new(incl_range(20, "1", "3")),
9824 body: Box::new(block(30, vec![let_f], None)),
9825 },
9826 );
9827 let out = gen(&module(
9828 vec![],
9829 vec![fn_decl(1, "main", vec![], block(2, vec![for_loop], None))],
9830 ));
9831 assert!(
9832 out.contains("return x;"),
9833 "a lambda body's tail inside a loop must still `return`; got:\n{out}"
9834 );
9835 }
9836
9837 #[test]
9838 fn function_body_tail_call_still_returns() {
9839 let body = block(
9843 2,
9844 vec![],
9845 Some(call_node(10, "println", vec![str_lit(12, "hi")])),
9846 );
9847 let out = gen(&module(vec![], vec![fn_decl(1, "greet", vec![], body)]));
9848 assert!(
9849 out.contains("return console.log(\"hi\");"),
9850 "a function-body tail call must still `return`; got:\n{out}"
9851 );
9852 }
9853
9854 #[test]
9858 fn e2e_for_loop_prints_all_iterations_not_just_first() {
9859 if !has_node() {
9860 return;
9861 }
9862 let loop_body = block(
9864 30,
9865 vec![],
9866 Some(call_node(31, "println", vec![id_node(33, "i")])),
9867 );
9868 let for_loop = node(
9869 10,
9870 NodeKind::For {
9871 pattern: Box::new(bind_pat(11, "i")),
9872 iterable: Box::new(incl_range(20, "1", "5")),
9873 body: Box::new(loop_body),
9874 },
9875 );
9876 let code = gen(&module(
9877 vec![],
9878 vec![fn_decl(1, "main", vec![], block(2, vec![for_loop], None))],
9879 ));
9880 let full = format!("{code}\nmain();\n");
9881 assert!(check_js_syntax(&full), "emitted JS must be valid:\n{full}");
9882 let out = run_js(&full);
9883 let lines: Vec<&str> = out.lines().collect();
9884 assert_eq!(
9885 lines.len(),
9886 5,
9887 "the loop must print all 5 iterations (a `return` in the body would \
9888 stop after the first); got {} line(s):\n{out}\n--- source ---\n{full}",
9889 lines.len()
9890 );
9891 assert_eq!(lines, vec!["1", "2", "3", "4", "5"], "got:\n{out}");
9892 }
9893}