1use husk_ast::{
10 Block, EnumVariantFields, Expr, ExprKind, ExternItemKind, File, FormatSegment, FormatSpec,
11 Ident, ImplItemKind, ItemKind, LiteralKind, Param, Pattern, PatternKind, Span, Stmt, StmtKind,
12 StructField, TypeExpr, TypeExprKind, TypeParam,
13};
14use husk_runtime_js::std_preamble_js;
15use husk_semantic::{NameResolution, TypeResolution, VariantCallMap, VariantPatternMap};
16use sourcemap::SourceMapBuilder;
17use std::collections::HashMap;
18use std::path::Path;
19
20fn snake_to_camel(s: &str) -> String {
26 let mut result = String::with_capacity(s.len());
27 let mut capitalize_next = false;
28 for c in s.chars() {
29 if c == '_' {
30 capitalize_next = true;
31 } else if capitalize_next {
32 result.push(c.to_ascii_uppercase());
33 capitalize_next = false;
34 } else {
35 result.push(c);
36 }
37 }
38 result
39}
40
41#[derive(Debug, Clone, Default)]
48struct PropertyAccessors {
49 getters: HashMap<(String, String), String>,
52 setters: HashMap<(String, String), String>,
55 method_js_names: HashMap<(String, String), String>,
59 extern_methods: std::collections::HashSet<(String, String)>,
63}
64
65#[derive(Debug, Clone)]
75struct CodegenContext<'a> {
76 accessors: &'a PropertyAccessors,
78 source_path: Option<&'a Path>,
80 name_resolution: &'a NameResolution,
83 type_resolution: &'a TypeResolution,
86 variant_calls: &'a VariantCallMap,
88 variant_patterns: &'a VariantPatternMap,
90}
91
92impl<'a> CodegenContext<'a> {
93 fn new(
94 accessors: &'a PropertyAccessors,
95 name_resolution: &'a NameResolution,
96 type_resolution: &'a TypeResolution,
97 variant_calls: &'a VariantCallMap,
98 variant_patterns: &'a VariantPatternMap,
99 ) -> Self {
100 Self {
101 accessors,
102 source_path: None,
103 name_resolution,
104 type_resolution,
105 variant_calls,
106 variant_patterns,
107 }
108 }
109
110 fn with_source_path(
111 accessors: &'a PropertyAccessors,
112 source_path: &'a Path,
113 name_resolution: &'a NameResolution,
114 type_resolution: &'a TypeResolution,
115 variant_calls: &'a VariantCallMap,
116 variant_patterns: &'a VariantPatternMap,
117 ) -> Self {
118 Self {
119 accessors,
120 source_path: Some(source_path),
121 name_resolution,
122 type_resolution,
123 variant_calls,
124 variant_patterns,
125 }
126 }
127
128 fn resolve_name(&self, name: &str, span: &Span) -> String {
131 self.name_resolution
132 .get(&(span.range.start, span.range.end))
133 .cloned()
134 .unwrap_or_else(|| name.to_string())
135 }
136
137 fn fresh_temp(&self, prefix: &str) -> String {
139 use std::sync::atomic::{AtomicUsize, Ordering};
140 static COUNTER: AtomicUsize = AtomicUsize::new(0);
141 format!("{}_{}", prefix, COUNTER.fetch_add(1, Ordering::SeqCst))
142 }
143}
144
145fn handle_include_str(args: &[Expr], ctx: &CodegenContext) -> JsExpr {
150 if args.len() != 1 {
152 panic!(
153 "include_str: expected 1 argument, got {}",
154 args.len()
155 );
156 }
157
158 let path_arg = match &args[0].kind {
160 ExprKind::Literal(lit) => match &lit.kind {
161 LiteralKind::String(s) => s.clone(),
162 _ => {
163 panic!("include_str: argument must be a string literal");
164 }
165 },
166 _ => {
167 panic!("include_str: argument must be a string literal");
168 }
169 };
170
171 let source_path = match ctx.source_path {
173 Some(path) => path,
174 None => {
175 panic!(
176 "include_str: source file path not available. \
177 This is required to resolve the relative path '{}'",
178 path_arg
179 );
180 }
181 };
182
183 let base_dir = source_path.parent().unwrap_or(Path::new("."));
185 let full_path = base_dir.join(&path_arg);
186
187 match std::fs::read_to_string(&full_path) {
189 Ok(contents) => JsExpr::String(contents),
190 Err(e) => {
191 panic!(
192 "include_str: failed to read '{}': {}",
193 full_path.display(),
194 e
195 );
196 }
197 }
198}
199
200#[derive(Debug, Clone, PartialEq)]
202pub struct JsModule {
203 pub body: Vec<JsStmt>,
204}
205
206#[derive(Debug, Clone, PartialEq, Default)]
208pub struct SourceSpan {
209 pub line: u32,
211 pub column: u32,
213}
214
215pub fn offset_to_line_col(source: &str, offset: usize) -> (u32, u32) {
217 let mut line = 0u32;
218 let mut col = 0u32;
219 for (i, ch) in source.char_indices() {
220 if i >= offset {
221 break;
222 }
223 if ch == '\n' {
224 line += 1;
225 col = 0;
226 } else {
227 col += 1;
228 }
229 }
230 (line, col)
231}
232
233#[derive(Debug, Clone, PartialEq)]
235pub enum DestructurePattern {
236 Binding(String),
238 Wildcard,
240 Array(Vec<DestructurePattern>),
242}
243
244#[derive(Debug, Clone, PartialEq)]
246pub enum JsStmt {
247 Import { name: String, source: String },
249 NamedImport { names: Vec<String>, source: String },
251 Require { name: String, source: String },
253 NamedRequire { names: Vec<String>, source: String },
255 ExportNamed { names: Vec<String> },
257 Function {
259 name: String,
260 params: Vec<String>,
261 body: Vec<JsStmt>,
262 source_span: Option<SourceSpan>,
264 },
265 Return(JsExpr),
267 Let { name: String, init: Option<JsExpr> },
269 LetDestructure { pattern: Vec<DestructurePattern>, init: Option<JsExpr> },
271 Expr(JsExpr),
273 TryCatch {
275 try_block: Vec<JsStmt>,
276 catch_ident: String,
277 catch_block: Vec<JsStmt>,
278 },
279 If {
281 cond: JsExpr,
282 then_block: Vec<JsStmt>,
283 else_block: Option<Vec<JsStmt>>,
284 },
285 ForOf {
287 binding: String,
288 iterable: JsExpr,
289 body: Vec<JsStmt>,
290 },
291 For {
293 binding: String,
294 start: JsExpr,
295 end: JsExpr,
296 inclusive: bool,
297 body: Vec<JsStmt>,
298 },
299 Assign {
301 target: JsExpr,
302 op: JsAssignOp,
303 value: JsExpr,
304 },
305 While { cond: JsExpr, body: Vec<JsStmt> },
307 Break,
309 Continue,
311 Block(Vec<JsStmt>),
313 Sequence(Vec<JsStmt>),
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum JsAssignOp {
321 Assign, AddAssign, SubAssign, ModAssign, }
326
327#[derive(Debug, Clone, PartialEq)]
329pub enum JsExpr {
330 Ident(String),
331 Number(i64),
332 BigInt(i64),
333 Float(f64),
334 Bool(bool),
335 String(String),
336 Object(Vec<(String, JsExpr)>),
338 Member {
340 object: Box<JsExpr>,
341 property: String,
342 },
343 Assignment {
345 left: Box<JsExpr>,
346 op: JsAssignOp,
347 right: Box<JsExpr>,
348 },
349 Conditional {
351 test: Box<JsExpr>,
352 then_branch: Box<JsExpr>,
353 else_branch: Box<JsExpr>,
354 },
355 Call {
356 callee: Box<JsExpr>,
357 args: Vec<JsExpr>,
358 },
359 Binary {
360 op: JsBinaryOp,
361 left: Box<JsExpr>,
362 right: Box<JsExpr>,
363 },
364 Iife {
366 body: Vec<JsStmt>,
367 },
368 Function {
370 params: Vec<String>,
371 body: Vec<JsStmt>,
372 },
373 New {
375 constructor: String,
376 args: Vec<JsExpr>,
377 },
378 Arrow {
380 params: Vec<String>,
381 body: Vec<JsStmt>,
382 },
383 Array(Vec<JsExpr>),
385 Index {
387 object: Box<JsExpr>,
388 index: Box<JsExpr>,
389 },
390 Raw(String),
392 Unary {
394 op: JsUnaryOp,
395 expr: Box<JsExpr>,
396 },
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub enum JsUnaryOp {
402 Not, Neg, }
405
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408pub enum JsBinaryOp {
409 Add,
410 Sub,
411 Mul,
412 Div,
413 Mod,
414 EqEq,
415 NotEq,
416 Lt,
417 Gt,
418 Le,
419 Ge,
420 And,
421 Or,
422 StrictEq, StrictNe, }
425
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
428pub enum JsTarget {
429 Esm,
430 Cjs,
431}
432
433pub fn lower_file_to_js(
454 file: &File,
455 call_main: bool,
456 target: JsTarget,
457 name_resolution: &NameResolution,
458 type_resolution: &TypeResolution,
459 variant_calls: &VariantCallMap,
460 variant_patterns: &VariantPatternMap,
461) -> JsModule {
462 lower_file_to_js_with_source(file, call_main, target, None, None, name_resolution, type_resolution, variant_calls, variant_patterns)
463}
464
465pub fn lower_file_to_js_with_source(
479 file: &File,
480 call_main: bool,
481 target: JsTarget,
482 source: Option<&str>,
483 source_path: Option<&Path>,
484 name_resolution: &NameResolution,
485 type_resolution: &TypeResolution,
486 variant_calls: &VariantCallMap,
487 variant_patterns: &VariantPatternMap,
488) -> JsModule {
489 use std::collections::HashSet;
490
491 let mut imports = Vec::new();
492 let mut body = Vec::new();
493 let mut has_main_entry = false;
494 let mut fn_names: Vec<String> = Vec::new();
495
496 let mut extern_structs: HashSet<String> = HashSet::new();
498 for item in &file.items {
499 if let ItemKind::ExternBlock { abi, items } = &item.kind {
500 if abi == "js" {
501 for ext in items {
502 if let ExternItemKind::Struct { name, .. } = &ext.kind {
503 extern_structs.insert(name.name.clone());
504 }
505 }
506 }
507 }
508 }
509
510 let mut accessors = PropertyAccessors::default();
514 for item in &file.items {
515 if let ItemKind::Impl(impl_block) = &item.kind {
516 let type_name = type_expr_to_js_name(&impl_block.self_ty);
517 for impl_item in &impl_block.items {
518 match &impl_item.kind {
519 ImplItemKind::Property(prop) => {
521 let prop_name = &prop.name.name;
522 let js_name = prop
524 .js_name()
525 .map(|s| s.to_string())
526 .unwrap_or_else(|| snake_to_camel(prop_name));
527
528 if prop.has_getter() {
529 accessors.getters.insert(
531 (type_name.clone(), prop_name.clone()),
532 js_name.clone(),
533 );
534 }
535 if prop.has_setter() {
536 accessors.setters.insert(
538 (type_name.clone(), prop_name.clone()),
539 js_name,
540 );
541 }
542 }
543 ImplItemKind::Method(method) => {
546 let method_name = &method.name.name;
547
548 if let Some(js_name) = method.js_name() {
551 accessors.method_js_names.insert(
552 (type_name.clone(), method_name.clone()),
553 js_name.to_string(),
554 );
555 }
556
557 if method.is_extern && method.receiver.is_some() {
558 accessors.extern_methods.insert((type_name.clone(), method_name.clone()));
561
562 const NON_GETTER_METHODS: &[&str] = &[
565 "all", "toJsValue", "open", "run", "iterate", "bind", "pluck", "raw", "columns", ];
575 let is_non_getter = NON_GETTER_METHODS.contains(&method_name.as_str());
576
577 if method.params.is_empty() && method.ret_type.is_some() && !is_non_getter
579 {
580 accessors.getters.insert(
581 (type_name.clone(), method_name.clone()),
582 method_name.clone(),
583 );
584 }
585 else if method_name.starts_with("set_")
587 && method.params.len() == 1
588 && method.ret_type.is_none()
589 {
590 let prop_name =
591 method_name.strip_prefix("set_").unwrap().to_string();
592 accessors.setters.insert(
593 (type_name.clone(), method_name.clone()),
594 prop_name,
595 );
596 }
597 }
598 }
599 }
600 }
601 }
602 }
603
604 let ctx = match source_path {
606 Some(path) => CodegenContext::with_source_path(&accessors, path, name_resolution, type_resolution, variant_calls, variant_patterns),
607 None => CodegenContext::new(&accessors, name_resolution, type_resolution, variant_calls, variant_patterns),
608 };
609
610 for item in &file.items {
612 if let ItemKind::ExternBlock { abi, items } = &item.kind {
613 if abi == "js" {
614 for ext in items {
615 if let ExternItemKind::Mod {
616 package,
617 binding,
618 items: mod_items,
619 is_global,
620 } = &ext.kind
621 {
622 if *is_global {
624 continue;
625 }
626
627 if mod_items.is_empty() {
628 match target {
630 JsTarget::Esm => imports.push(JsStmt::Import {
631 name: binding.name.clone(),
632 source: package.clone(),
633 }),
634 JsTarget::Cjs => imports.push(JsStmt::Require {
635 name: binding.name.clone(),
636 source: package.clone(),
637 }),
638 }
639 } else {
640 let names: Vec<String> = mod_items
642 .iter()
643 .filter_map(|mi| match &mi.kind {
644 husk_ast::ModItemKind::Fn { name, .. } => {
645 Some(name.name.clone())
646 }
647 })
648 .collect();
649 match target {
650 JsTarget::Esm => imports.push(JsStmt::NamedImport {
651 names,
652 source: package.clone(),
653 }),
654 JsTarget::Cjs => imports.push(JsStmt::NamedRequire {
655 names,
656 source: package.clone(),
657 }),
658 }
659 }
660 }
661 }
662 }
663 }
664 }
665
666 for item in &file.items {
669 if let ItemKind::Struct { name, fields, .. } = &item.kind {
670 if !extern_structs.contains(&name.name) {
671 body.push(lower_struct_constructor(&name.name, fields));
672 }
673 }
674 }
675
676 for item in &file.items {
678 match &item.kind {
679 ItemKind::Fn {
680 name,
681 params,
682 body: fn_body,
683 ..
684 } => {
685 body.push(lower_fn_with_span(
686 &name.name, params, fn_body, &item.span, source, &ctx,
687 ));
688 fn_names.push(name.name.clone());
689 if name.name == "main" && params.is_empty() {
690 has_main_entry = true;
691 }
692 }
693 ItemKind::ExternBlock { abi, items } => {
694 if abi == "js" {
695 const PREAMBLE_FUNCTIONS: &[&str] = &[
698 "JsObject_new",
699 "jsvalue_get",
700 "jsvalue_getString",
701 "jsvalue_getNumber",
702 "jsvalue_getBool",
703 "jsvalue_getArray",
704 "jsvalue_isNull",
705 "jsvalue_toString",
706 "jsvalue_toBool",
707 "jsvalue_toNumber",
708 "express_json",
709 ];
710
711 for ext in items {
712 match &ext.kind {
713 ExternItemKind::Fn {
714 name,
715 params,
716 ret_type,
717 } => {
718 if !PREAMBLE_FUNCTIONS.contains(&name.name.as_str()) {
720 body.push(lower_extern_fn(name, params, ret_type.as_ref()));
721 }
722 }
723 ExternItemKind::Mod { .. } => {
724 }
726 ExternItemKind::Struct { .. } => {
727 }
730 ExternItemKind::Static { .. } => {
731 }
734 }
735 }
736 }
737 }
738 ItemKind::Impl(impl_block) => {
739 let self_ty_name = type_expr_to_js_name(&impl_block.self_ty);
741
742 for impl_item in &impl_block.items {
743 if let ImplItemKind::Method(method) = &impl_item.kind {
744 if method.is_extern {
747 continue;
748 }
749 body.push(lower_impl_method(
750 &self_ty_name,
751 method,
752 &impl_item.span,
753 source,
754 &ctx,
755 ));
756 }
757 }
758 }
759 ItemKind::Trait(_) => {
760 }
762 _ => {}
763 }
764 }
765 if has_main_entry && call_main {
766 body.push(JsStmt::Expr(JsExpr::Call {
767 callee: Box::new(JsExpr::Ident("main".to_string())),
768 args: Vec::new(),
769 }));
770 }
771
772 if !fn_names.is_empty() {
774 match target {
775 JsTarget::Esm => body.push(JsStmt::ExportNamed { names: fn_names }),
776 JsTarget::Cjs => {
777 let exports_obj = JsExpr::Object(
778 fn_names
779 .into_iter()
780 .map(|name| (name.clone(), JsExpr::Ident(name)))
781 .collect(),
782 );
783 let assign = JsExpr::Assignment {
784 left: Box::new(JsExpr::Member {
785 object: Box::new(JsExpr::Ident("module".to_string())),
786 property: "exports".to_string(),
787 }),
788 op: JsAssignOp::Assign,
789 right: Box::new(exports_obj),
790 };
791 body.push(JsStmt::Expr(assign));
792 }
793 }
794 }
795
796 let mut full_body = imports;
798 full_body.append(&mut body);
799
800 JsModule { body: full_body }
801}
802
803fn lower_fn_with_span(
804 name: &str,
805 params: &[Param],
806 body: &[Stmt],
807 span: &Span,
808 source: Option<&str>,
809 ctx: &CodegenContext,
810) -> JsStmt {
811 let js_params: Vec<String> = params.iter().map(|p| p.name.name.clone()).collect();
812 let mut js_body = Vec::new();
813 for (i, stmt) in body.iter().enumerate() {
814 let is_last = i + 1 == body.len();
815 if is_last {
816 js_body.push(lower_tail_stmt(stmt, ctx));
817 } else {
818 js_body.push(lower_stmt(stmt, ctx));
819 }
820 }
821
822 let source_span = source.map(|src| {
824 let (line, column) = offset_to_line_col(src, span.range.start);
825 SourceSpan { line, column }
826 });
827
828 JsStmt::Function {
829 name: name.to_string(),
830 params: js_params,
831 body: js_body,
832 source_span,
833 }
834}
835
836fn lower_struct_constructor(name: &str, fields: &[StructField]) -> JsStmt {
843 let param_names: Vec<String> = fields.iter().map(|f| f.name.name.clone()).collect();
844
845 let mut body_stmts = Vec::new();
847 for field in fields {
848 let field_name = field.name.name.clone();
849 body_stmts.push(JsStmt::Expr(JsExpr::Assignment {
850 left: Box::new(JsExpr::Member {
851 object: Box::new(JsExpr::Ident("this".to_string())),
852 property: field_name.clone(),
853 }),
854 op: JsAssignOp::Assign,
855 right: Box::new(JsExpr::Ident(field_name)),
856 }));
857 }
858
859 JsStmt::Function {
860 name: name.to_string(),
861 params: param_names,
862 body: body_stmts,
863 source_span: None,
864 }
865}
866
867fn type_expr_to_js_name(ty: &TypeExpr) -> String {
869 match &ty.kind {
870 TypeExprKind::Named(ident) => ident.name.clone(),
871 TypeExprKind::Generic { name, .. } => name.name.clone(),
872 TypeExprKind::Function { params, ret } => {
873 let param_names: Vec<String> = params.iter().map(type_expr_to_js_name).collect();
875 format!("(({}) => {})", param_names.join(", "), type_expr_to_js_name(ret))
876 }
877 TypeExprKind::Array(elem) => {
878 format!("{}[]", type_expr_to_js_name(elem))
879 }
880 TypeExprKind::Tuple(types) => {
881 let type_names: Vec<String> = types.iter().map(type_expr_to_js_name).collect();
883 format!("[{}]", type_names.join(", "))
884 }
885 }
886}
887
888fn lower_impl_method(
896 type_name: &str,
897 method: &husk_ast::ImplMethod,
898 span: &Span,
899 source: Option<&str>,
900 ctx: &CodegenContext,
901) -> JsStmt {
902 let method_name = &method.name.name;
903
904 let js_params: Vec<String> = method.params.iter().map(|p| p.name.name.clone()).collect();
907
908 let mut js_body = Vec::new();
910 for (i, stmt) in method.body.iter().enumerate() {
911 let is_last = i + 1 == method.body.len();
912 if is_last {
913 js_body.push(lower_tail_stmt(stmt, ctx));
914 } else {
915 js_body.push(lower_stmt(stmt, ctx));
916 }
917 }
918
919 let _source_span = source.map(|src| {
921 let (line, column) = offset_to_line_col(src, span.range.start);
922 SourceSpan { line, column }
923 });
924
925 let target = if method.receiver.is_some() {
928 JsExpr::Member {
930 object: Box::new(JsExpr::Member {
931 object: Box::new(JsExpr::Ident(type_name.to_string())),
932 property: "prototype".to_string(),
933 }),
934 property: method_name.clone(),
935 }
936 } else {
937 JsExpr::Member {
939 object: Box::new(JsExpr::Ident(type_name.to_string())),
940 property: method_name.clone(),
941 }
942 };
943
944 let func_expr = JsExpr::Function {
946 params: js_params,
947 body: js_body,
948 };
949
950 JsStmt::Expr(JsExpr::Assignment {
952 left: Box::new(target),
953 op: JsAssignOp::Assign,
954 right: Box::new(func_expr),
955 })
956}
957
958fn lower_tail_stmt(stmt: &Stmt, ctx: &CodegenContext) -> JsStmt {
959 match &stmt.kind {
960 StmtKind::Expr(expr) => JsStmt::Return(lower_expr(expr, ctx)),
963 StmtKind::If {
965 cond,
966 then_branch,
967 else_branch,
968 } => {
969 let js_cond = lower_expr(cond, ctx);
970 let then_block: Vec<JsStmt> = then_branch
971 .stmts
972 .iter()
973 .enumerate()
974 .map(|(i, s)| {
975 if i + 1 == then_branch.stmts.len() {
976 lower_tail_stmt(s, ctx)
977 } else {
978 lower_stmt(s, ctx)
979 }
980 })
981 .collect();
982 let else_block = else_branch.as_ref().map(|else_stmt| {
983 match &else_stmt.kind {
985 StmtKind::Block(block) => block
986 .stmts
987 .iter()
988 .enumerate()
989 .map(|(i, s)| {
990 if i + 1 == block.stmts.len() {
991 lower_tail_stmt(s, ctx)
992 } else {
993 lower_stmt(s, ctx)
994 }
995 })
996 .collect(),
997 StmtKind::If { .. } => vec![lower_tail_stmt(else_stmt, ctx)],
998 _ => vec![lower_tail_stmt(else_stmt, ctx)],
999 }
1000 });
1001 JsStmt::If {
1002 cond: js_cond,
1003 then_block,
1004 else_block,
1005 }
1006 }
1007 _ => lower_stmt(stmt, ctx),
1008 }
1009}
1010
1011fn lower_extern_fn(
1012 name: &husk_ast::Ident,
1013 params: &[Param],
1014 ret_type: Option<&TypeExpr>,
1015) -> JsStmt {
1016 let js_params: Vec<String> = params.iter().map(|p| p.name.name.clone()).collect();
1017 let body = lower_extern_body(&name.name, &js_params, ret_type);
1018 JsStmt::Function {
1019 name: name.name.clone(),
1020 params: js_params,
1021 body,
1022 source_span: None,
1023 }
1024}
1025
1026fn is_result_type(ret_type: &TypeExpr) -> bool {
1027 match &ret_type.kind {
1028 TypeExprKind::Generic { name, args } => name.name == "Result" && args.len() == 2,
1029 _ => false,
1030 }
1031}
1032
1033fn lower_extern_body(
1034 name: &str,
1035 param_names: &[String],
1036 ret_type: Option<&TypeExpr>,
1037) -> Vec<JsStmt> {
1038 let callee = JsExpr::Member {
1040 object: Box::new(JsExpr::Ident("globalThis".to_string())),
1041 property: name.to_string(),
1042 };
1043 let args: Vec<JsExpr> = param_names.iter().cloned().map(JsExpr::Ident).collect();
1044 let call = JsExpr::Call {
1045 callee: Box::new(callee),
1046 args,
1047 };
1048
1049 if let Some(ret_ty) = ret_type {
1050 if is_result_type(ret_ty) {
1051 let ok_call = JsExpr::Call {
1053 callee: Box::new(JsExpr::Ident("Ok".to_string())),
1054 args: vec![call],
1055 };
1056 let try_block = vec![JsStmt::Return(ok_call)];
1057
1058 let err_call = JsExpr::Call {
1059 callee: Box::new(JsExpr::Ident("Err".to_string())),
1060 args: vec![JsExpr::Ident("e".to_string())],
1061 };
1062 let catch_block = vec![JsStmt::Return(err_call)];
1063
1064 vec![JsStmt::TryCatch {
1065 try_block,
1066 catch_ident: "e".to_string(),
1067 catch_block,
1068 }]
1069 } else {
1070 vec![JsStmt::Return(call)]
1071 }
1072 } else {
1073 vec![JsStmt::Expr(call)]
1074 }
1075}
1076
1077fn lower_stmt(stmt: &Stmt, ctx: &CodegenContext) -> JsStmt {
1078 match &stmt.kind {
1079 StmtKind::Let {
1080 mutable: _,
1081 pattern,
1082 ty: _,
1083 value,
1084 else_block,
1085 } => {
1086 lower_let_pattern(pattern, value.as_ref(), else_block.as_ref(), ctx)
1087 }
1088 StmtKind::Expr(expr) | StmtKind::Semi(expr) => {
1089 if let ExprKind::Match { scrutinee, arms } = &expr.kind {
1093 return lower_match_stmt(scrutinee, arms, ctx);
1094 }
1095 JsStmt::Expr(lower_expr(expr, ctx))
1096 }
1097 StmtKind::Return { value } => {
1098 let expr = value
1099 .as_ref()
1100 .map(|e| lower_expr(e, ctx))
1101 .unwrap_or_else(|| JsExpr::Ident("undefined".to_string()));
1103 JsStmt::Return(expr)
1104 }
1105 StmtKind::Block(block) => {
1106 if let Some(last) = block.stmts.last() {
1111 lower_stmt(last, ctx)
1112 } else {
1113 JsStmt::Expr(JsExpr::Ident("undefined".to_string()))
1114 }
1115 }
1116 StmtKind::If {
1117 cond,
1118 then_branch,
1119 else_branch,
1120 } => {
1121 let js_cond = lower_expr(cond, ctx);
1122 let then_block: Vec<JsStmt> = then_branch
1123 .stmts
1124 .iter()
1125 .map(|s| lower_stmt(s, ctx))
1126 .collect();
1127 let else_block = else_branch.as_ref().map(|else_stmt| {
1128 match &else_stmt.kind {
1130 StmtKind::Block(block) => block
1131 .stmts
1132 .iter()
1133 .map(|s| lower_stmt(s, ctx))
1134 .collect(),
1135 StmtKind::If { .. } => vec![lower_stmt(else_stmt, ctx)],
1136 _ => vec![lower_stmt(else_stmt, ctx)],
1137 }
1138 });
1139 JsStmt::If {
1140 cond: js_cond,
1141 then_block,
1142 else_block,
1143 }
1144 }
1145 StmtKind::ForIn {
1146 binding,
1147 iterable,
1148 body,
1149 } => {
1150 if let ExprKind::Range {
1152 start: Some(start),
1153 end: Some(end),
1154 inclusive,
1155 } = &iterable.kind
1156 {
1157 let js_start = lower_expr(start, ctx);
1158 let js_end = lower_expr(end, ctx);
1159 let js_body: Vec<JsStmt> = body.stmts.iter().map(|s| lower_stmt(s, ctx)).collect();
1160
1161 JsStmt::For {
1162 binding: ctx.resolve_name(&binding.name, &binding.span),
1163 start: js_start,
1164 end: js_end,
1165 inclusive: *inclusive,
1166 body: js_body,
1167 }
1168 } else {
1169 let js_iterable = lower_expr(iterable, ctx);
1170 let js_body: Vec<JsStmt> = body.stmts.iter().map(|s| lower_stmt(s, ctx)).collect();
1171 JsStmt::ForOf {
1172 binding: ctx.resolve_name(&binding.name, &binding.span),
1173 iterable: js_iterable,
1174 body: js_body,
1175 }
1176 }
1177 }
1178 StmtKind::While { cond, body } => {
1179 let js_cond = lower_expr(cond, ctx);
1180 let js_body: Vec<JsStmt> = body.stmts.iter().map(|s| lower_stmt(s, ctx)).collect();
1181 JsStmt::While {
1182 cond: js_cond,
1183 body: js_body,
1184 }
1185 }
1186 StmtKind::Loop { body } => {
1187 let js_body: Vec<JsStmt> = body.stmts.iter().map(|s| lower_stmt(s, ctx)).collect();
1189 JsStmt::While {
1190 cond: JsExpr::Bool(true),
1191 body: js_body,
1192 }
1193 }
1194 StmtKind::Break => JsStmt::Break,
1195 StmtKind::Continue => JsStmt::Continue,
1196 StmtKind::Assign { target, op, value } => {
1197 let js_op = match op {
1198 husk_ast::AssignOp::Assign => JsAssignOp::Assign,
1199 husk_ast::AssignOp::AddAssign => JsAssignOp::AddAssign,
1200 husk_ast::AssignOp::SubAssign => JsAssignOp::SubAssign,
1201 husk_ast::AssignOp::ModAssign => JsAssignOp::ModAssign,
1202 };
1203 JsStmt::Assign {
1204 target: lower_expr(target, ctx),
1205 op: js_op,
1206 value: lower_expr(value, ctx),
1207 }
1208 }
1209 StmtKind::IfLet {
1210 pattern,
1211 scrutinee,
1212 then_branch,
1213 else_branch,
1214 } => {
1215 lower_if_let_stmt(pattern, scrutinee, then_branch, else_branch, ctx)
1216 }
1217 }
1218}
1219
1220fn pattern_to_destructure(pattern: &Pattern, ctx: &CodegenContext) -> DestructurePattern {
1222 match &pattern.kind {
1223 PatternKind::Binding(name) => {
1224 DestructurePattern::Binding(ctx.resolve_name(&name.name, &name.span))
1225 }
1226 PatternKind::Wildcard => DestructurePattern::Wildcard,
1227 PatternKind::Tuple { fields } => {
1228 let elements = fields.iter().map(|p| pattern_to_destructure(p, ctx)).collect();
1229 DestructurePattern::Array(elements)
1230 }
1231 _ => DestructurePattern::Wildcard,
1233 }
1234}
1235
1236fn lower_let_pattern(
1238 pattern: &Pattern,
1239 value: Option<&Expr>,
1240 else_block: Option<&Block>,
1241 ctx: &CodegenContext,
1242) -> JsStmt {
1243 match &pattern.kind {
1244 PatternKind::Binding(name) => {
1245 let span_key = (pattern.span.range.start, pattern.span.range.end);
1247 if let Some((_, variant_name)) = ctx.variant_patterns.get(&span_key) {
1248 if let Some(blk) = else_block {
1250 return lower_let_refutable_binding(name, variant_name, value, blk, ctx);
1251 }
1252 }
1253 JsStmt::Let {
1255 name: ctx.resolve_name(&name.name, &name.span),
1256 init: value.map(|e| lower_expr(e, ctx)),
1257 }
1258 }
1259 PatternKind::Tuple { fields } => {
1260 let elements: Vec<DestructurePattern> = fields
1263 .iter()
1264 .map(|p| pattern_to_destructure(p, ctx))
1265 .collect();
1266
1267 JsStmt::LetDestructure {
1268 pattern: elements,
1269 init: value.map(|e| lower_expr(e, ctx)),
1270 }
1271 }
1272 PatternKind::Wildcard => {
1273 if let Some(e) = value {
1275 JsStmt::Expr(lower_expr(e, ctx))
1276 } else {
1277 JsStmt::Expr(JsExpr::Ident("undefined".to_string()))
1278 }
1279 }
1280 PatternKind::EnumUnit { path } | PatternKind::EnumTuple { path, .. } => {
1281 if let Some(blk) = else_block {
1283 lower_let_refutable(pattern, path, value, blk, ctx)
1284 } else {
1285 emit_unreachable_error()
1287 }
1288 }
1289 PatternKind::EnumStruct { path, fields } => {
1290 if let Some(blk) = else_block {
1292 lower_let_refutable_struct(path, fields, value, blk, ctx)
1293 } else {
1294 emit_unreachable_error()
1295 }
1296 }
1297 }
1298}
1299
1300fn emit_unreachable_error() -> JsStmt {
1302 JsStmt::Expr(JsExpr::Raw(
1303 "/* SEMANTIC ERROR: unreachable - missing else block */ undefined".to_string(),
1304 ))
1305}
1306
1307fn lower_let_refutable_binding(
1309 _name: &Ident,
1310 variant_name: &str,
1311 value: Option<&Expr>,
1312 else_block: &Block,
1313 ctx: &CodegenContext,
1314) -> JsStmt {
1315 let expr = match value {
1316 Some(e) => e,
1317 None => {
1318 return JsStmt::Expr(JsExpr::Ident("undefined".to_string()));
1320 }
1321 };
1322 let value_js = lower_expr(expr, ctx);
1323
1324 let temp_name = ctx.fresh_temp("__letelse");
1325 let temp_expr = JsExpr::Ident(temp_name.clone());
1326
1327 let condition = JsExpr::Binary {
1329 op: JsBinaryOp::StrictNe,
1330 left: Box::new(JsExpr::Member {
1331 object: Box::new(temp_expr.clone()),
1332 property: "tag".to_string(),
1333 }),
1334 right: Box::new(JsExpr::String(variant_name.to_string())),
1335 };
1336
1337 let else_stmts: Vec<JsStmt> = else_block.stmts.iter().map(|s| lower_stmt(s, ctx)).collect();
1338
1339 JsStmt::Sequence(vec![
1342 JsStmt::Let {
1343 name: temp_name,
1344 init: Some(value_js),
1345 },
1346 JsStmt::If {
1347 cond: condition,
1348 then_block: else_stmts,
1349 else_block: None,
1350 },
1351 ])
1352}
1353
1354fn lower_let_refutable(
1356 pattern: &Pattern,
1357 path: &[Ident],
1358 value: Option<&Expr>,
1359 else_block: &Block,
1360 ctx: &CodegenContext,
1361) -> JsStmt {
1362 let expr = match value {
1363 Some(e) => e,
1364 None => {
1365 return JsStmt::Expr(JsExpr::Ident("undefined".to_string()));
1366 }
1367 };
1368 let value_js = lower_expr(expr, ctx);
1369
1370 let temp_name = ctx.fresh_temp("__letelse");
1371 let temp_expr = JsExpr::Ident(temp_name.clone());
1372
1373 let variant = path.last().map(|id| id.name.clone()).unwrap_or_default();
1375 let condition = JsExpr::Binary {
1376 op: JsBinaryOp::StrictNe,
1377 left: Box::new(JsExpr::Member {
1378 object: Box::new(temp_expr.clone()),
1379 property: "tag".to_string(),
1380 }),
1381 right: Box::new(JsExpr::String(variant)),
1382 };
1383
1384 let else_stmts: Vec<JsStmt> = else_block.stmts.iter().map(|s| lower_stmt(s, ctx)).collect();
1385
1386 let bindings = extract_refutable_pattern_bindings(pattern, &temp_expr, ctx);
1388
1389 let mut stmts = vec![
1390 JsStmt::Let {
1391 name: temp_name,
1392 init: Some(value_js),
1393 },
1394 JsStmt::If {
1395 cond: condition,
1396 then_block: else_stmts,
1397 else_block: None,
1398 },
1399 ];
1400 stmts.extend(bindings.into_iter().map(|(name, accessor)| JsStmt::Let {
1401 name,
1402 init: Some(accessor),
1403 }));
1404
1405 JsStmt::Sequence(stmts)
1407}
1408
1409fn lower_let_refutable_struct(
1411 path: &[Ident],
1412 fields: &[(Ident, Pattern)],
1413 value: Option<&Expr>,
1414 else_block: &Block,
1415 ctx: &CodegenContext,
1416) -> JsStmt {
1417 let expr = match value {
1418 Some(e) => e,
1419 None => {
1420 return JsStmt::Expr(JsExpr::Ident("undefined".to_string()));
1421 }
1422 };
1423 let value_js = lower_expr(expr, ctx);
1424
1425 let temp_name = ctx.fresh_temp("__letelse");
1426 let temp_expr = JsExpr::Ident(temp_name.clone());
1427
1428 let variant = path.last().map(|id| id.name.clone()).unwrap_or_default();
1430 let condition = JsExpr::Binary {
1431 op: JsBinaryOp::StrictNe,
1432 left: Box::new(JsExpr::Member {
1433 object: Box::new(temp_expr.clone()),
1434 property: "tag".to_string(),
1435 }),
1436 right: Box::new(JsExpr::String(variant)),
1437 };
1438
1439 let else_stmts: Vec<JsStmt> = else_block.stmts.iter().map(|s| lower_stmt(s, ctx)).collect();
1440
1441 let bindings = extract_struct_field_bindings(fields, &temp_expr, ctx);
1443
1444 let mut stmts = vec![
1445 JsStmt::Let {
1446 name: temp_name,
1447 init: Some(value_js),
1448 },
1449 JsStmt::If {
1450 cond: condition,
1451 then_block: else_stmts,
1452 else_block: None,
1453 },
1454 ];
1455 stmts.extend(bindings.into_iter().map(|(name, accessor)| JsStmt::Let {
1456 name,
1457 init: Some(accessor),
1458 }));
1459
1460 JsStmt::Sequence(stmts)
1462}
1463
1464fn extract_struct_field_bindings(
1466 fields: &[(Ident, Pattern)],
1467 scrutinee_js: &JsExpr,
1468 ctx: &CodegenContext,
1469) -> Vec<(String, JsExpr)> {
1470 let mut bindings = Vec::new();
1471 for (field_name, sub_pattern) in fields {
1472 let field_accessor = JsExpr::Member {
1474 object: Box::new(JsExpr::Member {
1475 object: Box::new(scrutinee_js.clone()),
1476 property: "value".to_string(),
1477 }),
1478 property: field_name.name.clone(),
1479 };
1480
1481 match &sub_pattern.kind {
1482 PatternKind::Binding(ident) => {
1483 bindings.push((ctx.resolve_name(&ident.name, &ident.span), field_accessor));
1484 }
1485 PatternKind::Wildcard => {
1486 }
1488 _ => {
1489 }
1491 }
1492 }
1493 bindings
1494}
1495
1496fn extract_refutable_pattern_bindings(
1499 pattern: &Pattern,
1500 scrutinee_js: &JsExpr,
1501 ctx: &CodegenContext,
1502) -> Vec<(String, JsExpr)> {
1503 match &pattern.kind {
1504 PatternKind::EnumTuple { fields, .. } => {
1505 if fields.len() == 1 {
1507 if let PatternKind::Binding(ident) = &fields[0].kind {
1508 let accessor = JsExpr::Member {
1509 object: Box::new(scrutinee_js.clone()),
1510 property: "value".to_string(),
1511 };
1512 return vec![(ctx.resolve_name(&ident.name, &ident.span), accessor)];
1513 }
1514 } else {
1515 let mut bindings = Vec::new();
1517 for (i, field) in fields.iter().enumerate() {
1518 if let PatternKind::Binding(ident) = &field.kind {
1519 let accessor = JsExpr::Index {
1520 object: Box::new(scrutinee_js.clone()),
1521 index: Box::new(JsExpr::String(i.to_string())),
1522 };
1523 bindings.push((ctx.resolve_name(&ident.name, &ident.span), accessor));
1524 }
1525 }
1526 return bindings;
1527 }
1528 Vec::new()
1529 }
1530 PatternKind::EnumStruct { fields, .. } => {
1531 extract_struct_field_bindings(fields, scrutinee_js, ctx)
1532 }
1533 _ => Vec::new(),
1534 }
1535}
1536
1537fn lower_if_let_stmt(
1539 pattern: &Pattern,
1540 scrutinee: &Expr,
1541 then_branch: &Block,
1542 else_branch: &Option<Box<Stmt>>,
1543 ctx: &CodegenContext,
1544) -> JsStmt {
1545 let scrutinee_js = lower_expr(scrutinee, ctx);
1546 let temp_name = ctx.fresh_temp("__iflet");
1547 let temp_expr = JsExpr::Ident(temp_name.clone());
1548
1549 let condition = pattern_test_for_if_let(pattern, &temp_expr, ctx)
1551 .unwrap_or(JsExpr::Bool(true));
1552
1553 let bindings = extract_refutable_pattern_bindings(pattern, &temp_expr, ctx);
1555 let mut then_stmts: Vec<JsStmt> = bindings
1556 .into_iter()
1557 .map(|(name, accessor)| JsStmt::Let {
1558 name,
1559 init: Some(accessor),
1560 })
1561 .collect();
1562 then_stmts.extend(then_branch.stmts.iter().map(|s| lower_stmt(s, ctx)));
1563
1564 let else_block = else_branch.as_ref().map(|else_stmt| {
1565 match &else_stmt.kind {
1566 StmtKind::Block(block) => block
1567 .stmts
1568 .iter()
1569 .map(|s| lower_stmt(s, ctx))
1570 .collect(),
1571 StmtKind::If { .. } | StmtKind::IfLet { .. } => vec![lower_stmt(else_stmt, ctx)],
1572 _ => vec![lower_stmt(else_stmt, ctx)],
1573 }
1574 });
1575
1576 JsStmt::Block(vec![
1577 JsStmt::Let {
1578 name: temp_name,
1579 init: Some(scrutinee_js),
1580 },
1581 JsStmt::If {
1582 cond: condition,
1583 then_block: then_stmts,
1584 else_block,
1585 },
1586 ])
1587}
1588
1589fn pattern_test_for_if_let(
1591 pattern: &Pattern,
1592 scrutinee_js: &JsExpr,
1593 ctx: &CodegenContext,
1594) -> Option<JsExpr> {
1595 match &pattern.kind {
1596 PatternKind::EnumUnit { path }
1597 | PatternKind::EnumTuple { path, .. }
1598 | PatternKind::EnumStruct { path, .. } => {
1599 let variant = path.last().map(|id| id.name.clone()).unwrap_or_default();
1600 Some(JsExpr::Binary {
1601 op: JsBinaryOp::StrictEq,
1602 left: Box::new(JsExpr::Member {
1603 object: Box::new(scrutinee_js.clone()),
1604 property: "tag".to_string(),
1605 }),
1606 right: Box::new(JsExpr::String(variant)),
1607 })
1608 }
1609 PatternKind::Binding(_) => {
1610 let span_key = (pattern.span.range.start, pattern.span.range.end);
1612 if let Some((_, variant_name)) = ctx.variant_patterns.get(&span_key) {
1613 Some(JsExpr::Binary {
1614 op: JsBinaryOp::StrictEq,
1615 left: Box::new(JsExpr::Member {
1616 object: Box::new(scrutinee_js.clone()),
1617 property: "tag".to_string(),
1618 }),
1619 right: Box::new(JsExpr::String(variant_name.clone())),
1620 })
1621 } else {
1622 None }
1624 }
1625 _ => None,
1626 }
1627}
1628
1629fn strip_variadic_suffix(method_name: &str) -> String {
1634 let last_non_digit = method_name
1636 .char_indices()
1637 .rev()
1638 .find(|(_, c)| !c.is_ascii_digit());
1639
1640 match last_non_digit {
1641 Some((idx, _)) if idx < method_name.len() - 1 => {
1642 method_name[..=idx].to_string()
1644 }
1645 _ => {
1646 method_name.to_string()
1648 }
1649 }
1650}
1651
1652fn lower_conversion_method(
1659 receiver: &Expr,
1660 method_name: &str,
1661 target_type: &str,
1662 ctx: &CodegenContext,
1663) -> JsExpr {
1664 let receiver_js = lower_expr(receiver, ctx);
1665
1666 match method_name {
1667 "into" => {
1668 match target_type {
1670 "String" => {
1671 JsExpr::Call {
1674 callee: Box::new(JsExpr::Ident("String".to_string())),
1675 args: vec![receiver_js],
1676 }
1677 }
1678 "i64" => {
1679 JsExpr::Call {
1681 callee: Box::new(JsExpr::Ident("BigInt".to_string())),
1682 args: vec![receiver_js],
1683 }
1684 }
1685 "f64" => {
1686 JsExpr::Call {
1688 callee: Box::new(JsExpr::Ident("Number".to_string())),
1689 args: vec![receiver_js],
1690 }
1691 }
1692 _ => {
1693 receiver_js
1695 }
1696 }
1697 }
1698 "parse" => {
1699 let parse_call = match target_type {
1702 "i32" => {
1703 JsExpr::Call {
1705 callee: Box::new(JsExpr::Ident("__husk_parse_i32".to_string())),
1706 args: vec![receiver_js],
1707 }
1708 }
1709 "i64" => {
1710 JsExpr::Call {
1712 callee: Box::new(JsExpr::Ident("__husk_parse_i64".to_string())),
1713 args: vec![receiver_js],
1714 }
1715 }
1716 "f64" => {
1717 JsExpr::Call {
1719 callee: Box::new(JsExpr::Ident("__husk_parse_f64".to_string())),
1720 args: vec![receiver_js],
1721 }
1722 }
1723 _ => {
1724 JsExpr::Object(vec![
1726 ("tag".to_string(), JsExpr::String("Err".to_string())),
1727 (
1728 "value".to_string(),
1729 JsExpr::String(format!("cannot parse to {}", target_type)),
1730 ),
1731 ])
1732 }
1733 };
1734 parse_call
1735 }
1736 "try_into" => {
1737 match target_type {
1740 "i32" => {
1741 JsExpr::Call {
1743 callee: Box::new(JsExpr::Ident("__husk_try_into_i32".to_string())),
1744 args: vec![receiver_js],
1745 }
1746 }
1747 _ => {
1748 JsExpr::Object(vec![
1750 ("tag".to_string(), JsExpr::String("Ok".to_string())),
1751 ("value".to_string(), receiver_js),
1752 ])
1753 }
1754 }
1755 }
1756 _ => receiver_js,
1757 }
1758}
1759
1760fn lower_expr(expr: &Expr, ctx: &CodegenContext) -> JsExpr {
1761 match &expr.kind {
1762 ExprKind::Literal(lit) => match &lit.kind {
1763 LiteralKind::Int(n) => JsExpr::Number(*n),
1764 LiteralKind::Float(f) => JsExpr::Float(*f),
1765 LiteralKind::Bool(b) => JsExpr::Bool(*b),
1766 LiteralKind::String(s) => JsExpr::String(s.clone()),
1767 },
1768 ExprKind::Ident(id) => {
1769 if id.name == "self" {
1771 JsExpr::Ident("this".to_string())
1772 } else if let Some((_enum_name, variant_name)) = ctx.variant_calls.get(&(expr.span.range.start, expr.span.range.end)) {
1773 JsExpr::Object(vec![("tag".to_string(), JsExpr::String(variant_name.clone()))])
1775 } else {
1776 JsExpr::Ident(ctx.resolve_name(&id.name, &id.span))
1778 }
1779 }
1780 ExprKind::Path { segments } => {
1781 let variant = segments
1784 .last()
1785 .map(|id| id.name.clone())
1786 .unwrap_or_else(|| "Unknown".to_string());
1787 JsExpr::Object(vec![("tag".to_string(), JsExpr::String(variant))])
1788 }
1789 ExprKind::Field { base, member } => {
1790 let field_name = &member.name;
1791 let js_property = ctx
1793 .accessors
1794 .getters
1795 .iter()
1796 .find(|((_, prop_name), _)| prop_name == field_name)
1797 .map(|(_, js_name)| js_name.clone())
1798 .unwrap_or_else(|| field_name.clone());
1799
1800 JsExpr::Member {
1801 object: Box::new(lower_expr(base, ctx)),
1802 property: js_property,
1803 }
1804 }
1805 ExprKind::MethodCall {
1806 receiver,
1807 method,
1808 type_args: _, args,
1810 } => {
1811 let method_name = &method.name;
1815
1816 if args.is_empty() {
1818 for ((_, m), prop) in &ctx.accessors.getters {
1820 if m == method_name {
1821 return JsExpr::Member {
1823 object: Box::new(lower_expr(receiver, ctx)),
1824 property: prop.clone(),
1825 };
1826 }
1827 }
1828 }
1829
1830 if args.len() == 1 && method_name.starts_with("set_") {
1832 for ((_, m), prop) in &ctx.accessors.setters {
1834 if m == method_name {
1835 return JsExpr::Assignment {
1837 left: Box::new(JsExpr::Member {
1838 object: Box::new(lower_expr(receiver, ctx)),
1839 property: prop.clone(),
1840 }),
1841 op: JsAssignOp::Assign,
1842 right: Box::new(lower_expr(&args[0], ctx)),
1843 };
1844 }
1845 }
1846 }
1847
1848 if method_name == "unwrap" && args.is_empty() {
1850 return JsExpr::Call {
1852 callee: Box::new(JsExpr::Ident("__husk_unwrap".to_string())),
1853 args: vec![lower_expr(receiver, ctx)],
1854 };
1855 }
1856 if method_name == "expect" && args.len() == 1 {
1857 return JsExpr::Call {
1859 callee: Box::new(JsExpr::Ident("__husk_expect".to_string())),
1860 args: vec![lower_expr(receiver, ctx), lower_expr(&args[0], ctx)],
1861 };
1862 }
1863
1864 if (method_name == "into" || method_name == "parse" || method_name == "try_into") && args.is_empty() {
1867 if let Some(target_type) = ctx.type_resolution.get(&(expr.span.range.start, expr.span.range.end)) {
1868 return lower_conversion_method(receiver, method_name, target_type, ctx);
1869 }
1870 }
1871
1872 let base_method_name = strip_variadic_suffix(&method.name);
1877
1878 let js_name_override = ctx.accessors.method_js_names
1881 .iter()
1882 .find(|((_, m), _)| m == &base_method_name)
1883 .map(|(_, js_name)| js_name.clone());
1884
1885 let is_extern_method = ctx.accessors.extern_methods
1887 .iter()
1888 .any(|(_, m)| m == &base_method_name);
1889
1890 let js_method_name = js_name_override.unwrap_or_else(|| {
1891 if is_extern_method {
1893 snake_to_camel(&base_method_name)
1894 } else {
1895 base_method_name.clone()
1896 }
1897 });
1898 JsExpr::Call {
1899 callee: Box::new(JsExpr::Member {
1900 object: Box::new(lower_expr(receiver, ctx)),
1901 property: js_method_name,
1902 }),
1903 args: args.iter().map(|a| lower_expr(a, ctx)).collect(),
1904 }
1905 }
1906 ExprKind::Call { callee, type_args: _, args } => {
1907 if let ExprKind::Ident(ref id) = callee.kind {
1909 if id.name == "println" {
1910 return JsExpr::Call {
1911 callee: Box::new(JsExpr::Member {
1912 object: Box::new(JsExpr::Ident("console".to_string())),
1913 property: "log".to_string(),
1914 }),
1915 args: args.iter().map(|a| lower_expr(a, ctx)).collect(),
1916 };
1917 }
1918 if id.name == "print" {
1920 return JsExpr::Call {
1921 callee: Box::new(JsExpr::Member {
1922 object: Box::new(JsExpr::Member {
1923 object: Box::new(JsExpr::Ident("process".to_string())),
1924 property: "stdout".to_string(),
1925 }),
1926 property: "write".to_string(),
1927 }),
1928 args: args.iter().map(|a| lower_expr(a, ctx)).collect(),
1929 };
1930 }
1931 if id.name == "express" {
1933 return JsExpr::Call {
1934 callee: Box::new(JsExpr::Ident("__husk_express".to_string())),
1935 args: args.iter().map(|a| lower_expr(a, ctx)).collect(),
1936 };
1937 }
1938
1939 if id.name == "include_str" {
1941 return handle_include_str(args, ctx);
1942 }
1943
1944 if id.name == "parse_int" {
1946 return JsExpr::Call {
1947 callee: Box::new(JsExpr::Ident("parseInt".to_string())),
1948 args: args.iter().map(|a| lower_expr(a, ctx)).collect(),
1949 };
1950 }
1951
1952 if id.name == "parseLong" {
1954 return JsExpr::Call {
1955 callee: Box::new(JsExpr::Ident("BigInt".to_string())),
1956 args: args.iter().map(|a| lower_expr(a, ctx)).collect(),
1957 };
1958 }
1959 }
1960
1961 if let Some((_enum_name, variant_name)) = ctx.variant_calls.get(&(expr.span.range.start, expr.span.range.end)) {
1964 let mut fields = vec![("tag".to_string(), JsExpr::String(variant_name.clone()))];
1965
1966 if args.len() == 1 {
1968 fields.push(("value".to_string(), lower_expr(&args[0], ctx)));
1969 } else {
1970 for (i, arg) in args.iter().enumerate() {
1971 fields.push((i.to_string(), lower_expr(arg, ctx)));
1972 }
1973 }
1974
1975 return JsExpr::Object(fields);
1976 }
1977
1978 if let ExprKind::Path { segments } = &callee.kind {
1980 let variant = segments
1981 .last()
1982 .map(|id| id.name.clone())
1983 .unwrap_or_else(|| "Unknown".to_string());
1984
1985 let mut fields = vec![("tag".to_string(), JsExpr::String(variant))];
1986
1987 if args.len() == 1 {
1989 fields.push(("value".to_string(), lower_expr(&args[0], ctx)));
1991 } else {
1992 for (i, arg) in args.iter().enumerate() {
1994 fields.push((i.to_string(), lower_expr(arg, ctx)));
1995 }
1996 }
1997
1998 return JsExpr::Object(fields);
1999 }
2000
2001 JsExpr::Call {
2002 callee: Box::new(lower_expr(callee, ctx)),
2003 args: args.iter().map(|a| lower_expr(a, ctx)).collect(),
2004 }
2005 }
2006 ExprKind::Binary { op, left, right } => {
2007 use husk_ast::BinaryOp::*;
2008
2009 match op {
2011 Eq => {
2012 return JsExpr::Call {
2013 callee: Box::new(JsExpr::Ident("__husk_eq".to_string())),
2014 args: vec![lower_expr(left, ctx), lower_expr(right, ctx)],
2015 };
2016 }
2017 NotEq => {
2018 return JsExpr::Unary {
2020 op: JsUnaryOp::Not,
2021 expr: Box::new(JsExpr::Call {
2022 callee: Box::new(JsExpr::Ident("__husk_eq".to_string())),
2023 args: vec![lower_expr(left, ctx), lower_expr(right, ctx)],
2024 }),
2025 };
2026 }
2027 _ => {}
2028 }
2029
2030 let js_op = match op {
2031 Add => JsBinaryOp::Add,
2032 Sub => JsBinaryOp::Sub,
2033 Mul => JsBinaryOp::Mul,
2034 Div => JsBinaryOp::Div,
2035 Mod => JsBinaryOp::Mod,
2036 Eq | NotEq => unreachable!(), Lt => JsBinaryOp::Lt,
2038 Gt => JsBinaryOp::Gt,
2039 Le => JsBinaryOp::Le,
2040 Ge => JsBinaryOp::Ge,
2041 And => JsBinaryOp::And,
2042 Or => JsBinaryOp::Or,
2043 };
2044 JsExpr::Binary {
2045 op: js_op,
2046 left: Box::new(lower_expr(left, ctx)),
2047 right: Box::new(lower_expr(right, ctx)),
2048 }
2049 }
2050 ExprKind::Match { scrutinee, arms } => lower_match_expr(scrutinee, arms, ctx),
2051 ExprKind::Struct { name, fields } => {
2052 let args: Vec<JsExpr> = fields
2057 .iter()
2058 .map(|f| lower_expr(&f.value, ctx))
2059 .collect();
2060 let constructor_name = name
2062 .last()
2063 .map(|id| id.name.clone())
2064 .unwrap_or_else(|| "UnknownType".to_string());
2065 JsExpr::New {
2066 constructor: constructor_name,
2067 args,
2068 }
2069 }
2070 ExprKind::Block(block) => {
2071 let mut body: Vec<JsStmt> = block
2074 .stmts
2075 .iter()
2076 .map(|s| lower_stmt(s, ctx))
2077 .collect();
2078 if let Some(last) = body.pop() {
2080 match last {
2081 JsStmt::Expr(expr) => body.push(JsStmt::Return(expr)),
2082 other => body.push(other),
2083 }
2084 }
2085 JsExpr::Iife { body }
2086 }
2087 ExprKind::Unary { op, expr: inner } => {
2088 let js_op = match op {
2089 husk_ast::UnaryOp::Not => JsUnaryOp::Not,
2090 husk_ast::UnaryOp::Neg => JsUnaryOp::Neg,
2091 };
2092 JsExpr::Unary {
2093 op: js_op,
2094 expr: Box::new(lower_expr(inner, ctx)),
2095 }
2096 }
2097 ExprKind::FormatPrint { format, args, newline } => {
2098 lower_format_print(&format.segments, args, *newline, ctx)
2101 }
2102 ExprKind::Format { format, args } => {
2103 lower_format_string(&format.segments, args, ctx)
2105 }
2106 ExprKind::Closure {
2107 params,
2108 ret_type: _,
2109 body,
2110 } => {
2111 let js_params: Vec<String> = params
2115 .iter()
2116 .map(|p| ctx.resolve_name(&p.name.name, &p.name.span))
2117 .collect();
2118
2119 let js_body = match &body.kind {
2121 ExprKind::Block(block) => {
2122 let mut stmts: Vec<JsStmt> = Vec::new();
2124 for (i, stmt) in block.stmts.iter().enumerate() {
2125 let is_last = i + 1 == block.stmts.len();
2126 if is_last {
2127 stmts.push(lower_tail_stmt(stmt, ctx));
2128 } else {
2129 stmts.push(lower_stmt(stmt, ctx));
2130 }
2131 }
2132 stmts
2133 }
2134 _ => {
2135 vec![JsStmt::Return(lower_expr(body, ctx))]
2137 }
2138 };
2139
2140 JsExpr::Arrow {
2141 params: js_params,
2142 body: js_body,
2143 }
2144 }
2145 ExprKind::Array { elements } => {
2146 let js_elements: Vec<JsExpr> = elements.iter().map(|e| lower_expr(e, ctx)).collect();
2147 JsExpr::Array(js_elements)
2148 }
2149 ExprKind::Index { base, index } => {
2150 if let ExprKind::Range {
2152 start,
2153 end,
2154 inclusive,
2155 } = &index.kind
2156 {
2157 let base_js = lower_expr(base, ctx);
2159 let slice_callee = JsExpr::Member {
2160 object: Box::new(base_js),
2161 property: "slice".to_string(),
2162 };
2163
2164 let mut args = Vec::new();
2165
2166 match start {
2168 Some(s) => args.push(lower_expr(s, ctx)),
2169 None => args.push(JsExpr::Number(0)),
2170 }
2171
2172 if let Some(e) = end {
2174 if *inclusive {
2175 args.push(JsExpr::Binary {
2177 op: JsBinaryOp::Add,
2178 left: Box::new(lower_expr(e, ctx)),
2179 right: Box::new(JsExpr::Number(1)),
2180 });
2181 } else {
2182 args.push(lower_expr(e, ctx));
2183 }
2184 }
2185 JsExpr::Call {
2188 callee: Box::new(slice_callee),
2189 args,
2190 }
2191 } else {
2192 JsExpr::Index {
2194 object: Box::new(lower_expr(base, ctx)),
2195 index: Box::new(lower_expr(index, ctx)),
2196 }
2197 }
2198 }
2199 ExprKind::Range { .. } => {
2200 JsExpr::Object(vec![
2204 ("start".to_string(), JsExpr::Number(0)),
2205 ("end".to_string(), JsExpr::Number(0)),
2206 ])
2207 }
2208 ExprKind::Assign { target, op, value } => {
2209 let js_op = match op {
2210 husk_ast::AssignOp::Assign => JsAssignOp::Assign,
2211 husk_ast::AssignOp::AddAssign => JsAssignOp::AddAssign,
2212 husk_ast::AssignOp::SubAssign => JsAssignOp::SubAssign,
2213 husk_ast::AssignOp::ModAssign => JsAssignOp::ModAssign,
2214 };
2215 JsExpr::Assignment {
2216 left: Box::new(lower_expr(target, ctx)),
2217 op: js_op,
2218 right: Box::new(lower_expr(value, ctx)),
2219 }
2220 }
2221 ExprKind::JsLiteral { code } => {
2222 JsExpr::Raw(format!("({})", code))
2224 }
2225 ExprKind::Cast {
2226 expr: inner,
2227 target_ty,
2228 } => {
2229 let js_inner = lower_expr(inner, ctx);
2230
2231 match &target_ty.kind {
2233 TypeExprKind::Named(ident) => match ident.name.as_str() {
2234 "i32" => {
2235 let mut inner_str = String::new();
2240 write_expr(&js_inner, &mut inner_str);
2241 JsExpr::Raw(format!("(Math.trunc(Number({})) | 0)", inner_str))
2242 }
2243 "i64" => {
2244 let mut inner_str = String::new();
2247 write_expr(&js_inner, &mut inner_str);
2248 JsExpr::Raw(format!("BigInt(Math.trunc({}))", inner_str))
2249 }
2250 "f64" => {
2251 JsExpr::Call {
2253 callee: Box::new(JsExpr::Ident("Number".to_string())),
2254 args: vec![js_inner],
2255 }
2256 }
2257 "String" => {
2258 JsExpr::Call {
2260 callee: Box::new(JsExpr::Ident("String".to_string())),
2261 args: vec![js_inner],
2262 }
2263 }
2264 "bool" => {
2265 panic!("invalid cast to bool should have been rejected by semantic analysis")
2267 }
2268 _ => {
2269 js_inner
2271 }
2272 },
2273 _ => {
2274 js_inner
2276 }
2277 }
2278 }
2279 ExprKind::Tuple { elements } => {
2280 let js_elements: Vec<JsExpr> = elements.iter().map(|e| lower_expr(e, ctx)).collect();
2282 JsExpr::Array(js_elements)
2283 }
2284 ExprKind::TupleField { base, index } => {
2285 let js_base = lower_expr(base, ctx);
2287 JsExpr::Index {
2288 object: Box::new(js_base),
2289 index: Box::new(JsExpr::Number(*index as i64)),
2290 }
2291 }
2292 }
2293}
2294
2295fn lower_format_print(
2297 segments: &[FormatSegment],
2298 args: &[Expr],
2299 newline: bool,
2300 ctx: &CodegenContext,
2301) -> JsExpr {
2302 let mut implicit_index = 0;
2304 let mut parts: Vec<JsExpr> = Vec::new();
2305
2306 for segment in segments {
2307 match segment {
2308 FormatSegment::Literal(text) => {
2309 if !text.is_empty() {
2310 parts.push(JsExpr::String(text.clone()));
2311 }
2312 }
2313 FormatSegment::Placeholder(ph) => {
2314 let arg_index = ph.position.unwrap_or_else(|| {
2316 let idx = implicit_index;
2317 implicit_index += 1;
2318 idx
2319 });
2320
2321 if let Some(arg) = args.get(arg_index) {
2322 let arg_js = lower_expr(arg, ctx);
2323 let formatted = format_arg(arg_js, &ph.spec);
2324 parts.push(formatted);
2325 }
2326 }
2327 }
2328 }
2329
2330 let output_arg = if parts.is_empty() {
2334 JsExpr::String(String::new())
2335 } else if parts.len() == 1 {
2336 parts.pop().unwrap()
2337 } else {
2338 let mut iter = parts.into_iter();
2340 let mut acc = iter.next().unwrap();
2341 for part in iter {
2342 acc = JsExpr::Binary {
2343 op: JsBinaryOp::Add,
2344 left: Box::new(acc),
2345 right: Box::new(part),
2346 };
2347 }
2348 acc
2349 };
2350
2351 if newline {
2352 JsExpr::Call {
2354 callee: Box::new(JsExpr::Member {
2355 object: Box::new(JsExpr::Ident("console".to_string())),
2356 property: "log".to_string(),
2357 }),
2358 args: vec![output_arg],
2359 }
2360 } else {
2361 JsExpr::Call {
2363 callee: Box::new(JsExpr::Member {
2364 object: Box::new(JsExpr::Member {
2365 object: Box::new(JsExpr::Ident("process".to_string())),
2366 property: "stdout".to_string(),
2367 }),
2368 property: "write".to_string(),
2369 }),
2370 args: vec![output_arg],
2371 }
2372 }
2373}
2374
2375fn lower_format_string(
2377 segments: &[FormatSegment],
2378 args: &[Expr],
2379 ctx: &CodegenContext,
2380) -> JsExpr {
2381 let mut implicit_index = 0;
2383 let mut parts: Vec<JsExpr> = Vec::new();
2384
2385 for segment in segments {
2386 match segment {
2387 FormatSegment::Literal(text) => {
2388 if !text.is_empty() {
2389 parts.push(JsExpr::String(text.clone()));
2390 }
2391 }
2392 FormatSegment::Placeholder(ph) => {
2393 let arg_index = ph.position.unwrap_or_else(|| {
2395 let idx = implicit_index;
2396 implicit_index += 1;
2397 idx
2398 });
2399
2400 if let Some(arg) = args.get(arg_index) {
2401 let arg_js = lower_expr(arg, ctx);
2402 let formatted = format_arg(arg_js, &ph.spec);
2403 parts.push(formatted);
2404 }
2405 }
2406 }
2407 }
2408
2409 if parts.is_empty() {
2413 JsExpr::String(String::new())
2414 } else if parts.len() == 1 {
2415 parts.pop().unwrap()
2416 } else {
2417 let mut iter = parts.into_iter();
2419 let mut acc = iter.next().unwrap();
2420 for part in iter {
2421 acc = JsExpr::Binary {
2422 op: JsBinaryOp::Add,
2423 left: Box::new(acc),
2424 right: Box::new(part),
2425 };
2426 }
2427 acc
2428 }
2429}
2430
2431fn format_arg(arg: JsExpr, spec: &FormatSpec) -> JsExpr {
2433 match spec.ty {
2435 Some('?') => {
2436 let pretty = spec.alternate;
2438 JsExpr::Call {
2439 callee: Box::new(JsExpr::Ident("__husk_fmt_debug".to_string())),
2440 args: vec![arg, JsExpr::Bool(pretty)],
2441 }
2442 }
2443 Some('x') | Some('X') | Some('b') | Some('o') => {
2444 let base = match spec.ty {
2446 Some('x') | Some('X') => 16,
2447 Some('b') => 2,
2448 Some('o') => 8,
2449 _ => 10,
2450 };
2451 let uppercase = spec.ty == Some('X');
2452
2453 JsExpr::Call {
2454 callee: Box::new(JsExpr::Ident("__husk_fmt_num".to_string())),
2455 args: vec![
2456 arg,
2457 JsExpr::Number(base),
2458 spec.width
2459 .map_or(JsExpr::Number(0), |w| JsExpr::Number(w as i64)),
2460 spec.precision
2461 .map_or(JsExpr::Ident("null".to_string()), |p| {
2462 JsExpr::Number(p as i64)
2463 }),
2464 spec.fill.map_or(JsExpr::Ident("null".to_string()), |c| {
2465 JsExpr::String(c.to_string())
2466 }),
2467 spec.align.map_or(JsExpr::Ident("null".to_string()), |c| {
2468 JsExpr::String(c.to_string())
2469 }),
2470 JsExpr::Bool(spec.sign),
2471 JsExpr::Bool(spec.alternate),
2472 JsExpr::Bool(spec.zero_pad),
2473 JsExpr::Bool(uppercase),
2474 ],
2475 }
2476 }
2477 None if has_formatting(spec) => {
2478 if spec.zero_pad || spec.sign {
2481 JsExpr::Call {
2483 callee: Box::new(JsExpr::Ident("__husk_fmt_num".to_string())),
2484 args: vec![
2485 arg,
2486 JsExpr::Number(10), spec.width
2488 .map_or(JsExpr::Number(0), |w| JsExpr::Number(w as i64)),
2489 spec.precision
2490 .map_or(JsExpr::Ident("null".to_string()), |p| {
2491 JsExpr::Number(p as i64)
2492 }),
2493 spec.fill.map_or(JsExpr::Ident("null".to_string()), |c| {
2494 JsExpr::String(c.to_string())
2495 }),
2496 spec.align.map_or(JsExpr::Ident("null".to_string()), |c| {
2497 JsExpr::String(c.to_string())
2498 }),
2499 JsExpr::Bool(spec.sign),
2500 JsExpr::Bool(spec.alternate),
2501 JsExpr::Bool(spec.zero_pad),
2502 JsExpr::Bool(false), ],
2504 }
2505 } else if spec.width.is_some() {
2506 let str_arg = JsExpr::Call {
2508 callee: Box::new(JsExpr::Ident("String".to_string())),
2509 args: vec![arg],
2510 };
2511 JsExpr::Call {
2512 callee: Box::new(JsExpr::Ident("__husk_fmt_pad".to_string())),
2513 args: vec![
2514 str_arg,
2515 JsExpr::Number(spec.width.unwrap_or(0) as i64),
2516 spec.fill.map_or(JsExpr::Ident("null".to_string()), |c| {
2517 JsExpr::String(c.to_string())
2518 }),
2519 spec.align.map_or(JsExpr::Ident("null".to_string()), |c| {
2520 JsExpr::String(c.to_string())
2521 }),
2522 ],
2523 }
2524 } else {
2525 JsExpr::Call {
2527 callee: Box::new(JsExpr::Ident("__husk_fmt".to_string())),
2528 args: vec![arg],
2529 }
2530 }
2531 }
2532 None => {
2533 JsExpr::Call {
2535 callee: Box::new(JsExpr::Ident("__husk_fmt".to_string())),
2536 args: vec![arg],
2537 }
2538 }
2539 _ => {
2540 JsExpr::Call {
2542 callee: Box::new(JsExpr::Ident("__husk_fmt".to_string())),
2543 args: vec![arg],
2544 }
2545 }
2546 }
2547}
2548
2549fn has_formatting(spec: &FormatSpec) -> bool {
2551 spec.fill.is_some()
2552 || spec.align.is_some()
2553 || spec.sign
2554 || spec.alternate
2555 || spec.zero_pad
2556 || spec.width.is_some()
2557 || spec.precision.is_some()
2558}
2559
2560fn extract_pattern_bindings(
2563 pattern: &husk_ast::Pattern,
2564 scrutinee_js: &JsExpr,
2565) -> Vec<(String, JsExpr)> {
2566 use husk_ast::PatternKind;
2567
2568 match &pattern.kind {
2569 PatternKind::EnumTuple { fields, .. } => {
2570 let mut bindings = Vec::new();
2571 for (i, field) in fields.iter().enumerate() {
2572 if let PatternKind::Binding(ident) = &field.kind {
2573 let accessor = if fields.len() == 1 {
2575 JsExpr::Member {
2576 object: Box::new(scrutinee_js.clone()),
2577 property: "value".to_string(),
2578 }
2579 } else {
2580 JsExpr::Index {
2581 object: Box::new(scrutinee_js.clone()),
2582 index: Box::new(JsExpr::String(i.to_string())),
2583 }
2584 };
2585 bindings.push((ident.name.clone(), accessor));
2586 }
2587 }
2588 bindings
2589 }
2590 PatternKind::Tuple { fields } => {
2591 let mut bindings = Vec::new();
2593 for (i, field) in fields.iter().enumerate() {
2594 let accessor = JsExpr::Index {
2595 object: Box::new(scrutinee_js.clone()),
2596 index: Box::new(JsExpr::Number(i as i64)),
2597 };
2598 match &field.kind {
2600 PatternKind::Binding(ident) => {
2601 bindings.push((ident.name.clone(), accessor));
2602 }
2603 PatternKind::Wildcard => {
2604 }
2606 PatternKind::Tuple { .. } => {
2607 bindings.extend(extract_pattern_bindings(field, &accessor));
2609 }
2610 _ => {}
2611 }
2612 }
2613 bindings
2614 }
2615 _ => Vec::new(),
2616 }
2617}
2618
2619fn lower_match_expr(
2620 scrutinee: &Expr,
2621 arms: &[husk_ast::MatchArm],
2622 ctx: &CodegenContext,
2623) -> JsExpr {
2624 use husk_ast::PatternKind;
2625
2626 let scrutinee_js = lower_expr(scrutinee, ctx);
2627
2628 fn pattern_test(
2633 pattern: &husk_ast::Pattern,
2634 scrutinee_js: &JsExpr,
2635 variant_patterns: &VariantPatternMap,
2636 ) -> Option<JsExpr> {
2637 match &pattern.kind {
2638 PatternKind::EnumUnit { path } | PatternKind::EnumTuple { path, .. } => {
2639 let variant = path
2641 .last()
2642 .map(|id| id.name.clone())
2643 .unwrap_or_else(|| "Unknown".to_string());
2644 let tag_access = JsExpr::Member {
2645 object: Box::new(scrutinee_js.clone()),
2646 property: "tag".to_string(),
2647 };
2648 Some(JsExpr::Binary {
2649 op: JsBinaryOp::EqEq,
2650 left: Box::new(tag_access),
2651 right: Box::new(JsExpr::String(variant)),
2652 })
2653 }
2654 PatternKind::Binding(_) => {
2655 if let Some((_enum_name, variant_name)) =
2657 variant_patterns.get(&(pattern.span.range.start, pattern.span.range.end))
2658 {
2659 let tag_access = JsExpr::Member {
2660 object: Box::new(scrutinee_js.clone()),
2661 property: "tag".to_string(),
2662 };
2663 Some(JsExpr::Binary {
2664 op: JsBinaryOp::EqEq,
2665 left: Box::new(tag_access),
2666 right: Box::new(JsExpr::String(variant_name.clone())),
2667 })
2668 } else {
2669 None
2671 }
2672 }
2673 PatternKind::Wildcard => None,
2674 PatternKind::EnumStruct { .. } => None,
2675 PatternKind::Tuple { .. } => None,
2677 }
2678 }
2679
2680 fn lower_arm_body(
2682 arm: &husk_ast::MatchArm,
2683 scrutinee_js: &JsExpr,
2684 ctx: &CodegenContext,
2685 ) -> JsExpr {
2686 let bindings = extract_pattern_bindings(&arm.pattern, scrutinee_js);
2687 if bindings.is_empty() {
2688 lower_expr(&arm.expr, ctx)
2689 } else {
2690 let mut body = Vec::new();
2693 for (name, accessor) in bindings {
2694 body.push(JsStmt::Let {
2695 name,
2696 init: Some(accessor),
2697 });
2698 }
2699 body.push(JsStmt::Return(lower_expr(&arm.expr, ctx)));
2700 JsExpr::Iife { body }
2701 }
2702 }
2703
2704 if arms.is_empty() {
2705 return JsExpr::Ident("undefined".to_string());
2706 }
2707
2708 let mut iter = arms.iter().rev();
2710 let last_arm = iter.next().unwrap();
2711 let mut acc = lower_arm_body(last_arm, &scrutinee_js, ctx);
2712
2713 for arm in iter {
2714 if let Some(test) = pattern_test(&arm.pattern, &scrutinee_js, ctx.variant_patterns) {
2715 let then_expr = lower_arm_body(arm, &scrutinee_js, ctx);
2716 acc = JsExpr::Conditional {
2717 test: Box::new(test),
2718 then_branch: Box::new(then_expr),
2719 else_branch: Box::new(acc),
2720 };
2721 } else {
2722 acc = lower_arm_body(arm, &scrutinee_js, ctx);
2724 }
2725 }
2726
2727 acc
2728}
2729
2730fn lower_match_stmt(
2733 scrutinee: &Expr,
2734 arms: &[husk_ast::MatchArm],
2735 ctx: &CodegenContext,
2736) -> JsStmt {
2737 use husk_ast::PatternKind;
2738
2739 let scrutinee_js = lower_expr(scrutinee, ctx);
2740
2741 fn pattern_test(
2745 pattern: &husk_ast::Pattern,
2746 scrutinee_js: &JsExpr,
2747 variant_patterns: &VariantPatternMap,
2748 ) -> Option<JsExpr> {
2749 match &pattern.kind {
2750 PatternKind::EnumUnit { path } | PatternKind::EnumTuple { path, .. } => {
2751 let variant = path
2752 .last()
2753 .map(|id| id.name.clone())
2754 .unwrap_or_else(|| "Unknown".to_string());
2755 let tag_access = JsExpr::Member {
2756 object: Box::new(scrutinee_js.clone()),
2757 property: "tag".to_string(),
2758 };
2759 Some(JsExpr::Binary {
2760 op: JsBinaryOp::EqEq,
2761 left: Box::new(tag_access),
2762 right: Box::new(JsExpr::String(variant)),
2763 })
2764 }
2765 PatternKind::Binding(_) => {
2766 if let Some((_enum_name, variant_name)) =
2768 variant_patterns.get(&(pattern.span.range.start, pattern.span.range.end))
2769 {
2770 let tag_access = JsExpr::Member {
2771 object: Box::new(scrutinee_js.clone()),
2772 property: "tag".to_string(),
2773 };
2774 Some(JsExpr::Binary {
2775 op: JsBinaryOp::EqEq,
2776 left: Box::new(tag_access),
2777 right: Box::new(JsExpr::String(variant_name.clone())),
2778 })
2779 } else {
2780 None
2782 }
2783 }
2784 PatternKind::Wildcard => None,
2785 PatternKind::EnumStruct { .. } => None,
2786 PatternKind::Tuple { .. } => None,
2788 }
2789 }
2790
2791 fn lower_arm_body_stmts(
2793 arm: &husk_ast::MatchArm,
2794 scrutinee_js: &JsExpr,
2795 ctx: &CodegenContext,
2796 ) -> Vec<JsStmt> {
2797 let bindings = extract_pattern_bindings(&arm.pattern, scrutinee_js);
2798 let mut stmts = Vec::new();
2799
2800 for (name, accessor) in bindings {
2802 stmts.push(JsStmt::Let {
2803 name,
2804 init: Some(accessor),
2805 });
2806 }
2807
2808 match &arm.expr.kind {
2810 ExprKind::Block(block) => {
2811 for stmt in &block.stmts {
2812 stmts.push(lower_stmt(stmt, ctx));
2813 }
2814 }
2815 _ => {
2816 stmts.push(JsStmt::Expr(lower_expr(&arm.expr, ctx)));
2817 }
2818 }
2819
2820 stmts
2821 }
2822
2823 if arms.is_empty() {
2824 return JsStmt::Expr(JsExpr::Ident("undefined".to_string()));
2825 }
2826
2827 let mut result: Option<JsStmt> = None;
2829
2830 for arm in arms.iter().rev() {
2831 let body = lower_arm_body_stmts(arm, &scrutinee_js, ctx);
2832
2833 if let Some(test) = pattern_test(&arm.pattern, &scrutinee_js, ctx.variant_patterns) {
2834 let else_block = result.map(|s| vec![s]);
2835 result = Some(JsStmt::If {
2836 cond: test,
2837 then_block: body,
2838 else_block,
2839 });
2840 } else {
2841 if body.len() == 1 {
2844 result = Some(body.into_iter().next().unwrap());
2845 } else {
2846 result = Some(JsStmt::If {
2848 cond: JsExpr::Bool(true),
2849 then_block: body,
2850 else_block: None,
2851 });
2852 }
2853 }
2854 }
2855
2856 result.unwrap_or_else(|| JsStmt::Expr(JsExpr::Ident("undefined".to_string())))
2857}
2858
2859impl JsModule {
2860 pub fn to_source(&self) -> String {
2862 let mut out = String::new();
2863 for stmt in &self.body {
2864 write_stmt(stmt, 0, &mut out);
2865 out.push('\n');
2866 }
2867 out
2868 }
2869
2870 pub fn to_source_with_preamble(&self) -> String {
2874 let mut out = String::new();
2875
2876 let mut has_imports = false;
2878 for stmt in &self.body {
2879 if is_import_stmt(stmt) {
2880 write_stmt(stmt, 0, &mut out);
2881 out.push('\n');
2882 has_imports = true;
2883 }
2884 }
2885 if has_imports {
2886 out.push('\n');
2887 }
2888
2889 out.push_str(std_preamble_js());
2891 if !out.ends_with('\n') {
2892 out.push('\n');
2893 }
2894 out.push('\n');
2895
2896 for stmt in &self.body {
2898 if !is_import_stmt(stmt) {
2899 write_stmt(stmt, 0, &mut out);
2900 out.push('\n');
2901 }
2902 }
2903
2904 out
2905 }
2906
2907 pub fn to_source_with_sourcemap(
2910 &self,
2911 source_file: &str,
2912 source_content: &str,
2913 ) -> (String, String) {
2914 let mut out = String::new();
2915 let mut builder = SourceMapBuilder::new(Some(source_file));
2916
2917 let source_id = builder.add_source(source_file);
2919 builder.set_source_contents(source_id, Some(source_content));
2920
2921 let preamble = std_preamble_js();
2923 let preamble_lines = preamble.lines().count() as u32;
2924
2925 let mut has_imports = false;
2927 for stmt in &self.body {
2928 if is_import_stmt(stmt) {
2929 write_stmt(stmt, 0, &mut out);
2930 out.push('\n');
2931 has_imports = true;
2932 }
2933 }
2934 let import_lines = if has_imports {
2935 out.lines().count() as u32 + 1 } else {
2937 0
2938 };
2939 if has_imports {
2940 out.push('\n');
2941 }
2942
2943 out.push_str(preamble);
2945 if !out.ends_with('\n') {
2946 out.push('\n');
2947 }
2948 out.push('\n');
2949
2950 let mut current_line = import_lines + preamble_lines + 1; for stmt in &self.body {
2955 if !is_import_stmt(stmt) {
2956 if let JsStmt::Function {
2958 name,
2959 source_span: Some(span),
2960 ..
2961 } = stmt
2962 {
2963 builder.add(
2964 current_line,
2965 0,
2966 span.line,
2967 span.column,
2968 Some(source_file),
2969 Some(name.as_str()),
2970 false,
2971 );
2972 }
2973 write_stmt(stmt, 0, &mut out);
2974 out.push('\n');
2975 current_line += count_newlines_in_stmt(stmt) + 1;
2976 }
2977 }
2978
2979 let source_map = builder.into_sourcemap();
2980 let mut sm_out = Vec::new();
2981 source_map
2982 .to_writer(&mut sm_out)
2983 .expect("failed to write source map");
2984 let sm_json = String::from_utf8(sm_out).expect("source map is utf8");
2985
2986 (out, sm_json)
2987 }
2988}
2989
2990fn count_newlines_in_stmt(stmt: &JsStmt) -> u32 {
2992 match stmt {
2993 JsStmt::Function { body, .. } => {
2994 1 + body
2996 .iter()
2997 .map(|s| count_newlines_in_stmt(s) + 1)
2998 .sum::<u32>()
2999 }
3000 JsStmt::TryCatch {
3001 try_block,
3002 catch_block,
3003 ..
3004 } => {
3005 2 + try_block
3007 .iter()
3008 .map(|s| count_newlines_in_stmt(s) + 1)
3009 .sum::<u32>()
3010 + catch_block
3011 .iter()
3012 .map(|s| count_newlines_in_stmt(s) + 1)
3013 .sum::<u32>()
3014 }
3015 JsStmt::If {
3016 then_block,
3017 else_block,
3018 ..
3019 } => {
3020 let then_lines = then_block
3022 .iter()
3023 .map(|s| count_newlines_in_stmt(s) + 1)
3024 .sum::<u32>();
3025 let else_lines = else_block.as_ref().map_or(0, |eb| {
3026 1 + eb
3027 .iter()
3028 .map(|s| count_newlines_in_stmt(s) + 1)
3029 .sum::<u32>()
3030 });
3031 1 + then_lines + else_lines
3032 }
3033 JsStmt::Block(stmts) => {
3034 1 + stmts
3036 .iter()
3037 .map(|s| count_newlines_in_stmt(s) + 1)
3038 .sum::<u32>()
3039 }
3040 JsStmt::Sequence(stmts) => {
3041 stmts
3043 .iter()
3044 .map(|s| count_newlines_in_stmt(s) + 1)
3045 .sum::<u32>()
3046 .saturating_sub(1)
3047 }
3048 _ => 0, }
3050}
3051
3052fn indent(level: usize, out: &mut String) {
3053 for _ in 0..level {
3054 out.push_str(" ");
3055 }
3056}
3057
3058fn is_import_stmt(stmt: &JsStmt) -> bool {
3060 matches!(
3061 stmt,
3062 JsStmt::Import { .. }
3063 | JsStmt::NamedImport { .. }
3064 | JsStmt::Require { .. }
3065 | JsStmt::NamedRequire { .. }
3066 )
3067}
3068
3069fn write_stmt(stmt: &JsStmt, indent_level: usize, out: &mut String) {
3070 match stmt {
3071 JsStmt::Import { name, source } => {
3072 indent(indent_level, out);
3073 out.push_str("import ");
3074 out.push_str(name);
3075 out.push_str(" from \"");
3076 out.push_str(&source.replace('"', "\\\""));
3077 out.push_str("\";");
3078 }
3079 JsStmt::NamedImport { names, source } => {
3080 let safe_name = source.replace('-', "_").replace('@', "").replace('/', "_");
3085 let star_name = format!("__{}", safe_name);
3086 let pkg_name = format!("_{}", safe_name);
3087
3088 indent(indent_level, out);
3089 out.push_str("import * as ");
3090 out.push_str(&star_name);
3091 out.push_str(" from \"");
3092 out.push_str(&source.replace('"', "\\\""));
3093 out.push_str("\";\n");
3094
3095 indent(indent_level, out);
3096 out.push_str("const ");
3097 out.push_str(&pkg_name);
3098 out.push_str(" = ");
3099 out.push_str(&star_name);
3100 out.push_str(".default || ");
3101 out.push_str(&star_name);
3102 out.push_str(";\n");
3103
3104 indent(indent_level, out);
3105 out.push_str("const { ");
3106 for (i, name) in names.iter().enumerate() {
3107 if i > 0 {
3108 out.push_str(", ");
3109 }
3110 out.push_str(name);
3111 }
3112 out.push_str(" } = ");
3113 out.push_str(&pkg_name);
3114 out.push(';');
3115 }
3116 JsStmt::Require { name, source } => {
3117 indent(indent_level, out);
3118 out.push_str("const ");
3119 out.push_str(name);
3120 out.push_str(" = require(\"");
3121 out.push_str(&source.replace('"', "\\\""));
3122 out.push_str("\");");
3123 }
3124 JsStmt::NamedRequire { names, source } => {
3125 indent(indent_level, out);
3126 out.push_str("const { ");
3127 for (i, name) in names.iter().enumerate() {
3128 if i > 0 {
3129 out.push_str(", ");
3130 }
3131 out.push_str(name);
3132 }
3133 out.push_str(" } = require(\"");
3134 out.push_str(&source.replace('"', "\\\""));
3135 out.push_str("\");");
3136 }
3137 JsStmt::ExportNamed { names } => {
3138 indent(indent_level, out);
3139 out.push_str("export { ");
3140 for (i, name) in names.iter().enumerate() {
3141 if i > 0 {
3142 out.push_str(", ");
3143 }
3144 out.push_str(name);
3145 }
3146 out.push_str(" };");
3147 }
3148 JsStmt::Function {
3149 name, params, body, ..
3150 } => {
3151 indent(indent_level, out);
3152 out.push_str("function ");
3153 out.push_str(name);
3154 out.push('(');
3155 for (i, p) in params.iter().enumerate() {
3156 if i > 0 {
3157 out.push_str(", ");
3158 }
3159 out.push_str(p);
3160 }
3161 out.push_str(") {\n");
3162 for s in body {
3163 write_stmt(s, indent_level + 1, out);
3164 out.push('\n');
3165 }
3166 indent(indent_level, out);
3167 out.push('}');
3168 }
3169 JsStmt::Return(expr) => {
3170 indent(indent_level, out);
3171 out.push_str("return ");
3172 write_expr(expr, out);
3173 out.push(';');
3174 }
3175 JsStmt::Let { name, init } => {
3176 indent(indent_level, out);
3177 out.push_str("let ");
3178 out.push_str(name);
3179 if let Some(expr) = init {
3180 out.push_str(" = ");
3181 write_expr(expr, out);
3182 }
3183 out.push(';');
3184 }
3185 JsStmt::LetDestructure { pattern, init } => {
3186 indent(indent_level, out);
3187 out.push_str("let ");
3188 write_destructure_pattern_list(pattern, out);
3189 if let Some(expr) = init {
3190 out.push_str(" = ");
3191 write_expr(expr, out);
3192 }
3193 out.push(';');
3194 }
3195 JsStmt::Expr(expr) => {
3196 indent(indent_level, out);
3197 write_expr(expr, out);
3198 out.push(';');
3199 }
3200 JsStmt::TryCatch {
3201 try_block,
3202 catch_ident,
3203 catch_block,
3204 } => {
3205 indent(indent_level, out);
3206 out.push_str("try {\n");
3207 for s in try_block {
3208 write_stmt(s, indent_level + 1, out);
3209 out.push('\n');
3210 }
3211 indent(indent_level, out);
3212 out.push_str("} catch (");
3213 out.push_str(catch_ident);
3214 out.push_str(") {\n");
3215 for s in catch_block {
3216 write_stmt(s, indent_level + 1, out);
3217 out.push('\n');
3218 }
3219 indent(indent_level, out);
3220 out.push('}');
3221 }
3222 JsStmt::If {
3223 cond,
3224 then_block,
3225 else_block,
3226 } => {
3227 indent(indent_level, out);
3228 out.push_str("if (");
3229 write_expr(cond, out);
3230 out.push_str(") {\n");
3231 for s in then_block {
3232 write_stmt(s, indent_level + 1, out);
3233 out.push('\n');
3234 }
3235 indent(indent_level, out);
3236 out.push('}');
3237 if let Some(else_stmts) = else_block {
3238 if else_stmts.len() == 1 {
3240 if let JsStmt::If { .. } = &else_stmts[0] {
3241 out.push_str(" else ");
3242 write_stmt(&else_stmts[0], 0, out);
3244 return;
3246 }
3247 }
3248 out.push_str(" else {\n");
3249 for s in else_stmts {
3250 write_stmt(s, indent_level + 1, out);
3251 out.push('\n');
3252 }
3253 indent(indent_level, out);
3254 out.push('}');
3255 }
3256 }
3257 JsStmt::ForOf {
3258 binding,
3259 iterable,
3260 body,
3261 } => {
3262 indent(indent_level, out);
3263 out.push_str("for (const ");
3264 out.push_str(binding);
3265 out.push_str(" of ");
3266 write_expr(iterable, out);
3267 out.push_str(") {\n");
3268 for s in body {
3269 write_stmt(s, indent_level + 1, out);
3270 out.push('\n');
3271 }
3272 indent(indent_level, out);
3273 out.push('}');
3274 }
3275 JsStmt::For {
3276 binding,
3277 start,
3278 end,
3279 inclusive,
3280 body,
3281 } => {
3282 indent(indent_level, out);
3283 out.push_str("for (let ");
3284 out.push_str(binding);
3285 out.push_str(" = ");
3286 write_expr(start, out);
3287 out.push_str("; ");
3288 out.push_str(binding);
3289 if *inclusive {
3290 out.push_str(" <= ");
3291 } else {
3292 out.push_str(" < ");
3293 }
3294 write_expr(end, out);
3295 out.push_str("; ");
3296 out.push_str(binding);
3297 out.push_str("++) {\n");
3298 for s in body {
3299 write_stmt(s, indent_level + 1, out);
3300 out.push('\n');
3301 }
3302 indent(indent_level, out);
3303 out.push('}');
3304 }
3305 JsStmt::Assign { target, op, value } => {
3306 indent(indent_level, out);
3307 write_expr(target, out);
3308 out.push_str(match op {
3309 JsAssignOp::Assign => " = ",
3310 JsAssignOp::AddAssign => " += ",
3311 JsAssignOp::SubAssign => " -= ",
3312 JsAssignOp::ModAssign => " %= ",
3313 });
3314 write_expr(value, out);
3315 out.push(';');
3316 }
3317 JsStmt::While { cond, body } => {
3318 indent(indent_level, out);
3319 out.push_str("while (");
3320 write_expr(cond, out);
3321 out.push_str(") {\n");
3322 for s in body {
3323 write_stmt(s, indent_level + 1, out);
3324 out.push('\n');
3325 }
3326 indent(indent_level, out);
3327 out.push('}');
3328 }
3329 JsStmt::Break => {
3330 indent(indent_level, out);
3331 out.push_str("break;");
3332 }
3333 JsStmt::Continue => {
3334 indent(indent_level, out);
3335 out.push_str("continue;");
3336 }
3337 JsStmt::Block(stmts) => {
3338 indent(indent_level, out);
3339 out.push_str("{\n");
3340 for s in stmts {
3341 write_stmt(s, indent_level + 1, out);
3342 out.push('\n');
3343 }
3344 indent(indent_level, out);
3345 out.push('}');
3346 }
3347 JsStmt::Sequence(stmts) => {
3348 for (i, s) in stmts.iter().enumerate() {
3351 if i > 0 {
3352 out.push('\n');
3353 }
3354 write_stmt(s, indent_level, out);
3355 }
3356 }
3357 }
3358}
3359
3360fn write_destructure_pattern(pattern: &DestructurePattern, out: &mut String) {
3362 match pattern {
3363 DestructurePattern::Binding(name) => out.push_str(name),
3364 DestructurePattern::Wildcard => out.push('_'),
3365 DestructurePattern::Array(elements) => {
3366 write_destructure_pattern_list(elements, out);
3367 }
3368 }
3369}
3370
3371fn write_destructure_pattern_list(patterns: &[DestructurePattern], out: &mut String) {
3373 out.push('[');
3374 for (i, pat) in patterns.iter().enumerate() {
3375 if i > 0 {
3376 out.push_str(", ");
3377 }
3378 write_destructure_pattern(pat, out);
3379 }
3380 out.push(']');
3381}
3382
3383fn write_expr(expr: &JsExpr, out: &mut String) {
3384 match expr {
3385 JsExpr::Ident(name) => out.push_str(name),
3386 JsExpr::Number(n) => out.push_str(&n.to_string()),
3387 JsExpr::BigInt(n) => {
3388 out.push_str(&n.to_string());
3389 out.push('n');
3390 }
3391 JsExpr::Float(f) => out.push_str(&f.to_string()),
3392 JsExpr::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
3393 JsExpr::Object(props) => {
3394 out.push('{');
3395 for (i, (key, value)) in props.iter().enumerate() {
3396 if i > 0 {
3397 out.push_str(", ");
3398 }
3399 out.push_str(key);
3400 out.push_str(": ");
3401 write_expr(value, out);
3402 }
3403 out.push('}');
3404 }
3405 JsExpr::Member { object, property } => {
3406 write_expr(object, out);
3407 out.push('.');
3408 out.push_str(property);
3409 }
3410 JsExpr::String(s) => {
3411 out.push('"');
3412 for ch in s.chars() {
3413 match ch {
3414 '\\' => out.push_str("\\\\"),
3415 '"' => out.push_str("\\\""),
3416 '\n' => out.push_str("\\n"),
3417 '\r' => out.push_str("\\r"),
3418 '\t' => out.push_str("\\t"),
3419 '\0' => out.push_str("\\0"),
3420 c if c.is_control() => {
3421 out.push_str(&format!("\\u{:04x}", c as u32));
3422 }
3423 c => out.push(c),
3424 }
3425 }
3426 out.push('"');
3427 }
3428 JsExpr::Assignment { left, op, right } => {
3429 write_expr(left, out);
3430 match op {
3431 JsAssignOp::Assign => out.push_str(" = "),
3432 JsAssignOp::AddAssign => out.push_str(" += "),
3433 JsAssignOp::SubAssign => out.push_str(" -= "),
3434 JsAssignOp::ModAssign => out.push_str(" %= "),
3435 }
3436 write_expr(right, out);
3437 }
3438 JsExpr::Call { callee, args } => {
3439 write_expr(callee, out);
3440 out.push('(');
3441 for (i, arg) in args.iter().enumerate() {
3442 if i > 0 {
3443 out.push_str(", ");
3444 }
3445 write_expr(arg, out);
3446 }
3447 out.push(')');
3448 }
3449 JsExpr::Conditional {
3450 test,
3451 then_branch,
3452 else_branch,
3453 } => {
3454 out.push('(');
3455 write_expr(test, out);
3456 out.push_str(" ? ");
3457 write_expr(then_branch, out);
3458 out.push_str(" : ");
3459 write_expr(else_branch, out);
3460 out.push(')');
3461 }
3462 JsExpr::Binary { op, left, right } => {
3463 write_expr(left, out);
3464 out.push(' ');
3465 out.push_str(match op {
3466 JsBinaryOp::Add => "+",
3467 JsBinaryOp::Sub => "-",
3468 JsBinaryOp::Mul => "*",
3469 JsBinaryOp::Div => "/",
3470 JsBinaryOp::Mod => "%",
3471 JsBinaryOp::EqEq => "==",
3472 JsBinaryOp::NotEq => "!=",
3473 JsBinaryOp::Lt => "<",
3474 JsBinaryOp::Gt => ">",
3475 JsBinaryOp::Le => "<=",
3476 JsBinaryOp::Ge => ">=",
3477 JsBinaryOp::And => "&&",
3478 JsBinaryOp::Or => "||",
3479 JsBinaryOp::StrictEq => "===",
3480 JsBinaryOp::StrictNe => "!==",
3481 });
3482 out.push(' ');
3483 write_expr(right, out);
3484 }
3485 JsExpr::Iife { body } => {
3486 out.push_str("(function() {\n");
3487 for s in body {
3488 write_stmt(s, 1, out);
3489 out.push('\n');
3490 }
3491 out.push_str("})()");
3492 }
3493 JsExpr::Function { params, body } => {
3494 out.push_str("function(");
3495 for (i, p) in params.iter().enumerate() {
3496 if i > 0 {
3497 out.push_str(", ");
3498 }
3499 out.push_str(p);
3500 }
3501 out.push_str(") {\n");
3502 for s in body {
3503 write_stmt(s, 1, out);
3504 out.push('\n');
3505 }
3506 out.push('}');
3507 }
3508 JsExpr::New { constructor, args } => {
3509 out.push_str("new ");
3510 out.push_str(constructor);
3511 out.push('(');
3512 for (i, arg) in args.iter().enumerate() {
3513 if i > 0 {
3514 out.push_str(", ");
3515 }
3516 write_expr(arg, out);
3517 }
3518 out.push(')');
3519 }
3520 JsExpr::Arrow { params, body } => {
3521 out.push('(');
3523 for (i, p) in params.iter().enumerate() {
3524 if i > 0 {
3525 out.push_str(", ");
3526 }
3527 out.push_str(p);
3528 }
3529 out.push_str(") => ");
3530
3531 if body.len() == 1 {
3533 if let JsStmt::Return(expr) = &body[0] {
3534 write_expr(expr, out);
3535 return;
3536 }
3537 }
3538
3539 out.push_str("{\n");
3541 for s in body {
3542 write_stmt(s, 1, out);
3543 out.push('\n');
3544 }
3545 out.push('}');
3546 }
3547 JsExpr::Array(elements) => {
3548 out.push('[');
3549 for (i, elem) in elements.iter().enumerate() {
3550 if i > 0 {
3551 out.push_str(", ");
3552 }
3553 write_expr(elem, out);
3554 }
3555 out.push(']');
3556 }
3557 JsExpr::Index { object, index } => {
3558 write_expr(object, out);
3559 out.push('[');
3560 write_expr(index, out);
3561 out.push(']');
3562 }
3563 JsExpr::Raw(code) => {
3564 out.push_str(code);
3566 }
3567 JsExpr::Unary { op, expr } => {
3568 let op_str = match op {
3569 JsUnaryOp::Not => "!",
3570 JsUnaryOp::Neg => "-",
3571 };
3572 out.push_str(op_str);
3573 out.push('(');
3575 write_expr(expr, out);
3576 out.push(')');
3577 }
3578 }
3579}
3580
3581pub fn file_to_dts(file: &File) -> String {
3585 let mut out = String::new();
3586
3587 for item in &file.items {
3588 match &item.kind {
3589 ItemKind::Struct {
3590 name,
3591 type_params,
3592 fields,
3593 } => {
3594 write_struct_dts(name, type_params, fields, &mut out);
3595 out.push('\n');
3596 }
3597 ItemKind::Enum {
3598 name,
3599 type_params,
3600 variants,
3601 } => {
3602 write_enum_dts(name, type_params, variants, &mut out);
3603 out.push('\n');
3604 }
3605 ItemKind::Fn {
3606 name,
3607 type_params,
3608 params,
3609 ret_type,
3610 ..
3611 } => {
3612 write_fn_dts(name, type_params, params, ret_type.as_ref(), &mut out);
3613 out.push('\n');
3614 }
3615 _ => {}
3616 }
3617 }
3618
3619 out
3620}
3621
3622fn write_struct_dts(name: &Ident, type_params: &[Ident], fields: &[StructField], out: &mut String) {
3623 out.push_str("export interface ");
3624 out.push_str(&name.name);
3625 write_simple_type_params(type_params, out);
3626 out.push_str(" {\n");
3627 for field in fields {
3628 out.push_str(" ");
3629 out.push_str(&field.name.name);
3630 out.push_str(": ");
3631 write_type_expr(&field.ty, out);
3632 out.push_str(";\n");
3633 }
3634 out.push_str("}\n");
3635}
3636
3637fn write_enum_dts(
3638 name: &Ident,
3639 type_params: &[Ident],
3640 variants: &[husk_ast::EnumVariant],
3641 out: &mut String,
3642) {
3643 out.push_str("export type ");
3644 out.push_str(&name.name);
3645 write_simple_type_params(type_params, out);
3646 out.push_str(" =\n");
3647
3648 for (i, variant) in variants.iter().enumerate() {
3649 out.push_str(" | { tag: \"");
3650 out.push_str(&variant.name.name);
3651 out.push('"');
3652
3653 match &variant.fields {
3654 EnumVariantFields::Unit => {}
3655 EnumVariantFields::Tuple(tys) => {
3656 if tys.len() == 1 {
3657 out.push_str("; value: ");
3658 write_type_expr(&tys[0], out);
3659 } else {
3660 for (idx, ty) in tys.iter().enumerate() {
3661 out.push_str("; value");
3662 out.push(char::from(b'0' + (idx as u8)));
3663 out.push_str(": ");
3664 write_type_expr(ty, out);
3665 }
3666 }
3667 }
3668 EnumVariantFields::Struct(fields) => {
3669 for field in fields {
3670 out.push_str("; ");
3671 out.push_str(&field.name.name);
3672 out.push_str(": ");
3673 write_type_expr(&field.ty, out);
3674 }
3675 }
3676 }
3677
3678 out.push_str(" }");
3679 if i + 1 < variants.len() {
3680 out.push('\n');
3681 } else {
3682 out.push_str(";\n");
3683 }
3684 }
3685}
3686
3687fn write_fn_dts(
3688 name: &Ident,
3689 type_params: &[TypeParam],
3690 params: &[Param],
3691 ret_type: Option<&TypeExpr>,
3692 out: &mut String,
3693) {
3694 out.push_str("export function ");
3695 out.push_str(&name.name);
3696 write_type_params(type_params, out);
3697 out.push('(');
3698 for (i, param) in params.iter().enumerate() {
3699 if i > 0 {
3700 out.push_str(", ");
3701 }
3702 out.push_str(¶m.name.name);
3703 out.push_str(": ");
3704 write_type_expr(¶m.ty, out);
3705 }
3706 out.push(')');
3707 out.push_str(": ");
3708 if let Some(ret) = ret_type {
3709 write_type_expr(ret, out);
3710 } else {
3711 out.push_str("void");
3712 }
3713 out.push_str(";\n");
3714}
3715
3716fn write_type_params(type_params: &[TypeParam], out: &mut String) {
3717 if !type_params.is_empty() {
3718 out.push('<');
3719 for (i, tp) in type_params.iter().enumerate() {
3720 if i > 0 {
3721 out.push_str(", ");
3722 }
3723 out.push_str(&tp.name.name);
3724 if !tp.bounds.is_empty() {
3726 out.push_str(" extends ");
3727 for (j, bound) in tp.bounds.iter().enumerate() {
3728 if j > 0 {
3729 out.push_str(" & ");
3730 }
3731 write_type_expr(bound, out);
3732 }
3733 }
3734 }
3735 out.push('>');
3736 }
3737}
3738
3739fn write_simple_type_params(type_params: &[Ident], out: &mut String) {
3741 if !type_params.is_empty() {
3742 out.push('<');
3743 for (i, tp) in type_params.iter().enumerate() {
3744 if i > 0 {
3745 out.push_str(", ");
3746 }
3747 out.push_str(&tp.name);
3748 }
3749 out.push('>');
3750 }
3751}
3752
3753fn write_type_expr(ty: &TypeExpr, out: &mut String) {
3754 match &ty.kind {
3755 TypeExprKind::Named(id) => match id.name.as_str() {
3756 "i32" => out.push_str("number"),
3757 "bool" => out.push_str("boolean"),
3758 "String" => out.push_str("string"),
3759 "()" => out.push_str("void"),
3760 other => out.push_str(other),
3761 },
3762 TypeExprKind::Generic { name, args } => {
3763 out.push_str(&name.name);
3764 out.push('<');
3765 for (i, arg) in args.iter().enumerate() {
3766 if i > 0 {
3767 out.push_str(", ");
3768 }
3769 write_type_expr(arg, out);
3770 }
3771 out.push('>');
3772 }
3773 TypeExprKind::Function { params, ret } => {
3774 out.push('(');
3776 for (i, param) in params.iter().enumerate() {
3777 if i > 0 {
3778 out.push_str(", ");
3779 }
3780 out.push_str(&format!("arg{}: ", i));
3781 write_type_expr(param, out);
3782 }
3783 out.push_str(") => ");
3784 write_type_expr(ret, out);
3785 }
3786 TypeExprKind::Array(elem) => {
3787 write_type_expr(elem, out);
3789 out.push_str("[]");
3790 }
3791 TypeExprKind::Tuple(types) => {
3792 out.push('[');
3794 for (i, ty) in types.iter().enumerate() {
3795 if i > 0 {
3796 out.push_str(", ");
3797 }
3798 write_type_expr(ty, out);
3799 }
3800 out.push(']');
3801 }
3802 }
3803}
3804
3805#[cfg(test)]
3806mod tests {
3807 use super::*;
3808 use husk_ast::{
3809 ExprKind as HuskExprKind, Ident as HuskIdent, Literal as HuskLiteral,
3810 LiteralKind as HuskLiteralKind, MatchArm as HuskMatchArm, Pattern as HuskPattern,
3811 PatternKind as HuskPatternKind, Span as HuskSpan, TypeExpr as HuskTypeExpr,
3812 TypeExprKind as HuskTypeExprKind,
3813 };
3814
3815 #[test]
3816 fn prints_simple_function() {
3817 let module = JsModule {
3818 body: vec![JsStmt::Function {
3819 name: "main".to_string(),
3820 params: vec!["a".into(), "b".into()],
3821 body: vec![
3822 JsStmt::Let {
3823 name: "x".to_string(),
3824 init: Some(JsExpr::Binary {
3825 op: JsBinaryOp::Add,
3826 left: Box::new(JsExpr::Ident("a".into())),
3827 right: Box::new(JsExpr::Ident("b".into())),
3828 }),
3829 },
3830 JsStmt::Return(JsExpr::Ident("x".into())),
3831 ],
3832 source_span: None,
3833 }],
3834 };
3835
3836 let src = module.to_source();
3837 assert!(src.contains("function main"));
3838 assert!(src.contains("let x = a + b;"));
3839 assert!(src.contains("return x;"));
3840 }
3841
3842 #[test]
3843 fn appends_module_exports_for_top_level_functions() {
3844 let span = |s: usize, e: usize| HuskSpan { range: s..e };
3846 let ident = |name: &str, s: usize| HuskIdent {
3847 name: name.to_string(),
3848 span: span(s, s + name.len()),
3849 };
3850
3851 let main_fn = husk_ast::Item {
3852 attributes: Vec::new(),
3853 visibility: husk_ast::Visibility::Private,
3854 kind: husk_ast::ItemKind::Fn {
3855 name: ident("main", 0),
3856 type_params: Vec::new(),
3857 params: Vec::new(),
3858 ret_type: None,
3859 body: Vec::new(),
3860 },
3861 span: span(0, 10),
3862 };
3863 let helper_fn = husk_ast::Item {
3864 attributes: Vec::new(),
3865 visibility: husk_ast::Visibility::Private,
3866 kind: husk_ast::ItemKind::Fn {
3867 name: ident("helper", 20),
3868 type_params: Vec::new(),
3869 params: Vec::new(),
3870 ret_type: None,
3871 body: Vec::new(),
3872 },
3873 span: span(20, 30),
3874 };
3875
3876 let file = husk_ast::File {
3877 items: vec![main_fn, helper_fn],
3878 };
3879
3880 let empty_resolution = HashMap::new();
3881 let empty_type_resolution = HashMap::new();
3882 let empty_variant_calls = HashMap::new();
3883 let empty_variant_patterns = HashMap::new();
3884 let module = lower_file_to_js(&file, true, JsTarget::Cjs, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
3885 let src = module.to_source();
3886
3887 assert!(src.contains("function main()"));
3888 assert!(src.contains("function helper()"));
3889 assert!(
3890 src.contains("module.exports = {main: main, helper: helper};")
3891 || src.contains("module.exports = {main: main, helper: helper}")
3892 );
3893 }
3894
3895 #[test]
3896 fn prints_object_and_member_access() {
3897 let obj = JsExpr::Object(vec![
3898 ("tag".to_string(), JsExpr::String("Red".to_string())),
3899 ("value".to_string(), JsExpr::Number(1)),
3900 ]);
3901 let member = JsExpr::Member {
3902 object: Box::new(JsExpr::Ident("color".into())),
3903 property: "tag".to_string(),
3904 };
3905
3906 let mut out = String::new();
3907 write_expr(&obj, &mut out);
3908 assert!(out.contains("{tag: \"Red\", value: 1}"));
3909
3910 out.clear();
3911 write_expr(&member, &mut out);
3912 assert_eq!(out, "color.tag");
3913 }
3914
3915 #[test]
3916 fn lowers_simple_enum_match_to_conditional() {
3917 let span = |s: usize, e: usize| HuskSpan { range: s..e };
3927 let ident = |name: &str, s: usize| HuskIdent {
3928 name: name.to_string(),
3929 span: span(s, s + name.len()),
3930 };
3931
3932 let c_ident = ident("c", 0);
3933 let scrutinee = husk_ast::Expr {
3934 kind: HuskExprKind::Ident(c_ident.clone()),
3935 span: c_ident.span.clone(),
3936 };
3937
3938 let color_ident = ident("Color", 10);
3939 let red_pat = HuskPattern {
3940 kind: HuskPatternKind::EnumUnit {
3941 path: vec![color_ident.clone(), ident("Red", 20)],
3942 },
3943 span: span(10, 23),
3944 };
3945 let blue_pat = HuskPattern {
3946 kind: HuskPatternKind::EnumUnit {
3947 path: vec![color_ident.clone(), ident("Blue", 30)],
3948 },
3949 span: span(24, 38),
3950 };
3951
3952 let arm_red = HuskMatchArm {
3953 pattern: red_pat,
3954 expr: husk_ast::Expr {
3955 kind: HuskExprKind::Literal(HuskLiteral {
3956 kind: HuskLiteralKind::Int(1),
3957 span: span(40, 41),
3958 }),
3959 span: span(40, 41),
3960 },
3961 };
3962 let arm_blue = HuskMatchArm {
3963 pattern: blue_pat,
3964 expr: husk_ast::Expr {
3965 kind: HuskExprKind::Literal(HuskLiteral {
3966 kind: HuskLiteralKind::Int(2),
3967 span: span(50, 51),
3968 }),
3969 span: span(50, 51),
3970 },
3971 };
3972
3973 let match_expr = husk_ast::Expr {
3974 kind: HuskExprKind::Match {
3975 scrutinee: Box::new(scrutinee),
3976 arms: vec![arm_red, arm_blue],
3977 },
3978 span: span(0, 60),
3979 };
3980
3981 let empty_resolution = HashMap::new();
3982 let empty_type_resolution = HashMap::new();
3983 let empty_variant_calls = HashMap::new();
3984 let empty_variant_patterns = HashMap::new();
3985 let js = lower_expr(&match_expr, &CodegenContext::new(&PropertyAccessors::default(), &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns));
3986 let mut out = String::new();
3987 write_expr(&js, &mut out);
3988
3989 assert!(out.contains("c.tag"));
3991 assert!(out.contains("=="));
3992 assert!(out.contains("\"Red\""));
3993 assert!(out.contains(" ? "));
3994 assert!(out.contains(" : "));
3995 }
3996
3997 #[test]
3998 fn adds_main_call_for_zero_arg_main_function() {
3999 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4002 let ident = |name: &str, s: usize| HuskIdent {
4003 name: name.to_string(),
4004 span: span(s, s + name.len()),
4005 };
4006
4007 let main_ident = ident("main", 0);
4008 let fn_item = husk_ast::Item {
4009 attributes: Vec::new(),
4010 visibility: husk_ast::Visibility::Private,
4011 kind: husk_ast::ItemKind::Fn {
4012 name: main_ident.clone(),
4013 type_params: Vec::new(),
4014 params: Vec::new(),
4015 ret_type: None,
4016 body: Vec::new(),
4017 },
4018 span: span(0, 10),
4019 };
4020
4021 let file = husk_ast::File {
4022 items: vec![fn_item],
4023 };
4024
4025 let empty_resolution = HashMap::new();
4026 let empty_type_resolution = HashMap::new();
4027 let empty_variant_calls = HashMap::new();
4028 let empty_variant_patterns = HashMap::new();
4029 let module = lower_file_to_js(&file, true, JsTarget::Cjs, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4030 let src = module.to_source();
4031
4032 assert!(src.contains("function main()"));
4033 assert!(src.contains("main();"));
4034 }
4035
4036 #[test]
4037 fn emits_basic_dts_for_struct_enum_and_function() {
4038 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4043 let ident = |name: &str, s: usize| HuskIdent {
4044 name: name.to_string(),
4045 span: span(s, s + name.len()),
4046 };
4047
4048 let user_ident = ident("User", 0);
4050 let name_field_ident = ident("name", 10);
4051 let id_field_ident = ident("id", 20);
4052 let string_ty_ident = ident("String", 30);
4053 let i32_ty_ident = ident("i32", 40);
4054
4055 let string_ty = HuskTypeExpr {
4056 kind: HuskTypeExprKind::Named(string_ty_ident.clone()),
4057 span: string_ty_ident.span.clone(),
4058 };
4059 let i32_ty = HuskTypeExpr {
4060 kind: HuskTypeExprKind::Named(i32_ty_ident.clone()),
4061 span: i32_ty_ident.span.clone(),
4062 };
4063
4064 let user_struct = husk_ast::Item {
4065 attributes: Vec::new(),
4066 visibility: husk_ast::Visibility::Private,
4067 kind: husk_ast::ItemKind::Struct {
4068 name: user_ident.clone(),
4069 type_params: Vec::new(),
4070 fields: vec![
4071 husk_ast::StructField {
4072 name: name_field_ident.clone(),
4073 ty: string_ty.clone(),
4074 },
4075 husk_ast::StructField {
4076 name: id_field_ident.clone(),
4077 ty: i32_ty.clone(),
4078 },
4079 ],
4080 },
4081 span: span(0, 50),
4082 };
4083
4084 let color_ident = ident("Color", 60);
4086 let enum_item = husk_ast::Item {
4087 attributes: Vec::new(),
4088 visibility: husk_ast::Visibility::Private,
4089 kind: husk_ast::ItemKind::Enum {
4090 name: color_ident.clone(),
4091 type_params: Vec::new(),
4092 variants: vec![
4093 husk_ast::EnumVariant {
4094 name: ident("Red", 70),
4095 fields: husk_ast::EnumVariantFields::Unit,
4096 },
4097 husk_ast::EnumVariant {
4098 name: ident("Blue", 80),
4099 fields: husk_ast::EnumVariantFields::Unit,
4100 },
4101 ],
4102 },
4103 span: span(60, 100),
4104 };
4105
4106 let add_ident = ident("add", 110);
4108 let a_ident = ident("a", 120);
4109 let b_ident = ident("b", 130);
4110
4111 let a_param = husk_ast::Param {
4112 name: a_ident.clone(),
4113 ty: i32_ty.clone(),
4114 };
4115 let b_param = husk_ast::Param {
4116 name: b_ident.clone(),
4117 ty: i32_ty.clone(),
4118 };
4119 let ret_ty = i32_ty.clone();
4120
4121 let add_fn = husk_ast::Item {
4122 attributes: Vec::new(),
4123 visibility: husk_ast::Visibility::Private,
4124 kind: husk_ast::ItemKind::Fn {
4125 name: add_ident.clone(),
4126 type_params: Vec::new(),
4127 params: vec![a_param, b_param],
4128 ret_type: Some(ret_ty),
4129 body: Vec::new(),
4130 },
4131 span: span(110, 150),
4132 };
4133
4134 let file = husk_ast::File {
4135 items: vec![user_struct, enum_item, add_fn],
4136 };
4137
4138 let dts = file_to_dts(&file);
4139
4140 assert!(dts.contains("export interface User"));
4141 assert!(dts.contains("name: string;"));
4142 assert!(dts.contains("id: number;"));
4143
4144 assert!(dts.contains("export type Color"));
4145 assert!(dts.contains("{ tag: \"Red\" }"));
4146 assert!(dts.contains("{ tag: \"Blue\" }"));
4147
4148 assert!(dts.contains("export function add"));
4149 assert!(dts.contains("a: number"));
4150 assert!(dts.contains("b: number"));
4151 assert!(dts.contains("): number;"));
4152 }
4153
4154 #[test]
4155 fn lowers_extern_js_result_function_with_try_catch() {
4156 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4167 let ident = |name: &str, s: usize| HuskIdent {
4168 name: name.to_string(),
4169 span: span(s, s + name.len()),
4170 };
4171
4172 let name = ident("parse", 0);
4173 let param_ident = ident("text", 10);
4174 let string_ty_ident = ident("String", 20);
4175 let result_ident = ident("Result", 30);
4176 let i32_ident = ident("i32", 40);
4177 let err_ident = ident("String", 50);
4178
4179 let string_ty = HuskTypeExpr {
4180 kind: HuskTypeExprKind::Named(string_ty_ident.clone()),
4181 span: string_ty_ident.span.clone(),
4182 };
4183 let i32_ty = HuskTypeExpr {
4184 kind: HuskTypeExprKind::Named(i32_ident.clone()),
4185 span: i32_ident.span.clone(),
4186 };
4187 let err_ty = HuskTypeExpr {
4188 kind: HuskTypeExprKind::Named(err_ident.clone()),
4189 span: err_ident.span.clone(),
4190 };
4191
4192 let result_ty = HuskTypeExpr {
4193 kind: HuskTypeExprKind::Generic {
4194 name: result_ident.clone(),
4195 args: vec![i32_ty, err_ty],
4196 },
4197 span: result_ident.span.clone(),
4198 };
4199
4200 let param = husk_ast::Param {
4201 name: param_ident.clone(),
4202 ty: string_ty,
4203 };
4204
4205 let ext_item = husk_ast::ExternItem {
4206 kind: husk_ast::ExternItemKind::Fn {
4207 name: name.clone(),
4208 params: vec![param],
4209 ret_type: Some(result_ty),
4210 },
4211 span: span(0, 60),
4212 };
4213
4214 let file = husk_ast::File {
4215 items: vec![husk_ast::Item {
4216 attributes: Vec::new(),
4217 visibility: husk_ast::Visibility::Private,
4218 kind: husk_ast::ItemKind::ExternBlock {
4219 abi: "js".to_string(),
4220 items: vec![ext_item],
4221 },
4222 span: span(0, 60),
4223 }],
4224 };
4225
4226 let empty_resolution = HashMap::new();
4227 let empty_type_resolution = HashMap::new();
4228 let empty_variant_calls = HashMap::new();
4229 let empty_variant_patterns = HashMap::new();
4230 let module = lower_file_to_js(&file, true, JsTarget::Cjs, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4231 let src = module.to_source();
4232
4233 assert!(src.contains("function parse"));
4234 assert!(src.contains("try {"));
4235 assert!(src.contains("return Ok("));
4236 assert!(src.contains("globalThis.parse"));
4237 assert!(src.contains("} catch (e) {"));
4238 assert!(src.contains("return Err(e);"));
4239 }
4240
4241 #[test]
4242 fn generates_esm_imports_for_extern_mod_declarations() {
4243 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4250 let ident = |name: &str, s: usize| HuskIdent {
4251 name: name.to_string(),
4252 span: span(s, s + name.len()),
4253 };
4254
4255 let express_mod = husk_ast::ExternItem {
4256 kind: husk_ast::ExternItemKind::Mod {
4257 package: "express".to_string(),
4258 binding: ident("express", 0),
4259 items: Vec::new(),
4260 is_global: false,
4261 },
4262 span: span(0, 10),
4263 };
4264
4265 let fs_mod = husk_ast::ExternItem {
4266 kind: husk_ast::ExternItemKind::Mod {
4267 package: "fs".to_string(),
4268 binding: ident("fs", 20),
4269 items: Vec::new(),
4270 is_global: false,
4271 },
4272 span: span(20, 25),
4273 };
4274
4275 let main_fn = husk_ast::Item {
4276 attributes: Vec::new(),
4277 visibility: husk_ast::Visibility::Private,
4278 kind: husk_ast::ItemKind::Fn {
4279 name: ident("main", 40),
4280 type_params: Vec::new(),
4281 params: Vec::new(),
4282 ret_type: None,
4283 body: Vec::new(),
4284 },
4285 span: span(40, 50),
4286 };
4287
4288 let file = husk_ast::File {
4289 items: vec![
4290 husk_ast::Item {
4291 attributes: Vec::new(),
4292 visibility: husk_ast::Visibility::Private,
4293 kind: husk_ast::ItemKind::ExternBlock {
4294 abi: "js".to_string(),
4295 items: vec![express_mod, fs_mod],
4296 },
4297 span: span(0, 30),
4298 },
4299 main_fn,
4300 ],
4301 };
4302
4303 let empty_resolution = HashMap::new();
4304 let empty_type_resolution = HashMap::new();
4305 let empty_variant_calls = HashMap::new();
4306 let empty_variant_patterns = HashMap::new();
4307 let module = lower_file_to_js(&file, true, JsTarget::Esm, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4308 let src = module.to_source();
4309
4310 assert!(src.contains("import express from \"express\";"));
4312 assert!(src.contains("import fs from \"fs\";"));
4313
4314 assert!(src.contains("export { main }"));
4316 assert!(!src.contains("module.exports"));
4317 }
4318
4319 #[test]
4320 fn esm_exports_even_without_imports_when_target_forced() {
4321 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4322 let ident = |name: &str, s: usize| HuskIdent {
4323 name: name.to_string(),
4324 span: span(s, s + name.len()),
4325 };
4326
4327 let main_fn = husk_ast::Item {
4328 attributes: Vec::new(),
4329 visibility: husk_ast::Visibility::Private,
4330 kind: husk_ast::ItemKind::Fn {
4331 name: ident("main", 0),
4332 type_params: Vec::new(),
4333 params: Vec::new(),
4334 ret_type: None,
4335 body: Vec::new(),
4336 },
4337 span: span(0, 10),
4338 };
4339
4340 let file = husk_ast::File {
4341 items: vec![main_fn],
4342 };
4343 let empty_resolution = HashMap::new();
4344 let empty_type_resolution = HashMap::new();
4345 let empty_variant_calls = HashMap::new();
4346 let empty_variant_patterns = HashMap::new();
4347 let module = lower_file_to_js(&file, true, JsTarget::Esm, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4348 let src = module.to_source();
4349
4350 assert!(src.contains("export { main }"));
4351 assert!(!src.contains("module.exports"));
4352 }
4353
4354 #[test]
4355 fn generates_cjs_requires_for_extern_mod_declarations() {
4356 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4359 let ident = |name: &str, s: usize| HuskIdent {
4360 name: name.to_string(),
4361 span: span(s, s + name.len()),
4362 };
4363
4364 let express_mod = husk_ast::ExternItem {
4365 kind: husk_ast::ExternItemKind::Mod {
4366 package: "express".to_string(),
4367 binding: ident("express", 0),
4368 items: Vec::new(),
4369 is_global: false,
4370 },
4371 span: span(0, 10),
4372 };
4373
4374 let main_fn = husk_ast::Item {
4375 attributes: Vec::new(),
4376 visibility: husk_ast::Visibility::Private,
4377 kind: husk_ast::ItemKind::Fn {
4378 name: ident("main", 20),
4379 type_params: Vec::new(),
4380 params: Vec::new(),
4381 ret_type: None,
4382 body: Vec::new(),
4383 },
4384 span: span(20, 30),
4385 };
4386
4387 let file = husk_ast::File {
4388 items: vec![
4389 husk_ast::Item {
4390 attributes: Vec::new(),
4391 visibility: husk_ast::Visibility::Private,
4392 kind: husk_ast::ItemKind::ExternBlock {
4393 abi: "js".to_string(),
4394 items: vec![express_mod],
4395 },
4396 span: span(0, 15),
4397 },
4398 main_fn,
4399 ],
4400 };
4401
4402 let empty_resolution = HashMap::new();
4403 let empty_type_resolution = HashMap::new();
4404 let empty_variant_calls = HashMap::new();
4405 let empty_variant_patterns = HashMap::new();
4406 let module = lower_file_to_js(&file, true, JsTarget::Cjs, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4407 let src = module.to_source();
4408
4409 assert!(src.contains("const express = require(\"express\");"));
4410 assert!(src.contains("module.exports"));
4411 assert!(!src.contains("export {"));
4412 }
4413
4414 #[test]
4415 fn lowers_format_to_string_concatenation() {
4416 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4418 let ident = |name: &str, s: usize| HuskIdent {
4419 name: name.to_string(),
4420 span: span(s, s + name.len()),
4421 };
4422
4423 let format_expr = husk_ast::Expr {
4425 kind: HuskExprKind::Format {
4426 format: husk_ast::FormatString {
4427 span: span(0, 12),
4428 segments: vec![
4429 husk_ast::FormatSegment::Literal("Hello, ".to_string()),
4430 husk_ast::FormatSegment::Placeholder(husk_ast::FormatPlaceholder {
4431 position: None,
4432 name: None,
4433 spec: husk_ast::FormatSpec::default(),
4434 span: span(7, 9),
4435 }),
4436 husk_ast::FormatSegment::Literal("!".to_string()),
4437 ],
4438 },
4439 args: vec![husk_ast::Expr {
4440 kind: HuskExprKind::Ident(ident("name", 20)),
4441 span: span(20, 24),
4442 }],
4443 },
4444 span: span(0, 25),
4445 };
4446
4447 let accessors = PropertyAccessors::default();
4448 let empty_resolution = HashMap::new();
4449 let empty_type_resolution = HashMap::new();
4450 let empty_variant_calls = HashMap::new();
4451 let empty_variant_patterns = HashMap::new();
4452 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4453 let js_expr = lower_expr(&format_expr, &ctx);
4454 let mut js_str = String::new();
4455 write_expr(&js_expr, &mut js_str);
4456
4457 assert!(
4459 js_str.contains("\"Hello, \""),
4460 "expected 'Hello, ' literal in output: {}",
4461 js_str
4462 );
4463 assert!(
4464 js_str.contains("__husk_fmt"),
4465 "expected __husk_fmt call in output: {}",
4466 js_str
4467 );
4468 assert!(
4469 js_str.contains("\"!\""),
4470 "expected '!' literal in output: {}",
4471 js_str
4472 );
4473 assert!(
4475 !js_str.contains("console.log"),
4476 "format should not generate console.log: {}",
4477 js_str
4478 );
4479 }
4480
4481 #[test]
4482 fn lowers_format_with_hex_specifier() {
4483 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4485 let ident = |name: &str, s: usize| HuskIdent {
4486 name: name.to_string(),
4487 span: span(s, s + name.len()),
4488 };
4489
4490 let format_expr = husk_ast::Expr {
4491 kind: HuskExprKind::Format {
4492 format: husk_ast::FormatString {
4493 span: span(0, 5),
4494 segments: vec![husk_ast::FormatSegment::Placeholder(
4495 husk_ast::FormatPlaceholder {
4496 position: None,
4497 name: None,
4498 spec: husk_ast::FormatSpec {
4499 ty: Some('x'),
4500 ..Default::default()
4501 },
4502 span: span(0, 4),
4503 },
4504 )],
4505 },
4506 args: vec![husk_ast::Expr {
4507 kind: HuskExprKind::Ident(ident("num", 10)),
4508 span: span(10, 13),
4509 }],
4510 },
4511 span: span(0, 15),
4512 };
4513
4514 let accessors = PropertyAccessors::default();
4515 let empty_resolution = HashMap::new();
4516 let empty_type_resolution = HashMap::new();
4517 let empty_variant_calls = HashMap::new();
4518 let empty_variant_patterns = HashMap::new();
4519 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4520 let js_expr = lower_expr(&format_expr, &ctx);
4521 let mut js_str = String::new();
4522 write_expr(&js_expr, &mut js_str);
4523
4524 assert!(
4526 js_str.contains("__husk_fmt_num"),
4527 "expected __husk_fmt_num call in output: {}",
4528 js_str
4529 );
4530 assert!(
4531 js_str.contains("16"),
4532 "expected base 16 for hex format: {}",
4533 js_str
4534 );
4535 }
4536
4537 #[test]
4538 fn lowers_format_simple_string_returns_string_directly() {
4539 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4541
4542 let format_expr = husk_ast::Expr {
4543 kind: HuskExprKind::Format {
4544 format: husk_ast::FormatString {
4545 span: span(0, 7),
4546 segments: vec![husk_ast::FormatSegment::Literal("hello".to_string())],
4547 },
4548 args: vec![],
4549 },
4550 span: span(0, 10),
4551 };
4552
4553 let accessors = PropertyAccessors::default();
4554 let empty_resolution = HashMap::new();
4555 let empty_type_resolution = HashMap::new();
4556 let empty_variant_calls = HashMap::new();
4557 let empty_variant_patterns = HashMap::new();
4558 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4559 let js_expr = lower_expr(&format_expr, &ctx);
4560 let mut js_str = String::new();
4561 write_expr(&js_expr, &mut js_str);
4562
4563 assert_eq!(js_str, "\"hello\"");
4565 }
4566
4567 #[test]
4568 fn lowers_loop_to_while_true() {
4569 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4571 let ident = |name: &str, s: usize| HuskIdent {
4572 name: name.to_string(),
4573 span: span(s, s + name.len()),
4574 };
4575
4576 let loop_stmt = husk_ast::Stmt {
4577 kind: husk_ast::StmtKind::Loop {
4578 body: husk_ast::Block {
4579 stmts: vec![husk_ast::Stmt {
4580 kind: husk_ast::StmtKind::Break,
4581 span: span(10, 16),
4582 }],
4583 span: span(5, 18),
4584 },
4585 },
4586 span: span(0, 20),
4587 };
4588
4589 let main_fn = husk_ast::Item {
4590 attributes: Vec::new(),
4591 visibility: husk_ast::Visibility::Private,
4592 kind: husk_ast::ItemKind::Fn {
4593 name: ident("main", 0),
4594 type_params: Vec::new(),
4595 params: Vec::new(),
4596 ret_type: None,
4597 body: vec![loop_stmt],
4598 },
4599 span: span(0, 25),
4600 };
4601
4602 let file = husk_ast::File {
4603 items: vec![main_fn],
4604 };
4605 let empty_resolution = HashMap::new();
4606 let empty_type_resolution = HashMap::new();
4607 let empty_variant_calls = HashMap::new();
4608 let empty_variant_patterns = HashMap::new();
4609 let module = lower_file_to_js(&file, true, JsTarget::Cjs, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4610 let src = module.to_source();
4611
4612 assert!(
4613 src.contains("while (true)"),
4614 "expected 'while (true)' in output: {}",
4615 src
4616 );
4617 assert!(
4618 src.contains("break;"),
4619 "expected 'break;' in output: {}",
4620 src
4621 );
4622 }
4623
4624 #[test]
4625 fn lowers_while_with_condition() {
4626 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4628 let ident = |name: &str, s: usize| HuskIdent {
4629 name: name.to_string(),
4630 span: span(s, s + name.len()),
4631 };
4632
4633 let while_stmt = husk_ast::Stmt {
4634 kind: husk_ast::StmtKind::While {
4635 cond: husk_ast::Expr {
4636 kind: HuskExprKind::Ident(ident("running", 6)),
4637 span: span(6, 13),
4638 },
4639 body: husk_ast::Block {
4640 stmts: vec![husk_ast::Stmt {
4641 kind: husk_ast::StmtKind::Continue,
4642 span: span(16, 25),
4643 }],
4644 span: span(14, 27),
4645 },
4646 },
4647 span: span(0, 28),
4648 };
4649
4650 let main_fn = husk_ast::Item {
4651 attributes: Vec::new(),
4652 visibility: husk_ast::Visibility::Private,
4653 kind: husk_ast::ItemKind::Fn {
4654 name: ident("main", 0),
4655 type_params: Vec::new(),
4656 params: Vec::new(),
4657 ret_type: None,
4658 body: vec![while_stmt],
4659 },
4660 span: span(0, 30),
4661 };
4662
4663 let file = husk_ast::File {
4664 items: vec![main_fn],
4665 };
4666 let empty_resolution = HashMap::new();
4667 let empty_type_resolution = HashMap::new();
4668 let empty_variant_calls = HashMap::new();
4669 let empty_variant_patterns = HashMap::new();
4670 let module = lower_file_to_js(&file, true, JsTarget::Cjs, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4671 let src = module.to_source();
4672
4673 assert!(
4674 src.contains("while (running)"),
4675 "expected 'while (running)' in output: {}",
4676 src
4677 );
4678 assert!(
4679 src.contains("continue;"),
4680 "expected 'continue;' in output: {}",
4681 src
4682 );
4683 }
4684
4685 #[test]
4686 fn lowers_break_and_continue() {
4687 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4689
4690 let break_stmt = husk_ast::Stmt {
4691 kind: husk_ast::StmtKind::Break,
4692 span: span(0, 6),
4693 };
4694
4695 let continue_stmt = husk_ast::Stmt {
4696 kind: husk_ast::StmtKind::Continue,
4697 span: span(7, 16),
4698 };
4699
4700 let accessors = PropertyAccessors::default();
4701 let empty_resolution = HashMap::new();
4702 let empty_type_resolution = HashMap::new();
4703 let empty_variant_calls = HashMap::new();
4704 let empty_variant_patterns = HashMap::new();
4705 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4706
4707 let js_break = lower_stmt(&break_stmt, &ctx);
4708 let js_continue = lower_stmt(&continue_stmt, &ctx);
4709
4710 assert!(matches!(js_break, JsStmt::Break), "expected JsStmt::Break");
4711 assert!(
4712 matches!(js_continue, JsStmt::Continue),
4713 "expected JsStmt::Continue"
4714 );
4715 }
4716
4717 #[test]
4718 fn lowers_enum_variant_with_single_arg_to_tagged_object() {
4719 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4721 let ident = |name: &str, s: usize| HuskIdent {
4722 name: name.to_string(),
4723 span: span(s, s + name.len()),
4724 };
4725
4726 let callee = husk_ast::Expr {
4728 kind: HuskExprKind::Path {
4729 segments: vec![ident("Option", 0), ident("Some", 8)],
4730 },
4731 span: span(0, 12),
4732 };
4733
4734 let arg = husk_ast::Expr {
4735 kind: HuskExprKind::Literal(HuskLiteral {
4736 kind: HuskLiteralKind::Int(42),
4737 span: span(13, 15),
4738 }),
4739 span: span(13, 15),
4740 };
4741
4742 let call_expr = husk_ast::Expr {
4743 kind: HuskExprKind::Call {
4744 callee: Box::new(callee),
4745 type_args: vec![],
4746 args: vec![arg],
4747 },
4748 span: span(0, 16),
4749 };
4750
4751 let accessors = PropertyAccessors::default();
4752 let empty_resolution = HashMap::new();
4753 let empty_type_resolution = HashMap::new();
4754 let empty_variant_calls = HashMap::new();
4755 let empty_variant_patterns = HashMap::new();
4756 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4757
4758 let js_expr = lower_expr(&call_expr, &ctx);
4759
4760 if let JsExpr::Object(fields) = js_expr {
4762 assert_eq!(fields.len(), 2, "expected 2 fields (tag and value)");
4763 assert_eq!(fields[0].0, "tag");
4764 assert!(matches!(&fields[0].1, JsExpr::String(s) if s == "Some"));
4765 assert_eq!(fields[1].0, "value");
4766 assert!(matches!(&fields[1].1, JsExpr::Number(42)));
4767 } else {
4768 panic!("expected JsExpr::Object, got {:?}", js_expr);
4769 }
4770 }
4771
4772 #[test]
4773 fn lowers_enum_variant_with_multiple_args_to_indexed_object() {
4774 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4776 let ident = |name: &str, s: usize| HuskIdent {
4777 name: name.to_string(),
4778 span: span(s, s + name.len()),
4779 };
4780
4781 let callee = husk_ast::Expr {
4783 kind: HuskExprKind::Path {
4784 segments: vec![ident("MyEnum", 0), ident("Pair", 8)],
4785 },
4786 span: span(0, 12),
4787 };
4788
4789 let arg1 = husk_ast::Expr {
4790 kind: HuskExprKind::Literal(HuskLiteral {
4791 kind: HuskLiteralKind::Int(1),
4792 span: span(13, 14),
4793 }),
4794 span: span(13, 14),
4795 };
4796
4797 let arg2 = husk_ast::Expr {
4798 kind: HuskExprKind::Literal(HuskLiteral {
4799 kind: HuskLiteralKind::Int(2),
4800 span: span(16, 17),
4801 }),
4802 span: span(16, 17),
4803 };
4804
4805 let call_expr = husk_ast::Expr {
4806 kind: HuskExprKind::Call {
4807 callee: Box::new(callee),
4808 type_args: vec![],
4809 args: vec![arg1, arg2],
4810 },
4811 span: span(0, 18),
4812 };
4813
4814 let accessors = PropertyAccessors::default();
4815 let empty_resolution = HashMap::new();
4816 let empty_type_resolution = HashMap::new();
4817 let empty_variant_calls = HashMap::new();
4818 let empty_variant_patterns = HashMap::new();
4819 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4820
4821 let js_expr = lower_expr(&call_expr, &ctx);
4822
4823 if let JsExpr::Object(fields) = js_expr {
4825 assert_eq!(fields.len(), 3, "expected 3 fields (tag, 0, 1)");
4826 assert_eq!(fields[0].0, "tag");
4827 assert!(matches!(&fields[0].1, JsExpr::String(s) if s == "Pair"));
4828 assert_eq!(fields[1].0, "0");
4829 assert!(matches!(&fields[1].1, JsExpr::Number(1)));
4830 assert_eq!(fields[2].0, "1");
4831 assert!(matches!(&fields[2].1, JsExpr::Number(2)));
4832 } else {
4833 panic!("expected JsExpr::Object, got {:?}", js_expr);
4834 }
4835 }
4836
4837 #[test]
4838 fn lowers_unit_enum_variant_to_tagged_object() {
4839 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4841 let ident = |name: &str, s: usize| HuskIdent {
4842 name: name.to_string(),
4843 span: span(s, s + name.len()),
4844 };
4845
4846 let path_expr = husk_ast::Expr {
4848 kind: HuskExprKind::Path {
4849 segments: vec![ident("Option", 0), ident("None", 8)],
4850 },
4851 span: span(0, 12),
4852 };
4853
4854 let accessors = PropertyAccessors::default();
4855 let empty_resolution = HashMap::new();
4856 let empty_type_resolution = HashMap::new();
4857 let empty_variant_calls = HashMap::new();
4858 let empty_variant_patterns = HashMap::new();
4859 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4860
4861 let js_expr = lower_expr(&path_expr, &ctx);
4862
4863 if let JsExpr::Object(fields) = js_expr {
4865 assert_eq!(fields.len(), 1, "expected 1 field (tag only)");
4866 assert_eq!(fields[0].0, "tag");
4867 assert!(matches!(&fields[0].1, JsExpr::String(s) if s == "None"));
4868 } else {
4869 panic!("expected JsExpr::Object, got {:?}", js_expr);
4870 }
4871 }
4872
4873 #[test]
4874 fn lower_match_stmt_with_break() {
4875 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4877 let ident = |name: &str, s: usize| HuskIdent {
4878 name: name.to_string(),
4879 span: span(s, s + name.len()),
4880 };
4881
4882 let scrutinee = husk_ast::Expr {
4884 kind: HuskExprKind::Ident(ident("x", 0)),
4885 span: span(0, 1),
4886 };
4887
4888 let some_arm = husk_ast::MatchArm {
4889 pattern: husk_ast::Pattern {
4890 kind: HuskPatternKind::EnumTuple {
4891 path: vec![ident("Option", 0), ident("Some", 7)],
4892 fields: vec![husk_ast::Pattern {
4893 kind: HuskPatternKind::Binding(ident("v", 12)),
4894 span: span(12, 13),
4895 }],
4896 },
4897 span: span(0, 14),
4898 },
4899 expr: husk_ast::Expr {
4900 kind: HuskExprKind::Block(husk_ast::Block {
4901 stmts: vec![husk_ast::Stmt {
4902 kind: husk_ast::StmtKind::Break,
4903 span: span(18, 23),
4904 }],
4905 span: span(17, 24),
4906 }),
4907 span: span(17, 24),
4908 },
4909 };
4910
4911 let none_arm = husk_ast::MatchArm {
4912 pattern: husk_ast::Pattern {
4913 kind: HuskPatternKind::EnumUnit {
4914 path: vec![ident("Option", 26), ident("None", 33)],
4915 },
4916 span: span(26, 38),
4917 },
4918 expr: husk_ast::Expr {
4919 kind: HuskExprKind::Block(husk_ast::Block {
4920 stmts: vec![],
4921 span: span(42, 44),
4922 }),
4923 span: span(42, 44),
4924 },
4925 };
4926
4927 let accessors = PropertyAccessors::default();
4928 let empty_resolution = HashMap::new();
4929 let empty_type_resolution = HashMap::new();
4930 let empty_variant_calls = HashMap::new();
4931 let empty_variant_patterns = HashMap::new();
4932 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
4933
4934 let js_stmt = lower_match_stmt(&scrutinee, &[some_arm, none_arm], &ctx);
4935
4936 if let JsStmt::If {
4938 cond,
4939 then_block,
4940 else_block,
4941 } = js_stmt
4942 {
4943 if let JsExpr::Binary { op, left, right } = cond {
4945 assert!(matches!(op, JsBinaryOp::EqEq));
4946 if let JsExpr::Member { property, .. } = *left {
4947 assert_eq!(property, "tag");
4948 } else {
4949 panic!("expected Member access for tag");
4950 }
4951 assert!(matches!(*right, JsExpr::String(s) if s == "Some"));
4952 } else {
4953 panic!("expected Binary comparison");
4954 }
4955
4956 assert_eq!(then_block.len(), 2, "expected 2 statements (let + break)");
4958 assert!(matches!(&then_block[0], JsStmt::Let { name, .. } if name == "v"));
4959 assert!(matches!(&then_block[1], JsStmt::Break));
4960
4961 assert!(else_block.is_some());
4963 } else {
4964 panic!("expected JsStmt::If, got {:?}", js_stmt);
4965 }
4966 }
4967
4968 #[test]
4969 fn lower_match_stmt_with_continue() {
4970 let span = |s: usize, e: usize| HuskSpan { range: s..e };
4972 let ident = |name: &str, s: usize| HuskIdent {
4973 name: name.to_string(),
4974 span: span(s, s + name.len()),
4975 };
4976
4977 let scrutinee = husk_ast::Expr {
4979 kind: HuskExprKind::Ident(ident("x", 0)),
4980 span: span(0, 1),
4981 };
4982
4983 let wildcard_arm = husk_ast::MatchArm {
4984 pattern: husk_ast::Pattern {
4985 kind: HuskPatternKind::Wildcard,
4986 span: span(0, 1),
4987 },
4988 expr: husk_ast::Expr {
4989 kind: HuskExprKind::Block(husk_ast::Block {
4990 stmts: vec![husk_ast::Stmt {
4991 kind: husk_ast::StmtKind::Continue,
4992 span: span(5, 13),
4993 }],
4994 span: span(4, 14),
4995 }),
4996 span: span(4, 14),
4997 },
4998 };
4999
5000 let accessors = PropertyAccessors::default();
5001 let empty_resolution = HashMap::new();
5002 let empty_type_resolution = HashMap::new();
5003 let empty_variant_calls = HashMap::new();
5004 let empty_variant_patterns = HashMap::new();
5005 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
5006
5007 let js_stmt = lower_match_stmt(&scrutinee, &[wildcard_arm], &ctx);
5008
5009 assert!(
5011 matches!(&js_stmt, JsStmt::Continue),
5012 "expected Continue, got {:?}",
5013 js_stmt
5014 );
5015 }
5016
5017 #[test]
5018 fn lower_match_stmt_generates_if_else_chain() {
5019 let span = |s: usize, e: usize| HuskSpan { range: s..e };
5021 let ident = |name: &str, s: usize| HuskIdent {
5022 name: name.to_string(),
5023 span: span(s, s + name.len()),
5024 };
5025
5026 let scrutinee = husk_ast::Expr {
5028 kind: HuskExprKind::Ident(ident("x", 0)),
5029 span: span(0, 1),
5030 };
5031
5032 let red_arm = husk_ast::MatchArm {
5033 pattern: husk_ast::Pattern {
5034 kind: HuskPatternKind::EnumUnit {
5035 path: vec![ident("Color", 0), ident("Red", 7)],
5036 },
5037 span: span(0, 10),
5038 },
5039 expr: husk_ast::Expr {
5040 kind: HuskExprKind::Literal(husk_ast::Literal {
5041 kind: husk_ast::LiteralKind::Int(1),
5042 span: span(14, 15),
5043 }),
5044 span: span(14, 15),
5045 },
5046 };
5047
5048 let blue_arm = husk_ast::MatchArm {
5049 pattern: husk_ast::Pattern {
5050 kind: HuskPatternKind::EnumUnit {
5051 path: vec![ident("Color", 17), ident("Blue", 24)],
5052 },
5053 span: span(17, 28),
5054 },
5055 expr: husk_ast::Expr {
5056 kind: HuskExprKind::Literal(husk_ast::Literal {
5057 kind: husk_ast::LiteralKind::Int(2),
5058 span: span(32, 33),
5059 }),
5060 span: span(32, 33),
5061 },
5062 };
5063
5064 let accessors = PropertyAccessors::default();
5065 let empty_resolution = HashMap::new();
5066 let empty_type_resolution = HashMap::new();
5067 let empty_variant_calls = HashMap::new();
5068 let empty_variant_patterns = HashMap::new();
5069 let ctx = CodegenContext::new(&accessors, &empty_resolution, &empty_type_resolution, &empty_variant_calls, &empty_variant_patterns);
5070
5071 let js_stmt = lower_match_stmt(&scrutinee, &[red_arm, blue_arm], &ctx);
5072
5073 if let JsStmt::If {
5075 cond: _,
5076 then_block,
5077 else_block,
5078 } = js_stmt
5079 {
5080 assert_eq!(then_block.len(), 1);
5082 if let JsStmt::Expr(JsExpr::Number(n)) = &then_block[0] {
5083 assert_eq!(*n, 1);
5084 } else {
5085 panic!("expected Expr(Number(1)), got {:?}", then_block[0]);
5086 }
5087
5088 assert!(else_block.is_some());
5090 let else_stmts = else_block.unwrap();
5091 assert_eq!(else_stmts.len(), 1);
5092 if let JsStmt::If { then_block, .. } = &else_stmts[0] {
5093 assert_eq!(then_block.len(), 1);
5094 if let JsStmt::Expr(JsExpr::Number(n)) = &then_block[0] {
5095 assert_eq!(*n, 2);
5096 } else {
5097 panic!("expected Expr(Number(2)), got {:?}", then_block[0]);
5098 }
5099 } else {
5100 panic!("expected nested If for second arm");
5101 }
5102 } else {
5103 panic!("expected JsStmt::If, got {:?}", js_stmt);
5104 }
5105 }
5106
5107 #[test]
5108 fn lowers_imported_unit_variant() {
5109 let span = |s: usize, e: usize| HuskSpan { range: s..e };
5112 let ident = |name: &str, s: usize| HuskIdent {
5113 name: name.to_string(),
5114 span: span(s, s + name.len()),
5115 };
5116
5117 let none_ident = ident("None", 0);
5118 let none_expr = husk_ast::Expr {
5119 kind: HuskExprKind::Ident(none_ident.clone()),
5120 span: none_ident.span.clone(),
5121 };
5122
5123 let accessors = PropertyAccessors::default();
5124 let empty_resolution = HashMap::new();
5125 let empty_type_resolution = HashMap::new();
5126 let mut variant_calls = HashMap::new();
5127 variant_calls.insert((0, 4), ("Option".to_string(), "None".to_string()));
5128 let empty_variant_patterns = HashMap::new();
5129
5130 let ctx = CodegenContext::new(
5131 &accessors,
5132 &empty_resolution,
5133 &empty_type_resolution,
5134 &variant_calls,
5135 &empty_variant_patterns,
5136 );
5137
5138 let js_expr = lower_expr(&none_expr, &ctx);
5139
5140 if let JsExpr::Object(fields) = js_expr {
5141 assert_eq!(fields.len(), 1);
5142 assert_eq!(fields[0].0, "tag");
5143 assert!(matches!(&fields[0].1, JsExpr::String(s) if s == "None"));
5144 } else {
5145 panic!("expected JsExpr::Object, got {:?}", js_expr);
5146 }
5147 }
5148
5149 #[test]
5150 fn lowers_imported_variant_constructor() {
5151 let span = |s: usize, e: usize| HuskSpan { range: s..e };
5154 let ident = |name: &str, s: usize| HuskIdent {
5155 name: name.to_string(),
5156 span: span(s, s + name.len()),
5157 };
5158
5159 let some_ident = ident("Some", 0);
5161 let callee = husk_ast::Expr {
5162 kind: HuskExprKind::Ident(some_ident.clone()),
5163 span: some_ident.span.clone(),
5164 };
5165
5166 let arg = husk_ast::Expr {
5167 kind: HuskExprKind::Literal(HuskLiteral {
5168 kind: HuskLiteralKind::Int(42),
5169 span: span(5, 7),
5170 }),
5171 span: span(5, 7),
5172 };
5173
5174 let call_expr = husk_ast::Expr {
5175 kind: HuskExprKind::Call {
5176 callee: Box::new(callee),
5177 type_args: vec![],
5178 args: vec![arg],
5179 },
5180 span: span(0, 8),
5181 };
5182
5183 let accessors = PropertyAccessors::default();
5184 let empty_resolution = HashMap::new();
5185 let empty_type_resolution = HashMap::new();
5186 let mut variant_calls = HashMap::new();
5187 variant_calls.insert((0, 8), ("Option".to_string(), "Some".to_string()));
5189 let empty_variant_patterns = HashMap::new();
5190
5191 let ctx = CodegenContext::new(
5192 &accessors,
5193 &empty_resolution,
5194 &empty_type_resolution,
5195 &variant_calls,
5196 &empty_variant_patterns,
5197 );
5198
5199 let js_expr = lower_expr(&call_expr, &ctx);
5200
5201 if let JsExpr::Object(fields) = js_expr {
5202 assert_eq!(fields.len(), 2, "expected 2 fields (tag and value)");
5203 assert_eq!(fields[0].0, "tag");
5204 assert!(matches!(&fields[0].1, JsExpr::String(s) if s == "Some"));
5205 assert_eq!(fields[1].0, "value");
5206 assert!(matches!(&fields[1].1, JsExpr::Number(42)));
5207 } else {
5208 panic!("expected JsExpr::Object, got {:?}", js_expr);
5209 }
5210 }
5211
5212 #[test]
5213 fn nested_tuple_destructuring_generates_nested_array_pattern() {
5214 use DestructurePattern::*;
5216
5217 let pattern = vec![
5218 Array(vec![Binding("a".to_string()), Binding("b".to_string())]),
5219 Binding("c".to_string()),
5220 ];
5221
5222 let stmt = JsStmt::LetDestructure {
5223 pattern,
5224 init: Some(JsExpr::Ident("expr".to_string())),
5225 };
5226
5227 let mut out = String::new();
5228 write_stmt(&stmt, 0, &mut out);
5229
5230 assert_eq!(out, "let [[a, b], c] = expr;");
5231 }
5232
5233 #[test]
5234 fn deeply_nested_tuple_destructuring() {
5235 use DestructurePattern::*;
5237
5238 let pattern = vec![
5239 Array(vec![
5240 Array(vec![Binding("a".to_string()), Binding("b".to_string())]),
5241 Binding("c".to_string()),
5242 ]),
5243 Binding("d".to_string()),
5244 ];
5245
5246 let stmt = JsStmt::LetDestructure {
5247 pattern,
5248 init: Some(JsExpr::Ident("expr".to_string())),
5249 };
5250
5251 let mut out = String::new();
5252 write_stmt(&stmt, 0, &mut out);
5253
5254 assert_eq!(out, "let [[[a, b], c], d] = expr;");
5255 }
5256
5257 #[test]
5258 fn tuple_destructuring_with_wildcards() {
5259 use DestructurePattern::*;
5261
5262 let pattern = vec![
5263 Binding("a".to_string()),
5264 Wildcard,
5265 Binding("c".to_string()),
5266 ];
5267
5268 let stmt = JsStmt::LetDestructure {
5269 pattern,
5270 init: Some(JsExpr::Ident("expr".to_string())),
5271 };
5272
5273 let mut out = String::new();
5274 write_stmt(&stmt, 0, &mut out);
5275
5276 assert_eq!(out, "let [a, _, c] = expr;");
5277 }
5278}