1use std::collections::{HashMap, HashSet};
11use std::fmt::Write;
12use std::path::PathBuf;
13
14use bock_air::{AIRNode, AirInterpolationPart, EnumVariantPayload, NodeKind, ResultVariant};
15use bock_ast::{AssignOp, BinOp, Literal, TypeExpr, UnaryOp, Visibility};
16use bock_errors::Span;
17use bock_types::AIRModule;
18
19use crate::error::CodegenError;
20use crate::generator::{CodeGenerator, GeneratedCode, OutputFile, SourceMap, SourceMapping};
21use crate::profile::TargetProfile;
22
23fn module_uses_concurrency(items: &[AIRNode]) -> bool {
29 items.iter().any(|n| {
30 let s = format!("{n:?}");
31 s.contains("\"Channel\"") || s.contains("\"spawn\"")
32 })
33}
34
35const OPTIONAL_RUNTIME_TS: &str = "\
45// ── Bock Optional runtime ──
46type BockOption<T> =
47 | { readonly _tag: \"Some\"; readonly _0: T }
48 | { readonly _tag: \"None\" };
49";
50
51fn module_uses_optional(items: &[AIRNode]) -> bool {
59 items.iter().any(|n| {
60 let s = format!("{n:?}");
61 s.contains("\"Optional\"")
62 || s.contains("TypeOptional")
63 || s.contains("\"Some\"")
64 || s.contains("\"None\"")
65 || s.contains("\"pop\"")
66 })
67}
68
69const RESULT_RUNTIME_TS: &str = "\
79// ── Bock Result runtime ──
80type BockResult<T, E> =
81 | { readonly _tag: \"Ok\"; readonly _0: T }
82 | { readonly _tag: \"Err\"; readonly _0: E };
83";
84
85fn module_uses_result(items: &[AIRNode]) -> bool {
88 items.iter().any(|n| {
89 let s = format!("{n:?}");
90 s.contains("\"Result\"")
91 || s.contains("ResultConstruct")
92 || s.contains("\"Ok\"")
93 || s.contains("\"Err\"")
94 })
95}
96
97const CONCURRENCY_RUNTIME_TS: &str = "\
98// ── Bock concurrency runtime ──
99type __BockChannel<T> = {
100 send(v: T): void;
101 recv(): Promise<T>;
102 close(): void;
103};
104const __bockChannelNew = <T>(): [__BockChannel<T>, __BockChannel<T>] => {
105 const queue: T[] = [];
106 const waiters: Array<(v: T) => void> = [];
107 const ch: __BockChannel<T> = {
108 send(v: T) {
109 if (waiters.length > 0) { waiters.shift()!(v); } else { queue.push(v); }
110 },
111 recv(): Promise<T> {
112 return new Promise<T>((resolve) => {
113 if (queue.length > 0) { resolve(queue.shift()!); }
114 else { waiters.push(resolve); }
115 });
116 },
117 close() {}
118 };
119 return [ch, ch];
120};
121const __bockSpawn = <T>(x: Promise<T>): Promise<T> => x;
122";
123
124const RANGE_RUNTIME_TS: &str = "\
132// ── Bock range runtime ──
133const range = (lo: number, hi: number): number[] => { const r: number[] = []; for (let i = lo; i < hi; i++) r.push(i); return r; };
134const rangeInclusive = (lo: number, hi: number): number[] => { const r: number[] = []; for (let i = lo; i <= hi; i++) r.push(i); return r; };
135";
136
137fn module_uses_range(items: &[AIRNode]) -> bool {
142 items.iter().any(|n| format!("{n:?}").contains("Range {"))
143}
144
145const EQ_RUNTIME_TS: &str = "\
156// ── Bock structural equality runtime ──
157const __bockEq = (a: unknown, b: unknown): boolean => {
158 if (a === b) return true;
159 if (typeof a !== \"object\" || typeof b !== \"object\" || a === null || b === null) {
160 return a === b;
161 }
162 const ea = (a as { eq?: unknown }).eq;
163 const eb = (b as { eq?: unknown }).eq;
164 if (typeof ea === \"function\" && typeof eb === \"function\") {
165 return (ea as (s: unknown, o: unknown) => boolean).call(a, a, b);
166 }
167 if (Array.isArray(a)) {
168 if (!Array.isArray(b) || a.length !== b.length) return false;
169 for (let i = 0; i < a.length; i++) { if (!__bockEq(a[i], b[i])) return false; }
170 return true;
171 }
172 if (a instanceof Map) {
173 if (!(b instanceof Map) || a.size !== b.size) return false;
174 for (const [k, v] of a) {
175 if (b.has(k)) { if (!__bockEq(b.get(k), v)) return false; continue; }
176 let found = false;
177 for (const [bk, bv] of b) { if (__bockEq(k, bk) && __bockEq(v, bv)) { found = true; break; } }
178 if (!found) return false;
179 }
180 return true;
181 }
182 if (a instanceof Set) {
183 if (!(b instanceof Set) || a.size !== b.size) return false;
184 for (const x of a) {
185 if (b.has(x)) continue;
186 let found = false;
187 for (const y of b) { if (__bockEq(x, y)) { found = true; break; } }
188 if (!found) return false;
189 }
190 return true;
191 }
192 const ra = a as Record<string, unknown>;
193 const rb = b as Record<string, unknown>;
194 const ka = Object.keys(ra);
195 const kb = Object.keys(rb);
196 if (ka.length !== kb.length) return false;
197 for (const k of ka) { if (!__bockEq(ra[k], rb[k])) return false; }
198 return true;
199};
200";
201
202fn module_uses_eq(items: &[AIRNode]) -> bool {
207 items.iter().any(|n| {
208 let dbg = format!("{n:?}");
209 dbg.contains("\"user_eq\": String(\"structural\")")
210 || dbg.contains("\"user_eq\": String(\"deep\")")
211 || dbg.contains("\"user_eq\": String(\"deep_custom\")")
212 || dbg.contains("\"user_eq\": String(\"generic\")")
213 || dbg.contains("TraitBound:Equatable")
214 })
215}
216
217const COMPARE_RUNTIME_TS: &str = "// ── Bock structural comparison runtime ──
228type __BockOrdering = { _tag: \"Less\" } | { _tag: \"Equal\" } | { _tag: \"Greater\" };
229const __bockCompare = (a: any, b: any): __BockOrdering => {
230 if (a !== null && typeof a === \"object\" && typeof a.compare === \"function\") {
231 return a.compare(a, b);
232 }
233 return (a < b ? { _tag: \"Less\" } : (a === b ? { _tag: \"Equal\" } : { _tag: \"Greater\" }));
234};
235";
236
237fn module_uses_compare(items: &[AIRNode]) -> bool {
241 items.iter().any(|n| {
242 let dbg = format!("{n:?}");
243 dbg.contains("TraitBound:Comparable")
244 })
245}
246
247const STR_RUNTIME_TS: &str = "// ── Bock display-string runtime ──
256const __bockStr = (x: any): string => {
257 if (x !== null && typeof x === \"object\" && typeof x.to_string === \"function\") {
258 return x.to_string(x);
259 }
260 return String(x);
261};
262";
263
264fn module_uses_str(items: &[AIRNode]) -> bool {
267 items.iter().any(|n| {
268 let dbg = format!("{n:?}");
269 dbg.contains("Interpolation")
270 })
271}
272
273const RUNTIME_MODULE_TS: &str = "_bock_runtime";
281#[derive(Debug)]
282pub struct TsGenerator {
283 profile: TargetProfile,
284}
285
286impl TsGenerator {
287 #[must_use]
289 pub fn new() -> Self {
290 Self {
291 profile: TargetProfile::typescript(),
292 }
293 }
294}
295
296impl Default for TsGenerator {
297 fn default() -> Self {
298 Self::new()
299 }
300}
301
302impl CodeGenerator for TsGenerator {
303 fn target(&self) -> &TargetProfile {
304 &self.profile
305 }
306
307 fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
308 let module =
311 &crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(module.clone()));
312 let mut ctx = TsEmitCtx::new();
313 ctx.enum_variants =
314 crate::generator::collect_enum_variants(&[(module, std::path::Path::new(""))]);
315 ctx.generic_decls =
316 crate::generator::collect_generic_decls(&[(module, std::path::Path::new(""))]);
317 ctx.trait_decls =
318 crate::generator::collect_trait_decls(&[(module, std::path::Path::new(""))]);
319 ctx.exported_types =
320 crate::generator::collect_exported_type_names(&[(module, std::path::Path::new(""))]);
321 ctx.class_fields =
322 crate::generator::collect_class_fields(&[(module, std::path::Path::new(""))]);
323 ctx.const_names =
324 crate::generator::collect_const_names(&[(module, std::path::Path::new(""))]);
325 ctx.emit_node(module)?;
326 let (content, mappings) = ctx.finish();
327 let source_map = SourceMap {
328 generated_file: String::new(),
329 mappings,
330 ..Default::default()
331 };
332 Ok(GeneratedCode {
333 files: vec![OutputFile {
334 path: PathBuf::new(),
335 content,
336 source_map: Some(source_map),
337 }],
338 })
339 }
340
341 fn entry_invocation(&self, main_is_async: bool) -> Option<String> {
342 if main_is_async {
343 Some("(async () => { await main(); })();\n".to_string())
344 } else {
345 Some("main();\n".to_string())
346 }
347 }
348
349 fn generate_project(
367 &self,
368 modules: &[(&AIRModule, &std::path::Path)],
369 ) -> Result<GeneratedCode, CodegenError> {
370 let hoisted: Vec<(AIRModule, &std::path::Path)> = modules
373 .iter()
374 .map(|(m, p)| {
375 (
376 crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(
377 (*m).clone(),
378 )),
379 *p,
380 )
381 })
382 .collect();
383 let modules: Vec<(&AIRModule, &std::path::Path)> =
384 hoisted.iter().map(|(m, p)| (m, *p)).collect();
385 let modules = modules.as_slice();
386 let reachable = crate::generator::reachable_modules(modules);
389 let modules = reachable.as_slice();
390 if modules.is_empty() {
391 return Ok(GeneratedCode { files: vec![] });
392 }
393
394 let entry_idx = modules
395 .iter()
396 .position(|(m, _)| crate::generator::module_declares_main_fn(m))
397 .unwrap_or(modules.len() - 1);
398
399 let enum_variants = crate::generator::collect_enum_variants(modules);
400 let generic_decls = crate::generator::collect_generic_decls(modules);
401 let trait_decls = crate::generator::collect_trait_decls(modules);
402 let exported_types = crate::generator::collect_exported_type_names(modules);
403 let record_names = crate::generator::collect_record_names(modules);
404 let class_fields = crate::generator::collect_class_fields(modules);
405 let const_names = crate::generator::collect_const_names(modules);
406 let public_symbols = crate::generator::collect_public_symbols_for_esm(modules);
407 let mut field_method_collisions = HashSet::new();
411 for (module, _) in modules {
412 field_method_collisions.extend(crate::generator::collect_record_field_names(
413 module,
414 to_camel_case,
415 ));
416 }
417
418 let main_is_async = modules
419 .iter()
420 .any(|(m, _)| crate::generator::module_main_fn_is_async(m));
421 let invocation = self.entry_invocation(main_is_async);
422
423 let mut files: Vec<OutputFile> = Vec::with_capacity(modules.len() + 2);
424 let mut runtime_optional = false;
425 let mut runtime_result = false;
426 let mut runtime_concurrency = false;
427 let mut runtime_range = false;
428 let mut runtime_eq = false;
429 let mut runtime_compare = false;
430 let mut runtime_str = false;
431
432 for (i, (module, source_path)) in modules.iter().enumerate() {
433 let own_path = crate::generator::module_path_string(module).unwrap_or_default();
434 let mut ctx = TsEmitCtx::new();
435 ctx.per_module = true;
436 ctx.enum_variants = enum_variants.clone();
437 ctx.generic_decls = generic_decls.clone();
438 ctx.trait_decls = trait_decls.clone();
439 ctx.exported_types = exported_types.clone();
440 ctx.record_names = record_names.clone();
443 ctx.class_fields = class_fields.clone();
446 ctx.const_names = const_names.clone();
447 ctx.field_method_collisions = field_method_collisions.clone();
448 ctx.seed_effect_registries(modules);
449 ctx.self_module_path = if i == entry_idx {
450 String::new()
451 } else {
452 own_path.clone()
453 };
454 ctx.implicit_imports =
459 crate::generator::implicit_esm_imports_for(module, &public_symbols, &own_path);
460 ctx.public_symbols = public_symbols.clone();
461 ctx.enum_variant_exports = crate::generator::enum_variant_value_names(module);
462 ctx.emit_node(module)?;
463 runtime_optional |= ctx.needs_runtime_optional;
464 runtime_result |= ctx.needs_runtime_result;
465 runtime_concurrency |= ctx.needs_runtime_concurrency;
466 runtime_range |= ctx.needs_runtime_range;
467 runtime_eq |= ctx.needs_runtime_eq;
468 runtime_compare |= ctx.needs_runtime_compare;
469 runtime_str |= ctx.needs_runtime_str;
470 let (mut content, mappings) = ctx.finish();
471
472 if i == entry_idx && crate::generator::module_declares_main_fn(module) {
473 if let Some(invoc) = invocation.as_ref() {
474 if !content.is_empty() && !content.ends_with('\n') {
475 content.push('\n');
476 }
477 content.push_str(invoc);
478 }
479 }
480
481 let out_path = self.module_output_path(module, source_path, i == entry_idx);
482 let generated_file = out_path
483 .file_name()
484 .and_then(|s| s.to_str())
485 .unwrap_or("")
486 .to_string();
487 files.push(OutputFile {
488 path: out_path,
489 content,
490 source_map: Some(SourceMap {
491 generated_file,
492 mappings,
493 ..Default::default()
494 }),
495 });
496 }
497
498 if runtime_optional
502 || runtime_result
503 || runtime_concurrency
504 || runtime_range
505 || runtime_eq
506 || runtime_compare
507 || runtime_str
508 {
509 let mut content = String::new();
510 if runtime_optional {
511 content.push_str(&export_runtime_decls(OPTIONAL_RUNTIME_TS));
512 content.push('\n');
513 }
514 if runtime_result {
515 content.push_str(&export_runtime_decls(RESULT_RUNTIME_TS));
516 content.push('\n');
517 }
518 if runtime_concurrency {
519 content.push_str(&export_runtime_decls(CONCURRENCY_RUNTIME_TS));
520 content.push('\n');
521 }
522 if runtime_range {
523 content.push_str(&export_runtime_decls(RANGE_RUNTIME_TS));
524 content.push('\n');
525 }
526 if runtime_compare {
527 content.push_str(&export_runtime_decls(COMPARE_RUNTIME_TS));
528 content.push('\n');
529 }
530 if runtime_str {
531 content.push_str(&export_runtime_decls(STR_RUNTIME_TS));
532 content.push('\n');
533 }
534 if runtime_eq {
535 content.push_str(&export_runtime_decls(EQ_RUNTIME_TS));
536 content.push('\n');
537 }
538 files.push(OutputFile {
539 path: PathBuf::from(format!("{RUNTIME_MODULE_TS}.ts")),
540 content,
541 source_map: Some(SourceMap {
542 generated_file: format!("{RUNTIME_MODULE_TS}.ts"),
543 ..Default::default()
544 }),
545 });
546 }
547
548 Ok(GeneratedCode { files })
558 }
559
560 fn generate_tests(
567 &self,
568 modules: &[(&AIRModule, &std::path::Path)],
569 framework: &str,
570 ) -> Result<crate::generator::TestArtifacts, CodegenError> {
571 crate::generator::js_ts_generate_tests(
572 modules,
573 framework,
574 &self.target().conventions.file_extension,
575 "ts",
581 |module, source_path, is_entry| self.module_output_path(module, source_path, is_entry),
582 |ctx_modules| {
583 let mut ctx = TsEmitCtx::new();
584 ctx.per_module = true;
585 ctx.enum_variants = crate::generator::collect_enum_variants(ctx_modules);
586 ctx.generic_decls = crate::generator::collect_generic_decls(ctx_modules);
587 ctx.trait_decls = crate::generator::collect_trait_decls(ctx_modules);
588 ctx.exported_types = crate::generator::collect_exported_type_names(ctx_modules);
589 ctx.record_names = crate::generator::collect_record_names(ctx_modules);
590 ctx.class_fields = crate::generator::collect_class_fields(ctx_modules);
591 ctx.const_names = crate::generator::collect_const_names(ctx_modules);
592 let mut field_method_collisions = HashSet::new();
593 for (module, _) in ctx_modules {
594 field_method_collisions.extend(crate::generator::collect_record_field_names(
595 module,
596 to_camel_case,
597 ));
598 }
599 ctx.field_method_collisions = field_method_collisions;
600 ctx.seed_effect_registries(ctx_modules);
601 Box::new(TsTestEmitter { ctx })
602 },
603 )
604 }
605}
606
607struct TsTestEmitter {
610 ctx: TsEmitCtx,
611}
612
613impl crate::generator::JsTsExprEmitter for TsTestEmitter {
614 fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
615 self.ctx.expr_to_string(node)
616 }
617}
618
619impl TsGenerator {
620 fn module_output_path(
624 &self,
625 module: &AIRModule,
626 source_path: &std::path::Path,
627 is_entry: bool,
628 ) -> PathBuf {
629 if is_entry {
630 return crate::generator::derive_output_path(source_path, self.target());
631 }
632 match crate::generator::module_path_string(module) {
633 Some(path) if !path.is_empty() => {
634 let rel: PathBuf = path.split('.').collect();
635 rel.with_extension(&self.target().conventions.file_extension)
636 }
637 _ => crate::generator::derive_output_path(source_path, self.target()),
638 }
639 }
640}
641
642fn export_runtime_decls(runtime: &str) -> String {
649 runtime
650 .lines()
651 .map(|line| {
652 if let Some(rest) = line.strip_prefix("type ") {
653 format!("export type {rest}")
654 } else if let Some(rest) = line.strip_prefix("const ") {
655 format!("export const {rest}")
656 } else {
657 line.to_string()
658 }
659 })
660 .collect::<Vec<_>>()
661 .join("\n")
662}
663
664#[derive(Clone)]
672enum ValueSink {
673 Return,
675 Assign(String),
677 Discard,
683}
684
685struct TsEmitCtx {
687 buf: String,
688 indent: usize,
689 effect_ops: HashMap<String, String>,
691 current_handler_vars: HashMap<String, String>,
693 fn_effects: HashMap<String, Vec<String>>,
695 composite_effects: HashMap<String, Vec<String>>,
697 record_names: HashSet<String>,
699 class_fields: HashMap<String, Vec<String>>,
708 const_names: HashSet<String>,
713 effect_names: HashSet<String>,
715 cur_line: u32,
717 cur_col: u32,
719 scan_pos: usize,
721 last_marked: Option<(u32, u32)>,
723 mappings: Vec<SourceMapping>,
725 loop_labels: Vec<Option<String>>,
729 switch_label_depth: usize,
732 loop_label_counter: usize,
734 match_temp_counter: usize,
741 optional_runtime_emitted: bool,
747 result_runtime_emitted: bool,
751 concurrency_runtime_emitted: bool,
754 range_runtime_emitted: bool,
758 eq_runtime_emitted: bool,
761 compare_runtime_emitted: bool,
765 str_runtime_emitted: bool,
768 enum_variants: crate::generator::EnumVariantRegistry,
774 generic_decls: crate::generator::GenericDeclRegistry,
781 trait_decls: crate::generator::TraitDeclRegistry,
788 trait_self_subst: Option<String>,
799 exported_types: std::collections::HashSet<String>,
804 current_expected_type: Option<String>,
818 per_module: bool,
828 needs_runtime_optional: bool,
832 needs_runtime_result: bool,
833 needs_runtime_concurrency: bool,
836 needs_runtime_range: bool,
837 needs_runtime_eq: bool,
838 needs_runtime_compare: bool,
842 needs_runtime_str: bool,
845 implicit_imports: Vec<crate::generator::ImplicitEsmImport>,
849 public_symbols: HashMap<String, crate::generator::EsmSymbol>,
852 self_module_path: String,
856 enum_variant_exports: Vec<String>,
862 field_method_collisions: HashSet<String>,
873 value_sink: Option<ValueSink>,
882 loop_value_sinks: Vec<Option<ValueSink>>,
889 current_fn_propagate_tag: Option<&'static str>,
897 let_scopes: Vec<LetScope>,
907}
908
909#[derive(Default)]
912struct LetScope {
913 declared: HashSet<String>,
915 needs_let: HashSet<String>,
918}
919
920impl TsEmitCtx {
921 fn new() -> Self {
922 Self {
923 buf: String::with_capacity(4096),
924 indent: 0,
925 effect_ops: HashMap::new(),
926 current_handler_vars: HashMap::new(),
927 fn_effects: HashMap::new(),
928 composite_effects: HashMap::new(),
929 record_names: HashSet::new(),
930 class_fields: HashMap::new(),
931 const_names: HashSet::new(),
932 effect_names: HashSet::new(),
933 cur_line: 1,
934 cur_col: 1,
935 scan_pos: 0,
936 last_marked: None,
937 mappings: Vec::new(),
938 loop_labels: Vec::new(),
939 switch_label_depth: 0,
940 loop_label_counter: 0,
941 match_temp_counter: 0,
942 optional_runtime_emitted: false,
943 result_runtime_emitted: false,
944 concurrency_runtime_emitted: false,
945 range_runtime_emitted: false,
946 eq_runtime_emitted: false,
947 compare_runtime_emitted: false,
948 str_runtime_emitted: false,
949 enum_variants: crate::generator::EnumVariantRegistry::new(),
950 generic_decls: crate::generator::GenericDeclRegistry::new(),
951 trait_decls: crate::generator::TraitDeclRegistry::new(),
952 trait_self_subst: None,
953 exported_types: std::collections::HashSet::new(),
954 current_expected_type: None,
955 per_module: false,
956 needs_runtime_optional: false,
957 needs_runtime_result: false,
958 needs_runtime_concurrency: false,
959 needs_runtime_range: false,
960 needs_runtime_eq: false,
961 needs_runtime_compare: false,
962 needs_runtime_str: false,
963 implicit_imports: Vec::new(),
964 public_symbols: HashMap::new(),
965 self_module_path: String::new(),
966 enum_variant_exports: Vec::new(),
967 field_method_collisions: HashSet::new(),
968 value_sink: None,
969 loop_value_sinks: Vec::new(),
970 current_fn_propagate_tag: None,
971 let_scopes: Vec::new(),
972 }
973 }
974
975 fn propagate_tag_for_return(ret: Option<&AIRNode>) -> Option<&'static str> {
979 match ret.map(|r| &r.kind) {
980 Some(NodeKind::TypeOptional { .. }) => Some("None"),
981 Some(NodeKind::TypeNamed { path, .. }) => {
982 match path.segments.last().map(|s| s.name.as_str()) {
983 Some("Result") => Some("Err"),
984 Some("Optional") => Some("None"),
985 _ => None,
986 }
987 }
988 _ => None,
989 }
990 }
991
992 fn ts_method_name(&self, rendered: &str) -> String {
1007 crate::generator::disambiguate_method_name(
1008 rendered.to_string(),
1009 &self.field_method_collisions,
1010 "Method",
1011 )
1012 }
1013
1014 fn finish(self) -> (String, Vec<SourceMapping>) {
1015 (self.buf, self.mappings)
1016 }
1017
1018 fn seed_effect_registries(&mut self, modules: &[(&AIRModule, &std::path::Path)]) {
1026 for (module, _) in modules {
1027 let NodeKind::Module { items, .. } = &module.kind else {
1028 continue;
1029 };
1030 for item in items {
1031 let NodeKind::EffectDecl {
1032 name,
1033 components,
1034 operations,
1035 ..
1036 } = &item.kind
1037 else {
1038 continue;
1039 };
1040 if !components.is_empty() {
1041 let comp_names: Vec<String> = components
1042 .iter()
1043 .map(|tp| {
1044 tp.segments
1045 .last()
1046 .map_or("effect".to_string(), |s| s.name.clone())
1047 })
1048 .collect();
1049 self.composite_effects.insert(name.name.clone(), comp_names);
1050 continue;
1051 }
1052 self.effect_names.insert(name.name.clone());
1053 for op in operations {
1054 if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
1055 self.effect_ops
1056 .insert(op_name.name.clone(), name.name.clone());
1057 }
1058 }
1059 }
1060 }
1061 }
1062
1063 fn emit_esm_imports(&mut self, imports: &[AIRNode]) -> Result<(), CodegenError> {
1072 let ext = "ts";
1084
1085 let mut type_names: Vec<&str> = Vec::new();
1089 if self.needs_runtime_optional {
1090 type_names.push("BockOption");
1091 }
1092 if self.needs_runtime_result {
1093 type_names.push("BockResult");
1094 }
1095 let mut value_names: Vec<&str> = Vec::new();
1096 if self.needs_runtime_concurrency {
1097 value_names.extend(["__bockChannelNew", "__bockSpawn"]);
1098 }
1099 if self.needs_runtime_range {
1100 value_names.extend(["range", "rangeInclusive"]);
1101 }
1102 if self.needs_runtime_eq {
1103 value_names.push("__bockEq");
1104 }
1105 if self.needs_runtime_compare {
1106 value_names.push("__bockCompare");
1107 }
1108 if self.needs_runtime_str {
1109 value_names.push("__bockStr");
1110 }
1111 if !type_names.is_empty() || !value_names.is_empty() {
1112 let spec = crate::generator::esm_relative_specifier(
1113 &self.self_module_path,
1114 RUNTIME_MODULE_TS,
1115 ext,
1116 );
1117 if !type_names.is_empty() {
1118 self.writeln(&format!(
1119 "import type {{ {} }} from \"{spec}\";",
1120 type_names.join(", ")
1121 ));
1122 }
1123 if !value_names.is_empty() {
1124 self.writeln(&format!(
1125 "import {{ {} }} from \"{spec}\";",
1126 value_names.join(", ")
1127 ));
1128 }
1129 }
1130
1131 let mut value_by_module: std::collections::BTreeMap<String, Vec<String>> =
1138 std::collections::BTreeMap::new();
1139 let mut type_by_module: std::collections::BTreeMap<String, Vec<String>> =
1140 std::collections::BTreeMap::new();
1141
1142 for import in imports {
1143 if let NodeKind::ImportDecl { path, items } = &import.kind {
1144 let target_path = path
1145 .segments
1146 .iter()
1147 .map(|s| s.name.as_str())
1148 .collect::<Vec<_>>()
1149 .join(".");
1150 if target_path.is_empty() {
1151 continue;
1152 }
1153 if let bock_ast::ImportItems::Named(named) = items {
1154 for n in named {
1155 if crate::generator::ESM_RUNTIME_PRELUDE_NAMES
1158 .contains(&n.name.name.as_str())
1159 {
1160 continue;
1161 }
1162 let kind = self.public_symbols.get(&n.name.name).map(|s| s.kind);
1163 let is_fn = matches!(kind, Some(crate::generator::EsmDeclKind::Function));
1164 let src = ts_esm_emit_name(&n.name.name, is_fn);
1165 let rendered = match &n.alias {
1166 Some(alias) => {
1167 let dst = ts_esm_emit_name(&alias.name, is_fn);
1168 if dst == src {
1169 src
1170 } else {
1171 format!("{src} as {dst}")
1172 }
1173 }
1174 None => src,
1175 };
1176 let type_only = kind.is_some_and(|k| k.is_ts_type_only());
1177 if type_only {
1178 type_by_module.entry(target_path.clone()).or_default()
1179 } else {
1180 value_by_module.entry(target_path.clone()).or_default()
1181 }
1182 .push(rendered);
1183 }
1184 }
1185 }
1187 }
1188
1189 for imp in &self.implicit_imports {
1192 let rendered = ts_esm_emit_name(&imp.name, imp.is_fn());
1193 if imp.kind.is_ts_type_only() {
1194 type_by_module.entry(imp.module_path.clone()).or_default()
1195 } else {
1196 value_by_module.entry(imp.module_path.clone()).or_default()
1197 }
1198 .push(rendered);
1199 }
1200
1201 for (module_path, mut names) in value_by_module {
1202 names.sort_unstable();
1203 names.dedup();
1204 let spec =
1205 crate::generator::esm_relative_specifier(&self.self_module_path, &module_path, ext);
1206 self.writeln(&format!(
1207 "import {{ {} }} from \"{spec}\";",
1208 names.join(", ")
1209 ));
1210 }
1211 for (module_path, mut names) in type_by_module {
1212 names.sort_unstable();
1213 names.dedup();
1214 let spec =
1215 crate::generator::esm_relative_specifier(&self.self_module_path, &module_path, ext);
1216 self.writeln(&format!(
1217 "import type {{ {} }} from \"{spec}\";",
1218 names.join(", ")
1219 ));
1220 }
1221 Ok(())
1222 }
1223
1224 fn emit_trailing_exports(&mut self) {
1230 if self.enum_variant_exports.is_empty() {
1231 return;
1232 }
1233 let mut names = self.enum_variant_exports.clone();
1234 names.sort_unstable();
1235 names.dedup();
1236 if !self.buf.is_empty() && !self.buf.ends_with('\n') {
1237 self.buf.push('\n');
1238 }
1239 self.writeln(&format!("export {{ {} }};", names.join(", ")));
1240 }
1241
1242 fn user_variant_for_path(
1246 &self,
1247 path: &bock_ast::TypePath,
1248 ) -> Option<&crate::generator::EnumVariantInfo> {
1249 let info = crate::generator::registered_variant(&self.enum_variants, path)?;
1250 if matches!(info.enum_name.as_str(), "Optional" | "Result") {
1251 return None;
1252 }
1253 Some(info)
1254 }
1255
1256 fn user_variant_for_name(&self, name: &str) -> Option<&crate::generator::EnumVariantInfo> {
1258 let info = self.enum_variants.get(name)?;
1259 if matches!(info.enum_name.as_str(), "Optional" | "Result") {
1260 return None;
1261 }
1262 Some(info)
1263 }
1264
1265 fn variant_construct_enum(&self, value: &AIRNode) -> Option<String> {
1280 match &value.kind {
1281 NodeKind::RecordConstruct { path, spread, .. } if spread.is_none() => self
1283 .user_variant_for_path(path)
1284 .map(|i| i.enum_name.clone()),
1285 NodeKind::Identifier { name } => self
1287 .user_variant_for_name(&name.name)
1288 .map(|i| i.enum_name.clone()),
1289 NodeKind::Call { callee, .. } => match &callee.kind {
1293 NodeKind::Identifier { name } => self
1294 .user_variant_for_name(&name.name)
1295 .map(|i| i.enum_name.clone()),
1296 _ => None,
1297 },
1298 _ => None,
1299 }
1300 }
1301
1302 fn binding_needs_union_widening(&self, value: &AIRNode, ty_ts: Option<&str>) -> bool {
1309 match (ty_ts, self.variant_construct_enum(value)) {
1310 (Some(declared), Some(enum_name)) => declared == enum_name,
1311 _ => false,
1312 }
1313 }
1314
1315 fn emit_let_value(
1327 &mut self,
1328 value: &AIRNode,
1329 ty_ts: Option<String>,
1330 ) -> Result<(), CodegenError> {
1331 let widen_to = self
1335 .binding_needs_union_widening(value, ty_ts.as_deref())
1336 .then(|| ty_ts.clone())
1337 .flatten();
1338 let prev_expected = self.current_expected_type.take();
1339 self.current_expected_type = ty_ts;
1340 self.emit_expr(value)?;
1341 if let Some(union) = widen_to {
1342 let _ = write!(self.buf, " as {union}");
1343 }
1344 self.current_expected_type = prev_expected;
1345 Ok(())
1346 }
1347
1348 fn sync_pos(&mut self) {
1351 if self.scan_pos >= self.buf.len() {
1352 return;
1353 }
1354 let slice = &self.buf[self.scan_pos..];
1355 for ch in slice.chars() {
1356 if ch == '\n' {
1357 self.cur_line += 1;
1358 self.cur_col = 1;
1359 } else {
1360 self.cur_col += 1;
1361 }
1362 }
1363 self.scan_pos = self.buf.len();
1364 }
1365
1366 fn mark_span(&mut self, span: Span) {
1367 if span.start == 0 && span.end == 0 {
1368 return;
1369 }
1370 self.sync_pos();
1371 let key = (self.cur_line, self.cur_col);
1372 if self.last_marked == Some(key) {
1373 return;
1374 }
1375 self.last_marked = Some(key);
1376 self.mappings.push(SourceMapping {
1377 gen_line: self.cur_line,
1378 gen_col: self.cur_col,
1379 src_line: 0,
1380 src_col: 0,
1381 src_offset: span.start as u32,
1382 src_file_id: span.file.0,
1383 });
1384 }
1385
1386 fn indent_str(&self) -> String {
1387 " ".repeat(self.indent)
1388 }
1389
1390 fn write_indent(&mut self) {
1391 let indent = self.indent_str();
1392 self.buf.push_str(&indent);
1393 }
1394
1395 fn writeln(&mut self, s: &str) {
1396 self.write_indent();
1397 self.buf.push_str(s);
1398 self.buf.push('\n');
1399 }
1400
1401 fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
1405 let start = self.buf.len();
1406 let saved_line = self.cur_line;
1407 let saved_col = self.cur_col;
1408 let saved_scan = self.scan_pos;
1409 let saved_marked = self.last_marked;
1410 let mappings_len = self.mappings.len();
1411 self.emit_expr(node)?;
1412 let s = self.buf[start..].to_string();
1413 self.buf.truncate(start);
1414 self.cur_line = saved_line;
1415 self.cur_col = saved_col;
1416 self.scan_pos = saved_scan;
1417 self.last_marked = saved_marked;
1418 self.mappings.truncate(mappings_len);
1419 Ok(s)
1420 }
1421
1422 fn map_prelude_call(
1424 &mut self,
1425 callee: &AIRNode,
1426 args: &[bock_air::AirArg],
1427 ) -> Result<Option<String>, CodegenError> {
1428 let name = match &callee.kind {
1429 NodeKind::Identifier { name } => name.name.as_str(),
1430 _ => return Ok(None),
1431 };
1432 let arg_strs: Vec<String> = args
1433 .iter()
1434 .map(|a| self.expr_to_string(&a.value))
1435 .collect::<Result<_, _>>()?;
1436 let code = match name {
1437 "println" => {
1438 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1439 format!("console.log({a})")
1440 }
1441 "print" => {
1442 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1443 format!("process.stdout.write(String({a}))")
1444 }
1445 "debug" => {
1446 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1447 format!("console.debug({a})")
1448 }
1449 "assert" => {
1450 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1451 format!("if (!{a}) throw new Error(\"assertion failed\")")
1452 }
1453 "todo" => "throw new Error(\"not implemented\")".to_string(),
1454 "unreachable" => "throw new Error(\"unreachable\")".to_string(),
1455 "sleep" => {
1456 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1457 if let Some(handler) = self.clock_handler_var() {
1460 format!("{handler}.sleep({a})")
1461 } else {
1462 format!("new Promise<void>((__r) => setTimeout(__r, Math.floor(({a}) / 1e6)))")
1463 }
1464 }
1465 _ => return Ok(None),
1466 };
1467 Ok(Some(code))
1468 }
1469
1470 fn try_emit_time_assoc_call(
1475 &mut self,
1476 callee: &AIRNode,
1477 args: &[bock_air::AirArg],
1478 ) -> Result<bool, CodegenError> {
1479 let NodeKind::FieldAccess { object, field } = &callee.kind else {
1480 return Ok(false);
1481 };
1482 let NodeKind::Identifier { name: type_name } = &object.kind else {
1483 return Ok(false);
1484 };
1485 let arg_strs: Vec<String> = args
1486 .iter()
1487 .map(|a| self.expr_to_string(&a.value))
1488 .collect::<Result<_, _>>()?;
1489 let arg0 = || arg_strs.first().cloned().unwrap_or_default();
1490 let code = match (type_name.name.as_str(), field.name.as_str()) {
1491 ("Duration", "zero") => "0".to_string(),
1492 ("Duration", "nanos") => arg0(),
1493 ("Duration", "micros") => format!("(({}) * 1000)", arg0()),
1494 ("Duration", "millis") => format!("(({}) * 1000000)", arg0()),
1495 ("Duration", "seconds") => format!("(({}) * 1000000000)", arg0()),
1496 ("Duration", "minutes") => format!("(({}) * 60000000000)", arg0()),
1497 ("Duration", "hours") => format!("(({}) * 3600000000000)", arg0()),
1498 ("Instant", "now") => {
1499 if let Some(handler) = self.clock_handler_var() {
1502 format!("{handler}.now_monotonic()")
1503 } else {
1504 "(performance.now() * 1000000)".to_string()
1505 }
1506 }
1507 _ => return Ok(false),
1508 };
1509 self.buf.push_str(&code);
1510 Ok(true)
1511 }
1512
1513 fn try_emit_concurrency_call(
1517 &mut self,
1518 callee: &AIRNode,
1519 args: &[bock_air::AirArg],
1520 ) -> Result<bool, CodegenError> {
1521 if let NodeKind::Identifier { name } = &callee.kind {
1522 if name.name == "spawn" {
1523 self.buf.push_str("__bockSpawn(");
1524 for (i, arg) in args.iter().enumerate() {
1525 if i > 0 {
1526 self.buf.push_str(", ");
1527 }
1528 self.emit_expr(&arg.value)?;
1529 }
1530 self.buf.push(')');
1531 return Ok(true);
1532 }
1533 }
1534 let NodeKind::FieldAccess { object, field } = &callee.kind else {
1535 return Ok(false);
1536 };
1537 if let NodeKind::Identifier { name: type_name } = &object.kind {
1538 if type_name.name == "Channel" && field.name == "new" {
1539 self.buf.push_str("__bockChannelNew()");
1540 return Ok(true);
1541 }
1542 }
1543 if matches!(field.name.as_str(), "send" | "recv" | "close") {
1544 self.emit_expr(object)?;
1546 let _ = write!(self.buf, ".{}", field.name);
1547 self.buf.push('(');
1548 for (i, arg) in args.iter().skip(1).enumerate() {
1549 if i > 0 {
1550 self.buf.push_str(", ");
1551 }
1552 self.emit_expr(&arg.value)?;
1553 }
1554 self.buf.push(')');
1555 return Ok(true);
1556 }
1557 Ok(false)
1558 }
1559
1560 fn try_emit_time_desugared_method(
1569 &mut self,
1570 node: &AIRNode,
1571 callee: &AIRNode,
1572 args: &[bock_air::AirArg],
1573 ) -> Result<bool, CodegenError> {
1574 if crate::generator::primitive_recv_kind(node).is_some() {
1575 return Ok(false);
1576 }
1577 let NodeKind::FieldAccess { object, field } = &callee.kind else {
1578 return Ok(false);
1579 };
1580 if let NodeKind::Identifier { name } = &object.kind {
1581 if matches!(name.name.as_str(), "Duration" | "Instant") {
1582 return Ok(false);
1583 }
1584 }
1585 if !is_time_method_name(&field.name) {
1586 return Ok(false);
1587 }
1588 let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
1589 self.try_emit_time_method(object, &field.name, &remaining)
1590 }
1591
1592 fn try_emit_time_method(
1595 &mut self,
1596 receiver: &AIRNode,
1597 method: &str,
1598 args: &[bock_air::AirArg],
1599 ) -> Result<bool, CodegenError> {
1600 let recv_str = self.expr_to_string(receiver)?;
1601 let arg_strs: Vec<String> = args
1602 .iter()
1603 .map(|a| self.expr_to_string(&a.value))
1604 .collect::<Result<_, _>>()?;
1605 let code = match method {
1606 "as_nanos" => format!("({recv_str})"),
1607 "as_millis" => format!("Math.floor(({recv_str}) / 1000000)"),
1608 "as_seconds" => format!("Math.floor(({recv_str}) / 1000000000)"),
1609 "is_zero" => format!("(({recv_str}) === 0)"),
1610 "is_negative" => format!("(({recv_str}) < 0)"),
1611 "abs" => format!("Math.abs({recv_str})"),
1612 "elapsed" => {
1613 if let Some(handler) = self.clock_handler_var() {
1617 format!("({handler}.now_monotonic() - ({recv_str}))")
1618 } else {
1619 format!("((performance.now() * 1000000) - ({recv_str}))")
1620 }
1621 }
1622 "duration_since" => {
1623 let other = arg_strs.first().cloned().unwrap_or_default();
1624 format!("(({recv_str}) - ({other}))")
1625 }
1626 _ => return Ok(false),
1627 };
1628 self.buf.push_str(&code);
1629 Ok(true)
1630 }
1631
1632 fn try_emit_prelude_ctor(
1636 &mut self,
1637 callee: &AIRNode,
1638 args: &[bock_air::AirArg],
1639 ) -> Result<bool, CodegenError> {
1640 let name = match &callee.kind {
1641 NodeKind::Identifier { name } => name.name.as_str(),
1642 _ => return Ok(false),
1643 };
1644 if !matches!(name, "Some" | "Ok" | "Err") {
1645 return Ok(false);
1646 }
1647 let _ = write!(self.buf, "{{ _tag: \"{name}\" as const");
1648 if let Some(arg) = args.first() {
1649 self.buf.push_str(", _0: ");
1650 self.emit_expr(&arg.value)?;
1651 }
1652 self.buf.push_str(" }");
1653 Ok(true)
1654 }
1655
1656 fn try_emit_primitive_conversion(
1665 &mut self,
1666 node: &AIRNode,
1667 callee: &AIRNode,
1668 args: &[bock_air::AirArg],
1669 ) -> Result<bool, CodegenError> {
1670 let Some((target, method, arg)) =
1671 crate::generator::primitive_conversion_call(node, callee, args)
1672 else {
1673 return Ok(false);
1674 };
1675 let arg_str = self.expr_to_string(arg)?;
1676 let code = match (target, method) {
1677 ("Float" | "Int" | "String", "from") => format!("({arg_str})"),
1678 ("Int", "try_from") => format!(
1679 "((__s: string) => /^[+-]?[0-9]+$/.test(__s.trim()) \
1680 ? {{ _tag: \"Ok\" as const, _0: Number.parseInt(__s.trim(), 10) }} \
1681 : {{ _tag: \"Err\" as const, _0: new ConvertError({{ message: \
1682 `cannot parse '${{__s}}' as Int` }}) }})({arg_str})"
1683 ),
1684 ("Float", "try_from") => format!(
1685 "((__s: string) => {{ const __t = __s.trim(); const __n = Number(__t); \
1686 return (__t.length > 0 && !Number.isNaN(__n)) \
1687 ? {{ _tag: \"Ok\" as const, _0: __n }} \
1688 : {{ _tag: \"Err\" as const, _0: new ConvertError({{ message: \
1689 `cannot parse '${{__s}}' as Float` }}) }}; }})({arg_str})"
1690 ),
1691 _ => return Ok(false),
1692 };
1693 self.buf.push_str(&code);
1694 Ok(true)
1695 }
1696
1697 fn try_emit_container_method(
1708 &mut self,
1709 node: &AIRNode,
1710 callee: &AIRNode,
1711 args: &[bock_air::AirArg],
1712 ) -> Result<bool, CodegenError> {
1713 if let Some((recv, method, rest)) =
1714 crate::generator::desugared_optional_method(node, callee, args)
1715 {
1716 self.emit_tagged_container_method(recv, method, rest, "Some", "<T,>", "BockOption<T>")?;
1717 return Ok(true);
1718 }
1719 if let Some((recv, method, rest)) =
1720 crate::generator::desugared_result_method(node, callee, args)
1721 {
1722 self.emit_tagged_container_method(
1723 recv,
1724 method,
1725 rest,
1726 "Ok",
1727 "<T, E>",
1728 "BockResult<T, E>",
1729 )?;
1730 return Ok(true);
1731 }
1732 Ok(false)
1733 }
1734
1735 fn emit_tagged_container_method(
1740 &mut self,
1741 recv: &AIRNode,
1742 method: &str,
1743 rest: &[bock_air::AirArg],
1744 present_tag: &str,
1745 type_params: &str,
1746 param_ty: &str,
1747 ) -> Result<(), CodegenError> {
1748 match method {
1750 "is_some" | "is_ok" => {
1751 self.buf.push('(');
1752 self.emit_expr(recv)?;
1753 let _ = write!(self.buf, "._tag === \"{present_tag}\")");
1754 return Ok(());
1755 }
1756 "is_none" | "is_err" => {
1757 self.buf.push('(');
1758 self.emit_expr(recv)?;
1759 let _ = write!(self.buf, "._tag !== \"{present_tag}\")");
1760 return Ok(());
1761 }
1762 _ => {}
1763 }
1764 let _ = write!(self.buf, "(({type_params}(__c: {param_ty}) => ");
1765 match method {
1766 "unwrap" => {
1767 let _ = write!(
1768 self.buf,
1769 "__c._tag === \"{present_tag}\" ? __c._0 : (undefined as never)"
1770 );
1771 }
1772 "unwrap_or" => {
1773 let _ = write!(self.buf, "__c._tag === \"{present_tag}\" ? __c._0 : (");
1774 if let Some(d) = rest.first() {
1775 self.emit_expr(&d.value)?;
1776 } else {
1777 self.buf.push_str("undefined");
1778 }
1779 self.buf.push(')');
1780 }
1781 "map" => {
1782 let _ = write!(
1787 self.buf,
1788 "__c._tag === \"{present_tag}\" ? {{ _tag: \"{present_tag}\" as const, _0: ("
1789 );
1790 if let Some(f) = rest.first() {
1791 self.emit_expr(&f.value)?;
1792 } else {
1793 self.buf.push_str("(x) => x");
1794 }
1795 self.buf.push_str(")(__c._0 as any) } : __c");
1796 }
1797 "flat_map" => {
1798 let _ = write!(self.buf, "__c._tag === \"{present_tag}\" ? (");
1799 if let Some(f) = rest.first() {
1800 self.emit_expr(&f.value)?;
1801 } else {
1802 self.buf.push_str("(x) => x");
1803 }
1804 self.buf.push_str(")(__c._0 as any) : __c");
1805 }
1806 "map_err" => {
1807 self.buf
1808 .push_str("__c._tag === \"Ok\" ? __c : { _tag: \"Err\" as const, _0: (");
1809 if let Some(f) = rest.first() {
1810 self.emit_expr(&f.value)?;
1811 } else {
1812 self.buf.push_str("(x) => x");
1813 }
1814 self.buf.push_str(")(__c._0 as any) }");
1815 }
1816 _ => self.buf.push_str("(undefined as never)"),
1817 }
1818 self.buf.push_str(")(");
1819 self.emit_expr(recv)?;
1820 self.buf.push_str("))");
1821 Ok(())
1822 }
1823
1824 fn try_emit_list_method(
1834 &mut self,
1835 node: &AIRNode,
1836 callee: &AIRNode,
1837 args: &[bock_air::AirArg],
1838 ) -> Result<bool, CodegenError> {
1839 let Some((recv, method, rest)) =
1840 crate::generator::desugared_list_method(node, callee, args)
1841 else {
1842 return Ok(false);
1843 };
1844 match method {
1845 "len" | "length" | "count" => {
1846 self.buf.push('(');
1847 self.emit_expr(recv)?;
1848 self.buf.push_str(").length");
1849 }
1850 "is_empty" => {
1851 self.buf.push_str("((");
1852 self.emit_expr(recv)?;
1853 self.buf.push_str(").length === 0)");
1854 }
1855 "get" => {
1856 let Some(idx) = rest.first() else {
1857 return Ok(false);
1858 };
1859 self.buf.push_str(
1860 "(<T,>(__r: ReadonlyArray<T>, __i: number): BockOption<T> => \
1861 (__i >= 0 && __i < __r.length) ? \
1862 { _tag: \"Some\" as const, _0: __r[__i] } : { _tag: \"None\" as const })(",
1863 );
1864 self.emit_expr(recv)?;
1865 self.buf.push_str(", ");
1866 self.emit_expr(&idx.value)?;
1867 self.buf.push(')');
1868 }
1869 "first" => {
1870 self.buf.push_str(
1871 "(<T,>(__r: ReadonlyArray<T>): BockOption<T> => __r.length > 0 ? \
1872 { _tag: \"Some\" as const, _0: __r[0] } : { _tag: \"None\" as const })(",
1873 );
1874 self.emit_expr(recv)?;
1875 self.buf.push(')');
1876 }
1877 "last" => {
1878 self.buf.push_str(
1879 "(<T,>(__r: ReadonlyArray<T>): BockOption<T> => __r.length > 0 ? \
1880 { _tag: \"Some\" as const, _0: __r[__r.length - 1] } : \
1881 { _tag: \"None\" as const })(",
1882 );
1883 self.emit_expr(recv)?;
1884 self.buf.push(')');
1885 }
1886 "contains" => {
1887 let Some(x) = rest.first() else {
1888 return Ok(false);
1889 };
1890 self.buf.push('(');
1891 self.emit_expr(recv)?;
1892 self.buf.push_str(").includes(");
1893 self.emit_expr(&x.value)?;
1894 self.buf.push(')');
1895 }
1896 "index_of" => {
1897 let Some(x) = rest.first() else {
1898 return Ok(false);
1899 };
1900 self.buf.push_str(
1901 "(<T,>(__r: ReadonlyArray<T>, __x: T): BockOption<number> => \
1902 { const __i = __r.indexOf(__x); return __i >= 0 ? \
1903 { _tag: \"Some\" as const, _0: __i } : { _tag: \"None\" as const }; })(",
1904 );
1905 self.emit_expr(recv)?;
1906 self.buf.push_str(", ");
1907 self.emit_expr(&x.value)?;
1908 self.buf.push(')');
1909 }
1910 "concat" => {
1911 let Some(o) = rest.first() else {
1912 return Ok(false);
1913 };
1914 self.buf.push('(');
1915 self.emit_expr(recv)?;
1916 self.buf.push_str(").concat(");
1917 self.emit_expr(&o.value)?;
1918 self.buf.push(')');
1919 }
1920 "join" => {
1921 let Some(sep) = rest.first() else {
1922 return Ok(false);
1923 };
1924 self.buf.push('(');
1925 self.emit_expr(recv)?;
1926 self.buf.push_str(").join(");
1927 self.emit_expr(&sep.value)?;
1928 self.buf.push(')');
1929 }
1930 _ => return Ok(false),
1931 }
1932 Ok(true)
1933 }
1934
1935 fn try_emit_list_mutating_method(
1942 &mut self,
1943 node: &AIRNode,
1944 callee: &AIRNode,
1945 args: &[bock_air::AirArg],
1946 ) -> Result<bool, CodegenError> {
1947 let Some((recv, _method, rest)) =
1948 crate::generator::desugared_list_mutating_method(node, callee, args)
1949 else {
1950 return Ok(false);
1951 };
1952 let Some(x) = rest.first() else {
1953 return Ok(false);
1954 };
1955 self.buf.push('(');
1956 self.emit_expr(recv)?;
1957 self.buf.push_str(").push(");
1958 self.emit_expr(&x.value)?;
1959 self.buf.push(')');
1960 Ok(true)
1961 }
1962
1963 fn try_emit_list_inplace_mutator(
1978 &mut self,
1979 node: &AIRNode,
1980 callee: &AIRNode,
1981 args: &[bock_air::AirArg],
1982 ) -> Result<bool, CodegenError> {
1983 let Some((recv, method, rest)) =
1984 crate::generator::desugared_list_inplace_mutator(node, callee, args)
1985 else {
1986 return Ok(false);
1987 };
1988 match method {
1989 "pop" => {
1990 self.buf.push_str(
1991 "(<T,>(__r: T[]): BockOption<T> => __r.length > 0 ? \
1992 { _tag: \"Some\" as const, _0: __r.pop() as T } : \
1993 { _tag: \"None\" as const })(",
1994 );
1995 self.emit_expr(recv)?;
1996 self.buf.push(')');
1997 }
1998 "remove_at" => {
1999 let Some(idx) = rest.first() else {
2000 return Ok(false);
2001 };
2002 self.buf.push_str(
2003 "(<T,>(__r: T[], __i: number): T => { \
2004 if (__i < 0 || __i >= __r.length) { throw new Error(\
2005 \"List.remove_at: index \" + __i + \" out of bounds (len \" + __r.length + \")\"); } \
2006 return __r.splice(__i, 1)[0] as T; })(",
2007 );
2008 self.emit_expr(recv)?;
2009 self.buf.push_str(", ");
2010 self.emit_expr(&idx.value)?;
2011 self.buf.push(')');
2012 }
2013 "insert" => {
2014 let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
2015 return Ok(false);
2016 };
2017 self.buf.push_str(
2018 "(<T,>(__r: T[], __i: number, __x: T): void => { \
2019 if (__i < 0 || __i > __r.length) { throw new Error(\
2020 \"List.insert: index \" + __i + \" out of bounds (len \" + __r.length + \")\"); } \
2021 __r.splice(__i, 0, __x); })(",
2022 );
2023 self.emit_expr(recv)?;
2024 self.buf.push_str(", ");
2025 self.emit_expr(&idx.value)?;
2026 self.buf.push_str(", ");
2027 self.emit_expr(&x.value)?;
2028 self.buf.push(')');
2029 }
2030 "reverse" => {
2031 self.buf.push('(');
2032 self.emit_expr(recv)?;
2033 self.buf.push_str(").reverse()");
2034 }
2035 "set" => {
2036 let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
2037 return Ok(false);
2038 };
2039 self.buf.push_str(
2040 "(<T,>(__r: T[], __i: number, __x: T): void => { \
2041 if (__i < 0 || __i >= __r.length) { throw new Error(\
2042 \"List.set: index \" + __i + \" out of bounds (len \" + __r.length + \")\"); } \
2043 __r[__i] = __x; })(",
2044 );
2045 self.emit_expr(recv)?;
2046 self.buf.push_str(", ");
2047 self.emit_expr(&idx.value)?;
2048 self.buf.push_str(", ");
2049 self.emit_expr(&x.value)?;
2050 self.buf.push(')');
2051 }
2052 _ => return Ok(false),
2053 }
2054 Ok(true)
2055 }
2056
2057 fn try_emit_list_functional_method(
2069 &mut self,
2070 node: &AIRNode,
2071 callee: &AIRNode,
2072 args: &[bock_air::AirArg],
2073 ) -> Result<bool, CodegenError> {
2074 let Some((recv, method, rest)) =
2075 crate::generator::desugared_list_functional_method(node, callee, args)
2076 else {
2077 return Ok(false);
2078 };
2079 match method {
2080 "map" | "filter" | "for_each" | "any" | "all" | "flat_map" => {
2081 let Some(cb) = rest.first() else {
2082 return Ok(false);
2083 };
2084 let native = match method {
2085 "map" => "map",
2086 "filter" => "filter",
2087 "for_each" => "forEach",
2088 "any" => "some",
2089 "all" => "every",
2090 "flat_map" => "flatMap",
2091 _ => unreachable!(),
2092 };
2093 self.buf.push('(');
2094 self.emit_expr(recv)?;
2095 let _ = write!(self.buf, ").{native}(");
2096 self.emit_expr(&cb.value)?;
2097 self.buf.push(')');
2098 }
2099 "reduce" => {
2100 let Some(cb) = rest.first() else {
2101 return Ok(false);
2102 };
2103 self.buf.push('(');
2104 self.emit_expr(recv)?;
2105 self.buf.push_str(").reduce(");
2106 self.emit_expr(&cb.value)?;
2107 self.buf.push(')');
2108 }
2109 "fold" => {
2110 let (Some(init), Some(cb)) = (rest.first(), rest.get(1)) else {
2111 return Ok(false);
2112 };
2113 self.buf.push('(');
2114 self.emit_expr(recv)?;
2115 self.buf.push_str(").reduce(");
2116 self.emit_expr(&cb.value)?;
2117 self.buf.push_str(", ");
2118 self.emit_expr(&init.value)?;
2119 self.buf.push(')');
2120 }
2121 "find" => {
2122 self.buf.push_str(
2123 "(<T,>(__r: ReadonlyArray<T>, __p: (x: T) => boolean): BockOption<T> => \
2124 { const __m = __r.find(__p); return __m === undefined ? \
2125 { _tag: \"None\" as const } : { _tag: \"Some\" as const, _0: __m }; })(",
2126 );
2127 self.emit_expr(recv)?;
2128 self.buf.push_str(", ");
2129 let Some(cb) = rest.first() else {
2130 return Ok(false);
2131 };
2132 self.emit_expr(&cb.value)?;
2133 self.buf.push(')');
2134 }
2135 _ => return Ok(false),
2136 }
2137 Ok(true)
2138 }
2139
2140 fn try_emit_map_method(
2151 &mut self,
2152 node: &AIRNode,
2153 callee: &AIRNode,
2154 args: &[bock_air::AirArg],
2155 ) -> Result<bool, CodegenError> {
2156 let Some((recv, method, rest)) = crate::generator::desugared_map_method(node, callee, args)
2157 else {
2158 return Ok(false);
2159 };
2160 match method {
2161 "len" | "length" | "count" => {
2162 self.buf.push('(');
2163 self.emit_expr(recv)?;
2164 self.buf.push_str(").size");
2165 }
2166 "is_empty" => {
2167 self.buf.push_str("((");
2168 self.emit_expr(recv)?;
2169 self.buf.push_str(").size === 0)");
2170 }
2171 "contains_key" => {
2172 let Some(k) = rest.first() else {
2173 return Ok(false);
2174 };
2175 self.buf.push('(');
2176 self.emit_expr(recv)?;
2177 self.buf.push_str(").has(");
2178 self.emit_expr(&k.value)?;
2179 self.buf.push(')');
2180 }
2181 "get" => {
2182 let Some(k) = rest.first() else {
2183 return Ok(false);
2184 };
2185 self.buf.push_str(
2186 "(<K, V>(__m: Map<K, V>, __k: K): BockOption<V> => __m.has(__k) ? \
2187 { _tag: \"Some\" as const, _0: __m.get(__k)! } : { _tag: \"None\" as const })(",
2188 );
2189 self.emit_expr(recv)?;
2190 self.buf.push_str(", ");
2191 self.emit_expr(&k.value)?;
2192 self.buf.push(')');
2193 }
2194 "set" => {
2195 let (Some(k), Some(v)) = (rest.first(), rest.get(1)) else {
2196 return Ok(false);
2197 };
2198 self.buf.push_str(
2199 "(<K, V>(__m: Map<K, V>, __k: K, __v: V): Map<K, V> => \
2200 { __m.set(__k, __v); return __m; })(",
2201 );
2202 self.emit_expr(recv)?;
2203 self.buf.push_str(", ");
2204 self.emit_expr(&k.value)?;
2205 self.buf.push_str(", ");
2206 self.emit_expr(&v.value)?;
2207 self.buf.push(')');
2208 }
2209 "delete" => {
2210 let Some(k) = rest.first() else {
2211 return Ok(false);
2212 };
2213 self.buf.push_str(
2214 "(<K, V>(__m: Map<K, V>, __k: K): Map<K, V> => \
2215 { __m.delete(__k); return __m; })(",
2216 );
2217 self.emit_expr(recv)?;
2218 self.buf.push_str(", ");
2219 self.emit_expr(&k.value)?;
2220 self.buf.push(')');
2221 }
2222 "merge" => {
2223 let Some(o) = rest.first() else {
2224 return Ok(false);
2225 };
2226 self.buf.push_str(
2227 "(<K, V>(__m: Map<K, V>, __o: Map<K, V>): Map<K, V> => \
2228 { for (const [__k, __v] of __o) __m.set(__k, __v); return __m; })(",
2229 );
2230 self.emit_expr(recv)?;
2231 self.buf.push_str(", ");
2232 self.emit_expr(&o.value)?;
2233 self.buf.push(')');
2234 }
2235 "filter" => {
2236 let Some(f) = rest.first() else {
2237 return Ok(false);
2238 };
2239 self.buf.push_str(
2240 "(<K, V>(__m: Map<K, V>, __f: (k: K, v: V) => boolean): Map<K, V> => \
2241 { const __r = new Map<K, V>(); \
2242 for (const [__k, __v] of __m) if (__f(__k, __v)) __r.set(__k, __v); \
2243 return __r; })(",
2244 );
2245 self.emit_expr(recv)?;
2246 self.buf.push_str(", ");
2247 self.emit_expr(&f.value)?;
2248 self.buf.push(')');
2249 }
2250 "keys" => {
2251 self.buf.push_str("[...(");
2252 self.emit_expr(recv)?;
2253 self.buf.push_str(").keys()]");
2254 }
2255 "values" => {
2256 self.buf.push_str("[...(");
2257 self.emit_expr(recv)?;
2258 self.buf.push_str(").values()]");
2259 }
2260 "entries" | "to_list" => {
2261 self.buf.push_str("[...(");
2262 self.emit_expr(recv)?;
2263 self.buf.push_str(").entries()]");
2264 }
2265 "for_each" => {
2266 let Some(f) = rest.first() else {
2267 return Ok(false);
2268 };
2269 self.buf.push_str(
2270 "(<K, V>(__m: Map<K, V>, __f: (k: K, v: V) => void): void => \
2271 { for (const [__k, __v] of __m) __f(__k, __v); })(",
2272 );
2273 self.emit_expr(recv)?;
2274 self.buf.push_str(", ");
2275 self.emit_expr(&f.value)?;
2276 self.buf.push(')');
2277 }
2278 _ => return Ok(false),
2279 }
2280 Ok(true)
2281 }
2282
2283 fn try_emit_set_method(
2290 &mut self,
2291 node: &AIRNode,
2292 callee: &AIRNode,
2293 args: &[bock_air::AirArg],
2294 ) -> Result<bool, CodegenError> {
2295 let Some((recv, method, rest)) = crate::generator::desugared_set_method(node, callee, args)
2296 else {
2297 return Ok(false);
2298 };
2299 match method {
2300 "len" | "length" | "count" => {
2301 self.buf.push('(');
2302 self.emit_expr(recv)?;
2303 self.buf.push_str(").size");
2304 }
2305 "is_empty" => {
2306 self.buf.push_str("((");
2307 self.emit_expr(recv)?;
2308 self.buf.push_str(").size === 0)");
2309 }
2310 "contains" => {
2311 let Some(x) = rest.first() else {
2312 return Ok(false);
2313 };
2314 self.buf.push('(');
2315 self.emit_expr(recv)?;
2316 self.buf.push_str(").has(");
2317 self.emit_expr(&x.value)?;
2318 self.buf.push(')');
2319 }
2320 "add" => {
2321 let Some(x) = rest.first() else {
2322 return Ok(false);
2323 };
2324 self.buf.push_str(
2325 "(<E>(__s: Set<E>, __x: E): Set<E> => { __s.add(__x); return __s; })(",
2326 );
2327 self.emit_expr(recv)?;
2328 self.buf.push_str(", ");
2329 self.emit_expr(&x.value)?;
2330 self.buf.push(')');
2331 }
2332 "remove" => {
2333 let Some(x) = rest.first() else {
2334 return Ok(false);
2335 };
2336 self.buf.push_str(
2337 "(<E>(__s: Set<E>, __x: E): Set<E> => { __s.delete(__x); return __s; })(",
2338 );
2339 self.emit_expr(recv)?;
2340 self.buf.push_str(", ");
2341 self.emit_expr(&x.value)?;
2342 self.buf.push(')');
2343 }
2344 "union" => {
2345 let Some(o) = rest.first() else {
2346 return Ok(false);
2347 };
2348 self.buf.push_str(
2349 "(<E>(__a: Set<E>, __b: Set<E>): Set<E> => new Set<E>([...__a, ...__b]))(",
2350 );
2351 self.emit_expr(recv)?;
2352 self.buf.push_str(", ");
2353 self.emit_expr(&o.value)?;
2354 self.buf.push(')');
2355 }
2356 "intersection" => {
2357 let Some(o) = rest.first() else {
2358 return Ok(false);
2359 };
2360 self.buf.push_str(
2361 "(<E>(__a: Set<E>, __b: Set<E>): Set<E> => \
2362 new Set<E>([...__a].filter((__x) => __b.has(__x))))(",
2363 );
2364 self.emit_expr(recv)?;
2365 self.buf.push_str(", ");
2366 self.emit_expr(&o.value)?;
2367 self.buf.push(')');
2368 }
2369 "difference" => {
2370 let Some(o) = rest.first() else {
2371 return Ok(false);
2372 };
2373 self.buf.push_str(
2374 "(<E>(__a: Set<E>, __b: Set<E>): Set<E> => \
2375 new Set<E>([...__a].filter((__x) => !__b.has(__x))))(",
2376 );
2377 self.emit_expr(recv)?;
2378 self.buf.push_str(", ");
2379 self.emit_expr(&o.value)?;
2380 self.buf.push(')');
2381 }
2382 "is_subset" => {
2383 let Some(o) = rest.first() else {
2384 return Ok(false);
2385 };
2386 self.buf.push_str(
2387 "(<E>(__a: Set<E>, __b: Set<E>): boolean => \
2388 [...__a].every((__x) => __b.has(__x)))(",
2389 );
2390 self.emit_expr(recv)?;
2391 self.buf.push_str(", ");
2392 self.emit_expr(&o.value)?;
2393 self.buf.push(')');
2394 }
2395 "is_superset" => {
2396 let Some(o) = rest.first() else {
2397 return Ok(false);
2398 };
2399 self.buf.push_str(
2400 "(<E>(__a: Set<E>, __b: Set<E>): boolean => \
2401 [...__b].every((__x) => __a.has(__x)))(",
2402 );
2403 self.emit_expr(recv)?;
2404 self.buf.push_str(", ");
2405 self.emit_expr(&o.value)?;
2406 self.buf.push(')');
2407 }
2408 "filter" => {
2409 let Some(f) = rest.first() else {
2410 return Ok(false);
2411 };
2412 self.buf.push_str(
2413 "(<E>(__s: Set<E>, __f: (x: E) => boolean): Set<E> => \
2414 new Set<E>([...__s].filter(__f)))(",
2415 );
2416 self.emit_expr(recv)?;
2417 self.buf.push_str(", ");
2418 self.emit_expr(&f.value)?;
2419 self.buf.push(')');
2420 }
2421 "map" => {
2422 let Some(f) = rest.first() else {
2423 return Ok(false);
2424 };
2425 self.buf.push_str(
2426 "(<E>(__s: Set<E>, __f: (x: E) => E): Set<E> => \
2427 new Set<E>([...__s].map(__f)))(",
2428 );
2429 self.emit_expr(recv)?;
2430 self.buf.push_str(", ");
2431 self.emit_expr(&f.value)?;
2432 self.buf.push(')');
2433 }
2434 "to_list" => {
2435 self.buf.push_str("[...(");
2436 self.emit_expr(recv)?;
2437 self.buf.push_str(")]");
2438 }
2439 "for_each" => {
2440 let Some(f) = rest.first() else {
2441 return Ok(false);
2442 };
2443 self.buf.push_str(
2444 "(<E>(__s: Set<E>, __f: (x: E) => void): void => \
2445 { for (const __x of __s) __f(__x); })(",
2446 );
2447 self.emit_expr(recv)?;
2448 self.buf.push_str(", ");
2449 self.emit_expr(&f.value)?;
2450 self.buf.push(')');
2451 }
2452 _ => return Ok(false),
2453 }
2454 Ok(true)
2455 }
2456
2457 fn try_emit_string_method(
2482 &mut self,
2483 node: &AIRNode,
2484 callee: &AIRNode,
2485 args: &[bock_air::AirArg],
2486 ) -> Result<bool, CodegenError> {
2487 if crate::generator::primitive_recv_kind(node) != Some("String") {
2488 return Ok(false);
2489 }
2490 let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
2491 return Ok(false);
2492 };
2493 let method = field.name.as_str();
2494 let recv_str = self.expr_to_string(recv)?;
2495 let arg0 = |this: &mut Self| -> Result<Option<String>, CodegenError> {
2496 rest.first()
2497 .map(|a| this.expr_to_string(&a.value))
2498 .transpose()
2499 };
2500 let code = match method {
2501 "len" | "length" | "count" => format!("[...({recv_str})].length"),
2502 "byte_len" => format!("new TextEncoder().encode({recv_str}).length"),
2503 "is_empty" => format!("(({recv_str}).length === 0)"),
2504 "to_upper" => format!("({recv_str}).toUpperCase()"),
2505 "to_lower" => format!("({recv_str}).toLowerCase()"),
2506 "trim" => format!("({recv_str}).trim()"),
2507 "trim_start" => format!("({recv_str}).trimStart()"),
2508 "trim_end" => format!("({recv_str}).trimEnd()"),
2509 "reverse" => format!("[...({recv_str})].reverse().join('')"),
2510 "to_string" | "display" => format!("String({recv_str})"),
2511 "repeat" => {
2512 let Some(n) = arg0(self)? else {
2513 return Ok(false);
2514 };
2515 format!("({recv_str}).repeat({n})")
2516 }
2517 "contains" => {
2518 let Some(p) = arg0(self)? else {
2519 return Ok(false);
2520 };
2521 format!("({recv_str}).includes({p})")
2522 }
2523 "starts_with" => {
2524 let Some(p) = arg0(self)? else {
2525 return Ok(false);
2526 };
2527 format!("({recv_str}).startsWith({p})")
2528 }
2529 "ends_with" => {
2530 let Some(p) = arg0(self)? else {
2531 return Ok(false);
2532 };
2533 format!("({recv_str}).endsWith({p})")
2534 }
2535 "replace" => {
2536 let Some(from) = arg0(self)? else {
2537 return Ok(false);
2538 };
2539 let Some(to) = rest
2540 .get(1)
2541 .map(|a| self.expr_to_string(&a.value))
2542 .transpose()?
2543 else {
2544 return Ok(false);
2545 };
2546 format!("({recv_str}).replaceAll({from}, {to})")
2547 }
2548 "split" => {
2549 let Some(sep) = arg0(self)? else {
2550 return Ok(false);
2551 };
2552 format!("({recv_str}).split({sep})")
2553 }
2554 "slice" | "substring" => {
2557 let Some(start) = arg0(self)? else {
2558 return Ok(false);
2559 };
2560 let Some(end) = rest
2561 .get(1)
2562 .map(|a| self.expr_to_string(&a.value))
2563 .transpose()?
2564 else {
2565 return Ok(false);
2566 };
2567 format!("[...({recv_str})].slice({start}, {end}).join('')")
2568 }
2569 "char_at" => {
2571 let Some(i) = arg0(self)? else {
2572 return Ok(false);
2573 };
2574 self.needs_runtime_optional = true;
2575 format!(
2576 "((__s: string[], __i: number): BockOption<string> => __i >= 0 && __i < __s.length ? {{ _tag: \"Some\" as const, _0: __s[__i] }} : {{ _tag: \"None\" as const }})([...({recv_str})], {i})"
2577 )
2578 }
2579 "index_of" => {
2583 let Some(p) = arg0(self)? else {
2584 return Ok(false);
2585 };
2586 self.needs_runtime_optional = true;
2587 format!(
2588 "((__s: string, __p: string): BockOption<number> => {{ const __b = __s.indexOf(__p); return __b >= 0 ? {{ _tag: \"Some\" as const, _0: [...__s.slice(0, __b)].length }} : {{ _tag: \"None\" as const }}; }})({recv_str}, {p})"
2589 )
2590 }
2591 _ => return Ok(false),
2592 };
2593 self.buf.push_str(&code);
2594 Ok(true)
2595 }
2596
2597 fn try_emit_numeric_method(
2608 &mut self,
2609 node: &AIRNode,
2610 callee: &AIRNode,
2611 args: &[bock_air::AirArg],
2612 ) -> Result<bool, CodegenError> {
2613 let prim = match crate::generator::primitive_recv_kind(node) {
2614 Some(p @ ("Int" | "Float" | "Char" | "Bool")) => p,
2615 _ => return Ok(false),
2616 };
2617 let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
2618 return Ok(false);
2619 };
2620 let method = field.name.as_str();
2621 let recv_str = self.expr_to_string(recv)?;
2622 let arg = |this: &mut Self, i: usize| -> Result<Option<String>, CodegenError> {
2623 rest.get(i)
2624 .map(|a| this.expr_to_string(&a.value))
2625 .transpose()
2626 };
2627 let code = match (prim, method) {
2628 ("Int", "to_float") => format!("({recv_str})"),
2631 ("Float", "to_int") => format!("Math.trunc({recv_str})"),
2632 ("Char", "to_int") => format!("(({recv_str}).codePointAt(0) ?? 0)"),
2633 ("Bool", "to_int") => format!("(({recv_str}) ? 1 : 0)"),
2634 ("Int", "abs") => format!("Math.abs({recv_str})"),
2636 ("Int" | "Float", "min") => {
2637 let Some(o) = arg(self, 0)? else {
2638 return Ok(false);
2639 };
2640 format!("Math.min({recv_str}, {o})")
2641 }
2642 ("Int" | "Float", "max") => {
2643 let Some(o) = arg(self, 0)? else {
2644 return Ok(false);
2645 };
2646 format!("Math.max({recv_str}, {o})")
2647 }
2648 ("Int" | "Float", "clamp") => {
2649 let (Some(lo), Some(hi)) = (arg(self, 0)?, arg(self, 1)?) else {
2650 return Ok(false);
2651 };
2652 format!("Math.min(Math.max({recv_str}, {lo}), {hi})")
2653 }
2654 ("Int", "shift_left") => {
2655 let Some(o) = arg(self, 0)? else {
2656 return Ok(false);
2657 };
2658 format!("(({recv_str}) << ({o}))")
2659 }
2660 ("Int", "shift_right") => {
2661 let Some(o) = arg(self, 0)? else {
2662 return Ok(false);
2663 };
2664 format!("(({recv_str}) >> ({o}))")
2665 }
2666 ("Float", "abs") => format!("Math.abs({recv_str})"),
2668 ("Float", "floor") => format!("Math.floor({recv_str})"),
2669 ("Float", "ceil") => format!("Math.ceil({recv_str})"),
2670 ("Float", "round") => format!("Math.round({recv_str})"),
2671 ("Float", "sqrt") => format!("Math.sqrt({recv_str})"),
2672 ("Float", "is_nan") => format!("Number.isNaN({recv_str})"),
2673 ("Float", "is_infinite") => format!("(!Number.isFinite({recv_str}))"),
2674 ("Bool", "negate") => format!("(!({recv_str}))"),
2676 ("Char", "to_upper") => format!("({recv_str}).toUpperCase()"),
2678 ("Char", "to_lower") => format!("({recv_str}).toLowerCase()"),
2679 ("Char", "is_alpha") => format!("(/\\p{{L}}/u.test({recv_str}))"),
2680 ("Char", "is_digit") => format!("(/[0-9]/.test({recv_str}))"),
2681 ("Char", "is_whitespace") => format!("(/\\s/.test({recv_str}))"),
2682 _ => return Ok(false),
2683 };
2684 self.buf.push_str(&code);
2685 Ok(true)
2686 }
2687
2688 fn try_emit_primitive_bridge(
2689 &mut self,
2690 node: &AIRNode,
2691 callee: &AIRNode,
2692 args: &[bock_air::AirArg],
2693 ) -> Result<bool, CodegenError> {
2694 let Some((recv, method, rest, prim)) =
2695 crate::generator::primitive_bridge_call(node, callee, args)
2696 else {
2697 return Ok(false);
2698 };
2699 self.emit_bridge_method(recv, method, rest, Some(prim))
2700 }
2701
2702 fn try_emit_trait_bound_bridge(
2709 &mut self,
2710 node: &AIRNode,
2711 callee: &AIRNode,
2712 args: &[bock_air::AirArg],
2713 ) -> Result<bool, CodegenError> {
2714 let Some((recv, method, rest, _tr)) =
2715 crate::generator::trait_bound_bridge_call(node, callee, args, &self.trait_decls)
2716 else {
2717 return Ok(false);
2718 };
2719 if method == "eq" {
2725 let Some(other) = rest.first() else {
2726 return Ok(false);
2727 };
2728 let recv_str = self.expr_to_string(recv)?;
2729 let other = self.expr_to_string(&other.value)?;
2730 let _ = write!(self.buf, "__bockEq({recv_str}, {other})");
2731 return Ok(true);
2732 }
2733 if method == "compare" {
2742 let Some(other) = rest.first() else {
2743 return Ok(false);
2744 };
2745 let recv_str = self.expr_to_string(recv)?;
2746 let other = self.expr_to_string(&other.value)?;
2747 self.needs_runtime_compare = true;
2748 let _ = write!(self.buf, "__bockCompare({recv_str}, {other})");
2749 return Ok(true);
2750 }
2751 self.emit_bridge_method(recv, method, rest, None)
2752 }
2753
2754 fn emit_bridge_method(
2770 &mut self,
2771 recv: &AIRNode,
2772 method: &str,
2773 rest: &[bock_air::AirArg],
2774 widen_prim: Option<&str>,
2775 ) -> Result<bool, CodegenError> {
2776 let recv_str = self.expr_to_string(recv)?;
2777 match method {
2778 "compare" => {
2779 let Some(other) = rest.first() else {
2780 return Ok(false);
2781 };
2782 let other = self.expr_to_string(&other.value)?;
2783 let _ = write!(
2784 self.buf,
2785 "(({recv_str}) < ({other}) ? {{ _tag: \"Less\" as const }} : \
2786 (({recv_str}) === ({other}) ? {{ _tag: \"Equal\" as const }} : \
2787 {{ _tag: \"Greater\" as const }}))"
2788 );
2789 }
2790 "eq" => {
2791 let Some(other) = rest.first() else {
2792 return Ok(false);
2793 };
2794 let other = self.expr_to_string(&other.value)?;
2795 if let Some(prim) = widen_prim {
2800 let ts_ty = self.map_type_name(prim);
2801 let _ = write!(self.buf, "(({recv_str} as {ts_ty}) === ({other}))");
2802 } else {
2803 let _ = write!(self.buf, "(({recv_str}) === ({other}))");
2804 }
2805 }
2806 "to_string" | "display" => {
2807 let _ = write!(self.buf, "String({recv_str})");
2808 }
2809 _ => return Ok(false),
2810 }
2811 Ok(true)
2812 }
2813
2814 fn type_to_ts(&self, node: &AIRNode) -> String {
2818 match &node.kind {
2819 NodeKind::TypeNamed { path, args } => {
2820 let name = path
2821 .segments
2822 .iter()
2823 .map(|s| s.name.as_str())
2824 .collect::<Vec<_>>()
2825 .join(".");
2826 let ts_name = self.map_type_name(&name);
2827 if args.is_empty() {
2828 ts_name
2829 } else {
2830 let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_ts(a)).collect();
2831 format!("{ts_name}<{}>", arg_strs.join(", "))
2832 }
2833 }
2834 NodeKind::TypeTuple { elems } => {
2835 let elem_strs: Vec<String> = elems.iter().map(|e| self.type_to_ts(e)).collect();
2836 format!("[{}]", elem_strs.join(", "))
2837 }
2838 NodeKind::TypeFunction { params, ret, .. } => {
2839 let param_strs: Vec<String> = params
2840 .iter()
2841 .enumerate()
2842 .map(|(i, p)| format!("arg{i}: {}", self.type_to_ts(p)))
2843 .collect();
2844 format!("({}) => {}", param_strs.join(", "), self.type_to_ts(ret))
2845 }
2846 NodeKind::TypeOptional { inner } => {
2847 format!("BockOption<{}>", self.type_to_ts(inner))
2852 }
2853 NodeKind::TypeSelf => self
2854 .trait_self_subst
2855 .clone()
2856 .unwrap_or_else(|| "this".into()),
2857 _ => "unknown".into(),
2858 }
2859 }
2860
2861 fn ordering_enum_reachable(&self) -> bool {
2868 self.enum_variants
2869 .get("Less")
2870 .is_some_and(|info| info.enum_name == "Ordering")
2871 }
2872
2873 fn map_type_name(&self, name: &str) -> String {
2874 match name {
2875 "Int" => "number".into(),
2876 "Float" => "number".into(),
2877 "Bool" => "boolean".into(),
2878 "String" => "string".into(),
2879 "Char" => "string".into(),
2884 "Void" | "Unit" => "void".into(),
2885 "List" => "Array".into(),
2886 "Map" => "Map".into(),
2887 "Set" => "Set".into(),
2888 "Any" => "any".into(),
2889 "Never" => "never".into(),
2890 "Result" => "BockResult".into(),
2893 "Optional" => "BockOption".into(),
2900 "Duration" | "Instant" => "number".into(),
2907 "Ordering" if !self.ordering_enum_reachable() => {
2915 "({ _tag: \"Less\" } | { _tag: \"Equal\" } | { _tag: \"Greater\" })".into()
2916 }
2917 other => other.into(),
2918 }
2919 }
2920
2921 fn ast_type_to_ts(&self, ty: &TypeExpr) -> String {
2923 match ty {
2924 TypeExpr::Named { path, args, .. } => {
2925 let name = path
2926 .segments
2927 .iter()
2928 .map(|s| s.name.as_str())
2929 .collect::<Vec<_>>()
2930 .join(".");
2931 let ts_name = self.map_type_name(&name);
2932 if args.is_empty() {
2933 ts_name
2934 } else {
2935 let arg_strs: Vec<String> =
2936 args.iter().map(|a| self.ast_type_to_ts(a)).collect();
2937 format!("{ts_name}<{}>", arg_strs.join(", "))
2938 }
2939 }
2940 TypeExpr::Tuple { elems, .. } => {
2941 let elem_strs: Vec<String> = elems.iter().map(|e| self.ast_type_to_ts(e)).collect();
2942 format!("[{}]", elem_strs.join(", "))
2943 }
2944 TypeExpr::Function { params, ret, .. } => {
2945 let param_strs: Vec<String> = params
2946 .iter()
2947 .enumerate()
2948 .map(|(i, p)| format!("arg{i}: {}", self.ast_type_to_ts(p)))
2949 .collect();
2950 format!(
2951 "({}) => {}",
2952 param_strs.join(", "),
2953 self.ast_type_to_ts(ret)
2954 )
2955 }
2956 TypeExpr::Optional { inner, .. } => {
2957 format!("BockOption<{}>", self.ast_type_to_ts(inner))
2960 }
2961 TypeExpr::SelfType { .. } => "this".into(),
2962 }
2963 }
2964
2965 fn impl_target_generics(
2971 &self,
2972 impl_params: &[bock_ast::GenericParam],
2973 target_name: &str,
2974 ) -> Vec<bock_ast::GenericParam> {
2975 if !impl_params.is_empty() {
2976 return impl_params.to_vec();
2977 }
2978 self.generic_decls
2979 .get(target_name)
2980 .cloned()
2981 .unwrap_or_default()
2982 }
2983
2984 fn generic_param_args(&self, params: &[bock_ast::GenericParam]) -> String {
2988 if params.is_empty() {
2989 return String::new();
2990 }
2991 let names: Vec<&str> = params.iter().map(|p| p.name.name.as_str()).collect();
2992 format!("<{}>", names.join(", "))
2993 }
2994
2995 fn merge_generic_params_to_ts(
3000 &self,
3001 target_params: &[bock_ast::GenericParam],
3002 method_params: &[bock_ast::GenericParam],
3003 ) -> String {
3004 let mut merged = target_params.to_vec();
3005 merged.extend(method_params.iter().cloned());
3006 self.generic_params_to_ts(&merged)
3007 }
3008
3009 fn generic_params_to_ts(&self, params: &[bock_ast::GenericParam]) -> String {
3010 if params.is_empty() {
3011 return String::new();
3012 }
3013 let items: Vec<String> = params
3014 .iter()
3015 .map(|p| {
3016 let bounds: Vec<String> = p
3023 .bounds
3024 .iter()
3025 .map(|b| {
3026 b.segments
3027 .iter()
3028 .map(|s| s.name.as_str())
3029 .collect::<Vec<_>>()
3030 .join(".")
3031 })
3032 .filter(|name| {
3033 !crate::generator::is_unimplemented_sealed_core_trait(
3034 name,
3035 &self.trait_decls,
3036 )
3037 })
3038 .collect();
3039 if bounds.is_empty() {
3040 p.name.name.clone()
3041 } else {
3042 format!("{} extends {}", p.name.name, bounds.join(" & "))
3043 }
3044 })
3045 .collect();
3046 format!("<{}>", items.join(", "))
3047 }
3048
3049 fn generic_args_to_ts(&self, params: &[bock_ast::GenericParam]) -> String {
3063 if params.is_empty() {
3064 return String::new();
3065 }
3066 let items: Vec<String> = params.iter().map(|p| p.name.name.clone()).collect();
3067 format!("<{}>", items.join(", "))
3068 }
3069
3070 fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
3073 self.mark_span(node.span);
3074 match &node.kind {
3075 NodeKind::Module { items, imports, .. } => {
3076 self.field_method_collisions
3082 .extend(crate::generator::collect_record_field_names(
3083 node,
3084 to_camel_case,
3085 ));
3086 if self.per_module {
3087 if module_uses_optional(items) {
3094 self.needs_runtime_optional = true;
3095 }
3096 if module_uses_result(items) {
3097 self.needs_runtime_result = true;
3098 }
3099 if module_uses_concurrency(items) {
3100 self.needs_runtime_concurrency = true;
3101 }
3102 if module_uses_range(items) {
3103 self.needs_runtime_range = true;
3104 }
3105 if module_uses_eq(items) {
3106 self.needs_runtime_eq = true;
3107 }
3108 if module_uses_compare(items) {
3109 self.needs_runtime_compare = true;
3110 }
3111 if module_uses_str(items) {
3112 self.needs_runtime_str = true;
3113 }
3114 self.emit_esm_imports(imports)?;
3115 } else {
3116 if !self.optional_runtime_emitted && module_uses_optional(items) {
3122 self.buf.push_str(OPTIONAL_RUNTIME_TS);
3123 self.buf.push('\n');
3124 self.optional_runtime_emitted = true;
3125 }
3126 if !self.result_runtime_emitted && module_uses_result(items) {
3127 self.buf.push_str(RESULT_RUNTIME_TS);
3128 self.buf.push('\n');
3129 self.result_runtime_emitted = true;
3130 }
3131 if !self.concurrency_runtime_emitted && module_uses_concurrency(items) {
3132 self.buf.push_str(CONCURRENCY_RUNTIME_TS);
3133 self.buf.push('\n');
3134 self.concurrency_runtime_emitted = true;
3135 }
3136 if !self.range_runtime_emitted && module_uses_range(items) {
3137 self.buf.push_str(RANGE_RUNTIME_TS);
3138 self.buf.push('\n');
3139 self.range_runtime_emitted = true;
3140 }
3141 if !self.compare_runtime_emitted && module_uses_compare(items) {
3142 self.buf.push_str(COMPARE_RUNTIME_TS);
3143 self.buf.push('\n');
3144 self.compare_runtime_emitted = true;
3145 }
3146 if !self.str_runtime_emitted && module_uses_str(items) {
3147 self.buf.push_str(STR_RUNTIME_TS);
3148 self.buf.push('\n');
3149 self.str_runtime_emitted = true;
3150 }
3151 if !self.eq_runtime_emitted && module_uses_eq(items) {
3152 self.buf.push_str(EQ_RUNTIME_TS);
3153 self.buf.push('\n');
3154 self.eq_runtime_emitted = true;
3155 }
3156 }
3157 let mut first = true;
3162 for item in items.iter() {
3163 if crate::generator::fn_is_test(item) {
3164 continue;
3165 }
3166 if !first {
3167 self.buf.push('\n');
3168 }
3169 first = false;
3170 self.emit_node(item)?;
3171 }
3172 if self.per_module {
3175 self.emit_trailing_exports();
3176 }
3177 Ok(())
3178 }
3179 NodeKind::ImportDecl { .. } => {
3180 Ok(())
3184 }
3185 NodeKind::FnDecl {
3186 visibility,
3187 is_async,
3188 name,
3189 generic_params,
3190 params,
3191 return_type,
3192 effect_clause,
3193 where_clause,
3194 body,
3195 ..
3196 } => {
3197 let merged = crate::generator::merge_where_bounds_into_generics(
3203 generic_params,
3204 where_clause,
3205 );
3206 self.emit_fn_decl(
3207 *visibility,
3208 *is_async,
3209 &name.name,
3210 &merged,
3211 params,
3212 return_type.as_deref(),
3213 effect_clause,
3214 body,
3215 false,
3216 )
3217 }
3218 NodeKind::RecordDecl {
3219 visibility,
3220 name,
3221 generic_params,
3222 fields,
3223 ..
3224 } => {
3225 let export = if matches!(visibility, Visibility::Public) {
3226 "export "
3227 } else {
3228 ""
3229 };
3230 let generics = self.generic_params_to_ts(generic_params);
3231 self.record_names.insert(name.name.clone());
3232 if fields.is_empty() {
3233 self.writeln(&format!("{export}class {}{generics} {{}}", name.name));
3234 } else {
3235 self.writeln(&format!("{export}class {}{generics} {{", name.name));
3236 self.indent += 1;
3237 for f in fields {
3238 let ty = self.ast_type_to_ts(&f.ty);
3239 self.writeln(&format!("{}: {};", f.name.name, ty));
3240 }
3241 let init_fields: Vec<String> = fields
3242 .iter()
3243 .map(|f| format!("{}: {}", f.name.name, self.ast_type_to_ts(&f.ty)))
3244 .collect();
3245 let destructure: Vec<&str> =
3246 fields.iter().map(|f| f.name.name.as_str()).collect();
3247 self.writeln(&format!(
3248 "constructor({{ {} }}: {{ {} }}) {{",
3249 destructure.join(", "),
3250 init_fields.join("; "),
3251 ));
3252 self.indent += 1;
3253 for fname in &destructure {
3254 self.writeln(&format!("this.{fname} = {fname};"));
3255 }
3256 self.indent -= 1;
3257 self.writeln("}");
3258 self.indent -= 1;
3259 self.writeln("}");
3260 }
3261 Ok(())
3262 }
3263 NodeKind::EnumDecl {
3264 visibility,
3265 name,
3266 generic_params,
3267 variants,
3268 ..
3269 } => {
3270 let export = if matches!(visibility, Visibility::Public) {
3271 "export "
3272 } else {
3273 ""
3274 };
3275 let generics = self.generic_params_to_ts(generic_params);
3276 let type_args = self.generic_args_to_ts(generic_params);
3282
3283 let variant_names: Vec<String> = variants
3285 .iter()
3286 .filter_map(|v| {
3287 if let NodeKind::EnumVariant { name: vn, .. } = &v.kind {
3288 Some(format!("{}_{}{type_args}", name.name, vn.name))
3289 } else {
3290 None
3291 }
3292 })
3293 .collect();
3294 if !variant_names.is_empty() {
3295 self.writeln(&format!(
3296 "{export}type {}{generics} = {};",
3297 name.name,
3298 variant_names.join(" | "),
3299 ));
3300 self.buf.push('\n');
3301 }
3302
3303 for variant in variants {
3305 self.emit_enum_variant(&name.name, generic_params, variant)?;
3306 }
3307 Ok(())
3308 }
3309 NodeKind::ClassDecl {
3310 visibility,
3311 name,
3312 generic_params,
3313 fields,
3314 methods,
3315 ..
3316 } => {
3317 self.class_fields.insert(
3323 name.name.clone(),
3324 fields.iter().map(|f| f.name.name.clone()).collect(),
3325 );
3326 let export = if matches!(visibility, Visibility::Public) {
3327 "export "
3328 } else {
3329 ""
3330 };
3331 let generics = self.generic_params_to_ts(generic_params);
3332 self.writeln(&format!("{export}class {}{generics} {{", name.name));
3333 self.indent += 1;
3334 for f in fields {
3336 let ty = self.ast_type_to_ts(&f.ty);
3337 self.writeln(&format!("{}: {};", f.name.name, ty));
3338 }
3339 if !fields.is_empty() {
3340 self.buf.push('\n');
3341 }
3342 let ctor_params: Vec<String> = fields
3344 .iter()
3345 .map(|f| format!("{}: {}", f.name.name, self.ast_type_to_ts(&f.ty)))
3346 .collect();
3347 self.writeln(&format!("constructor({}) {{", ctor_params.join(", ")));
3348 self.indent += 1;
3349 for f in fields {
3350 self.writeln(&format!("this.{} = {};", f.name.name, f.name.name));
3351 }
3352 self.indent -= 1;
3353 self.writeln("}");
3354 for method in methods {
3356 self.buf.push('\n');
3357 self.emit_class_method(method)?;
3358 }
3359 self.indent -= 1;
3360 self.writeln("}");
3361 Ok(())
3362 }
3363 NodeKind::TraitDecl {
3364 visibility,
3365 name,
3366 generic_params,
3367 methods,
3368 ..
3369 } => {
3370 let export = if matches!(visibility, Visibility::Public) {
3371 "export "
3372 } else {
3373 ""
3374 };
3375 let generics = self.generic_params_to_ts(generic_params);
3376 let trait_self_ty =
3384 format!("{}{}", name.name, self.generic_param_args(generic_params));
3385 self.writeln(&format!("{export}interface {}{generics} {{", name.name));
3386 self.indent += 1;
3387 for (i, method) in methods.iter().enumerate() {
3388 if i > 0 {
3389 self.buf.push('\n');
3390 }
3391 if let NodeKind::FnDecl {
3392 name,
3393 generic_params: method_generics,
3394 params,
3395 return_type,
3396 ..
3397 } = &method.kind
3398 {
3399 let m_generics = self.generic_params_to_ts(method_generics);
3400 let param_list = self.collect_trait_typed_params(params, &trait_self_ty);
3401 let ret = return_type
3402 .as_ref()
3403 .map(|r| self.type_to_ts(r))
3404 .unwrap_or_else(|| "void".into());
3405 self.writeln(&format!(
3406 "{}{m_generics}({}): {};",
3407 self.ts_method_name(&name.name),
3408 param_list.join(", "),
3409 ret,
3410 ));
3411 }
3412 }
3413 self.indent -= 1;
3414 self.writeln("}");
3415 Ok(())
3416 }
3417 NodeKind::ImplBlock {
3418 generic_params,
3419 trait_path,
3420 trait_args,
3421 target,
3422 methods,
3423 ..
3424 } => {
3425 let target_base = self.type_expr_to_string(target);
3426 let target_params = self.impl_target_generics(generic_params, &target_base);
3434 let target_name =
3435 format!("{target_base}{}", self.generic_param_args(&target_params));
3436 let default_methods: Vec<AIRNode> = trait_path
3445 .as_ref()
3446 .map(|tp| {
3447 crate::generator::inherited_default_methods(&self.trait_decls, tp, methods)
3448 })
3449 .unwrap_or_default();
3450 let all_methods: Vec<(&AIRNode, bool)> = methods
3459 .iter()
3460 .map(|m| (m, false))
3461 .chain(default_methods.iter().map(|m| (m, true)))
3462 .collect();
3463 let mut iface_sigs: Vec<String> = Vec::new();
3474 for (method, _is_default) in &all_methods {
3475 if crate::generator::is_associated_impl_method(method, &self.effect_ops) {
3479 continue;
3480 }
3481 if let NodeKind::FnDecl {
3482 is_async,
3483 name,
3484 generic_params,
3485 params,
3486 return_type,
3487 effect_clause,
3488 ..
3489 } = &method.kind
3490 {
3491 let prev_subst = self.trait_self_subst.take();
3498 self.trait_self_subst = Some(target_name.clone());
3499 let generics = self.generic_params_to_ts(generic_params);
3500 let mut all_params = self.collect_impl_typed_params(params, &target_name);
3501 if let Some(ep) = self.effects_param(effect_clause) {
3502 all_params.push(ep);
3503 }
3504 let ret_str = build_ts_return_type(
3505 *is_async,
3506 return_type.as_deref().map(|r| self.type_to_ts(r)),
3507 );
3508 self.trait_self_subst = prev_subst;
3509 iface_sigs.push(format!(
3510 "{}{generics}({}){ret_str};",
3511 self.ts_method_name(&name.name),
3512 all_params.join(", "),
3513 ));
3514 }
3515 }
3516 let iface_export = if self.exported_types.contains(&target_base) {
3520 "export "
3521 } else {
3522 ""
3523 };
3524 let prelude_trait = trait_path.as_ref().is_some_and(|tp| {
3534 tp.segments.last().is_some_and(|seg| {
3535 crate::generator::is_unimplemented_sealed_core_trait(
3536 &seg.name,
3537 &self.trait_decls,
3538 )
3539 })
3540 });
3541 if let Some(tp) = trait_path.as_ref().filter(|_| !prelude_trait) {
3542 let trait_base = tp
3543 .segments
3544 .iter()
3545 .map(|s| s.name.as_str())
3546 .collect::<Vec<_>>()
3547 .join(".");
3548 let trait_name = if trait_args.is_empty() {
3556 trait_base
3557 } else {
3558 let arg_strs: Vec<String> =
3559 trait_args.iter().map(|a| self.type_to_ts(a)).collect();
3560 format!("{trait_base}<{}>", arg_strs.join(", "))
3561 };
3562 if iface_sigs.is_empty() {
3571 self.writeln(&format!("// impl {trait_name} for {target_name}"));
3572 } else {
3573 self.writeln(&format!(
3578 "{iface_export}interface {target_name} extends {trait_name} {{"
3579 ));
3580 self.indent += 1;
3581 for sig in &iface_sigs {
3582 self.writeln(sig);
3583 }
3584 self.indent -= 1;
3585 self.writeln("}");
3586 self.writeln(&format!("// impl {trait_name} for {target_name}"));
3587 }
3588 } else if iface_sigs.is_empty() {
3589 self.writeln(&format!("// impl {target_name}"));
3590 } else {
3591 self.writeln(&format!("{iface_export}interface {target_name} {{"));
3592 self.indent += 1;
3593 for sig in &iface_sigs {
3594 self.writeln(sig);
3595 }
3596 self.indent -= 1;
3597 self.writeln("}");
3598 self.writeln(&format!("// impl {target_name}"));
3599 }
3600 for (method, _is_default) in &all_methods {
3601 if crate::generator::is_associated_impl_method(method, &self.effect_ops) {
3604 continue;
3605 }
3606 if let NodeKind::FnDecl {
3607 is_async,
3608 name,
3609 generic_params,
3610 params,
3611 return_type,
3612 effect_clause,
3613 body,
3614 ..
3615 } = &method.kind
3616 {
3617 let prev_subst = self.trait_self_subst.take();
3625 self.trait_self_subst = Some(target_name.clone());
3626 let async_kw = if *is_async { "async " } else { "" };
3627 let generics =
3635 self.merge_generic_params_to_ts(&target_params, generic_params);
3636 let param_list = self.collect_impl_typed_params(params, &target_name);
3637 let effects_param = self.effects_param(effect_clause);
3638 let mut all_params = param_list;
3639 if let Some(ep) = effects_param {
3640 all_params.push(ep);
3641 }
3642 let ret_str = build_ts_return_type(
3643 *is_async,
3644 return_type.as_deref().map(|r| self.type_to_ts(r)),
3645 );
3646 self.writeln(&format!(
3647 "{target_base}.prototype.{} = {async_kw}function{generics}({}){ret_str} {{",
3648 self.ts_method_name(&name.name),
3649 all_params.join(", "),
3650 ));
3651 self.indent += 1;
3652 let old_handler_vars = self.current_handler_vars.clone();
3653 let expanded = self.expand_effect_names(effect_clause);
3654 for ename in &expanded {
3655 self.current_handler_vars
3656 .insert(ename.clone(), to_camel_case(ename));
3657 }
3658 self.emit_block_body(body)?;
3659 self.current_handler_vars = old_handler_vars;
3660 self.indent -= 1;
3661 self.writeln("};");
3662 self.trait_self_subst = prev_subst;
3666 }
3667 }
3668 let assoc_methods: Vec<&AIRNode> = all_methods
3675 .iter()
3676 .filter(|(m, _)| {
3677 crate::generator::is_associated_impl_method(m, &self.effect_ops)
3678 })
3679 .map(|(m, _)| *m)
3680 .collect();
3681 if !assoc_methods.is_empty() {
3682 let ns_export = if self.exported_types.contains(&target_base) {
3683 "export "
3684 } else {
3685 ""
3686 };
3687 self.writeln(&format!("{ns_export}namespace {target_base} {{"));
3688 self.indent += 1;
3689 for method in assoc_methods {
3690 if let NodeKind::FnDecl {
3691 is_async,
3692 name,
3693 generic_params,
3694 params,
3695 return_type,
3696 effect_clause,
3697 body,
3698 ..
3699 } = &method.kind
3700 {
3701 let prev_subst = self.trait_self_subst.take();
3702 self.trait_self_subst = Some(target_name.clone());
3703 let async_kw = if *is_async { "async " } else { "" };
3704 let generics =
3705 self.merge_generic_params_to_ts(&target_params, generic_params);
3706 let param_list = self.collect_impl_typed_params(params, &target_name);
3707 let effects_param = self.effects_param(effect_clause);
3708 let mut all_params = param_list;
3709 if let Some(ep) = effects_param {
3710 all_params.push(ep);
3711 }
3712 let ret_str = build_ts_return_type(
3713 *is_async,
3714 return_type.as_deref().map(|r| self.type_to_ts(r)),
3715 );
3716 self.writeln(&format!(
3717 "export {async_kw}function {}{generics}({}){ret_str} {{",
3718 self.ts_method_name(&name.name),
3719 all_params.join(", "),
3720 ));
3721 self.indent += 1;
3722 let old_handler_vars = self.current_handler_vars.clone();
3723 let expanded = self.expand_effect_names(effect_clause);
3724 for ename in &expanded {
3725 self.current_handler_vars
3726 .insert(ename.clone(), to_camel_case(ename));
3727 }
3728 self.emit_block_body(body)?;
3729 self.current_handler_vars = old_handler_vars;
3730 self.indent -= 1;
3731 self.writeln("}");
3732 self.trait_self_subst = prev_subst;
3733 }
3734 }
3735 self.indent -= 1;
3736 self.writeln("}");
3737 }
3738 Ok(())
3739 }
3740 NodeKind::EffectDecl {
3741 visibility,
3742 name,
3743 generic_params,
3744 components,
3745 operations,
3746 ..
3747 } => {
3748 if !components.is_empty() {
3749 let comp_names: Vec<String> = components
3750 .iter()
3751 .map(|tp| {
3752 tp.segments
3753 .last()
3754 .map_or("effect".to_string(), |s| s.name.clone())
3755 })
3756 .collect();
3757 self.writeln(&format!(
3758 "// composite effect {} = {}",
3759 name.name,
3760 comp_names.join(" + ")
3761 ));
3762 self.composite_effects.insert(name.name.clone(), comp_names);
3763 return Ok(());
3764 }
3765 for op in operations {
3767 if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
3768 self.effect_ops
3769 .insert(op_name.name.clone(), name.name.clone());
3770 }
3771 }
3772 self.effect_names.insert(name.name.clone());
3773 let export = if matches!(visibility, Visibility::Public) {
3775 "export "
3776 } else {
3777 ""
3778 };
3779 let generics = self.generic_params_to_ts(generic_params);
3780 self.writeln(&format!("{export}interface {}{generics} {{", name.name));
3781 self.indent += 1;
3782 for op in operations {
3783 if let NodeKind::FnDecl {
3784 name,
3785 params,
3786 return_type,
3787 ..
3788 } = &op.kind
3789 {
3790 let param_list = self.collect_typed_params(params);
3791 let ret = return_type
3792 .as_ref()
3793 .map(|r| self.type_to_ts(r))
3794 .unwrap_or_else(|| "void".into());
3795 self.writeln(&format!(
3796 "{}({}): {};",
3797 name.name,
3798 param_list.join(", "),
3799 ret,
3800 ));
3801 }
3802 }
3803 self.indent -= 1;
3804 self.writeln("}");
3805 Ok(())
3806 }
3807 NodeKind::TypeAlias {
3808 visibility,
3809 name,
3810 generic_params,
3811 ty,
3812 ..
3813 } => {
3814 let export = if matches!(visibility, Visibility::Public) {
3815 "export "
3816 } else {
3817 ""
3818 };
3819 let generics = self.generic_params_to_ts(generic_params);
3820 let ty_str = self.type_to_ts(ty);
3821 self.writeln(&format!("{export}type {}{generics} = {ty_str};", name.name));
3822 Ok(())
3823 }
3824 NodeKind::ConstDecl {
3825 visibility,
3826 name,
3827 ty,
3828 value,
3829 ..
3830 } => {
3831 let export = if matches!(visibility, Visibility::Public) {
3832 "export "
3833 } else {
3834 ""
3835 };
3836 let ty_str = self.type_to_ts(ty);
3837 let ind = self.indent_str();
3838 let _ = write!(self.buf, "{ind}{export}const {}: {ty_str} = ", name.name);
3839 self.emit_expr(value)?;
3840 self.buf.push_str(";\n");
3841 Ok(())
3842 }
3843 NodeKind::ModuleHandle { effect, handler } => {
3844 let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
3845 let var_name = format!("__{}", to_camel_case(effect_name));
3846 let type_name = effect_name;
3847 let ind = self.indent_str();
3848 let _ = write!(self.buf, "{ind}const {var_name}: {type_name} = ");
3849 self.emit_expr(handler)?;
3850 self.buf.push_str(";\n");
3851 self.current_handler_vars
3853 .insert(effect_name.to_string(), var_name);
3854 Ok(())
3855 }
3856 NodeKind::PropertyTest { name, body, .. } => {
3857 self.writeln(&format!("// property test: {name}"));
3858 self.writeln("// (property tests are not emitted in TS output)");
3859 let _ = body;
3860 Ok(())
3861 }
3862 NodeKind::LetBinding { .. }
3864 | NodeKind::If { .. }
3865 | NodeKind::For { .. }
3866 | NodeKind::While { .. }
3867 | NodeKind::Loop { .. }
3868 | NodeKind::Return { .. }
3869 | NodeKind::Break { .. }
3870 | NodeKind::Continue
3871 | NodeKind::Guard { .. }
3872 | NodeKind::Match { .. }
3873 | NodeKind::Block { .. }
3874 | NodeKind::HandlingBlock { .. }
3875 | NodeKind::Assign { .. } => self.emit_stmt(node),
3876 NodeKind::Propagate { expr } => {
3880 self.emit_propagate(expr)?;
3881 Ok(())
3882 }
3883 _ => {
3885 self.write_indent();
3886 self.emit_expr(node)?;
3887 self.buf.push_str(";\n");
3888 Ok(())
3889 }
3890 }
3891 }
3892
3893 #[allow(clippy::too_many_arguments)]
3896 fn emit_fn_decl(
3897 &mut self,
3898 visibility: Visibility,
3899 is_async: bool,
3900 name: &str,
3901 generic_params: &[bock_ast::GenericParam],
3902 params: &[AIRNode],
3903 return_type: Option<&AIRNode>,
3904 effect_clause: &[bock_ast::TypePath],
3905 body: &AIRNode,
3906 _is_method: bool,
3907 ) -> Result<(), CodegenError> {
3908 let export = if matches!(visibility, Visibility::Public) {
3909 "export "
3910 } else {
3911 ""
3912 };
3913 let async_kw = if is_async { "async " } else { "" };
3914 let generics = self.generic_params_to_ts(generic_params);
3915 let param_list = self.collect_typed_params(params);
3916 let effects_param = self.effects_param(effect_clause);
3917 let mut all_params = param_list;
3918 if let Some(ep) = effects_param {
3919 all_params.push(ep);
3920 }
3921 let ret_str = build_ts_return_type(is_async, return_type.map(|r| self.type_to_ts(r)));
3922 if !effect_clause.is_empty() {
3923 let effect_names = self.expand_effect_names(effect_clause);
3924 self.fn_effects.insert(name.to_string(), effect_names);
3925 }
3926 let ts_name = ts_value_ident(name);
3927 self.writeln(&format!(
3928 "{export}{async_kw}function {ts_name}{generics}({}){ret_str} {{",
3929 all_params.join(", "),
3930 ));
3931 self.indent += 1;
3932 let old_handler_vars = self.current_handler_vars.clone();
3933 let expanded = self.expand_effect_names(effect_clause);
3934 for ename in &expanded {
3935 self.current_handler_vars
3936 .insert(ename.clone(), to_camel_case(ename));
3937 }
3938 let prev_prop_tag = self.current_fn_propagate_tag;
3939 self.current_fn_propagate_tag = Self::propagate_tag_for_return(return_type);
3940 self.emit_fn_body_seeded(params, body)?;
3941 self.current_fn_propagate_tag = prev_prop_tag;
3942 self.current_handler_vars = old_handler_vars;
3943 self.indent -= 1;
3944 self.writeln("}");
3945 Ok(())
3946 }
3947
3948 fn emit_class_method(&mut self, method: &AIRNode) -> Result<(), CodegenError> {
3949 if let NodeKind::FnDecl {
3950 is_async,
3951 name,
3952 generic_params,
3953 params,
3954 return_type,
3955 effect_clause,
3956 body,
3957 ..
3958 } = &method.kind
3959 {
3960 let async_kw = if *is_async { "async " } else { "" };
3961 let generics = self.generic_params_to_ts(generic_params);
3962 let param_list = self.collect_typed_params(params);
3963 let effects_param = self.effects_param(effect_clause);
3964 let mut all_params = param_list;
3965 if let Some(ep) = effects_param {
3966 all_params.push(ep);
3967 }
3968 let ret_str = build_ts_return_type(
3969 *is_async,
3970 return_type.as_deref().map(|r| self.type_to_ts(r)),
3971 );
3972 let method_name = self.ts_method_name(&to_camel_case(&name.name));
3973 self.writeln(&format!(
3974 "{async_kw}{method_name}{generics}({}){ret_str} {{",
3975 all_params.join(", "),
3976 ));
3977 self.indent += 1;
3978 let old_handler_vars = self.current_handler_vars.clone();
3979 let expanded = self.expand_effect_names(effect_clause);
3980 for ename in &expanded {
3981 self.current_handler_vars
3982 .insert(ename.clone(), to_camel_case(ename));
3983 }
3984 let prev_prop_tag = self.current_fn_propagate_tag;
3985 self.current_fn_propagate_tag = Self::propagate_tag_for_return(return_type.as_deref());
3986 self.emit_fn_body_seeded(params, body)?;
3987 self.current_fn_propagate_tag = prev_prop_tag;
3988 self.current_handler_vars = old_handler_vars;
3989 self.indent -= 1;
3990 self.writeln("}");
3991 }
3992 Ok(())
3993 }
3994
3995 fn collect_lambda_params(&self, params: &[AIRNode]) -> Vec<String> {
4006 params
4007 .iter()
4008 .filter_map(|p| {
4009 let NodeKind::Param { pattern, ty, .. } = &p.kind else {
4010 return None;
4011 };
4012 let name = self.pattern_to_binding_name(pattern);
4013 let ty_str = match ty {
4014 Some(t) => format!(": {}", self.type_to_ts(t)),
4015 None => ": any".to_string(),
4016 };
4017 Some(format!("{name}{ty_str}"))
4018 })
4019 .collect()
4020 }
4021
4022 fn collect_typed_params(&self, params: &[AIRNode]) -> Vec<String> {
4024 params
4025 .iter()
4026 .filter_map(|p| {
4027 if let NodeKind::Param {
4028 pattern,
4029 ty,
4030 default,
4031 } = &p.kind
4032 {
4033 let name = self.pattern_to_binding_name(pattern);
4034 let ty_str = ty
4035 .as_ref()
4036 .map(|t| format!(": {}", self.type_to_ts(t)))
4037 .unwrap_or_default();
4038 if let Some(def) = default {
4039 let mut ctx = TsEmitCtx::new();
4040 ctx.indent = self.indent;
4041 ctx.enum_variants = self.enum_variants.clone();
4042 if ctx.emit_expr_to_string(def).is_ok() {
4043 let (def_str, _) = ctx.finish();
4044 return Some(format!("{name}{ty_str} = {def_str}"));
4045 }
4046 }
4047 Some(format!("{name}{ty_str}"))
4048 } else {
4049 None
4050 }
4051 })
4052 .collect()
4053 }
4054
4055 fn collect_impl_typed_params(&self, params: &[AIRNode], target_name: &str) -> Vec<String> {
4066 params
4067 .iter()
4068 .filter_map(|p| {
4069 let NodeKind::Param {
4070 pattern,
4071 ty,
4072 default,
4073 } = &p.kind
4074 else {
4075 return None;
4076 };
4077 let name = self.pattern_to_binding_name(pattern);
4078 let ty_str = match ty {
4079 Some(t) => format!(": {}", self.type_to_ts(t)),
4080 None if name == "self" => format!(": {target_name}"),
4081 None => String::new(),
4082 };
4083 if let Some(def) = default {
4084 let mut ctx = TsEmitCtx::new();
4085 ctx.indent = self.indent;
4086 ctx.enum_variants = self.enum_variants.clone();
4087 if ctx.emit_expr_to_string(def).is_ok() {
4088 let (def_str, _) = ctx.finish();
4089 return Some(format!("{name}{ty_str} = {def_str}"));
4090 }
4091 }
4092 Some(format!("{name}{ty_str}"))
4093 })
4094 .collect()
4095 }
4096
4097 fn collect_trait_typed_params(&self, params: &[AIRNode], trait_self_ty: &str) -> Vec<String> {
4112 params
4113 .iter()
4114 .filter_map(|p| {
4115 let NodeKind::Param {
4116 pattern,
4117 ty,
4118 default,
4119 } = &p.kind
4120 else {
4121 return None;
4122 };
4123 let name = self.pattern_to_binding_name(pattern);
4124 let ty_str = match ty {
4125 Some(t) => format!(": {}", self.type_to_ts(t)),
4126 None if name == "self" => format!(": {trait_self_ty}"),
4127 None => String::new(),
4128 };
4129 if let Some(def) = default {
4130 let mut ctx = TsEmitCtx::new();
4131 ctx.indent = self.indent;
4132 ctx.enum_variants = self.enum_variants.clone();
4133 if ctx.emit_expr_to_string(def).is_ok() {
4134 let (def_str, _) = ctx.finish();
4135 return Some(format!("{name}{ty_str} = {def_str}"));
4136 }
4137 }
4138 Some(format!("{name}{ty_str}"))
4139 })
4140 .collect()
4141 }
4142
4143 fn emit_expr_to_string(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
4144 self.emit_expr(node)
4145 }
4146
4147 fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
4149 let mut result = Vec::new();
4150 for tp in effects {
4151 let name = tp
4152 .segments
4153 .last()
4154 .map_or("effect".to_string(), |s| s.name.clone());
4155 if let Some(components) = self.composite_effects.get(&name) {
4156 result.extend(components.iter().cloned());
4157 } else {
4158 result.push(name);
4159 }
4160 }
4161 result
4162 }
4163
4164 fn clock_handler_var(&self) -> Option<&str> {
4171 self.current_handler_vars.get("Clock").map(String::as_str)
4172 }
4173
4174 fn effects_param(&self, effects: &[bock_ast::TypePath]) -> Option<String> {
4176 if effects.is_empty() {
4177 return None;
4178 }
4179 let expanded = self.expand_effect_names(effects);
4180 if expanded.is_empty() {
4181 return None;
4182 }
4183 let names: Vec<String> = expanded.iter().map(|n| to_camel_case(n)).collect();
4184 let type_entries: Vec<String> = expanded
4185 .iter()
4186 .zip(names.iter())
4187 .map(|(orig, camel)| format!("{camel}: {orig}"))
4188 .collect();
4189 Some(format!(
4190 "{{ {} }}: {{ {} }}",
4191 names.join(", "),
4192 type_entries.join(", ")
4193 ))
4194 }
4195
4196 fn build_effects_call_arg_ts(&self, fn_name: &str) -> Option<String> {
4198 let effects = self.fn_effects.get(fn_name)?;
4199 let entries: Vec<String> = effects
4200 .iter()
4201 .filter_map(|e| {
4202 let handler_var = self.current_handler_vars.get(e)?;
4203 let param_name = to_camel_case(e);
4204 Some(format!("{param_name}: {handler_var}"))
4205 })
4206 .collect();
4207 if entries.is_empty() {
4208 return None;
4209 }
4210 Some(format!("{{ {} }}", entries.join(", ")))
4211 }
4212
4213 fn emit_enum_variant(
4216 &mut self,
4217 enum_name: &str,
4218 generic_params: &[bock_ast::GenericParam],
4219 variant: &AIRNode,
4220 ) -> Result<(), CodegenError> {
4221 if let NodeKind::EnumVariant { name, payload } = &variant.kind {
4222 let vname = &name.name;
4223 let generics = self.generic_params_to_ts(generic_params);
4224 let type_args = self.generic_args_to_ts(generic_params);
4230 let unit_generics = if generic_params.is_empty() {
4237 String::new()
4238 } else {
4239 let items: Vec<String> = generic_params
4240 .iter()
4241 .map(|p| format!("{} = unknown", p.name.name))
4242 .collect();
4243 format!("<{}>", items.join(", "))
4244 };
4245 let qualified = format!("{enum_name}_{vname}");
4246
4247 match payload {
4248 EnumVariantPayload::Unit => {
4249 self.writeln(&format!(
4251 "interface {qualified}{unit_generics} {{ readonly _tag: \"{vname}\"; }}"
4252 ));
4253 self.writeln(&format!(
4254 "const {qualified}: {qualified} = Object.freeze({{ _tag: \"{vname}\" as const }});"
4255 ));
4256 }
4257 EnumVariantPayload::Struct(fields) => {
4258 self.writeln(&format!("interface {qualified}{generics} {{"));
4260 self.indent += 1;
4261 self.writeln(&format!("readonly _tag: \"{vname}\";"));
4262 for f in fields {
4263 let ty = self.ast_type_to_ts(&f.ty);
4264 self.writeln(&format!("readonly {}: {};", f.name.name, ty));
4265 }
4266 self.indent -= 1;
4267 self.writeln("}");
4268 let field_params: Vec<String> = fields
4269 .iter()
4270 .map(|f| format!("{}: {}", f.name.name, self.ast_type_to_ts(&f.ty)))
4271 .collect();
4272 let field_names: Vec<&str> =
4273 fields.iter().map(|f| f.name.name.as_str()).collect();
4274 self.writeln(&format!(
4275 "function {qualified}{generics}({}): {qualified}{type_args} {{",
4276 field_params.join(", "),
4277 ));
4278 self.indent += 1;
4279 self.writeln(&format!(
4280 "return {{ _tag: \"{vname}\" as const, {} }};",
4281 field_names.join(", "),
4282 ));
4283 self.indent -= 1;
4284 self.writeln("}");
4285 }
4286 EnumVariantPayload::Tuple(elems) => {
4287 self.writeln(&format!("interface {qualified}{generics} {{"));
4289 self.indent += 1;
4290 self.writeln(&format!("readonly _tag: \"{vname}\";"));
4291 for (i, elem) in elems.iter().enumerate() {
4292 let ty = self.type_to_ts(elem);
4293 self.writeln(&format!("readonly _{i}: {ty};"));
4294 }
4295 self.indent -= 1;
4296 self.writeln("}");
4297 let param_decls: Vec<String> = elems
4298 .iter()
4299 .enumerate()
4300 .map(|(i, e)| format!("_{i}: {}", self.type_to_ts(e)))
4301 .collect();
4302 let param_names: Vec<String> =
4303 (0..elems.len()).map(|i| format!("_{i}")).collect();
4304 self.writeln(&format!(
4305 "function {qualified}{generics}({}): {qualified}{type_args} {{",
4306 param_decls.join(", "),
4307 ));
4308 self.indent += 1;
4309 self.writeln(&format!(
4310 "return {{ _tag: \"{vname}\" as const, {} }};",
4311 param_names
4312 .iter()
4313 .enumerate()
4314 .map(|(i, p)| format!("_{i}: {p}"))
4315 .collect::<Vec<_>>()
4316 .join(", ")
4317 ));
4318 self.indent -= 1;
4319 self.writeln("}");
4320 }
4321 }
4322 }
4323 Ok(())
4324 }
4325
4326 fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
4329 self.mark_span(node.span);
4330 match &node.kind {
4331 NodeKind::LetBinding {
4332 is_mut,
4333 pattern,
4334 ty,
4335 value,
4336 ..
4337 } => {
4338 let kw = if *is_mut { "let" } else { "const" };
4339 let binding = self.pattern_to_ts_destructure(pattern);
4340 let ty_ts = ty.as_ref().map(|t| self.type_to_ts(t));
4341 let ty_str = ty_ts.as_ref().map(|t| format!(": {t}")).unwrap_or_default();
4342 if node.metadata.contains_key(crate::generator::DECL_ONLY_META) {
4346 let ind = self.indent_str();
4347 let _ = writeln!(self.buf, "{ind}let {binding}{ty_str};");
4348 return Ok(());
4349 }
4350 if let NodeKind::Propagate { expr } = &value.kind {
4355 if let NodeKind::BindPat { name, .. } = &pattern.kind {
4356 let ts_name = ts_value_ident(&name.name);
4357 let access = self.emit_propagate(expr)?;
4358 let kw = if *is_mut || self.simple_let_needs_let(&ts_name) {
4359 "let"
4360 } else {
4361 "const"
4362 };
4363 if self.simple_let_redeclared(&ts_name) {
4364 let ind = self.indent_str();
4365 let _ = writeln!(self.buf, "{ind}{ts_name} = {access};");
4366 } else {
4367 self.mark_simple_let_declared(&ts_name);
4368 let ind = self.indent_str();
4369 let _ = writeln!(self.buf, "{ind}{kw} {ts_name}{ty_str} = {access};");
4370 }
4371 return Ok(());
4372 }
4373 let access = self.emit_propagate(expr)?;
4376 let ind = self.indent_str();
4377 let _ = writeln!(self.buf, "{ind}{kw} {binding}{ty_str} = {access};");
4378 return Ok(());
4379 }
4380 if let NodeKind::BindPat { name, .. } = &pattern.kind {
4389 if !matches!(value.kind, NodeKind::Propagate { .. }) {
4390 let ts_name = ts_value_ident(&name.name);
4391 if self.simple_let_redeclared(&ts_name) {
4392 let ind = self.indent_str();
4393 let _ = write!(self.buf, "{ind}{ts_name} = ");
4394 self.emit_let_value(value, ty_ts)?;
4395 self.buf.push_str(";\n");
4396 return Ok(());
4397 }
4398 if !self.value_needs_stmt_form(value) {
4399 let needs_let = *is_mut || self.simple_let_needs_let(&ts_name);
4400 let kw = if needs_let { "let" } else { "const" };
4401 self.mark_simple_let_declared(&ts_name);
4402 let ind = self.indent_str();
4403 let _ = write!(self.buf, "{ind}{kw} {ts_name}{ty_str} = ");
4404 self.emit_let_value(value, ty_ts)?;
4405 self.buf.push_str(";\n");
4406 return Ok(());
4407 }
4408 }
4409 }
4410 if self.value_needs_stmt_form(value) {
4416 let ind = self.indent_str();
4417 let _ = writeln!(self.buf, "{ind}let {binding}{ty_str};");
4421 return self.emit_value_in_stmt_pos(value, &ValueSink::Assign(binding));
4422 }
4423 let ind = self.indent_str();
4424 let _ = write!(self.buf, "{ind}{kw} {binding}{ty_str} = ");
4425 self.emit_let_value(value, ty_ts)?;
4433 self.buf.push_str(";\n");
4434 Ok(())
4435 }
4436 NodeKind::If {
4437 let_pattern,
4438 condition,
4439 then_block,
4440 else_block,
4441 } => {
4442 if let Some(pat) = let_pattern {
4443 let ind = self.indent_str();
4444 let _ = write!(self.buf, "{ind}if (");
4445 self.emit_expr(condition)?;
4446 self.buf.push_str(" != null) {\n");
4447 self.indent += 1;
4448 let binding = self.pattern_to_ts_destructure(pat);
4449 self.writeln(&format!("const {binding} = "));
4450 self.emit_block_body(then_block)?;
4451 self.indent -= 1;
4452 } else {
4453 let ind = self.indent_str();
4454 let _ = write!(self.buf, "{ind}if (");
4455 self.emit_expr(condition)?;
4456 self.buf.push_str(") {\n");
4457 self.indent += 1;
4458 self.emit_block_body(then_block)?;
4459 self.indent -= 1;
4460 }
4461 if let Some(else_b) = else_block {
4462 if matches!(else_b.kind, NodeKind::If { .. }) {
4463 let ind = self.indent_str();
4464 let _ = write!(self.buf, "{ind}}} else ");
4465 self.emit_stmt(else_b)?;
4466 return Ok(());
4467 }
4468 self.writeln("} else {");
4469 self.indent += 1;
4470 self.emit_block_body(else_b)?;
4471 self.indent -= 1;
4472 }
4473 self.writeln("}");
4474 Ok(())
4475 }
4476 NodeKind::For {
4477 pattern,
4478 iterable,
4479 body,
4480 } => {
4481 let binding = self.pattern_to_ts_destructure(pattern);
4482 self.emit_loop_label_prefix(body);
4483 let ind = self.indent_str();
4484 let _ = write!(self.buf, "{ind}for (const {binding} of ");
4485 self.emit_expr(iterable)?;
4486 self.buf.push_str(") {\n");
4487 self.indent += 1;
4488 self.emit_loop_body(body)?;
4489 self.indent -= 1;
4490 self.writeln("}");
4491 self.pop_loop_frame();
4492 Ok(())
4493 }
4494 NodeKind::While { condition, body } => {
4495 self.emit_loop_label_prefix(body);
4496 let ind = self.indent_str();
4497 let _ = write!(self.buf, "{ind}while (");
4498 self.emit_expr(condition)?;
4499 self.buf.push_str(") {\n");
4500 self.indent += 1;
4501 self.emit_loop_body(body)?;
4502 self.indent -= 1;
4503 self.writeln("}");
4504 self.pop_loop_frame();
4505 Ok(())
4506 }
4507 NodeKind::Loop { body } => {
4508 self.emit_loop_label_prefix(body);
4509 self.writeln("while (true) {");
4510 self.indent += 1;
4511 self.emit_loop_body(body)?;
4512 self.indent -= 1;
4513 self.writeln("}");
4514 self.pop_loop_frame();
4515 Ok(())
4516 }
4517 NodeKind::Return { value } => {
4518 if let Some(val) = value {
4519 let ind = self.indent_str();
4520 let _ = write!(self.buf, "{ind}return ");
4521 self.emit_expr(val)?;
4522 self.buf.push_str(";\n");
4523 } else {
4524 self.writeln("return;");
4525 }
4526 Ok(())
4527 }
4528 NodeKind::Break { value } => {
4529 if let Some(val) = value {
4530 if let Some(sink) = self.innermost_loop_value_sink() {
4536 self.emit_sink_value(val, &sink)?;
4537 } else {
4538 let ind = self.indent_str();
4539 let _ = write!(self.buf, "{ind}/* break value: ");
4540 self.emit_expr(val)?;
4541 self.buf.push_str(" */\n");
4542 }
4543 }
4544 if self.switch_label_depth > 0 {
4545 if let Some(label) = self.innermost_loop_label() {
4546 self.writeln(&format!("break {label};"));
4547 return Ok(());
4548 }
4549 }
4550 self.writeln("break;");
4551 Ok(())
4552 }
4553 NodeKind::Continue => {
4554 if self.switch_label_depth > 0 {
4555 if let Some(label) = self.innermost_loop_label() {
4556 self.writeln(&format!("continue {label};"));
4557 return Ok(());
4558 }
4559 }
4560 self.writeln("continue;");
4561 Ok(())
4562 }
4563 NodeKind::Guard {
4564 let_pattern,
4565 condition,
4566 else_block,
4567 } => {
4568 if let Some(pat) = let_pattern {
4569 self.match_temp_counter += 1;
4575 let tmp = format!("__guard{}", self.match_temp_counter);
4576 let ind = self.indent_str();
4577 let _ = write!(self.buf, "{ind}const {tmp} = ");
4578 self.emit_expr(condition)?;
4579 self.buf.push_str(";\n");
4580 let test = self.pattern_test_ts(pat, &tmp);
4581 if !test.is_empty() {
4583 let ind = self.indent_str();
4584 let _ = writeln!(self.buf, "{ind}if (!({test})) {{");
4585 self.indent += 1;
4586 self.emit_block_body(else_block)?;
4587 self.indent -= 1;
4588 self.writeln("}");
4589 }
4590 self.pattern_binds_ts(pat, &tmp)?;
4593 } else {
4594 let ind = self.indent_str();
4595 let _ = write!(self.buf, "{ind}if (!(");
4596 self.emit_expr(condition)?;
4597 self.buf.push_str(")) {\n");
4598 self.indent += 1;
4599 self.emit_block_body(else_block)?;
4600 self.indent -= 1;
4601 self.writeln("}");
4602 }
4603 Ok(())
4604 }
4605 NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms, false),
4606 NodeKind::Block { stmts, tail } => {
4607 self.writeln("{");
4611 self.indent += 1;
4612 self.enter_let_scope(node);
4613 for s in stmts {
4614 self.emit_node(s)?;
4615 }
4616 if let Some(t) = tail {
4617 self.write_indent();
4618 self.emit_expr(t)?;
4619 self.buf.push_str(";\n");
4620 }
4621 self.leave_let_scope();
4622 self.indent -= 1;
4623 self.writeln("}");
4624 Ok(())
4625 }
4626 NodeKind::HandlingBlock { handlers, body } => {
4627 self.writeln("{");
4638 self.indent += 1;
4639 self.enter_let_scope(body);
4640 let old_handler_vars = self.current_handler_vars.clone();
4641 for h in handlers {
4642 let effect_name = h
4643 .effect
4644 .segments
4645 .last()
4646 .map_or("effect", |s| s.name.as_str());
4647 let var_name = format!("__{}", to_camel_case(effect_name));
4648 let type_name = effect_name;
4649 let ind = self.indent_str();
4650 let _ = write!(self.buf, "{ind}const {var_name}: {type_name} = ");
4651 self.emit_expr(&h.handler)?;
4652 self.buf.push_str(";\n");
4653 self.current_handler_vars
4654 .insert(effect_name.to_string(), var_name);
4655 }
4656 if let NodeKind::Block { stmts, tail } = &body.kind {
4657 for s in stmts {
4658 self.emit_node(s)?;
4659 }
4660 if let Some(t) = tail {
4661 self.write_indent();
4662 self.emit_expr(t)?;
4663 self.buf.push_str(";\n");
4664 }
4665 } else {
4666 self.emit_stmt(body)?;
4667 }
4668 self.current_handler_vars = old_handler_vars;
4669 self.leave_let_scope();
4670 self.indent -= 1;
4671 self.writeln("}");
4672 Ok(())
4673 }
4674 NodeKind::Assign { op, target, value } => {
4675 let ind = self.indent_str();
4676 let _ = write!(self.buf, "{ind}");
4677 self.emit_expr(target)?;
4678 let op_str = match op {
4679 AssignOp::Assign => "=",
4680 AssignOp::AddAssign => "+=",
4681 AssignOp::SubAssign => "-=",
4682 AssignOp::MulAssign => "*=",
4683 AssignOp::DivAssign => "/=",
4684 AssignOp::RemAssign => "%=",
4685 };
4686 let _ = write!(self.buf, " {op_str} ");
4687 self.emit_expr(value)?;
4688 self.buf.push_str(";\n");
4689 Ok(())
4690 }
4691 _ => {
4692 self.write_indent();
4693 self.emit_expr(node)?;
4694 self.buf.push_str(";\n");
4695 Ok(())
4696 }
4697 }
4698 }
4699
4700 fn emit_propagate(&mut self, inner: &AIRNode) -> Result<String, CodegenError> {
4713 self.match_temp_counter += 1;
4714 let tmp = format!("__prop{}", self.match_temp_counter);
4715 let ind = self.indent_str();
4716 let _ = write!(self.buf, "{ind}const {tmp} = ");
4717 self.emit_expr(inner)?;
4718 self.buf.push_str(";\n");
4719 let test = match self.current_fn_propagate_tag {
4725 Some(tag) => format!("{tmp}._tag === \"{tag}\""),
4726 None => {
4732 format!("({tmp}._tag as string) === \"Err\" || ({tmp}._tag as string) === \"None\"")
4733 }
4734 };
4735 let ind = self.indent_str();
4736 let _ = writeln!(self.buf, "{ind}if ({test}) {{");
4737 self.indent += 1;
4738 let ind = self.indent_str();
4739 let _ = writeln!(self.buf, "{ind}return {tmp} as never;");
4743 self.indent -= 1;
4744 self.writeln("}");
4745 Ok(format!("{tmp}._0"))
4746 }
4747
4748 fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
4751 self.mark_span(node.span);
4752 match &node.kind {
4753 NodeKind::Literal { lit } => {
4754 match lit {
4755 Literal::Int(s) => self.buf.push_str(s),
4756 Literal::Float(s) => self.buf.push_str(s),
4757 Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
4758 Literal::Char(s) => {
4759 self.buf.push('\'');
4760 self.buf.push_str(s);
4761 self.buf.push('\'');
4762 }
4763 Literal::String(s) => {
4764 self.buf.push('"');
4765 self.buf.push_str(&escape_js_string(s));
4766 self.buf.push('"');
4767 }
4768 Literal::Unit => self.buf.push_str("undefined"),
4769 }
4770 Ok(())
4771 }
4772 NodeKind::Identifier { name } => {
4773 if name.name == "None" {
4774 self.buf.push_str("{ _tag: \"None\" as const }");
4775 } else if let Some(variant) = crate::generator::ordering_variant(&name.name) {
4776 let _ = write!(self.buf, "{{ _tag: \"{variant}\" as const }}");
4780 } else if let Some(enum_name) = self
4781 .user_variant_for_name(&name.name)
4782 .map(|i| i.enum_name.clone())
4783 {
4784 let _ = write!(self.buf, "{enum_name}_{}", name.name);
4787 } else if self.const_names.contains(&name.name) {
4788 self.buf.push_str(&name.name);
4792 } else {
4793 self.buf.push_str(&ts_value_ident(&name.name));
4794 }
4795 Ok(())
4796 }
4797 NodeKind::BinaryOp { op, left, right } => {
4798 if matches!(op, BinOp::Add) && crate::generator::is_list_concat(node, left, right) {
4803 self.buf.push_str("[...");
4804 self.emit_expr(left)?;
4805 self.buf.push_str(", ...");
4806 self.emit_expr(right)?;
4807 self.buf.push(']');
4808 return Ok(());
4809 }
4810 if matches!(op, BinOp::Div | BinOp::Rem) && crate::generator::is_int_arith(node) {
4818 let body = if matches!(op, BinOp::Div) {
4819 "Math.trunc(__a / __b)"
4820 } else {
4821 "__a % __b"
4822 };
4823 self.buf.push_str("((__a: number, __b: number): number => { if (__b === 0) { throw new Error(\"integer division or modulo by zero\"); } return ");
4824 self.buf.push_str(body);
4825 self.buf.push_str("; })(");
4826 self.emit_expr(left)?;
4827 self.buf.push_str(", ");
4828 self.emit_expr(right)?;
4829 self.buf.push(')');
4830 return Ok(());
4831 }
4832 if crate::generator::is_user_compare(node) {
4839 if let Some((tag, is_eq)) = crate::generator::user_compare_variant(*op) {
4840 let recv = self.expr_to_string(left)?;
4841 let other = self.expr_to_string(right)?;
4842 let eq = if is_eq { "===" } else { "!==" };
4843 let _ = write!(
4844 self.buf,
4845 "(({recv}).compare({recv}, {other})._tag {eq} \"{tag}\")"
4846 );
4847 return Ok(());
4848 }
4849 }
4850 if matches!(op, BinOp::Eq | BinOp::Ne) {
4858 if let Some(kind) = crate::generator::user_eq_kind(node) {
4859 let recv = self.expr_to_string(left)?;
4860 let other = self.expr_to_string(right)?;
4861 let neg = if *op == BinOp::Ne { "!" } else { "" };
4862 if kind == "impl" {
4863 let _ = write!(self.buf, "{neg}(({recv}).eq({recv}, {other}))");
4864 } else {
4865 let _ = write!(self.buf, "{neg}__bockEq({recv}, {other})");
4866 }
4867 return Ok(());
4868 }
4869 }
4870 self.buf.push('(');
4871 self.emit_expr(left)?;
4872 let op_str = match op {
4873 BinOp::Add => " + ",
4874 BinOp::Sub => " - ",
4875 BinOp::Mul => " * ",
4876 BinOp::Div => " / ",
4877 BinOp::Rem => " % ",
4878 BinOp::Pow => " ** ",
4879 BinOp::Eq => " === ",
4880 BinOp::Ne => " !== ",
4881 BinOp::Lt => " < ",
4882 BinOp::Le => " <= ",
4883 BinOp::Gt => " > ",
4884 BinOp::Ge => " >= ",
4885 BinOp::And => " && ",
4886 BinOp::Or => " || ",
4887 BinOp::BitAnd => " & ",
4888 BinOp::BitOr => " | ",
4889 BinOp::BitXor => " ^ ",
4890 BinOp::Compose => " /* >> */ ",
4891 BinOp::Is => " instanceof ",
4892 };
4893 self.buf.push_str(op_str);
4894 self.emit_expr(right)?;
4895 self.buf.push(')');
4896 Ok(())
4897 }
4898 NodeKind::UnaryOp { op, operand } => {
4899 let op_str = match op {
4900 UnaryOp::Neg => "-",
4901 UnaryOp::Not => "!",
4902 UnaryOp::BitNot => "~",
4903 };
4904 self.buf.push_str(op_str);
4905 self.emit_expr(operand)?;
4906 Ok(())
4907 }
4908 NodeKind::Call { callee, args, .. } => {
4909 if let Some(code) = self.map_prelude_call(callee, args)? {
4910 self.buf.push_str(&code);
4911 return Ok(());
4912 }
4913 if self.try_emit_prelude_ctor(callee, args)? {
4914 return Ok(());
4915 }
4916 if self.try_emit_time_assoc_call(callee, args)? {
4917 return Ok(());
4918 }
4919 if self.try_emit_time_desugared_method(node, callee, args)? {
4920 return Ok(());
4921 }
4922 if self.try_emit_concurrency_call(callee, args)? {
4923 return Ok(());
4924 }
4925 if self.try_emit_map_method(node, callee, args)? {
4928 return Ok(());
4929 }
4930 if self.try_emit_set_method(node, callee, args)? {
4931 return Ok(());
4932 }
4933 if self.try_emit_string_method(node, callee, args)? {
4937 return Ok(());
4938 }
4939 if self.try_emit_numeric_method(node, callee, args)? {
4943 return Ok(());
4944 }
4945 if self.try_emit_list_mutating_method(node, callee, args)? {
4946 return Ok(());
4947 }
4948 if self.try_emit_list_inplace_mutator(node, callee, args)? {
4949 return Ok(());
4950 }
4951 if self.try_emit_list_method(node, callee, args)? {
4952 return Ok(());
4953 }
4954 if self.try_emit_list_functional_method(node, callee, args)? {
4955 return Ok(());
4956 }
4957 if self.try_emit_primitive_bridge(node, callee, args)? {
4958 return Ok(());
4959 }
4960 if self.try_emit_trait_bound_bridge(node, callee, args)? {
4961 return Ok(());
4962 }
4963 if self.try_emit_container_method(node, callee, args)? {
4964 return Ok(());
4965 }
4966 if let NodeKind::Identifier { name } = &callee.kind {
4968 if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
4969 if let Some(handler_var) =
4970 self.current_handler_vars.get(&effect_name).cloned()
4971 {
4972 let _ = write!(self.buf, "{}.{}", handler_var, name.name);
4973 self.buf.push('(');
4974 for (i, arg) in args.iter().enumerate() {
4975 if i > 0 {
4976 self.buf.push_str(", ");
4977 }
4978 self.emit_expr(&arg.value)?;
4979 }
4980 self.buf.push(')');
4981 return Ok(());
4982 }
4983 }
4984 }
4985 if self.try_emit_primitive_conversion(node, callee, args)? {
4990 return Ok(());
4991 }
4992 if crate::generator::is_associated_call(node) {
4999 if let NodeKind::FieldAccess { object, field } = &callee.kind {
5000 if let NodeKind::Identifier { name: type_name } = &object.kind {
5001 let _ = write!(
5002 self.buf,
5003 "{}.{}",
5004 type_name.name,
5005 self.ts_method_name(&field.name)
5006 );
5007 self.buf.push('(');
5008 for (i, arg) in args.iter().enumerate() {
5009 if i > 0 {
5010 self.buf.push_str(", ");
5011 }
5012 self.emit_expr(&arg.value)?;
5013 }
5014 self.buf.push(')');
5015 return Ok(());
5016 }
5017 }
5018 }
5019 if let NodeKind::FieldAccess { object, field } = &callee.kind {
5030 if crate::generator::desugared_self_call(callee, args).is_some() {
5031 let renamed = self.ts_method_name(&field.name);
5036 if renamed != field.name {
5037 self.emit_expr(object)?;
5038 let _ = write!(self.buf, ".{renamed}");
5039 self.buf.push('(');
5040 for (i, arg) in args.iter().enumerate() {
5041 if i > 0 {
5042 self.buf.push_str(", ");
5043 }
5044 self.emit_expr(&arg.value)?;
5045 }
5046 self.buf.push(')');
5047 return Ok(());
5048 }
5049 }
5050 }
5051 let effects_arg = if let NodeKind::Identifier { name } = &callee.kind {
5053 self.build_effects_call_arg_ts(&name.name)
5054 } else {
5055 None
5056 };
5057 self.emit_callee(callee)?;
5058 self.buf.push('(');
5059 for (i, arg) in args.iter().enumerate() {
5060 if i > 0 {
5061 self.buf.push_str(", ");
5062 }
5063 self.emit_expr(&arg.value)?;
5064 }
5065 if let Some(ea) = effects_arg {
5066 if !args.is_empty() {
5067 self.buf.push_str(", ");
5068 }
5069 self.buf.push_str(&ea);
5070 }
5071 self.buf.push(')');
5072 Ok(())
5073 }
5074 NodeKind::MethodCall {
5075 receiver,
5076 method,
5077 args,
5078 ..
5079 } => {
5080 if self.try_emit_time_method(receiver, &method.name, args)? {
5081 return Ok(());
5082 }
5083 self.emit_expr(receiver)?;
5084 let _ = write!(
5085 self.buf,
5086 ".{}",
5087 self.ts_method_name(&to_camel_case(&method.name))
5088 );
5089 self.buf.push('(');
5090 for (i, arg) in args.iter().enumerate() {
5091 if i > 0 {
5092 self.buf.push_str(", ");
5093 }
5094 self.emit_expr(&arg.value)?;
5095 }
5096 self.buf.push(')');
5097 Ok(())
5098 }
5099 NodeKind::FieldAccess { object, field } => {
5100 self.emit_expr(object)?;
5101 let _ = write!(self.buf, ".{}", field.name);
5102 Ok(())
5103 }
5104 NodeKind::Index { object, index } => {
5105 self.emit_expr(object)?;
5106 self.buf.push('[');
5107 self.emit_expr(index)?;
5108 self.buf.push(']');
5109 Ok(())
5110 }
5111 NodeKind::Lambda { params, body } => {
5112 let param_list = self.collect_lambda_params(params);
5113 let _ = write!(self.buf, "({}) => ", param_list.join(", "));
5114 if matches!(body.kind, NodeKind::Block { .. }) {
5115 self.buf.push_str("{\n");
5116 self.indent += 1;
5117 let prev_sink = self.value_sink.take();
5122 let r = self.emit_block_body(body);
5123 self.value_sink = prev_sink;
5124 r?;
5125 self.indent -= 1;
5126 self.write_indent();
5127 self.buf.push('}');
5128 } else {
5129 self.emit_expr(body)?;
5130 }
5131 Ok(())
5132 }
5133 NodeKind::Pipe { left, right } => self.emit_pipe(left, right),
5134 NodeKind::Compose { left, right } => {
5135 let _ = write!(self.buf, "((x: any) => ");
5144 self.emit_callee(right)?;
5145 self.buf.push('(');
5146 self.emit_callee(left)?;
5147 self.buf.push_str("(x)))");
5148 Ok(())
5149 }
5150 NodeKind::Await { expr } => {
5151 self.buf.push_str("(await ");
5152 self.emit_expr(expr)?;
5153 self.buf.push(')');
5154 Ok(())
5155 }
5156 NodeKind::Propagate { expr } => {
5157 self.buf.push('(');
5166 self.emit_expr(expr)?;
5167 self.buf.push_str(")._0");
5168 Ok(())
5169 }
5170 NodeKind::Range { lo, hi, inclusive } => {
5171 if *inclusive {
5172 self.buf.push_str("rangeInclusive(");
5173 } else {
5174 self.buf.push_str("range(");
5175 }
5176 self.emit_expr(lo)?;
5177 self.buf.push_str(", ");
5178 self.emit_expr(hi)?;
5179 self.buf.push(')');
5180 Ok(())
5181 }
5182 NodeKind::RecordConstruct {
5183 path,
5184 fields,
5185 spread,
5186 } => {
5187 let struct_variant = if spread.is_none() {
5191 self.user_variant_for_path(path).and_then(|info| {
5192 if let crate::generator::VariantPayloadKind::Struct(field_order) =
5193 &info.payload
5194 {
5195 Some((info.enum_name.clone(), field_order.clone()))
5196 } else {
5197 None
5198 }
5199 })
5200 } else {
5201 None
5202 };
5203 if let Some((enum_name, field_order)) = struct_variant {
5204 let variant = path.segments.last().map_or("", |s| s.name.as_str());
5205 let _ = write!(self.buf, "{enum_name}_{variant}(");
5206 for (i, fname) in field_order.iter().enumerate() {
5207 if i > 0 {
5208 self.buf.push_str(", ");
5209 }
5210 let supplied = fields.iter().find(|f| &f.name.name == fname);
5211 match supplied.and_then(|f| f.value.as_ref()) {
5212 Some(val) => self.emit_expr(val)?,
5213 None => self.buf.push_str(&ts_value_ident(fname)),
5214 }
5215 }
5216 self.buf.push(')');
5217 return Ok(());
5218 }
5219 let type_name = path.segments.last().map(|s| s.name.as_str()).unwrap_or("");
5220 if let Some(field_order) = self.class_fields.get(type_name).cloned() {
5229 let _ = write!(self.buf, "new {type_name}(");
5230 for (i, fname) in field_order.iter().enumerate() {
5231 if i > 0 {
5232 self.buf.push_str(", ");
5233 }
5234 let supplied = fields.iter().find(|f| &f.name.name == fname);
5235 match supplied.and_then(|f| f.value.as_ref()) {
5236 Some(val) => self.emit_expr(val)?,
5237 None if supplied.is_some() => {
5242 self.buf.push_str(&ts_value_ident(fname));
5243 }
5244 None => match spread {
5245 Some(sp) => {
5246 self.emit_expr(sp)?;
5247 let _ = write!(self.buf, ".{}", ts_value_ident(fname));
5248 }
5249 None => self.buf.push_str("undefined"),
5250 },
5251 }
5252 }
5253 self.buf.push(')');
5254 return Ok(());
5255 }
5256 let is_class = self.record_names.contains(type_name);
5257 if is_class {
5258 let _ = write!(self.buf, "new {type_name}(");
5259 if fields.is_empty() && spread.is_none() {
5260 self.buf.push(')');
5261 return Ok(());
5262 }
5263 }
5264 if let Some(sp) = spread {
5265 self.buf.push_str("{ ...");
5266 self.emit_expr(sp)?;
5267 if !fields.is_empty() {
5268 self.buf.push_str(", ");
5269 }
5270 } else {
5271 self.buf.push_str("{ ");
5272 }
5273 for (i, f) in fields.iter().enumerate() {
5274 if i > 0 {
5275 self.buf.push_str(", ");
5276 }
5277 if let Some(val) = &f.value {
5278 let _ = write!(self.buf, "{}: ", f.name.name);
5279 self.emit_expr(val)?;
5280 } else {
5281 self.buf.push_str(&f.name.name);
5282 }
5283 }
5284 self.buf.push_str(" }");
5285 if is_class {
5286 self.buf.push(')');
5287 }
5288 Ok(())
5289 }
5290 NodeKind::ListLiteral { elems } => {
5291 self.buf.push('[');
5292 for (i, e) in elems.iter().enumerate() {
5293 if i > 0 {
5294 self.buf.push_str(", ");
5295 }
5296 self.emit_expr(e)?;
5297 }
5298 self.buf.push(']');
5299 Ok(())
5300 }
5301 NodeKind::MapLiteral { entries } => {
5302 self.buf.push_str("new Map([");
5303 for (i, entry) in entries.iter().enumerate() {
5304 if i > 0 {
5305 self.buf.push_str(", ");
5306 }
5307 self.buf.push('[');
5308 self.emit_expr(&entry.key)?;
5309 self.buf.push_str(", ");
5310 self.emit_expr(&entry.value)?;
5311 self.buf.push(']');
5312 }
5313 self.buf.push_str("])");
5314 Ok(())
5315 }
5316 NodeKind::SetLiteral { elems } => {
5317 self.buf.push_str("new Set([");
5318 for (i, e) in elems.iter().enumerate() {
5319 if i > 0 {
5320 self.buf.push_str(", ");
5321 }
5322 self.emit_expr(e)?;
5323 }
5324 self.buf.push_str("])");
5325 Ok(())
5326 }
5327 NodeKind::TupleLiteral { elems } => {
5328 self.buf.push('[');
5330 for (i, e) in elems.iter().enumerate() {
5331 if i > 0 {
5332 self.buf.push_str(", ");
5333 }
5334 self.emit_expr(e)?;
5335 }
5336 self.buf.push(']');
5337 Ok(())
5338 }
5339 NodeKind::Interpolation { parts } => {
5340 self.buf.push('`');
5341 for part in parts {
5342 match part {
5343 AirInterpolationPart::Literal(s) => {
5344 self.buf.push_str(&escape_template_literal(s));
5345 }
5346 AirInterpolationPart::Expr(expr) => {
5347 self.needs_runtime_str = true;
5353 self.buf.push_str("${__bockStr(");
5354 self.emit_expr(expr)?;
5355 self.buf.push_str(")}");
5356 }
5357 }
5358 }
5359 self.buf.push('`');
5360 Ok(())
5361 }
5362 NodeKind::Placeholder => {
5363 self.buf.push('_');
5364 Ok(())
5365 }
5366 NodeKind::Unreachable => {
5367 self.buf
5368 .push_str("(() => { throw new Error(\"unreachable\"); })()");
5369 Ok(())
5370 }
5371 NodeKind::ResultConstruct { variant, value } => {
5372 let tag = match variant {
5377 ResultVariant::Ok => "Ok",
5378 ResultVariant::Err => "Err",
5379 };
5380 let _ = write!(self.buf, "{{ _tag: \"{tag}\" as const, _0: ");
5381 if let Some(v) = value {
5382 self.emit_expr(v)?;
5383 } else {
5384 self.buf.push_str("undefined");
5385 }
5386 self.buf.push_str(" }");
5387 Ok(())
5388 }
5389 NodeKind::Assign { op, target, value } => {
5390 self.emit_expr(target)?;
5391 let op_str = match op {
5392 AssignOp::Assign => " = ",
5393 AssignOp::AddAssign => " += ",
5394 AssignOp::SubAssign => " -= ",
5395 AssignOp::MulAssign => " *= ",
5396 AssignOp::DivAssign => " /= ",
5397 AssignOp::RemAssign => " %= ",
5398 };
5399 self.buf.push_str(op_str);
5400 self.emit_expr(value)?;
5401 Ok(())
5402 }
5403 NodeKind::If {
5404 condition,
5405 then_block,
5406 else_block,
5407 ..
5408 } => {
5409 self.buf.push('(');
5411 self.emit_expr(condition)?;
5412 self.buf.push_str(" ? ");
5413 self.emit_block_as_expr(then_block)?;
5414 self.buf.push_str(" : ");
5415 if let Some(eb) = else_block {
5416 self.emit_block_as_expr(eb)?;
5417 } else {
5418 self.buf.push_str("undefined");
5419 }
5420 self.buf.push(')');
5421 Ok(())
5422 }
5423 NodeKind::Block { stmts, tail } => {
5424 self.buf.push_str("(() => {\n");
5429 self.indent += 1;
5430 self.enter_let_scope(node);
5431 for s in stmts {
5432 self.emit_node(s)?;
5433 }
5434 if let Some(t) = tail {
5435 let ind = self.indent_str();
5436 let _ = write!(self.buf, "{ind}return ");
5437 self.emit_expr(t)?;
5438 self.buf.push_str(";\n");
5439 }
5440 self.leave_let_scope();
5441 self.indent -= 1;
5442 self.write_indent();
5443 self.buf.push_str("})()");
5444 Ok(())
5445 }
5446 NodeKind::Match { scrutinee, arms } => {
5447 let expected = self.current_expected_type.take();
5456 let arrow_ret = expected
5457 .as_deref()
5458 .map(|t| format!(" : {t}"))
5459 .unwrap_or_default();
5460 let force_hoist = expected.is_some();
5461 let _ = write!(self.buf, "((){arrow_ret} => {{");
5462 self.buf.push('\n');
5463 self.indent += 1;
5464 let prev_sink = self.value_sink.replace(ValueSink::Return);
5471 let r = self.emit_match(scrutinee, arms, force_hoist);
5472 self.value_sink = prev_sink;
5473 r?;
5474 self.indent -= 1;
5475 self.write_indent();
5476 self.buf.push_str("})()");
5477 self.current_expected_type = expected;
5478 Ok(())
5479 }
5480 NodeKind::Move { expr }
5482 | NodeKind::Borrow { expr }
5483 | NodeKind::MutableBorrow { expr } => self.emit_expr(expr),
5484 NodeKind::EffectOp {
5486 effect,
5487 operation,
5488 args,
5489 } => {
5490 let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
5491 let _ = write!(
5492 self.buf,
5493 "{}.{}",
5494 to_camel_case(effect_name),
5495 operation.name
5496 );
5497 self.buf.push('(');
5498 for (i, arg) in args.iter().enumerate() {
5499 if i > 0 {
5500 self.buf.push_str(", ");
5501 }
5502 self.emit_expr(&arg.value)?;
5503 }
5504 self.buf.push(')');
5505 Ok(())
5506 }
5507 NodeKind::TypeNamed { .. }
5509 | NodeKind::TypeTuple { .. }
5510 | NodeKind::TypeFunction { .. }
5511 | NodeKind::TypeOptional { .. }
5512 | NodeKind::TypeSelf => {
5513 let ty_str = self.type_to_ts(node);
5514 let _ = write!(self.buf, "/* {ty_str} */");
5515 Ok(())
5516 }
5517 NodeKind::EffectRef { path } => {
5518 let name = path
5519 .segments
5520 .iter()
5521 .map(|s| s.name.as_str())
5522 .collect::<Vec<_>>()
5523 .join(".");
5524 self.buf.push_str(&name);
5525 Ok(())
5526 }
5527 NodeKind::Error => {
5528 self.buf.push_str("/* error */");
5529 Ok(())
5530 }
5531 _ => {
5532 self.buf.push_str("/* unsupported */");
5533 Ok(())
5534 }
5535 }
5536 }
5537
5538 fn emit_match(
5547 &mut self,
5548 scrutinee: &AIRNode,
5549 arms: &[AIRNode],
5550 force_hoist: bool,
5551 ) -> Result<(), CodegenError> {
5552 if crate::generator::match_needs_ifchain(arms) {
5560 return self.emit_match_ifchain(scrutinee, arms);
5561 }
5562
5563 let is_adt = arms.iter().any(|arm| {
5568 let NodeKind::MatchArm { pattern, .. } = &arm.kind else {
5569 return false;
5570 };
5571 match &pattern.kind {
5572 NodeKind::ConstructorPat { .. } => true,
5573 NodeKind::RecordPat { path, .. } => self.user_variant_for_path(path).is_some(),
5574 _ => false,
5575 }
5576 });
5577
5578 let temp = if matches!(scrutinee.kind, NodeKind::Identifier { .. }) && !force_hoist {
5586 None
5587 } else {
5588 self.match_temp_counter += 1;
5589 let name = format!("__match{}", self.match_temp_counter);
5590 let ind = self.indent_str();
5591 let _ = write!(self.buf, "{ind}const {name} = ");
5592 self.emit_expr(scrutinee)?;
5593 self.buf.push_str(";\n");
5594 Some(name)
5595 };
5596
5597 let ind = self.indent_str();
5598 let _ = write!(self.buf, "{ind}switch (");
5599 self.emit_scrutinee_ref(scrutinee, temp.as_deref())?;
5600 if is_adt {
5601 self.buf.push_str("._tag) {\n");
5602 } else {
5603 self.buf.push_str(") {\n");
5604 }
5605 self.indent += 1;
5606 self.switch_label_depth += 1;
5607 for arm in arms {
5608 self.emit_match_arm(arm, is_adt, scrutinee, temp.as_deref())?;
5609 }
5610 self.switch_label_depth -= 1;
5611 self.indent -= 1;
5612 self.writeln("}");
5613 Ok(())
5614 }
5615
5616 fn emit_scrutinee_ref(
5619 &mut self,
5620 scrutinee: &AIRNode,
5621 temp: Option<&str>,
5622 ) -> Result<(), CodegenError> {
5623 match temp {
5624 Some(name) => {
5625 self.buf.push_str(name);
5626 Ok(())
5627 }
5628 None => self.emit_expr(scrutinee),
5629 }
5630 }
5631
5632 fn scrutinee_ref_string(
5637 &mut self,
5638 scrutinee: &AIRNode,
5639 temp: Option<&str>,
5640 ) -> Result<String, CodegenError> {
5641 let saved = std::mem::take(&mut self.buf);
5642 let result = self.emit_scrutinee_ref(scrutinee, temp);
5643 let rendered = std::mem::replace(&mut self.buf, saved);
5644 result.map(|()| rendered)
5645 }
5646
5647 fn narrowing_cast(scrutinee_ref: &str, variant: &str) -> String {
5664 format!("({scrutinee_ref} as Extract<typeof {scrutinee_ref}, {{ _tag: \"{variant}\" }}>)")
5665 }
5666
5667 fn emit_loop_label_prefix(&mut self, body: &AIRNode) {
5672 if crate::generator::loop_needs_break_label(body) {
5673 self.loop_label_counter += 1;
5674 let label = format!("__bockLoop{}", self.loop_label_counter);
5675 self.writeln(&format!("{label}:"));
5676 self.loop_labels.push(Some(label));
5677 } else {
5678 self.loop_labels.push(None);
5679 }
5680 self.loop_value_sinks.push(None);
5681 }
5682
5683 fn pop_loop_frame(&mut self) {
5686 self.loop_labels.pop();
5687 self.loop_value_sinks.pop();
5688 }
5689
5690 fn set_loop_value_sink(&mut self, sink: ValueSink) {
5694 if let Some(top) = self.loop_value_sinks.last_mut() {
5695 *top = Some(sink);
5696 }
5697 }
5698
5699 fn innermost_loop_value_sink(&self) -> Option<ValueSink> {
5701 self.loop_value_sinks.last().and_then(Clone::clone)
5702 }
5703
5704 fn innermost_loop_label(&self) -> Option<&str> {
5706 self.loop_labels.last().and_then(|l| l.as_deref())
5707 }
5708
5709 fn emit_match_arm(
5710 &mut self,
5711 arm: &AIRNode,
5712 is_adt: bool,
5713 scrutinee: &AIRNode,
5714 temp: Option<&str>,
5715 ) -> Result<(), CodegenError> {
5716 if let NodeKind::MatchArm {
5717 pattern,
5718 guard,
5719 body,
5720 } = &arm.kind
5721 {
5722 match &pattern.kind {
5723 NodeKind::WildcardPat => {
5724 self.writeln("default: {");
5725 }
5726 NodeKind::BindPat { name, is_mut } if !is_adt => {
5727 self.writeln("default: {");
5728 self.indent += 1;
5729 let kw = if *is_mut { "let" } else { "const" };
5733 let ind = self.indent_str();
5734 let _ = write!(self.buf, "{ind}{kw} {} = ", name.name);
5735 self.emit_scrutinee_ref(scrutinee, temp)?;
5736 self.buf.push_str(";\n");
5737 self.indent -= 1;
5738 }
5739 NodeKind::LiteralPat { lit } => {
5740 let ind = self.indent_str();
5741 let _ = write!(self.buf, "{ind}case ");
5742 match lit {
5743 Literal::Int(s) => self.buf.push_str(s),
5744 Literal::Float(s) => self.buf.push_str(s),
5745 Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
5746 Literal::Char(s) => {
5747 self.buf.push('\'');
5748 self.buf.push_str(s);
5749 self.buf.push('\'');
5750 }
5751 Literal::String(s) => {
5752 self.buf.push('"');
5753 self.buf.push_str(&escape_js_string(s));
5754 self.buf.push('"');
5755 }
5756 Literal::Unit => self.buf.push_str("undefined"),
5757 }
5758 self.buf.push_str(": {\n");
5759 }
5760 NodeKind::ConstructorPat { path, fields } => {
5761 let variant_name = path.segments.last().map_or("_", |s| s.name.as_str());
5762 self.writeln(&format!("case \"{variant_name}\": {{"));
5763 if !fields.is_empty() {
5764 self.indent += 1;
5765 let scrut_ref = self.scrutinee_ref_string(scrutinee, temp)?;
5769 let cast = Self::narrowing_cast(&scrut_ref, variant_name);
5770 for (i, field) in fields.iter().enumerate() {
5771 let binding = self.pattern_to_binding_name(field);
5772 let ind = self.indent_str();
5773 let _ = writeln!(self.buf, "{ind}const {binding} = {cast}._{i};");
5774 }
5775 self.indent -= 1;
5776 }
5777 }
5778 NodeKind::RecordPat { path, fields, .. } => {
5779 let variant_name = path.segments.last().map_or("_", |s| s.name.as_str());
5780 if is_adt {
5781 self.writeln(&format!("case \"{variant_name}\": {{"));
5782 } else {
5783 self.writeln("default: {");
5784 }
5785 if !fields.is_empty() {
5786 self.indent += 1;
5787 let scrut_ref = self.scrutinee_ref_string(scrutinee, temp)?;
5793 let access = if is_adt {
5794 Self::narrowing_cast(&scrut_ref, variant_name)
5795 } else {
5796 scrut_ref
5797 };
5798 for f in fields {
5799 let field_name = &f.name.name;
5800 let binding = match &f.pattern {
5801 Some(pat) => self.pattern_to_binding_name(pat),
5802 None => field_name.clone(),
5803 };
5804 let ind = self.indent_str();
5805 let _ =
5806 writeln!(self.buf, "{ind}const {binding} = {access}.{field_name};");
5807 }
5808 self.indent -= 1;
5809 }
5810 }
5811 _ => {
5812 self.writeln("default: {");
5813 }
5814 }
5815
5816 self.indent += 1;
5817 if let Some(g) = guard {
5818 let ind = self.indent_str();
5819 let _ = write!(self.buf, "{ind}if (!(");
5820 self.emit_expr(g)?;
5821 self.buf.push_str(")) break;\n");
5822 }
5823 self.emit_block_body(body)?;
5824 self.writeln("break;");
5825 self.indent -= 1;
5826 self.writeln("}");
5827 }
5828 Ok(())
5829 }
5830
5831 fn emit_match_ifchain(
5840 &mut self,
5841 scrutinee: &AIRNode,
5842 arms: &[AIRNode],
5843 ) -> Result<(), CodegenError> {
5844 let root: String = if let NodeKind::Identifier { name } = &scrutinee.kind {
5848 format!("({} as any)", name.name)
5849 } else {
5850 self.match_temp_counter += 1;
5851 let name = format!("__match{}", self.match_temp_counter);
5852 let ind = self.indent_str();
5853 let _ = write!(self.buf, "{ind}const {name} = ");
5854 self.emit_expr(scrutinee)?;
5855 self.buf.push_str(";\n");
5856 format!("({name} as any)")
5857 };
5858
5859 let mut first = true;
5860 let mut closed = false;
5861 let arm_count = arms.len();
5862 for (idx, arm) in arms.iter().enumerate() {
5863 let NodeKind::MatchArm {
5864 pattern,
5865 guard,
5866 body,
5867 } = &arm.kind
5868 else {
5869 continue;
5870 };
5871 let test = self.pattern_test_ts(pattern, &root);
5872 let is_catch_all = matches!(
5873 pattern.kind,
5874 NodeKind::WildcardPat | NodeKind::BindPat { .. }
5875 );
5876 let is_last = idx + 1 == arm_count;
5877 let unconditional = guard.is_none() && (is_catch_all || is_last);
5881 let ind = self.indent_str();
5882 if unconditional {
5883 if first {
5884 let _ = writeln!(self.buf, "{ind}{{");
5885 } else {
5886 let _ = writeln!(self.buf, "{ind}else {{");
5887 }
5888 closed = true;
5889 } else {
5890 let mut cond = if test.is_empty() {
5891 "true".to_string()
5892 } else {
5893 test
5894 };
5895 if let Some(g) = guard {
5896 let g_str = self.expr_to_string(g)?;
5897 let binds = self.pattern_binds_to_string_ts(pattern, &root);
5898 let guard_test = if binds.is_empty() {
5899 format!("({g_str})")
5900 } else {
5901 format!("(() => {{ {binds}return ({g_str}); }})()")
5902 };
5903 if cond == "true" {
5904 cond = guard_test;
5905 } else {
5906 cond = format!("{cond} && {guard_test}");
5907 }
5908 }
5909 if first {
5910 let _ = writeln!(self.buf, "{ind}if ({cond}) {{");
5911 } else {
5912 let _ = writeln!(self.buf, "{ind}else if ({cond}) {{");
5913 }
5914 }
5915 first = false;
5916 self.indent += 1;
5917 self.pattern_binds_ts(pattern, &root)?;
5918 self.emit_block_body(body)?;
5919 self.indent -= 1;
5920 self.writeln("}");
5921 }
5922 if !closed && !first {
5923 self.writeln("else { throw new Error(\"non-exhaustive match\"); }");
5924 }
5925 Ok(())
5926 }
5927
5928 fn pattern_test_ts(&self, pat: &AIRNode, access: &str) -> String {
5931 match &pat.kind {
5932 NodeKind::WildcardPat | NodeKind::BindPat { .. } => String::new(),
5933 NodeKind::LiteralPat { lit } => {
5934 format!("{access} === {}", ts_literal(lit))
5935 }
5936 NodeKind::ConstructorPat { path, fields } => {
5937 let variant = path.segments.last().map_or("_", |s| s.name.as_str());
5938 let mut tests = vec![format!("{access}._tag === \"{variant}\"")];
5939 for (i, field) in fields.iter().enumerate() {
5940 let sub = self.pattern_test_ts(field, &format!("{access}._{i}"));
5941 if !sub.is_empty() {
5942 tests.push(sub);
5943 }
5944 }
5945 tests.join(" && ")
5946 }
5947 NodeKind::RecordPat { path, fields, .. } => {
5948 let variant = path.segments.last().map_or("_", |s| s.name.as_str());
5949 let mut tests = Vec::new();
5950 if self.user_variant_for_path(path).is_some() {
5951 tests.push(format!("{access}._tag === \"{variant}\""));
5952 }
5953 for f in fields {
5954 if let Some(p) = &f.pattern {
5955 let sub = self.pattern_test_ts(p, &format!("{access}.{}", f.name.name));
5956 if !sub.is_empty() {
5957 tests.push(sub);
5958 }
5959 }
5960 }
5961 if tests.is_empty() {
5962 String::new()
5963 } else {
5964 tests.join(" && ")
5965 }
5966 }
5967 NodeKind::TuplePat { elems } => {
5968 let mut tests = vec![format!("Array.isArray({access})")];
5969 for (i, e) in elems.iter().enumerate() {
5970 let sub = self.pattern_test_ts(e, &format!("{access}[{i}]"));
5971 if !sub.is_empty() {
5972 tests.push(sub);
5973 }
5974 }
5975 tests.join(" && ")
5976 }
5977 NodeKind::ListPat { elems, rest } => {
5978 let n = elems.len();
5983 let len_test = if rest.is_some() {
5984 format!("{access}.length >= {n}")
5985 } else {
5986 format!("{access}.length === {n}")
5987 };
5988 let mut tests = vec![format!("Array.isArray({access})"), len_test];
5989 for (i, e) in elems.iter().enumerate() {
5990 let sub = self.pattern_test_ts(e, &format!("{access}[{i}]"));
5991 if !sub.is_empty() {
5992 tests.push(sub);
5993 }
5994 }
5995 tests.join(" && ")
5996 }
5997 NodeKind::RangePat { lo, hi, inclusive } => {
5998 let lo_s = range_bound_to_ts(lo);
6001 let hi_s = range_bound_to_ts(hi);
6002 let upper = if *inclusive { "<=" } else { "<" };
6003 format!("{access} >= {lo_s} && {access} {upper} {hi_s}")
6004 }
6005 NodeKind::OrPat { alternatives } => {
6006 let alts: Vec<String> = alternatives
6007 .iter()
6008 .map(|a| {
6009 let t = self.pattern_test_ts(a, access);
6010 if t.is_empty() {
6011 "true".to_string()
6012 } else {
6013 format!("({t})")
6014 }
6015 })
6016 .collect();
6017 alts.join(" || ")
6018 }
6019 _ => String::new(),
6020 }
6021 }
6022
6023 fn pattern_binds_ts(&mut self, pat: &AIRNode, access: &str) -> Result<(), CodegenError> {
6026 match &pat.kind {
6027 NodeKind::BindPat { name, .. } if !ts_bind_is_self_reference(&name.name, access) => {
6035 let ind = self.indent_str();
6036 let _ = writeln!(
6037 self.buf,
6038 "{ind}const {} = {access};",
6039 ts_value_ident(&name.name)
6040 );
6041 }
6042 NodeKind::ConstructorPat { fields, .. } => {
6043 for (i, field) in fields.iter().enumerate() {
6044 self.pattern_binds_ts(field, &format!("{access}._{i}"))?;
6045 }
6046 }
6047 NodeKind::RecordPat { fields, .. } => {
6048 for f in fields {
6049 let field_access = format!("{access}.{}", f.name.name);
6050 match &f.pattern {
6051 Some(p) => self.pattern_binds_ts(p, &field_access)?,
6052 None => {
6053 let ind = self.indent_str();
6054 let _ =
6055 writeln!(self.buf, "{ind}const {} = {field_access};", f.name.name);
6056 }
6057 }
6058 }
6059 }
6060 NodeKind::TuplePat { elems } => {
6061 for (i, e) in elems.iter().enumerate() {
6062 self.pattern_binds_ts(e, &format!("{access}[{i}]"))?;
6063 }
6064 }
6065 NodeKind::ListPat { elems, rest } => {
6066 for (i, e) in elems.iter().enumerate() {
6067 self.pattern_binds_ts(e, &format!("{access}[{i}]"))?;
6068 }
6069 if let Some(r) = rest {
6072 if let NodeKind::BindPat { name, .. } = &r.kind {
6073 let ind = self.indent_str();
6074 let _ = writeln!(
6075 self.buf,
6076 "{ind}const {} = {access}.slice({});",
6077 ts_value_ident(&name.name),
6078 elems.len()
6079 );
6080 }
6081 }
6082 }
6083 NodeKind::OrPat { alternatives } => {
6084 if let Some(first) = alternatives.first() {
6085 self.pattern_binds_ts(first, access)?;
6086 }
6087 }
6088 _ => {}
6089 }
6090 Ok(())
6091 }
6092
6093 fn pattern_binds_to_string_ts(&self, pat: &AIRNode, access: &str) -> String {
6096 let mut out = String::new();
6097 self.collect_binds_ts(pat, access, &mut out);
6098 out
6099 }
6100
6101 fn collect_binds_ts(&self, pat: &AIRNode, access: &str, out: &mut String) {
6102 match &pat.kind {
6103 NodeKind::BindPat { name, .. } => {
6104 if ts_bind_is_self_reference(&name.name, access) {
6108 return;
6109 }
6110 let _ = write!(out, "const {} = {access}; ", ts_value_ident(&name.name));
6111 }
6112 NodeKind::ConstructorPat { fields, .. } => {
6113 for (i, field) in fields.iter().enumerate() {
6114 self.collect_binds_ts(field, &format!("{access}._{i}"), out);
6115 }
6116 }
6117 NodeKind::RecordPat { fields, .. } => {
6118 for f in fields {
6119 let field_access = format!("{access}.{}", f.name.name);
6120 match &f.pattern {
6121 Some(p) => self.collect_binds_ts(p, &field_access, out),
6122 None => {
6123 let _ = write!(out, "const {} = {field_access}; ", f.name.name);
6124 }
6125 }
6126 }
6127 }
6128 NodeKind::TuplePat { elems } => {
6129 for (i, e) in elems.iter().enumerate() {
6130 self.collect_binds_ts(e, &format!("{access}[{i}]"), out);
6131 }
6132 }
6133 NodeKind::ListPat { elems, rest } => {
6134 for (i, e) in elems.iter().enumerate() {
6135 self.collect_binds_ts(e, &format!("{access}[{i}]"), out);
6136 }
6137 if let Some(r) = rest {
6138 if let NodeKind::BindPat { name, .. } = &r.kind {
6139 let _ = write!(
6140 out,
6141 "const {} = {access}.slice({}); ",
6142 ts_value_ident(&name.name),
6143 elems.len()
6144 );
6145 }
6146 }
6147 }
6148 NodeKind::OrPat { alternatives } => {
6149 if let Some(first) = alternatives.first() {
6150 self.collect_binds_ts(first, access, out);
6151 }
6152 }
6153 _ => {}
6154 }
6155 }
6156
6157 fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
6160 if let NodeKind::Call { callee, args, .. } = &right.kind {
6161 let has_placeholder = args
6162 .iter()
6163 .any(|a| matches!(a.value.kind, NodeKind::Placeholder));
6164 if has_placeholder {
6165 self.emit_callee(callee)?;
6166 self.buf.push('(');
6167 for (i, arg) in args.iter().enumerate() {
6168 if i > 0 {
6169 self.buf.push_str(", ");
6170 }
6171 if matches!(arg.value.kind, NodeKind::Placeholder) {
6172 self.emit_expr(left)?;
6173 } else {
6174 self.emit_expr(&arg.value)?;
6175 }
6176 }
6177 self.buf.push(')');
6178 return Ok(());
6179 }
6180 }
6181 self.emit_callee(right)?;
6185 self.buf.push('(');
6186 self.emit_expr(left)?;
6187 self.buf.push(')');
6188 Ok(())
6189 }
6190
6191 fn emit_callee(&mut self, callee: &AIRNode) -> Result<(), CodegenError> {
6203 if matches!(
6204 callee.kind,
6205 NodeKind::Lambda { .. } | NodeKind::Compose { .. }
6206 ) {
6207 self.buf.push('(');
6208 self.emit_expr(callee)?;
6209 self.buf.push(')');
6210 Ok(())
6211 } else {
6212 self.emit_expr(callee)
6213 }
6214 }
6215
6216 fn simple_let_redeclared(&self, ts_name: &str) -> bool {
6222 self.let_scopes
6223 .last()
6224 .is_some_and(|s| s.declared.contains(ts_name))
6225 }
6226
6227 fn simple_let_needs_let(&self, ts_name: &str) -> bool {
6230 self.let_scopes
6231 .last()
6232 .is_some_and(|s| s.needs_let.contains(ts_name))
6233 }
6234
6235 fn mark_simple_let_declared(&mut self, ts_name: &str) {
6237 if let Some(s) = self.let_scopes.last_mut() {
6238 s.declared.insert(ts_name.to_string());
6239 }
6240 }
6241
6242 fn enter_let_scope(&mut self, block: &AIRNode) {
6249 let mut needs_let = HashSet::new();
6250 if let NodeKind::Block { stmts, tail } = &block.kind {
6251 let mut seen: HashSet<String> = HashSet::new();
6252 let mut visit = |n: &AIRNode, needs_let: &mut HashSet<String>| match &n.kind {
6253 NodeKind::LetBinding { pattern, .. } => {
6254 if let NodeKind::BindPat { name, .. } = &pattern.kind {
6255 let ts = ts_value_ident(&name.name);
6256 if !seen.insert(ts.clone()) {
6258 needs_let.insert(ts);
6259 }
6260 }
6261 }
6262 NodeKind::Assign { target, .. } => {
6263 if let NodeKind::Identifier { name } = &target.kind {
6264 needs_let.insert(ts_value_ident(&name.name));
6265 }
6266 }
6267 _ => {}
6268 };
6269 for s in stmts {
6270 visit(s, &mut needs_let);
6271 }
6272 if let Some(t) = tail {
6273 visit(t, &mut needs_let);
6274 }
6275 }
6276 self.let_scopes.push(LetScope {
6277 declared: HashSet::new(),
6278 needs_let,
6279 });
6280 }
6281
6282 fn leave_let_scope(&mut self) {
6284 self.let_scopes.pop();
6285 }
6286
6287 fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
6288 self.enter_let_scope(node);
6289 let r = self.emit_block_body_inner(node);
6290 self.leave_let_scope();
6291 r
6292 }
6293
6294 fn emit_loop_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
6306 let prev_sink = self.value_sink.replace(ValueSink::Discard);
6307 let r = self.emit_block_body(node);
6308 self.value_sink = prev_sink;
6309 r
6310 }
6311
6312 fn emit_fn_body_seeded(
6318 &mut self,
6319 params: &[AIRNode],
6320 body: &AIRNode,
6321 ) -> Result<(), CodegenError> {
6322 self.enter_let_scope(body);
6323 if let Some(scope) = self.let_scopes.last_mut() {
6324 for p in params {
6325 if let NodeKind::Param { pattern, .. } = &p.kind {
6326 if let NodeKind::BindPat { name, .. } = &pattern.kind {
6327 let ts = ts_value_ident(&name.name);
6328 scope.needs_let.insert(ts.clone());
6329 scope.declared.insert(ts);
6330 }
6331 }
6332 }
6333 }
6334 let r = self.emit_block_body_inner(body);
6335 self.leave_let_scope();
6336 r
6337 }
6338
6339 fn emit_block_body_inner(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
6340 if let NodeKind::Block { stmts, tail } = &node.kind {
6341 let prev_sink = self.value_sink.replace(ValueSink::Discard);
6351 let mut stmt_res = Ok(());
6352 for s in stmts {
6353 stmt_res = self.emit_node(s);
6354 if stmt_res.is_err() {
6355 break;
6356 }
6357 }
6358 self.value_sink = prev_sink;
6359 stmt_res?;
6360 if let Some(t) = tail {
6361 if crate::generator::node_is_statement(t) {
6362 self.emit_node(t)?;
6363 return Ok(());
6364 }
6365 if self.call_is_diverging(t) {
6369 self.write_indent();
6370 self.emit_expr(t)?;
6371 self.buf.push_str(";\n");
6372 return Ok(());
6373 }
6374 if let NodeKind::Propagate { expr } = &t.kind {
6378 let access = self.emit_propagate(expr)?;
6379 if let Some(sink) = self.value_sink.clone() {
6380 return self.emit_sink_value_str(&access, &sink);
6381 }
6382 let ind = self.indent_str();
6383 let _ = writeln!(self.buf, "{ind}return {access};");
6384 return Ok(());
6385 }
6386 if self.value_needs_stmt_form(t) {
6393 let sink = self.value_sink.clone().unwrap_or(ValueSink::Return);
6394 return self.emit_value_in_stmt_pos(t, &sink);
6395 }
6396 if let NodeKind::Match { scrutinee, arms } = &t.kind {
6397 if crate::generator::match_has_statement_arm(arms) {
6398 self.emit_match(scrutinee, arms, false)?;
6399 return Ok(());
6400 }
6401 }
6402 self.emit_block_tail_value(t)?;
6403 }
6404 } else if crate::generator::node_is_statement(node) {
6405 self.emit_node(node)?;
6406 } else if self.call_is_diverging(node) {
6407 self.write_indent();
6408 self.emit_expr(node)?;
6409 self.buf.push_str(";\n");
6410 } else if self.value_needs_stmt_form(node) {
6411 let sink = self.value_sink.clone().unwrap_or(ValueSink::Return);
6412 self.emit_value_in_stmt_pos(node, &sink)?;
6413 } else if let NodeKind::Match { scrutinee, arms } = &node.kind {
6414 if crate::generator::match_has_statement_arm(arms) {
6415 self.emit_match(scrutinee, arms, false)?;
6416 } else {
6417 self.emit_block_tail_value(node)?;
6418 }
6419 } else {
6420 self.emit_block_tail_value(node)?;
6421 }
6422 Ok(())
6423 }
6424
6425 fn emit_block_tail_value(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
6430 if let Some(sink) = self.value_sink.clone() {
6431 return self.emit_sink_value(node, &sink);
6432 }
6433 let ind = self.indent_str();
6434 let _ = write!(self.buf, "{ind}return ");
6435 self.emit_expr(node)?;
6436 self.buf.push_str(";\n");
6437 Ok(())
6438 }
6439
6440 fn call_is_diverging(&self, node: &AIRNode) -> bool {
6445 if let NodeKind::Call { callee, .. } = &node.kind {
6446 if let NodeKind::Identifier { name } = &callee.kind {
6447 return matches!(name.name.as_str(), "todo" | "unreachable");
6448 }
6449 }
6450 false
6451 }
6452
6453 fn value_needs_stmt_form(&self, node: &AIRNode) -> bool {
6465 match &node.kind {
6466 NodeKind::Loop { .. } => true,
6468 NodeKind::If {
6469 then_block,
6470 else_block,
6471 let_pattern: None,
6472 ..
6473 } => {
6474 self.branch_needs_stmt_form(then_block)
6475 || else_block
6476 .as_deref()
6477 .is_some_and(|e| self.branch_needs_stmt_form(e))
6478 }
6479 NodeKind::Match { arms, .. } => arms.iter().any(|arm| {
6480 matches!(&arm.kind, NodeKind::MatchArm { body, .. } if self.branch_needs_stmt_form(body))
6481 }),
6482 NodeKind::Block { tail, .. } => {
6483 tail.as_deref().is_some_and(|t| self.value_needs_stmt_form(t))
6484 }
6485 _ => false,
6486 }
6487 }
6488
6489 fn branch_needs_stmt_form(&self, node: &AIRNode) -> bool {
6494 self.value_tail_diverges(node) || self.value_needs_stmt_form(node)
6495 }
6496
6497 fn value_tail_diverges(&self, node: &AIRNode) -> bool {
6503 match &node.kind {
6504 NodeKind::Return { .. } | NodeKind::Break { .. } | NodeKind::Continue => true,
6505 NodeKind::Call { .. } => self.call_is_diverging(node),
6506 NodeKind::Block { stmts, tail } => match tail {
6507 Some(t) => self.value_tail_diverges(t),
6508 None => stmts.last().is_some_and(|s| self.value_tail_diverges(s)),
6510 },
6511 _ => false,
6512 }
6513 }
6514
6515 fn emit_value_in_stmt_pos(
6524 &mut self,
6525 node: &AIRNode,
6526 sink: &ValueSink,
6527 ) -> Result<(), CodegenError> {
6528 match &node.kind {
6529 NodeKind::Return { .. } | NodeKind::Break { .. } | NodeKind::Continue => {
6531 self.emit_node(node)
6532 }
6533 NodeKind::Call { .. } if self.call_is_diverging(node) => {
6534 self.write_indent();
6535 self.emit_expr(node)?;
6536 self.buf.push_str(";\n");
6537 Ok(())
6538 }
6539 NodeKind::Loop { body } => {
6540 self.emit_loop_label_prefix(body);
6541 self.set_loop_value_sink(sink.clone());
6546 self.writeln("while (true) {");
6547 self.indent += 1;
6548 self.emit_loop_body(body)?;
6549 self.indent -= 1;
6550 self.writeln("}");
6551 self.pop_loop_frame();
6552 Ok(())
6553 }
6554 NodeKind::If {
6555 let_pattern: None,
6556 condition,
6557 then_block,
6558 else_block,
6559 } => {
6560 let ind = self.indent_str();
6561 let _ = write!(self.buf, "{ind}if (");
6562 self.emit_expr(condition)?;
6563 self.buf.push_str(") {\n");
6564 self.indent += 1;
6565 self.emit_value_in_stmt_pos(then_block, sink)?;
6566 self.indent -= 1;
6567 if let Some(else_b) = else_block {
6568 if matches!(
6569 else_b.kind,
6570 NodeKind::If {
6571 let_pattern: None,
6572 ..
6573 }
6574 ) {
6575 let ind = self.indent_str();
6576 let _ = write!(self.buf, "{ind}}} else ");
6577 self.emit_value_in_stmt_pos_else_if(else_b, sink)?;
6579 return Ok(());
6580 }
6581 self.writeln("} else {");
6582 self.indent += 1;
6583 self.emit_value_in_stmt_pos(else_b, sink)?;
6584 self.indent -= 1;
6585 }
6586 self.writeln("}");
6587 Ok(())
6588 }
6589 NodeKind::Match { scrutinee, arms } => {
6590 let prev = self.value_sink.replace(sink.clone());
6591 let res = self.emit_match(scrutinee, arms, false);
6592 self.value_sink = prev;
6593 res
6594 }
6595 NodeKind::Block { stmts, tail } => {
6596 for s in stmts {
6597 self.emit_node(s)?;
6598 }
6599 if let Some(t) = tail {
6600 self.emit_value_in_stmt_pos(t, sink)?;
6601 }
6602 Ok(())
6603 }
6604 _ => self.emit_sink_value(node, sink),
6606 }
6607 }
6608
6609 fn emit_value_in_stmt_pos_else_if(
6612 &mut self,
6613 node: &AIRNode,
6614 sink: &ValueSink,
6615 ) -> Result<(), CodegenError> {
6616 let NodeKind::If {
6617 condition,
6618 then_block,
6619 else_block,
6620 ..
6621 } = &node.kind
6622 else {
6623 return self.emit_value_in_stmt_pos(node, sink);
6624 };
6625 self.buf.push_str("if (");
6626 self.emit_expr(condition)?;
6627 self.buf.push_str(") {\n");
6628 self.indent += 1;
6629 self.emit_value_in_stmt_pos(then_block, sink)?;
6630 self.indent -= 1;
6631 if let Some(else_b) = else_block {
6632 if matches!(
6633 else_b.kind,
6634 NodeKind::If {
6635 let_pattern: None,
6636 ..
6637 }
6638 ) {
6639 let ind = self.indent_str();
6640 let _ = write!(self.buf, "{ind}}} else ");
6641 self.emit_value_in_stmt_pos_else_if(else_b, sink)?;
6642 return Ok(());
6643 }
6644 self.writeln("} else {");
6645 self.indent += 1;
6646 self.emit_value_in_stmt_pos(else_b, sink)?;
6647 self.indent -= 1;
6648 }
6649 self.writeln("}");
6650 Ok(())
6651 }
6652
6653 fn emit_sink_value(&mut self, node: &AIRNode, sink: &ValueSink) -> Result<(), CodegenError> {
6655 let ind = self.indent_str();
6656 match sink {
6657 ValueSink::Return => {
6658 let _ = write!(self.buf, "{ind}return ");
6659 self.emit_expr(node)?;
6660 self.buf.push_str(";\n");
6661 }
6662 ValueSink::Assign(name) => {
6663 let _ = write!(self.buf, "{ind}{name} = ");
6664 self.emit_expr(node)?;
6665 self.buf.push_str(";\n");
6666 }
6667 ValueSink::Discard => {
6668 let _ = write!(self.buf, "{ind}");
6669 self.emit_expr(node)?;
6670 self.buf.push_str(";\n");
6671 }
6672 }
6673 Ok(())
6674 }
6675
6676 fn emit_sink_value_str(&mut self, value: &str, sink: &ValueSink) -> Result<(), CodegenError> {
6681 let ind = self.indent_str();
6682 match sink {
6683 ValueSink::Return => {
6684 let _ = writeln!(self.buf, "{ind}return {value};");
6685 }
6686 ValueSink::Assign(name) => {
6687 let _ = writeln!(self.buf, "{ind}{name} = {value};");
6688 }
6689 ValueSink::Discard => {
6690 let _ = writeln!(self.buf, "{ind}{value};");
6691 }
6692 }
6693 Ok(())
6694 }
6695
6696 fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
6697 if let NodeKind::Block { stmts, tail } = &node.kind {
6698 if stmts.is_empty() {
6699 if let Some(t) = tail {
6700 return self.emit_expr(t);
6701 }
6702 }
6703 }
6704 self.emit_expr(node)
6705 }
6706
6707 fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
6708 match &pat.kind {
6709 NodeKind::BindPat { name, .. } => ts_value_ident(&name.name),
6710 NodeKind::WildcardPat => "_".into(),
6711 NodeKind::TuplePat { elems } => {
6712 format!(
6713 "[{}]",
6714 elems
6715 .iter()
6716 .map(|e| self.pattern_to_binding_name(e))
6717 .collect::<Vec<_>>()
6718 .join(", ")
6719 )
6720 }
6721 NodeKind::RecordPat { fields, .. } => {
6722 format!(
6723 "{{ {} }}",
6724 fields
6725 .iter()
6726 .map(|f| to_camel_case(&f.name.name).to_string())
6727 .collect::<Vec<_>>()
6728 .join(", ")
6729 )
6730 }
6731 _ => "_".into(),
6732 }
6733 }
6734
6735 fn pattern_to_ts_destructure(&self, pat: &AIRNode) -> String {
6736 self.pattern_to_binding_name(pat)
6737 }
6738
6739 fn type_expr_to_string(&self, node: &AIRNode) -> String {
6740 match &node.kind {
6741 NodeKind::TypeNamed { path, .. } => path
6742 .segments
6743 .iter()
6744 .map(|s| s.name.as_str())
6745 .collect::<Vec<_>>()
6746 .join("."),
6747 NodeKind::Identifier { name } => name.name.clone(),
6748 _ => "Unknown".into(),
6749 }
6750 }
6751}
6752
6753fn build_ts_return_type(is_async: bool, inner: Option<String>) -> String {
6759 match (is_async, inner) {
6760 (true, Some(t)) => format!(": Promise<{t}>"),
6761 (true, None) => ": Promise<void>".to_string(),
6762 (false, Some(t)) => format!(": {t}"),
6763 (false, None) => String::new(),
6764 }
6765}
6766
6767fn is_time_method_name(name: &str) -> bool {
6770 matches!(
6771 name,
6772 "as_nanos"
6773 | "as_millis"
6774 | "as_seconds"
6775 | "is_zero"
6776 | "is_negative"
6777 | "abs"
6778 | "elapsed"
6779 | "duration_since"
6780 )
6781}
6782
6783fn ts_value_ident(name: &str) -> String {
6800 let escaped = crate::generator::escape_target_keyword(
6801 &to_camel_case(name),
6802 crate::generator::KeywordTarget::Ts,
6803 );
6804 if matches!(escaped.as_str(), "eval" | "arguments") {
6805 format!("{escaped}_")
6806 } else {
6807 escaped
6808 }
6809}
6810
6811fn ts_bind_is_self_reference(bind_name: &str, access: &str) -> bool {
6817 let ts = ts_value_ident(bind_name);
6818 access == ts || access == format!("({ts} as any)")
6819}
6820
6821fn ts_esm_emit_name(name: &str, is_fn: bool) -> String {
6827 if is_fn {
6828 ts_value_ident(name)
6829 } else {
6830 name.to_string()
6831 }
6832}
6833
6834fn to_camel_case(s: &str) -> String {
6835 if s.is_empty() || s == "_" {
6836 return s.to_string();
6837 }
6838 if !s.contains('_') && s.starts_with(|c: char| c.is_lowercase()) {
6840 return s.to_string();
6841 }
6842 if s.contains('_') {
6844 let parts: Vec<&str> = s.split('_').filter(|p| !p.is_empty()).collect();
6845 if parts.is_empty() {
6846 return s.to_string();
6847 }
6848 let mut result = parts[0].to_lowercase();
6849 for part in &parts[1..] {
6850 let mut chars = part.chars();
6851 if let Some(first) = chars.next() {
6852 result.push(
6853 first
6854 .to_uppercase()
6855 .next()
6856 .expect("uppercase yields at least one char"),
6857 );
6858 result.extend(chars);
6859 }
6860 }
6861 return result;
6862 }
6863 let mut chars = s.chars();
6865 let first = chars.next().expect("non-empty string guaranteed by caller");
6866 let mut result = first.to_lowercase().to_string();
6867 result.extend(chars);
6868 result
6869}
6870
6871fn escape_js_string(s: &str) -> String {
6873 let mut out = String::with_capacity(s.len());
6874 for ch in s.chars() {
6875 match ch {
6876 '"' => out.push_str("\\\""),
6877 '\\' => out.push_str("\\\\"),
6878 '\n' => out.push_str("\\n"),
6879 '\r' => out.push_str("\\r"),
6880 '\t' => out.push_str("\\t"),
6881 _ => out.push(ch),
6882 }
6883 }
6884 out
6885}
6886
6887fn range_bound_to_ts(node: &AIRNode) -> String {
6894 match &node.kind {
6895 NodeKind::LiteralPat { lit } => ts_literal(lit),
6896 NodeKind::Literal { lit } => ts_literal(lit),
6897 NodeKind::Identifier { name } => ts_value_ident(&name.name),
6898 _ => "0".to_string(),
6899 }
6900}
6901
6902fn ts_literal(lit: &Literal) -> String {
6903 match lit {
6904 Literal::Int(s) | Literal::Float(s) => s.clone(),
6905 Literal::Bool(b) => {
6906 if *b {
6907 "true".to_string()
6908 } else {
6909 "false".to_string()
6910 }
6911 }
6912 Literal::Char(s) => format!("'{s}'"),
6913 Literal::String(s) => format!("\"{}\"", escape_js_string(s)),
6914 Literal::Unit => "undefined".to_string(),
6915 }
6916}
6917
6918fn escape_template_literal(s: &str) -> String {
6920 let mut out = String::with_capacity(s.len());
6921 for ch in s.chars() {
6922 match ch {
6923 '`' => out.push_str("\\`"),
6924 '\\' => out.push_str("\\\\"),
6925 '$' => out.push_str("\\$"),
6926 _ => out.push(ch),
6927 }
6928 }
6929 out
6930}
6931
6932#[cfg(test)]
6935mod tests {
6936 use super::*;
6937 use bock_air::{AirArg, AirRecordField};
6938 use bock_ast::{GenericParam, Ident, TypeExpr, TypePath};
6939 use bock_errors::{FileId, Span};
6940
6941 fn span() -> Span {
6942 Span {
6943 file: FileId(0),
6944 start: 0,
6945 end: 0,
6946 }
6947 }
6948
6949 fn ident(name: &str) -> Ident {
6950 Ident {
6951 name: name.to_string(),
6952 span: span(),
6953 }
6954 }
6955
6956 fn type_path(segments: &[&str]) -> TypePath {
6957 TypePath {
6958 segments: segments.iter().map(|s| ident(s)).collect(),
6959 span: span(),
6960 }
6961 }
6962
6963 fn node(id: u32, kind: NodeKind) -> AIRNode {
6964 AIRNode::new(id, span(), kind)
6965 }
6966
6967 fn int_lit(id: u32, val: &str) -> AIRNode {
6968 node(
6969 id,
6970 NodeKind::Literal {
6971 lit: Literal::Int(val.into()),
6972 },
6973 )
6974 }
6975
6976 fn str_lit(id: u32, val: &str) -> AIRNode {
6977 node(
6978 id,
6979 NodeKind::Literal {
6980 lit: Literal::String(val.into()),
6981 },
6982 )
6983 }
6984
6985 fn id_node(id: u32, name: &str) -> AIRNode {
6986 node(id, NodeKind::Identifier { name: ident(name) })
6987 }
6988
6989 fn bind_pat(id: u32, name: &str) -> AIRNode {
6990 node(
6991 id,
6992 NodeKind::BindPat {
6993 name: ident(name),
6994 is_mut: false,
6995 },
6996 )
6997 }
6998
6999 fn typed_param_node(id: u32, name: &str, ty_name: &str) -> AIRNode {
7000 node(
7001 id,
7002 NodeKind::Param {
7003 pattern: Box::new(bind_pat(id + 100, name)),
7004 ty: Some(Box::new(node(
7005 id + 200,
7006 NodeKind::TypeNamed {
7007 path: type_path(&[ty_name]),
7008 args: vec![],
7009 },
7010 ))),
7011 default: None,
7012 },
7013 )
7014 }
7015
7016 fn type_node(id: u32, name: &str) -> AIRNode {
7017 node(
7018 id,
7019 NodeKind::TypeNamed {
7020 path: type_path(&[name]),
7021 args: vec![],
7022 },
7023 )
7024 }
7025
7026 fn untyped_param_node(id: u32, name: &str) -> AIRNode {
7029 node(
7030 id,
7031 NodeKind::Param {
7032 pattern: Box::new(bind_pat(id + 100, name)),
7033 ty: None,
7034 default: None,
7035 },
7036 )
7037 }
7038
7039 fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
7040 node(
7041 id,
7042 NodeKind::Block {
7043 stmts,
7044 tail: tail.map(Box::new),
7045 },
7046 )
7047 }
7048
7049 fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
7050 node(
7051 0,
7052 NodeKind::Module {
7053 path: None,
7054 annotations: vec![],
7055 imports,
7056 items,
7057 },
7058 )
7059 }
7060
7061 fn gen(module: &AIRNode) -> String {
7062 let gen = TsGenerator::new();
7063 let result = gen.generate_module(module).unwrap();
7064 result.files[0].content.clone()
7065 }
7066
7067 fn make_generic_param(name: &str) -> GenericParam {
7068 GenericParam {
7069 id: 0,
7070 span: span(),
7071 name: ident(name),
7072 bounds: vec![],
7073 }
7074 }
7075
7076 fn make_bounded_generic_param(name: &str, bounds: &[&str]) -> GenericParam {
7077 GenericParam {
7078 id: 0,
7079 span: span(),
7080 name: ident(name),
7081 bounds: bounds.iter().map(|b| type_path(&[b])).collect(),
7082 }
7083 }
7084
7085 fn make_type_expr(name: &str) -> TypeExpr {
7086 TypeExpr::Named {
7087 id: 0,
7088 span: span(),
7089 path: type_path(&[name]),
7090 args: vec![],
7091 }
7092 }
7093
7094 fn make_record_field(name: &str, ty_name: &str) -> bock_ast::RecordDeclField {
7095 bock_ast::RecordDeclField {
7096 id: 0,
7097 span: span(),
7098 name: ident(name),
7099 ty: make_type_expr(ty_name),
7100 default: None,
7101 }
7102 }
7103
7104 #[test]
7107 fn implements_code_generator_trait() {
7108 let gen = TsGenerator::new();
7109 assert_eq!(gen.target().id, "ts");
7110 }
7111
7112 #[test]
7113 fn empty_module() {
7114 let m = module(vec![], vec![]);
7115 let out = gen(&m);
7116 assert_eq!(out, "");
7117 }
7118
7119 #[test]
7120 fn generate_project_uses_source_mirrored_path_for_ts() {
7121 let gen = TsGenerator::new();
7122 let m = module(vec![], vec![]);
7123 let src_path = std::path::Path::new("src/lib.bock");
7124 let result = gen.generate_project(&[(&m, src_path)]).unwrap();
7125 assert_eq!(result.files[0].path, std::path::PathBuf::from("lib.ts"));
7126 }
7127
7128 fn module_with_path(path: &[&str], imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
7130 node(
7131 0,
7132 NodeKind::Module {
7133 path: Some(bock_ast::ModulePath {
7134 segments: path.iter().map(|s| ident(s)).collect(),
7135 span: span(),
7136 }),
7137 annotations: vec![],
7138 imports,
7139 items,
7140 },
7141 )
7142 }
7143
7144 fn import_named(id: u32, path: &[&str], name: &str) -> AIRNode {
7146 node(
7147 id,
7148 NodeKind::ImportDecl {
7149 path: bock_ast::ModulePath {
7150 segments: path.iter().map(|s| ident(s)).collect(),
7151 span: span(),
7152 },
7153 items: bock_ast::ImportItems::Named(vec![bock_ast::ImportedName {
7154 span: span(),
7155 name: ident(name),
7156 alias: None,
7157 }]),
7158 },
7159 )
7160 }
7161
7162 fn fn_decl_tail(id: u32, vis: Visibility, name: &str, tail: AIRNode) -> AIRNode {
7164 node(
7165 id,
7166 NodeKind::FnDecl {
7167 annotations: vec![],
7168 visibility: vis,
7169 is_async: false,
7170 name: ident(name),
7171 generic_params: vec![],
7172 params: vec![],
7173 return_type: None,
7174 effect_clause: vec![],
7175 where_clause: vec![],
7176 body: Box::new(block(id + 1, vec![], Some(tail))),
7177 },
7178 )
7179 }
7180
7181 #[test]
7182 fn per_module_emits_native_esm_import_tree_ts() {
7183 let call = node(
7189 10,
7190 NodeKind::Call {
7191 callee: Box::new(id_node(11, "add_one")),
7192 args: vec![bock_air::AirArg {
7193 label: None,
7194 value: int_lit(12, "6"),
7195 }],
7196 type_args: vec![],
7197 },
7198 );
7199 let main_mod = module_with_path(
7200 &["main"],
7201 vec![import_named(5, &["mathutil"], "add_one")],
7202 vec![fn_decl_tail(1, Visibility::Private, "main", call)],
7203 );
7204 let util_mod = module_with_path(
7205 &["mathutil"],
7206 vec![],
7207 vec![fn_decl_tail(
7208 20,
7209 Visibility::Public,
7210 "add_one",
7211 int_lit(22, "7"),
7212 )],
7213 );
7214
7215 let gen = TsGenerator::new();
7216 let out = gen
7217 .generate_project(&[
7218 (&main_mod, std::path::Path::new("src/main.bock")),
7219 (&util_mod, std::path::Path::new("src/mathutil.bock")),
7220 ])
7221 .unwrap();
7222 let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
7223 let main_file = by_name("main.ts").expect("main.ts emitted");
7224 let util_file = by_name("mathutil.ts").expect("mathutil.ts emitted");
7225 assert!(
7228 by_name("package.json").is_none(),
7229 "codegen must NOT emit package.json — the scaffolder owns it (S6a)"
7230 );
7231 assert!(
7232 main_file
7233 .content
7234 .contains("import { addOne } from \"./mathutil.ts\";"),
7235 "main.ts must import the camelCased fn via the `.ts` specifier; got:\n{}",
7236 main_file.content
7237 );
7238 assert!(
7239 util_file.content.contains("export function addOne("),
7240 "mathutil.ts must export the fn inline; got:\n{}",
7241 util_file.content
7242 );
7243 }
7244
7245 #[test]
7246 fn per_module_imports_enum_type_via_import_type_ts() {
7247 let color_enum = node(
7253 40,
7254 NodeKind::EnumDecl {
7255 annotations: vec![],
7256 visibility: Visibility::Public,
7257 name: ident("Color"),
7258 generic_params: vec![],
7259 variants: vec![
7260 node(
7261 41,
7262 NodeKind::EnumVariant {
7263 name: ident("Red"),
7264 payload: EnumVariantPayload::Unit,
7265 },
7266 ),
7267 node(
7268 42,
7269 NodeKind::EnumVariant {
7270 name: ident("Blue"),
7271 payload: EnumVariantPayload::Unit,
7272 },
7273 ),
7274 ],
7275 },
7276 );
7277 let palette_mod = module_with_path(&["palette"], vec![], vec![color_enum]);
7278 let paint = node(
7280 1,
7281 NodeKind::FnDecl {
7282 annotations: vec![],
7283 visibility: Visibility::Private,
7284 is_async: false,
7285 name: ident("main"),
7286 generic_params: vec![],
7287 params: vec![typed_param_node(2, "c", "Color")],
7288 return_type: None,
7289 effect_clause: vec![],
7290 where_clause: vec![],
7291 body: Box::new(block(4, vec![], Some(int_lit(5, "0")))),
7292 },
7293 );
7294 let main_mod = module_with_path(
7295 &["main"],
7296 vec![import_named(6, &["palette"], "Color")],
7297 vec![paint],
7298 );
7299
7300 let gen = TsGenerator::new();
7301 let out = gen
7302 .generate_project(&[
7303 (&main_mod, std::path::Path::new("src/main.bock")),
7304 (&palette_mod, std::path::Path::new("src/palette.bock")),
7305 ])
7306 .unwrap();
7307 let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
7308 let main_file = by_name("main.ts").expect("main.ts emitted");
7309 let palette_file = by_name("palette.ts").expect("palette.ts emitted");
7310 assert!(
7311 main_file
7312 .content
7313 .contains("import type { Color } from \"./palette.ts\";"),
7314 "main.ts must `import type` the enum type; got:\n{}",
7315 main_file.content
7316 );
7317 assert!(
7318 palette_file
7319 .content
7320 .contains("export { Color_Blue, Color_Red };"),
7321 "palette.ts must re-export the enum variant values; got:\n{}",
7322 palette_file.content
7323 );
7324 }
7325
7326 #[test]
7329 fn function_with_type_annotations() {
7330 let body = block(10, vec![], Some(id_node(11, "x")));
7331 let f = node(
7332 1,
7333 NodeKind::FnDecl {
7334 annotations: vec![],
7335 visibility: Visibility::Public,
7336 is_async: false,
7337 name: ident("add"),
7338 generic_params: vec![],
7339 params: vec![
7340 typed_param_node(2, "x", "Int"),
7341 typed_param_node(3, "y", "Int"),
7342 ],
7343 return_type: Some(Box::new(type_node(4, "Int"))),
7344 effect_clause: vec![],
7345 where_clause: vec![],
7346 body: Box::new(body),
7347 },
7348 );
7349 let out = gen(&module(vec![], vec![f]));
7350 assert!(out.contains("x: number"), "got: {out}");
7351 assert!(out.contains("y: number"), "got: {out}");
7352 assert!(out.contains("): number"), "got: {out}");
7353 assert!(out.contains("export function add"));
7354 }
7355
7356 #[test]
7357 fn function_without_type_annotations() {
7358 let body = block(10, vec![], Some(int_lit(11, "42")));
7359 let f = node(
7360 1,
7361 NodeKind::FnDecl {
7362 annotations: vec![],
7363 visibility: Visibility::Private,
7364 is_async: false,
7365 name: ident("answer"),
7366 generic_params: vec![],
7367 params: vec![],
7368 return_type: None,
7369 effect_clause: vec![],
7370 where_clause: vec![],
7371 body: Box::new(body),
7372 },
7373 );
7374 let out = gen(&module(vec![], vec![f]));
7375 assert!(out.contains("function answer()"), "got: {out}");
7376 assert!(!out.contains("export"), "got: {out}");
7377 }
7378
7379 #[test]
7382 fn function_with_generics() {
7383 let body = block(10, vec![], Some(id_node(11, "x")));
7384 let f = node(
7385 1,
7386 NodeKind::FnDecl {
7387 annotations: vec![],
7388 visibility: Visibility::Private,
7389 is_async: false,
7390 name: ident("identity"),
7391 generic_params: vec![make_generic_param("T")],
7392 params: vec![typed_param_node(2, "x", "T")],
7393 return_type: Some(Box::new(type_node(3, "T"))),
7394 effect_clause: vec![],
7395 where_clause: vec![],
7396 body: Box::new(body),
7397 },
7398 );
7399 let out = gen(&module(vec![], vec![f]));
7400 assert!(out.contains("function identity<T>"), "got: {out}");
7401 assert!(out.contains("x: T"), "got: {out}");
7402 assert!(out.contains("): T"), "got: {out}");
7403 }
7404
7405 #[test]
7406 fn generics_with_bounds() {
7407 let body = block(10, vec![], Some(id_node(11, "x")));
7408 let f = node(
7409 1,
7410 NodeKind::FnDecl {
7411 annotations: vec![],
7412 visibility: Visibility::Private,
7413 is_async: false,
7414 name: ident("sorted"),
7415 generic_params: vec![make_bounded_generic_param("T", &["Comparable"])],
7416 params: vec![typed_param_node(2, "x", "T")],
7417 return_type: Some(Box::new(type_node(3, "T"))),
7418 effect_clause: vec![],
7419 where_clause: vec![],
7420 body: Box::new(body),
7421 },
7422 );
7423 let out = gen(&module(vec![], vec![f]));
7424 assert!(
7429 out.contains("function sorted<T>(x: T): T {") && !out.contains("extends"),
7430 "got: {out}"
7431 );
7432 }
7433
7434 #[test]
7437 fn trait_becomes_interface() {
7438 let method = node(
7439 2,
7440 NodeKind::FnDecl {
7441 annotations: vec![],
7442 visibility: Visibility::Public,
7443 is_async: false,
7444 name: ident("area"),
7445 generic_params: vec![],
7446 params: vec![],
7447 return_type: Some(Box::new(type_node(3, "Float"))),
7448 effect_clause: vec![],
7449 where_clause: vec![],
7450 body: Box::new(block(4, vec![], None)),
7451 },
7452 );
7453 let trait_decl = node(
7454 1,
7455 NodeKind::TraitDecl {
7456 annotations: vec![],
7457 visibility: Visibility::Public,
7458 is_platform: false,
7459 name: ident("Shape"),
7460 generic_params: vec![],
7461 associated_types: vec![],
7462 methods: vec![method],
7463 },
7464 );
7465 let out = gen(&module(vec![], vec![trait_decl]));
7466 assert!(out.contains("export interface Shape"), "got: {out}");
7467 assert!(out.contains("area(): number"), "got: {out}");
7468 }
7469
7470 #[test]
7471 fn trait_with_generics() {
7472 let method = node(
7473 2,
7474 NodeKind::FnDecl {
7475 annotations: vec![],
7476 visibility: Visibility::Public,
7477 is_async: false,
7478 name: ident("compare"),
7479 generic_params: vec![],
7480 params: vec![typed_param_node(3, "other", "T")],
7481 return_type: Some(Box::new(type_node(4, "Int"))),
7482 effect_clause: vec![],
7483 where_clause: vec![],
7484 body: Box::new(block(5, vec![], None)),
7485 },
7486 );
7487 let trait_decl = node(
7488 1,
7489 NodeKind::TraitDecl {
7490 annotations: vec![],
7491 visibility: Visibility::Public,
7492 is_platform: false,
7493 name: ident("Comparable"),
7494 generic_params: vec![make_generic_param("T")],
7495 associated_types: vec![],
7496 methods: vec![method],
7497 },
7498 );
7499 let out = gen(&module(vec![], vec![trait_decl]));
7500 assert!(out.contains("interface Comparable<T>"), "got: {out}");
7501 assert!(out.contains("compare(other: T): number"), "got: {out}");
7502 }
7503
7504 #[test]
7505 fn trait_decl_self_param_typed_as_trait_interface() {
7506 let method = node(
7510 2,
7511 NodeKind::FnDecl {
7512 annotations: vec![],
7513 visibility: Visibility::Public,
7514 is_async: false,
7515 name: ident("equals"),
7516 generic_params: vec![],
7517 params: vec![untyped_param_node(3, "self")],
7518 return_type: Some(Box::new(type_node(4, "Bool"))),
7519 effect_clause: vec![],
7520 where_clause: vec![],
7521 body: Box::new(block(5, vec![], None)),
7522 },
7523 );
7524 let trait_decl = node(
7525 1,
7526 NodeKind::TraitDecl {
7527 annotations: vec![],
7528 visibility: Visibility::Public,
7529 is_platform: false,
7530 name: ident("Eq"),
7531 generic_params: vec![],
7532 associated_types: vec![],
7533 methods: vec![method],
7534 },
7535 );
7536 let out = gen(&module(vec![], vec![trait_decl]));
7537 assert!(
7538 out.contains("equals(self: Eq): boolean"),
7539 "trait `self` should be typed as the trait interface, got: {out}"
7540 );
7541 }
7542
7543 #[test]
7546 fn record_becomes_interface_and_factory() {
7547 let record = node(
7548 1,
7549 NodeKind::RecordDecl {
7550 annotations: vec![],
7551 visibility: Visibility::Public,
7552 name: ident("Point"),
7553 generic_params: vec![],
7554 fields: vec![
7555 make_record_field("x", "Float"),
7556 make_record_field("y", "Float"),
7557 ],
7558 },
7559 );
7560 let out = gen(&module(vec![], vec![record]));
7561 assert!(out.contains("export class Point"), "got: {out}");
7562 assert!(out.contains("x: number"), "got: {out}");
7563 assert!(out.contains("y: number"), "got: {out}");
7564 assert!(
7565 out.contains("constructor({ x, y }: { x: number; y: number })"),
7566 "got: {out}"
7567 );
7568 assert!(out.contains("this.x = x;"), "got: {out}");
7569 assert!(out.contains("this.y = y;"), "got: {out}");
7570 }
7571
7572 #[test]
7575 fn enum_becomes_discriminated_union() {
7576 let variants = vec![
7577 node(
7578 2,
7579 NodeKind::EnumVariant {
7580 name: ident("None"),
7581 payload: EnumVariantPayload::Unit,
7582 },
7583 ),
7584 node(
7585 3,
7586 NodeKind::EnumVariant {
7587 name: ident("Some"),
7588 payload: EnumVariantPayload::Struct(vec![make_record_field("value", "T")]),
7589 },
7590 ),
7591 ];
7592 let enum_decl = node(
7593 1,
7594 NodeKind::EnumDecl {
7595 annotations: vec![],
7596 visibility: Visibility::Public,
7597 name: ident("Option"),
7598 generic_params: vec![make_generic_param("T")],
7599 variants,
7600 },
7601 );
7602 let out = gen(&module(vec![], vec![enum_decl]));
7603 assert!(
7608 out.contains("export type Option<T> = Option_None<T> | Option_Some<T>;"),
7609 "got: {out}"
7610 );
7611 assert!(
7615 out.contains("interface Option_None<T = unknown>"),
7616 "got: {out}"
7617 );
7618 assert!(out.contains("readonly _tag: \"None\""), "got: {out}");
7619 assert!(
7620 out.contains("const Option_None: Option_None = "),
7621 "got: {out}"
7622 );
7623 assert!(out.contains("interface Option_Some<T>"), "got: {out}");
7626 assert!(out.contains("readonly value: T"), "got: {out}");
7627 assert!(
7628 out.contains("function Option_Some<T>(value: T): Option_Some<T>"),
7629 "got: {out}"
7630 );
7631 }
7632
7633 #[test]
7641 fn typed_let_binding_widens_variant_construct_to_declared_union() {
7642 let gate = node(
7643 1,
7644 NodeKind::EnumDecl {
7645 annotations: vec![],
7646 visibility: Visibility::Public,
7647 name: ident("Gate"),
7648 generic_params: vec![],
7649 variants: vec![
7650 node(
7651 2,
7652 NodeKind::EnumVariant {
7653 name: ident("Open"),
7654 payload: EnumVariantPayload::Struct(vec![make_record_field(
7655 "level", "Int",
7656 )]),
7657 },
7658 ),
7659 node(
7660 3,
7661 NodeKind::EnumVariant {
7662 name: ident("Closed"),
7663 payload: EnumVariantPayload::Tuple(vec![type_node(4, "Int")]),
7664 },
7665 ),
7666 node(
7667 5,
7668 NodeKind::EnumVariant {
7669 name: ident("Locked"),
7670 payload: EnumVariantPayload::Unit,
7671 },
7672 ),
7673 ],
7674 },
7675 );
7676 let open = node(
7678 10,
7679 NodeKind::RecordConstruct {
7680 path: type_path(&["Open"]),
7681 fields: vec![bock_air::AirRecordField {
7682 name: ident("level"),
7683 value: Some(Box::new(int_lit(11, "7"))),
7684 }],
7685 spread: None,
7686 },
7687 );
7688 let let_g = node(
7689 12,
7690 NodeKind::LetBinding {
7691 is_mut: false,
7692 pattern: Box::new(bind_pat(13, "g")),
7693 ty: Some(Box::new(type_node(14, "Gate"))),
7694 value: Box::new(open),
7695 },
7696 );
7697 let closed = node(
7699 20,
7700 NodeKind::Call {
7701 callee: Box::new(id_node(21, "Closed")),
7702 args: vec![AirArg {
7703 label: None,
7704 value: int_lit(22, "3"),
7705 }],
7706 type_args: vec![],
7707 },
7708 );
7709 let let_c = node(
7710 23,
7711 NodeKind::LetBinding {
7712 is_mut: false,
7713 pattern: Box::new(bind_pat(24, "c")),
7714 ty: Some(Box::new(type_node(25, "Gate"))),
7715 value: Box::new(closed),
7716 },
7717 );
7718 let let_k = node(
7720 30,
7721 NodeKind::LetBinding {
7722 is_mut: false,
7723 pattern: Box::new(bind_pat(31, "k")),
7724 ty: Some(Box::new(type_node(32, "Gate"))),
7725 value: Box::new(id_node(33, "Locked")),
7726 },
7727 );
7728 let f = ts_fn_decl(
7729 40,
7730 "main",
7731 vec![],
7732 None,
7733 block(41, vec![let_g, let_c, let_k], None),
7734 );
7735 let out = gen(&module(vec![], vec![gate, f]));
7736 assert!(
7737 out.contains("const g: Gate = Gate_Open(7) as Gate;"),
7738 "struct-variant binding must widen to the declared union, got:\n{out}"
7739 );
7740 assert!(
7741 out.contains("const c: Gate = Gate_Closed(3) as Gate;"),
7742 "tuple-variant binding must widen to the declared union, got:\n{out}"
7743 );
7744 assert!(
7745 out.contains("const k: Gate = Gate_Locked as Gate;"),
7746 "unit-variant binding must widen to the declared union, got:\n{out}"
7747 );
7748 }
7749
7750 #[test]
7754 fn typed_let_binding_no_widening_for_non_enum_construct() {
7755 let point = node(
7756 1,
7757 NodeKind::RecordDecl {
7758 annotations: vec![],
7759 visibility: Visibility::Public,
7760 name: ident("Point"),
7761 generic_params: vec![],
7762 fields: vec![make_record_field("x", "Int")],
7763 },
7764 );
7765 let ctor = node(
7766 10,
7767 NodeKind::RecordConstruct {
7768 path: type_path(&["Point"]),
7769 fields: vec![bock_air::AirRecordField {
7770 name: ident("x"),
7771 value: Some(Box::new(int_lit(11, "5"))),
7772 }],
7773 spread: None,
7774 },
7775 );
7776 let let_p = node(
7777 12,
7778 NodeKind::LetBinding {
7779 is_mut: false,
7780 pattern: Box::new(bind_pat(13, "p")),
7781 ty: Some(Box::new(type_node(14, "Point"))),
7782 value: Box::new(ctor),
7783 },
7784 );
7785 let f = ts_fn_decl(20, "main", vec![], None, block(21, vec![let_p], None));
7786 let out = gen(&module(vec![], vec![point, f]));
7787 assert!(
7788 !out.contains(" as Point"),
7789 "a record (non-enum) binding must not get a widening cast, got:\n{out}"
7790 );
7791 }
7792
7793 #[test]
7796 fn type_alias_emitted() {
7797 let alias = node(
7798 1,
7799 NodeKind::TypeAlias {
7800 annotations: vec![],
7801 visibility: Visibility::Public,
7802 name: ident("UserId"),
7803 generic_params: vec![],
7804 ty: Box::new(type_node(2, "String")),
7805 where_clause: vec![],
7806 },
7807 );
7808 let out = gen(&module(vec![], vec![alias]));
7809 assert!(out.contains("export type UserId = string;"), "got: {out}");
7810 }
7811
7812 #[test]
7813 fn generic_type_alias() {
7814 let alias = node(
7815 1,
7816 NodeKind::TypeAlias {
7817 annotations: vec![],
7818 visibility: Visibility::Private,
7819 name: ident("Pair"),
7820 generic_params: vec![make_generic_param("A"), make_generic_param("B")],
7821 ty: Box::new(node(
7822 2,
7823 NodeKind::TypeTuple {
7824 elems: vec![type_node(3, "A"), type_node(4, "B")],
7825 },
7826 )),
7827 where_clause: vec![],
7828 },
7829 );
7830 let out = gen(&module(vec![], vec![alias]));
7831 assert!(out.contains("type Pair<A, B> = [A, B];"), "got: {out}");
7832 }
7833
7834 #[test]
7837 fn effects_as_typed_params() {
7838 let body = block(10, vec![], None);
7839 let f = node(
7840 1,
7841 NodeKind::FnDecl {
7842 annotations: vec![],
7843 visibility: Visibility::Private,
7844 is_async: false,
7845 name: ident("process"),
7846 generic_params: vec![],
7847 params: vec![typed_param_node(2, "data", "String")],
7848 return_type: None,
7849 effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
7850 where_clause: vec![],
7851 body: Box::new(body),
7852 },
7853 );
7854 let out = gen(&module(vec![], vec![f]));
7855 assert!(
7856 out.contains("{ log, clock }: { log: Log, clock: Clock }"),
7857 "got: {out}"
7858 );
7859 }
7860
7861 #[test]
7869 fn clock_time_ops_route_through_handler() {
7870 let out = gen(&module(vec![], vec![clock_timed_fn()]));
7871 assert!(out.contains("clock.now_monotonic()"), "got: {out}");
7872 assert!(out.contains("clock.sleep("), "got: {out}");
7873 assert!(
7874 !out.contains("performance.now()"),
7875 "host clock primitive leaked past the handler: {out}"
7876 );
7877 assert!(
7878 !out.contains("setTimeout"),
7879 "host sleep primitive leaked past the handler: {out}"
7880 );
7881 }
7882
7883 #[test]
7887 fn builtin_time_types_map_to_number() {
7888 let f = node(
7889 1,
7890 NodeKind::FnDecl {
7891 annotations: vec![],
7892 visibility: Visibility::Private,
7893 is_async: false,
7894 name: ident("span"),
7895 generic_params: vec![],
7896 params: vec![typed_param_node(2, "d", "Duration")],
7897 return_type: Some(Box::new(type_node(3, "Instant"))),
7898 effect_clause: vec![],
7899 where_clause: vec![],
7900 body: Box::new(block(10, vec![], None)),
7901 },
7902 );
7903 let out = gen(&module(vec![], vec![f]));
7904 assert!(out.contains("d: number"), "Duration annotation: {out}");
7905 assert!(out.contains("): number"), "Instant annotation: {out}");
7906 assert!(!out.contains(": Duration"), "leaked Duration type: {out}");
7907 assert!(!out.contains(": Instant"), "leaked Instant type: {out}");
7908 }
7909
7910 fn clock_timed_fn() -> AIRNode {
7914 let instant_now = node(
7915 40,
7916 NodeKind::Call {
7917 callee: Box::new(node(
7918 41,
7919 NodeKind::FieldAccess {
7920 object: Box::new(id_node(42, "Instant")),
7921 field: ident("now"),
7922 },
7923 )),
7924 args: vec![],
7925 type_args: vec![],
7926 },
7927 );
7928 let duration_millis = node(
7929 50,
7930 NodeKind::Call {
7931 callee: Box::new(node(
7932 51,
7933 NodeKind::FieldAccess {
7934 object: Box::new(id_node(52, "Duration")),
7935 field: ident("millis"),
7936 },
7937 )),
7938 args: vec![AirArg {
7939 label: None,
7940 value: int_lit(53, "1"),
7941 }],
7942 type_args: vec![],
7943 },
7944 );
7945 let sleep_call = node(
7946 60,
7947 NodeKind::Call {
7948 callee: Box::new(id_node(61, "sleep")),
7949 args: vec![AirArg {
7950 label: None,
7951 value: duration_millis,
7952 }],
7953 type_args: vec![],
7954 },
7955 );
7956 let elapsed_call = node(
7957 70,
7958 NodeKind::MethodCall {
7959 receiver: Box::new(id_node(71, "start")),
7960 method: ident("elapsed"),
7961 type_args: vec![],
7962 args: vec![],
7963 },
7964 );
7965 let body = block(
7966 30,
7967 vec![
7968 node(
7969 31,
7970 NodeKind::LetBinding {
7971 is_mut: false,
7972 pattern: Box::new(bind_pat(32, "start")),
7973 ty: None,
7974 value: Box::new(instant_now),
7975 },
7976 ),
7977 sleep_call,
7978 node(
7979 33,
7980 NodeKind::LetBinding {
7981 is_mut: false,
7982 pattern: Box::new(bind_pat(34, "d")),
7983 ty: None,
7984 value: Box::new(elapsed_call),
7985 },
7986 ),
7987 ],
7988 None,
7989 );
7990 node(
7991 1,
7992 NodeKind::FnDecl {
7993 annotations: vec![],
7994 visibility: Visibility::Private,
7995 is_async: false,
7996 name: ident("timed"),
7997 generic_params: vec![],
7998 params: vec![],
7999 return_type: None,
8000 effect_clause: vec![type_path(&["Clock"])],
8001 where_clause: vec![],
8002 body: Box::new(body),
8003 },
8004 )
8005 }
8006
8007 #[test]
8010 fn async_function_with_types() {
8011 let body = block(10, vec![], Some(str_lit(11, "done")));
8012 let f = node(
8013 1,
8014 NodeKind::FnDecl {
8015 annotations: vec![],
8016 visibility: Visibility::Public,
8017 is_async: true,
8018 name: ident("fetch"),
8019 generic_params: vec![],
8020 params: vec![typed_param_node(2, "url", "String")],
8021 return_type: Some(Box::new(type_node(3, "String"))),
8022 effect_clause: vec![],
8023 where_clause: vec![],
8024 body: Box::new(body),
8025 },
8026 );
8027 let out = gen(&module(vec![], vec![f]));
8028 assert!(out.contains("export async function fetch"), "got: {out}");
8029 assert!(out.contains("url: string"), "got: {out}");
8030 assert!(out.contains("): Promise<string>"), "got: {out}");
8032 }
8033
8034 #[test]
8035 fn async_function_without_return_type_is_promise_void() {
8036 let body = block(10, vec![], None);
8037 let f = node(
8038 1,
8039 NodeKind::FnDecl {
8040 annotations: vec![],
8041 visibility: Visibility::Private,
8042 is_async: true,
8043 name: ident("tick"),
8044 generic_params: vec![],
8045 params: vec![],
8046 return_type: None,
8047 effect_clause: vec![],
8048 where_clause: vec![],
8049 body: Box::new(body),
8050 },
8051 );
8052 let out = gen(&module(vec![], vec![f]));
8053 assert!(out.contains("async function tick()"), "got: {out}");
8054 assert!(out.contains("): Promise<void>"), "got: {out}");
8055 }
8056
8057 #[test]
8058 fn sync_function_return_type_unchanged() {
8059 let body = block(10, vec![], Some(str_lit(11, "done")));
8060 let f = node(
8061 1,
8062 NodeKind::FnDecl {
8063 annotations: vec![],
8064 visibility: Visibility::Private,
8065 is_async: false,
8066 name: ident("hello"),
8067 generic_params: vec![],
8068 params: vec![],
8069 return_type: Some(Box::new(type_node(2, "String"))),
8070 effect_clause: vec![],
8071 where_clause: vec![],
8072 body: Box::new(body),
8073 },
8074 );
8075 let out = gen(&module(vec![], vec![f]));
8076 assert!(out.contains("function hello(): string"), "got: {out}");
8077 assert!(!out.contains("Promise"), "got: {out}");
8078 }
8079
8080 #[test]
8081 fn entry_invocation_async_main_ts() {
8082 let inv = TsGenerator::new().entry_invocation(true).unwrap();
8083 assert!(inv.contains("async () =>"));
8084 assert!(inv.contains("await main()"));
8085 }
8086
8087 #[test]
8088 fn generate_project_async_main_wraps_entry_ts() {
8089 let main_fn = node(
8090 1,
8091 NodeKind::FnDecl {
8092 annotations: vec![],
8093 visibility: Visibility::Private,
8094 is_async: true,
8095 name: ident("main"),
8096 generic_params: vec![],
8097 params: vec![],
8098 return_type: None,
8099 effect_clause: vec![],
8100 where_clause: vec![],
8101 body: Box::new(block(2, vec![], None)),
8102 },
8103 );
8104 let m = module(vec![], vec![main_fn]);
8105 let gen = TsGenerator::new();
8106 let src_path = std::path::Path::new("src/main.bock");
8107 let out = gen.generate_project(&[(&m, src_path)]).unwrap();
8108 let src = &out.files[0].content;
8109 assert_eq!(out.files[0].path, std::path::PathBuf::from("main.ts"));
8110 assert!(src.contains("async function main()"), "got: {src}");
8111 assert!(
8112 src.contains("(async () => { await main(); })();"),
8113 "got: {src}"
8114 );
8115 }
8116
8117 #[test]
8120 fn let_binding_with_type() {
8121 let stmt = node(
8122 1,
8123 NodeKind::LetBinding {
8124 is_mut: false,
8125 pattern: Box::new(bind_pat(2, "x")),
8126 ty: Some(Box::new(type_node(3, "Int"))),
8127 value: Box::new(int_lit(4, "42")),
8128 },
8129 );
8130 let f = node(
8131 5,
8132 NodeKind::FnDecl {
8133 annotations: vec![],
8134 visibility: Visibility::Private,
8135 is_async: false,
8136 name: ident("test"),
8137 generic_params: vec![],
8138 params: vec![],
8139 return_type: None,
8140 effect_clause: vec![],
8141 where_clause: vec![],
8142 body: Box::new(block(6, vec![stmt], None)),
8143 },
8144 );
8145 let out = gen(&module(vec![], vec![f]));
8146 assert!(out.contains("const x: number = 42;"), "got: {out}");
8147 }
8148
8149 #[test]
8150 fn mutable_binding_with_type() {
8151 let stmt = node(
8152 1,
8153 NodeKind::LetBinding {
8154 is_mut: true,
8155 pattern: Box::new(bind_pat(2, "count")),
8156 ty: Some(Box::new(type_node(3, "Int"))),
8157 value: Box::new(int_lit(4, "0")),
8158 },
8159 );
8160 let f = node(
8161 5,
8162 NodeKind::FnDecl {
8163 annotations: vec![],
8164 visibility: Visibility::Private,
8165 is_async: false,
8166 name: ident("test"),
8167 generic_params: vec![],
8168 params: vec![],
8169 return_type: None,
8170 effect_clause: vec![],
8171 where_clause: vec![],
8172 body: Box::new(block(6, vec![stmt], None)),
8173 },
8174 );
8175 let out = gen(&module(vec![], vec![f]));
8176 assert!(out.contains("let count: number = 0;"), "got: {out}");
8177 }
8178
8179 #[test]
8182 fn type_mapping_primitives() {
8183 let ctx = TsEmitCtx::new();
8184 assert_eq!(ctx.map_type_name("Int"), "number");
8185 assert_eq!(ctx.map_type_name("Float"), "number");
8186 assert_eq!(ctx.map_type_name("Bool"), "boolean");
8187 assert_eq!(ctx.map_type_name("String"), "string");
8188 assert_eq!(ctx.map_type_name("Void"), "void");
8189 assert_eq!(ctx.map_type_name("Unit"), "void");
8190 assert_eq!(ctx.map_type_name("List"), "Array");
8191 assert_eq!(ctx.map_type_name("CustomType"), "CustomType");
8192 }
8193
8194 #[test]
8195 fn optional_type_emitted() {
8196 let ctx = TsEmitCtx::new();
8200 let opt = node(
8201 1,
8202 NodeKind::TypeOptional {
8203 inner: Box::new(type_node(2, "String")),
8204 },
8205 );
8206 assert_eq!(ctx.type_to_ts(&opt), "BockOption<string>");
8207 }
8208
8209 #[test]
8210 fn generic_type_args() {
8211 let ctx = TsEmitCtx::new();
8212 let list_of_int = node(
8213 1,
8214 NodeKind::TypeNamed {
8215 path: type_path(&["List"]),
8216 args: vec![type_node(2, "Int")],
8217 },
8218 );
8219 assert_eq!(ctx.type_to_ts(&list_of_int), "Array<number>");
8220 }
8221
8222 #[test]
8223 fn function_type() {
8224 let ctx = TsEmitCtx::new();
8225 let fn_type = node(
8226 1,
8227 NodeKind::TypeFunction {
8228 params: vec![type_node(2, "Int"), type_node(3, "String")],
8229 ret: Box::new(type_node(4, "Bool")),
8230 effects: vec![],
8231 },
8232 );
8233 assert_eq!(
8234 ctx.type_to_ts(&fn_type),
8235 "(arg0: number, arg1: string) => boolean"
8236 );
8237 }
8238
8239 #[test]
8240 fn tuple_type() {
8241 let ctx = TsEmitCtx::new();
8242 let tuple = node(
8243 1,
8244 NodeKind::TypeTuple {
8245 elems: vec![type_node(2, "Int"), type_node(3, "String")],
8246 },
8247 );
8248 assert_eq!(ctx.type_to_ts(&tuple), "[number, string]");
8249 }
8250
8251 #[test]
8254 fn const_with_type() {
8255 let c = node(
8256 1,
8257 NodeKind::ConstDecl {
8258 annotations: vec![],
8259 visibility: Visibility::Public,
8260 name: ident("PI"),
8261 ty: Box::new(type_node(2, "Float")),
8262 value: Box::new(node(
8263 3,
8264 NodeKind::Literal {
8265 lit: Literal::Float("3.14159".into()),
8266 },
8267 )),
8268 },
8269 );
8270 let out = gen(&module(vec![], vec![c]));
8271 assert!(
8272 out.contains("export const PI: number = 3.14159;"),
8273 "got: {out}"
8274 );
8275 }
8276
8277 #[test]
8280 fn class_with_typed_fields() {
8281 let class = node(
8282 1,
8283 NodeKind::ClassDecl {
8284 annotations: vec![],
8285 visibility: Visibility::Public,
8286 name: ident("Point"),
8287 generic_params: vec![],
8288 base: None,
8289 traits: vec![],
8290 fields: vec![
8291 make_record_field("x", "Float"),
8292 make_record_field("y", "Float"),
8293 ],
8294 methods: vec![],
8295 },
8296 );
8297 let out = gen(&module(vec![], vec![class]));
8298 assert!(out.contains("export class Point"), "got: {out}");
8299 assert!(out.contains("x: number;"), "got: {out}");
8300 assert!(out.contains("y: number;"), "got: {out}");
8301 assert!(
8302 out.contains("constructor(x: number, y: number)"),
8303 "got: {out}"
8304 );
8305 }
8306
8307 #[test]
8312 fn class_literal_constructs_positionally() {
8313 let class = node(
8314 1,
8315 NodeKind::ClassDecl {
8316 annotations: vec![],
8317 visibility: Visibility::Public,
8318 name: ident("Button"),
8319 generic_params: vec![],
8320 base: None,
8321 traits: vec![],
8322 fields: vec![
8323 make_record_field("label", "String"),
8324 make_record_field("on_click", "String"),
8325 ],
8326 methods: vec![],
8327 },
8328 );
8329 let construct = node(
8332 10,
8333 NodeKind::RecordConstruct {
8334 path: type_path(&["Button"]),
8335 fields: vec![
8336 AirRecordField {
8337 name: ident("on_click"),
8338 value: Some(Box::new(str_lit(11, "click"))),
8339 },
8340 AirRecordField {
8341 name: ident("label"),
8342 value: Some(Box::new(str_lit(12, "Submit"))),
8343 },
8344 ],
8345 spread: None,
8346 },
8347 );
8348 let main_fn = node(
8349 20,
8350 NodeKind::FnDecl {
8351 annotations: vec![],
8352 visibility: Visibility::Private,
8353 is_async: false,
8354 name: ident("main"),
8355 generic_params: vec![],
8356 params: vec![],
8357 return_type: None,
8358 effect_clause: vec![],
8359 where_clause: vec![],
8360 body: Box::new(block(
8361 21,
8362 vec![node(
8363 22,
8364 NodeKind::LetBinding {
8365 is_mut: false,
8366 pattern: Box::new(bind_pat(23, "b")),
8367 ty: None,
8368 value: Box::new(construct),
8369 },
8370 )],
8371 None,
8372 )),
8373 },
8374 );
8375 let out = gen(&module(vec![], vec![class, main_fn]));
8376 assert!(
8377 out.contains(r#"new Button("Submit", "click")"#),
8378 "expected positional `new Button(...)` in declaration order, got:\n{out}"
8379 );
8380 assert!(
8381 !out.contains("{ label:"),
8382 "class literal must not emit a bare object literal:\n{out}"
8383 );
8384 }
8385
8386 #[test]
8389 fn effect_becomes_interface() {
8390 let op = node(
8391 2,
8392 NodeKind::FnDecl {
8393 annotations: vec![],
8394 visibility: Visibility::Public,
8395 is_async: false,
8396 name: ident("log"),
8397 generic_params: vec![],
8398 params: vec![typed_param_node(3, "msg", "String")],
8399 return_type: Some(Box::new(type_node(4, "Void"))),
8400 effect_clause: vec![],
8401 where_clause: vec![],
8402 body: Box::new(block(5, vec![], None)),
8403 },
8404 );
8405 let effect = node(
8406 1,
8407 NodeKind::EffectDecl {
8408 annotations: vec![],
8409 visibility: Visibility::Public,
8410 name: ident("Logger"),
8411 generic_params: vec![],
8412 components: vec![],
8413 operations: vec![op],
8414 },
8415 );
8416 let out = gen(&module(vec![], vec![effect]));
8417 assert!(out.contains("interface Logger"), "got: {out}");
8418 assert!(out.contains("log(msg: string): void"), "got: {out}");
8419 }
8420
8421 #[test]
8424 fn ownership_erased() {
8425 let move_expr = node(
8426 1,
8427 NodeKind::Move {
8428 expr: Box::new(id_node(2, "x")),
8429 },
8430 );
8431 let borrow_expr = node(
8432 3,
8433 NodeKind::Borrow {
8434 expr: Box::new(id_node(4, "y")),
8435 },
8436 );
8437 let stmts = vec![
8438 node(
8439 5,
8440 NodeKind::LetBinding {
8441 is_mut: false,
8442 pattern: Box::new(bind_pat(6, "a")),
8443 ty: None,
8444 value: Box::new(move_expr),
8445 },
8446 ),
8447 node(
8448 7,
8449 NodeKind::LetBinding {
8450 is_mut: false,
8451 pattern: Box::new(bind_pat(8, "b")),
8452 ty: None,
8453 value: Box::new(borrow_expr),
8454 },
8455 ),
8456 ];
8457 let f = node(
8458 9,
8459 NodeKind::FnDecl {
8460 annotations: vec![],
8461 visibility: Visibility::Private,
8462 is_async: false,
8463 name: ident("test"),
8464 generic_params: vec![],
8465 params: vec![],
8466 return_type: None,
8467 effect_clause: vec![],
8468 where_clause: vec![],
8469 body: Box::new(block(10, stmts, None)),
8470 },
8471 );
8472 let out = gen(&module(vec![], vec![f]));
8473 assert!(out.contains("const a = x;"), "got: {out}");
8474 assert!(out.contains("const b = y;"), "got: {out}");
8475 }
8476
8477 #[test]
8480 fn string_interpolation() {
8481 let interp = node(
8482 1,
8483 NodeKind::Interpolation {
8484 parts: vec![
8485 AirInterpolationPart::Literal("Hello, ".into()),
8486 AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
8487 AirInterpolationPart::Literal("!".into()),
8488 ],
8489 },
8490 );
8491 let stmt = node(
8492 3,
8493 NodeKind::LetBinding {
8494 is_mut: false,
8495 pattern: Box::new(bind_pat(4, "msg")),
8496 ty: None,
8497 value: Box::new(interp),
8498 },
8499 );
8500 let f = node(
8501 5,
8502 NodeKind::FnDecl {
8503 annotations: vec![],
8504 visibility: Visibility::Private,
8505 is_async: false,
8506 name: ident("test"),
8507 generic_params: vec![],
8508 params: vec![],
8509 return_type: None,
8510 effect_clause: vec![],
8511 where_clause: vec![],
8512 body: Box::new(block(6, vec![stmt], None)),
8513 },
8514 );
8515 let out = gen(&module(vec![], vec![f]));
8516 assert!(out.contains("`Hello, ${__bockStr(name)}!`"), "got: {out}");
8519 }
8520
8521 #[test]
8524 fn collections() {
8525 let list = node(
8526 1,
8527 NodeKind::ListLiteral {
8528 elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
8529 },
8530 );
8531 let map = node(
8532 5,
8533 NodeKind::MapLiteral {
8534 entries: vec![bock_air::AirMapEntry {
8535 key: str_lit(6, "a"),
8536 value: int_lit(7, "1"),
8537 }],
8538 },
8539 );
8540 let set = node(
8541 8,
8542 NodeKind::SetLiteral {
8543 elems: vec![int_lit(9, "1"), int_lit(10, "2")],
8544 },
8545 );
8546 let stmts = vec![
8547 node(
8548 11,
8549 NodeKind::LetBinding {
8550 is_mut: false,
8551 pattern: Box::new(bind_pat(12, "xs")),
8552 ty: None,
8553 value: Box::new(list),
8554 },
8555 ),
8556 node(
8557 13,
8558 NodeKind::LetBinding {
8559 is_mut: false,
8560 pattern: Box::new(bind_pat(14, "m")),
8561 ty: None,
8562 value: Box::new(map),
8563 },
8564 ),
8565 node(
8566 15,
8567 NodeKind::LetBinding {
8568 is_mut: false,
8569 pattern: Box::new(bind_pat(16, "s")),
8570 ty: None,
8571 value: Box::new(set),
8572 },
8573 ),
8574 ];
8575 let f = node(
8576 17,
8577 NodeKind::FnDecl {
8578 annotations: vec![],
8579 visibility: Visibility::Private,
8580 is_async: false,
8581 name: ident("test"),
8582 generic_params: vec![],
8583 params: vec![],
8584 return_type: None,
8585 effect_clause: vec![],
8586 where_clause: vec![],
8587 body: Box::new(block(18, stmts, None)),
8588 },
8589 );
8590 let out = gen(&module(vec![], vec![f]));
8591 assert!(out.contains("[1, 2, 3]"), "got: {out}");
8592 assert!(out.contains("new Map("), "got: {out}");
8593 assert!(out.contains("new Set("), "got: {out}");
8594 }
8595
8596 #[test]
8599 fn result_construct_has_as_const() {
8600 let ok = node(
8601 1,
8602 NodeKind::ResultConstruct {
8603 variant: ResultVariant::Ok,
8604 value: Some(Box::new(int_lit(2, "42"))),
8605 },
8606 );
8607 let stmt = node(
8608 3,
8609 NodeKind::LetBinding {
8610 is_mut: false,
8611 pattern: Box::new(bind_pat(4, "r")),
8612 ty: None,
8613 value: Box::new(ok),
8614 },
8615 );
8616 let f = node(
8617 5,
8618 NodeKind::FnDecl {
8619 annotations: vec![],
8620 visibility: Visibility::Private,
8621 is_async: false,
8622 name: ident("test"),
8623 generic_params: vec![],
8624 params: vec![],
8625 return_type: None,
8626 effect_clause: vec![],
8627 where_clause: vec![],
8628 body: Box::new(block(6, vec![stmt], None)),
8629 },
8630 );
8631 let out = gen(&module(vec![], vec![f]));
8632 assert!(
8634 out.contains("{ _tag: \"Ok\" as const, _0: 42 }"),
8635 "got: {out}"
8636 );
8637 }
8638
8639 #[test]
8642 fn record_construct() {
8643 let rc = node(
8644 1,
8645 NodeKind::RecordConstruct {
8646 path: type_path(&["Point"]),
8647 fields: vec![
8648 AirRecordField {
8649 name: ident("x"),
8650 value: Some(Box::new(int_lit(2, "1"))),
8651 },
8652 AirRecordField {
8653 name: ident("y"),
8654 value: Some(Box::new(int_lit(3, "2"))),
8655 },
8656 ],
8657 spread: None,
8658 },
8659 );
8660 let stmt = node(
8661 4,
8662 NodeKind::LetBinding {
8663 is_mut: false,
8664 pattern: Box::new(bind_pat(5, "p")),
8665 ty: None,
8666 value: Box::new(rc),
8667 },
8668 );
8669 let f = node(
8670 6,
8671 NodeKind::FnDecl {
8672 annotations: vec![],
8673 visibility: Visibility::Private,
8674 is_async: false,
8675 name: ident("test"),
8676 generic_params: vec![],
8677 params: vec![],
8678 return_type: None,
8679 effect_clause: vec![],
8680 where_clause: vec![],
8681 body: Box::new(block(7, vec![stmt], None)),
8682 },
8683 );
8684 let out = gen(&module(vec![], vec![f]));
8685 assert!(out.contains("{ x: 1, y: 2 }"), "got: {out}");
8686 }
8687
8688 #[test]
8689 fn to_camel_case_converts_snake_case() {
8690 assert_eq!(to_camel_case("create_user"), "createUser");
8691 assert_eq!(to_camel_case("get_all_items"), "getAllItems");
8692 assert_eq!(to_camel_case("Log"), "log");
8693 assert_eq!(to_camel_case("createUser"), "createUser");
8694 assert_eq!(to_camel_case("_"), "_");
8695 assert_eq!(to_camel_case(""), "");
8696 }
8697
8698 #[test]
8699 fn snake_case_fn_becomes_camel_case_ts() {
8700 let body = block(2, vec![], Some(int_lit(3, "42")));
8701 let f = node(
8702 1,
8703 NodeKind::FnDecl {
8704 annotations: vec![],
8705 visibility: Visibility::Private,
8706 is_async: false,
8707 name: ident("create_user"),
8708 generic_params: vec![],
8709 params: vec![typed_param_node(4, "name", "String")],
8710 return_type: Some(Box::new(type_node(5, "Int"))),
8711 effect_clause: vec![],
8712 where_clause: vec![],
8713 body: Box::new(body),
8714 },
8715 );
8716 let out = gen(&module(vec![], vec![f]));
8717 assert!(
8718 out.contains("function createUser("),
8719 "expected camelCase function name, got: {out}"
8720 );
8721 assert!(
8722 out.contains("name: string"),
8723 "expected type annotations, got: {out}"
8724 );
8725 }
8726
8727 fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
8731 let call = node(
8732 10,
8733 NodeKind::Call {
8734 callee: Box::new(id_node(11, func_name)),
8735 args: vec![AirArg {
8736 label: None,
8737 value: arg,
8738 }],
8739 type_args: vec![],
8740 },
8741 );
8742 let body = block(2, vec![call], None);
8743 let f = node(
8744 1,
8745 NodeKind::FnDecl {
8746 name: ident("main"),
8747 params: vec![],
8748 return_type: None,
8749 body: Box::new(body),
8750 generic_params: vec![],
8751 visibility: Visibility::Private,
8752 annotations: vec![],
8753 effect_clause: vec![],
8754 where_clause: vec![],
8755 is_async: false,
8756 },
8757 );
8758 gen(&module(vec![], vec![f]))
8759 }
8760
8761 fn gen_prelude_call_no_args(func_name: &str) -> String {
8763 let call = node(
8764 10,
8765 NodeKind::Call {
8766 callee: Box::new(id_node(11, func_name)),
8767 args: vec![],
8768 type_args: vec![],
8769 },
8770 );
8771 let body = block(2, vec![call], None);
8772 let f = node(
8773 1,
8774 NodeKind::FnDecl {
8775 name: ident("main"),
8776 params: vec![],
8777 return_type: None,
8778 body: Box::new(body),
8779 generic_params: vec![],
8780 visibility: Visibility::Private,
8781 annotations: vec![],
8782 effect_clause: vec![],
8783 where_clause: vec![],
8784 is_async: false,
8785 },
8786 );
8787 gen(&module(vec![], vec![f]))
8788 }
8789
8790 #[test]
8791 fn prelude_println_maps_to_console_log() {
8792 let out = gen_prelude_call("println", str_lit(12, "hello"));
8793 assert!(
8794 out.contains("console.log("),
8795 "println should map to console.log, got: {out}"
8796 );
8797 assert!(
8798 !out.contains("println("),
8799 "should not emit bare println(, got: {out}"
8800 );
8801 }
8802
8803 #[test]
8804 fn prelude_print_maps_to_process_stdout() {
8805 let out = gen_prelude_call("print", str_lit(12, "hello"));
8806 assert!(
8807 out.contains("process.stdout.write(String("),
8808 "print should map to process.stdout.write, got: {out}"
8809 );
8810 }
8811
8812 #[test]
8813 fn prelude_debug_maps_to_console_debug() {
8814 let out = gen_prelude_call("debug", str_lit(12, "val"));
8815 assert!(
8816 out.contains("console.debug("),
8817 "debug should map to console.debug, got: {out}"
8818 );
8819 }
8820
8821 #[test]
8822 fn prelude_assert_maps_to_throw() {
8823 let arg = node(
8824 12,
8825 NodeKind::Literal {
8826 lit: Literal::Bool(true),
8827 },
8828 );
8829 let out = gen_prelude_call("assert", arg);
8830 assert!(
8831 out.contains("if (!true) throw new Error(\"assertion failed\")"),
8832 "assert should map to if-throw, got: {out}"
8833 );
8834 }
8835
8836 #[test]
8837 fn prelude_todo_maps_to_throw_not_implemented() {
8838 let out = gen_prelude_call_no_args("todo");
8839 assert!(
8840 out.contains("throw new Error(\"not implemented\")"),
8841 "todo should map to throw, got: {out}"
8842 );
8843 }
8844
8845 #[test]
8846 fn prelude_unreachable_maps_to_throw_unreachable() {
8847 let out = gen_prelude_call_no_args("unreachable");
8848 assert!(
8849 out.contains("throw new Error(\"unreachable\")"),
8850 "unreachable should map to throw, got: {out}"
8851 );
8852 }
8853
8854 #[test]
8855 fn non_prelude_call_passes_through() {
8856 let out = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
8857 assert!(
8858 out.contains("myCustomFunc("),
8859 "non-prelude call should use camelCase, got: {out}"
8860 );
8861 }
8862
8863 #[test]
8864 fn handling_block_passes_handlers_to_effectful_call() {
8865 use bock_air::AirHandlerPair;
8866
8867 let effect_decl = node(
8868 1,
8869 NodeKind::EffectDecl {
8870 annotations: vec![],
8871 visibility: Visibility::Public,
8872 name: ident("Logger"),
8873 generic_params: vec![],
8874 components: vec![],
8875 operations: vec![node(
8876 2,
8877 NodeKind::FnDecl {
8878 annotations: vec![],
8879 visibility: Visibility::Public,
8880 is_async: false,
8881 name: ident("log"),
8882 generic_params: vec![],
8883 params: vec![typed_param_node(3, "msg", "String")],
8884 return_type: None,
8885 effect_clause: vec![],
8886 where_clause: vec![],
8887 body: Box::new(block(4, vec![], None)),
8888 },
8889 )],
8890 },
8891 );
8892
8893 let inner_fn = node(
8894 10,
8895 NodeKind::FnDecl {
8896 annotations: vec![],
8897 visibility: Visibility::Private,
8898 is_async: false,
8899 name: ident("inner"),
8900 generic_params: vec![],
8901 params: vec![],
8902 return_type: None,
8903 effect_clause: vec![type_path(&["Logger"])],
8904 where_clause: vec![],
8905 body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
8906 },
8907 );
8908
8909 let call_inner = node(
8910 20,
8911 NodeKind::Call {
8912 callee: Box::new(id_node(21, "inner")),
8913 args: vec![],
8914 type_args: vec![],
8915 },
8916 );
8917 let handling = node(
8918 30,
8919 NodeKind::HandlingBlock {
8920 handlers: vec![AirHandlerPair {
8921 effect: type_path(&["Logger"]),
8922 handler: Box::new(node(
8923 31,
8924 NodeKind::Call {
8925 callee: Box::new(id_node(32, "StdoutLogger")),
8926 args: vec![],
8927 type_args: vec![],
8928 },
8929 )),
8930 }],
8931 body: Box::new(block(33, vec![], Some(call_inner))),
8932 },
8933 );
8934 let main_fn = node(
8935 40,
8936 NodeKind::FnDecl {
8937 annotations: vec![],
8938 visibility: Visibility::Private,
8939 is_async: false,
8940 name: ident("main"),
8941 generic_params: vec![],
8942 params: vec![],
8943 return_type: None,
8944 effect_clause: vec![],
8945 where_clause: vec![],
8946 body: Box::new(block(41, vec![handling], None)),
8947 },
8948 );
8949
8950 let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
8951 assert!(
8953 out.contains("inner({ logger: __logger })"),
8954 "handling block should pass handler to effectful call, got: {out}"
8955 );
8956 assert!(
8957 out.contains("const __logger: Logger = stdoutLogger()"),
8958 "handling block should instantiate handler with type, got: {out}"
8959 );
8960 }
8961
8962 #[test]
8963 fn sibling_handling_blocks_do_not_share_let_scope() {
8964 use bock_air::AirHandlerPair;
8965
8966 let make_handling = |id: u32, val: &str| {
8973 node(
8974 id,
8975 NodeKind::HandlingBlock {
8976 handlers: vec![AirHandlerPair {
8977 effect: type_path(&["Logger"]),
8978 handler: Box::new(node(
8979 id + 1,
8980 NodeKind::Call {
8981 callee: Box::new(id_node(id + 2, "StdoutLogger")),
8982 args: vec![],
8983 type_args: vec![],
8984 },
8985 )),
8986 }],
8987 body: Box::new(block(
8988 id + 3,
8989 vec![node(
8990 id + 4,
8991 NodeKind::LetBinding {
8992 is_mut: false,
8993 pattern: Box::new(bind_pat(id + 5, "part")),
8994 ty: None,
8995 value: Box::new(str_lit(id + 6, val)),
8996 },
8997 )],
8998 Some(id_node(id + 7, "part")),
8999 )),
9000 },
9001 )
9002 };
9003 let main_fn = node(
9004 40,
9005 NodeKind::FnDecl {
9006 annotations: vec![],
9007 visibility: Visibility::Private,
9008 is_async: false,
9009 name: ident("main"),
9010 generic_params: vec![],
9011 params: vec![],
9012 return_type: None,
9013 effect_clause: vec![],
9014 where_clause: vec![],
9015 body: Box::new(block(
9016 41,
9017 vec![make_handling(50, "first"), make_handling(70, "second")],
9018 None,
9019 )),
9020 },
9021 );
9022
9023 let out = gen(&module(vec![], vec![main_fn]));
9024 assert_eq!(
9025 out.matches("const part = ").count(),
9026 2,
9027 "each sibling handling block should declare its own `const part`, got: {out}"
9028 );
9029 assert!(
9030 !out.contains("\n part = "),
9031 "no sibling handling block may rewrite its `let part` into a bare \
9032 assignment, got: {out}"
9033 );
9034 }
9035
9036 #[test]
9037 fn record_becomes_class() {
9038 let rec = node(
9039 1,
9040 NodeKind::RecordDecl {
9041 annotations: vec![],
9042 visibility: Visibility::Public,
9043 name: ident("ConsoleLogger"),
9044 generic_params: vec![],
9045 fields: vec![],
9046 },
9047 );
9048 let out = gen(&module(vec![], vec![rec]));
9049 assert!(
9050 out.contains("export class ConsoleLogger {}"),
9051 "empty record should be an empty exported class, got: {out}"
9052 );
9053 }
9054
9055 #[test]
9056 fn impl_emits_interface_extension_for_declaration_merging() {
9057 use bock_air::AirHandlerPair;
9058 let _ = AirHandlerPair {
9059 effect: type_path(&["X"]),
9060 handler: Box::new(id_node(0, "x")),
9061 };
9062
9063 let effect_decl = node(
9064 1,
9065 NodeKind::EffectDecl {
9066 annotations: vec![],
9067 visibility: Visibility::Public,
9068 name: ident("Logger"),
9069 generic_params: vec![],
9070 components: vec![],
9071 operations: vec![node(
9072 2,
9073 NodeKind::FnDecl {
9074 annotations: vec![],
9075 visibility: Visibility::Public,
9076 is_async: false,
9077 name: ident("log"),
9078 generic_params: vec![],
9079 params: vec![typed_param_node(3, "msg", "String")],
9080 return_type: None,
9081 effect_clause: vec![],
9082 where_clause: vec![],
9083 body: Box::new(block(4, vec![], None)),
9084 },
9085 )],
9086 },
9087 );
9088
9089 let rec = node(
9090 5,
9091 NodeKind::RecordDecl {
9092 annotations: vec![],
9093 visibility: Visibility::Public,
9094 name: ident("StdLogger"),
9095 generic_params: vec![],
9096 fields: vec![],
9097 },
9098 );
9099
9100 let impl_block = node(
9101 10,
9102 NodeKind::ImplBlock {
9103 annotations: vec![],
9104 trait_path: Some(type_path(&["Logger"])),
9105 trait_args: vec![],
9106 target: Box::new(type_node(11, "StdLogger")),
9107 generic_params: vec![],
9108 methods: vec![node(
9109 12,
9110 NodeKind::FnDecl {
9111 annotations: vec![],
9112 visibility: Visibility::Public,
9113 is_async: false,
9114 name: ident("log"),
9115 generic_params: vec![],
9116 params: vec![
9121 untyped_param_node(15, "self"),
9122 typed_param_node(13, "msg", "String"),
9123 ],
9124 return_type: None,
9125 effect_clause: vec![],
9126 where_clause: vec![],
9127 body: Box::new(block(14, vec![], None)),
9128 },
9129 )],
9130 where_clause: vec![],
9131 },
9132 );
9133
9134 let out = gen(&module(vec![], vec![effect_decl, rec, impl_block]));
9135 assert!(
9136 out.contains("interface StdLogger extends Logger {"),
9137 "impl should emit interface extension for declaration merging, got: {out}"
9138 );
9139 assert!(
9142 out.contains("log(msg: string): void;"),
9143 "merged interface should declare the method signature, got: {out}"
9144 );
9145 assert!(
9146 out.contains("StdLogger.prototype.log"),
9147 "impl should attach method to prototype, got: {out}"
9148 );
9149 }
9150
9151 #[test]
9152 fn generic_trait_impl_extends_clause_carries_type_args() {
9153 let rec = node(
9157 1,
9158 NodeKind::RecordDecl {
9159 annotations: vec![],
9160 visibility: Visibility::Public,
9161 name: ident("R"),
9162 generic_params: vec![make_generic_param("T")],
9163 fields: vec![make_record_field("v", "T")],
9164 },
9165 );
9166 let impl_block = node(
9167 10,
9168 NodeKind::ImplBlock {
9169 annotations: vec![],
9170 trait_path: Some(type_path(&["P"])),
9171 trait_args: vec![node(
9172 11,
9173 NodeKind::TypeNamed {
9174 path: type_path(&["T"]),
9175 args: vec![],
9176 },
9177 )],
9178 target: Box::new(node(
9179 12,
9180 NodeKind::TypeNamed {
9181 path: type_path(&["R"]),
9182 args: vec![node(
9183 13,
9184 NodeKind::TypeNamed {
9185 path: type_path(&["T"]),
9186 args: vec![],
9187 },
9188 )],
9189 },
9190 )),
9191 generic_params: vec![],
9192 methods: vec![node(
9193 14,
9194 NodeKind::FnDecl {
9195 annotations: vec![],
9196 visibility: Visibility::Public,
9197 is_async: false,
9198 name: ident("f"),
9199 generic_params: vec![],
9200 params: vec![untyped_param_node(15, "self")],
9201 return_type: None,
9202 effect_clause: vec![],
9203 where_clause: vec![],
9204 body: Box::new(block(16, vec![], None)),
9205 },
9206 )],
9207 where_clause: vec![],
9208 },
9209 );
9210 let out = gen(&module(vec![], vec![rec, impl_block]));
9211 assert!(
9212 out.contains("interface R<T> extends P<T> {"),
9213 "generic trait impl's extends clause must carry `<T>`, got: {out}"
9214 );
9215 }
9216
9217 #[test]
9218 fn optional_named_type_maps_to_bock_option() {
9219 let ctx = TsEmitCtx::new();
9223 let opt = node(
9224 1,
9225 NodeKind::TypeNamed {
9226 path: type_path(&["Optional"]),
9227 args: vec![node(
9228 2,
9229 NodeKind::TypeNamed {
9230 path: type_path(&["String"]),
9231 args: vec![],
9232 },
9233 )],
9234 },
9235 );
9236 assert_eq!(ctx.type_to_ts(&opt), "BockOption<string>");
9237 }
9238
9239 #[test]
9240 fn impl_self_method_typed_and_declaration_merged() {
9241 let rec = node(
9247 1,
9248 NodeKind::RecordDecl {
9249 annotations: vec![],
9250 visibility: Visibility::Public,
9251 name: ident("Point"),
9252 generic_params: vec![],
9253 fields: vec![make_record_field("x", "Int"), make_record_field("y", "Int")],
9254 },
9255 );
9256 let body = block(
9257 20,
9258 vec![],
9259 Some(node(
9260 21,
9261 NodeKind::BinaryOp {
9262 op: BinOp::Add,
9263 left: Box::new(node(
9264 22,
9265 NodeKind::FieldAccess {
9266 object: Box::new(id_node(23, "self")),
9267 field: ident("x"),
9268 },
9269 )),
9270 right: Box::new(node(
9271 24,
9272 NodeKind::FieldAccess {
9273 object: Box::new(id_node(25, "self")),
9274 field: ident("y"),
9275 },
9276 )),
9277 },
9278 )),
9279 );
9280 let impl_block = node(
9281 10,
9282 NodeKind::ImplBlock {
9283 annotations: vec![],
9284 trait_path: None,
9285 trait_args: vec![],
9286 target: Box::new(type_node(11, "Point")),
9287 generic_params: vec![],
9288 methods: vec![node(
9289 12,
9290 NodeKind::FnDecl {
9291 annotations: vec![],
9292 visibility: Visibility::Public,
9293 is_async: false,
9294 name: ident("sum"),
9295 generic_params: vec![],
9296 params: vec![untyped_param_node(13, "self")],
9297 return_type: Some(Box::new(type_node(14, "Int"))),
9298 effect_clause: vec![],
9299 where_clause: vec![],
9300 body: Box::new(body),
9301 },
9302 )],
9303 where_clause: vec![],
9304 },
9305 );
9306 let out = gen(&module(vec![], vec![rec, impl_block]));
9307 assert!(
9308 out.contains("interface Point {"),
9309 "inherent impl should emit a declaration-merging interface, got: {out}"
9310 );
9311 assert!(
9312 out.contains("sum(self: Point): number;"),
9313 "merged interface should declare the self-typed method, got: {out}"
9314 );
9315 assert!(
9316 out.contains("Point.prototype.sum = function(self: Point): number {"),
9317 "prototype function should type the self param as the target, got: {out}"
9318 );
9319 }
9320
9321 #[test]
9331 fn self_in_plain_impl_resolves_to_target_not_this() {
9332 let rec = node(
9333 1,
9334 NodeKind::RecordDecl {
9335 annotations: vec![],
9336 visibility: Visibility::Public,
9337 name: ident("Counter"),
9338 generic_params: vec![],
9339 fields: vec![make_record_field("value", "Int")],
9340 },
9341 );
9342 let other_param = node(
9343 30,
9344 NodeKind::Param {
9345 pattern: Box::new(bind_pat(31, "other")),
9346 ty: Some(Box::new(node(32, NodeKind::TypeSelf))),
9347 default: None,
9348 },
9349 );
9350 let impl_block = node(
9351 10,
9352 NodeKind::ImplBlock {
9353 annotations: vec![],
9354 trait_path: None,
9355 trait_args: vec![],
9356 target: Box::new(type_node(11, "Counter")),
9357 generic_params: vec![],
9358 methods: vec![node(
9359 12,
9360 NodeKind::FnDecl {
9361 annotations: vec![],
9362 visibility: Visibility::Public,
9363 is_async: false,
9364 name: ident("combine"),
9365 generic_params: vec![],
9366 params: vec![untyped_param_node(13, "self"), other_param],
9367 return_type: Some(Box::new(node(14, NodeKind::TypeSelf))),
9368 effect_clause: vec![],
9369 where_clause: vec![],
9370 body: Box::new(block(15, vec![], None)),
9371 },
9372 )],
9373 where_clause: vec![],
9374 },
9375 );
9376 let out = gen(&module(vec![], vec![rec, impl_block]));
9377 assert!(
9378 !out.contains(": this"),
9379 "Self must not lower to the `this` type (TS2526), got: {out}"
9380 );
9381 assert!(
9382 out.contains("combine(self: Counter, other: Counter): Counter;"),
9383 "merged interface should render Self as the target in param & return, got: {out}"
9384 );
9385 assert!(
9386 out.contains(
9387 "Counter.prototype.combine = function(self: Counter, other: Counter): Counter {"
9388 ),
9389 "prototype function should render Self as the target in param & return, got: {out}"
9390 );
9391 }
9392
9393 #[test]
9394 fn optional_runtime_prelude_and_value_type_agree() {
9395 let body = block(
9400 20,
9401 vec![],
9402 Some(node(
9403 21,
9404 NodeKind::Call {
9405 callee: Box::new(id_node(22, "Some")),
9406 args: vec![AirArg {
9407 label: None,
9408 value: int_lit(23, "7"),
9409 }],
9410 type_args: vec![],
9411 },
9412 )),
9413 );
9414 let f = node(
9415 1,
9416 NodeKind::FnDecl {
9417 annotations: vec![],
9418 visibility: Visibility::Private,
9419 is_async: false,
9420 name: ident("pick"),
9421 generic_params: vec![],
9422 params: vec![],
9423 return_type: Some(Box::new(node(
9424 2,
9425 NodeKind::TypeOptional {
9426 inner: Box::new(type_node(3, "Int")),
9427 },
9428 ))),
9429 effect_clause: vec![],
9430 where_clause: vec![],
9431 body: Box::new(body),
9432 },
9433 );
9434 let out = gen(&module(vec![], vec![f]));
9435 assert!(
9436 out.contains("type BockOption<T> ="),
9437 "Optional runtime type prelude should be emitted, got: {out}"
9438 );
9439 assert!(
9440 out.contains("): BockOption<number> {"),
9441 "Optional return type should be BockOption<number>, got: {out}"
9442 );
9443 assert!(
9444 out.contains("{ _tag: \"Some\" as const, _0: 7 }"),
9445 "Some should lower to the matching tagged-object value, got: {out}"
9446 );
9447 }
9448
9449 #[test]
9456 fn match_call_scrutinee_hoisted_to_temp() {
9457 let scrutinee = node(
9459 10,
9460 NodeKind::Call {
9461 callee: Box::new(id_node(11, "f")),
9462 args: vec![],
9463 type_args: vec![],
9464 },
9465 );
9466 let some_arm = node(
9467 20,
9468 NodeKind::MatchArm {
9469 pattern: Box::new(node(
9470 21,
9471 NodeKind::ConstructorPat {
9472 path: type_path(&["Some"]),
9473 fields: vec![bind_pat(22, "x")],
9474 },
9475 )),
9476 guard: None,
9477 body: Box::new(block(23, vec![], Some(id_node(24, "x")))),
9478 },
9479 );
9480 let none_arm = node(
9481 30,
9482 NodeKind::MatchArm {
9483 pattern: Box::new(node(
9484 31,
9485 NodeKind::ConstructorPat {
9486 path: type_path(&["None"]),
9487 fields: vec![],
9488 },
9489 )),
9490 guard: None,
9491 body: Box::new(block(32, vec![], Some(int_lit(33, "0")))),
9492 },
9493 );
9494 let match_stmt = node(
9495 40,
9496 NodeKind::Match {
9497 scrutinee: Box::new(scrutinee),
9498 arms: vec![some_arm, none_arm],
9499 },
9500 );
9501 let f = node(
9502 1,
9503 NodeKind::FnDecl {
9504 annotations: vec![],
9505 visibility: Visibility::Private,
9506 is_async: false,
9507 name: ident("run"),
9508 generic_params: vec![],
9509 params: vec![],
9510 return_type: None,
9511 effect_clause: vec![],
9512 where_clause: vec![],
9513 body: Box::new(block(2, vec![match_stmt], None)),
9514 },
9515 );
9516 let out = gen(&module(vec![], vec![f]));
9517 assert!(
9518 out.contains("const __match1 = f();"),
9519 "call scrutinee should be hoisted to a temp, got: {out}"
9520 );
9521 assert!(
9522 out.contains("switch (__match1._tag)"),
9523 "switch should dispatch on the hoisted temp, got: {out}"
9524 );
9525 assert!(
9526 out.contains(
9527 r#"const x = (__match1 as Extract<typeof __match1, { _tag: "Some" }>)._0;"#
9528 ),
9529 "payload binding should read the hoisted temp through the narrowing cast, got: {out}"
9530 );
9531 assert!(
9533 !out.contains("f()._tag") && !out.contains("f()._0"),
9534 "call scrutinee must not be re-emitted inline, got: {out}"
9535 );
9536 }
9537
9538 #[test]
9546 fn expr_position_value_match_hoists_scrutinee_and_annotates_iife() {
9547 let zero_arm = node(
9549 20,
9550 NodeKind::MatchArm {
9551 pattern: Box::new(node(
9552 21,
9553 NodeKind::LiteralPat {
9554 lit: Literal::Int("0".into()),
9555 },
9556 )),
9557 guard: None,
9558 body: Box::new(block(22, vec![], Some(id_node(23, "n")))),
9559 },
9560 );
9561 let default_arm = node(
9562 30,
9563 NodeKind::MatchArm {
9564 pattern: Box::new(node(31, NodeKind::WildcardPat)),
9565 guard: None,
9566 body: Box::new(block(32, vec![], Some(id_node(33, "n")))),
9567 },
9568 );
9569 let m = node(
9570 40,
9571 NodeKind::Match {
9572 scrutinee: Box::new(id_node(41, "n")),
9573 arms: vec![zero_arm, default_arm],
9574 },
9575 );
9576 let let_flag = node(
9577 50,
9578 NodeKind::LetBinding {
9579 is_mut: false,
9580 pattern: Box::new(bind_pat(51, "flag")),
9581 ty: Some(Box::new(type_node(52, "Int"))),
9582 value: Box::new(m),
9583 },
9584 );
9585 let f = node(
9586 1,
9587 NodeKind::FnDecl {
9588 annotations: vec![],
9589 visibility: Visibility::Private,
9590 is_async: false,
9591 name: ident("classify"),
9592 generic_params: vec![],
9593 params: vec![{
9594 node(
9595 2,
9596 NodeKind::Param {
9597 pattern: Box::new(bind_pat(3, "n")),
9598 ty: Some(Box::new(type_node(4, "Int"))),
9599 default: None,
9600 },
9601 )
9602 }],
9603 return_type: Some(Box::new(type_node(5, "Int"))),
9604 effect_clause: vec![],
9605 where_clause: vec![],
9606 body: Box::new(block(6, vec![let_flag], Some(id_node(7, "flag")))),
9607 },
9608 );
9609 let out = gen(&module(vec![], vec![f]));
9610 assert!(
9611 out.contains("(() : number => {"),
9612 "value-position match IIFE arrow should be annotated with the binding type, got: {out}"
9613 );
9614 assert!(
9615 out.contains("const __match1 = n;"),
9616 "bare-identifier scrutinee must be hoisted in expression position, got: {out}"
9617 );
9618 assert!(
9619 out.contains("switch (__match1)"),
9620 "switch should dispatch on the hoisted temp, not the original binding, got: {out}"
9621 );
9622 }
9623
9624 fn generic_param(id: u32, name: &str) -> GenericParam {
9627 GenericParam {
9628 id,
9629 span: span(),
9630 name: ident(name),
9631 bounds: vec![],
9632 }
9633 }
9634
9635 fn named_type(id: u32, name: &str) -> AIRNode {
9636 node(
9637 id,
9638 NodeKind::TypeNamed {
9639 path: type_path(&[name]),
9640 args: vec![],
9641 },
9642 )
9643 }
9644
9645 fn generic_box_record() -> AIRNode {
9647 node(
9648 10,
9649 NodeKind::RecordDecl {
9650 annotations: vec![],
9651 visibility: Visibility::Private,
9652 name: ident("Box"),
9653 generic_params: vec![generic_param(11, "T")],
9654 fields: vec![bock_ast::RecordDeclField {
9655 id: 12,
9656 span: span(),
9657 name: ident("value"),
9658 ty: TypeExpr::Named {
9659 id: 13,
9660 span: span(),
9661 path: type_path(&["T"]),
9662 args: vec![],
9663 },
9664 default: None,
9665 }],
9666 },
9667 )
9668 }
9669
9670 fn generic_box_impl() -> AIRNode {
9672 let self_param = node(
9673 20,
9674 NodeKind::Param {
9675 pattern: Box::new(bind_pat(21, "self")),
9676 ty: None,
9677 default: None,
9678 },
9679 );
9680 let body = block(
9681 22,
9682 vec![],
9683 Some(node(
9684 23,
9685 NodeKind::Return {
9686 value: Some(Box::new(node(
9687 24,
9688 NodeKind::FieldAccess {
9689 object: Box::new(id_node(25, "self")),
9690 field: ident("value"),
9691 },
9692 ))),
9693 },
9694 )),
9695 );
9696 let method = node(
9697 26,
9698 NodeKind::FnDecl {
9699 annotations: vec![],
9700 visibility: Visibility::Private,
9701 is_async: false,
9702 name: ident("get"),
9703 generic_params: vec![],
9704 params: vec![self_param],
9705 return_type: Some(Box::new(named_type(27, "T"))),
9706 effect_clause: vec![],
9707 where_clause: vec![],
9708 body: Box::new(body),
9709 },
9710 );
9711 node(
9712 30,
9713 NodeKind::ImplBlock {
9714 annotations: vec![],
9715 generic_params: vec![],
9716 trait_path: None,
9717 trait_args: vec![],
9718 target: Box::new(named_type(31, "Box")),
9719 where_clause: vec![],
9720 methods: vec![method],
9721 },
9722 )
9723 }
9724
9725 #[test]
9726 fn generic_impl_merges_onto_generic_class() {
9727 let out = gen(&module(
9731 vec![],
9732 vec![generic_box_record(), generic_box_impl()],
9733 ));
9734 assert!(
9735 out.contains("interface Box<T> {"),
9736 "merged interface should carry `<T>`, got: {out}"
9737 );
9738 assert!(
9739 out.contains("get(self: Box<T>): T;"),
9740 "interface signature should type `self` as `Box<T>`, got: {out}"
9741 );
9742 assert!(
9743 out.contains("Box.prototype.get = function<T>(self: Box<T>): T {"),
9744 "prototype function should re-declare `<T>` and reference `Box.prototype`, got: {out}"
9745 );
9746 }
9747
9748 #[test]
9749 fn method_colliding_with_field_is_disambiguated() {
9750 let trait_decl = node(
9752 1,
9753 NodeKind::TraitDecl {
9754 annotations: vec![],
9755 visibility: Visibility::Public,
9756 is_platform: false,
9757 name: ident("Error"),
9758 generic_params: vec![],
9759 associated_types: vec![],
9760 methods: vec![node(
9761 2,
9762 NodeKind::FnDecl {
9763 annotations: vec![],
9764 visibility: Visibility::Public,
9765 is_async: false,
9766 name: ident("message"),
9767 generic_params: vec![],
9768 params: vec![typed_param_node(3, "self", "Error")],
9769 return_type: Some(Box::new(node(
9770 4,
9771 NodeKind::TypeNamed {
9772 path: type_path(&["String"]),
9773 args: vec![],
9774 },
9775 ))),
9776 effect_clause: vec![],
9777 where_clause: vec![],
9778 body: Box::new(block(5, vec![], None)),
9779 },
9780 )],
9781 },
9782 );
9783 let record_decl = node(
9785 10,
9786 NodeKind::RecordDecl {
9787 annotations: vec![],
9788 visibility: Visibility::Public,
9789 name: ident("SimpleError"),
9790 generic_params: vec![],
9791 fields: vec![make_record_field("message", "String")],
9792 },
9793 );
9794 let method = node(
9796 20,
9797 NodeKind::FnDecl {
9798 annotations: vec![],
9799 visibility: Visibility::Public,
9800 is_async: false,
9801 name: ident("message"),
9802 generic_params: vec![],
9803 params: vec![typed_param_node(21, "self", "SimpleError")],
9804 return_type: Some(Box::new(node(
9805 22,
9806 NodeKind::TypeNamed {
9807 path: type_path(&["String"]),
9808 args: vec![],
9809 },
9810 ))),
9811 effect_clause: vec![],
9812 where_clause: vec![],
9813 body: Box::new(block(
9814 23,
9815 vec![],
9816 Some(node(
9817 24,
9818 NodeKind::FieldAccess {
9819 object: Box::new(id_node(25, "self")),
9820 field: ident("message"),
9821 },
9822 )),
9823 )),
9824 },
9825 );
9826 let impl_block = node(
9827 30,
9828 NodeKind::ImplBlock {
9829 annotations: vec![],
9830 target: Box::new(node(
9831 31,
9832 NodeKind::TypeNamed {
9833 path: type_path(&["SimpleError"]),
9834 args: vec![],
9835 },
9836 )),
9837 trait_path: Some(type_path(&["Error"])),
9838 trait_args: vec![],
9839 generic_params: vec![],
9840 where_clause: vec![],
9841 methods: vec![method],
9842 },
9843 );
9844 let read_fn = node(
9846 40,
9847 NodeKind::FnDecl {
9848 annotations: vec![],
9849 visibility: Visibility::Public,
9850 is_async: false,
9851 name: ident("read"),
9852 generic_params: vec![],
9853 params: vec![typed_param_node(41, "e", "SimpleError")],
9854 return_type: Some(Box::new(node(
9855 42,
9856 NodeKind::TypeNamed {
9857 path: type_path(&["String"]),
9858 args: vec![],
9859 },
9860 ))),
9861 effect_clause: vec![],
9862 where_clause: vec![],
9863 body: Box::new(block(
9864 43,
9865 vec![],
9866 Some(node(
9867 44,
9868 NodeKind::Call {
9869 callee: Box::new(node(
9870 45,
9871 NodeKind::FieldAccess {
9872 object: Box::new(id_node(46, "e")),
9877 field: ident("message"),
9878 },
9879 )),
9880 type_args: vec![],
9881 args: vec![AirArg {
9882 label: None,
9883 value: id_node(46, "e"),
9884 }],
9885 },
9886 )),
9887 )),
9888 },
9889 );
9890 let out = gen(&module(
9891 vec![],
9892 vec![trait_decl, record_decl, impl_block, read_fn],
9893 ));
9894 assert!(
9896 out.contains("message: string;"),
9897 "class field should remain `message: string`, got: {out}"
9898 );
9899 assert!(
9902 out.contains("messageMethod(self: Error): string;"),
9903 "trait interface should declare `messageMethod`, got: {out}"
9904 );
9905 assert!(
9906 out.contains("messageMethod(self: SimpleError): string;"),
9907 "merged interface should declare `messageMethod`, got: {out}"
9908 );
9909 assert!(
9910 out.contains("SimpleError.prototype.messageMethod = "),
9911 "prototype method should be `messageMethod`, got: {out}"
9912 );
9913 assert!(
9915 out.contains(".messageMethod(e)"),
9916 "call site should be `.messageMethod(e)`, got: {out}"
9917 );
9918 assert!(
9919 out.contains("return self.message;"),
9920 "method body should read the field `self.message`, got: {out}"
9921 );
9922 assert!(
9924 !out.contains("message(self: SimpleError)"),
9925 "must NOT emit a `message(...)` method colliding with the field, got: {out}"
9926 );
9927 }
9928
9929 fn diverging_value_if_fn() -> AIRNode {
9934 let then_b = block(2, vec![], Some(int_lit(3, "1")));
9935 let ret = node(
9936 5,
9937 NodeKind::Return {
9938 value: Some(Box::new(int_lit(6, "0"))),
9939 },
9940 );
9941 let else_b = block(4, vec![], Some(ret));
9942 let if_node = node(
9943 1,
9944 NodeKind::If {
9945 let_pattern: None,
9946 condition: Box::new(id_node(7, "c")),
9947 then_block: Box::new(then_b),
9948 else_block: Some(Box::new(else_b)),
9949 },
9950 );
9951 let let_x = node(
9952 10,
9953 NodeKind::LetBinding {
9954 is_mut: false,
9955 pattern: Box::new(bind_pat(11, "x")),
9956 ty: None,
9957 value: Box::new(if_node),
9958 },
9959 );
9960 let body = block(20, vec![let_x], Some(id_node(21, "x")));
9961 let f = node(
9962 30,
9963 NodeKind::FnDecl {
9964 annotations: vec![],
9965 visibility: Visibility::Private,
9966 is_async: false,
9967 name: ident("f"),
9968 generic_params: vec![],
9969 params: vec![],
9970 return_type: None,
9971 effect_clause: vec![],
9972 where_clause: vec![],
9973 body: Box::new(body),
9974 },
9975 );
9976 module(vec![], vec![f])
9977 }
9978
9979 #[test]
9980 fn diverging_value_if_hoists_to_stmt_form_no_iife() {
9981 let out = gen(&diverging_value_if_fn());
9982 assert!(
9983 !out.contains("/* unsupported */"),
9984 "diverging value-if must not emit `/* unsupported */`, got: {out}"
9985 );
9986 assert!(
9987 out.contains("bockCf0 = 1"),
9988 "value arm must assign the temp, got: {out}"
9989 );
9990 assert!(
9991 out.contains("return 0"),
9992 "diverging arm must keep its return (not wrapped in an IIFE), got: {out}"
9993 );
9994 }
9995
9996 fn ts_let_binding(id: u32, name: &str, is_mut: bool, value: AIRNode) -> AIRNode {
10000 node(
10001 id,
10002 NodeKind::LetBinding {
10003 is_mut,
10004 pattern: Box::new(node(
10005 id + 1,
10006 NodeKind::BindPat {
10007 name: ident(name),
10008 is_mut,
10009 },
10010 )),
10011 ty: None,
10012 value: Box::new(value),
10013 },
10014 )
10015 }
10016
10017 fn ts_call(id: u32, callee: &str) -> AIRNode {
10019 node(
10020 id,
10021 NodeKind::Call {
10022 callee: Box::new(id_node(id + 1, callee)),
10023 args: vec![],
10024 type_args: vec![],
10025 },
10026 )
10027 }
10028
10029 fn ts_fn_decl(
10032 id: u32,
10033 name: &str,
10034 params: Vec<AIRNode>,
10035 ret: Option<&str>,
10036 body: AIRNode,
10037 ) -> AIRNode {
10038 node(
10039 id,
10040 NodeKind::FnDecl {
10041 annotations: vec![],
10042 visibility: Visibility::Private,
10043 is_async: false,
10044 name: ident(name),
10045 generic_params: vec![],
10046 params,
10047 return_type: ret.map(|r| Box::new(type_node(id + 500, r))),
10048 effect_clause: vec![],
10049 where_clause: vec![],
10050 body: Box::new(body),
10051 },
10052 )
10053 }
10054
10055 #[test]
10056 fn ts_guard_let_binds_into_enclosing_scope() {
10057 let guard = node(
10059 10,
10060 NodeKind::Guard {
10061 let_pattern: Some(Box::new(node(
10062 11,
10063 NodeKind::ConstructorPat {
10064 path: type_path(&["Ok"]),
10065 fields: vec![bind_pat(12, "guess")],
10066 },
10067 ))),
10068 condition: Box::new(ts_call(13, "parse")),
10069 else_block: Box::new(block(
10070 15,
10071 vec![node(16, NodeKind::Return { value: None })],
10072 None,
10073 )),
10074 },
10075 );
10076 let body = block(2, vec![guard], Some(id_node(20, "guess")));
10077 let out = gen(&module(
10078 vec![],
10079 vec![ts_fn_decl(1, "run", vec![], None, body)],
10080 ));
10081 assert!(
10084 out.contains("const __guard1 = parse();"),
10085 "guard-let must evaluate the scrutinee once into a temp, got: {out}"
10086 );
10087 assert!(
10088 out.contains("if (!(__guard1._tag === \"Ok\"))"),
10089 "guard-let must test the pattern's tag, got: {out}"
10090 );
10091 assert!(
10092 out.contains("const guess = __guard1._0;"),
10093 "guard-let must bind the payload into the enclosing scope, got: {out}"
10094 );
10095 let bind_pos = out.find("const guess = __guard1._0;").unwrap();
10097 let else_pos = out.find("if (!(__guard1._tag").unwrap();
10098 assert!(
10099 bind_pos > else_pos,
10100 "the binding must follow the guard's else, got: {out}"
10101 );
10102 }
10103
10104 #[test]
10105 fn ts_let_shadow_rebinds_to_assignment_not_redeclaration() {
10106 let body = block(
10108 2,
10109 vec![
10110 ts_let_binding(10, "acc", false, int_lit(11, "1")),
10111 ts_let_binding(20, "acc", false, int_lit(21, "2")),
10112 ts_let_binding(30, "acc", false, int_lit(31, "3")),
10113 ],
10114 None,
10115 );
10116 let out = gen(&module(
10117 vec![],
10118 vec![ts_fn_decl(1, "run", vec![], None, body)],
10119 ));
10120 assert!(
10122 out.contains("let acc = 1;"),
10123 "first re-bound `let` should declare with `let`, got: {out}"
10124 );
10125 assert!(
10126 out.contains("acc = 2;") && out.contains("acc = 3;"),
10127 "later bindings should be plain assignments, got: {out}"
10128 );
10129 assert!(
10130 !out.contains("const acc"),
10131 "a re-bound binding must not emit `const acc` (TS2451), got: {out}"
10132 );
10133 assert_eq!(
10135 out.matches("let acc").count(),
10136 1,
10137 "exactly one `let acc` declaration expected, got: {out}"
10138 );
10139 }
10140
10141 #[test]
10142 fn ts_let_shadow_of_param_lowers_to_assignment() {
10143 let body = block(
10146 2,
10147 vec![ts_let_binding(10, "x", false, int_lit(11, "1"))],
10148 Some(id_node(20, "x")),
10149 );
10150 let f = ts_fn_decl(1, "run", vec![typed_param_node(3, "x", "Int")], None, body);
10151 let out = gen(&module(vec![], vec![f]));
10152 assert!(
10153 out.contains("x = 1;") && !out.contains("const x = 1;") && !out.contains("let x = 1;"),
10154 "a `let` shadowing a param must lower to assignment, got: {out}"
10155 );
10156 }
10157
10158 #[test]
10159 fn ts_propagate_let_unwraps_and_early_returns_result() {
10160 let body = block(
10162 2,
10163 vec![ts_let_binding(
10164 10,
10165 "v",
10166 false,
10167 node(
10168 12,
10169 NodeKind::Propagate {
10170 expr: Box::new(ts_call(13, "f")),
10171 },
10172 ),
10173 )],
10174 Some(id_node(20, "v")),
10175 );
10176 let out = gen(&module(
10177 vec![],
10178 vec![ts_fn_decl(1, "run", vec![], Some("Result"), body)],
10179 ));
10180 assert!(
10181 out.contains("const __prop1 = f();"),
10182 "`?` must evaluate the inner once into a temp, got: {out}"
10183 );
10184 assert!(
10186 out.contains("if (__prop1._tag === \"Err\") {"),
10187 "`?` in a Result fn must guard on the Err tag, got: {out}"
10188 );
10189 assert!(
10190 out.contains("return __prop1 as never;"),
10191 "`?` must early-return the failure container, got: {out}"
10192 );
10193 assert!(
10194 out.contains("const v = __prop1._0;"),
10195 "`?` must bind the unwrapped payload, got: {out}"
10196 );
10197 }
10198
10199 #[test]
10200 fn ts_propagate_optional_guards_on_none_tag() {
10201 let body = block(
10203 2,
10204 vec![ts_let_binding(
10205 10,
10206 "v",
10207 false,
10208 node(
10209 12,
10210 NodeKind::Propagate {
10211 expr: Box::new(ts_call(13, "f")),
10212 },
10213 ),
10214 )],
10215 Some(id_node(20, "v")),
10216 );
10217 let out = gen(&module(
10218 vec![],
10219 vec![ts_fn_decl(1, "run", vec![], Some("Optional"), body)],
10220 ));
10221 assert!(
10222 out.contains("if (__prop1._tag === \"None\") {"),
10223 "`?` in an Optional fn must guard on the None tag, got: {out}"
10224 );
10225 }
10226
10227 #[test]
10228 fn ts_propagate_bare_statement_early_returns_discards_payload() {
10229 let body = block(
10232 2,
10233 vec![node(
10234 10,
10235 NodeKind::Propagate {
10236 expr: Box::new(ts_call(11, "f")),
10237 },
10238 )],
10239 Some(ts_call(20, "g")),
10240 );
10241 let out = gen(&module(
10242 vec![],
10243 vec![ts_fn_decl(1, "run", vec![], Some("Result"), body)],
10244 ));
10245 assert!(
10246 out.contains("const __prop1 = f();"),
10247 "a bare `expr?` statement must still hoist + guard, got: {out}"
10248 );
10249 assert!(
10250 out.contains("if (__prop1._tag === \"Err\") {")
10251 && out.contains("return __prop1 as never;"),
10252 "a bare `expr?` must early-return on failure, got: {out}"
10253 );
10254 assert!(
10256 !out.contains("__prop1._0"),
10257 "a bare `expr?` discards the payload (no `._0` use), got: {out}"
10258 );
10259 }
10260
10261 #[test]
10266 fn ts_list_pattern_match_lowers_to_ifchain_with_binds() {
10267 let empty_arm = node(
10269 20,
10270 NodeKind::MatchArm {
10271 pattern: Box::new(node(
10272 21,
10273 NodeKind::ListPat {
10274 elems: vec![],
10275 rest: None,
10276 },
10277 )),
10278 guard: None,
10279 body: Box::new(block(22, vec![], Some(str_lit(23, "empty")))),
10280 },
10281 );
10282 let single_arm = node(
10283 30,
10284 NodeKind::MatchArm {
10285 pattern: Box::new(node(
10286 31,
10287 NodeKind::ListPat {
10288 elems: vec![bind_pat(32, "only")],
10289 rest: None,
10290 },
10291 )),
10292 guard: None,
10293 body: Box::new(block(33, vec![], Some(id_node(34, "only")))),
10294 },
10295 );
10296 let head_rest_arm = node(
10297 40,
10298 NodeKind::MatchArm {
10299 pattern: Box::new(node(
10300 41,
10301 NodeKind::ListPat {
10302 elems: vec![bind_pat(42, "first")],
10303 rest: Some(Box::new(bind_pat(43, "rest"))),
10304 },
10305 )),
10306 guard: None,
10307 body: Box::new(block(44, vec![], Some(id_node(45, "first")))),
10308 },
10309 );
10310 let else_arm = node(
10313 46,
10314 NodeKind::MatchArm {
10315 pattern: Box::new(node(47, NodeKind::WildcardPat)),
10316 guard: None,
10317 body: Box::new(block(48, vec![], Some(str_lit(49, "other")))),
10318 },
10319 );
10320 let match_stmt = node(
10321 50,
10322 NodeKind::Match {
10323 scrutinee: Box::new(id_node(51, "items")),
10324 arms: vec![empty_arm, single_arm, head_rest_arm, else_arm],
10325 },
10326 );
10327 let f = ts_fn_decl(
10328 1,
10329 "describeList",
10330 vec![typed_param_node(2, "items", "Array")],
10331 Some("String"),
10332 block(3, vec![match_stmt], None),
10333 );
10334 let out = gen(&module(vec![], vec![f]));
10335 assert!(
10337 !out.contains("switch ("),
10338 "list-pattern match must not use the switch fast-path, got: {out}"
10339 );
10340 assert!(
10342 out.contains(".length === 0"),
10343 "`[]` arm should test length === 0, got: {out}"
10344 );
10345 assert!(
10347 out.contains(".length === 1") && out.contains("const only = "),
10348 "`[only]` should test length === 1 and bind `only`, got: {out}"
10349 );
10350 assert!(
10352 out.contains(".length >= 1"),
10353 "`[first, ..rest]` should test length >= 1, got: {out}"
10354 );
10355 assert!(
10356 out.contains("const first = ")
10357 && out.contains("const rest = ")
10358 && out.contains(".slice(1)"),
10359 "`[first, ..rest]` should bind `first` and a `rest = …slice(1)`, got: {out}"
10360 );
10361 }
10362
10363 #[test]
10367 fn ts_range_pattern_match_lowers_to_ifchain_with_bounds() {
10368 let lo_arm = node(
10370 20,
10371 NodeKind::MatchArm {
10372 pattern: Box::new(node(
10373 21,
10374 NodeKind::RangePat {
10375 lo: Box::new(int_lit(22, "1")),
10376 hi: Box::new(int_lit(23, "10")),
10377 inclusive: false,
10378 },
10379 )),
10380 guard: None,
10381 body: Box::new(block(24, vec![], Some(str_lit(25, "a")))),
10382 },
10383 );
10384 let hi_arm = node(
10385 30,
10386 NodeKind::MatchArm {
10387 pattern: Box::new(node(
10388 31,
10389 NodeKind::RangePat {
10390 lo: Box::new(int_lit(32, "10")),
10391 hi: Box::new(int_lit(33, "20")),
10392 inclusive: true,
10393 },
10394 )),
10395 guard: None,
10396 body: Box::new(block(34, vec![], Some(str_lit(35, "b")))),
10397 },
10398 );
10399 let else_arm = node(
10400 40,
10401 NodeKind::MatchArm {
10402 pattern: Box::new(node(41, NodeKind::WildcardPat)),
10403 guard: None,
10404 body: Box::new(block(42, vec![], Some(str_lit(43, "c")))),
10405 },
10406 );
10407 let match_stmt = node(
10408 50,
10409 NodeKind::Match {
10410 scrutinee: Box::new(id_node(51, "n")),
10411 arms: vec![lo_arm, hi_arm, else_arm],
10412 },
10413 );
10414 let f = ts_fn_decl(
10415 1,
10416 "classifyRange",
10417 vec![typed_param_node(2, "n", "Int")],
10418 Some("String"),
10419 block(3, vec![match_stmt], None),
10420 );
10421 let out = gen(&module(vec![], vec![f]));
10422 assert!(
10423 !out.contains("switch ("),
10424 "range-pattern match must not use the switch fast-path, got: {out}"
10425 );
10426 assert!(
10428 out.contains(">= 1") && out.contains("< 10"),
10429 "`1..10` should test `>= 1 && < 10`, got: {out}"
10430 );
10431 assert!(
10433 out.contains(">= 10") && out.contains("<= 20"),
10434 "`10..=20` should test `>= 10 && <= 20`, got: {out}"
10435 );
10436 }
10437
10438 #[test]
10448 fn ts_match_narrow_payload_binding_casts_through_extract() {
10449 let ok_arm = node(
10451 20,
10452 NodeKind::MatchArm {
10453 pattern: Box::new(node(
10454 21,
10455 NodeKind::ConstructorPat {
10456 path: type_path(&["Ok"]),
10457 fields: vec![bind_pat(22, "x")],
10458 },
10459 )),
10460 guard: None,
10461 body: Box::new(block(23, vec![], Some(id_node(24, "x")))),
10462 },
10463 );
10464 let err_arm = node(
10465 30,
10466 NodeKind::MatchArm {
10467 pattern: Box::new(node(
10468 31,
10469 NodeKind::ConstructorPat {
10470 path: type_path(&["Err"]),
10471 fields: vec![bind_pat(32, "e")],
10472 },
10473 )),
10474 guard: None,
10475 body: Box::new(block(33, vec![], Some(id_node(34, "e")))),
10476 },
10477 );
10478 let match_stmt = node(
10479 40,
10480 NodeKind::Match {
10481 scrutinee: Box::new(id_node(41, "r")),
10482 arms: vec![ok_arm, err_arm],
10483 },
10484 );
10485 let f = ts_fn_decl(
10486 1,
10487 "run",
10488 vec![typed_param_node(2, "r", "Result")],
10489 None,
10490 block(3, vec![match_stmt], None),
10491 );
10492 let out = gen(&module(vec![], vec![f]));
10493 assert!(
10494 out.contains(r#"const x = (r as Extract<typeof r, { _tag: "Ok" }>)._0;"#),
10495 "Ok-arm payload binding should cast through Extract for narrowing, got: {out}"
10496 );
10497 assert!(
10498 out.contains(r#"const e = (r as Extract<typeof r, { _tag: "Err" }>)._0;"#),
10499 "Err-arm payload binding should cast through Extract for narrowing, got: {out}"
10500 );
10501 }
10502
10503 fn println_call(id: u32, arg: AIRNode) -> AIRNode {
10515 node(
10516 id,
10517 NodeKind::Call {
10518 callee: Box::new(id_node(id + 1, "println")),
10519 args: vec![AirArg {
10520 label: None,
10521 value: arg,
10522 }],
10523 type_args: vec![],
10524 },
10525 )
10526 }
10527
10528 #[test]
10529 fn ts_for_loop_body_tail_call_is_discarded_not_returned() {
10530 let range = node(
10532 20,
10533 NodeKind::Range {
10534 lo: Box::new(int_lit(21, "1")),
10535 hi: Box::new(int_lit(22, "3")),
10536 inclusive: true,
10537 },
10538 );
10539 let loop_body = block(30, vec![], Some(println_call(31, id_node(33, "i"))));
10540 let for_loop = node(
10541 10,
10542 NodeKind::For {
10543 pattern: Box::new(bind_pat(11, "i")),
10544 iterable: Box::new(range),
10545 body: Box::new(loop_body),
10546 },
10547 );
10548 let f = ts_fn_decl(1, "main", vec![], None, block(2, vec![for_loop], None));
10549 let out = gen(&module(vec![], vec![f]));
10550 assert!(
10551 !out.contains("return console.log"),
10552 "a for-loop body's tail call must be a discarded statement, not a \
10553 `return` (which aborts the loop after one iteration); got:\n{out}"
10554 );
10555 assert!(
10556 out.contains("console.log(i);"),
10557 "the loop body should still emit the call as a statement; got:\n{out}"
10558 );
10559 }
10560
10561 #[test]
10562 fn ts_while_loop_body_tail_call_is_discarded_not_returned() {
10563 let cond = node(
10565 20,
10566 NodeKind::Literal {
10567 lit: Literal::Bool(true),
10568 },
10569 );
10570 let loop_body = block(30, vec![], Some(println_call(31, str_lit(33, "x"))));
10571 let while_loop = node(
10572 10,
10573 NodeKind::While {
10574 condition: Box::new(cond),
10575 body: Box::new(loop_body),
10576 },
10577 );
10578 let f = ts_fn_decl(1, "main", vec![], None, block(2, vec![while_loop], None));
10579 let out = gen(&module(vec![], vec![f]));
10580 assert!(
10581 !out.contains("return console.log"),
10582 "a while-loop body's tail call must be a discarded statement, not a \
10583 `return`; got:\n{out}"
10584 );
10585 }
10586
10587 #[test]
10588 fn ts_statement_match_arm_tail_call_is_discarded_not_returned() {
10589 let ok_arm = node(
10593 20,
10594 NodeKind::MatchArm {
10595 pattern: Box::new(node(
10596 21,
10597 NodeKind::ConstructorPat {
10598 path: type_path(&["Ok"]),
10599 fields: vec![bind_pat(22, "v")],
10600 },
10601 )),
10602 guard: None,
10603 body: Box::new(block(23, vec![], Some(println_call(24, id_node(26, "v"))))),
10604 },
10605 );
10606 let err_arm = node(
10607 30,
10608 NodeKind::MatchArm {
10609 pattern: Box::new(node(
10610 31,
10611 NodeKind::ConstructorPat {
10612 path: type_path(&["Err"]),
10613 fields: vec![bind_pat(32, "e")],
10614 },
10615 )),
10616 guard: None,
10617 body: Box::new(block(33, vec![], Some(println_call(34, id_node(36, "e"))))),
10618 },
10619 );
10620 let match_stmt = node(
10621 40,
10622 NodeKind::Match {
10623 scrutinee: Box::new(id_node(41, "r")),
10624 arms: vec![ok_arm, err_arm],
10625 },
10626 );
10627 let trailer = println_call(50, str_lit(52, "done"));
10629 let f = ts_fn_decl(
10630 1,
10631 "run",
10632 vec![typed_param_node(2, "r", "Result")],
10633 None,
10634 block(3, vec![match_stmt], Some(trailer)),
10635 );
10636 let out = gen(&module(vec![], vec![f]));
10637 assert!(
10641 out.contains("console.log(v);") && out.contains("console.log(e);"),
10642 "statement-position match arms should emit their tail call as a \
10643 discarded statement; got:\n{out}"
10644 );
10645 assert!(
10646 !out.contains("return console.log(v);") && !out.contains("return console.log(e);"),
10647 "a statement-position match arm's tail call must be a discarded \
10648 statement, not a `return` (which would skip the code after the \
10649 match); got:\n{out}"
10650 );
10651 }
10652
10653 fn import_glob(id: u32, path: &[&str]) -> AIRNode {
10665 node(
10666 id,
10667 NodeKind::ImportDecl {
10668 path: bock_ast::ModulePath {
10669 segments: path.iter().map(|s| ident(s)).collect(),
10670 span: span(),
10671 },
10672 items: bock_ast::ImportItems::Glob,
10673 },
10674 )
10675 }
10676
10677 #[test]
10678 fn ts_glob_imported_enum_variant_is_imported() {
10679 let category_enum = node(
10681 40,
10682 NodeKind::EnumDecl {
10683 annotations: vec![],
10684 visibility: Visibility::Public,
10685 name: ident("Category"),
10686 generic_params: vec![],
10687 variants: vec![
10688 node(
10689 41,
10690 NodeKind::EnumVariant {
10691 name: ident("Electronics"),
10692 payload: EnumVariantPayload::Unit,
10693 },
10694 ),
10695 node(
10696 42,
10697 NodeKind::EnumVariant {
10698 name: ident("Clothing"),
10699 payload: EnumVariantPayload::Unit,
10700 },
10701 ),
10702 ],
10703 },
10704 );
10705 let models_mod = module_with_path(&["models"], vec![], vec![category_enum]);
10706
10707 let let_c = node(
10709 10,
10710 NodeKind::LetBinding {
10711 is_mut: false,
10712 pattern: Box::new(bind_pat(11, "c")),
10713 ty: None,
10714 value: Box::new(id_node(12, "Electronics")),
10715 },
10716 );
10717 let main_fn = ts_fn_decl(1, "main", vec![], None, block(2, vec![let_c], None));
10718 let main_mod =
10719 module_with_path(&["main"], vec![import_glob(5, &["models"])], vec![main_fn]);
10720
10721 let gen = TsGenerator::new();
10722 let out = gen
10723 .generate_project(&[
10724 (&main_mod, std::path::Path::new("src/main.bock")),
10725 (&models_mod, std::path::Path::new("src/models.bock")),
10726 ])
10727 .unwrap();
10728 let main_file = out
10729 .files
10730 .iter()
10731 .find(|f| f.path == std::path::Path::new("main.ts"))
10732 .expect("main.ts emitted");
10733 assert!(
10734 main_file.content.contains("Category_Electronics")
10735 && main_file.content.contains(r#"from "./models.ts""#),
10736 "main.ts must import the glob-referenced enum variant \
10737 `Category_Electronics` from ./models.ts; got:\n{}",
10738 main_file.content
10739 );
10740 assert!(
10742 !main_file
10743 .content
10744 .contains("import type { Category_Electronics"),
10745 "the variant const must be a value import, not `import type`; got:\n{}",
10746 main_file.content
10747 );
10748 }
10749}