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, ImportItems, 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 CONCURRENCY_RUNTIME_TS: &str = "\
36// ── Bock concurrency runtime ──
37type __BockChannel<T> = {
38 send(v: T): void;
39 recv(): Promise<T>;
40 close(): void;
41};
42const __bockChannelNew = <T>(): [__BockChannel<T>, __BockChannel<T>] => {
43 const queue: T[] = [];
44 const waiters: Array<(v: T) => void> = [];
45 const ch: __BockChannel<T> = {
46 send(v: T) {
47 if (waiters.length > 0) { waiters.shift()!(v); } else { queue.push(v); }
48 },
49 recv(): Promise<T> {
50 return new Promise<T>((resolve) => {
51 if (queue.length > 0) { resolve(queue.shift()!); }
52 else { waiters.push(resolve); }
53 });
54 },
55 close() {}
56 };
57 return [ch, ch];
58};
59const __bockSpawn = <T>(x: Promise<T>): Promise<T> => x;
60";
61
62#[derive(Debug)]
64pub struct TsGenerator {
65 profile: TargetProfile,
66}
67
68impl TsGenerator {
69 #[must_use]
71 pub fn new() -> Self {
72 Self {
73 profile: TargetProfile::typescript(),
74 }
75 }
76}
77
78impl Default for TsGenerator {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl CodeGenerator for TsGenerator {
85 fn target(&self) -> &TargetProfile {
86 &self.profile
87 }
88
89 fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
90 let mut ctx = TsEmitCtx::new();
91 ctx.emit_node(module)?;
92 let (content, mappings) = ctx.finish();
93 let source_map = SourceMap {
94 generated_file: "output.ts".to_string(),
95 mappings,
96 ..Default::default()
97 };
98 Ok(GeneratedCode {
99 files: vec![OutputFile {
100 path: PathBuf::from("output.ts"),
101 content,
102 }],
103 source_map: Some(source_map),
104 })
105 }
106
107 fn entry_invocation(&self, main_is_async: bool) -> Option<String> {
108 if main_is_async {
109 Some("(async () => { await main(); })();\n".to_string())
110 } else {
111 Some("main();\n".to_string())
112 }
113 }
114}
115
116struct TsEmitCtx {
120 buf: String,
121 indent: usize,
122 effect_ops: HashMap<String, String>,
124 current_handler_vars: HashMap<String, String>,
126 fn_effects: HashMap<String, Vec<String>>,
128 composite_effects: HashMap<String, Vec<String>>,
130 record_names: HashSet<String>,
132 effect_names: HashSet<String>,
134 cur_line: u32,
136 cur_col: u32,
138 scan_pos: usize,
140 last_marked: Option<(u32, u32)>,
142 mappings: Vec<SourceMapping>,
144}
145
146impl TsEmitCtx {
147 fn new() -> Self {
148 Self {
149 buf: String::with_capacity(4096),
150 indent: 0,
151 effect_ops: HashMap::new(),
152 current_handler_vars: HashMap::new(),
153 fn_effects: HashMap::new(),
154 composite_effects: HashMap::new(),
155 record_names: HashSet::new(),
156 effect_names: HashSet::new(),
157 cur_line: 1,
158 cur_col: 1,
159 scan_pos: 0,
160 last_marked: None,
161 mappings: Vec::new(),
162 }
163 }
164
165 fn finish(self) -> (String, Vec<SourceMapping>) {
166 (self.buf, self.mappings)
167 }
168
169 fn sync_pos(&mut self) {
172 if self.scan_pos >= self.buf.len() {
173 return;
174 }
175 let slice = &self.buf[self.scan_pos..];
176 for ch in slice.chars() {
177 if ch == '\n' {
178 self.cur_line += 1;
179 self.cur_col = 1;
180 } else {
181 self.cur_col += 1;
182 }
183 }
184 self.scan_pos = self.buf.len();
185 }
186
187 fn mark_span(&mut self, span: Span) {
188 if span.start == 0 && span.end == 0 {
189 return;
190 }
191 self.sync_pos();
192 let key = (self.cur_line, self.cur_col);
193 if self.last_marked == Some(key) {
194 return;
195 }
196 self.last_marked = Some(key);
197 self.mappings.push(SourceMapping {
198 gen_line: self.cur_line,
199 gen_col: self.cur_col,
200 src_line: 0,
201 src_col: 0,
202 src_offset: span.start as u32,
203 src_file_id: span.file.0,
204 });
205 }
206
207 fn indent_str(&self) -> String {
208 " ".repeat(self.indent)
209 }
210
211 fn write_indent(&mut self) {
212 let indent = self.indent_str();
213 self.buf.push_str(&indent);
214 }
215
216 fn writeln(&mut self, s: &str) {
217 self.write_indent();
218 self.buf.push_str(s);
219 self.buf.push('\n');
220 }
221
222 fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
226 let start = self.buf.len();
227 let saved_line = self.cur_line;
228 let saved_col = self.cur_col;
229 let saved_scan = self.scan_pos;
230 let saved_marked = self.last_marked;
231 let mappings_len = self.mappings.len();
232 self.emit_expr(node)?;
233 let s = self.buf[start..].to_string();
234 self.buf.truncate(start);
235 self.cur_line = saved_line;
236 self.cur_col = saved_col;
237 self.scan_pos = saved_scan;
238 self.last_marked = saved_marked;
239 self.mappings.truncate(mappings_len);
240 Ok(s)
241 }
242
243 fn map_prelude_call(
245 &mut self,
246 callee: &AIRNode,
247 args: &[bock_air::AirArg],
248 ) -> Result<Option<String>, CodegenError> {
249 let name = match &callee.kind {
250 NodeKind::Identifier { name } => name.name.as_str(),
251 _ => return Ok(None),
252 };
253 let arg_strs: Vec<String> = args
254 .iter()
255 .map(|a| self.expr_to_string(&a.value))
256 .collect::<Result<_, _>>()?;
257 let code = match name {
258 "println" => {
259 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
260 format!("console.log({a})")
261 }
262 "print" => {
263 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
264 format!("process.stdout.write(String({a}))")
265 }
266 "debug" => {
267 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
268 format!("console.debug({a})")
269 }
270 "assert" => {
271 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
272 format!("if (!{a}) throw new Error(\"assertion failed\")")
273 }
274 "todo" => "throw new Error(\"not implemented\")".to_string(),
275 "unreachable" => "throw new Error(\"unreachable\")".to_string(),
276 "sleep" => {
277 let a = arg_strs.first().map_or(String::new(), |s| s.clone());
278 format!("new Promise<void>((__r) => setTimeout(__r, Math.floor(({a}) / 1e6)))")
279 }
280 _ => return Ok(None),
281 };
282 Ok(Some(code))
283 }
284
285 fn try_emit_time_assoc_call(
290 &mut self,
291 callee: &AIRNode,
292 args: &[bock_air::AirArg],
293 ) -> Result<bool, CodegenError> {
294 let NodeKind::FieldAccess { object, field } = &callee.kind else {
295 return Ok(false);
296 };
297 let NodeKind::Identifier { name: type_name } = &object.kind else {
298 return Ok(false);
299 };
300 let arg_strs: Vec<String> = args
301 .iter()
302 .map(|a| self.expr_to_string(&a.value))
303 .collect::<Result<_, _>>()?;
304 let arg0 = || arg_strs.first().cloned().unwrap_or_default();
305 let code = match (type_name.name.as_str(), field.name.as_str()) {
306 ("Duration", "zero") => "0".to_string(),
307 ("Duration", "nanos") => arg0(),
308 ("Duration", "micros") => format!("(({}) * 1000)", arg0()),
309 ("Duration", "millis") => format!("(({}) * 1000000)", arg0()),
310 ("Duration", "seconds") => format!("(({}) * 1000000000)", arg0()),
311 ("Duration", "minutes") => format!("(({}) * 60000000000)", arg0()),
312 ("Duration", "hours") => format!("(({}) * 3600000000000)", arg0()),
313 ("Instant", "now") => "(performance.now() * 1000000)".to_string(),
314 _ => return Ok(false),
315 };
316 self.buf.push_str(&code);
317 Ok(true)
318 }
319
320 fn try_emit_concurrency_call(
324 &mut self,
325 callee: &AIRNode,
326 args: &[bock_air::AirArg],
327 ) -> Result<bool, CodegenError> {
328 if let NodeKind::Identifier { name } = &callee.kind {
329 if name.name == "spawn" {
330 self.buf.push_str("__bockSpawn(");
331 for (i, arg) in args.iter().enumerate() {
332 if i > 0 {
333 self.buf.push_str(", ");
334 }
335 self.emit_expr(&arg.value)?;
336 }
337 self.buf.push(')');
338 return Ok(true);
339 }
340 }
341 let NodeKind::FieldAccess { object, field } = &callee.kind else {
342 return Ok(false);
343 };
344 if let NodeKind::Identifier { name: type_name } = &object.kind {
345 if type_name.name == "Channel" && field.name == "new" {
346 self.buf.push_str("__bockChannelNew()");
347 return Ok(true);
348 }
349 }
350 if matches!(field.name.as_str(), "send" | "recv" | "close") {
351 self.emit_expr(object)?;
353 let _ = write!(self.buf, ".{}", field.name);
354 self.buf.push('(');
355 for (i, arg) in args.iter().skip(1).enumerate() {
356 if i > 0 {
357 self.buf.push_str(", ");
358 }
359 self.emit_expr(&arg.value)?;
360 }
361 self.buf.push(')');
362 return Ok(true);
363 }
364 Ok(false)
365 }
366
367 fn try_emit_time_desugared_method(
370 &mut self,
371 callee: &AIRNode,
372 args: &[bock_air::AirArg],
373 ) -> Result<bool, CodegenError> {
374 let NodeKind::FieldAccess { object, field } = &callee.kind else {
375 return Ok(false);
376 };
377 if let NodeKind::Identifier { name } = &object.kind {
378 if matches!(name.name.as_str(), "Duration" | "Instant") {
379 return Ok(false);
380 }
381 }
382 if !is_time_method_name(&field.name) {
383 return Ok(false);
384 }
385 let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
386 self.try_emit_time_method(object, &field.name, &remaining)
387 }
388
389 fn try_emit_time_method(
392 &mut self,
393 receiver: &AIRNode,
394 method: &str,
395 args: &[bock_air::AirArg],
396 ) -> Result<bool, CodegenError> {
397 let recv_str = self.expr_to_string(receiver)?;
398 let arg_strs: Vec<String> = args
399 .iter()
400 .map(|a| self.expr_to_string(&a.value))
401 .collect::<Result<_, _>>()?;
402 let code = match method {
403 "as_nanos" => format!("({recv_str})"),
404 "as_millis" => format!("Math.floor(({recv_str}) / 1000000)"),
405 "as_seconds" => format!("Math.floor(({recv_str}) / 1000000000)"),
406 "is_zero" => format!("(({recv_str}) === 0)"),
407 "is_negative" => format!("(({recv_str}) < 0)"),
408 "abs" => format!("Math.abs({recv_str})"),
409 "elapsed" => format!("((performance.now() * 1000000) - ({recv_str}))"),
410 "duration_since" => {
411 let other = arg_strs.first().cloned().unwrap_or_default();
412 format!("(({recv_str}) - ({other}))")
413 }
414 _ => return Ok(false),
415 };
416 self.buf.push_str(&code);
417 Ok(true)
418 }
419
420 fn try_emit_prelude_ctor(
424 &mut self,
425 callee: &AIRNode,
426 args: &[bock_air::AirArg],
427 ) -> Result<bool, CodegenError> {
428 let name = match &callee.kind {
429 NodeKind::Identifier { name } => name.name.as_str(),
430 _ => return Ok(false),
431 };
432 if !matches!(name, "Some" | "Ok" | "Err") {
433 return Ok(false);
434 }
435 let _ = write!(self.buf, "{{ _tag: \"{name}\" as const");
436 if let Some(arg) = args.first() {
437 self.buf.push_str(", _0: ");
438 self.emit_expr(&arg.value)?;
439 }
440 self.buf.push_str(" }");
441 Ok(true)
442 }
443
444 fn type_to_ts(&self, node: &AIRNode) -> String {
448 match &node.kind {
449 NodeKind::TypeNamed { path, args } => {
450 let name = path
451 .segments
452 .iter()
453 .map(|s| s.name.as_str())
454 .collect::<Vec<_>>()
455 .join(".");
456 let ts_name = self.map_type_name(&name);
457 if args.is_empty() {
458 ts_name
459 } else {
460 let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_ts(a)).collect();
461 format!("{ts_name}<{}>", arg_strs.join(", "))
462 }
463 }
464 NodeKind::TypeTuple { elems } => {
465 let elem_strs: Vec<String> = elems.iter().map(|e| self.type_to_ts(e)).collect();
466 format!("[{}]", elem_strs.join(", "))
467 }
468 NodeKind::TypeFunction { params, ret, .. } => {
469 let param_strs: Vec<String> = params
470 .iter()
471 .enumerate()
472 .map(|(i, p)| format!("arg{i}: {}", self.type_to_ts(p)))
473 .collect();
474 format!("({}) => {}", param_strs.join(", "), self.type_to_ts(ret))
475 }
476 NodeKind::TypeOptional { inner } => {
477 format!("{} | null", self.type_to_ts(inner))
478 }
479 NodeKind::TypeSelf => "this".into(),
480 _ => "unknown".into(),
481 }
482 }
483
484 fn map_type_name(&self, name: &str) -> String {
486 match name {
487 "Int" => "number".into(),
488 "Float" => "number".into(),
489 "Bool" => "boolean".into(),
490 "String" => "string".into(),
491 "Void" | "Unit" => "void".into(),
492 "List" => "Array".into(),
493 "Map" => "Map".into(),
494 "Set" => "Set".into(),
495 "Any" => "any".into(),
496 "Never" => "never".into(),
497 other => other.into(),
498 }
499 }
500
501 fn ast_type_to_ts(&self, ty: &TypeExpr) -> String {
503 match ty {
504 TypeExpr::Named { path, args, .. } => {
505 let name = path
506 .segments
507 .iter()
508 .map(|s| s.name.as_str())
509 .collect::<Vec<_>>()
510 .join(".");
511 let ts_name = self.map_type_name(&name);
512 if args.is_empty() {
513 ts_name
514 } else {
515 let arg_strs: Vec<String> =
516 args.iter().map(|a| self.ast_type_to_ts(a)).collect();
517 format!("{ts_name}<{}>", arg_strs.join(", "))
518 }
519 }
520 TypeExpr::Tuple { elems, .. } => {
521 let elem_strs: Vec<String> = elems.iter().map(|e| self.ast_type_to_ts(e)).collect();
522 format!("[{}]", elem_strs.join(", "))
523 }
524 TypeExpr::Function { params, ret, .. } => {
525 let param_strs: Vec<String> = params
526 .iter()
527 .enumerate()
528 .map(|(i, p)| format!("arg{i}: {}", self.ast_type_to_ts(p)))
529 .collect();
530 format!(
531 "({}) => {}",
532 param_strs.join(", "),
533 self.ast_type_to_ts(ret)
534 )
535 }
536 TypeExpr::Optional { inner, .. } => {
537 format!("{} | null", self.ast_type_to_ts(inner))
538 }
539 TypeExpr::SelfType { .. } => "this".into(),
540 }
541 }
542
543 fn generic_params_to_ts(&self, params: &[bock_ast::GenericParam]) -> String {
545 if params.is_empty() {
546 return String::new();
547 }
548 let items: Vec<String> = params
549 .iter()
550 .map(|p| {
551 if p.bounds.is_empty() {
552 p.name.name.clone()
553 } else {
554 let bounds: Vec<String> = p
555 .bounds
556 .iter()
557 .map(|b| {
558 b.segments
559 .iter()
560 .map(|s| s.name.as_str())
561 .collect::<Vec<_>>()
562 .join(".")
563 })
564 .collect();
565 format!("{} extends {}", p.name.name, bounds.join(" & "))
566 }
567 })
568 .collect();
569 format!("<{}>", items.join(", "))
570 }
571
572 fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
575 self.mark_span(node.span);
576 match &node.kind {
577 NodeKind::Module { imports, items, .. } => {
578 if module_uses_concurrency(items) {
579 self.buf.push_str(CONCURRENCY_RUNTIME_TS);
580 self.buf.push('\n');
581 }
582 for imp in imports {
583 self.emit_node(imp)?;
584 }
585 if !imports.is_empty() && !items.is_empty() {
586 self.buf.push('\n');
587 }
588 for (i, item) in items.iter().enumerate() {
589 if i > 0 {
590 self.buf.push('\n');
591 }
592 self.emit_node(item)?;
593 }
594 Ok(())
595 }
596 NodeKind::ImportDecl { path, items } => {
597 let path_str = path
598 .segments
599 .iter()
600 .map(|s| s.name.as_str())
601 .collect::<Vec<_>>()
602 .join(".");
603 match items {
604 ImportItems::Module => {
605 self.writeln(&format!("// import {path_str}"));
606 }
607 ImportItems::Named(names) => {
608 let names_str = names
609 .iter()
610 .map(|n| n.name.name.as_str())
611 .collect::<Vec<_>>()
612 .join(", ");
613 self.writeln(&format!("// import {{ {names_str} }} from {path_str}"));
614 }
615 ImportItems::Glob => {
616 self.writeln(&format!("// import * from {path_str}"));
617 }
618 }
619 Ok(())
620 }
621 NodeKind::FnDecl {
622 visibility,
623 is_async,
624 name,
625 generic_params,
626 params,
627 return_type,
628 effect_clause,
629 body,
630 ..
631 } => self.emit_fn_decl(
632 *visibility,
633 *is_async,
634 &name.name,
635 generic_params,
636 params,
637 return_type.as_deref(),
638 effect_clause,
639 body,
640 false,
641 ),
642 NodeKind::RecordDecl {
643 visibility,
644 name,
645 generic_params,
646 fields,
647 ..
648 } => {
649 let export = if matches!(visibility, Visibility::Public) {
650 "export "
651 } else {
652 ""
653 };
654 let generics = self.generic_params_to_ts(generic_params);
655 self.record_names.insert(name.name.clone());
656 if fields.is_empty() {
657 self.writeln(&format!("{export}class {}{generics} {{}}", name.name));
658 } else {
659 self.writeln(&format!("{export}class {}{generics} {{", name.name));
660 self.indent += 1;
661 for f in fields {
662 let ty = self.ast_type_to_ts(&f.ty);
663 self.writeln(&format!("{}: {};", f.name.name, ty));
664 }
665 let init_fields: Vec<String> = fields
666 .iter()
667 .map(|f| format!("{}: {}", f.name.name, self.ast_type_to_ts(&f.ty)))
668 .collect();
669 let destructure: Vec<&str> =
670 fields.iter().map(|f| f.name.name.as_str()).collect();
671 self.writeln(&format!(
672 "constructor({{ {} }}: {{ {} }}) {{",
673 destructure.join(", "),
674 init_fields.join("; "),
675 ));
676 self.indent += 1;
677 for fname in &destructure {
678 self.writeln(&format!("this.{fname} = {fname};"));
679 }
680 self.indent -= 1;
681 self.writeln("}");
682 self.indent -= 1;
683 self.writeln("}");
684 }
685 Ok(())
686 }
687 NodeKind::EnumDecl {
688 visibility,
689 name,
690 generic_params,
691 variants,
692 ..
693 } => {
694 let export = if matches!(visibility, Visibility::Public) {
695 "export "
696 } else {
697 ""
698 };
699 let generics = self.generic_params_to_ts(generic_params);
700
701 let variant_names: Vec<String> = variants
703 .iter()
704 .filter_map(|v| {
705 if let NodeKind::EnumVariant { name: vn, .. } = &v.kind {
706 Some(format!("{}_{}", name.name, vn.name))
707 } else {
708 None
709 }
710 })
711 .collect();
712 if !variant_names.is_empty() {
713 self.writeln(&format!(
714 "{export}type {}{generics} = {};",
715 name.name,
716 variant_names.join(" | "),
717 ));
718 self.buf.push('\n');
719 }
720
721 for variant in variants {
723 self.emit_enum_variant(&name.name, generic_params, variant)?;
724 }
725 Ok(())
726 }
727 NodeKind::ClassDecl {
728 visibility,
729 name,
730 generic_params,
731 fields,
732 methods,
733 ..
734 } => {
735 let export = if matches!(visibility, Visibility::Public) {
736 "export "
737 } else {
738 ""
739 };
740 let generics = self.generic_params_to_ts(generic_params);
741 self.writeln(&format!("{export}class {}{generics} {{", name.name));
742 self.indent += 1;
743 for f in fields {
745 let ty = self.ast_type_to_ts(&f.ty);
746 self.writeln(&format!("{}: {};", f.name.name, ty));
747 }
748 if !fields.is_empty() {
749 self.buf.push('\n');
750 }
751 let ctor_params: Vec<String> = fields
753 .iter()
754 .map(|f| format!("{}: {}", f.name.name, self.ast_type_to_ts(&f.ty)))
755 .collect();
756 self.writeln(&format!("constructor({}) {{", ctor_params.join(", ")));
757 self.indent += 1;
758 for f in fields {
759 self.writeln(&format!("this.{} = {};", f.name.name, f.name.name));
760 }
761 self.indent -= 1;
762 self.writeln("}");
763 for method in methods {
765 self.buf.push('\n');
766 self.emit_class_method(method)?;
767 }
768 self.indent -= 1;
769 self.writeln("}");
770 Ok(())
771 }
772 NodeKind::TraitDecl {
773 visibility,
774 name,
775 generic_params,
776 methods,
777 ..
778 } => {
779 let export = if matches!(visibility, Visibility::Public) {
780 "export "
781 } else {
782 ""
783 };
784 let generics = self.generic_params_to_ts(generic_params);
785 self.writeln(&format!("{export}interface {}{generics} {{", name.name));
786 self.indent += 1;
787 for (i, method) in methods.iter().enumerate() {
788 if i > 0 {
789 self.buf.push('\n');
790 }
791 if let NodeKind::FnDecl {
792 name,
793 generic_params: method_generics,
794 params,
795 return_type,
796 ..
797 } = &method.kind
798 {
799 let m_generics = self.generic_params_to_ts(method_generics);
800 let param_list = self.collect_typed_params(params);
801 let ret = return_type
802 .as_ref()
803 .map(|r| self.type_to_ts(r))
804 .unwrap_or_else(|| "void".into());
805 self.writeln(&format!(
806 "{}{m_generics}({}): {};",
807 name.name,
808 param_list.join(", "),
809 ret,
810 ));
811 }
812 }
813 self.indent -= 1;
814 self.writeln("}");
815 Ok(())
816 }
817 NodeKind::ImplBlock {
818 trait_path,
819 target,
820 methods,
821 ..
822 } => {
823 let target_name = self.type_expr_to_string(target);
824 if let Some(tp) = trait_path {
825 let trait_name = tp
826 .segments
827 .iter()
828 .map(|s| s.name.as_str())
829 .collect::<Vec<_>>()
830 .join(".");
831 self.writeln(&format!(
835 "interface {target_name} extends {trait_name} {{}}"
836 ));
837 self.writeln(&format!("// impl {trait_name} for {target_name}"));
838 } else {
839 self.writeln(&format!("// impl {target_name}"));
840 }
841 for method in methods {
842 if let NodeKind::FnDecl {
843 is_async,
844 name,
845 generic_params,
846 params,
847 return_type,
848 effect_clause,
849 body,
850 ..
851 } = &method.kind
852 {
853 let async_kw = if *is_async { "async " } else { "" };
854 let generics = self.generic_params_to_ts(generic_params);
855 let param_list = self.collect_typed_params(params);
856 let effects_param = self.effects_param(effect_clause);
857 let mut all_params = param_list;
858 if let Some(ep) = effects_param {
859 all_params.push(ep);
860 }
861 let ret_str = build_ts_return_type(
862 *is_async,
863 return_type.as_deref().map(|r| self.type_to_ts(r)),
864 );
865 self.writeln(&format!(
866 "{target_name}.prototype.{} = {async_kw}function{generics}({}){ret_str} {{",
867 name.name,
868 all_params.join(", "),
869 ));
870 self.indent += 1;
871 let old_handler_vars = self.current_handler_vars.clone();
872 let expanded = self.expand_effect_names(effect_clause);
873 for ename in &expanded {
874 self.current_handler_vars
875 .insert(ename.clone(), to_camel_case(ename));
876 }
877 self.emit_block_body(body)?;
878 self.current_handler_vars = old_handler_vars;
879 self.indent -= 1;
880 self.writeln("};");
881 }
882 }
883 Ok(())
884 }
885 NodeKind::EffectDecl {
886 visibility,
887 name,
888 generic_params,
889 components,
890 operations,
891 ..
892 } => {
893 if !components.is_empty() {
894 let comp_names: Vec<String> = components
895 .iter()
896 .map(|tp| {
897 tp.segments
898 .last()
899 .map_or("effect".to_string(), |s| s.name.clone())
900 })
901 .collect();
902 self.writeln(&format!(
903 "// composite effect {} = {}",
904 name.name,
905 comp_names.join(" + ")
906 ));
907 self.composite_effects
908 .insert(name.name.clone(), comp_names);
909 return Ok(());
910 }
911 for op in operations {
913 if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
914 self.effect_ops
915 .insert(op_name.name.clone(), name.name.clone());
916 }
917 }
918 self.effect_names.insert(name.name.clone());
919 let export = if matches!(visibility, Visibility::Public) {
921 "export "
922 } else {
923 ""
924 };
925 let generics = self.generic_params_to_ts(generic_params);
926 self.writeln(&format!("{export}interface {}{generics} {{", name.name));
927 self.indent += 1;
928 for op in operations {
929 if let NodeKind::FnDecl {
930 name,
931 params,
932 return_type,
933 ..
934 } = &op.kind
935 {
936 let param_list = self.collect_typed_params(params);
937 let ret = return_type
938 .as_ref()
939 .map(|r| self.type_to_ts(r))
940 .unwrap_or_else(|| "void".into());
941 self.writeln(&format!(
942 "{}({}): {};",
943 name.name,
944 param_list.join(", "),
945 ret,
946 ));
947 }
948 }
949 self.indent -= 1;
950 self.writeln("}");
951 Ok(())
952 }
953 NodeKind::TypeAlias {
954 visibility,
955 name,
956 generic_params,
957 ty,
958 ..
959 } => {
960 let export = if matches!(visibility, Visibility::Public) {
961 "export "
962 } else {
963 ""
964 };
965 let generics = self.generic_params_to_ts(generic_params);
966 let ty_str = self.type_to_ts(ty);
967 self.writeln(&format!("{export}type {}{generics} = {ty_str};", name.name));
968 Ok(())
969 }
970 NodeKind::ConstDecl {
971 visibility,
972 name,
973 ty,
974 value,
975 ..
976 } => {
977 let export = if matches!(visibility, Visibility::Public) {
978 "export "
979 } else {
980 ""
981 };
982 let ty_str = self.type_to_ts(ty);
983 let ind = self.indent_str();
984 let _ = write!(self.buf, "{ind}{export}const {}: {ty_str} = ", name.name);
985 self.emit_expr(value)?;
986 self.buf.push_str(";\n");
987 Ok(())
988 }
989 NodeKind::ModuleHandle { effect, handler } => {
990 let effect_name =
991 effect.segments.last().map_or("effect", |s| s.name.as_str());
992 let var_name = format!("__{}", to_camel_case(effect_name));
993 let type_name = effect_name;
994 let ind = self.indent_str();
995 let _ = write!(self.buf, "{ind}const {var_name}: {type_name} = ");
996 self.emit_expr(handler)?;
997 self.buf.push_str(";\n");
998 self.current_handler_vars
1000 .insert(effect_name.to_string(), var_name);
1001 Ok(())
1002 }
1003 NodeKind::PropertyTest { name, body, .. } => {
1004 self.writeln(&format!("// property test: {name}"));
1005 self.writeln("// (property tests are not emitted in TS output)");
1006 let _ = body;
1007 Ok(())
1008 }
1009 NodeKind::LetBinding { .. }
1011 | NodeKind::If { .. }
1012 | NodeKind::For { .. }
1013 | NodeKind::While { .. }
1014 | NodeKind::Loop { .. }
1015 | NodeKind::Return { .. }
1016 | NodeKind::Break { .. }
1017 | NodeKind::Continue
1018 | NodeKind::Guard { .. }
1019 | NodeKind::Match { .. }
1020 | NodeKind::Block { .. }
1021 | NodeKind::HandlingBlock { .. }
1022 | NodeKind::Assign { .. } => self.emit_stmt(node),
1023 _ => {
1025 self.write_indent();
1026 self.emit_expr(node)?;
1027 self.buf.push_str(";\n");
1028 Ok(())
1029 }
1030 }
1031 }
1032
1033 #[allow(clippy::too_many_arguments)]
1036 fn emit_fn_decl(
1037 &mut self,
1038 visibility: Visibility,
1039 is_async: bool,
1040 name: &str,
1041 generic_params: &[bock_ast::GenericParam],
1042 params: &[AIRNode],
1043 return_type: Option<&AIRNode>,
1044 effect_clause: &[bock_ast::TypePath],
1045 body: &AIRNode,
1046 _is_method: bool,
1047 ) -> Result<(), CodegenError> {
1048 let export = if matches!(visibility, Visibility::Public) {
1049 "export "
1050 } else {
1051 ""
1052 };
1053 let async_kw = if is_async { "async " } else { "" };
1054 let generics = self.generic_params_to_ts(generic_params);
1055 let param_list = self.collect_typed_params(params);
1056 let effects_param = self.effects_param(effect_clause);
1057 let mut all_params = param_list;
1058 if let Some(ep) = effects_param {
1059 all_params.push(ep);
1060 }
1061 let ret_str = build_ts_return_type(is_async, return_type.map(|r| self.type_to_ts(r)));
1062 if !effect_clause.is_empty() {
1063 let effect_names = self.expand_effect_names(effect_clause);
1064 self.fn_effects.insert(name.to_string(), effect_names);
1065 }
1066 let ts_name = to_camel_case(name);
1067 self.writeln(&format!(
1068 "{export}{async_kw}function {ts_name}{generics}({}){ret_str} {{",
1069 all_params.join(", "),
1070 ));
1071 self.indent += 1;
1072 let old_handler_vars = self.current_handler_vars.clone();
1073 let expanded = self.expand_effect_names(effect_clause);
1074 for ename in &expanded {
1075 self.current_handler_vars
1076 .insert(ename.clone(), to_camel_case(ename));
1077 }
1078 self.emit_block_body(body)?;
1079 self.current_handler_vars = old_handler_vars;
1080 self.indent -= 1;
1081 self.writeln("}");
1082 Ok(())
1083 }
1084
1085 fn emit_class_method(&mut self, method: &AIRNode) -> Result<(), CodegenError> {
1086 if let NodeKind::FnDecl {
1087 is_async,
1088 name,
1089 generic_params,
1090 params,
1091 return_type,
1092 effect_clause,
1093 body,
1094 ..
1095 } = &method.kind
1096 {
1097 let async_kw = if *is_async { "async " } else { "" };
1098 let generics = self.generic_params_to_ts(generic_params);
1099 let param_list = self.collect_typed_params(params);
1100 let effects_param = self.effects_param(effect_clause);
1101 let mut all_params = param_list;
1102 if let Some(ep) = effects_param {
1103 all_params.push(ep);
1104 }
1105 let ret_str = build_ts_return_type(
1106 *is_async,
1107 return_type.as_deref().map(|r| self.type_to_ts(r)),
1108 );
1109 let method_name = to_camel_case(&name.name);
1110 self.writeln(&format!(
1111 "{async_kw}{method_name}{generics}({}){ret_str} {{",
1112 all_params.join(", "),
1113 ));
1114 self.indent += 1;
1115 let old_handler_vars = self.current_handler_vars.clone();
1116 let expanded = self.expand_effect_names(effect_clause);
1117 for ename in &expanded {
1118 self.current_handler_vars
1119 .insert(ename.clone(), to_camel_case(ename));
1120 }
1121 self.emit_block_body(body)?;
1122 self.current_handler_vars = old_handler_vars;
1123 self.indent -= 1;
1124 self.writeln("}");
1125 }
1126 Ok(())
1127 }
1128
1129 fn collect_typed_params(&self, params: &[AIRNode]) -> Vec<String> {
1131 params
1132 .iter()
1133 .filter_map(|p| {
1134 if let NodeKind::Param {
1135 pattern,
1136 ty,
1137 default,
1138 } = &p.kind
1139 {
1140 let name = self.pattern_to_binding_name(pattern);
1141 let ty_str = ty
1142 .as_ref()
1143 .map(|t| format!(": {}", self.type_to_ts(t)))
1144 .unwrap_or_default();
1145 if let Some(def) = default {
1146 let mut ctx = TsEmitCtx::new();
1147 ctx.indent = self.indent;
1148 if ctx.emit_expr_to_string(def).is_ok() {
1149 let (def_str, _) = ctx.finish();
1150 return Some(format!("{name}{ty_str} = {def_str}"));
1151 }
1152 }
1153 Some(format!("{name}{ty_str}"))
1154 } else {
1155 None
1156 }
1157 })
1158 .collect()
1159 }
1160
1161 fn emit_expr_to_string(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1162 self.emit_expr(node)
1163 }
1164
1165 fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
1167 let mut result = Vec::new();
1168 for tp in effects {
1169 let name = tp
1170 .segments
1171 .last()
1172 .map_or("effect".to_string(), |s| s.name.clone());
1173 if let Some(components) = self.composite_effects.get(&name) {
1174 result.extend(components.iter().cloned());
1175 } else {
1176 result.push(name);
1177 }
1178 }
1179 result
1180 }
1181
1182 fn effects_param(&self, effects: &[bock_ast::TypePath]) -> Option<String> {
1184 if effects.is_empty() {
1185 return None;
1186 }
1187 let expanded = self.expand_effect_names(effects);
1188 if expanded.is_empty() {
1189 return None;
1190 }
1191 let names: Vec<String> = expanded.iter().map(|n| to_camel_case(n)).collect();
1192 let type_entries: Vec<String> = expanded
1193 .iter()
1194 .zip(names.iter())
1195 .map(|(orig, camel)| format!("{camel}: {orig}"))
1196 .collect();
1197 Some(format!(
1198 "{{ {} }}: {{ {} }}",
1199 names.join(", "),
1200 type_entries.join(", ")
1201 ))
1202 }
1203
1204 fn build_effects_call_arg_ts(&self, fn_name: &str) -> Option<String> {
1206 let effects = self.fn_effects.get(fn_name)?;
1207 let entries: Vec<String> = effects
1208 .iter()
1209 .filter_map(|e| {
1210 let handler_var = self.current_handler_vars.get(e)?;
1211 let param_name = to_camel_case(e);
1212 Some(format!("{param_name}: {handler_var}"))
1213 })
1214 .collect();
1215 if entries.is_empty() {
1216 return None;
1217 }
1218 Some(format!("{{ {} }}", entries.join(", ")))
1219 }
1220
1221 fn emit_enum_variant(
1224 &mut self,
1225 enum_name: &str,
1226 generic_params: &[bock_ast::GenericParam],
1227 variant: &AIRNode,
1228 ) -> Result<(), CodegenError> {
1229 if let NodeKind::EnumVariant { name, payload } = &variant.kind {
1230 let vname = &name.name;
1231 let generics = self.generic_params_to_ts(generic_params);
1232 let qualified = format!("{enum_name}_{vname}");
1233
1234 match payload {
1235 EnumVariantPayload::Unit => {
1236 self.writeln(&format!(
1238 "interface {qualified}{generics} {{ readonly _tag: \"{vname}\"; }}"
1239 ));
1240 self.writeln(&format!(
1241 "const {qualified}: {qualified} = Object.freeze({{ _tag: \"{vname}\" as const }});"
1242 ));
1243 }
1244 EnumVariantPayload::Struct(fields) => {
1245 self.writeln(&format!("interface {qualified}{generics} {{"));
1247 self.indent += 1;
1248 self.writeln(&format!("readonly _tag: \"{vname}\";"));
1249 for f in fields {
1250 let ty = self.ast_type_to_ts(&f.ty);
1251 self.writeln(&format!("readonly {}: {};", f.name.name, ty));
1252 }
1253 self.indent -= 1;
1254 self.writeln("}");
1255 let field_params: Vec<String> = fields
1256 .iter()
1257 .map(|f| format!("{}: {}", f.name.name, self.ast_type_to_ts(&f.ty)))
1258 .collect();
1259 let field_names: Vec<&str> =
1260 fields.iter().map(|f| f.name.name.as_str()).collect();
1261 self.writeln(&format!(
1262 "function {qualified}{generics}({}): {qualified} {{",
1263 field_params.join(", "),
1264 ));
1265 self.indent += 1;
1266 self.writeln(&format!(
1267 "return {{ _tag: \"{vname}\" as const, {} }};",
1268 field_names.join(", "),
1269 ));
1270 self.indent -= 1;
1271 self.writeln("}");
1272 }
1273 EnumVariantPayload::Tuple(elems) => {
1274 self.writeln(&format!("interface {qualified}{generics} {{"));
1276 self.indent += 1;
1277 self.writeln(&format!("readonly _tag: \"{vname}\";"));
1278 for (i, elem) in elems.iter().enumerate() {
1279 let ty = self.type_to_ts(elem);
1280 self.writeln(&format!("readonly _{i}: {ty};"));
1281 }
1282 self.indent -= 1;
1283 self.writeln("}");
1284 let param_decls: Vec<String> = elems
1285 .iter()
1286 .enumerate()
1287 .map(|(i, e)| format!("_{i}: {}", self.type_to_ts(e)))
1288 .collect();
1289 let param_names: Vec<String> =
1290 (0..elems.len()).map(|i| format!("_{i}")).collect();
1291 self.writeln(&format!(
1292 "function {qualified}{generics}({}): {qualified} {{",
1293 param_decls.join(", "),
1294 ));
1295 self.indent += 1;
1296 self.writeln(&format!(
1297 "return {{ _tag: \"{vname}\" as const, {} }};",
1298 param_names
1299 .iter()
1300 .enumerate()
1301 .map(|(i, p)| format!("_{i}: {p}"))
1302 .collect::<Vec<_>>()
1303 .join(", ")
1304 ));
1305 self.indent -= 1;
1306 self.writeln("}");
1307 }
1308 }
1309 }
1310 Ok(())
1311 }
1312
1313 fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1316 self.mark_span(node.span);
1317 match &node.kind {
1318 NodeKind::LetBinding {
1319 is_mut,
1320 pattern,
1321 ty,
1322 value,
1323 ..
1324 } => {
1325 let kw = if *is_mut { "let" } else { "const" };
1326 let binding = self.pattern_to_ts_destructure(pattern);
1327 let ty_str = ty
1328 .as_ref()
1329 .map(|t| format!(": {}", self.type_to_ts(t)))
1330 .unwrap_or_default();
1331 let ind = self.indent_str();
1332 let _ = write!(self.buf, "{ind}{kw} {binding}{ty_str} = ");
1333 self.emit_expr(value)?;
1334 self.buf.push_str(";\n");
1335 Ok(())
1336 }
1337 NodeKind::If {
1338 let_pattern,
1339 condition,
1340 then_block,
1341 else_block,
1342 } => {
1343 if let Some(pat) = let_pattern {
1344 let ind = self.indent_str();
1345 let _ = write!(self.buf, "{ind}if (");
1346 self.emit_expr(condition)?;
1347 self.buf.push_str(" != null) {\n");
1348 self.indent += 1;
1349 let binding = self.pattern_to_ts_destructure(pat);
1350 self.writeln(&format!("const {binding} = "));
1351 self.emit_block_body(then_block)?;
1352 self.indent -= 1;
1353 } else {
1354 let ind = self.indent_str();
1355 let _ = write!(self.buf, "{ind}if (");
1356 self.emit_expr(condition)?;
1357 self.buf.push_str(") {\n");
1358 self.indent += 1;
1359 self.emit_block_body(then_block)?;
1360 self.indent -= 1;
1361 }
1362 if let Some(else_b) = else_block {
1363 if matches!(else_b.kind, NodeKind::If { .. }) {
1364 let ind = self.indent_str();
1365 let _ = write!(self.buf, "{ind}}} else ");
1366 self.emit_stmt(else_b)?;
1367 return Ok(());
1368 }
1369 self.writeln("} else {");
1370 self.indent += 1;
1371 self.emit_block_body(else_b)?;
1372 self.indent -= 1;
1373 }
1374 self.writeln("}");
1375 Ok(())
1376 }
1377 NodeKind::For {
1378 pattern,
1379 iterable,
1380 body,
1381 } => {
1382 let binding = self.pattern_to_ts_destructure(pattern);
1383 let ind = self.indent_str();
1384 let _ = write!(self.buf, "{ind}for (const {binding} of ");
1385 self.emit_expr(iterable)?;
1386 self.buf.push_str(") {\n");
1387 self.indent += 1;
1388 self.emit_block_body(body)?;
1389 self.indent -= 1;
1390 self.writeln("}");
1391 Ok(())
1392 }
1393 NodeKind::While { condition, body } => {
1394 let ind = self.indent_str();
1395 let _ = write!(self.buf, "{ind}while (");
1396 self.emit_expr(condition)?;
1397 self.buf.push_str(") {\n");
1398 self.indent += 1;
1399 self.emit_block_body(body)?;
1400 self.indent -= 1;
1401 self.writeln("}");
1402 Ok(())
1403 }
1404 NodeKind::Loop { body } => {
1405 self.writeln("while (true) {");
1406 self.indent += 1;
1407 self.emit_block_body(body)?;
1408 self.indent -= 1;
1409 self.writeln("}");
1410 Ok(())
1411 }
1412 NodeKind::Return { value } => {
1413 if let Some(val) = value {
1414 let ind = self.indent_str();
1415 let _ = write!(self.buf, "{ind}return ");
1416 self.emit_expr(val)?;
1417 self.buf.push_str(";\n");
1418 } else {
1419 self.writeln("return;");
1420 }
1421 Ok(())
1422 }
1423 NodeKind::Break { value } => {
1424 if let Some(val) = value {
1425 let ind = self.indent_str();
1426 let _ = write!(self.buf, "{ind}/* break value: ");
1427 self.emit_expr(val)?;
1428 self.buf.push_str(" */ break;\n");
1429 } else {
1430 self.writeln("break;");
1431 }
1432 Ok(())
1433 }
1434 NodeKind::Continue => {
1435 self.writeln("continue;");
1436 Ok(())
1437 }
1438 NodeKind::Guard {
1439 condition,
1440 else_block,
1441 ..
1442 } => {
1443 let ind = self.indent_str();
1444 let _ = write!(self.buf, "{ind}if (!(");
1445 self.emit_expr(condition)?;
1446 self.buf.push_str(")) {\n");
1447 self.indent += 1;
1448 self.emit_block_body(else_block)?;
1449 self.indent -= 1;
1450 self.writeln("}");
1451 Ok(())
1452 }
1453 NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms),
1454 NodeKind::Block { stmts, tail } => {
1455 self.writeln("{");
1456 self.indent += 1;
1457 for s in stmts {
1458 self.emit_node(s)?;
1459 }
1460 if let Some(t) = tail {
1461 self.write_indent();
1462 self.emit_expr(t)?;
1463 self.buf.push_str(";\n");
1464 }
1465 self.indent -= 1;
1466 self.writeln("}");
1467 Ok(())
1468 }
1469 NodeKind::HandlingBlock { handlers, body } => {
1470 self.writeln("{");
1472 self.indent += 1;
1473 let old_handler_vars = self.current_handler_vars.clone();
1474 for h in handlers {
1475 let effect_name =
1476 h.effect.segments.last().map_or("effect", |s| s.name.as_str());
1477 let var_name = format!("__{}", to_camel_case(effect_name));
1478 let type_name = effect_name;
1479 let ind = self.indent_str();
1480 let _ = write!(self.buf, "{ind}const {var_name}: {type_name} = ");
1481 self.emit_expr(&h.handler)?;
1482 self.buf.push_str(";\n");
1483 self.current_handler_vars
1484 .insert(effect_name.to_string(), var_name);
1485 }
1486 if let NodeKind::Block { stmts, tail } = &body.kind {
1487 for s in stmts {
1488 self.emit_node(s)?;
1489 }
1490 if let Some(t) = tail {
1491 self.write_indent();
1492 self.emit_expr(t)?;
1493 self.buf.push_str(";\n");
1494 }
1495 } else {
1496 self.emit_stmt(body)?;
1497 }
1498 self.current_handler_vars = old_handler_vars;
1499 self.indent -= 1;
1500 self.writeln("}");
1501 Ok(())
1502 }
1503 NodeKind::Assign { op, target, value } => {
1504 let ind = self.indent_str();
1505 let _ = write!(self.buf, "{ind}");
1506 self.emit_expr(target)?;
1507 let op_str = match op {
1508 AssignOp::Assign => "=",
1509 AssignOp::AddAssign => "+=",
1510 AssignOp::SubAssign => "-=",
1511 AssignOp::MulAssign => "*=",
1512 AssignOp::DivAssign => "/=",
1513 AssignOp::RemAssign => "%=",
1514 };
1515 let _ = write!(self.buf, " {op_str} ");
1516 self.emit_expr(value)?;
1517 self.buf.push_str(";\n");
1518 Ok(())
1519 }
1520 _ => {
1521 self.write_indent();
1522 self.emit_expr(node)?;
1523 self.buf.push_str(";\n");
1524 Ok(())
1525 }
1526 }
1527 }
1528
1529 fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1532 self.mark_span(node.span);
1533 match &node.kind {
1534 NodeKind::Literal { lit } => {
1535 match lit {
1536 Literal::Int(s) => self.buf.push_str(s),
1537 Literal::Float(s) => self.buf.push_str(s),
1538 Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
1539 Literal::Char(s) => {
1540 self.buf.push('\'');
1541 self.buf.push_str(s);
1542 self.buf.push('\'');
1543 }
1544 Literal::String(s) => {
1545 self.buf.push('"');
1546 self.buf.push_str(&escape_js_string(s));
1547 self.buf.push('"');
1548 }
1549 Literal::Unit => self.buf.push_str("undefined"),
1550 }
1551 Ok(())
1552 }
1553 NodeKind::Identifier { name } => {
1554 if name.name == "None" {
1555 self.buf.push_str("{ _tag: \"None\" as const }");
1556 } else {
1557 self.buf.push_str(&to_camel_case(&name.name));
1558 }
1559 Ok(())
1560 }
1561 NodeKind::BinaryOp { op, left, right } => {
1562 self.buf.push('(');
1563 self.emit_expr(left)?;
1564 let op_str = match op {
1565 BinOp::Add => " + ",
1566 BinOp::Sub => " - ",
1567 BinOp::Mul => " * ",
1568 BinOp::Div => " / ",
1569 BinOp::Rem => " % ",
1570 BinOp::Pow => " ** ",
1571 BinOp::Eq => " === ",
1572 BinOp::Ne => " !== ",
1573 BinOp::Lt => " < ",
1574 BinOp::Le => " <= ",
1575 BinOp::Gt => " > ",
1576 BinOp::Ge => " >= ",
1577 BinOp::And => " && ",
1578 BinOp::Or => " || ",
1579 BinOp::BitAnd => " & ",
1580 BinOp::BitOr => " | ",
1581 BinOp::BitXor => " ^ ",
1582 BinOp::Compose => " /* >> */ ",
1583 BinOp::Is => " instanceof ",
1584 };
1585 self.buf.push_str(op_str);
1586 self.emit_expr(right)?;
1587 self.buf.push(')');
1588 Ok(())
1589 }
1590 NodeKind::UnaryOp { op, operand } => {
1591 let op_str = match op {
1592 UnaryOp::Neg => "-",
1593 UnaryOp::Not => "!",
1594 UnaryOp::BitNot => "~",
1595 };
1596 self.buf.push_str(op_str);
1597 self.emit_expr(operand)?;
1598 Ok(())
1599 }
1600 NodeKind::Call { callee, args, .. } => {
1601 if let Some(code) = self.map_prelude_call(callee, args)? {
1602 self.buf.push_str(&code);
1603 return Ok(());
1604 }
1605 if self.try_emit_prelude_ctor(callee, args)? {
1606 return Ok(());
1607 }
1608 if self.try_emit_time_assoc_call(callee, args)? {
1609 return Ok(());
1610 }
1611 if self.try_emit_time_desugared_method(callee, args)? {
1612 return Ok(());
1613 }
1614 if self.try_emit_concurrency_call(callee, args)? {
1615 return Ok(());
1616 }
1617 if let NodeKind::Identifier { name } = &callee.kind {
1619 if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
1620 if let Some(handler_var) =
1621 self.current_handler_vars.get(&effect_name).cloned()
1622 {
1623 let _ = write!(self.buf, "{}.{}", handler_var, name.name);
1624 self.buf.push('(');
1625 for (i, arg) in args.iter().enumerate() {
1626 if i > 0 {
1627 self.buf.push_str(", ");
1628 }
1629 self.emit_expr(&arg.value)?;
1630 }
1631 self.buf.push(')');
1632 return Ok(());
1633 }
1634 }
1635 }
1636 let effects_arg = if let NodeKind::Identifier { name } = &callee.kind {
1638 self.build_effects_call_arg_ts(&name.name)
1639 } else {
1640 None
1641 };
1642 self.emit_expr(callee)?;
1643 self.buf.push('(');
1644 for (i, arg) in args.iter().enumerate() {
1645 if i > 0 {
1646 self.buf.push_str(", ");
1647 }
1648 self.emit_expr(&arg.value)?;
1649 }
1650 if let Some(ea) = effects_arg {
1651 if !args.is_empty() {
1652 self.buf.push_str(", ");
1653 }
1654 self.buf.push_str(&ea);
1655 }
1656 self.buf.push(')');
1657 Ok(())
1658 }
1659 NodeKind::MethodCall {
1660 receiver,
1661 method,
1662 args,
1663 ..
1664 } => {
1665 if self.try_emit_time_method(receiver, &method.name, args)? {
1666 return Ok(());
1667 }
1668 self.emit_expr(receiver)?;
1669 let _ = write!(self.buf, ".{}", to_camel_case(&method.name));
1670 self.buf.push('(');
1671 for (i, arg) in args.iter().enumerate() {
1672 if i > 0 {
1673 self.buf.push_str(", ");
1674 }
1675 self.emit_expr(&arg.value)?;
1676 }
1677 self.buf.push(')');
1678 Ok(())
1679 }
1680 NodeKind::FieldAccess { object, field } => {
1681 self.emit_expr(object)?;
1682 let _ = write!(self.buf, ".{}", field.name);
1683 Ok(())
1684 }
1685 NodeKind::Index { object, index } => {
1686 self.emit_expr(object)?;
1687 self.buf.push('[');
1688 self.emit_expr(index)?;
1689 self.buf.push(']');
1690 Ok(())
1691 }
1692 NodeKind::Lambda { params, body } => {
1693 let param_list = self.collect_typed_params(params);
1694 let _ = write!(self.buf, "({}) => ", param_list.join(", "));
1695 if matches!(body.kind, NodeKind::Block { .. }) {
1696 self.buf.push_str("{\n");
1697 self.indent += 1;
1698 self.emit_block_body(body)?;
1699 self.indent -= 1;
1700 self.write_indent();
1701 self.buf.push('}');
1702 } else {
1703 self.emit_expr(body)?;
1704 }
1705 Ok(())
1706 }
1707 NodeKind::Pipe { left, right } => self.emit_pipe(left, right),
1708 NodeKind::Compose { left, right } => {
1709 let _ = write!(self.buf, "((x: any) => ");
1710 self.emit_expr(right)?;
1711 self.buf.push('(');
1712 self.emit_expr(left)?;
1713 self.buf.push_str("(x)))");
1714 Ok(())
1715 }
1716 NodeKind::Await { expr } => {
1717 self.buf.push_str("(await ");
1718 self.emit_expr(expr)?;
1719 self.buf.push(')');
1720 Ok(())
1721 }
1722 NodeKind::Propagate { expr } => {
1723 self.emit_expr(expr)?;
1724 Ok(())
1725 }
1726 NodeKind::Range { lo, hi, inclusive } => {
1727 if *inclusive {
1728 self.buf.push_str("rangeInclusive(");
1729 } else {
1730 self.buf.push_str("range(");
1731 }
1732 self.emit_expr(lo)?;
1733 self.buf.push_str(", ");
1734 self.emit_expr(hi)?;
1735 self.buf.push(')');
1736 Ok(())
1737 }
1738 NodeKind::RecordConstruct {
1739 path,
1740 fields,
1741 spread,
1742 } => {
1743 let type_name = path
1744 .segments
1745 .last()
1746 .map(|s| s.name.as_str())
1747 .unwrap_or("");
1748 let is_class = self.record_names.contains(type_name);
1749 if is_class {
1750 let _ = write!(self.buf, "new {type_name}(");
1751 if fields.is_empty() && spread.is_none() {
1752 self.buf.push(')');
1753 return Ok(());
1754 }
1755 }
1756 if let Some(sp) = spread {
1757 self.buf.push_str("{ ...");
1758 self.emit_expr(sp)?;
1759 if !fields.is_empty() {
1760 self.buf.push_str(", ");
1761 }
1762 } else {
1763 self.buf.push_str("{ ");
1764 }
1765 for (i, f) in fields.iter().enumerate() {
1766 if i > 0 {
1767 self.buf.push_str(", ");
1768 }
1769 if let Some(val) = &f.value {
1770 let _ = write!(self.buf, "{}: ", f.name.name);
1771 self.emit_expr(val)?;
1772 } else {
1773 self.buf.push_str(&f.name.name);
1774 }
1775 }
1776 self.buf.push_str(" }");
1777 if is_class {
1778 self.buf.push(')');
1779 }
1780 Ok(())
1781 }
1782 NodeKind::ListLiteral { elems } => {
1783 self.buf.push('[');
1784 for (i, e) in elems.iter().enumerate() {
1785 if i > 0 {
1786 self.buf.push_str(", ");
1787 }
1788 self.emit_expr(e)?;
1789 }
1790 self.buf.push(']');
1791 Ok(())
1792 }
1793 NodeKind::MapLiteral { entries } => {
1794 self.buf.push_str("new Map([");
1795 for (i, entry) in entries.iter().enumerate() {
1796 if i > 0 {
1797 self.buf.push_str(", ");
1798 }
1799 self.buf.push('[');
1800 self.emit_expr(&entry.key)?;
1801 self.buf.push_str(", ");
1802 self.emit_expr(&entry.value)?;
1803 self.buf.push(']');
1804 }
1805 self.buf.push_str("])");
1806 Ok(())
1807 }
1808 NodeKind::SetLiteral { elems } => {
1809 self.buf.push_str("new Set([");
1810 for (i, e) in elems.iter().enumerate() {
1811 if i > 0 {
1812 self.buf.push_str(", ");
1813 }
1814 self.emit_expr(e)?;
1815 }
1816 self.buf.push_str("])");
1817 Ok(())
1818 }
1819 NodeKind::TupleLiteral { elems } => {
1820 self.buf.push('[');
1822 for (i, e) in elems.iter().enumerate() {
1823 if i > 0 {
1824 self.buf.push_str(", ");
1825 }
1826 self.emit_expr(e)?;
1827 }
1828 self.buf.push(']');
1829 Ok(())
1830 }
1831 NodeKind::Interpolation { parts } => {
1832 self.buf.push('`');
1833 for part in parts {
1834 match part {
1835 AirInterpolationPart::Literal(s) => {
1836 self.buf.push_str(&escape_template_literal(s));
1837 }
1838 AirInterpolationPart::Expr(expr) => {
1839 self.buf.push_str("${");
1840 self.emit_expr(expr)?;
1841 self.buf.push('}');
1842 }
1843 }
1844 }
1845 self.buf.push('`');
1846 Ok(())
1847 }
1848 NodeKind::Placeholder => {
1849 self.buf.push('_');
1850 Ok(())
1851 }
1852 NodeKind::Unreachable => {
1853 self.buf
1854 .push_str("(() => { throw new Error(\"unreachable\"); })()");
1855 Ok(())
1856 }
1857 NodeKind::ResultConstruct { variant, value } => {
1858 match variant {
1859 ResultVariant::Ok => {
1860 self.buf.push_str("{ _tag: \"Ok\" as const, value: ");
1861 if let Some(v) = value {
1862 self.emit_expr(v)?;
1863 } else {
1864 self.buf.push_str("undefined");
1865 }
1866 self.buf.push_str(" }");
1867 }
1868 ResultVariant::Err => {
1869 self.buf.push_str("{ _tag: \"Err\" as const, error: ");
1870 if let Some(v) = value {
1871 self.emit_expr(v)?;
1872 } else {
1873 self.buf.push_str("undefined");
1874 }
1875 self.buf.push_str(" }");
1876 }
1877 }
1878 Ok(())
1879 }
1880 NodeKind::Assign { op, target, value } => {
1881 self.emit_expr(target)?;
1882 let op_str = match op {
1883 AssignOp::Assign => " = ",
1884 AssignOp::AddAssign => " += ",
1885 AssignOp::SubAssign => " -= ",
1886 AssignOp::MulAssign => " *= ",
1887 AssignOp::DivAssign => " /= ",
1888 AssignOp::RemAssign => " %= ",
1889 };
1890 self.buf.push_str(op_str);
1891 self.emit_expr(value)?;
1892 Ok(())
1893 }
1894 NodeKind::If {
1895 condition,
1896 then_block,
1897 else_block,
1898 ..
1899 } => {
1900 self.buf.push('(');
1902 self.emit_expr(condition)?;
1903 self.buf.push_str(" ? ");
1904 self.emit_block_as_expr(then_block)?;
1905 self.buf.push_str(" : ");
1906 if let Some(eb) = else_block {
1907 self.emit_block_as_expr(eb)?;
1908 } else {
1909 self.buf.push_str("undefined");
1910 }
1911 self.buf.push(')');
1912 Ok(())
1913 }
1914 NodeKind::Block { stmts, tail } => {
1915 self.buf.push_str("(() => {\n");
1917 self.indent += 1;
1918 for s in stmts {
1919 self.emit_node(s)?;
1920 }
1921 if let Some(t) = tail {
1922 let ind = self.indent_str();
1923 let _ = write!(self.buf, "{ind}return ");
1924 self.emit_expr(t)?;
1925 self.buf.push_str(";\n");
1926 }
1927 self.indent -= 1;
1928 self.write_indent();
1929 self.buf.push_str("})()");
1930 Ok(())
1931 }
1932 NodeKind::Match { scrutinee, arms } => {
1933 self.buf.push_str("(() => {\n");
1935 self.indent += 1;
1936 self.emit_match(scrutinee, arms)?;
1937 self.indent -= 1;
1938 self.write_indent();
1939 self.buf.push_str("})()");
1940 Ok(())
1941 }
1942 NodeKind::Move { expr }
1944 | NodeKind::Borrow { expr }
1945 | NodeKind::MutableBorrow { expr } => self.emit_expr(expr),
1946 NodeKind::EffectOp {
1948 effect,
1949 operation,
1950 args,
1951 } => {
1952 let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
1953 let _ = write!(
1954 self.buf,
1955 "{}.{}",
1956 to_camel_case(effect_name),
1957 operation.name
1958 );
1959 self.buf.push('(');
1960 for (i, arg) in args.iter().enumerate() {
1961 if i > 0 {
1962 self.buf.push_str(", ");
1963 }
1964 self.emit_expr(&arg.value)?;
1965 }
1966 self.buf.push(')');
1967 Ok(())
1968 }
1969 NodeKind::TypeNamed { .. }
1971 | NodeKind::TypeTuple { .. }
1972 | NodeKind::TypeFunction { .. }
1973 | NodeKind::TypeOptional { .. }
1974 | NodeKind::TypeSelf => {
1975 let ty_str = self.type_to_ts(node);
1976 let _ = write!(self.buf, "/* {ty_str} */");
1977 Ok(())
1978 }
1979 NodeKind::EffectRef { path } => {
1980 let name = path
1981 .segments
1982 .iter()
1983 .map(|s| s.name.as_str())
1984 .collect::<Vec<_>>()
1985 .join(".");
1986 self.buf.push_str(&name);
1987 Ok(())
1988 }
1989 NodeKind::Error => {
1990 self.buf.push_str("/* error */");
1991 Ok(())
1992 }
1993 _ => {
1994 self.buf.push_str("/* unsupported */");
1995 Ok(())
1996 }
1997 }
1998 }
1999
2000 fn emit_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<(), CodegenError> {
2003 let is_adt = arms.iter().any(|arm| {
2004 if let NodeKind::MatchArm { pattern, .. } = &arm.kind {
2005 matches!(pattern.kind, NodeKind::ConstructorPat { .. })
2006 } else {
2007 false
2008 }
2009 });
2010
2011 if is_adt {
2012 let ind = self.indent_str();
2013 let _ = write!(self.buf, "{ind}switch (");
2014 self.emit_expr(scrutinee)?;
2015 self.buf.push_str("._tag) {\n");
2016 } else {
2017 let ind = self.indent_str();
2018 let _ = write!(self.buf, "{ind}switch (");
2019 self.emit_expr(scrutinee)?;
2020 self.buf.push_str(") {\n");
2021 }
2022 self.indent += 1;
2023 for arm in arms {
2024 self.emit_match_arm(arm, is_adt, scrutinee)?;
2025 }
2026 self.indent -= 1;
2027 self.writeln("}");
2028 Ok(())
2029 }
2030
2031 fn emit_match_arm(
2032 &mut self,
2033 arm: &AIRNode,
2034 is_adt: bool,
2035 scrutinee: &AIRNode,
2036 ) -> Result<(), CodegenError> {
2037 if let NodeKind::MatchArm {
2038 pattern,
2039 guard,
2040 body,
2041 } = &arm.kind
2042 {
2043 match &pattern.kind {
2044 NodeKind::WildcardPat => {
2045 self.writeln("default: {");
2046 }
2047 NodeKind::BindPat { name, .. } if !is_adt => {
2048 self.writeln("default: {");
2049 self.indent += 1;
2050 let ind = self.indent_str();
2051 let _ = write!(self.buf, "{ind}const {} = ", name.name);
2052 self.emit_expr(scrutinee)?;
2053 self.buf.push_str(";\n");
2054 self.indent -= 1;
2055 }
2056 NodeKind::LiteralPat { lit } => {
2057 let ind = self.indent_str();
2058 let _ = write!(self.buf, "{ind}case ");
2059 match lit {
2060 Literal::Int(s) => self.buf.push_str(s),
2061 Literal::Float(s) => self.buf.push_str(s),
2062 Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
2063 Literal::Char(s) => {
2064 self.buf.push('\'');
2065 self.buf.push_str(s);
2066 self.buf.push('\'');
2067 }
2068 Literal::String(s) => {
2069 self.buf.push('"');
2070 self.buf.push_str(&escape_js_string(s));
2071 self.buf.push('"');
2072 }
2073 Literal::Unit => self.buf.push_str("undefined"),
2074 }
2075 self.buf.push_str(": {\n");
2076 }
2077 NodeKind::ConstructorPat { path, fields } => {
2078 let variant_name = path.segments.last().map_or("_", |s| s.name.as_str());
2079 self.writeln(&format!("case \"{variant_name}\": {{"));
2080 if !fields.is_empty() {
2081 self.indent += 1;
2082 for (i, field) in fields.iter().enumerate() {
2083 let binding = self.pattern_to_binding_name(field);
2084 let ind = self.indent_str();
2085 let _ = write!(self.buf, "{ind}const {binding} = ");
2086 self.emit_expr(scrutinee)?;
2087 let _ = writeln!(self.buf, "._{i};");
2088 }
2089 self.indent -= 1;
2090 }
2091 }
2092 NodeKind::RecordPat { path, fields, .. } => {
2093 let variant_name = path.segments.last().map_or("_", |s| s.name.as_str());
2094 if is_adt {
2095 self.writeln(&format!("case \"{variant_name}\": {{"));
2096 } else {
2097 self.writeln("default: {");
2098 }
2099 if !fields.is_empty() {
2100 self.indent += 1;
2101 for f in fields {
2102 let field_name = &f.name.name;
2103 if let Some(pat) = &f.pattern {
2104 let binding = self.pattern_to_binding_name(pat);
2105 let ind = self.indent_str();
2106 let _ = write!(self.buf, "{ind}const {binding} = ");
2107 self.emit_expr(scrutinee)?;
2108 let _ = writeln!(self.buf, ".{field_name};");
2109 } else {
2110 let ind = self.indent_str();
2111 let _ = write!(self.buf, "{ind}const {field_name} = ");
2112 self.emit_expr(scrutinee)?;
2113 let _ = writeln!(self.buf, ".{field_name};");
2114 }
2115 }
2116 self.indent -= 1;
2117 }
2118 }
2119 _ => {
2120 self.writeln("default: {");
2121 }
2122 }
2123
2124 self.indent += 1;
2125 if let Some(g) = guard {
2126 let ind = self.indent_str();
2127 let _ = write!(self.buf, "{ind}if (!(");
2128 self.emit_expr(g)?;
2129 self.buf.push_str(")) break;\n");
2130 }
2131 self.emit_block_body(body)?;
2132 self.writeln("break;");
2133 self.indent -= 1;
2134 self.writeln("}");
2135 }
2136 Ok(())
2137 }
2138
2139 fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
2142 if let NodeKind::Call { callee, args, .. } = &right.kind {
2143 let has_placeholder = args
2144 .iter()
2145 .any(|a| matches!(a.value.kind, NodeKind::Placeholder));
2146 if has_placeholder {
2147 self.emit_expr(callee)?;
2148 self.buf.push('(');
2149 for (i, arg) in args.iter().enumerate() {
2150 if i > 0 {
2151 self.buf.push_str(", ");
2152 }
2153 if matches!(arg.value.kind, NodeKind::Placeholder) {
2154 self.emit_expr(left)?;
2155 } else {
2156 self.emit_expr(&arg.value)?;
2157 }
2158 }
2159 self.buf.push(')');
2160 return Ok(());
2161 }
2162 }
2163 self.emit_expr(right)?;
2164 self.buf.push('(');
2165 self.emit_expr(left)?;
2166 self.buf.push(')');
2167 Ok(())
2168 }
2169
2170 fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
2173 if let NodeKind::Block { stmts, tail } = &node.kind {
2174 for s in stmts {
2175 self.emit_node(s)?;
2176 }
2177 if let Some(t) = tail {
2178 let ind = self.indent_str();
2179 let _ = write!(self.buf, "{ind}return ");
2180 self.emit_expr(t)?;
2181 self.buf.push_str(";\n");
2182 }
2183 } else {
2184 let ind = self.indent_str();
2185 let _ = write!(self.buf, "{ind}return ");
2186 self.emit_expr(node)?;
2187 self.buf.push_str(";\n");
2188 }
2189 Ok(())
2190 }
2191
2192 fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
2193 if let NodeKind::Block { stmts, tail } = &node.kind {
2194 if stmts.is_empty() {
2195 if let Some(t) = tail {
2196 return self.emit_expr(t);
2197 }
2198 }
2199 }
2200 self.emit_expr(node)
2201 }
2202
2203 fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
2204 match &pat.kind {
2205 NodeKind::BindPat { name, .. } => to_camel_case(&name.name),
2206 NodeKind::WildcardPat => "_".into(),
2207 NodeKind::TuplePat { elems } => {
2208 format!(
2209 "[{}]",
2210 elems
2211 .iter()
2212 .map(|e| self.pattern_to_binding_name(e))
2213 .collect::<Vec<_>>()
2214 .join(", ")
2215 )
2216 }
2217 NodeKind::RecordPat { fields, .. } => {
2218 format!(
2219 "{{ {} }}",
2220 fields
2221 .iter()
2222 .map(|f| to_camel_case(&f.name.name).to_string())
2223 .collect::<Vec<_>>()
2224 .join(", ")
2225 )
2226 }
2227 _ => "_".into(),
2228 }
2229 }
2230
2231 fn pattern_to_ts_destructure(&self, pat: &AIRNode) -> String {
2232 self.pattern_to_binding_name(pat)
2233 }
2234
2235 fn type_expr_to_string(&self, node: &AIRNode) -> String {
2236 match &node.kind {
2237 NodeKind::TypeNamed { path, .. } => path
2238 .segments
2239 .iter()
2240 .map(|s| s.name.as_str())
2241 .collect::<Vec<_>>()
2242 .join("."),
2243 NodeKind::Identifier { name } => name.name.clone(),
2244 _ => "Unknown".into(),
2245 }
2246 }
2247}
2248
2249fn build_ts_return_type(is_async: bool, inner: Option<String>) -> String {
2255 match (is_async, inner) {
2256 (true, Some(t)) => format!(": Promise<{t}>"),
2257 (true, None) => ": Promise<void>".to_string(),
2258 (false, Some(t)) => format!(": {t}"),
2259 (false, None) => String::new(),
2260 }
2261}
2262
2263fn is_time_method_name(name: &str) -> bool {
2266 matches!(
2267 name,
2268 "as_nanos"
2269 | "as_millis"
2270 | "as_seconds"
2271 | "is_zero"
2272 | "is_negative"
2273 | "abs"
2274 | "elapsed"
2275 | "duration_since"
2276 )
2277}
2278
2279fn to_camel_case(s: &str) -> String {
2281 if s.is_empty() || s == "_" {
2282 return s.to_string();
2283 }
2284 if !s.contains('_') && s.starts_with(|c: char| c.is_lowercase()) {
2286 return s.to_string();
2287 }
2288 if s.contains('_') {
2290 let parts: Vec<&str> = s.split('_').filter(|p| !p.is_empty()).collect();
2291 if parts.is_empty() {
2292 return s.to_string();
2293 }
2294 let mut result = parts[0].to_lowercase();
2295 for part in &parts[1..] {
2296 let mut chars = part.chars();
2297 if let Some(first) = chars.next() {
2298 result.push(
2299 first
2300 .to_uppercase()
2301 .next()
2302 .expect("uppercase yields at least one char"),
2303 );
2304 result.extend(chars);
2305 }
2306 }
2307 return result;
2308 }
2309 let mut chars = s.chars();
2311 let first = chars.next().expect("non-empty string guaranteed by caller");
2312 let mut result = first.to_lowercase().to_string();
2313 result.extend(chars);
2314 result
2315}
2316
2317fn escape_js_string(s: &str) -> String {
2319 let mut out = String::with_capacity(s.len());
2320 for ch in s.chars() {
2321 match ch {
2322 '"' => out.push_str("\\\""),
2323 '\\' => out.push_str("\\\\"),
2324 '\n' => out.push_str("\\n"),
2325 '\r' => out.push_str("\\r"),
2326 '\t' => out.push_str("\\t"),
2327 _ => out.push(ch),
2328 }
2329 }
2330 out
2331}
2332
2333fn escape_template_literal(s: &str) -> String {
2335 let mut out = String::with_capacity(s.len());
2336 for ch in s.chars() {
2337 match ch {
2338 '`' => out.push_str("\\`"),
2339 '\\' => out.push_str("\\\\"),
2340 '$' => out.push_str("\\$"),
2341 _ => out.push(ch),
2342 }
2343 }
2344 out
2345}
2346
2347#[cfg(test)]
2350mod tests {
2351 use super::*;
2352 use bock_air::{AirArg, AirRecordField};
2353 use bock_ast::{GenericParam, Ident, TypeExpr, TypePath};
2354 use bock_errors::{FileId, Span};
2355
2356 fn span() -> Span {
2357 Span {
2358 file: FileId(0),
2359 start: 0,
2360 end: 0,
2361 }
2362 }
2363
2364 fn ident(name: &str) -> Ident {
2365 Ident {
2366 name: name.to_string(),
2367 span: span(),
2368 }
2369 }
2370
2371 fn type_path(segments: &[&str]) -> TypePath {
2372 TypePath {
2373 segments: segments.iter().map(|s| ident(s)).collect(),
2374 span: span(),
2375 }
2376 }
2377
2378 fn node(id: u32, kind: NodeKind) -> AIRNode {
2379 AIRNode::new(id, span(), kind)
2380 }
2381
2382 fn int_lit(id: u32, val: &str) -> AIRNode {
2383 node(
2384 id,
2385 NodeKind::Literal {
2386 lit: Literal::Int(val.into()),
2387 },
2388 )
2389 }
2390
2391 fn str_lit(id: u32, val: &str) -> AIRNode {
2392 node(
2393 id,
2394 NodeKind::Literal {
2395 lit: Literal::String(val.into()),
2396 },
2397 )
2398 }
2399
2400 fn id_node(id: u32, name: &str) -> AIRNode {
2401 node(id, NodeKind::Identifier { name: ident(name) })
2402 }
2403
2404 fn bind_pat(id: u32, name: &str) -> AIRNode {
2405 node(
2406 id,
2407 NodeKind::BindPat {
2408 name: ident(name),
2409 is_mut: false,
2410 },
2411 )
2412 }
2413
2414 fn typed_param_node(id: u32, name: &str, ty_name: &str) -> AIRNode {
2415 node(
2416 id,
2417 NodeKind::Param {
2418 pattern: Box::new(bind_pat(id + 100, name)),
2419 ty: Some(Box::new(node(
2420 id + 200,
2421 NodeKind::TypeNamed {
2422 path: type_path(&[ty_name]),
2423 args: vec![],
2424 },
2425 ))),
2426 default: None,
2427 },
2428 )
2429 }
2430
2431 fn type_node(id: u32, name: &str) -> AIRNode {
2432 node(
2433 id,
2434 NodeKind::TypeNamed {
2435 path: type_path(&[name]),
2436 args: vec![],
2437 },
2438 )
2439 }
2440
2441 fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
2442 node(
2443 id,
2444 NodeKind::Block {
2445 stmts,
2446 tail: tail.map(Box::new),
2447 },
2448 )
2449 }
2450
2451 fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
2452 node(
2453 0,
2454 NodeKind::Module {
2455 path: None,
2456 annotations: vec![],
2457 imports,
2458 items,
2459 },
2460 )
2461 }
2462
2463 fn gen(module: &AIRNode) -> String {
2464 let gen = TsGenerator::new();
2465 let result = gen.generate_module(module).unwrap();
2466 result.files[0].content.clone()
2467 }
2468
2469 fn make_generic_param(name: &str) -> GenericParam {
2470 GenericParam {
2471 id: 0,
2472 span: span(),
2473 name: ident(name),
2474 bounds: vec![],
2475 }
2476 }
2477
2478 fn make_bounded_generic_param(name: &str, bounds: &[&str]) -> GenericParam {
2479 GenericParam {
2480 id: 0,
2481 span: span(),
2482 name: ident(name),
2483 bounds: bounds.iter().map(|b| type_path(&[b])).collect(),
2484 }
2485 }
2486
2487 fn make_type_expr(name: &str) -> TypeExpr {
2488 TypeExpr::Named {
2489 id: 0,
2490 span: span(),
2491 path: type_path(&[name]),
2492 args: vec![],
2493 }
2494 }
2495
2496 fn make_record_field(name: &str, ty_name: &str) -> bock_ast::RecordDeclField {
2497 bock_ast::RecordDeclField {
2498 id: 0,
2499 span: span(),
2500 name: ident(name),
2501 ty: make_type_expr(ty_name),
2502 default: None,
2503 }
2504 }
2505
2506 #[test]
2509 fn implements_code_generator_trait() {
2510 let gen = TsGenerator::new();
2511 assert_eq!(gen.target().id, "ts");
2512 }
2513
2514 #[test]
2515 fn empty_module() {
2516 let m = module(vec![], vec![]);
2517 let out = gen(&m);
2518 assert_eq!(out, "");
2519 }
2520
2521 #[test]
2522 fn output_has_ts_extension() {
2523 let gen = TsGenerator::new();
2524 let m = module(vec![], vec![]);
2525 let result = gen.generate_module(&m).unwrap();
2526 assert_eq!(result.files[0].path.to_str().unwrap(), "output.ts");
2527 }
2528
2529 #[test]
2532 fn function_with_type_annotations() {
2533 let body = block(10, vec![], Some(id_node(11, "x")));
2534 let f = node(
2535 1,
2536 NodeKind::FnDecl {
2537 annotations: vec![],
2538 visibility: Visibility::Public,
2539 is_async: false,
2540 name: ident("add"),
2541 generic_params: vec![],
2542 params: vec![
2543 typed_param_node(2, "x", "Int"),
2544 typed_param_node(3, "y", "Int"),
2545 ],
2546 return_type: Some(Box::new(type_node(4, "Int"))),
2547 effect_clause: vec![],
2548 where_clause: vec![],
2549 body: Box::new(body),
2550 },
2551 );
2552 let out = gen(&module(vec![], vec![f]));
2553 assert!(out.contains("x: number"), "got: {out}");
2554 assert!(out.contains("y: number"), "got: {out}");
2555 assert!(out.contains("): number"), "got: {out}");
2556 assert!(out.contains("export function add"));
2557 }
2558
2559 #[test]
2560 fn function_without_type_annotations() {
2561 let body = block(10, vec![], Some(int_lit(11, "42")));
2562 let f = node(
2563 1,
2564 NodeKind::FnDecl {
2565 annotations: vec![],
2566 visibility: Visibility::Private,
2567 is_async: false,
2568 name: ident("answer"),
2569 generic_params: vec![],
2570 params: vec![],
2571 return_type: None,
2572 effect_clause: vec![],
2573 where_clause: vec![],
2574 body: Box::new(body),
2575 },
2576 );
2577 let out = gen(&module(vec![], vec![f]));
2578 assert!(out.contains("function answer()"), "got: {out}");
2579 assert!(!out.contains("export"), "got: {out}");
2580 }
2581
2582 #[test]
2585 fn function_with_generics() {
2586 let body = block(10, vec![], Some(id_node(11, "x")));
2587 let f = node(
2588 1,
2589 NodeKind::FnDecl {
2590 annotations: vec![],
2591 visibility: Visibility::Private,
2592 is_async: false,
2593 name: ident("identity"),
2594 generic_params: vec![make_generic_param("T")],
2595 params: vec![typed_param_node(2, "x", "T")],
2596 return_type: Some(Box::new(type_node(3, "T"))),
2597 effect_clause: vec![],
2598 where_clause: vec![],
2599 body: Box::new(body),
2600 },
2601 );
2602 let out = gen(&module(vec![], vec![f]));
2603 assert!(out.contains("function identity<T>"), "got: {out}");
2604 assert!(out.contains("x: T"), "got: {out}");
2605 assert!(out.contains("): T"), "got: {out}");
2606 }
2607
2608 #[test]
2609 fn generics_with_bounds() {
2610 let body = block(10, vec![], Some(id_node(11, "x")));
2611 let f = node(
2612 1,
2613 NodeKind::FnDecl {
2614 annotations: vec![],
2615 visibility: Visibility::Private,
2616 is_async: false,
2617 name: ident("sorted"),
2618 generic_params: vec![make_bounded_generic_param("T", &["Comparable"])],
2619 params: vec![typed_param_node(2, "x", "T")],
2620 return_type: Some(Box::new(type_node(3, "T"))),
2621 effect_clause: vec![],
2622 where_clause: vec![],
2623 body: Box::new(body),
2624 },
2625 );
2626 let out = gen(&module(vec![], vec![f]));
2627 assert!(out.contains("T extends Comparable"), "got: {out}");
2628 }
2629
2630 #[test]
2633 fn trait_becomes_interface() {
2634 let method = node(
2635 2,
2636 NodeKind::FnDecl {
2637 annotations: vec![],
2638 visibility: Visibility::Public,
2639 is_async: false,
2640 name: ident("area"),
2641 generic_params: vec![],
2642 params: vec![],
2643 return_type: Some(Box::new(type_node(3, "Float"))),
2644 effect_clause: vec![],
2645 where_clause: vec![],
2646 body: Box::new(block(4, vec![], None)),
2647 },
2648 );
2649 let trait_decl = node(
2650 1,
2651 NodeKind::TraitDecl {
2652 annotations: vec![],
2653 visibility: Visibility::Public,
2654 is_platform: false,
2655 name: ident("Shape"),
2656 generic_params: vec![],
2657 associated_types: vec![],
2658 methods: vec![method],
2659 },
2660 );
2661 let out = gen(&module(vec![], vec![trait_decl]));
2662 assert!(out.contains("export interface Shape"), "got: {out}");
2663 assert!(out.contains("area(): number"), "got: {out}");
2664 }
2665
2666 #[test]
2667 fn trait_with_generics() {
2668 let method = node(
2669 2,
2670 NodeKind::FnDecl {
2671 annotations: vec![],
2672 visibility: Visibility::Public,
2673 is_async: false,
2674 name: ident("compare"),
2675 generic_params: vec![],
2676 params: vec![typed_param_node(3, "other", "T")],
2677 return_type: Some(Box::new(type_node(4, "Int"))),
2678 effect_clause: vec![],
2679 where_clause: vec![],
2680 body: Box::new(block(5, vec![], None)),
2681 },
2682 );
2683 let trait_decl = node(
2684 1,
2685 NodeKind::TraitDecl {
2686 annotations: vec![],
2687 visibility: Visibility::Public,
2688 is_platform: false,
2689 name: ident("Comparable"),
2690 generic_params: vec![make_generic_param("T")],
2691 associated_types: vec![],
2692 methods: vec![method],
2693 },
2694 );
2695 let out = gen(&module(vec![], vec![trait_decl]));
2696 assert!(out.contains("interface Comparable<T>"), "got: {out}");
2697 assert!(out.contains("compare(other: T): number"), "got: {out}");
2698 }
2699
2700 #[test]
2703 fn record_becomes_interface_and_factory() {
2704 let record = node(
2705 1,
2706 NodeKind::RecordDecl {
2707 annotations: vec![],
2708 visibility: Visibility::Public,
2709 name: ident("Point"),
2710 generic_params: vec![],
2711 fields: vec![
2712 make_record_field("x", "Float"),
2713 make_record_field("y", "Float"),
2714 ],
2715 },
2716 );
2717 let out = gen(&module(vec![], vec![record]));
2718 assert!(out.contains("export class Point"), "got: {out}");
2719 assert!(out.contains("x: number"), "got: {out}");
2720 assert!(out.contains("y: number"), "got: {out}");
2721 assert!(
2722 out.contains("constructor({ x, y }: { x: number; y: number })"),
2723 "got: {out}"
2724 );
2725 assert!(out.contains("this.x = x;"), "got: {out}");
2726 assert!(out.contains("this.y = y;"), "got: {out}");
2727 }
2728
2729 #[test]
2732 fn enum_becomes_discriminated_union() {
2733 let variants = vec![
2734 node(
2735 2,
2736 NodeKind::EnumVariant {
2737 name: ident("None"),
2738 payload: EnumVariantPayload::Unit,
2739 },
2740 ),
2741 node(
2742 3,
2743 NodeKind::EnumVariant {
2744 name: ident("Some"),
2745 payload: EnumVariantPayload::Struct(vec![make_record_field("value", "T")]),
2746 },
2747 ),
2748 ];
2749 let enum_decl = node(
2750 1,
2751 NodeKind::EnumDecl {
2752 annotations: vec![],
2753 visibility: Visibility::Public,
2754 name: ident("Option"),
2755 generic_params: vec![make_generic_param("T")],
2756 variants,
2757 },
2758 );
2759 let out = gen(&module(vec![], vec![enum_decl]));
2760 assert!(
2762 out.contains("export type Option<T> = Option_None | Option_Some;"),
2763 "got: {out}"
2764 );
2765 assert!(out.contains("interface Option_None"), "got: {out}");
2767 assert!(out.contains("readonly _tag: \"None\""), "got: {out}");
2768 assert!(out.contains("interface Option_Some<T>"), "got: {out}");
2770 assert!(out.contains("readonly value: T"), "got: {out}");
2771 assert!(
2772 out.contains("function Option_Some<T>(value: T): Option_Some"),
2773 "got: {out}"
2774 );
2775 }
2776
2777 #[test]
2780 fn type_alias_emitted() {
2781 let alias = node(
2782 1,
2783 NodeKind::TypeAlias {
2784 annotations: vec![],
2785 visibility: Visibility::Public,
2786 name: ident("UserId"),
2787 generic_params: vec![],
2788 ty: Box::new(type_node(2, "String")),
2789 where_clause: vec![],
2790 },
2791 );
2792 let out = gen(&module(vec![], vec![alias]));
2793 assert!(out.contains("export type UserId = string;"), "got: {out}");
2794 }
2795
2796 #[test]
2797 fn generic_type_alias() {
2798 let alias = node(
2799 1,
2800 NodeKind::TypeAlias {
2801 annotations: vec![],
2802 visibility: Visibility::Private,
2803 name: ident("Pair"),
2804 generic_params: vec![make_generic_param("A"), make_generic_param("B")],
2805 ty: Box::new(node(
2806 2,
2807 NodeKind::TypeTuple {
2808 elems: vec![type_node(3, "A"), type_node(4, "B")],
2809 },
2810 )),
2811 where_clause: vec![],
2812 },
2813 );
2814 let out = gen(&module(vec![], vec![alias]));
2815 assert!(out.contains("type Pair<A, B> = [A, B];"), "got: {out}");
2816 }
2817
2818 #[test]
2821 fn effects_as_typed_params() {
2822 let body = block(10, vec![], None);
2823 let f = node(
2824 1,
2825 NodeKind::FnDecl {
2826 annotations: vec![],
2827 visibility: Visibility::Private,
2828 is_async: false,
2829 name: ident("process"),
2830 generic_params: vec![],
2831 params: vec![typed_param_node(2, "data", "String")],
2832 return_type: None,
2833 effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
2834 where_clause: vec![],
2835 body: Box::new(body),
2836 },
2837 );
2838 let out = gen(&module(vec![], vec![f]));
2839 assert!(
2840 out.contains("{ log, clock }: { log: Log, clock: Clock }"),
2841 "got: {out}"
2842 );
2843 }
2844
2845 #[test]
2848 fn async_function_with_types() {
2849 let body = block(10, vec![], Some(str_lit(11, "done")));
2850 let f = node(
2851 1,
2852 NodeKind::FnDecl {
2853 annotations: vec![],
2854 visibility: Visibility::Public,
2855 is_async: true,
2856 name: ident("fetch"),
2857 generic_params: vec![],
2858 params: vec![typed_param_node(2, "url", "String")],
2859 return_type: Some(Box::new(type_node(3, "String"))),
2860 effect_clause: vec![],
2861 where_clause: vec![],
2862 body: Box::new(body),
2863 },
2864 );
2865 let out = gen(&module(vec![], vec![f]));
2866 assert!(out.contains("export async function fetch"), "got: {out}");
2867 assert!(out.contains("url: string"), "got: {out}");
2868 assert!(out.contains("): Promise<string>"), "got: {out}");
2870 }
2871
2872 #[test]
2873 fn async_function_without_return_type_is_promise_void() {
2874 let body = block(10, vec![], None);
2875 let f = node(
2876 1,
2877 NodeKind::FnDecl {
2878 annotations: vec![],
2879 visibility: Visibility::Private,
2880 is_async: true,
2881 name: ident("tick"),
2882 generic_params: vec![],
2883 params: vec![],
2884 return_type: None,
2885 effect_clause: vec![],
2886 where_clause: vec![],
2887 body: Box::new(body),
2888 },
2889 );
2890 let out = gen(&module(vec![], vec![f]));
2891 assert!(out.contains("async function tick()"), "got: {out}");
2892 assert!(out.contains("): Promise<void>"), "got: {out}");
2893 }
2894
2895 #[test]
2896 fn sync_function_return_type_unchanged() {
2897 let body = block(10, vec![], Some(str_lit(11, "done")));
2898 let f = node(
2899 1,
2900 NodeKind::FnDecl {
2901 annotations: vec![],
2902 visibility: Visibility::Private,
2903 is_async: false,
2904 name: ident("hello"),
2905 generic_params: vec![],
2906 params: vec![],
2907 return_type: Some(Box::new(type_node(2, "String"))),
2908 effect_clause: vec![],
2909 where_clause: vec![],
2910 body: Box::new(body),
2911 },
2912 );
2913 let out = gen(&module(vec![], vec![f]));
2914 assert!(out.contains("function hello(): string"), "got: {out}");
2915 assert!(!out.contains("Promise"), "got: {out}");
2916 }
2917
2918 #[test]
2919 fn entry_invocation_async_main_ts() {
2920 let inv = TsGenerator::new().entry_invocation(true).unwrap();
2921 assert!(inv.contains("async () =>"));
2922 assert!(inv.contains("await main()"));
2923 }
2924
2925 #[test]
2926 fn generate_project_async_main_wraps_entry_ts() {
2927 let main_fn = node(
2928 1,
2929 NodeKind::FnDecl {
2930 annotations: vec![],
2931 visibility: Visibility::Private,
2932 is_async: true,
2933 name: ident("main"),
2934 generic_params: vec![],
2935 params: vec![],
2936 return_type: None,
2937 effect_clause: vec![],
2938 where_clause: vec![],
2939 body: Box::new(block(2, vec![], None)),
2940 },
2941 );
2942 let m = module(vec![], vec![main_fn]);
2943 let gen = TsGenerator::new();
2944 let out = gen.generate_project(&[&m]).unwrap();
2945 let src = &out.files[0].content;
2946 assert!(src.contains("async function main()"), "got: {src}");
2947 assert!(
2948 src.contains("(async () => { await main(); })();"),
2949 "got: {src}"
2950 );
2951 }
2952
2953 #[test]
2956 fn let_binding_with_type() {
2957 let stmt = node(
2958 1,
2959 NodeKind::LetBinding {
2960 is_mut: false,
2961 pattern: Box::new(bind_pat(2, "x")),
2962 ty: Some(Box::new(type_node(3, "Int"))),
2963 value: Box::new(int_lit(4, "42")),
2964 },
2965 );
2966 let f = node(
2967 5,
2968 NodeKind::FnDecl {
2969 annotations: vec![],
2970 visibility: Visibility::Private,
2971 is_async: false,
2972 name: ident("test"),
2973 generic_params: vec![],
2974 params: vec![],
2975 return_type: None,
2976 effect_clause: vec![],
2977 where_clause: vec![],
2978 body: Box::new(block(6, vec![stmt], None)),
2979 },
2980 );
2981 let out = gen(&module(vec![], vec![f]));
2982 assert!(out.contains("const x: number = 42;"), "got: {out}");
2983 }
2984
2985 #[test]
2986 fn mutable_binding_with_type() {
2987 let stmt = node(
2988 1,
2989 NodeKind::LetBinding {
2990 is_mut: true,
2991 pattern: Box::new(bind_pat(2, "count")),
2992 ty: Some(Box::new(type_node(3, "Int"))),
2993 value: Box::new(int_lit(4, "0")),
2994 },
2995 );
2996 let f = node(
2997 5,
2998 NodeKind::FnDecl {
2999 annotations: vec![],
3000 visibility: Visibility::Private,
3001 is_async: false,
3002 name: ident("test"),
3003 generic_params: vec![],
3004 params: vec![],
3005 return_type: None,
3006 effect_clause: vec![],
3007 where_clause: vec![],
3008 body: Box::new(block(6, vec![stmt], None)),
3009 },
3010 );
3011 let out = gen(&module(vec![], vec![f]));
3012 assert!(out.contains("let count: number = 0;"), "got: {out}");
3013 }
3014
3015 #[test]
3018 fn type_mapping_primitives() {
3019 let ctx = TsEmitCtx::new();
3020 assert_eq!(ctx.map_type_name("Int"), "number");
3021 assert_eq!(ctx.map_type_name("Float"), "number");
3022 assert_eq!(ctx.map_type_name("Bool"), "boolean");
3023 assert_eq!(ctx.map_type_name("String"), "string");
3024 assert_eq!(ctx.map_type_name("Void"), "void");
3025 assert_eq!(ctx.map_type_name("Unit"), "void");
3026 assert_eq!(ctx.map_type_name("List"), "Array");
3027 assert_eq!(ctx.map_type_name("CustomType"), "CustomType");
3028 }
3029
3030 #[test]
3031 fn optional_type_emitted() {
3032 let ctx = TsEmitCtx::new();
3033 let opt = node(
3034 1,
3035 NodeKind::TypeOptional {
3036 inner: Box::new(type_node(2, "String")),
3037 },
3038 );
3039 assert_eq!(ctx.type_to_ts(&opt), "string | null");
3040 }
3041
3042 #[test]
3043 fn generic_type_args() {
3044 let ctx = TsEmitCtx::new();
3045 let list_of_int = node(
3046 1,
3047 NodeKind::TypeNamed {
3048 path: type_path(&["List"]),
3049 args: vec![type_node(2, "Int")],
3050 },
3051 );
3052 assert_eq!(ctx.type_to_ts(&list_of_int), "Array<number>");
3053 }
3054
3055 #[test]
3056 fn function_type() {
3057 let ctx = TsEmitCtx::new();
3058 let fn_type = node(
3059 1,
3060 NodeKind::TypeFunction {
3061 params: vec![type_node(2, "Int"), type_node(3, "String")],
3062 ret: Box::new(type_node(4, "Bool")),
3063 effects: vec![],
3064 },
3065 );
3066 assert_eq!(
3067 ctx.type_to_ts(&fn_type),
3068 "(arg0: number, arg1: string) => boolean"
3069 );
3070 }
3071
3072 #[test]
3073 fn tuple_type() {
3074 let ctx = TsEmitCtx::new();
3075 let tuple = node(
3076 1,
3077 NodeKind::TypeTuple {
3078 elems: vec![type_node(2, "Int"), type_node(3, "String")],
3079 },
3080 );
3081 assert_eq!(ctx.type_to_ts(&tuple), "[number, string]");
3082 }
3083
3084 #[test]
3087 fn const_with_type() {
3088 let c = node(
3089 1,
3090 NodeKind::ConstDecl {
3091 annotations: vec![],
3092 visibility: Visibility::Public,
3093 name: ident("PI"),
3094 ty: Box::new(type_node(2, "Float")),
3095 value: Box::new(node(
3096 3,
3097 NodeKind::Literal {
3098 lit: Literal::Float("3.14159".into()),
3099 },
3100 )),
3101 },
3102 );
3103 let out = gen(&module(vec![], vec![c]));
3104 assert!(
3105 out.contains("export const PI: number = 3.14159;"),
3106 "got: {out}"
3107 );
3108 }
3109
3110 #[test]
3113 fn class_with_typed_fields() {
3114 let class = node(
3115 1,
3116 NodeKind::ClassDecl {
3117 annotations: vec![],
3118 visibility: Visibility::Public,
3119 name: ident("Point"),
3120 generic_params: vec![],
3121 base: None,
3122 traits: vec![],
3123 fields: vec![
3124 make_record_field("x", "Float"),
3125 make_record_field("y", "Float"),
3126 ],
3127 methods: vec![],
3128 },
3129 );
3130 let out = gen(&module(vec![], vec![class]));
3131 assert!(out.contains("export class Point"), "got: {out}");
3132 assert!(out.contains("x: number;"), "got: {out}");
3133 assert!(out.contains("y: number;"), "got: {out}");
3134 assert!(
3135 out.contains("constructor(x: number, y: number)"),
3136 "got: {out}"
3137 );
3138 }
3139
3140 #[test]
3143 fn effect_becomes_interface() {
3144 let op = node(
3145 2,
3146 NodeKind::FnDecl {
3147 annotations: vec![],
3148 visibility: Visibility::Public,
3149 is_async: false,
3150 name: ident("log"),
3151 generic_params: vec![],
3152 params: vec![typed_param_node(3, "msg", "String")],
3153 return_type: Some(Box::new(type_node(4, "Void"))),
3154 effect_clause: vec![],
3155 where_clause: vec![],
3156 body: Box::new(block(5, vec![], None)),
3157 },
3158 );
3159 let effect = node(
3160 1,
3161 NodeKind::EffectDecl {
3162 annotations: vec![],
3163 visibility: Visibility::Public,
3164 name: ident("Logger"),
3165 generic_params: vec![],
3166 components: vec![],
3167 operations: vec![op],
3168 },
3169 );
3170 let out = gen(&module(vec![], vec![effect]));
3171 assert!(out.contains("interface Logger"), "got: {out}");
3172 assert!(out.contains("log(msg: string): void"), "got: {out}");
3173 }
3174
3175 #[test]
3178 fn ownership_erased() {
3179 let move_expr = node(
3180 1,
3181 NodeKind::Move {
3182 expr: Box::new(id_node(2, "x")),
3183 },
3184 );
3185 let borrow_expr = node(
3186 3,
3187 NodeKind::Borrow {
3188 expr: Box::new(id_node(4, "y")),
3189 },
3190 );
3191 let stmts = vec![
3192 node(
3193 5,
3194 NodeKind::LetBinding {
3195 is_mut: false,
3196 pattern: Box::new(bind_pat(6, "a")),
3197 ty: None,
3198 value: Box::new(move_expr),
3199 },
3200 ),
3201 node(
3202 7,
3203 NodeKind::LetBinding {
3204 is_mut: false,
3205 pattern: Box::new(bind_pat(8, "b")),
3206 ty: None,
3207 value: Box::new(borrow_expr),
3208 },
3209 ),
3210 ];
3211 let f = node(
3212 9,
3213 NodeKind::FnDecl {
3214 annotations: vec![],
3215 visibility: Visibility::Private,
3216 is_async: false,
3217 name: ident("test"),
3218 generic_params: vec![],
3219 params: vec![],
3220 return_type: None,
3221 effect_clause: vec![],
3222 where_clause: vec![],
3223 body: Box::new(block(10, stmts, None)),
3224 },
3225 );
3226 let out = gen(&module(vec![], vec![f]));
3227 assert!(out.contains("const a = x;"), "got: {out}");
3228 assert!(out.contains("const b = y;"), "got: {out}");
3229 }
3230
3231 #[test]
3234 fn string_interpolation() {
3235 let interp = node(
3236 1,
3237 NodeKind::Interpolation {
3238 parts: vec![
3239 AirInterpolationPart::Literal("Hello, ".into()),
3240 AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
3241 AirInterpolationPart::Literal("!".into()),
3242 ],
3243 },
3244 );
3245 let stmt = node(
3246 3,
3247 NodeKind::LetBinding {
3248 is_mut: false,
3249 pattern: Box::new(bind_pat(4, "msg")),
3250 ty: None,
3251 value: Box::new(interp),
3252 },
3253 );
3254 let f = node(
3255 5,
3256 NodeKind::FnDecl {
3257 annotations: vec![],
3258 visibility: Visibility::Private,
3259 is_async: false,
3260 name: ident("test"),
3261 generic_params: vec![],
3262 params: vec![],
3263 return_type: None,
3264 effect_clause: vec![],
3265 where_clause: vec![],
3266 body: Box::new(block(6, vec![stmt], None)),
3267 },
3268 );
3269 let out = gen(&module(vec![], vec![f]));
3270 assert!(out.contains("`Hello, ${name}!`"), "got: {out}");
3271 }
3272
3273 #[test]
3276 fn collections() {
3277 let list = node(
3278 1,
3279 NodeKind::ListLiteral {
3280 elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
3281 },
3282 );
3283 let map = node(
3284 5,
3285 NodeKind::MapLiteral {
3286 entries: vec![bock_air::AirMapEntry {
3287 key: str_lit(6, "a"),
3288 value: int_lit(7, "1"),
3289 }],
3290 },
3291 );
3292 let set = node(
3293 8,
3294 NodeKind::SetLiteral {
3295 elems: vec![int_lit(9, "1"), int_lit(10, "2")],
3296 },
3297 );
3298 let stmts = vec![
3299 node(
3300 11,
3301 NodeKind::LetBinding {
3302 is_mut: false,
3303 pattern: Box::new(bind_pat(12, "xs")),
3304 ty: None,
3305 value: Box::new(list),
3306 },
3307 ),
3308 node(
3309 13,
3310 NodeKind::LetBinding {
3311 is_mut: false,
3312 pattern: Box::new(bind_pat(14, "m")),
3313 ty: None,
3314 value: Box::new(map),
3315 },
3316 ),
3317 node(
3318 15,
3319 NodeKind::LetBinding {
3320 is_mut: false,
3321 pattern: Box::new(bind_pat(16, "s")),
3322 ty: None,
3323 value: Box::new(set),
3324 },
3325 ),
3326 ];
3327 let f = node(
3328 17,
3329 NodeKind::FnDecl {
3330 annotations: vec![],
3331 visibility: Visibility::Private,
3332 is_async: false,
3333 name: ident("test"),
3334 generic_params: vec![],
3335 params: vec![],
3336 return_type: None,
3337 effect_clause: vec![],
3338 where_clause: vec![],
3339 body: Box::new(block(18, stmts, None)),
3340 },
3341 );
3342 let out = gen(&module(vec![], vec![f]));
3343 assert!(out.contains("[1, 2, 3]"), "got: {out}");
3344 assert!(out.contains("new Map("), "got: {out}");
3345 assert!(out.contains("new Set("), "got: {out}");
3346 }
3347
3348 #[test]
3351 fn result_construct_has_as_const() {
3352 let ok = node(
3353 1,
3354 NodeKind::ResultConstruct {
3355 variant: ResultVariant::Ok,
3356 value: Some(Box::new(int_lit(2, "42"))),
3357 },
3358 );
3359 let stmt = node(
3360 3,
3361 NodeKind::LetBinding {
3362 is_mut: false,
3363 pattern: Box::new(bind_pat(4, "r")),
3364 ty: None,
3365 value: Box::new(ok),
3366 },
3367 );
3368 let f = node(
3369 5,
3370 NodeKind::FnDecl {
3371 annotations: vec![],
3372 visibility: Visibility::Private,
3373 is_async: false,
3374 name: ident("test"),
3375 generic_params: vec![],
3376 params: vec![],
3377 return_type: None,
3378 effect_clause: vec![],
3379 where_clause: vec![],
3380 body: Box::new(block(6, vec![stmt], None)),
3381 },
3382 );
3383 let out = gen(&module(vec![], vec![f]));
3384 assert!(out.contains("\"Ok\" as const"), "got: {out}");
3385 }
3386
3387 #[test]
3390 fn record_construct() {
3391 let rc = node(
3392 1,
3393 NodeKind::RecordConstruct {
3394 path: type_path(&["Point"]),
3395 fields: vec![
3396 AirRecordField {
3397 name: ident("x"),
3398 value: Some(Box::new(int_lit(2, "1"))),
3399 },
3400 AirRecordField {
3401 name: ident("y"),
3402 value: Some(Box::new(int_lit(3, "2"))),
3403 },
3404 ],
3405 spread: None,
3406 },
3407 );
3408 let stmt = node(
3409 4,
3410 NodeKind::LetBinding {
3411 is_mut: false,
3412 pattern: Box::new(bind_pat(5, "p")),
3413 ty: None,
3414 value: Box::new(rc),
3415 },
3416 );
3417 let f = node(
3418 6,
3419 NodeKind::FnDecl {
3420 annotations: vec![],
3421 visibility: Visibility::Private,
3422 is_async: false,
3423 name: ident("test"),
3424 generic_params: vec![],
3425 params: vec![],
3426 return_type: None,
3427 effect_clause: vec![],
3428 where_clause: vec![],
3429 body: Box::new(block(7, vec![stmt], None)),
3430 },
3431 );
3432 let out = gen(&module(vec![], vec![f]));
3433 assert!(out.contains("{ x: 1, y: 2 }"), "got: {out}");
3434 }
3435
3436 #[test]
3437 fn to_camel_case_converts_snake_case() {
3438 assert_eq!(to_camel_case("create_user"), "createUser");
3439 assert_eq!(to_camel_case("get_all_items"), "getAllItems");
3440 assert_eq!(to_camel_case("Log"), "log");
3441 assert_eq!(to_camel_case("createUser"), "createUser");
3442 assert_eq!(to_camel_case("_"), "_");
3443 assert_eq!(to_camel_case(""), "");
3444 }
3445
3446 #[test]
3447 fn snake_case_fn_becomes_camel_case_ts() {
3448 let body = block(2, vec![], Some(int_lit(3, "42")));
3449 let f = node(
3450 1,
3451 NodeKind::FnDecl {
3452 annotations: vec![],
3453 visibility: Visibility::Private,
3454 is_async: false,
3455 name: ident("create_user"),
3456 generic_params: vec![],
3457 params: vec![typed_param_node(4, "name", "String")],
3458 return_type: Some(Box::new(type_node(5, "Int"))),
3459 effect_clause: vec![],
3460 where_clause: vec![],
3461 body: Box::new(body),
3462 },
3463 );
3464 let out = gen(&module(vec![], vec![f]));
3465 assert!(
3466 out.contains("function createUser("),
3467 "expected camelCase function name, got: {out}"
3468 );
3469 assert!(
3470 out.contains("name: string"),
3471 "expected type annotations, got: {out}"
3472 );
3473 }
3474
3475 fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
3479 let call = node(
3480 10,
3481 NodeKind::Call {
3482 callee: Box::new(id_node(11, func_name)),
3483 args: vec![AirArg {
3484 label: None,
3485 value: arg,
3486 }],
3487 type_args: vec![],
3488 },
3489 );
3490 let body = block(2, vec![call], None);
3491 let f = node(
3492 1,
3493 NodeKind::FnDecl {
3494 name: ident("main"),
3495 params: vec![],
3496 return_type: None,
3497 body: Box::new(body),
3498 generic_params: vec![],
3499 visibility: Visibility::Private,
3500 annotations: vec![],
3501 effect_clause: vec![],
3502 where_clause: vec![],
3503 is_async: false,
3504 },
3505 );
3506 gen(&module(vec![], vec![f]))
3507 }
3508
3509 fn gen_prelude_call_no_args(func_name: &str) -> String {
3511 let call = node(
3512 10,
3513 NodeKind::Call {
3514 callee: Box::new(id_node(11, func_name)),
3515 args: vec![],
3516 type_args: vec![],
3517 },
3518 );
3519 let body = block(2, vec![call], None);
3520 let f = node(
3521 1,
3522 NodeKind::FnDecl {
3523 name: ident("main"),
3524 params: vec![],
3525 return_type: None,
3526 body: Box::new(body),
3527 generic_params: vec![],
3528 visibility: Visibility::Private,
3529 annotations: vec![],
3530 effect_clause: vec![],
3531 where_clause: vec![],
3532 is_async: false,
3533 },
3534 );
3535 gen(&module(vec![], vec![f]))
3536 }
3537
3538 #[test]
3539 fn prelude_println_maps_to_console_log() {
3540 let out = gen_prelude_call("println", str_lit(12, "hello"));
3541 assert!(
3542 out.contains("console.log("),
3543 "println should map to console.log, got: {out}"
3544 );
3545 assert!(
3546 !out.contains("println("),
3547 "should not emit bare println(, got: {out}"
3548 );
3549 }
3550
3551 #[test]
3552 fn prelude_print_maps_to_process_stdout() {
3553 let out = gen_prelude_call("print", str_lit(12, "hello"));
3554 assert!(
3555 out.contains("process.stdout.write(String("),
3556 "print should map to process.stdout.write, got: {out}"
3557 );
3558 }
3559
3560 #[test]
3561 fn prelude_debug_maps_to_console_debug() {
3562 let out = gen_prelude_call("debug", str_lit(12, "val"));
3563 assert!(
3564 out.contains("console.debug("),
3565 "debug should map to console.debug, got: {out}"
3566 );
3567 }
3568
3569 #[test]
3570 fn prelude_assert_maps_to_throw() {
3571 let arg = node(
3572 12,
3573 NodeKind::Literal {
3574 lit: Literal::Bool(true),
3575 },
3576 );
3577 let out = gen_prelude_call("assert", arg);
3578 assert!(
3579 out.contains("if (!true) throw new Error(\"assertion failed\")"),
3580 "assert should map to if-throw, got: {out}"
3581 );
3582 }
3583
3584 #[test]
3585 fn prelude_todo_maps_to_throw_not_implemented() {
3586 let out = gen_prelude_call_no_args("todo");
3587 assert!(
3588 out.contains("throw new Error(\"not implemented\")"),
3589 "todo should map to throw, got: {out}"
3590 );
3591 }
3592
3593 #[test]
3594 fn prelude_unreachable_maps_to_throw_unreachable() {
3595 let out = gen_prelude_call_no_args("unreachable");
3596 assert!(
3597 out.contains("throw new Error(\"unreachable\")"),
3598 "unreachable should map to throw, got: {out}"
3599 );
3600 }
3601
3602 #[test]
3603 fn non_prelude_call_passes_through() {
3604 let out = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
3605 assert!(
3606 out.contains("myCustomFunc("),
3607 "non-prelude call should use camelCase, got: {out}"
3608 );
3609 }
3610
3611 #[test]
3612 fn handling_block_passes_handlers_to_effectful_call() {
3613 use bock_air::AirHandlerPair;
3614
3615 let effect_decl = node(
3616 1,
3617 NodeKind::EffectDecl {
3618 annotations: vec![],
3619 visibility: Visibility::Public,
3620 name: ident("Logger"),
3621 generic_params: vec![],
3622 components: vec![],
3623 operations: vec![node(
3624 2,
3625 NodeKind::FnDecl {
3626 annotations: vec![],
3627 visibility: Visibility::Public,
3628 is_async: false,
3629 name: ident("log"),
3630 generic_params: vec![],
3631 params: vec![typed_param_node(3, "msg", "String")],
3632 return_type: None,
3633 effect_clause: vec![],
3634 where_clause: vec![],
3635 body: Box::new(block(4, vec![], None)),
3636 },
3637 )],
3638 },
3639 );
3640
3641 let inner_fn = node(
3642 10,
3643 NodeKind::FnDecl {
3644 annotations: vec![],
3645 visibility: Visibility::Private,
3646 is_async: false,
3647 name: ident("inner"),
3648 generic_params: vec![],
3649 params: vec![],
3650 return_type: None,
3651 effect_clause: vec![type_path(&["Logger"])],
3652 where_clause: vec![],
3653 body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
3654 },
3655 );
3656
3657 let call_inner = node(
3658 20,
3659 NodeKind::Call {
3660 callee: Box::new(id_node(21, "inner")),
3661 args: vec![],
3662 type_args: vec![],
3663 },
3664 );
3665 let handling = node(
3666 30,
3667 NodeKind::HandlingBlock {
3668 handlers: vec![AirHandlerPair {
3669 effect: type_path(&["Logger"]),
3670 handler: Box::new(node(
3671 31,
3672 NodeKind::Call {
3673 callee: Box::new(id_node(32, "StdoutLogger")),
3674 args: vec![],
3675 type_args: vec![],
3676 },
3677 )),
3678 }],
3679 body: Box::new(block(33, vec![], Some(call_inner))),
3680 },
3681 );
3682 let main_fn = node(
3683 40,
3684 NodeKind::FnDecl {
3685 annotations: vec![],
3686 visibility: Visibility::Private,
3687 is_async: false,
3688 name: ident("main"),
3689 generic_params: vec![],
3690 params: vec![],
3691 return_type: None,
3692 effect_clause: vec![],
3693 where_clause: vec![],
3694 body: Box::new(block(41, vec![handling], None)),
3695 },
3696 );
3697
3698 let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
3699 assert!(
3701 out.contains("inner({ logger: __logger })"),
3702 "handling block should pass handler to effectful call, got: {out}"
3703 );
3704 assert!(
3705 out.contains("const __logger: Logger = stdoutLogger()"),
3706 "handling block should instantiate handler with type, got: {out}"
3707 );
3708 }
3709
3710 #[test]
3711 fn record_becomes_class() {
3712 let rec = node(
3713 1,
3714 NodeKind::RecordDecl {
3715 annotations: vec![],
3716 visibility: Visibility::Public,
3717 name: ident("ConsoleLogger"),
3718 generic_params: vec![],
3719 fields: vec![],
3720 },
3721 );
3722 let out = gen(&module(vec![], vec![rec]));
3723 assert!(
3724 out.contains("export class ConsoleLogger {}"),
3725 "empty record should be an empty exported class, got: {out}"
3726 );
3727 }
3728
3729 #[test]
3730 fn impl_emits_interface_extension_for_declaration_merging() {
3731 use bock_air::AirHandlerPair;
3732 let _ = AirHandlerPair {
3733 effect: type_path(&["X"]),
3734 handler: Box::new(id_node(0, "x")),
3735 };
3736
3737 let effect_decl = node(
3738 1,
3739 NodeKind::EffectDecl {
3740 annotations: vec![],
3741 visibility: Visibility::Public,
3742 name: ident("Logger"),
3743 generic_params: vec![],
3744 components: vec![],
3745 operations: vec![node(
3746 2,
3747 NodeKind::FnDecl {
3748 annotations: vec![],
3749 visibility: Visibility::Public,
3750 is_async: false,
3751 name: ident("log"),
3752 generic_params: vec![],
3753 params: vec![typed_param_node(3, "msg", "String")],
3754 return_type: None,
3755 effect_clause: vec![],
3756 where_clause: vec![],
3757 body: Box::new(block(4, vec![], None)),
3758 },
3759 )],
3760 },
3761 );
3762
3763 let rec = node(
3764 5,
3765 NodeKind::RecordDecl {
3766 annotations: vec![],
3767 visibility: Visibility::Public,
3768 name: ident("StdLogger"),
3769 generic_params: vec![],
3770 fields: vec![],
3771 },
3772 );
3773
3774 let impl_block = node(
3775 10,
3776 NodeKind::ImplBlock {
3777 annotations: vec![],
3778 trait_path: Some(type_path(&["Logger"])),
3779 target: Box::new(type_node(11, "StdLogger")),
3780 generic_params: vec![],
3781 methods: vec![node(
3782 12,
3783 NodeKind::FnDecl {
3784 annotations: vec![],
3785 visibility: Visibility::Public,
3786 is_async: false,
3787 name: ident("log"),
3788 generic_params: vec![],
3789 params: vec![typed_param_node(13, "msg", "String")],
3790 return_type: None,
3791 effect_clause: vec![],
3792 where_clause: vec![],
3793 body: Box::new(block(14, vec![], None)),
3794 },
3795 )],
3796 where_clause: vec![],
3797 },
3798 );
3799
3800 let out = gen(&module(vec![], vec![effect_decl, rec, impl_block]));
3801 assert!(
3802 out.contains("interface StdLogger extends Logger {}"),
3803 "impl should emit interface extension for declaration merging, got: {out}"
3804 );
3805 assert!(
3806 out.contains("StdLogger.prototype.log"),
3807 "impl should attach method to prototype, got: {out}"
3808 );
3809 }
3810}