Skip to main content

typr_core/processes/transpiling/
mod.rs

1pub mod checked_assertions;
2pub mod translatable;
3
4use crate::components::context::config::Environment;
5use crate::components::context::Context;
6use crate::components::error_message::help_data::HelpData;
7use crate::components::language::argument_value::ArgumentValue;
8use crate::components::language::format_backtick;
9use crate::components::language::function_lang::Function;
10use crate::components::language::operators::Op;
11use crate::components::language::set_related_type_if_variable;
12use crate::components::language::var::Var;
13use crate::components::language::Lang;
14use crate::components::language::ModulePosition;
15use crate::components::r#type::argument_type::ArgumentType;
16use crate::components::r#type::array_type::ArrayType;
17use crate::components::r#type::function_type::FunctionType;
18use crate::components::r#type::type_operator::TypeOperator;
19use crate::components::r#type::type_system::TypeSystem;
20use crate::components::r#type::vector_type::VecType;
21use crate::components::r#type::Type;
22use crate::processes::transpiling::translatable::Translatable;
23use crate::processes::type_checking::facets;
24use crate::processes::type_checking::flatten_operator_union;
25use crate::processes::type_checking::resolve_module_member_type;
26use crate::processes::type_checking::type_comparison::reduce_type;
27use crate::processes::type_checking::typing;
28use translatable::RTranslatable;
29
30#[cfg(not(target_arch = "wasm32"))]
31use std::fs::File;
32#[cfg(not(target_arch = "wasm32"))]
33use std::io::Write;
34#[cfg(not(target_arch = "wasm32"))]
35use std::path::PathBuf;
36
37use std::cell::RefCell;
38use std::collections::HashMap;
39
40/// Render a string value as an R double-quoted literal (R's canonical string
41/// form). The value is assumed to be already decoded (see
42/// `parsing::elements::decode_escapes`), so this is the single place that knows
43/// how to escape for the R target: backslashes and double quotes must be
44/// escaped, control characters are emitted as escape sequences.
45pub fn escape_r_string(s: &str) -> String {
46    let escaped = s
47        .replace('\\', "\\\\")
48        .replace('"', "\\\"")
49        .replace('\n', "\\n")
50        .replace('\t', "\\t")
51        .replace('\r', "\\r");
52    format!("\"{}\"", escaped)
53}
54
55/// Raw `typed_vec(...)` R code for an array literal, without the trailing
56/// `|> as.<TypeName>()` annotation. Used by the `Lang::Array` arm (which then
57/// appends its own annotation) and by `Lang::ValidatingCast` over an array
58/// literal, where the cast supplies the one meaningful annotation and the
59/// literal's own would be redundant (or worse, `as.Generic()` when the
60/// literal's inferred type — e.g. `[0, Empty]` for `[]` — has no registered
61/// alias).
62/// Atomic-representation array literal (step ③, unification_arrays.md):
63/// `c(el1, …)`, or the typed empty vector (`integer(0)`, …) matching `elem`
64/// when there are no elements (`c()` alone would be `NULL`, a different R
65/// value entirely).
66fn atomic_array_literal(array: &Lang, cont: &Context, elem: &Type) -> String {
67    let lin_array = array
68        .linearize_array()
69        .iter()
70        .map(|lang| lang.to_r(cont).0)
71        .collect::<Vec<_>>()
72        .join(", ");
73    if lin_array.is_empty() {
74        let empty = match elem {
75            Type::Integer(_, _) => "integer(0)",
76            Type::Char(_, _) => "character(0)",
77            Type::Boolean(_, _) => "logical(0)",
78            _ => "numeric(0)",
79        };
80        return empty.to_string();
81    }
82    format!("c({})", lin_array)
83}
84
85fn array_literal_raw(array: &Lang, cont: &Context) -> String {
86    let typ = array.typing(cont).value;
87    // Step ③ (unification_arrays.md): a primitive-element array is a bare R
88    // atomic vector. The elements keep their scalar transpilation (`1L |>
89    // as.Integer()`, …) — `c()` strips the per-element boxing classes, which
90    // is exactly the representation we want.
91    if let Some(elem) = cont.atomic_array_elem(&typ) {
92        return atomic_array_literal(array, cont, &elem);
93    }
94    let dimension = ArrayType::try_from(typ)
95        .expect("array literal should have an array type")
96        .get_shape()
97        .map(|sha| format!("c({})", sha))
98        .unwrap_or_else(|| "c(0)".to_string());
99    let lin_array = array
100        .linearize_array()
101        .iter()
102        .map(|lang| lang.to_r(cont).0)
103        .collect::<Vec<_>>()
104        .join(", ");
105    if lin_array.is_empty() {
106        format!("typed_vec(dim = {})", dimension)
107    } else {
108        format!("typed_vec({}, dim = {})", lin_array, dimension)
109    }
110}
111
112// Thread-local storage for generated files (used in WASM mode)
113thread_local! {
114    static GENERATED_FILES: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
115}
116
117// Thread-local stack of roxygen2 `@include` dependencies, scoped per output file.
118//
119// In Project mode, `mod foo;` dependencies must surface as top-level
120// `#' @include foo.R` tags in the *header* of the file that references them — a
121// `#'` comment buried inside a `local({ ... })` block is not attached to any
122// top-level object, so roxygen2 ignores it. Instead of emitting the tag inline,
123// each external module registers its filename into the current frame; the file
124// that owns that frame drains it into its header.
125//
126// The stack mirrors the `to_r` recursion: a new frame is pushed before
127// transpiling the body of a module that writes its own file, and drained when
128// that file is written. The bottom frame collects top-level (`main`) includes.
129thread_local! {
130    static INCLUDE_STACK: RefCell<Vec<Vec<String>>> = RefCell::new(vec![Vec::new()]);
131}
132
133/// Reset the include stack to a single empty bottom frame (call before a build).
134pub fn reset_include_stack() {
135    INCLUDE_STACK.with(|s| *s.borrow_mut() = vec![Vec::new()]);
136}
137
138/// Push a new frame for the body of a module that writes its own file.
139fn push_include_frame() {
140    INCLUDE_STACK.with(|s| s.borrow_mut().push(Vec::new()));
141}
142
143/// Pop the current frame, returning the includes collected within it.
144fn pop_include_frame() -> Vec<String> {
145    INCLUDE_STACK.with(|s| s.borrow_mut().pop().unwrap_or_default())
146}
147
148/// Register an `@include` target (e.g. "foo.R") into the current frame.
149fn register_include(file: &str) {
150    INCLUDE_STACK.with(|s| {
151        if let Some(top) = s.borrow_mut().last_mut() {
152            top.push(file.to_string());
153        }
154    });
155}
156
157/// Drain the bottom (main) frame — the top-level includes for `main.R`.
158pub fn take_main_includes() -> Vec<String> {
159    INCLUDE_STACK.with(|s| {
160        let mut stack = s.borrow_mut();
161        match stack.first_mut() {
162            Some(bottom) => std::mem::take(bottom),
163            None => Vec::new(),
164        }
165    })
166}
167
168thread_local! {
169    static IMPORT_FROM_STACK: RefCell<Vec<Vec<String>>> = RefCell::new(vec![Vec::new()]);
170}
171
172pub fn reset_import_from_stack() {
173    IMPORT_FROM_STACK.with(|s| *s.borrow_mut() = vec![Vec::new()]);
174}
175
176fn push_import_from_frame() {
177    IMPORT_FROM_STACK.with(|s| s.borrow_mut().push(Vec::new()));
178}
179
180fn pop_import_from_frame() -> Vec<String> {
181    IMPORT_FROM_STACK.with(|s| s.borrow_mut().pop().unwrap_or_default())
182}
183
184fn register_import_from(entry: &str) {
185    IMPORT_FROM_STACK.with(|s| {
186        if let Some(top) = s.borrow_mut().last_mut() {
187            top.push(entry.to_string());
188        }
189    });
190}
191
192pub fn take_main_import_froms() -> Vec<String> {
193    IMPORT_FROM_STACK.with(|s| {
194        let mut stack = s.borrow_mut();
195        match stack.first_mut() {
196            Some(bottom) => std::mem::take(bottom),
197            None => Vec::new(),
198        }
199    })
200}
201
202/// Register a generated file (used for WASM mode to capture file outputs)
203pub fn register_generated_file(path: &str, content: &str) {
204    GENERATED_FILES.with(|files| {
205        files.borrow_mut().insert(path.to_string(), content.to_string());
206    });
207}
208
209/// Get all generated files
210pub fn get_generated_files() -> HashMap<String, String> {
211    GENERATED_FILES.with(|files| files.borrow().clone())
212}
213
214/// Clear all generated files
215pub fn clear_generated_files() {
216    GENERATED_FILES.with(|files| {
217        files.borrow_mut().clear();
218    });
219}
220
221/// Write a file - in native mode writes to filesystem, in WASM mode stores in memory.
222/// Skips the write when the file already holds the same content, so unchanged
223/// outputs keep a stable mtime across builds.
224#[cfg(not(target_arch = "wasm32"))]
225fn write_output_file(path: &str, content: &str) -> Result<(), String> {
226    use std::fs;
227
228    // Also register in memory for consistency
229    register_generated_file(path, content);
230
231    let path_buf = PathBuf::from(path);
232    if let Ok(existing) = fs::read_to_string(&path_buf) {
233        if existing == content {
234            return Ok(());
235        }
236    }
237    if let Some(parent) = path_buf.parent() {
238        fs::create_dir_all(parent).map_err(|e| e.to_string())?;
239    }
240    let mut file = File::create(&path_buf).map_err(|e| e.to_string())?;
241    file.write_all(content.as_bytes()).map_err(|e| e.to_string())?;
242    Ok(())
243}
244
245#[cfg(target_arch = "wasm32")]
246fn write_output_file(path: &str, content: &str) -> Result<(), String> {
247    register_generated_file(path, content);
248    Ok(())
249}
250
251pub trait ToSome {
252    fn to_some(self) -> Option<Self>
253    where
254        Self: Sized;
255}
256
257impl<T: Sized> ToSome for T {
258    fn to_some(self) -> Option<Self> {
259        Some(self)
260    }
261}
262
263const JS_HEADER: &str = "";
264
265fn to_pattern_match_statement(exp: Lang, branches: &[(Lang, Box<Lang>)], context: &Context) -> String {
266    let match_var = "match_val__";
267    let res = branches
268        .iter()
269        .enumerate()
270        .map(|(id, (pattern, body))| {
271            let (cond, bindings) = pattern_to_condition(pattern, match_var, context);
272            let body_str = body.to_r(context).0;
273            let body_with_bindings = if bindings.is_empty() {
274                body_str
275            } else {
276                format!("{}\n{}", bindings, body_str)
277            };
278            if cond == "TRUE" {
279                // wildcard pattern: always matches
280                if id == 0 {
281                    format!("{{\n{}\n}}", body_with_bindings)
282                } else {
283                    format!("else {{\n{}\n}}", body_with_bindings)
284                }
285            } else if id == 0 {
286                format!("if ({}) {{\n{}\n}}", cond, body_with_bindings)
287            } else {
288                format!("else if ({}) {{\n{}\n}}", cond, body_with_bindings)
289            }
290        })
291        .collect::<Vec<_>>()
292        .join(" ");
293    format!("{{\n{} <- {}\n{}\n}}", match_var, exp.to_r(context).0, res)
294}
295
296/// Map a Type to its corresponding R type-check function name.
297/// Return the TypR lifting function name for the return type of an `@extern` function.
298/// `None` means the raw R value is passed through unchanged (opaque / Any / record types).
299fn extern_lift_fn(typ: &Type) -> Option<&'static str> {
300    match typ {
301        Type::Integer(_, _) => Some("from_int"),
302        Type::Number(_, _) => Some("from_num"),
303        Type::Char(_, _) => Some("from_char"),
304        Type::Boolean(_, _) => Some("from_bool"),
305        // Option<T> return: NULL from external fn becomes .None, any value becomes .Some(value)
306        Type::Alias(name, _, _, _) if name == "Option" => Some("from_nullable"),
307        _ => None,
308    }
309}
310
311fn type_to_r_check(typ: &Type) -> Option<&'static str> {
312    match typ {
313        Type::Integer(_, _) => Some("is.integer"),
314        Type::Boolean(_, _) => Some("is.logical"),
315        Type::Number(_, _) => Some("is.numeric"),
316        Type::Char(_, _) => Some("is.character"),
317        Type::Null(_) => Some("is.null"),
318        _ => None,
319    }
320}
321
322/// R class a value of `typ` is expected to carry at runtime, used by record
323/// validators to check field types via `inherits`. Relies on monomorphisation:
324/// every value already carries its type's class. Returns `None` for types with
325/// no reliable nominal class (generics, functions, unions…), in which case the
326/// field is only checked for presence.
327fn record_field_class(typ: &Type, cont: &Context) -> Option<String> {
328    match typ {
329        Type::Integer(_, _) => Some("integer".to_string()),
330        Type::Number(_, _) => Some("numeric".to_string()),
331        Type::Char(_, _) => Some("character".to_string()),
332        Type::Boolean(_, _) => Some("logical".to_string()),
333        Type::Alias(name, _, _, _) => match cont.aliases().find(|(var, _)| var.get_name() == *name).map(|(_, t)| t) {
334            // Record aliases carry their alias name as the S3 class.
335            Some(Type::Record(_, _)) => Some(name.clone()),
336            // Primitive aliases (e.g. `type Meters <- int`) carry the underlying
337            // R class — no constructor adds the alias name as a class.
338            Some(inner) => record_field_class(inner, cont),
339            None => None,
340        },
341        _ => None,
342    }
343}
344
345/// Find the name of a union alias that declares a tag variant called
346/// `tag_name`. Used by the `Lang::Tag` literal to enrich its runtime class
347/// with the union name (canonical representation, see
348/// `validation_variant_d_union.md` §2). Returns `None` for standalone tags
349/// (no declared union).
350fn find_union_for_tag(tag_name: &str, cont: &Context) -> Option<String> {
351    cont.aliases().find_map(|(var, typ)| {
352        let is_union = matches!(
353            typ,
354            Type::Operator(crate::components::r#type::type_operator::TypeOperator::Union, _, _, _)
355        );
356        if !is_union {
357            return None;
358        }
359        let declares_tag = flatten_operator_union(typ)
360            .iter()
361            .any(|m| matches!(m, Type::Tag(n, _, _) if n == tag_name));
362        if declares_tag {
363            Some(var.get_name())
364        } else {
365            None
366        }
367    })
368}
369
370/// Build the structural body-validation block for a tag's payload, shared by
371/// standalone tag aliases (`type Hello <- .Hello(char)`) and union variants.
372/// `name` is the type/variant name used in error messages; `inner_type` is the
373/// declared payload type. An empty payload (`.Nothing`) yields an empty block.
374fn tag_body_validation(name: &str, inner_type: &Type) -> String {
375    match inner_type {
376        Type::Empty(_) => String::new(),
377        Type::Integer(tint, _) => {
378            use crate::components::r#type::tint::Tint;
379            let null_check = format!("\n  if (is.null(x[[\"body\"]])) stop(\"Validation failed for type {name}: missing 'body' field\")\n  if (!is.integer(x[[\"body\"]])) stop(\"Validation failed for type {name}: body must be int\")");
380            match tint {
381                Tint::Val(i) => format!("{null_check}\n  if (x[[\"body\"]] != {i}L) stop(\"Validation failed for type {name}: body must be literal {i}\")"),
382                Tint::Unknown => null_check,
383            }
384        }
385        Type::Char(tchar, _) => {
386            use crate::components::r#type::tchar::Tchar;
387            let null_check = format!("\n  if (is.null(x[[\"body\"]])) stop(\"Validation failed for type {name}: missing 'body' field\")\n  if (!is.character(x[[\"body\"]])) stop(\"Validation failed for type {name}: body must be char\")");
388            match tchar {
389                Tchar::Val(s) => format!("{null_check}\n  if (x[[\"body\"]] != '{s}') stop(\"Validation failed for type {name}: body must be literal '{s}'\")"),
390                Tchar::Unknown => null_check,
391            }
392        }
393        Type::Boolean(tbool, _) => {
394            use crate::components::r#type::tbool::Tbool;
395            let null_check = format!("\n  if (is.null(x[[\"body\"]])) stop(\"Validation failed for type {name}: missing 'body' field\")\n  if (!is.logical(x[[\"body\"]])) stop(\"Validation failed for type {name}: body must be bool\")");
396            match tbool {
397                Tbool::Val(b) => {
398                    let r_val = if *b { "TRUE" } else { "FALSE" };
399                    format!("{null_check}\n  if (x[[\"body\"]] != {r_val}) stop(\"Validation failed for type {name}: body must be literal {r_val}\")")
400                }
401                Tbool::Unknown => null_check,
402            }
403        }
404        Type::Number(tnum, _) => {
405            use crate::components::r#type::tnumber::Tnum;
406            let null_check = format!("\n  if (is.null(x[[\"body\"]])) stop(\"Validation failed for type {name}: missing 'body' field\")\n  if (!is.numeric(x[[\"body\"]])) stop(\"Validation failed for type {name}: body must be num\")");
407            match tnum {
408                Tnum::Val(v) => format!("{null_check}\n  if (x[[\"body\"]] != {v}) stop(\"Validation failed for type {name}: body must be literal {v}\")"),
409                Tnum::Unknown => null_check,
410            }
411        }
412        Type::Alias(alias_name, _, _, _) => format!(
413            "\n  if (is.null(x[[\"body\"]])) stop(\"Validation failed for type {name}: missing 'body' field\")\n  validate_{alias_name}(x[[\"body\"]])"
414        ),
415        _ => format!(
416            "\n  if (is.null(x[[\"body\"]])) stop(\"Validation failed for type {name}: missing 'body' field\")"
417        ),
418    }
419}
420
421/// Emit the full constructor/annotator/validator pipeline for a single tag
422/// variant `V` of union `U`, in the canonical representation
423/// (`structure(list("V", body = p), class = c("V", "U", "Tag", "list"))`).
424/// Mirrors the record pipeline (see `Lang::Alias` / `Type::Record`).
425fn tag_variant_pipeline(variant_name: &str, union_name: &str, inner_type: &Type) -> String {
426    let is_empty = matches!(inner_type, Type::Empty(_));
427    // Constructor: build the raw value, then delegate entirely to the
428    // annotator. It neither sets the class nor validates.
429    let constructor = if is_empty {
430        format!("{variant_name} <- function() {{\n  x <- list(\"{variant_name}\")\n  as.{variant_name}(x)\n}}")
431    } else {
432        format!(
433            "{variant_name} <- function(x) {{\n  v <- list(\"{variant_name}\", body = x)\n  as.{variant_name}(v)\n}}"
434        )
435    };
436    // Annotator: single entry point. Sets the class idempotently, then runs
437    // the internal validator and the user validator (`validate` S3 generic,
438    // which dispatches to `validate.{variant_name}` then `validate.{union_name}`).
439    let annotator = format!(
440        "as.{variant_name} <- function(x) {{\n  if (!inherits(x, \"{variant_name}\")) class(x) <- c(\"{variant_name}\", \"{union_name}\", \"Tag\", \"list\")\n  x <- validate_{variant_name}(x)\n  x <- validate(x)\n  x\n}}"
441    );
442    // Internal validator: pure structural invariants (tag identity + payload).
443    let body_validation = tag_body_validation(variant_name, inner_type);
444    let validator = format!(
445        "validate_{variant_name} <- function(x) {{\n  if (x[[1]] != '{variant_name}') stop(\"Validation failed for type {variant_name}: expected tag '{variant_name}'\")\n{body_validation}\n  x\n}}"
446    );
447    format!("{constructor}\n{annotator}\n{validator}")
448}
449
450fn pattern_to_condition(pattern: &Lang, match_var: &str, _context: &Context) -> (String, String) {
451    match pattern {
452        // Tag with a binding variable: .Some(a)
453        Lang::Tag { name, value: inner, .. } => {
454            let cond = format!("{}[[1]] == '{}'", match_var, name);
455            match inner.as_ref() {
456                Lang::Variable { name: var_name, .. } => {
457                    let binding = format!("{} <- {}[[\"body\"]]", var_name, match_var);
458                    (cond, binding)
459                }
460                Lang::Empty(_) => (cond, String::new()),
461                _ => (cond, String::new()),
462            }
463        }
464        // Type pattern: x as int
465        Lang::TypePattern {
466            variable_name: var_name,
467            matched_type: typ,
468            ..
469        } => {
470            let check_fn = type_to_r_check(typ).unwrap_or("is.logical");
471            let cond = format!("{}({})", check_fn, match_var);
472            let binding = format!("{} <- {}", var_name, match_var);
473            (cond, binding)
474        }
475        // Tuple pattern: :{a, b, c}
476        Lang::Tuple { value: elements, .. } => {
477            let cond = format!(
478                "inherits({}, 'Tuple') && length({}) == {}",
479                match_var,
480                match_var,
481                elements.len()
482            );
483            let bindings: Vec<String> = elements
484                .iter()
485                .enumerate()
486                .filter_map(|(i, elem)| {
487                    if let Lang::Variable { name: var_name, .. } = elem {
488                        if var_name == "_" {
489                            None
490                        } else {
491                            Some(format!("{} <- {}[[{}]]", var_name, match_var, i + 1))
492                        }
493                    } else {
494                        None
495                    }
496                })
497                .collect();
498            (cond, bindings.join("\n"))
499        }
500        // List/record pattern: :{nom: n, age: a}
501        Lang::List { value: fields, .. } => {
502            let conditions: Vec<String> = fields
503                .iter()
504                .map(|arg_val: &ArgumentValue| format!("!is.null({}[[\"{}\"]])", match_var, arg_val.get_argument()))
505                .collect();
506            let cond = if conditions.is_empty() {
507                "is.list(".to_string() + match_var + ")"
508            } else {
509                format!("is.list({}) && {}", match_var, conditions.join(" && "))
510            };
511            let bindings: Vec<String> = fields
512                .iter()
513                .filter_map(|arg_val| {
514                    if let Lang::Variable { name: var_name, .. } = &arg_val.get_value() {
515                        Some(format!(
516                            "{} <- {}[[\"{}\"]]",
517                            var_name,
518                            match_var,
519                            arg_val.get_argument()
520                        ))
521                    } else {
522                        None
523                    }
524                })
525                .collect();
526            (cond, bindings.join("\n"))
527        }
528        // DataFrame pattern: data__frame(col1 = x, col2 = y)
529        Lang::DataFrame { value: fields, .. } => {
530            let conditions: Vec<String> = fields
531                .iter()
532                .map(|arg_val: &ArgumentValue| format!("!is.null({}[[\"{}\"]])", match_var, arg_val.get_argument()))
533                .collect();
534            let cond = if conditions.is_empty() {
535                "is.data.frame(".to_string() + match_var + ")"
536            } else {
537                format!("is.data.frame({}) && {}", match_var, conditions.join(" && "))
538            };
539            let bindings: Vec<String> = fields
540                .iter()
541                .filter_map(|arg_val| {
542                    if let Lang::Variable { name: var_name, .. } = &arg_val.get_value() {
543                        Some(format!(
544                            "{} <- {}[[\"{}\"]]",
545                            var_name,
546                            match_var,
547                            arg_val.get_argument()
548                        ))
549                    } else {
550                        None
551                    }
552                })
553                .collect();
554            (cond, bindings.join("\n"))
555        }
556        // Wildcard: _
557        Lang::Variable { name, .. } if name == "_" => ("TRUE".to_string(), String::new()),
558        // Other variable: bind the whole value
559        Lang::Variable { name, .. } => {
560            let binding = format!("{} <- {}", name, match_var);
561            ("TRUE".to_string(), binding)
562        }
563        _ => ("TRUE".to_string(), String::new()),
564    }
565}
566
567impl RTranslatable<(String, Context)> for Lang {
568    fn to_r(&self, cont: &Context) -> (String, Context) {
569        let result = match self {
570            Lang::Bool { value: b, .. } => {
571                let (typ, _, _) = typing(cont, self).to_tuple();
572                let anotation = cont.get_type_anotation(&typ);
573                (
574                    format!("{} |> {}", b.to_string().to_uppercase(), anotation),
575                    cont.clone(),
576                )
577            }
578            Lang::Number { value: n, .. } => {
579                let (typ, _, _) = typing(cont, self).to_tuple();
580                let anotation = cont.get_type_anotation(&typ);
581                (format!("{} |> {}", n, anotation), cont.clone())
582            }
583            Lang::Integer { value: i, .. } => {
584                let (typ, _, _) = typing(cont, self).to_tuple();
585                let anotation = cont.get_type_anotation(&typ);
586                (format!("{}L |> {}", i, anotation), cont.clone())
587            }
588            Lang::Char { value: s, .. } => {
589                let (typ, _, _) = typing(cont, self).to_tuple();
590                let anotation = cont.get_type_anotation(&typ);
591                (format!("{} |> {}", escape_r_string(s), anotation), cont.clone())
592            }
593            Lang::Operator {
594                operator: op @ (Op::Dot(_) | Op::Pipe(_)),
595                rhs: e1,
596                lhs: e2,
597                ..
598            } => {
599                // `rhs` is the syntactic-left operand, `lhs` is the
600                // syntactic-right operand (same convention as `Op::Dollar`
601                // and the generic operator case below: `e1.to_r() <op>
602                // e2.to_r()`). For plain field access (`p.x`, `e2` a bare
603                // `Lang::Variable` field name and `e1` not an `Integer`),
604                // that means the receiver `e1` must render first:
605                // `e1[['e2']]`, not `e2[['e1']]`. The `Lang::Integer`
606                // sub-case is the synthetic tuple-destructuring node built
607                // in `parsing/mod.rs` (`rhs: Integer(index), lhs: tmp_var`),
608                // which already has the receiver/index roles swapped
609                // relative to that convention on purpose — left as-is, and
610                // `Op::Pipe` keeps its prior (separate, untouched) ordering
611                // since a bare-variable pipe target (`a |> f`) means
612                // something else entirely (`f(a)`, not field indexing).
613                let is_dot = matches!(op, Op::Dot(_));
614                let e1 = (**e1).clone();
615                let e2 = (**e2).clone();
616                match e2.clone() {
617                    Lang::Variable { .. } => match e1 {
618                        Lang::Integer { .. } => Translatable::from(cont.clone())
619                            .to_r(&e2)
620                            .add("[[")
621                            .to_r(&e1)
622                            .add("]]")
623                            .into(),
624                        _ if is_dot => Translatable::from(cont.clone())
625                            .to_r(&e1)
626                            .add("[['")
627                            .to_r(&e2)
628                            .add("']]")
629                            .into(),
630                        _ => Translatable::from(cont.clone())
631                            .to_r(&e2)
632                            .add("[['")
633                            .to_r(&e1)
634                            .add("']]")
635                            .into(),
636                    },
637                    Lang::List { value: fields, .. } => {
638                        let at = fields[0].clone();
639                        Translatable::from(cont.clone())
640                            .add("within(")
641                            .to_r(&e2)
642                            .add(", { ")
643                            .add(&at.get_argument())
644                            .add(" <- ")
645                            .to_r(&at.get_value())
646                            .add(" })")
647                            .into()
648                    }
649                    Lang::DataFrame { value: fields, .. } => {
650                        let at = fields[0].clone();
651                        Translatable::from(cont.clone())
652                            .add("within(")
653                            .to_r(&e2)
654                            .add(", { ")
655                            .add(&at.get_argument())
656                            .add(" <- ")
657                            .to_r(&at.get_value())
658                            .add(" })")
659                            .into()
660                    }
661                    Lang::FunctionApp {
662                        identifier: var,
663                        arguments: v,
664                        help_data: h,
665                    } => {
666                        let v = [e1].iter().chain(v.iter()).cloned().collect();
667                        Lang::FunctionApp {
668                            identifier: var,
669                            arguments: v,
670                            help_data: h,
671                        }
672                        .to_r(cont)
673                    }
674                    _ => Translatable::from(cont.clone())
675                        .to_r(&e2)
676                        .add("[[")
677                        .add("]]")
678                        .to_r(&e1)
679                        .into(),
680                }
681            }
682            Lang::Operator {
683                operator: Op::Dollar(_),
684                rhs: e1,
685                lhs: e2,
686                ..
687            } => {
688                let e1 = (**e1).clone();
689                let e2 = (**e2).clone();
690                let t1 = typing(cont, &e1).value;
691                let val = match (t1.clone(), e2.clone()) {
692                    (Type::Vec(vtype, _, _, _), Lang::Variable { name, .. }) if vtype.is_array() => {
693                        format!("vec_apply(get, {}, typed_vec('{}'))", e1.to_r(cont).0, name)
694                    }
695                    (Type::Vec(VecType::S3, _, _, _), Lang::Variable { name, .. }) => {
696                        let name_str = name.replace("__", ".");
697                        format!("get({}, '{}')", e1.to_r(cont).0, name_str)
698                    }
699                    (_, Lang::Variable { name, .. }) => format!("{}${}", e1.to_r(cont).0, name),
700                    _ => format!("{}${}", e1.to_r(cont).0, e2.to_r(cont).0),
701                };
702                (val, cont.clone())
703            }
704            Lang::Operator {
705                operator: op,
706                rhs: e1,
707                lhs: e2,
708                ..
709            } => {
710                let op_str = format!(" {} ", op);
711                Translatable::from(cont.clone()).to_r(e1).add(&op_str).to_r(e2).into()
712            }
713            Lang::Scope { body: exps, .. } => Translatable::from(cont.clone())
714                .add("{\n")
715                .join(exps, "\n")
716                .add("\n}")
717                .into(),
718            Lang::Function {
719                parameters: params,
720                body,
721                help_data,
722                ..
723            } => {
724                let fn_type = FunctionType::try_from(typing(cont, self).value.clone())
725                    .expect("function expression should have a function type");
726                let return_type = fn_type.get_return_type();
727
728                // Record alias constructors take specific named fields — calling
729                // TypeName(single_value) would fail.  The body already constructs
730                // the correct type (via ConstructorCall or List), so skip the
731                // output conversion for record aliases.
732                let is_record_alias_return = match &return_type {
733                    // `cont.aliases()` only has aliases explicitly `use`d (or
734                    // declared) in this module's own scope. A return type can
735                    // name a record alias that was never imported by name here
736                    // (only its constructor/functions were), resolved instead
737                    // through the whole-program `record_aliases` registry — the
738                    // same fallback `get_matching_alias_signature` relies on.
739                    Type::Alias(alias_name, _, _, _) => cont
740                        .aliases()
741                        .find(|(var, _)| var.get_name() == *alias_name)
742                        .map(|(_, t)| t.clone())
743                        .or_else(|| {
744                            cont.record_aliases
745                                .iter()
746                                .find(|(name, _)| name == alias_name)
747                                .map(|(_, t)| t.clone())
748                        })
749                        .map(|t| matches!(t, Type::Record(_, _)))
750                        .unwrap_or(false),
751                    _ => false,
752                };
753                let output_conversion = if is_record_alias_return {
754                    "".to_string()
755                } else {
756                    cont.get_type_anotation(&return_type)
757                };
758
759                let has_variadic = params.last().map(|p| p.is_variadic()).unwrap_or(false);
760                let list_of_types = params.iter().map(ArgumentType::body_type).collect::<Vec<_>>();
761                let sub_context = params
762                    .iter()
763                    .map(|arg_typ| arg_typ.clone().set_type(arg_typ.body_type()).to_var(cont))
764                    .zip(list_of_types.clone())
765                    .fold(cont.clone(), |context: Context, (var, typ)| {
766                        context.clone().push_var_type(var, typ, &context)
767                    });
768                let res = if output_conversion.is_empty() {
769                    "".to_string()
770                } else {
771                    " |> ".to_owned() + &output_conversion
772                };
773                let body_r = body.to_r(&sub_context).0;
774                let with_variadic_collector = if has_variadic {
775                    let vname = params.last().unwrap().get_argument_str();
776                    // The body sees the variadic param as `[#N, T]`, so collect
777                    // the R `...` into a `typed_vec` to match the S3 dispatch the
778                    // stdlib array functions (`sum`, `map`, `length`, …) rely on.
779                    let collector = "typed_vec(..., dim = c(...length()))";
780                    // inject `vname <- typed_vec(...)` after opening `{`
781                    if let Some(rest) = body_r.strip_prefix('{') {
782                        format!("{{\n{} <- {}{}", vname, collector, rest)
783                    } else {
784                        body_r
785                    }
786                } else {
787                    body_r
788                };
789                // --checked (soundness_transpilation.md Phase A): assert each
790                // typed param on entry, and the returned value against the
791                // declared return type. No-op when checked mode is off.
792                let checked_prologue: String = params
793                    .iter()
794                    .filter(|p| !p.is_variadic())
795                    .filter_map(|p| {
796                        checked_assertions::param_assertion(cont, &p.get_argument_str(), &p.body_type(), help_data)
797                    })
798                    .collect();
799                let final_body_r = if checked_prologue.is_empty() {
800                    with_variadic_collector
801                } else if let Some(rest) = with_variadic_collector.strip_prefix('{') {
802                    format!("{{\n{}{}", checked_prologue, rest)
803                } else {
804                    with_variadic_collector
805                };
806                let final_body_r =
807                    checked_assertions::wrap_checked(cont, final_body_r, &return_type, help_data, "return");
808                (
809                    format!(
810                        "(function({}) {}{}) |> {}",
811                        params.iter().map(|x| x.to_r(cont)).collect::<Vec<_>>().join(", "),
812                        final_body_r,
813                        res,
814                        cont.get_type_anotation(&fn_type.into())
815                    ),
816                    cont.clone(),
817                )
818            }
819            Lang::Variable { .. } => {
820                //Here we only keep the variable name, the path and the type
821                let var = Var::from_language(self.clone()).unwrap();
822                let name = if var.contains("__") {
823                    var.replace("__", ".").get_name()
824                } else {
825                    var.display_type(cont).get_name()
826                };
827                (name.to_string(), cont.clone())
828            }
829            Lang::FunctionApp {
830                identifier: exp,
831                arguments: vals,
832                ..
833            } => {
834                // The vector lift decided at type-check time (this node
835                // becoming a `Lang::VecFunctionApp`) never survives inside a
836                // function body: `function()` returns the original expression
837                // and `Lines`/`Scope` typing only rebuilds its last statement,
838                // so a lifted call reaches transpilation as a plain
839                // FunctionApp again. Re-derive it on the fly — same pattern
840                // as the Module arm's PartialApp desugaring — and transpile
841                // the lifted node instead (no recursion back here: we only
842                // delegate when the variant changed).
843                let retyped = typing(cont, self).lang;
844                if matches!(retyped, Lang::VecFunctionApp { .. }) {
845                    return retyped.to_r(cont);
846                }
847                let var = Var::try_from(exp.clone()).unwrap();
848
849                let (exp_str, cont1) = exp.to_r(cont);
850                // Callee may be resolved purely structurally (e.g. an interface
851                // method called as a plain function on a rigid-generic/interface
852                // receiver, §5 elimination — `try_constrained_variable_match` in
853                // function_application.rs), with no directly registered
854                // `(name, FunctionType)` entry to look up here. The R call is
855                // still just `name(args...)`: S3 dispatch on the first argument
856                // resolves the concrete implementation at runtime, wherever it's
857                // defined. In that case skip the param-type reduction/annotation
858                // below (it only matters for annotating a variable *argument*
859                // that is itself an overloaded top-level function reference) and
860                // pass the arguments through unchanged.
861                let fn_t_opt = cont1
862                    .get_type_from_variable(&var)
863                    .ok()
864                    .and_then(|t| FunctionType::try_from(t).ok())
865                    .map(|ft| ft.adjust_nb_parameters(vals.len()));
866                let new_vals = match &fn_t_opt {
867                    Some(fn_t) => {
868                        let new_args = fn_t
869                            .get_param_types()
870                            .iter()
871                            .map(|arg| reduce_type(&cont1, arg))
872                            .collect::<Vec<_>>();
873                        vals.iter()
874                            .zip(new_args.iter())
875                            .map(set_related_type_if_variable)
876                            .collect::<Vec<_>>()
877                    }
878                    None => vals.clone(),
879                };
880                let cont1_fallback = cont1.clone();
881                Var::from_language(*exp.clone())
882                    .map(|var| {
883                        let name = var.get_name();
884                        let new_name = if &name[0..1] == "%" {
885                            format!("`{}`", name.replace("__", "."))
886                        } else {
887                            name.replace("__", ".")
888                        };
889                        if cont1.is_extern_fn(&name) {
890                            let r_name = cont1.get_extern_r_name(&name).unwrap_or_else(|| new_name.clone());
891                            let return_type = fn_t_opt
892                                .as_ref()
893                                .map(|ft| ft.get_return_type())
894                                .expect("extern function application identifier should have a function type");
895                            let lift_fn = extern_lift_fn(&return_type);
896                            let (args_vec, current_cont): (Vec<String>, Context) =
897                                new_vals.iter().fold((Vec::new(), cont1.clone()), |(mut v, c), val| {
898                                    let (s, c2) = val.to_r(&c);
899                                    v.push(format!("to_native({})", s));
900                                    (v, c2)
901                                });
902                            let args = args_vec.join(", ");
903                            let call = format!("{}({})", r_name, args);
904                            let result = match lift_fn {
905                                Some(f) => format!("{}({})", f, call),
906                                None => call,
907                            };
908                            (result, current_cont)
909                        } else if cont1.is_import_from_fn(&name) {
910                            let r_name = cont1.get_import_from_r_name(&name).unwrap_or_else(|| new_name.clone());
911                            let (args, current_cont) = Translatable::from(cont1).join(&new_vals, ", ").into();
912                            (format!("{}({})", r_name, args), current_cont)
913                        } else {
914                            // `name<Type>(...)` (turbofish-style forced dispatch):
915                            // the callee's `related_type` is only ever non-Empty
916                            // here when the user wrote an explicit `<Type>`
917                            // annotation on this identifier (never set by
918                            // unification or argument-position inference, which
919                            // only ever touch `vals`/`new_vals`, not `exp`
920                            // itself) — so it's always safe to bind straight to
921                            // the concrete S3 method, skipping `UseMethod` at
922                            // runtime. `Any` maps to `.default`, mirroring the
923                            // definition-site precedent (`Lang::Let` arm above)
924                            // rather than relying on `get_class`, which
925                            // deliberately leaves `Any` unsuffixed.
926                            let forced_name = match var.get_type() {
927                                Type::Empty(_) => new_name.clone(),
928                                Type::Any(_) => format!("{}.default", new_name),
929                                ty => format!("{}.{}", new_name, cont1.get_class_unquoted(&ty)),
930                            };
931                            let (args, current_cont) = Translatable::from(cont1).join(&new_vals, ", ").into();
932                            (format!("{}({})", forced_name, args), current_cont)
933                        }
934                    })
935                    .unwrap_or_else(|| {
936                        let (args, current_cont) = Translatable::from(cont1_fallback).join(&new_vals, ", ").into();
937                        (format!("{}({})", exp_str, args), current_cont)
938                    })
939            }
940            Lang::VecFunctionApp {
941                vector_type,
942                identifier: exp,
943                arguments: vals,
944                ..
945            } => {
946                let var = Var::try_from(exp.clone()).unwrap();
947                let name = var.get_name();
948                let str_vals = vals.iter().map(|x| x.to_r(cont).0).collect::<Vec<_>>().join(", ");
949                // Step ③ (unification_arrays.md): primitive-element arrays
950                // share the bare-atomic-vector representation with `Vec`, so
951                // a lifted call over them takes the same vapply/direct-call
952                // path. `vec_apply` (the list-based lift) only remains for
953                // composite-element (typed_vec) arrays.
954                let has_atomic_array_arg = vals
955                    .iter()
956                    .any(|val| cont.atomic_array_elem(&val.typing(cont).value).is_some());
957                if *vector_type == VecType::Vector || has_atomic_array_arg {
958                    // `Vec[N, T]` values transpile to plain R atomic vectors (see
959                    // vectors.md). Base-R/stdlib callees (no user declaration in
960                    // scope — NB `is_an_untyped_function` returns *false* for
961                    // them) keep the plain call: their R implementations are
962                    // natively vectorized. A user-declared scalar function must
963                    // NOT be called directly though: any `if`/`match` in its
964                    // transpiled body is scalar-only (`if` on a length > 1
965                    // condition is a fatal R error since 4.2), even when the
966                    // type-checker vector-lifted the call. The element-wise lift
967                    // is made explicit with a `vapply` whose `FUN.VALUE` comes
968                    // from the callee's static scalar return type (composite or
969                    // unresolved return → `lapply`). Exception (step 2 of the
970                    // vector/array unification plan): a user function whose
971                    // body was proven element-wise at declaration time
972                    // (`Context::is_vectorizable_fn`, computed by
973                    // `type_checking::vectorizability`) is called directly —
974                    // R runs the body once over the whole vector at native
975                    // speed, with identical element-wise semantics.
976                    if cont.is_an_untyped_function(&name)
977                        && !cont.is_vectorizable_fn(&name)
978                        && !crate::processes::type_checking::vectorizability::is_natively_vectorized_callee(&name)
979                    {
980                        let (_, cont1) = exp.to_r(cont);
981                        let fn_t_opt = cont1
982                            .get_type_from_variable(&var)
983                            .ok()
984                            .and_then(|t| FunctionType::try_from(t).ok());
985                        let new_vals = match &fn_t_opt {
986                            Some(fn_t) => {
987                                let new_args = fn_t
988                                    .get_param_types()
989                                    .iter()
990                                    .map(|arg| reduce_type(&cont1, arg))
991                                    .collect::<Vec<_>>();
992                                vals.iter()
993                                    .zip(new_args.iter())
994                                    .map(set_related_type_if_variable)
995                                    .collect::<Vec<_>>()
996                            }
997                            None => vals.clone(),
998                        };
999                        let (arg_strs, current_cont) = new_vals.iter().fold(
1000                            (Vec::new(), cont1.clone()),
1001                            |(mut acc, c): (Vec<String>, Context), val| {
1002                                let (s, c2) = val.to_r(&c);
1003                                acc.push(s);
1004                                (acc, c2)
1005                            },
1006                        );
1007                        let dotted = name.replace("__", ".");
1008                        let new_name = if &dotted[0..1] == "%" {
1009                            format!("`{}`", dotted)
1010                        } else {
1011                            dotted
1012                        };
1013                        // Which argument positions actually carry the vector
1014                        // being mapped over (the others are passed through as
1015                        // scalar constants).
1016                        let vec_positions: Vec<usize> = new_vals
1017                            .iter()
1018                            .enumerate()
1019                            .filter(|(_, val)| {
1020                                let t = reduce_type(&cont1, &val.typing(&cont1).value);
1021                                matches!(&t, Type::Vec(vt, _, _, _) if vt.is_vector())
1022                                    || cont1.atomic_array_elem(&t).is_some()
1023                            })
1024                            .map(|(i, _)| i)
1025                            .collect();
1026                        let fun_value = fn_t_opt
1027                            .as_ref()
1028                            .map(|fn_t| reduce_type(&cont1, &fn_t.get_return_type()))
1029                            .and_then(|ret| match ret {
1030                                Type::Boolean(_, _) => Some("logical(1)"),
1031                                Type::Integer(_, _) => Some("integer(1)"),
1032                                Type::Number(_, _) => Some("numeric(1)"),
1033                                Type::Char(_, _) => Some("character(1)"),
1034                                _ => None,
1035                            });
1036                        let code = match vec_positions.as_slice() {
1037                            [] => format!("{}({})", new_name, arg_strs.join(", ")),
1038                            [i] => {
1039                                let inner_args = arg_strs
1040                                    .iter()
1041                                    .enumerate()
1042                                    .map(|(j, s)| if j == *i { ".typr_x".to_string() } else { s.clone() })
1043                                    .collect::<Vec<_>>()
1044                                    .join(", ");
1045                                let fun = format!("function(.typr_x) {}({})", new_name, inner_args);
1046                                match fun_value {
1047                                    Some(fv) => format!("vapply({}, {}, {}, USE.NAMES = FALSE)", arg_strs[*i], fun, fv),
1048                                    None => format!("lapply({}, {})", arg_strs[*i], fun),
1049                                }
1050                            }
1051                            many => {
1052                                let first = &arg_strs[many[0]];
1053                                let inner_args = arg_strs
1054                                    .iter()
1055                                    .enumerate()
1056                                    .map(|(j, s)| {
1057                                        if many.contains(&j) {
1058                                            format!("{}[[.typr_i]]", s)
1059                                        } else {
1060                                            s.clone()
1061                                        }
1062                                    })
1063                                    .collect::<Vec<_>>()
1064                                    .join(", ");
1065                                let fun = format!("function(.typr_i) {}({})", new_name, inner_args);
1066                                match fun_value {
1067                                    Some(fv) => {
1068                                        format!("vapply(seq_along({}), {}, {}, USE.NAMES = FALSE)", first, fun, fv)
1069                                    }
1070                                    None => format!("lapply(seq_along({}), {})", first, fun),
1071                                }
1072                            }
1073                        };
1074                        (code, current_cont)
1075                    } else {
1076                        let (exp_str, cont1) = exp.to_r(cont);
1077                        // See the comment on the analogous fallback in
1078                        // `Lang::FunctionApp` above: a structurally-resolved
1079                        // (interface/generic-dispatched) callee has no
1080                        // registered signature to look up here.
1081                        let new_vals = match cont1
1082                            .get_type_from_variable(&var)
1083                            .ok()
1084                            .and_then(|t| FunctionType::try_from(t).ok())
1085                        {
1086                            Some(fn_t) => {
1087                                let new_args = fn_t
1088                                    .get_param_types()
1089                                    .iter()
1090                                    .map(|arg| reduce_type(&cont1, arg))
1091                                    .collect::<Vec<_>>();
1092                                vals.iter()
1093                                    .zip(new_args.iter())
1094                                    .map(set_related_type_if_variable)
1095                                    .collect::<Vec<_>>()
1096                            }
1097                            None => vals.clone(),
1098                        };
1099                        let (args, current_cont) = Translatable::from(cont1).join(&new_vals, ", ").into();
1100                        Var::from_language(*exp.clone())
1101                            .map(|var| {
1102                                let name = var.get_name();
1103                                let new_name = if &name[0..1] == "%" {
1104                                    format!("`{}`", name.replace("__", "."))
1105                                } else {
1106                                    name.replace("__", ".")
1107                                };
1108                                (format!("{}({})", new_name, args), current_cont.clone())
1109                            })
1110                            .unwrap_or((format!("{}({})", exp_str, args), current_cont))
1111                    }
1112                } else if name == "reduce" {
1113                    (format!("vec_reduce({})", str_vals), cont.clone())
1114                } else if name == "extend" {
1115                    (format!("vec_extend({})", str_vals), cont.clone())
1116                } else if cont.is_an_untyped_function(&name) {
1117                    let name = name.replace("__", ".");
1118                    let new_name = if &name[0..1] == "%" {
1119                        format!("`{}`", name)
1120                    } else {
1121                        name.to_string()
1122                    };
1123                    let s = format!("vec_apply({}, {})", new_name, str_vals);
1124                    (s, cont.clone())
1125                } else {
1126                    let (exp_str, cont1) = exp.to_r(cont);
1127                    // See the comment on the analogous fallback in
1128                    // `Lang::FunctionApp` above.
1129                    let new_vals = match cont1
1130                        .get_type_from_variable(&var)
1131                        .ok()
1132                        .and_then(|t| FunctionType::try_from(t).ok())
1133                    {
1134                        Some(fn_t) => {
1135                            let new_args = fn_t
1136                                .get_param_types()
1137                                .iter()
1138                                .map(|arg| reduce_type(&cont1, arg))
1139                                .collect::<Vec<_>>();
1140                            vals.iter()
1141                                .zip(new_args.iter())
1142                                .map(set_related_type_if_variable)
1143                                .collect::<Vec<_>>()
1144                        }
1145                        None => vals.clone(),
1146                    };
1147                    let (args, current_cont) = Translatable::from(cont1).join(&new_vals, ", ").into();
1148                    Var::from_language(*exp.clone())
1149                        .map(|var| {
1150                            let name = var.get_name();
1151                            let new_name = if &name[0..1] == "%" {
1152                                format!("`{}`", name.replace("__", "."))
1153                            } else {
1154                                name.replace("__", ".")
1155                            };
1156                            (format!("vec_apply({}, {})", new_name, args), current_cont.clone())
1157                        })
1158                        .unwrap_or((format!("vec_apply({}, {})", exp_str, args), current_cont))
1159                }
1160            }
1161            Lang::ArrayIndexing {
1162                identifier: exp,
1163                indexing: val,
1164                ..
1165            } => {
1166                let (exp_str, _) = exp.to_r(cont);
1167                // v[-n] → v[[length(v) + (1 - n)]] (count from end)
1168                let negative_idx = val.get_members_if_array().and_then(|members| {
1169                    if members.len() == 1 {
1170                        if let Lang::Integer { value: i, .. } = &members[0] {
1171                            if *i < 0 {
1172                                Some(*i)
1173                            } else {
1174                                None
1175                            }
1176                        } else {
1177                            None
1178                        }
1179                    } else {
1180                        None
1181                    }
1182                });
1183                let res = if let Some(neg) = negative_idx {
1184                    let offset = 1 + neg; // e.g. -1 → 0, -2 → -1
1185                    if offset == 0 {
1186                        format!("{}[[length({})]]", exp_str, exp_str)
1187                    } else if offset < 0 {
1188                        format!("{}[[length({}) - {}L]]", exp_str, exp_str, -offset)
1189                    } else {
1190                        format!("{}[[length({}) + {}L]]", exp_str, exp_str, offset)
1191                    }
1192                } else {
1193                    let (val_str, _) = val.to_simple_r(cont);
1194                    format!("{}[[{}]]", exp_str, val_str)
1195                };
1196                (res, cont.clone())
1197            }
1198            Lang::GenFunc { name: func, .. } => (format!("function(x, ...) UseMethod('{}')", func), cont.clone()),
1199            Lang::Let {
1200                variable: expr,
1201                r#type: ttype,
1202                expression: body,
1203                is_public: _,
1204                is_testable: _,
1205                is_export,
1206                help_data,
1207            } => {
1208                let (body_str, new_cont) = body.to_r(cont);
1209                // --checked (soundness_transpilation.md Phase A): only
1210                // annotated `let x: T <- expr` are instrumented, matching the
1211                // plan's v1 boundary table — un-annotated lets and function
1212                // definitions (which have no `let`-level type annotation)
1213                // stay untouched here (functions get their own check via the
1214                // `Lang::Function` return-type wrap).
1215                let body_str = if ttype.is_empty() {
1216                    body_str
1217                } else {
1218                    let what = format!("let {}", Var::try_from(expr).map(|v| v.get_name()).unwrap_or_default());
1219                    checked_assertions::wrap_checked(&new_cont, body_str, ttype, help_data, &what)
1220                };
1221                let new_name = format_backtick(expr.clone().to_r(cont).0);
1222
1223                let (r_code, _new_name2) = Function::try_from((**body).clone())
1224                    .map(|_| {
1225                        let related_type = Var::try_from(expr)
1226                            .ok()
1227                            .map(|v| v.get_type())
1228                            .filter(|t| !matches!(t, Type::Empty(_) | Type::UnknownFunction(_)))
1229                            .unwrap_or_else(|| typing(cont, expr).value);
1230                        let method = match cont.get_environment() {
1231                            Environment::Project => format!(
1232                                "#' @method {}\n",
1233                                new_name.replace(".", " ").replace("`", "")
1234                            ),
1235                            _ => "".to_string(),
1236                        };
1237                        match related_type {
1238                            Type::Empty(_) => {
1239                                (format!("{} <- {}", new_name, body_str), new_name.clone())
1240                            }
1241                            // `Type::Any` is the one case `display_type` (which produced
1242                            // `new_name` above) deliberately leaves unsuffixed — see its
1243                            // own `Type::Empty(_) | Type::Any(_) => ""` arm — so `.default`
1244                            // must be appended explicitly here. Bare/kinded generics
1245                            // (`Type::Generic`, `Type::KindedGen`) are NOT special-cased:
1246                            // `display_type` already resolved them to `name.default` via
1247                            // `get_class`'s "default" fallback, so they fall through to
1248                            // the catch-all `_` arm below and use `new_name` as-is.
1249                            Type::Any(_) => (
1250                                format!(
1251                                    "{} <- {}",
1252                                    format_backtick(format!(
1253                                        "{}.default",
1254                                        new_name.trim_matches('`')
1255                                    )),
1256                                    body_str
1257                                ),
1258                                new_name.clone(),
1259                            ),
1260                            _ => {
1261                                let mut code = format!("{}{} <- {}", method, new_name, body_str);
1262                                // Interfaces are satisfied structurally at compile
1263                                // time, but R dispatches nominally on the runtime
1264                                // class vector. Constructed records carry the
1265                                // interface class (see the record-alias arm), but
1266                                // primitives (`integer`, `character`, …) and values
1267                                // built outside TypR constructors never do — so a
1268                                // method whose dispatch parameter is a *pure*
1269                                // interface (interface facet, no record facet) also
1270                                // registers itself as the `.default` fallback.
1271                                // `UseMethod` only reaches `.default` when no more
1272                                // specific method matches, so this never overrides
1273                                // a concrete-type method.
1274                                //
1275                                // Same reasoning for a `Foreign<T>`-family dispatch
1276                                // parameter (soundness_transpilation.md Phase D):
1277                                // the emitted method's suffix is the TypR-side
1278                                // alias's own (possibly monomorphized) name (e.g.
1279                                // `Foreign0`), but a real foreign value's runtime
1280                                // class is whatever R/S4/R6/… gave it (`lm`,
1281                                // `Matrix`, a user S4 class, …) — never that
1282                                // synthetic name — so a plain `name.Foreign0`
1283                                // method is dead code no call can ever reach
1284                                // without this `.default` fallback.
1285                                let suffix = cont.get_class_unquoted(&related_type);
1286                                let is_foreign_dispatch = matches!(&related_type, Type::Alias(name, _, _, _) if cont.resolves_to_foreign_alias(name));
1287                                if suffix != "default"
1288                                    && (is_foreign_dispatch
1289                                        || (facets::interface_facet(cont, &related_type).is_some()
1290                                            && facets::record_facet(cont, &related_type).is_none()))
1291                                {
1292                                    // `new_name` may be backtick-wrapped (dotted
1293                                    // method names always are) — strip them before
1294                                    // splitting off the suffix, re-wrap on emit.
1295                                    if let Some(base) = new_name
1296                                        .trim_matches('`')
1297                                        .strip_suffix(&format!(".{}", suffix))
1298                                    {
1299                                        let default_method = match cont.get_environment() {
1300                                            Environment::Project => {
1301                                                format!("#' @method {} default\n", base)
1302                                            }
1303                                            _ => "".to_string(),
1304                                        };
1305                                        code = format!(
1306                                            "{}\n{}{} <- {}",
1307                                            code,
1308                                            default_method,
1309                                            format_backtick(format!("{}.default", base)),
1310                                            new_name
1311                                        );
1312                                    }
1313                                }
1314                                (code, new_name.clone())
1315                            }
1316                        }
1317                    })
1318                    .unwrap_or((format!("{} <- {}", new_name, body_str), new_name));
1319                let code = if !ttype.is_empty() {
1320                    let type_annotation = new_cont.get_type_anotation(ttype);
1321                    format!("{} |> {}\n", r_code, type_annotation)
1322                } else {
1323                    r_code + "\n"
1324                };
1325                // RFC-TR-032: @export prepends `#' @export` for R package API
1326                let code = if *is_export {
1327                    format!("#' @export\n{}", code)
1328                } else {
1329                    code
1330                };
1331                (code, new_cont)
1332            }
1333            Lang::Array { .. } => {
1334                let typ = self.typing(cont).value;
1335                let array = array_literal_raw(self, cont);
1336                (
1337                    format!("{} |> {}", array, cont.get_type_anotation(&typ)),
1338                    cont.to_owned(),
1339                )
1340            }
1341            Lang::List {
1342                value: args, spreads, ..
1343            } if spreads.is_empty() => {
1344                let (body, current_cont) = Translatable::from(cont.clone()).join_arg_val(args, ",\n ").into();
1345                let (typ, _, _) = typing(cont, self).to_tuple();
1346                // For record-alias types use the constructor directly
1347                if let Type::Alias(alias_name, _, _, _) = &typ {
1348                    let is_record = cont
1349                        .aliases()
1350                        .find(|(var, _)| var.get_name() == *alias_name)
1351                        .map(|(_, t)| matches!(t, Type::Record(_, _)))
1352                        .unwrap_or(false);
1353                    if is_record {
1354                        return (format!("{}({})", alias_name, body), current_cont);
1355                    }
1356                }
1357                let anotation = cont.get_type_anotation(&typ);
1358                cont.get_classes(&typ)
1359                    .map(|_| format!("list({}) |> {}", body, anotation))
1360                    .unwrap_or(format!("list({}) |> {}", body, anotation))
1361                    .to_some()
1362                    .map(|s| (s, current_cont))
1363                    .unwrap()
1364            }
1365            // Record literal with one or more `...source` spreads
1366            // (spread_operator2.md): merge spreads sequentially into `base`
1367            // with the runtime `spread()` helper, then apply explicit
1368            // fields as the final override — never via static field
1369            // expansion, so unknown/row-polymorphic fields carried by the
1370            // runtime value of `source` are preserved (§5-§7).
1371            Lang::List {
1372                value: args, spreads, ..
1373            } => {
1374                let mut spreads_iter = spreads.iter();
1375                let first = spreads_iter.next().expect("checked non-empty above");
1376                let (mut base, mut current_cont) = first.to_r(cont);
1377                for spread_expr in spreads_iter {
1378                    let (next, next_cont) = spread_expr.to_r(&current_cont);
1379                    base = format!("spread({}, {})", base, next);
1380                    current_cont = next_cont;
1381                }
1382                if args.is_empty() {
1383                    return (base, current_cont);
1384                }
1385                let (overrides, current_cont) = Translatable::from(current_cont).join_arg_val(args, ", ").into();
1386                (format!("spread({}, list({}))", base, overrides), current_cont)
1387            }
1388            Lang::DataFrame { value: args, .. } => {
1389                let (body, current_cont) = Translatable::from(cont.clone()).join_arg_val(args, ",\n ").into();
1390                let (typ, _, _) = typing(cont, self).to_tuple();
1391                let anotation = cont.get_type_anotation(&typ);
1392                cont.get_classes(&typ)
1393                    .map(|_| format!("data.frame({}) |> {}", body, anotation))
1394                    .unwrap_or(format!("data.frame({}) |> {}", body, anotation))
1395                    .to_some()
1396                    .map(|s| (s, current_cont))
1397                    .unwrap()
1398            }
1399            Lang::If {
1400                condition: cond,
1401                if_block: exp,
1402                else_block: els,
1403                ..
1404            } if els == &Box::new(Lang::Empty(HelpData::default())) => Translatable::from(cont.clone())
1405                .add("if(")
1406                .to_r(cond)
1407                .add(") {\n")
1408                .to_r(exp)
1409                .add(" \n}")
1410                .into(),
1411            Lang::If {
1412                condition: cond,
1413                if_block: exp,
1414                else_block: els,
1415                help_data: _,
1416            } => Translatable::from(cont.clone())
1417                .add("if(")
1418                .to_r(cond)
1419                .add(") {\n")
1420                .to_r(exp)
1421                .add(" \n} else ")
1422                .to_r(els)
1423                .into(),
1424            Lang::Tuple { value: vals, .. } => {
1425                // Attach the registered TupleN alias class on top of the bare
1426                // 'Tuple' marker — S3 methods on tuple-typed parameters are
1427                // emitted as `name.TupleN`, so the runtime value must carry
1428                // that class for UseMethod dispatch to find them.
1429                let typ = self.typing(cont).value;
1430                let (body, current_cont): (String, Context) = Translatable::from(cont.clone())
1431                    .add("struct(list(")
1432                    .join(vals, ", ")
1433                    .add("), 'Tuple')")
1434                    .into();
1435                (format!("{} |> {}", body, cont.get_type_anotation(&typ)), current_cont)
1436            }
1437            Lang::Assign {
1438                identifier: var,
1439                expression: exp,
1440                ..
1441            } => Translatable::from(cont.clone()).to_r(var).add(" <- ").to_r(exp).into(),
1442            Lang::Comment { value: txt, .. } => ("#".to_string() + txt, cont.clone()),
1443            Lang::Tag { name: s, value: t, .. } => {
1444                let (t_str, new_cont) = t.to_r(cont);
1445                let is_empty = matches!(t.as_ref(), Lang::Empty(_));
1446                // Canonical representation (see validation_variant_d_union.md §2):
1447                // tag identity in position 1, payload under `body`, class enriched
1448                // with the union name (when the tag belongs to a declared union)
1449                // plus `Tag`/`list`. This makes the literal interchangeable with
1450                // the value produced by the variant constructor `V(...)`, so
1451                // `match` and the variant validators apply to both origins.
1452                let class = match find_union_for_tag(s, cont) {
1453                    Some(union_name) => format!("c('{}', '{}', 'Tag', 'list')", s, union_name),
1454                    None => format!("c('{}', 'Tag', 'list')", s),
1455                };
1456                let value = if is_empty {
1457                    format!("structure(list('{}'), class = {})", s, class)
1458                } else {
1459                    format!("structure(list('{}', body = {}), class = {})", s, t_str, class)
1460                };
1461                (value, new_cont)
1462            }
1463            Lang::Null(_) => ("NULL".to_string(), cont.clone()),
1464            Lang::Empty(_) => ("NA".to_string(), cont.clone()),
1465            Lang::Lines { value: exps, .. } => Translatable::from(cont.clone()).join(exps, "\n").into(),
1466            // `return X` is a syntax error in R — `return` is an ordinary
1467            // function there, not a statement keyword, so the argument must
1468            // be parenthesized (`return(X)`).
1469            Lang::Return { value: exp, .. } => Translatable::from(cont.clone())
1470                .add("return(")
1471                .to_r(exp)
1472                .add(")")
1473                .into(),
1474            Lang::Lambda {
1475                parameters: params,
1476                body: bloc,
1477                ..
1478            } => {
1479                let param_names: Vec<String> = params
1480                    .iter()
1481                    .map(|p: &Lang| match p {
1482                        Lang::Variable { name, .. } => name.clone(),
1483                        _ => "x".to_string(),
1484                    })
1485                    .collect();
1486                (
1487                    format!("function({}) {{ {} }}", param_names.join(", "), bloc.to_r(cont).0),
1488                    cont.clone(),
1489                )
1490            }
1491            Lang::VecBlock { value: bloc, .. } => (bloc.to_string(), cont.clone()),
1492            Lang::RBlock { value: bloc, .. } => (bloc.to_string(), cont.clone()),
1493            Lang::Library { value: name, .. } => (format!("library({})", name), cont.clone()),
1494            Lang::Match {
1495                target: exp, branches, ..
1496            } => (
1497                to_pattern_match_statement((**exp).clone(), branches, cont),
1498                cont.clone(),
1499            ),
1500            Lang::Exp { value: exp, .. } => (exp.clone(), cont.clone()),
1501            Lang::ForLoop {
1502                identifier: var,
1503                expression: iterator,
1504                body,
1505                ..
1506            } => Translatable::from(cont.clone())
1507                .add("for (")
1508                .to_r_safe(var)
1509                .add(" in ")
1510                .to_r_safe(iterator)
1511                .add(") {\n")
1512                .to_r_safe(body)
1513                .add("\n}")
1514                .into(),
1515            Lang::RFunction {
1516                parameters: vars, body, ..
1517            } => Translatable::from(cont.clone())
1518                .add("function (")
1519                .join(vars, ", ")
1520                .add(") \n")
1521                .add(body)
1522                .add("\n")
1523                .into(),
1524            Lang::ExternBlock {
1525                parameters: params,
1526                body,
1527                ..
1528            } => {
1529                let param_names = params.iter().map(|p| p.to_r(cont)).collect::<Vec<_>>().join(", ");
1530                (format!("function({}) {{\n{}\n}}", param_names, body), cont.clone())
1531            }
1532            Lang::Signature { .. } => ("".to_string(), cont.clone()),
1533            Lang::TypeConstructor { .. } => ("".to_string(), cont.clone()),
1534            Lang::Alias {
1535                identifier: ident,
1536                target_type: typ,
1537                is_export,
1538                ..
1539            } => {
1540                let name = Var::from_language(*ident.clone())
1541                    .map(|v| v.get_name())
1542                    .unwrap_or_default();
1543                // An intersection alias (`A & B`, `%T & list {...}`, ...) has
1544                // no fixed runtime shape on any non-record member — TypR
1545                // doesn't monomorphize generics, R has no class for "any
1546                // record", and interface members contribute methods, not
1547                // fields — but every `Record`/list member found anywhere in
1548                // the intersection does have fields worth constructing and
1549                // validating. `facets::record_facet` flattens the whole
1550                // intersection and merges all such members into one field
1551                // set (see its doc comment for why a simple pairwise
1552                // `norm_intersection` reduction isn't enough on its own), so
1553                // substituting that merged record here routes any list-
1554                // bearing intersection into the existing `Type::Record`
1555                // pipeline below (constructor/annotator/validator) instead
1556                // of the unrelated union-alias `Type::Operator` catch-all,
1557                // which doesn't apply here and previously produced no R code
1558                // at all for these shapes, leaving `validate_X`/`as.X`
1559                // referenced elsewhere but never defined.
1560                let typ_for_dispatch: Type = match typ {
1561                    Type::Operator(TypeOperator::Intersection, _, _, h) => facets::record_facet(cont, typ)
1562                        .map(|fields| Type::Record(fields, h.clone()))
1563                        .unwrap_or_else(|| typ.clone()),
1564                    _ => typ.clone(),
1565                };
1566                let (alias_code, alias_cont) = match &typ_for_dispatch {
1567                    Type::Record(fields, _) => {
1568                        let mut sorted_fields: Vec<&ArgumentType> = fields.iter().collect();
1569                        sorted_fields.sort_by_key(|f| f.get_argument_str());
1570                        let params = sorted_fields
1571                            .iter()
1572                            .map(|f| f.get_argument_str())
1573                            .collect::<Vec<_>>()
1574                            .join(", ");
1575                        // Each field is only added to `explicit` when the caller
1576                        // actually supplied it (`missing()`, not a NULL default):
1577                        // a record-typed `.spread` (spread_operator3.md) may cover
1578                        // the field instead, and `missing()` never forces the
1579                        // argument promise, so unsupplied fields stay lazy/unevaluated.
1580                        let explicit_lines = sorted_fields
1581                            .iter()
1582                            .map(|f| {
1583                                let n = f.get_argument_str();
1584                                format!("  if (!missing({n})) explicit[[\"{n}\"]] <- {n}")
1585                            })
1586                            .collect::<Vec<_>>()
1587                            .join("\n");
1588                        // Constructor: collect the explicitly-supplied fields, merge
1589                        // them over `.spread` (explicit wins, extra spread fields are
1590                        // kept), then delegate entirely to the annotator. It neither
1591                        // adds classes nor validates.
1592                        let constructor = format!(
1593                            "{name} <- function({params}, .spread = NULL) {{\n  explicit <- list()\n{explicit_lines}\n  x <- typr_spread_record(explicit, .spread)\n  as.{name}(x)\n}}"
1594                        );
1595                        // Structural supertypes: any record alias whose fields are a
1596                        // strict subset of this alias's fields. They are included in
1597                        // the S3 class vector so that methods defined on the supertype
1598                        // dispatch correctly to subtype values.
1599                        //
1600                        // Candidates come from the whole-program `record_aliases`
1601                        // registry, not just `cont.aliases()`: the latter is scoped
1602                        // to the current module body, so a supertype declared in a
1603                        // sibling `mod` file (e.g. `Position` while transpiling
1604                        // `Circle` in another file) would otherwise never be found,
1605                        // even though R's S3 classes have no module privacy.
1606                        // Built as an ordered Vec (not a HashMap) and deduplicated by
1607                        // first occurrence, so candidate order — and therefore the
1608                        // final sort below — stays deterministic across runs.
1609                        let mut seen_names: std::collections::HashSet<String> = std::collections::HashSet::new();
1610                        let candidates: Vec<(String, Type)> = cont
1611                            .aliases()
1612                            .map(|(var, typ)| (var.get_name(), typ.clone()))
1613                            .chain(cont.record_aliases.iter().cloned())
1614                            .filter(|(other_name, _)| seen_names.insert(other_name.clone()))
1615                            .collect();
1616                        let mut supertype_entries: Vec<(String, usize)> = candidates
1617                            .into_iter()
1618                            .filter_map(|(other_name, typ)| {
1619                                if other_name == name {
1620                                    return None;
1621                                }
1622                                if let Type::Record(other_fields, _) = typ {
1623                                    if fields.is_superset(&other_fields) && other_fields != *fields {
1624                                        Some((other_name, other_fields.len()))
1625                                    } else {
1626                                        None
1627                                    }
1628                                } else {
1629                                    None
1630                                }
1631                            })
1632                            .collect();
1633                        // More-specific supertypes (more fields) first for correct S3
1634                        // dispatch order; ties broken by name for determinism.
1635                        supertype_entries.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
1636                        // Interfaces-as-classes: any named interface alias — or an
1637                        // intersection alias with an interface facet, e.g.
1638                        // `Combined <- list {...} & interface {...}` — that this
1639                        // record structurally satisfies is also added to the class
1640                        // vector, so functions dispatching on the interface name
1641                        // (`describe.Viewable`) apply to values of this type through
1642                        // normal S3 dispatch. Satisfaction is checked against the
1643                        // final whole-program context (transpiling runs after all
1644                        // typing), so implementing functions declared later in the
1645                        // source are already visible here.
1646                        let self_alias = Type::Alias(name.clone(), vec![], false, HelpData::default());
1647                        let mut seen_ifaces: std::collections::HashSet<String> = std::collections::HashSet::new();
1648                        let mut interface_entries: Vec<(String, usize)> = cont
1649                            .aliases()
1650                            .filter(|(var, _)| var.get_name() != name)
1651                            .filter(|(_, alias_typ)| !alias_typ.has_generic())
1652                            .filter_map(|(var, alias_typ)| {
1653                                let methods = facets::interface_facet(cont, alias_typ)?;
1654                                (seen_ifaces.insert(var.get_name()) && self_alias.is_subtype_raw(alias_typ, cont))
1655                                    .then(|| (var.get_name(), methods.len()))
1656                            })
1657                            .collect();
1658                        // More methods = more specific; ties broken by name.
1659                        interface_entries.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
1660                        // Record supertypes (structural field subsets) first, then
1661                        // interfaces: a record supertype is always at least as
1662                        // specific a match as an interface constraint.
1663                        let all_super_names: Vec<String> = supertype_entries
1664                            .iter()
1665                            .chain(interface_entries.iter())
1666                            .map(|(n, _)| format!("\"{n}\""))
1667                            .collect();
1668                        let supertype_class_str = if all_super_names.is_empty() {
1669                            String::new()
1670                        } else {
1671                            format!(", {}", all_super_names.join(", "))
1672                        };
1673                        // Annotator: the single entry point that adds the class
1674                        // (idempotently), runs the internal validator, then the
1675                        // user validator (`validate` S3 generic, default = identity).
1676                        let annotator = format!(
1677                            "as.{name} <- function(x) {{\n  if (!inherits(x, \"{name}\")) class(x) <- c(\"{name}\"{supertype_class_str}, \"list\")\n  x <- validate_{name}(x)\n  x <- validate(x)\n  x\n}}"
1678                        );
1679                        let fields_quoted = sorted_fields
1680                            .iter()
1681                            .map(|f| format!("\"{}\"", f.get_argument_str()))
1682                            .collect::<Vec<_>>()
1683                            .join(", ");
1684                        // Per-field type invariants, checked by class (`inherits`).
1685                        // Fields whose type has no reliable nominal class are only
1686                        // checked for presence (above).
1687                        let field_checks = sorted_fields
1688                            .iter()
1689                            .filter_map(|f| {
1690                                let n = f.get_argument_str();
1691                                record_field_class(&f.body_type(), cont).map(|cls| {
1692                                    format!(
1693                                        "  if (!inherits(x[[\"{n}\"]], \"{cls}\")) stop(\"Validation failed for type {name}: field '{n}' must be of class {cls}\")"
1694                                    )
1695                                })
1696                            })
1697                            .collect::<Vec<_>>()
1698                            .join("\n");
1699                        let field_checks_block = if field_checks.is_empty() {
1700                            String::new()
1701                        } else {
1702                            format!("{field_checks}\n")
1703                        };
1704                        // Internal validator: pure structural invariants only.
1705                        // It must not reconstruct the value (that would recurse
1706                        // back through the constructor / annotator).
1707                        let validator = format!(
1708                            "validate_{name} <- function(x) {{\n  required_fields <- c({fields_quoted})\n  missing_fields <- setdiff(required_fields, names(x))\n  if (length(missing_fields) > 0) {{\n    stop(paste0(\"Validation failed for type {name}: missing fields: \", paste(missing_fields, collapse = \", \")))\n  }}\n{field_checks_block}  x\n}}"
1709                        );
1710                        (format!("{constructor}\n{annotator}\n{validator}"), cont.clone())
1711                    }
1712                    // Dataframe alias (`type Df <- dataframe[#N]{ ... }` /
1713                    // `df[3]{ ... }`): same constructor/annotator/validator
1714                    // pipeline as a record alias, but columns are assembled
1715                    // into a `data.frame` instead of a `list`, the class
1716                    // chain carries `"data.frame"`, and a concrete size
1717                    // index (`df[3]{...}`) additionally checks `nrow(x)`.
1718                    Type::Vec(VecType::DataFrame, size, fields_type, _)
1719                        if matches!(fields_type.as_ref(), Type::Record(_, _)) =>
1720                    {
1721                        use crate::components::r#type::tint::Tint;
1722                        let fields = match fields_type.as_ref() {
1723                            Type::Record(fields, _) => fields,
1724                            _ => unreachable!(),
1725                        };
1726                        let mut sorted_fields: Vec<&ArgumentType> = fields.iter().collect();
1727                        sorted_fields.sort_by_key(|f| f.get_argument_str());
1728                        let params = sorted_fields
1729                            .iter()
1730                            .map(|f| f.get_argument_str())
1731                            .collect::<Vec<_>>()
1732                            .join(", ");
1733                        let explicit_lines = sorted_fields
1734                            .iter()
1735                            .map(|f| {
1736                                let n = f.get_argument_str();
1737                                format!("  if (!missing({n})) explicit[[\"{n}\"]] <- {n}")
1738                            })
1739                            .collect::<Vec<_>>()
1740                            .join("\n");
1741                        // Constructor: collect the explicitly-supplied columns,
1742                        // merge them over `.spread`, assemble into a
1743                        // `data.frame`, then delegate to the annotator.
1744                        let constructor = format!(
1745                            "{name} <- function({params}, .spread = NULL) {{\n  explicit <- list()\n{explicit_lines}\n  x <- typr_spread_record(explicit, .spread)\n  as.{name}(do.call(data.frame, c(x, list(stringsAsFactors = FALSE))))\n}}"
1746                        );
1747                        // Annotator: adds the class (idempotently), runs the
1748                        // internal validator, then the user validator.
1749                        let annotator = format!(
1750                            "as.{name} <- function(x) {{\n  if (!inherits(x, \"{name}\")) class(x) <- c(\"{name}\", \"data.frame\", \"list\")\n  x <- validate_{name}(x)\n  x <- validate(x)\n  x\n}}"
1751                        );
1752                        let fields_quoted = sorted_fields
1753                            .iter()
1754                            .map(|f| format!("\"{}\"", f.get_argument_str()))
1755                            .collect::<Vec<_>>()
1756                            .join(", ");
1757                        // Per-column type invariants, checked by class
1758                        // (`inherits`) on the column vector.
1759                        let field_checks = sorted_fields
1760                            .iter()
1761                            .filter_map(|f| {
1762                                let n = f.get_argument_str();
1763                                record_field_class(&f.body_type(), cont).map(|cls| {
1764                                    format!(
1765                                        "  if (!inherits(x[[\"{n}\"]], \"{cls}\")) stop(\"Validation failed for type {name}: column '{n}' must be of class {cls}\")"
1766                                    )
1767                                })
1768                            })
1769                            .collect::<Vec<_>>()
1770                            .join("\n");
1771                        let field_checks_block = if field_checks.is_empty() {
1772                            String::new()
1773                        } else {
1774                            format!("{field_checks}\n")
1775                        };
1776                        // Row-count check: only emitted when the size index is
1777                        // a concrete literal (`df[3]{...}`); a generic (`#N`)
1778                        // or unconstrained (`df{...}`) size imposes no check.
1779                        let size_check = if let Type::Integer(Tint::Val(n), _) = size.as_ref() {
1780                            format!(
1781                                "  if (nrow(x) != {n}) stop(paste0(\"Validation failed for type {name}: expected {n} rows, got \", nrow(x)))\n"
1782                            )
1783                        } else {
1784                            String::new()
1785                        };
1786                        let validator = format!(
1787                            "validate_{name} <- function(x) {{\n  if (!is.data.frame(x)) stop(\"Validation failed for type {name}: expected a data.frame\")\n  required_fields <- c({fields_quoted})\n  missing_fields <- setdiff(required_fields, names(x))\n  if (length(missing_fields) > 0) {{\n    stop(paste0(\"Validation failed for type {name}: missing columns: \", paste(missing_fields, collapse = \", \")))\n  }}\n{field_checks_block}{size_check}  x\n}}"
1788                        );
1789                        (format!("{constructor}\n{annotator}\n{validator}"), cont.clone())
1790                    }
1791                    // Vector alias (`type V <- Vec[#N, T]` / `Vec[3, T]` /
1792                    // `Vec[T]`): no class is added (R already distinguishes
1793                    // atomic vector kinds via `is.*`/implicit class), so the
1794                    // pipeline is the same shape as a primitive alias —
1795                    // constructor delegates straight to the validator — plus
1796                    // an optional length check when the size index is a
1797                    // concrete literal.
1798                    Type::Vec(VecType::Vector, size, elem_type, _) => {
1799                        use crate::components::r#type::tint::Tint;
1800                        let constructor = format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
1801                        let elem_check = record_field_class(elem_type.as_ref(), cont).map(|cls| {
1802                            format!(
1803                                "  if (!inherits(x, \"{cls}\")) stop(\"Validation failed for type {name}: expected vector of {cls}\")\n"
1804                            )
1805                        }).unwrap_or_default();
1806                        let size_check = if let Type::Integer(Tint::Val(n), _) = size.as_ref() {
1807                            format!(
1808                                "  if (length(x) != {n}) stop(paste0(\"Validation failed for type {name}: expected length {n}, got \", length(x)))\n"
1809                            )
1810                        } else {
1811                            String::new()
1812                        };
1813                        let validator = format!("validate_{name} <- function(x) {{\n{elem_check}{size_check}  x\n}}");
1814                        (format!("{constructor}\n{validator}"), cont.clone())
1815                    }
1816                    // Array alias (`type A <- [#N, T]` / `[3, T]` / bare
1817                    // brackets, and the explicit `Array[#N, T]` spelling —
1818                    // both `VecType::S3` and `VecType::Array` produce the
1819                    // same runtime shape): the literal/constructor-call
1820                    // transpilation already wraps elements in `typed_vec`
1821                    // (see `Lang::Array`/`Lang::ArrayConstructorCall`), so
1822                    // this alias just needs to register the alias name as a
1823                    // `typed_vec` subclass — same annotator/validator shape
1824                    // as a record alias — so generic functions written
1825                    // against `typed_vec` dispatch on it.
1826                    // Atomic-representation array alias (step ③,
1827                    // unification_arrays.md): the runtime value is a bare R
1828                    // atomic vector, so the pipeline must neither coerce to
1829                    // `typed_vec` nor validate against it. The annotator
1830                    // still appends the alias class (S3 dispatch for
1831                    // alias-typed parameters relies on it); `c()`/subsetting
1832                    // strip it, but TypR re-annotates from the static type.
1833                    Type::Vec(VecType::S3, size, elem_type, _) | Type::Vec(VecType::Array, size, elem_type, _)
1834                        if cont.atomic_array_elem(typ).is_some() =>
1835                    {
1836                        use crate::components::r#type::tint::Tint;
1837                        let constructor = format!("{name} <- function(x) {{\n  as.{name}(x)\n}}");
1838                        let annotator = format!(
1839                            "as.{name} <- function(x) {{\n  if (!inherits(x, \"{name}\")) class(x) <- c(\"{name}\", class(x))\n  x <- validate_{name}(x)\n  x <- validate(x)\n  x\n}}"
1840                        );
1841                        let elem_check = match cont.atomic_array_elem(typ) {
1842                            Some(Type::Integer(_, _)) => format!(
1843                                "  if (!(is.integer(x) || (is.numeric(x) && all(x == trunc(x))))) stop(\"Validation failed for type {name}: expected an integer vector\")\n"
1844                            ),
1845                            Some(Type::Char(_, _)) => format!(
1846                                "  if (!is.character(x)) stop(\"Validation failed for type {name}: expected a character vector\")\n"
1847                            ),
1848                            Some(Type::Boolean(_, _)) => format!(
1849                                "  if (!is.logical(x)) stop(\"Validation failed for type {name}: expected a logical vector\")\n"
1850                            ),
1851                            _ => format!(
1852                                "  if (!is.numeric(x)) stop(\"Validation failed for type {name}: expected a numeric vector\")\n"
1853                            ),
1854                        };
1855                        let size_check = if let Type::Integer(Tint::Val(n), _) = size.as_ref() {
1856                            format!(
1857                                "  if (length(x) != {n}) stop(paste0(\"Validation failed for type {name}: expected length {n}, got \", length(x)))\n"
1858                            )
1859                        } else {
1860                            String::new()
1861                        };
1862                        let validator = format!("validate_{name} <- function(x) {{\n{elem_check}{size_check}  x\n}}");
1863                        (format!("{constructor}\n{annotator}\n{validator}"), cont.clone())
1864                    }
1865                    Type::Vec(VecType::S3, size, elem_type, _) | Type::Vec(VecType::Array, size, elem_type, _) => {
1866                        use crate::components::r#type::tint::Tint;
1867                        let constructor = format!(
1868                            "{name} <- function(x) {{\n  if (!inherits(x, \"typed_vec\")) x <- typed_vec(x)\n  as.{name}(x)\n}}"
1869                        );
1870                        let annotator = format!(
1871                            "as.{name} <- function(x) {{\n  if (!inherits(x, \"{name}\")) class(x) <- c(\"{name}\", class(x))\n  x <- validate_{name}(x)\n  x <- validate(x)\n  x\n}}"
1872                        );
1873                        let elem_check = record_field_class(elem_type.as_ref(), cont).map(|cls| {
1874                            format!(
1875                                "  if (!all(vapply(x$data, inherits, logical(1), \"{cls}\"))) stop(\"Validation failed for type {name}: expected elements of class {cls}\")\n"
1876                            )
1877                        }).unwrap_or_default();
1878                        let size_check = if let Type::Integer(Tint::Val(n), _) = size.as_ref() {
1879                            format!(
1880                                "  if (length(x) != {n}) stop(paste0(\"Validation failed for type {name}: expected length {n}, got \", length(x)))\n"
1881                            )
1882                        } else {
1883                            String::new()
1884                        };
1885                        let validator = format!(
1886                            "validate_{name} <- function(x) {{\n  if (!inherits(x, \"typed_vec\")) stop(\"Validation failed for type {name}: expected typed_vec\")\n{elem_check}{size_check}  x\n}}"
1887                        );
1888                        (format!("{constructor}\n{annotator}\n{validator}"), cont.clone())
1889                    }
1890                    Type::Operator(_, _, _, _) => {
1891                        // Union alias: generate the full constructor/annotator/
1892                        // validator pipeline for each variant (see
1893                        // validation_variant_d_union.md).
1894                        let union_name = &name;
1895                        let members = flatten_operator_union(typ);
1896                        // Sort for deterministic output
1897                        let mut members_vec: Vec<Type> = members.into_iter().collect();
1898                        members_vec.sort_by_key(|t| t.pretty2());
1899                        let constructors: Vec<String> = members_vec
1900                            .iter()
1901                            .filter_map(|member| match member {
1902                                Type::Tag(variant_name, inner, _) => Some(tag_variant_pipeline(
1903                                    variant_name,
1904                                    union_name,
1905                                    inner.as_ref(),
1906                                )),
1907                                Type::Alias(alias_name, _, _, _) => {
1908                                    // Look up the record fields for this alias
1909                                    let record_fields = cont
1910                                        .aliases()
1911                                        .find(|(var, _)| var.get_name() == *alias_name)
1912                                        .and_then(|(_, t)| {
1913                                            if let Type::Record(fields, _) = t {
1914                                                Some(fields.clone())
1915                                            } else {
1916                                                None
1917                                            }
1918                                        });
1919                                    if let Some(fields) = record_fields {
1920                                        let mut sorted: Vec<&ArgumentType> =
1921                                            fields.iter().collect();
1922                                        sorted.sort_by_key(|f| f.get_argument_str());
1923                                        let params = sorted
1924                                            .iter()
1925                                            .map(|f| f.get_argument_str())
1926                                            .collect::<Vec<_>>()
1927                                            .join(", ");
1928                                        let field_args = sorted
1929                                            .iter()
1930                                            .map(|f| {
1931                                                let n = f.get_argument_str();
1932                                                format!("{n} = {n}")
1933                                            })
1934                                            .collect::<Vec<_>>()
1935                                            .join(", ");
1936                                        Some(format!(
1937                                            "{alias_name} <- function({params}) {{\n  structure(list({field_args}), class = c(\"{alias_name}\", \"{union_name}\", \"list\"))\n}}"
1938                                        ))
1939                                    } else {
1940                                        None
1941                                    }
1942                                }
1943                                _ => None,
1944                            })
1945                            .collect();
1946                        (constructors.join("\n"), cont.clone())
1947                    }
1948                    Type::Integer(tint, _) => {
1949                        use crate::components::r#type::tint::Tint;
1950                        let validator = match tint {
1951                            Tint::Val(i) => format!(
1952                                "validate_{name} <- function(x) {{\n  if (!is.integer(x)) stop(\"Validation failed for type {name}: expected int\")\n  if (x != {i}L) stop(\"Validation failed for type {name}: expected literal {i}\")\n  x\n}}"
1953                            ),
1954                            Tint::Unknown => format!(
1955                                "validate_{name} <- function(x) {{\n  if (!is.integer(x)) stop(\"Validation failed for type {name}: expected int\")\n  x\n}}"
1956                            ),
1957                        };
1958                        let constructor = format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
1959                        (format!("{constructor}\n{validator}"), cont.clone())
1960                    }
1961                    Type::Char(tchar, _) => {
1962                        use crate::components::r#type::tchar::Tchar;
1963                        let validator = match tchar {
1964                            Tchar::Val(s) => format!(
1965                                "validate_{name} <- function(x) {{\n  if (!is.character(x)) stop(\"Validation failed for type {name}: expected char\")\n  if (x != '{s}') stop(\"Validation failed for type {name}: expected literal '{s}'\")\n  x\n}}"
1966                            ),
1967                            Tchar::Unknown => format!(
1968                                "validate_{name} <- function(x) {{\n  if (!is.character(x)) stop(\"Validation failed for type {name}: expected char\")\n  x\n}}"
1969                            ),
1970                        };
1971                        let constructor = format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
1972                        (format!("{constructor}\n{validator}"), cont.clone())
1973                    }
1974                    Type::Boolean(tbool, _) => {
1975                        use crate::components::r#type::tbool::Tbool;
1976                        let validator = match tbool {
1977                            Tbool::Val(b) => {
1978                                let r_val = if *b { "TRUE" } else { "FALSE" };
1979                                format!(
1980                                    "validate_{name} <- function(x) {{\n  if (!is.logical(x)) stop(\"Validation failed for type {name}: expected bool\")\n  if (x != {r_val}) stop(\"Validation failed for type {name}: expected literal {r_val}\")\n  x\n}}"
1981                                )
1982                            }
1983                            Tbool::Unknown => format!(
1984                                "validate_{name} <- function(x) {{\n  if (!is.logical(x)) stop(\"Validation failed for type {name}: expected bool\")\n  x\n}}"
1985                            ),
1986                        };
1987                        let constructor = format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
1988                        (format!("{constructor}\n{validator}"), cont.clone())
1989                    }
1990                    Type::Number(tnum, _) => {
1991                        use crate::components::r#type::tnumber::Tnum;
1992                        let validator = match tnum {
1993                            Tnum::Val(v) => format!(
1994                                "validate_{name} <- function(x) {{\n  if (!is.numeric(x)) stop(\"Validation failed for type {name}: expected num\")\n  if (x != {v}) stop(\"Validation failed for type {name}: expected literal {v}\")\n  x\n}}"
1995                            ),
1996                            Tnum::Unknown => format!(
1997                                "validate_{name} <- function(x) {{\n  if (!is.numeric(x)) stop(\"Validation failed for type {name}: expected num\")\n  x\n}}"
1998                            ),
1999                        };
2000                        let constructor = format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
2001                        (format!("{constructor}\n{validator}"), cont.clone())
2002                    }
2003                    Type::Tag(tag_name, inner_type, _) => {
2004                        let body_validation = tag_body_validation(&name, inner_type.as_ref());
2005                        let validator = format!(
2006                            "validate_{name} <- function(x) {{\n  if (x[[1]] != '{tag_name}') stop(\"Validation failed for type {name}: expected tag '{tag_name}'\")\n{body_validation}\n  x\n}}"
2007                        );
2008                        (validator, cont.clone())
2009                    }
2010                    // Alias-to-alias (`type Object <- Circle;`): transparent, so reuse
2011                    // the target's own constructor/validator pipeline under this name
2012                    // instead of generating nothing — otherwise the module export step
2013                    // below (`module$Object <- Object`) would reference a binding that
2014                    // was never created.
2015                    Type::Alias(target_name, ..) => (format!("{name} <- {target_name}"), cont.clone()),
2016                    _ => ("".to_string(), cont.clone()),
2017                };
2018                // RFC-TR-032 parity with `Lang::Let`: `@export` prepends
2019                // `#' @export` so the generated constructor becomes real R
2020                // package API (picked up by roxygen2/NAMESPACE), instead of
2021                // just being visible to other TypR modules like plain `@pub`.
2022                let alias_code = if *is_export && !alias_code.is_empty() {
2023                    format!("#' @export\n{}", alias_code)
2024                } else {
2025                    alias_code
2026                };
2027                (alias_code, alias_cont)
2028            }
2029            Lang::UnionConstructor {
2030                variant_name, fields, ..
2031            } => {
2032                if fields.is_empty() {
2033                    (format!("{}()", variant_name), cont.clone())
2034                } else {
2035                    let (body, current_cont) = Translatable::from(cont.clone()).join_arg_val(fields, ", ").into();
2036                    (format!("{}({})", variant_name, body), current_cont)
2037                }
2038            }
2039            Lang::KeyValue { key: k, value: v, .. } => (format!("{} = {}", k, v.to_r(cont).0), cont.clone()),
2040            Lang::Vector { value: vals, .. } => {
2041                let res = "c(".to_string()
2042                    + &vals
2043                        .iter()
2044                        .map(|x: &Lang| x.to_r(cont).0)
2045                        .collect::<Vec<_>>()
2046                        .join(", ")
2047                    + ")";
2048                (res, cont.to_owned())
2049            }
2050            Lang::Not { value: exp, .. } => (format!("!{}", exp.to_r(cont).0), cont.clone()),
2051            Lang::Sequence { body: vals, .. } => {
2052                let res = if !vals.is_empty() {
2053                    "c(".to_string()
2054                        + &vals
2055                            .iter()
2056                            .map(|x: &Lang| "list(".to_string() + &x.to_r(cont).0 + ")")
2057                            .collect::<Vec<_>>()
2058                            .join(", ")
2059                        + ")"
2060                } else {
2061                    "c(list())".to_string()
2062                };
2063                (res, cont.to_owned())
2064            }
2065            Lang::TestBlock {
2066                value: body,
2067                help_data: h,
2068            } => {
2069                let file_name = h
2070                    .get_file_data()
2071                    .map(|(name, _)| format!("test-{}", name))
2072                    .unwrap_or_else(|| "test-unknown".to_string())
2073                    .replace("TypR/", "")
2074                    .replace(".ty", ".R");
2075
2076                let file_path = format!("tests/testthat/{}", file_name);
2077                let body_str = body.to_r(cont).0;
2078                // RFC-TR-031: bring `@testable` private members of the enclosing
2079                // module into scope (e.g. `sq <- Math$.test_sq`) so the test body
2080                // can call them by their bare names.
2081                let content = if cont.test_preamble.is_empty() {
2082                    body_str
2083                } else {
2084                    format!("{}\n{}", cont.test_preamble.join("\n"), body_str)
2085                };
2086
2087                let _ = write_output_file(&file_path, &content);
2088                ("".to_string(), cont.clone())
2089            }
2090            Lang::JSBlock(exp, _id, _h) => {
2091                let js_cont = Context::default(); //TODO get js context from memory
2092                let res = exp.to_js(&js_cont).0;
2093                (format!("'{}{}'", JS_HEADER, res), cont.clone())
2094            }
2095            Lang::WhileLoop { condition, body, .. } => (
2096                format!("while ({}) {{\n{}\n}}", condition.to_r(cont).0, body.to_r(cont).0),
2097                cont.clone(),
2098            ),
2099            Lang::Loop { body, .. } => (format!("while (TRUE) {{\n{}\n}}", body.to_r(cont).0), cont.clone()),
2100            Lang::Break(_) => ("break".to_string(), cont.clone()),
2101            Lang::Next(_) => ("next".to_string(), cont.clone()),
2102            Lang::NA(_) => ("NA".to_string(), cont.clone()),
2103            Lang::Module {
2104                name,
2105                body,
2106                module_position: position,
2107                config,
2108                ..
2109            } => {
2110                let name_str = if (name == "main") && (config.environment == Environment::Project) {
2111                    "a_main"
2112                } else {
2113                    name
2114                };
2115
2116                // A module that writes its own roxygen2 file (External in Project)
2117                // gets a dedicated frame so the `@include` deps generated inside its
2118                // body land in *its* header rather than the enclosing file's.
2119                let writes_own_file =
2120                    matches!(position, ModulePosition::External) && config.environment == Environment::Project;
2121                if writes_own_file {
2122                    push_include_frame();
2123                    push_import_from_frame();
2124                }
2125
2126                // Use the inner context cached during type-checking when available.
2127                // Fallback: re-run typing() on the module body (only happens if the
2128                // cached context is missing, e.g. in isolated transpile-only calls).
2129                let mut inner_cont = if let Some(cached) = cont.get_module_inner_context(name) {
2130                    // The cached snapshot was taken while typing *this* module's
2131                    // body, mid-compilation — it can't see whole-program registries
2132                    // (record_aliases, the subtype graph) as they stood after later
2133                    // sibling modules were typed (e.g. `Position` declared in a
2134                    // module processed after this one). `cont`, the ambient context
2135                    // at transpile time, has the final, fully-merged picture, so
2136                    // overlay those registries onto the cheap cached snapshot rather
2137                    // than trusting its stale copies.
2138                    let mut cached = cached.clone();
2139                    cached.record_aliases = cont.record_aliases.clone();
2140                    cached.subtypes = cont.subtypes.clone();
2141                    // Same staleness problem for auto-generated structural
2142                    // aliases (`ArrayN`/`RecordN`/…) hoisted into the outer
2143                    // scope from *other* modules after this one was typed —
2144                    // pull those in too so this module's own class chains
2145                    // (`class(x) <- c(..., "Record1", ...)`) still see them.
2146                    cached.typing_context = cached.typing_context.clone().hoist_aliases(&cont.typing_context);
2147                    cached
2148                } else {
2149                    let module_expr = if body.len() > 1 {
2150                        Lang::Lines {
2151                            value: body.to_vec(),
2152                            help_data: HelpData::default(),
2153                        }
2154                    } else {
2155                        body.first().cloned().unwrap_or(Lang::Empty(HelpData::default()))
2156                    };
2157                    typing(&cont.clone().set_in_module_body(), &module_expr).context
2158                };
2159
2160                // RFC-TR-031: in a test build, give any `Test { ... }` block in this
2161                // module access to its `@testable` private members by binding their
2162                // bare names to the exposed `M$.test_<name>` aliases at the top of the
2163                // generated test file (see `Lang::TestBlock` below).
2164                if cont.get_test_mode() {
2165                    let preamble: Vec<String> = body
2166                        .iter()
2167                        .filter_map(|lang| match lang {
2168                            Lang::Let {
2169                                variable: var,
2170                                is_testable: true,
2171                                ..
2172                            } => Var::from_language(*var.clone()).map(|v| {
2173                                let raw = v.get_name();
2174                                format!("{} <- {}$`.test_{}`", raw, name_str, raw)
2175                            }),
2176                            _ => None,
2177                        })
2178                        .collect();
2179                    inner_cont = inner_cont.set_test_preamble(preamble);
2180                }
2181
2182                // C1: partition body into file-level imports (mod foo; / External sub-modules)
2183                // and runtime content. Imports are processed first so their side-effects
2184                // (file writes, register_include) happen before the new.env binding, and
2185                // their output is emitted outside the local({}) block.
2186                let (import_langs, runtime_langs): (Vec<_>, Vec<_>) = body.iter().partition(|lang| {
2187                    matches!(
2188                        lang,
2189                        Lang::ModuleImport { .. }
2190                            | Lang::ImportFrom { .. }
2191                            | Lang::Module {
2192                                module_position: ModulePosition::External,
2193                                ..
2194                            }
2195                    )
2196                });
2197
2198                let imports_parts: Vec<String> = import_langs
2199                    .iter()
2200                    .map(|lang| lang.to_r(&inner_cont).0)
2201                    .filter(|s| !s.is_empty())
2202                    .collect();
2203                let imports_preamble = if imports_parts.is_empty() {
2204                    String::new()
2205                } else {
2206                    imports_parts.join("\n") + "\n"
2207                };
2208
2209                let body_content = runtime_langs
2210                    .iter()
2211                    .map(|lang| lang.to_r(&inner_cont).0)
2212                    .collect::<Vec<_>>()
2213                    .join("\n");
2214
2215                // Build exports (inside local) and generics (outside local) for @pub/@export members
2216                let mut exports: Vec<String> = Vec::new();
2217                let mut generics: Vec<String> = Vec::new();
2218                let mut generic_exports: Vec<String> = Vec::new();
2219                // RFC-TR-032: @export members also get a top-level #' @export re-export
2220                let mut package_exports: Vec<String> = Vec::new();
2221
2222                for lang in body.iter() {
2223                    if let Lang::Let {
2224                        variable: var,
2225                        is_public: true,
2226                        is_export,
2227                        ..
2228                    } = lang
2229                    {
2230                        if let Some(v) = Var::from_language(*var.clone()) {
2231                            let raw_name = v.get_name();
2232                            // Use `inner_cont`, not `cont`: `body_content` above rendered
2233                            // this same `Let` via `inner_cont` (the re-typed module-body
2234                            // context), so `display_type`'s alias lookup must use the same
2235                            // context here or it can resolve the same structural type to a
2236                            // *different* auto-named alias (e.g. the function gets defined
2237                            // as `animate_move.Record4` but exported/registered as
2238                            // `animate_move.Record1` — a dangling reference at R runtime).
2239                            let typed_name = v.clone().display_type(&inner_cont).get_name();
2240
2241                            // Export the (possibly type-suffixed) member into the module env
2242                            exports.push(format!("{}${} <- {}", name_str, typed_name, typed_name));
2243
2244                            // RFC-TR-032: @export also surfaces as a package-level function
2245                            if *is_export {
2246                                package_exports
2247                                    .push(format!("#' @export\n{} <- {}${}", raw_name, name_str, typed_name));
2248                            }
2249
2250                            // For typed functions: register as S3 method and create generic
2251                            let var_type = v.get_type();
2252                            if !var_type.is_empty() && typed_name != raw_name {
2253                                let class_name = inner_cont.get_class_unquoted(&var_type);
2254                                exports.push(format!(
2255                                    "registerS3method(\"{}\", \"{}\", {})",
2256                                    raw_name, class_name, typed_name
2257                                ));
2258
2259                                let generic_def =
2260                                    format!("{} <- function(x, ...) UseMethod(\"{}\")", raw_name, raw_name);
2261                                if !generics.contains(&generic_def) {
2262                                    generics.push(generic_def);
2263                                    generic_exports.push(format!("{}${} <- {}", name_str, raw_name, raw_name));
2264                                }
2265
2266                                // The method implementation itself (e.g. `do.Object`) is only
2267                                // bound inside `local({...})`, so it never becomes a top-level
2268                                // binding that load_module.R's dependency-copy step can see.
2269                                // `registerS3method` alone doesn't help here either: it relies
2270                                // on a real package namespace, which a `sys.source`'d module env
2271                                // is not. Re-expose it at top level (outside local) from the
2272                                // module env so UseMethod can find `typed_name` via normal
2273                                // lexical scoping from any file that imports this module.
2274                                generic_exports.push(format!("{} <- {}${}", typed_name, name_str, typed_name));
2275
2276                                // Same gap as above, but for the `.default` fallback (soundness_plan.md
2277                                // D.2): the `Lang::Let` arm above emits `raw_name.default <-
2278                                // typed_name` right next to `typed_name` itself whenever the dispatch
2279                                // param is a pure interface or a `Foreign<T>`-family alias (see the
2280                                // matching condition there) — that binding is just as local-only as
2281                                // `typed_name`, so it needs the identical export + re-expose treatment
2282                                // or it's unreachable from outside the module (a real foreign value's
2283                                // runtime class never matches `typed_name`'s suffix, so calls from
2284                                // outside this module can only ever reach it via `.default`).
2285                                let is_foreign_dispatch = matches!(&var_type, Type::Alias(alias_name, _, _, _) if inner_cont.resolves_to_foreign_alias(alias_name));
2286                                if class_name != "default"
2287                                    && (is_foreign_dispatch
2288                                        || (facets::interface_facet(&inner_cont, &var_type).is_some()
2289                                            && facets::record_facet(&inner_cont, &var_type).is_none()))
2290                                {
2291                                    exports.push(format!("{}${}.default <- {}.default", name_str, raw_name, raw_name));
2292                                    generic_exports
2293                                        .push(format!("{}.default <- {}${}.default", raw_name, name_str, raw_name));
2294                                }
2295                            }
2296                        }
2297                    }
2298                    // A *private* typed function is also emitted as an S3 method
2299                    // (`snapshot.StoryBoard`), and its call sites transpile to the
2300                    // bare generic (`snapshot(...)`) — so a generic stub must
2301                    // exist for them too. It stays inside `local({...})`: the
2302                    // sibling methods' closures resolve it lexically, and nothing
2303                    // leaks into the module env or the top level.
2304                    if let Lang::Let {
2305                        variable: var,
2306                        is_public: false,
2307                        ..
2308                    } = lang
2309                    {
2310                        if let Some(v) = Var::from_language(*var.clone()) {
2311                            let raw_name = v.get_name();
2312                            let typed_name = v.clone().display_type(&inner_cont).get_name();
2313                            let var_type = v.get_type();
2314                            if !var_type.is_empty() && typed_name != raw_name {
2315                                let class_name = inner_cont.get_class_unquoted(&var_type);
2316                                let generic_def =
2317                                    format!("{} <- function(x, ...) UseMethod(\"{}\")", raw_name, raw_name);
2318                                if !generics.contains(&generic_def) && !exports.contains(&generic_def) {
2319                                    exports.push(generic_def);
2320                                }
2321                                exports.push(format!(
2322                                    "registerS3method(\"{}\", \"{}\", {})",
2323                                    raw_name, class_name, typed_name
2324                                ));
2325                            }
2326                        }
2327                    }
2328                    // RFC-TR-032: in a test build, expose `@testable` (and implied-testable
2329                    // `@pub`/`@export`) members as `M$.test_<name>` while keeping them
2330                    // private in the module's regular API.
2331                    if cont.get_test_mode() {
2332                        if let Lang::Let {
2333                            variable: var,
2334                            is_testable: true,
2335                            ..
2336                        } = lang
2337                        {
2338                            if let Some(v) = Var::from_language(*var.clone()) {
2339                                let raw_name = v.get_name();
2340                                // Reference the actual (possibly type-suffixed)
2341                                // binding emitted in the module body. Must use
2342                                // `inner_cont` for the same reason as above.
2343                                let typed_name = v.clone().display_type(&inner_cont).get_name();
2344                                exports.push(format!("{}$`.test_{}` <- {}", name_str, raw_name, typed_name));
2345                            }
2346                        }
2347                    }
2348                    // Export @pub opaque type constructors into the module environment.
2349                    // Interfaces are compile-time-only structural validators (see CLAUDE.md
2350                    // "Interface Constructors") and generate no R binding at all, so exporting
2351                    // them here would reference an undefined variable (e.g. `object$Animable <-
2352                    // Animable` with `Animable` never assigned).
2353                    if let Lang::Alias {
2354                        identifier: var,
2355                        is_public: true,
2356                        is_export,
2357                        target_type,
2358                        ..
2359                    } = lang
2360                    {
2361                        if !target_type.is_interface() {
2362                            if let Some(v) = Var::from_language(*var.clone()) {
2363                                let alias_name = v.get_name();
2364                                exports.push(format!("{}${} <- {}", name_str, alias_name, alias_name));
2365
2366                                // RFC-TR-032 parity: @export type aliases also get a
2367                                // top-level package-level re-export, same as @export
2368                                // functions above.
2369                                if *is_export {
2370                                    package_exports
2371                                        .push(format!("#' @export\n{} <- {}${}", alias_name, name_str, alias_name));
2372                                }
2373                            }
2374                        }
2375                    }
2376                }
2377
2378                let exports_str = if exports.is_empty() {
2379                    String::new()
2380                } else {
2381                    "\n".to_string() + &exports.join("\n")
2382                };
2383
2384                let generics_defs_str = if generics.is_empty() {
2385                    String::new()
2386                } else {
2387                    generics.join("\n") + "\n"
2388                };
2389
2390                let generic_exports_str = if generic_exports.is_empty() {
2391                    String::new()
2392                } else {
2393                    "\n".to_string() + &generic_exports.join("\n")
2394                };
2395
2396                let package_exports_str = if package_exports.is_empty() {
2397                    String::new()
2398                } else {
2399                    "\n".to_string() + &package_exports.join("\n")
2400                };
2401
2402                let content = format!(
2403                    "{}{}{} <- new.env(parent = emptyenv())\nlocal({{\n{}{}\n}}){}{}",
2404                    generics_defs_str,
2405                    imports_preamble,
2406                    name_str,
2407                    body_content,
2408                    exports_str,
2409                    generic_exports_str,
2410                    package_exports_str
2411                );
2412
2413                match (position, config.environment) {
2414                    (ModulePosition::Internal, _) => (content, cont.clone()),
2415                    // In WASM mode, inline all external modules instead of writing files
2416                    (ModulePosition::External, Environment::Wasm) => {
2417                        let file_path = format!("{}.R", name_str);
2418                        let _ = write_output_file(&file_path, &content);
2419                        (content, cont.clone())
2420                    }
2421                    (ModulePosition::External, Environment::StandAlone)
2422                    | (ModulePosition::External, Environment::Repl) => {
2423                        let file_path = format!("{}.R", name_str);
2424                        let _ = write_output_file(&file_path, &content);
2425                        (format!("source('{}')", file_path), cont.clone())
2426                    }
2427                    (ModulePosition::External, Environment::Project) => {
2428                        let file_path = format!("R/{}.R", name_str);
2429                        // Drain the deps collected within this module's body and emit
2430                        // them as top-level `@include` tags in this file's header.
2431                        let nested = pop_include_frame();
2432                        let nested_includes = nested
2433                            .iter()
2434                            .map(|f| format!("#' @include {}\n", f))
2435                            .collect::<String>();
2436                        let nested_imports = pop_import_from_frame()
2437                            .iter()
2438                            .map(|e| format!("#' @importFrom {}\n", e))
2439                            .collect::<String>();
2440                        let project_preamble =
2441                            "#' @include std.R\n#' @include generic_functions.R\n#' @include types.R\n";
2442                        let _ = write_output_file(
2443                            &file_path,
2444                            &format!("{}{}{}{}", project_preamble, nested_includes, nested_imports, content),
2445                        );
2446                        // The enclosing file depends on this one: hoist the tag to its
2447                        // header instead of emitting it inline inside a `local({...})`.
2448                        register_include(&format!("{}.R", name_str));
2449                        (String::new(), cont.clone())
2450                    }
2451                }
2452            }
2453            Lang::UseModule {
2454                module_path, selector, ..
2455            } => {
2456                use crate::components::language::use_lang::UseSelector;
2457
2458                // Build the R accessor prefix: A::B::C → A$B$C
2459                let r_path = module_path.join("$");
2460
2461                // Resolve the module type from context to enumerate public members for wildcards
2462                let mod_type_opt = (|| {
2463                    let root = cont.get_type_from_variable(&Var::from_name(&module_path[0])).ok()?;
2464                    let mut current = root;
2465                    for seg in module_path.iter().skip(1) {
2466                        current = current.to_module_type().ok()?.get_type_from_name(seg).ok()?;
2467                    }
2468                    current.to_module_type().ok()
2469                })();
2470
2471                let bindings: Vec<String> = match selector {
2472                    UseSelector::Wildcard => mod_type_opt
2473                        .map(|mt| {
2474                            mt.get_public_members()
2475                                .iter()
2476                                .map(|m| {
2477                                    let name = m.get_argument_str();
2478                                    format!("{} <- {}${}", name, r_path, name)
2479                                })
2480                                .collect()
2481                        })
2482                        .unwrap_or_default(),
2483                    UseSelector::Items(items) => items
2484                        .iter()
2485                        .map(|item| {
2486                            let local_name = item.alias.as_deref().unwrap_or(&item.name);
2487                            format!("{} <- {}${}", local_name, r_path, item.name)
2488                        })
2489                        .collect(),
2490                };
2491
2492                (bindings.join("\n"), cont.clone())
2493            }
2494            Lang::ModuleImport { .. } => ("".to_string(), cont.clone()),
2495            Lang::ImportFrom { package, functions, .. } => {
2496                let entry = format!("{} {}", package, functions.join(" "));
2497                register_import_from(&entry);
2498                ("".to_string(), cont.clone())
2499            }
2500            // `Self:{ field = expr, ...base }` (generic_constructor.md §5):
2501            // resolve the actual R constructor straight from `base`'s type
2502            // rather than from the literal name "Self" — by the
2503            // type-checking rule `base`'s type is always Self or a subtype
2504            // of it, so this is exactly the constructor that should run.
2505            Lang::ConstructorCall {
2506                type_name,
2507                fields,
2508                spreads,
2509                ..
2510            } if type_name == "Self" => {
2511                let base_typ = spreads
2512                    .first()
2513                    .map(|e| typing(cont, e).value)
2514                    .unwrap_or_else(|| Type::Any(HelpData::default()));
2515                let resolved_name = match &base_typ {
2516                    Type::Alias(alias_name, ..) => cont
2517                        .aliases()
2518                        .find(|(var, _)| var.get_name() == *alias_name)
2519                        .map(|(_, t)| matches!(t, Type::Record(_, _)))
2520                        .unwrap_or(false)
2521                        .then(|| alias_name.clone()),
2522                    _ => None,
2523                };
2524                match (resolved_name, spreads.first()) {
2525                    (Some(name), Some(spread_expr)) => {
2526                        // Same codegen as the plain runtime-spread
2527                        // ConstructorCall path below, with the resolved
2528                        // alias name substituted for "Self".
2529                        let (spread_r, current_cont) = spread_expr.to_r(cont);
2530                        let (body, current_cont) = if fields.is_empty() {
2531                            (format!(".spread = {}", spread_r), current_cont)
2532                        } else {
2533                            let (overrides, next_cont) =
2534                                Translatable::from(current_cont).join_arg_val(fields, ", ").into();
2535                            (format!("{}, .spread = {}", overrides, spread_r), next_cont)
2536                        };
2537                        (format!("{}({})", name, body), current_cont)
2538                    }
2539                    (None, Some(spread_expr)) => {
2540                        // No nominal constructor to call: fall back to the
2541                        // generic spread()-merge used for plain `{ f=e, ...x }`
2542                        // record literals.
2543                        let (base, current_cont) = spread_expr.to_r(cont);
2544                        if fields.is_empty() {
2545                            (base, current_cont)
2546                        } else {
2547                            let (overrides, current_cont) =
2548                                Translatable::from(current_cont).join_arg_val(fields, ", ").into();
2549                            (format!("spread({}, list({}))", base, overrides), current_cont)
2550                        }
2551                    }
2552                    // No spread: ill-typed (type-checking already reports
2553                    // SelfOutsideContext); nothing sensible to emit.
2554                    (_, None) => ("NULL".to_string(), cont.clone()),
2555                }
2556            }
2557            Lang::ConstructorCall {
2558                module_path,
2559                type_name,
2560                fields,
2561                spreads,
2562                ..
2563            } if !spreads.is_empty() => {
2564                // Single runtime `...source` spread (spread_operator3.md): pass the
2565                // explicit fields plus the spread source straight to the generated
2566                // constructor's `.spread` parameter — it merges them at runtime via
2567                // `typr_spread_record` (explicit fields win, extra fields on the
2568                // source are preserved, unlike the old do.call/spread/select).
2569                let spread_expr = spreads.first().expect("checked non-empty above");
2570                let (spread_r, current_cont) = spread_expr.to_r(cont);
2571                let qualified = if module_path.is_empty() {
2572                    type_name.clone()
2573                } else {
2574                    format!("{}${}", module_path.join("$"), type_name)
2575                };
2576                let (body, current_cont) = if fields.is_empty() {
2577                    (format!(".spread = {}", spread_r), current_cont)
2578                } else {
2579                    let (overrides, next_cont) = Translatable::from(current_cont).join_arg_val(fields, ", ").into();
2580                    (format!("{}, .spread = {}", overrides, spread_r), next_cont)
2581                };
2582                (format!("{}({})", qualified, body), current_cont)
2583            }
2584            Lang::ConstructorCall {
2585                module_path,
2586                type_name,
2587                fields,
2588                spread,
2589                help_data: h,
2590                ..
2591            } => {
2592                // With a spread present, statically expand every record field absent
2593                // from `fields` into a `source$field` access (RFC-TR-033 §5: static
2594                // expansion, no runtime merge so the record's R class is preserved).
2595                let all_fields: Vec<ArgumentValue> = match spread {
2596                    Some((spread_path, spread_var, _)) => {
2597                        let resolved_alias = if module_path.is_empty() {
2598                            cont.get_type_from_aliases(&Var::from_name(type_name))
2599                        } else {
2600                            resolve_module_member_type(cont, module_path, type_name)
2601                        };
2602                        let record_fields = resolved_alias.and_then(|t| match t.reduce(cont) {
2603                            Type::Record(fields, _) => Some(fields),
2604                            _ => None,
2605                        });
2606                        let receiver = {
2607                            let qualifier = spread_path.split_first().map(|(first, rest)| {
2608                                rest.iter()
2609                                    .fold(Var::from_name(first).to_language(), |acc, seg| Lang::Operator {
2610                                        operator: Op::Dollar(h.clone()),
2611                                        rhs: Box::new(acc),
2612                                        lhs: Box::new(Var::from_name(seg).to_language()),
2613                                        help_data: h.clone(),
2614                                    })
2615                            });
2616                            match qualifier {
2617                                Some(qualifier) => Lang::Operator {
2618                                    operator: Op::Dollar(h.clone()),
2619                                    rhs: Box::new(qualifier),
2620                                    lhs: Box::new(Var::from_name(spread_var).to_language()),
2621                                    help_data: h.clone(),
2622                                },
2623                                None => Var::from_name(spread_var).to_language(),
2624                            }
2625                        };
2626                        let provided: std::collections::HashSet<String> =
2627                            fields.iter().map(|f| f.get_argument()).collect();
2628                        let synthetic = record_fields
2629                            .into_iter()
2630                            .flatten()
2631                            .filter(|rf| !provided.contains(&rf.get_argument_str()))
2632                            .map(|rf| {
2633                                let field_access = Lang::Operator {
2634                                    operator: Op::Dollar(h.clone()),
2635                                    rhs: Box::new(receiver.clone()),
2636                                    lhs: Box::new(Var::from_name(&rf.get_argument_str()).to_language()),
2637                                    help_data: h.clone(),
2638                                };
2639                                ArgumentValue(rf.get_argument_str(), field_access)
2640                            });
2641                        fields.iter().cloned().chain(synthetic).collect()
2642                    }
2643                    None => fields.clone(),
2644                };
2645                let (body, current_cont) = Translatable::from(cont.clone()).join_arg_val(&all_fields, ", ").into();
2646                let qualified = if module_path.is_empty() {
2647                    type_name.clone()
2648                } else {
2649                    format!("{}${}", module_path.join("$"), type_name)
2650                };
2651                (format!("{}({})", qualified, body), current_cont)
2652            }
2653            Lang::ArrayConstructorCall {
2654                type_name,
2655                elements,
2656                help_data: h,
2657            } => {
2658                let resolved_alias = cont
2659                    .get_type_from_aliases(&Var::from_name(type_name))
2660                    .map(|t| t.reduce(cont));
2661                let is_atomic_repr = match &resolved_alias {
2662                    Some(Type::Vec(vt, _, _, _)) if vt.is_vector() => true,
2663                    // Step ③ (unification_arrays.md): primitive-element array
2664                    // aliases share the bare-atomic representation.
2665                    Some(t @ Type::Vec(_, _, _, _)) => cont.atomic_array_elem(t).is_some(),
2666                    _ => false,
2667                };
2668                if is_atomic_repr {
2669                    // Plain vector alias (`type V <- Vec[#N, T]`): the runtime
2670                    // value is a bare R vector (no `dim`/`typed_vec` wrapper, see
2671                    // the Vec constructor pipeline in `Lang::Alias`), so the
2672                    // elements are simply collected with `c(...)`.
2673                    let inner = elements.iter().map(|el| el.to_r(cont).0).collect::<Vec<_>>().join(", ");
2674                    (format!("{}(c({}))", type_name, inner), cont.clone())
2675                } else {
2676                    let temp_array = Lang::Array {
2677                        value: elements.clone(),
2678                        help_data: h.clone(),
2679                    };
2680                    let typ = temp_array.typing(cont).value;
2681                    let dimension = ArrayType::try_from(typ)
2682                        .expect("array constructor call should have an array type")
2683                        .get_shape()
2684                        .map(|sha| format!("c({})", sha))
2685                        .unwrap_or_else(|| "c(0)".to_string());
2686                    let lin_array = temp_array
2687                        .linearize_array()
2688                        .iter()
2689                        .map(|lang| lang.to_r(cont).0)
2690                        .collect::<Vec<_>>()
2691                        .join(", ");
2692                    let inner = if lin_array.is_empty() {
2693                        format!("typed_vec(dim = {})", dimension)
2694                    } else {
2695                        format!("typed_vec({}, dim = {})", lin_array, dimension)
2696                    };
2697                    (format!("{}({})", type_name, inner), cont.clone())
2698                }
2699            }
2700            Lang::Import { .. } | Lang::Test { .. } | Lang::Use { .. } => ("".to_string(), cont.clone()),
2701            // `PartialApp` is desugared into a `Lang::Function` during typing and
2702            // normally never reaches transpilation — except when an enclosing
2703            // node re-transpiles its *original* parsed body (the `Lang::Module`
2704            // arm above renders `body.iter()`, not the typed AST). Desugar on
2705            // the fly here; the guard keeps a failed desugaring (unknown
2706            // function) from recursing back into this arm.
2707            Lang::PartialApp { .. } => {
2708                let typed = typing(cont, self).lang;
2709                if matches!(typed, Lang::PartialApp { .. }) {
2710                    ("".to_string(), cont.clone())
2711                } else {
2712                    typed.to_r(cont)
2713                }
2714            }
2715            Lang::ValidatingCast {
2716                expression,
2717                type_name,
2718                literal_type,
2719                ..
2720            } => {
2721                // An array literal under a cast emits its raw typed_vec: the
2722                // cast supplies the annotation, and the literal's own (based
2723                // on its inferred type, e.g. `[0, Empty]` for `[]`) would be
2724                // a redundant — usually unregistered → `as.Generic()` — cast.
2725                // When the cast *target* is an atomic-representation array
2726                // (step ③), the literal must take the atomic shape driven by
2727                // the target's element type — an empty literal's own inferred
2728                // type (`[0, Empty]`) would otherwise pick `typed_vec`.
2729                let expr_r = if matches!(expression.as_ref(), Lang::Array { .. }) {
2730                    match literal_type.as_ref().and_then(|t| cont.atomic_array_elem(t)) {
2731                        Some(elem) => atomic_array_literal(expression, cont, &elem),
2732                        None => array_literal_raw(expression, cont),
2733                    }
2734                } else {
2735                    expression.to_r(cont).0
2736                };
2737                match literal_type {
2738                    // Inline structural type (`as! [Any, int]` and friends):
2739                    // call the auto-generated `as.ArrayN`-style cast (see
2740                    // `Context::get_type_anotations`) registered for it at
2741                    // typing time, instead of a named `validate_<name>`.
2742                    Some(t) => (format!("{} |> {}", expr_r, cont.get_type_anotation(t)), cont.clone()),
2743                    None => (format!("validate_{}({})", type_name, expr_r), cont.clone()),
2744                }
2745            }
2746            _ => ("".to_string(), cont.clone()),
2747        };
2748
2749        // --checked (soundness_transpilation.md Phase A): assert the class
2750        // vector a constructor call produces matches its declared type.
2751        // Single insertion point covering all three `Lang::ConstructorCall`
2752        // arms above (Self-spread, runtime-spread, plain) rather than
2753        // touching each one — `self.typing(cont)` mirrors the existing
2754        // `Lang::Array` arm's own re-typing for its output annotation.
2755        let result = if cont.get_checked_mode() {
2756            if let Lang::ConstructorCall { type_name, .. } = self {
2757                let (r_code, r_cont) = result;
2758                let typ = self.typing(cont).value;
2759                let loc = self.get_help_data();
2760                let what = format!("constructor {}", type_name);
2761                let wrapped = checked_assertions::wrap_checked(&r_cont, r_code, &typ, &loc, &what);
2762                (wrapped, r_cont)
2763            } else {
2764                result
2765            }
2766        } else {
2767            result
2768        };
2769
2770        result
2771    }
2772}
2773
2774#[cfg(test)]
2775mod tests {
2776    use crate::components::context::config::{Config, Environment};
2777    use crate::components::context::Context;
2778    use crate::components::error_message::help_data::HelpData;
2779    use crate::components::language::{Lang, ModulePosition};
2780    use crate::processes::transpiling::translatable::RTranslatable;
2781    use crate::utils::fluent_parser::FluentParser;
2782
2783    #[test]
2784    fn test_escape_r_string() {
2785        use super::escape_r_string;
2786        assert_eq!(escape_r_string("hello"), r#""hello""#);
2787        assert_eq!(escape_r_string(r#"say "hi""#), r#""say \"hi\"""#);
2788        assert_eq!(escape_r_string(r"a\b"), r#""a\\b""#);
2789        assert_eq!(escape_r_string("line1\nline2"), r#""line1\nline2""#);
2790    }
2791
2792    /// Interfaces-as-classes + `.default` fallback (hybrid dispatch model).
2793    /// These go through `parse2` + `TypeChecker` (not `FluentParser`) because
2794    /// the interface-satisfaction check runs against the *final* whole-program
2795    /// context — exactly what `TypeChecker::transpile` provides and what
2796    /// per-statement `FluentParser::run()` chains do not.
2797    fn transpile_program(stmts: &[&str]) -> String {
2798        use crate::processes::parsing::parse2;
2799        use crate::processes::type_checking::type_checker::TypeChecker;
2800        let tc = stmts.iter().fold(TypeChecker::new(Context::default()), |tc, s| {
2801            let code = parse2((*s).into()).unwrap();
2802            tc.typing_no_panic(&code)
2803        });
2804        assert!(!tc.has_errors(), "type errors: {:?}", tc.get_errors());
2805        tc.transpile()
2806    }
2807
2808    #[test]
2809    fn test_record_class_chain_includes_satisfied_interface() {
2810        // `view` is declared *after* `Point`: satisfaction must still be
2811        // seen, since transpiling runs on the final context.
2812        let r = transpile_program(&[
2813            "type Viewable <- interface { view: (Self) -> char };",
2814            "type Point <- list { x: int };",
2815            "let view <- fn(p: Point): char { \"pt\" };",
2816            "let describe <- fn(v: Viewable): char { view(v) };",
2817        ]);
2818        assert!(
2819            r.contains("class(x) <- c(\"Point\", \"Viewable\", \"list\")"),
2820            "expected Viewable in Point's class chain, got: {r}"
2821        );
2822    }
2823
2824    #[test]
2825    fn test_pure_interface_param_function_emits_default_fallback() {
2826        // Primitives and foreign values never carry interface classes, so a
2827        // pure-interface method must also register as `.default`.
2828        let r = transpile_program(&[
2829            "type Incrementable <- interface { incr: (Self) -> Self };",
2830            "let incr <- fn(s: int): int { s + 1 };",
2831            "let double_up <- fn(i: Incrementable): Incrementable { i.incr() };",
2832        ]);
2833        assert!(
2834            r.contains("`double_up.default` <- `double_up.Incrementable`"),
2835            "expected .default fallback alias, got: {r}"
2836        );
2837    }
2838
2839    #[test]
2840    fn test_any_first_param_default_name_backticks_wrap_whole_name() {
2841        // `Type::Any` is left unsuffixed by `display_type`, so `.default`
2842        // must be appended before backtick-wrapping, not after: the
2843        // generated name must be `` `describe.default` ``, never
2844        // `` `describe`.default `` (which R would parse as `describe`
2845        // followed by an invalid bare `.default` member access).
2846        let r = transpile_program(&["let describe <- fn(x: Any): char { \"any\" };"]);
2847        assert!(
2848            r.contains("`describe.default` <-"),
2849            "expected backticks around the whole `name.default`, got: {r}"
2850        );
2851        assert!(
2852            !r.contains("`describe`.default"),
2853            "backticks must not wrap only the base name, got: {r}"
2854        );
2855    }
2856
2857    #[test]
2858    fn test_generic_function_own_type_has_no_self_cast() {
2859        // soundness_plan.md D.3 (cases/0042): a top-level generic function's
2860        // own type (e.g. `(T) -> T`) still contains an unresolved generic,
2861        // so `Context::get_type_anotations()` never emits an `as.FunctionN`
2862        // definition for its auto-registered structural alias. The transpile
2863        // site must not call that never-emitted cast either.
2864        let r_code = FluentParser::new()
2865            .push("let id <- fn(x: T): T { x };")
2866            .run()
2867            .get_r_code()
2868            .iter()
2869            .cloned()
2870            .collect::<Vec<_>>()
2871            .join("\n");
2872        assert!(
2873            !r_code.contains("as.Function"),
2874            "generic function must not reference an unemitted as.FunctionN self-cast: {}",
2875            r_code
2876        );
2877    }
2878
2879    #[test]
2880    fn test_module_foreign_dispatch_default_exported_outside_local() {
2881        // soundness_plan.md D.2: a @pub fn inside a module whose dispatch
2882        // param is a Foreign<T>-family alias emits a `describe.default <-
2883        // describe.Foreign0` fallback binding (same mechanism as
2884        // `test_pure_interface_param_function_emits_default_fallback`), but
2885        // that binding lives inside the module's `local({...})` block. It
2886        // must be re-exported the same way `typed_name` already is, or it's
2887        // unreachable from any file outside the module — real foreign
2888        // values never carry the synthetic `.Foreign0` suffix, so `.default`
2889        // is the only method that could ever catch them.
2890        let r_code = FluentParser::new()
2891            .push("type LmModel <- Foreign<Any>;")
2892            .run()
2893            .push("module Reporter { @pub let describe <- fn(x: LmModel): int { 1 }; };")
2894            .run()
2895            .get_r_code()
2896            .iter()
2897            .cloned()
2898            .collect::<Vec<_>>()
2899            .join("\n");
2900        assert!(
2901            r_code.contains("Reporter$describe.default <- describe.default"),
2902            "missing module-env .default export: {}",
2903            r_code
2904        );
2905        assert!(
2906            r_code.contains("describe.default <- Reporter$describe.default"),
2907            "missing top-level .default re-export: {}",
2908            r_code
2909        );
2910    }
2911
2912    #[test]
2913    fn test_mixed_intersection_alias_tags_satisfier_but_no_default() {
2914        // A record & interface intersection alias: satisfying records get the
2915        // alias in their class chain; no `.default` is emitted since only
2916        // records (which carry the class) can satisfy the record facet.
2917        let r = transpile_program(&[
2918            "type Combined <- list { y: int } & interface { show: (Self) -> char };",
2919            "type Widget <- list { y: int, label: char };",
2920            "let show <- fn(w: Widget): char { \"ws\" };",
2921            "let inspect <- fn(c: Combined): char { show(c) };",
2922        ]);
2923        assert!(
2924            r.contains("class(x) <- c(\"Widget\", \"Combined\", \"list\")"),
2925            "expected Combined in Widget's class chain, got: {r}"
2926        );
2927        assert!(
2928            !r.contains("inspect.default"),
2929            "mixed intersection param must not emit a .default fallback, got: {r}"
2930        );
2931    }
2932
2933    #[test]
2934    fn test_validating_cast_transpiles_to_validate_call() {
2935        let r_code = FluentParser::new()
2936            .push("type Person <- list { name: char, age: int };")
2937            .run()
2938            .check_transpiling("x as! Person");
2939        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2940        assert!(
2941            r_str.contains("validate_Person(x)"),
2942            "expected validate_Person(x), got: {}",
2943            r_str
2944        );
2945    }
2946
2947    #[test]
2948    fn test_validating_cast_type_is_alias() {
2949        let typ = FluentParser::new()
2950            .push("type Person <- list { name: char, age: int };")
2951            .run()
2952            .check_typing("x as! Person");
2953        assert!(
2954            typ.pretty2().contains("Person"),
2955            "expected Alias(Person), got: {}",
2956            typ.pretty2()
2957        );
2958    }
2959
2960    #[test]
2961    fn test_validating_cast_literal_array_type() {
2962        // Step ③ (unification_arrays.md): `[Any, int]` is an
2963        // atomic-representation array — no `as.ArrayN` class is applied.
2964        let r_code = FluentParser::new().check_transpiling("c() as! [Any, int]");
2965        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2966        assert!(
2967            r_str.contains("c() |> identity()"),
2968            "expected c() |> identity(), got: {}",
2969            r_str
2970        );
2971    }
2972
2973    #[test]
2974    fn test_validating_cast_literal_vec_and_array_keywords() {
2975        // `Vec[...]` / `Array[...]` prefixes are equivalent to the bare
2976        // `[...]` form for an inline (non-aliased) cast target.
2977        let r_code = FluentParser::new().check_transpiling("c() as! Vec[Any, int]");
2978        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2979        assert!(
2980            r_str.contains("c() |> identity()"),
2981            "expected c() |> identity(), got: {}",
2982            r_str
2983        );
2984    }
2985
2986    #[test]
2987    fn test_validating_cast_array_literal_single_annotation() {
2988        // `[] as! [T]`: the literal's own annotation (its inferred type
2989        // `[0, Empty]` has no registered alias → `as.Generic()`) must be
2990        // suppressed; the cast provides the only annotation. Step ③: the
2991        // atomic cast target drives the empty literal to the typed empty
2992        // vector (`integer(0)`), never `typed_vec`/`c()` (`c()` is NULL).
2993        let r_code = FluentParser::new().check_transpiling("[] as! [Any, int]");
2994        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2995        assert!(
2996            r_str.contains("integer(0) |> identity()"),
2997            "expected integer(0) |> identity(), got: {}",
2998            r_str
2999        );
3000        assert!(
3001            !r_str.contains("as.Generic"),
3002            "no as.Generic() should be emitted, got: {}",
3003            r_str
3004        );
3005    }
3006
3007    #[test]
3008    fn test_validating_cast_in_constructor_field_registers_alias() {
3009        // The ArrayN alias registered by an `as!` cast inside a constructor
3010        // call field must survive to transpile time (the ConstructorCall
3011        // typing arm used to drop the field-typing sub-context, so the
3012        // lookup fell back to as.Generic()).
3013        let r_code = FluentParser::new()
3014            .push("type Truc <- list { options: [Option] };")
3015            .run()
3016            .push("let new_truc <- Truc:{ options = [] as! [Option] };")
3017            .run()
3018            .get_r_code();
3019        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3020        assert!(
3021            r_str.contains("typed_vec(dim = c(0)) |> as.Array0()"),
3022            "expected the cast to resolve to as.Array0(), got: {}",
3023            r_str
3024        );
3025        assert!(
3026            !r_str.contains("as.Generic"),
3027            "no as.Generic() should be emitted, got: {}",
3028            r_str
3029        );
3030    }
3031
3032    #[test]
3033    fn test_validating_cast_literal_type_dedup() {
3034        // Two casts to the same structural type reuse the same auto-generated
3035        // alias instead of registering a new one each time.
3036        let r_code = FluentParser::new()
3037            .push("let a <- c() as! [Any, int];")
3038            .run()
3039            .push("let b <- c() as! [Any, int];")
3040            .run()
3041            .get_r_code();
3042        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3043        // Step ③: atomic-representation targets annotate with `identity()`;
3044        // the shared registration is no longer visible in the emitted R,
3045        // but both casts must agree on the same (atomic) shape.
3046        assert_eq!(
3047            r_str.matches("|> identity()").count(),
3048            2,
3049            "expected both casts to emit identity(), got: {}",
3050            r_str
3051        );
3052    }
3053
3054    #[test]
3055    fn test_alias_record_generates_validator() {
3056        let r_code = FluentParser::new().check_transpiling("type Person <- list { name: char, age: int };");
3057        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3058        assert!(
3059            r_str.contains("validate_Person <- function(x)"),
3060            "expected validator function, got: {}",
3061            r_str
3062        );
3063        assert!(
3064            r_str.contains("required_fields"),
3065            "expected field validation, got: {}",
3066            r_str
3067        );
3068    }
3069
3070    #[test]
3071    fn test_record_kinded_generic_param_transpiles_without_panic() {
3072        // Regression for a `get_class` panic ("%T has no class equivalent")
3073        // when a function parameter/alias is typed with a record-kinded
3074        // generic (`%T`), e.g. `type Animator<%T> <- %T & list {...}` plus
3075        // a function spreading a `%T`-typed parameter. `%T` has no fixed
3076        // runtime R class, so it must fall back to the same `Generic`
3077        // convention used for plain `T`, never panic.
3078        let fp = FluentParser::new()
3079            .push("type Animator<%T> <- %T & list { extra: int };")
3080            .run()
3081            .push("let combine <- fn(target: %T): %T { let more <- :{ extra = 1 }; :{ ...target, ...more } };")
3082            .run();
3083        assert_eq!(fp.get_last_log(), "The logs are empty");
3084        let r_code = fp.get_r_code().iter().cloned().collect::<Vec<_>>().join("\n");
3085        assert!(
3086            r_code.contains("spread(target, more)"),
3087            "expected the spread merge to transpile, got:\n{}",
3088            r_code
3089        );
3090    }
3091
3092    #[test]
3093    fn test_generic_intersection_alias_generates_validator() {
3094        // An alias mixing a record-kinded generic with a concrete record
3095        // (`%T & list {...}`) used to produce no R code at all for itself
3096        // (it fell into the union-alias `Type::Operator` catch-all), while
3097        // `as! Animator` elsewhere still emitted a call to `validate_Animator`
3098        // — a dangling reference at actual R runtime. The concrete side's
3099        // fields are the only part with a fixed runtime shape, so the
3100        // constructor/annotator/validator pipeline should be generated from
3101        // them, exactly like a plain `Type::Record` alias.
3102        let r_code = FluentParser::new().check_transpiling("type Animator<%T> <- %T & list { animations: int };");
3103        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3104        assert!(
3105            r_str.contains("Animator <- function(animations, .spread = NULL)"),
3106            "expected a constructor for the concrete side's fields, got:\n{}",
3107            r_str
3108        );
3109        assert!(
3110            r_str.contains("as.Animator <- function(x)"),
3111            "expected an annotator, got:\n{}",
3112            r_str
3113        );
3114        assert!(
3115            r_str.contains("validate_Animator <- function(x)")
3116                && r_str.contains("required_fields <- c(\"animations\")"),
3117            "expected a validator checking the concrete field, got:\n{}",
3118            r_str
3119        );
3120    }
3121
3122    #[test]
3123    fn test_multi_record_intersection_alias_merges_all_fields_into_constructor() {
3124        // `Alpha & Beta` where both sides are concrete list/record aliases
3125        // should get a constructor combining every field from every list
3126        // type in the intersection, not just fall through to no R code at
3127        // all (which is what happened before `record_facet` was
3128        // generalized to fold *all* record members together, not just the
3129        // first found).
3130        let r_code = FluentParser::new()
3131            .push("type Alpha <- list { x: int };")
3132            .run()
3133            .push("type Beta <- list { y: char };")
3134            .run()
3135            .push("type Combo <- Alpha & Beta;")
3136            .run()
3137            .get_r_code();
3138        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3139        assert!(
3140            r_str.contains("Combo <- function(x, y, .spread = NULL)"),
3141            "expected a constructor merging fields from both Alpha and Beta, got:\n{}",
3142            r_str
3143        );
3144        assert!(
3145            r_str.contains("as.Combo <- function(x)"),
3146            "expected an annotator, got:\n{}",
3147            r_str
3148        );
3149        assert!(
3150            r_str.contains("validate_Combo <- function(x)") && r_str.contains("required_fields <- c(\"x\", \"y\")"),
3151            "expected a validator checking both merged fields, got:\n{}",
3152            r_str
3153        );
3154    }
3155
3156    #[test]
3157    fn test_external_module_project_generates_include() {
3158        use super::{reset_include_stack, take_main_includes};
3159        reset_include_stack();
3160        let module = Lang::Module {
3161            name: "MyModule".to_string(),
3162            body: vec![],
3163            module_position: ModulePosition::External,
3164            config: Config::default().set_environment(Environment::Project),
3165            help_data: HelpData::default(),
3166        };
3167        let context = Context::default().set_environment(Environment::Project);
3168        let (r_code, _) = module.to_r(&context);
3169        // The include is no longer emitted inline; it is hoisted to the enclosing
3170        // file's header via the include stack.
3171        assert_eq!(r_code, "", "got: {}", r_code);
3172        let includes = take_main_includes();
3173        assert!(
3174            includes.contains(&"MyModule.R".to_string()),
3175            "expected MyModule.R to be registered, got: {:?}",
3176            includes
3177        );
3178    }
3179
3180    #[test]
3181    fn test_module_transpilation_with_pub() {
3182        let r_code = FluentParser::new()
3183            .push("module Math { let sq <- 2; @pub let pi <- 3; };")
3184            .run()
3185            .get_r_code()
3186            .iter()
3187            .cloned()
3188            .collect::<Vec<_>>()
3189            .join("\n");
3190        assert!(
3191            r_code.contains("Math <- new.env(parent = emptyenv())"),
3192            "missing env init: {}",
3193            r_code
3194        );
3195        assert!(r_code.contains("local({"), "missing local block: {}", r_code);
3196        assert!(r_code.contains("Math$pi <- pi"), "missing public export: {}", r_code);
3197        assert!(
3198            !r_code.contains("Math$sq"),
3199            "private member should not be exported: {}",
3200            r_code
3201        );
3202    }
3203
3204    #[test]
3205    fn test_partial_app_in_module_transpiles_to_closure() {
3206        // A `\Truc:{ ok = true }` bound inside a module reaches the transpiler
3207        // as a raw `Lang::PartialApp` (the `Lang::Module` arm re-renders the
3208        // original parsed body, not the typed AST). It used to hit the `_ => ""`
3209        // catch-all, emitting `new_truc <- ` with an empty RHS — R then parsed
3210        // the *next* statement as the value.
3211        let r_code = FluentParser::new()
3212            .push("module M { @pub type Truc <- list { truc: bool, ok: bool }; @pub let new_truc <- \\Truc:{ ok = true }; };")
3213            .run()
3214            .get_r_code()
3215            .iter()
3216            .cloned()
3217            .collect::<Vec<_>>()
3218            .join("\n");
3219        assert!(
3220            r_code.contains("function(truc) Truc("),
3221            "partial app should desugar to a single-hole closure: {}",
3222            r_code
3223        );
3224        assert!(
3225            !r_code.contains("`new_truc` <- \n"),
3226            "partial app must not transpile to an empty RHS: {}",
3227            r_code
3228        );
3229    }
3230
3231    #[test]
3232    fn test_fn_body_lifted_call_is_vapply_not_direct() {
3233        // The `Lang::VecFunctionApp` built by the type-checker's vector lift
3234        // never survives inside a function body (`function()` returns the
3235        // original expression; `Lines` typing only rebuilds its last
3236        // statement), so the FunctionApp transpile arm re-derives the lift on
3237        // the fly. Without it, a non-element-wise callee (`if` in the body)
3238        // was called directly on the whole vector — a fatal R error since 4.2.
3239        let r_code = FluentParser::new()
3240            .set_context(Context::default())
3241            .push("let clamp <- fn(i: int): int { if (i > 2) { 2 } else { i } };")
3242            .run()
3243            .push("let clamp_all <- fn(v: [#N, int]): [#N, int] { clamp(v) };")
3244            .run()
3245            .get_r_code()
3246            .iter()
3247            .cloned()
3248            .collect::<Vec<_>>()
3249            .join("\n");
3250        assert!(
3251            r_code.contains("vapply(v, function(.typr_x) clamp(.typr_x)"),
3252            "fn-body lifted call over a non-vectorizable callee must vapply: {}",
3253            r_code
3254        );
3255    }
3256
3257    #[test]
3258    fn test_fn_body_lifted_call_over_record_array_alias_uses_vec_apply() {
3259        // Same fn-body re-lift, composite flavor: an alias of `[Record]`
3260        // (typed_vec representation) must go through `vec_apply`, never a
3261        // direct call that would S3-dispatch on the typed_vec itself.
3262        let r_code = FluentParser::new()
3263            .set_context(Context::default())
3264            .push("type Todo <- list { name: char, done: bool };")
3265            .run()
3266            .push("type TodoList <- [Todo];")
3267            .run()
3268            .push("let check_if_name <- fn(self: Todo, name: char): Todo { self };")
3269            .run()
3270            .push("let check <- fn(self: TodoList, name: char): TodoList { check_if_name(self, name) };")
3271            .run()
3272            .get_r_code()
3273            .iter()
3274            .cloned()
3275            .collect::<Vec<_>>()
3276            .join("\n");
3277        assert!(
3278            r_code.contains("vec_apply(check_if_name, self, name)"),
3279            "fn-body lifted call over a record-array alias must vec_apply: {}",
3280            r_code
3281        );
3282    }
3283
3284    #[test]
3285    fn test_testable_member_hidden_in_normal_build() {
3286        // Without --test, a @testable member stays fully private.
3287        let r_code = FluentParser::new()
3288            .push("module Math { @testable let sq <- fn(x: int): int { x * x }; };")
3289            .run()
3290            .get_r_code()
3291            .iter()
3292            .cloned()
3293            .collect::<Vec<_>>()
3294            .join("\n");
3295        assert!(
3296            !r_code.contains(".test_sq"),
3297            "testable member must not be exposed in a normal build: {}",
3298            r_code
3299        );
3300    }
3301
3302    #[test]
3303    fn test_testable_member_exposed_in_test_build() {
3304        // With test_mode on, a @testable member is exposed as M$.test_<name>.
3305        let r_code = FluentParser::new()
3306            .set_context(Context::empty().set_test_mode(true))
3307            .push("module Math { @testable let sq <- fn(x: int): int { x * x }; };")
3308            .run()
3309            .get_r_code()
3310            .iter()
3311            .cloned()
3312            .collect::<Vec<_>>()
3313            .join("\n");
3314        assert!(
3315            r_code.contains("Math$`.test_sq` <- sq"),
3316            "testable member must be exposed in a test build: {}",
3317            r_code
3318        );
3319    }
3320
3321    #[test]
3322    fn test_module_transpilation_no_pub() {
3323        let r_code = FluentParser::new()
3324            .push("module Empty { let x <- 1; };")
3325            .run()
3326            .get_r_code()
3327            .iter()
3328            .cloned()
3329            .collect::<Vec<_>>()
3330            .join("\n");
3331        assert!(
3332            r_code.contains("Empty <- new.env(parent = emptyenv())"),
3333            "missing env init: {}",
3334            r_code
3335        );
3336        assert!(r_code.contains("local({"), "missing local block: {}", r_code);
3337        assert!(
3338            !r_code.contains("Empty$x"),
3339            "private member should not be exported: {}",
3340            r_code
3341        );
3342    }
3343
3344    #[test]
3345    fn test_module_s3_registration_for_typed_pub_fn() {
3346        let r_code = FluentParser::new()
3347            .push("module Math { @pub let double <- fn(x: Integer): Integer { x }; };")
3348            .run()
3349            .get_r_code()
3350            .iter()
3351            .cloned()
3352            .collect::<Vec<_>>()
3353            .join("\n");
3354        assert!(
3355            r_code.contains("Math <- new.env(parent = emptyenv())"),
3356            "missing env init: {}",
3357            r_code
3358        );
3359        assert!(
3360            r_code.contains("registerS3method(\"double\", \"integer\", double.integer)"),
3361            "missing S3 registration: {}",
3362            r_code
3363        );
3364        assert!(
3365            r_code.contains("double <- function(x, ...) UseMethod(\"double\")"),
3366            "missing generic: {}",
3367            r_code
3368        );
3369        assert!(
3370            r_code.contains("Math$double <- double"),
3371            "missing generic export: {}",
3372            r_code
3373        );
3374    }
3375
3376    #[test]
3377    fn test_module_no_trailing_semicolon() {
3378        let r_code = FluentParser::new()
3379            .push("module Geo { @pub let pi <- 3; }")
3380            .run()
3381            .get_r_code()
3382            .iter()
3383            .cloned()
3384            .collect::<Vec<_>>()
3385            .join("\n");
3386        assert!(
3387            r_code.contains("Geo <- new.env(parent = emptyenv())"),
3388            "missing env init: {}",
3389            r_code
3390        );
3391        assert!(r_code.contains("Geo$pi <- pi"), "missing public export: {}", r_code);
3392    }
3393
3394    #[test]
3395    fn test_import_module() {
3396        let r_code = FluentParser::new()
3397            .push("module Math { @pub let pi <- 3; }")
3398            .push("import Math")
3399            .run()
3400            .run()
3401            .get_r_code()
3402            .iter()
3403            .cloned()
3404            .collect::<Vec<_>>()
3405            .join("\n");
3406        assert!(
3407            r_code.contains("Math <- new.env(parent = emptyenv())"),
3408            "missing env init: {}",
3409            r_code
3410        );
3411    }
3412
3413    #[test]
3414    fn test_import_module_as_alias() {
3415        let r_code = FluentParser::new()
3416            .push("module Math { @pub let pi <- 3; }")
3417            .push("import Math as Maths")
3418            .run()
3419            .run()
3420            .get_r_code()
3421            .iter()
3422            .cloned()
3423            .collect::<Vec<_>>()
3424            .join("\n");
3425        assert!(
3426            r_code.contains("Math <- new.env(parent = emptyenv())"),
3427            "missing env init: {}",
3428            r_code
3429        );
3430        assert!(
3431            r_code.contains("Maths <- Math") || r_code.contains("`Maths` <- Math"),
3432            "missing alias assignment: {}",
3433            r_code
3434        );
3435    }
3436
3437    #[test]
3438    fn test_use_items_transpiles() {
3439        let r_code = FluentParser::new()
3440            .push("module Math { @pub let pi <- 3; @pub let e <- 2; };")
3441            .push("use Math::{pi, e as euler};")
3442            .run()
3443            .run()
3444            .get_r_code()
3445            .iter()
3446            .cloned()
3447            .collect::<Vec<_>>()
3448            .join("\n");
3449        assert!(r_code.contains("pi <- Math$pi"), "missing pi binding: {}", r_code);
3450        assert!(r_code.contains("euler <- Math$e"), "missing euler binding: {}", r_code);
3451    }
3452
3453    #[test]
3454    fn test_use_wildcard_transpiles() {
3455        let r_code = FluentParser::new()
3456            .push("module Math { @pub let pi <- 3; @pub let e <- 2; let secret <- 0; };")
3457            .push("use Math::*;")
3458            .run()
3459            .run()
3460            .get_r_code()
3461            .iter()
3462            .cloned()
3463            .collect::<Vec<_>>()
3464            .join("\n");
3465        assert!(r_code.contains("pi <- Math$pi"), "missing pi binding: {}", r_code);
3466        assert!(r_code.contains("e <- Math$e"), "missing e binding: {}", r_code);
3467        assert!(
3468            !r_code.contains("secret <- Math$secret"),
3469            "private member must not be imported: {}",
3470            r_code
3471        );
3472    }
3473
3474    // RFC-TR-032 @export tests
3475
3476    #[test]
3477    fn test_export_at_top_level_prepends_roxygen_tag() {
3478        let r_code = FluentParser::new()
3479            .push("@export let answer <- 42;")
3480            .run()
3481            .get_r_code()
3482            .iter()
3483            .cloned()
3484            .collect::<Vec<_>>()
3485            .join("\n");
3486        assert!(r_code.contains("#' @export"), "missing #' @export tag: {}", r_code);
3487        assert!(r_code.contains("answer"), "missing assignment: {}", r_code);
3488    }
3489
3490    #[test]
3491    fn test_export_type_alias_prepends_roxygen_tag_on_constructor() {
3492        let r_code = FluentParser::new()
3493            .push("@export type Point <- list { x: int, y: int };")
3494            .run()
3495            .get_r_code()
3496            .iter()
3497            .cloned()
3498            .collect::<Vec<_>>()
3499            .join("\n");
3500        assert!(r_code.contains("#' @export"), "missing #' @export tag: {}", r_code);
3501        assert!(r_code.contains("Point <- function"), "missing constructor: {}", r_code);
3502    }
3503
3504    #[test]
3505    fn test_pub_type_alias_does_not_get_roxygen_tag() {
3506        let r_code = FluentParser::new()
3507            .push("@pub type Point <- list { x: int, y: int };")
3508            .run()
3509            .get_r_code()
3510            .iter()
3511            .cloned()
3512            .collect::<Vec<_>>()
3513            .join("\n");
3514        assert!(!r_code.contains("#' @export"), "unexpected #' @export tag: {}", r_code);
3515    }
3516
3517    #[test]
3518    fn test_export_in_module_is_public_and_package_exported() {
3519        let r_code = FluentParser::new()
3520            .push("module Math { @export let norm <- fn(x: int): int { x }; };")
3521            .run()
3522            .get_r_code()
3523            .iter()
3524            .cloned()
3525            .collect::<Vec<_>>()
3526            .join("\n");
3527        assert!(
3528            r_code.contains("Math$norm <- norm"),
3529            "missing module export: {}",
3530            r_code
3531        );
3532        assert!(r_code.contains("#' @export"), "missing roxygen export tag: {}", r_code);
3533        assert!(
3534            r_code.contains("norm <- Math$norm"),
3535            "missing package-level re-export: {}",
3536            r_code
3537        );
3538    }
3539
3540    #[test]
3541    fn test_export_type_alias_in_module_is_public_and_package_exported() {
3542        let r_code = FluentParser::new()
3543            .push("module Shapes { @export type Point <- list { x: int, y: int }; };")
3544            .run()
3545            .get_r_code()
3546            .iter()
3547            .cloned()
3548            .collect::<Vec<_>>()
3549            .join("\n");
3550        assert!(
3551            r_code.contains("Shapes$Point <- Point"),
3552            "missing module export: {}",
3553            r_code
3554        );
3555        assert!(r_code.contains("#' @export"), "missing roxygen export tag: {}", r_code);
3556        assert!(
3557            r_code.contains("Point <- Shapes$Point"),
3558            "missing package-level re-export: {}",
3559            r_code
3560        );
3561    }
3562
3563    #[test]
3564    fn test_export_in_module_test_build_adds_test_alias() {
3565        let r_code = FluentParser::new()
3566            .set_context(Context::empty().set_test_mode(true))
3567            .push("module Math { @export let norm <- fn(x: int): int { x }; };")
3568            .run()
3569            .get_r_code()
3570            .iter()
3571            .cloned()
3572            .collect::<Vec<_>>()
3573            .join("\n");
3574        assert!(
3575            r_code.contains("Math$`.test_norm` <- norm"),
3576            "export member must get .test_ alias in test build: {}",
3577            r_code
3578        );
3579    }
3580
3581    #[test]
3582    fn test_pub_in_module_test_build_adds_test_alias() {
3583        let r_code = FluentParser::new()
3584            .set_context(Context::empty().set_test_mode(true))
3585            .push("module Math { @pub let pi <- 3; };")
3586            .run()
3587            .get_r_code()
3588            .iter()
3589            .cloned()
3590            .collect::<Vec<_>>()
3591            .join("\n");
3592        assert!(
3593            r_code.contains("Math$`.test_pi` <- pi"),
3594            "@pub member must get .test_ alias in test build (RFC-TR-032 §3.2): {}",
3595            r_code
3596        );
3597    }
3598
3599    #[test]
3600    fn test_array_constructor_call_transpilation() {
3601        let r_code = FluentParser::new()
3602            .push("type Bits <- [Any, int];")
3603            .run()
3604            .push("let b <- Bits:[1, 2, 3];")
3605            .run()
3606            .get_r_code()
3607            .iter()
3608            .cloned()
3609            .collect::<Vec<_>>()
3610            .join("\n");
3611        // Step ③ (unification_arrays.md): `Bits` targets a primitive-element
3612        // array → atomic representation, so the constructor receives a bare
3613        // `c(...)` instead of a `typed_vec`.
3614        assert!(
3615            r_code.contains("Bits(c("),
3616            "expected Bits(c(...)) constructor: {}",
3617            r_code
3618        );
3619        assert!(
3620            !r_code.contains("typed_vec"),
3621            "no typed_vec for an atomic-representation alias: {}",
3622            r_code
3623        );
3624    }
3625
3626    #[test]
3627    fn test_record_alias_return_no_constructor_pipe() {
3628        // A function returning a record alias must NOT get `|> TypeName()` in
3629        // its body — the constructor takes specific named fields, not a single
3630        // value, so piping the body result through it would fail at runtime.
3631        let r_code = FluentParser::new()
3632            .push("type Point <- list { x: int, y: int };")
3633            .run()
3634            .push("let incr <- fn(p: Point): Point { Point:{x: (p$x+1), y: (p$y+1)} };")
3635            .run()
3636            .get_r_code()
3637            .iter()
3638            .cloned()
3639            .collect::<Vec<_>>()
3640            .join("\n");
3641        // The method definition must not contain `|> Point()` inside the body
3642        assert!(
3643            !r_code.contains("}) |> Point()"),
3644            "record alias output conversion should not be added: {}",
3645            r_code
3646        );
3647        // The method should still be wrapped by as.Generic() or as.FunctionN() for S3 dispatch
3648        assert!(
3649            r_code.contains("|> as.Generic()") || r_code.contains("|> Function"),
3650            "function type annotation should still be applied: {}",
3651            r_code
3652        );
3653    }
3654
3655    #[test]
3656    fn test_alias_int_generates_validator() {
3657        let r_code = FluentParser::new().check_transpiling("type Meters <- int;");
3658        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3659        assert!(
3660            r_str.contains("validate_Meters <- function(x)"),
3661            "expected validator function, got: {}",
3662            r_str
3663        );
3664        assert!(
3665            r_str.contains("is.integer"),
3666            "expected is.integer check, got: {}",
3667            r_str
3668        );
3669    }
3670
3671    #[test]
3672    fn test_alias_char_generates_validator() {
3673        let r_code = FluentParser::new().check_transpiling("type Name <- char;");
3674        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3675        assert!(
3676            r_str.contains("validate_Name <- function(x)"),
3677            "expected validator function, got: {}",
3678            r_str
3679        );
3680        assert!(
3681            r_str.contains("is.character"),
3682            "expected is.character check, got: {}",
3683            r_str
3684        );
3685    }
3686
3687    #[test]
3688    fn test_alias_bool_generates_validator() {
3689        let r_code = FluentParser::new().check_transpiling("type Flag <- bool;");
3690        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3691        assert!(
3692            r_str.contains("validate_Flag <- function(x)"),
3693            "expected validator function, got: {}",
3694            r_str
3695        );
3696        assert!(
3697            r_str.contains("is.logical"),
3698            "expected is.logical check, got: {}",
3699            r_str
3700        );
3701    }
3702
3703    #[test]
3704    fn test_alias_num_generates_validator() {
3705        let r_code = FluentParser::new().check_transpiling("type Real <- num;");
3706        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3707        assert!(
3708            r_str.contains("validate_Real <- function(x)"),
3709            "expected validator function, got: {}",
3710            r_str
3711        );
3712        assert!(
3713            r_str.contains("is.numeric"),
3714            "expected is.numeric check, got: {}",
3715            r_str
3716        );
3717    }
3718
3719    #[test]
3720    fn test_validating_cast_int() {
3721        let r_code = FluentParser::new()
3722            .push("type Meters <- int;")
3723            .run()
3724            .check_transpiling("x as! Meters");
3725        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3726        assert!(
3727            r_str.contains("validate_Meters(x)"),
3728            "expected validate_Meters(x), got: {}",
3729            r_str
3730        );
3731    }
3732
3733    #[test]
3734    fn test_tag_alias_char_generates_validator() {
3735        let r_code = FluentParser::new().check_transpiling("type Hello <- .Hello(char);");
3736        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3737        assert!(
3738            r_str.contains("validate_Hello <- function(x)"),
3739            "expected validator function, got: {}",
3740            r_str
3741        );
3742        assert!(
3743            r_str.contains("x[[1]] != 'Hello'"),
3744            "expected tag name check, got: {}",
3745            r_str
3746        );
3747        assert!(
3748            r_str.contains("x[[\"body\"]]"),
3749            "expected body field check, got: {}",
3750            r_str
3751        );
3752        assert!(
3753            r_str.contains("is.character"),
3754            "expected is.character check on body, got: {}",
3755            r_str
3756        );
3757    }
3758
3759    #[test]
3760    fn test_tag_alias_int_generates_validator() {
3761        let r_code = FluentParser::new().check_transpiling("type Count <- .Count(int);");
3762        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3763        assert!(
3764            r_str.contains("validate_Count <- function(x)"),
3765            "expected validator, got: {}",
3766            r_str
3767        );
3768        assert!(
3769            r_str.contains("x[[1]] != 'Count'"),
3770            "expected tag name check, got: {}",
3771            r_str
3772        );
3773        assert!(
3774            r_str.contains("is.integer"),
3775            "expected is.integer check on body, got: {}",
3776            r_str
3777        );
3778    }
3779
3780    #[test]
3781    fn test_tag_alias_with_alias_body_calls_nested_validator() {
3782        let r_code = FluentParser::new()
3783            .push("type Name <- char;")
3784            .run()
3785            .check_transpiling("type Tagged <- .Tagged(Name);");
3786        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3787        assert!(
3788            r_str.contains("validate_Tagged <- function(x)"),
3789            "expected validator, got: {}",
3790            r_str
3791        );
3792        assert!(
3793            r_str.contains("validate_Name(x[[\"body\"]])"),
3794            "expected nested validator call, got: {}",
3795            r_str
3796        );
3797    }
3798
3799    #[test]
3800    fn test_union_variant_generates_full_pipeline() {
3801        // Each union variant gets the same constructor/annotator/validator
3802        // contract as records (validation_variant_d_union.md §3).
3803        let r_str = FluentParser::new()
3804            .check_transpiling("type Shape <- .Circle(num) | .Nothing;")
3805            .iter()
3806            .cloned()
3807            .collect::<Vec<_>>()
3808            .join("\n");
3809        // Constructor (payload variant) builds the canonical value then delegates.
3810        assert!(
3811            r_str.contains("Circle <- function(x) {")
3812                && r_str.contains("v <- list(\"Circle\", body = x)")
3813                && r_str.contains("as.Circle(v)"),
3814            "expected Circle constructor, got: {r_str}"
3815        );
3816        // Annotator sets the enriched class idempotently and validates.
3817        assert!(
3818            r_str.contains("as.Circle <- function(x) {")
3819                && r_str.contains("class(x) <- c(\"Circle\", \"Shape\", \"Tag\", \"list\")")
3820                && r_str.contains("x <- validate_Circle(x)")
3821                && r_str.contains("x <- validate(x)"),
3822            "expected Circle annotator, got: {r_str}"
3823        );
3824        // Internal validator checks tag identity and payload type.
3825        assert!(
3826            r_str.contains("validate_Circle <- function(x) {")
3827                && r_str.contains("x[[1]] != 'Circle'")
3828                && r_str.contains("is.numeric(x[[\"body\"]])"),
3829            "expected Circle validator, got: {r_str}"
3830        );
3831        // Empty variant has a zero-arg constructor and no body.
3832        assert!(
3833            r_str.contains("Nothing <- function() {") && r_str.contains("x <- list(\"Nothing\")"),
3834            "expected Nothing constructor, got: {r_str}"
3835        );
3836    }
3837
3838    #[test]
3839    fn test_tag_literal_canonical_representation() {
3840        // A `.Circle(..)` literal must produce the same runtime shape as the
3841        // variant constructor: tag in position 1, payload under `body`, class
3842        // enriched with the union name (validation_variant_d_union.md §2).
3843        let r_str = FluentParser::new()
3844            .push("type Shape <- .Circle(num) | .Square(num);")
3845            .run()
3846            .check_transpiling(".Circle(3.14)")
3847            .iter()
3848            .cloned()
3849            .collect::<Vec<_>>()
3850            .join("\n");
3851        assert!(
3852            r_str.contains("structure(list('Circle', body =")
3853                && r_str.contains("class = c('Circle', 'Shape', 'Tag', 'list')"),
3854            "expected canonical tag literal with union class, got: {r_str}"
3855        );
3856    }
3857
3858    #[test]
3859    fn test_tag_literal_without_union_omits_union_class() {
3860        // A tag with no declared union still uses the canonical shape, but the
3861        // class carries no union name.
3862        let r_str = FluentParser::new()
3863            .check_transpiling(".Loose(1)")
3864            .iter()
3865            .cloned()
3866            .collect::<Vec<_>>()
3867            .join("\n");
3868        assert!(
3869            r_str.contains("structure(list('Loose', body =") && r_str.contains("class = c('Loose', 'Tag', 'list')"),
3870            "expected canonical tag literal without union class, got: {r_str}"
3871        );
3872    }
3873
3874    #[test]
3875    fn test_literal_char_alias_generates_exact_validator() {
3876        let r_code = FluentParser::new().check_transpiling("type Hello <- \"hello\";");
3877        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3878        assert!(
3879            r_str.contains("validate_Hello <- function(x)"),
3880            "expected validator function, got: {}",
3881            r_str
3882        );
3883        assert!(
3884            r_str.contains("x != 'hello'"),
3885            "expected literal equality check, got: {}",
3886            r_str
3887        );
3888    }
3889
3890    #[test]
3891    fn test_literal_int_alias_generates_exact_validator() {
3892        let r_code = FluentParser::new().check_transpiling("type Byte <- 89;");
3893        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3894        assert!(
3895            r_str.contains("validate_Byte <- function(x)"),
3896            "expected validator function, got: {}",
3897            r_str
3898        );
3899        assert!(
3900            r_str.contains("x != 89L"),
3901            "expected literal equality check, got: {}",
3902            r_str
3903        );
3904    }
3905
3906    #[test]
3907    fn test_literal_num_alias_generates_exact_validator() {
3908        let r_code = FluentParser::new().check_transpiling("type Pi <- 3.14;");
3909        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3910        assert!(
3911            r_str.contains("validate_Pi <- function(x)"),
3912            "expected validator function, got: {}",
3913            r_str
3914        );
3915        assert!(
3916            r_str.contains("x != 3.14"),
3917            "expected literal equality check, got: {}",
3918            r_str
3919        );
3920    }
3921
3922    #[test]
3923    fn test_literal_bool_true_alias_generates_exact_validator() {
3924        let r_code = FluentParser::new().check_transpiling("type Yes <- true;");
3925        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3926        assert!(
3927            r_str.contains("validate_Yes <- function(x)"),
3928            "expected validator function, got: {}",
3929            r_str
3930        );
3931        assert!(
3932            r_str.contains("x != TRUE"),
3933            "expected literal TRUE check, got: {}",
3934            r_str
3935        );
3936    }
3937
3938    #[test]
3939    fn test_literal_bool_false_alias_generates_exact_validator() {
3940        let r_code = FluentParser::new().check_transpiling("type No <- false;");
3941        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3942        assert!(
3943            r_str.contains("validate_No <- function(x)"),
3944            "expected validator function, got: {}",
3945            r_str
3946        );
3947        assert!(
3948            r_str.contains("x != FALSE"),
3949            "expected literal FALSE check, got: {}",
3950            r_str
3951        );
3952    }
3953
3954    #[test]
3955    fn test_record_subtype_includes_supertype_in_s3_class() {
3956        // Person has all fields of Position, so Person <: Position.
3957        // The annotator for Person must include "Position" in its class vector
3958        // so that S3 methods defined on Position dispatch for Person values.
3959        let r_str = FluentParser::new()
3960            .push("type Position <- list{ position: int };")
3961            .run()
3962            .push("type Person <- list{ name: char, age: int, position: int };")
3963            .run()
3964            .get_r_code()
3965            .iter()
3966            .cloned()
3967            .collect::<Vec<_>>()
3968            .join("\n");
3969        assert!(
3970            r_str.contains("class(x) <- c(\"Person\", \"Position\", \"list\")"),
3971            "expected Person's annotator to include Position, got: {r_str}"
3972        );
3973    }
3974
3975    #[test]
3976    fn test_forced_dispatch_call_binds_directly_to_suffixed_method() {
3977        // `greet<Personne>(...)` (turbofish-style forced S3 dispatch) must
3978        // bind straight to `greet.Personne(...)`, bypassing `UseMethod` at
3979        // runtime, while a plain unforced call stays exactly as it was.
3980        let r_str = FluentParser::new()
3981            .push("type Personne <- list{ name: char };")
3982            .run()
3983            .push("type Animal <- list{ name: char };")
3984            .run()
3985            .push("let greet <- fn(x: Personne, y: Personne): Personne { x };")
3986            .run()
3987            .push("let greet <- fn(x: Animal, y: Animal): Animal { x };")
3988            .run()
3989            .push("let p1 <- Personne:{ name = \"a\" };")
3990            .run()
3991            .push("let p2 <- Personne:{ name = \"b\" };")
3992            .run()
3993            .check_transpiling("greet<Personne>(p1, p2)")
3994            .iter()
3995            .cloned()
3996            .collect::<Vec<_>>()
3997            .join("\n");
3998        assert!(
3999            r_str.contains("greet.Personne(p1, p2)"),
4000            "expected a direct `greet.Personne(...)` call, got: {r_str}"
4001        );
4002    }
4003
4004    #[test]
4005    fn test_unforced_call_stays_plain() {
4006        let r_str = FluentParser::new()
4007            .push("type Personne <- list{ name: char };")
4008            .run()
4009            .push("type Animal <- list{ name: char };")
4010            .run()
4011            .push("let greet <- fn(x: Personne, y: Personne): Personne { x };")
4012            .run()
4013            .push("let greet <- fn(x: Animal, y: Animal): Animal { x };")
4014            .run()
4015            .push("let p1 <- Personne:{ name = \"a\" };")
4016            .run()
4017            .push("let p2 <- Personne:{ name = \"b\" };")
4018            .run()
4019            .check_transpiling("greet(p1, p2)")
4020            .iter()
4021            .cloned()
4022            .collect::<Vec<_>>()
4023            .join("\n");
4024        assert!(
4025            r_str.contains("greet(p1, p2)") && !r_str.contains("greet.Personne(p1, p2)"),
4026            "expected a plain, unsuffixed `greet(...)` call relying on runtime UseMethod dispatch, got: {r_str}"
4027        );
4028    }
4029
4030    #[test]
4031    fn test_forced_dispatch_any_binds_to_default() {
4032        // `greet<Any>(...)` must bind to the `.default` fallback — mirroring
4033        // the same `Any -> .default` precedent already used at the
4034        // definition site (see the `Lang::Let` arm above), not to a literal
4035        // `.Any` suffix.
4036        let r_str = FluentParser::new()
4037            .push("type Personne <- list{ name: char };")
4038            .run()
4039            .push("let greet <- fn(x: Personne, y: Personne): Personne { x };")
4040            .run()
4041            .push("let greet <- fn(x: Any, y: Any): Any { x };")
4042            .run()
4043            .push("let p1 <- Personne:{ name = \"a\" };")
4044            .run()
4045            .push("let p2 <- Personne:{ name = \"b\" };")
4046            .run()
4047            .check_transpiling("greet<Any>(p1, p2)")
4048            .iter()
4049            .cloned()
4050            .collect::<Vec<_>>()
4051            .join("\n");
4052        assert!(
4053            r_str.contains("greet.default(p1, p2)"),
4054            "expected a direct `greet.default(...)` call, got: {r_str}"
4055        );
4056    }
4057
4058    #[test]
4059    fn test_record_without_supertype_keeps_plain_class() {
4060        // Position has no other record supertype, so its class stays c("Position", "list").
4061        let r_str = FluentParser::new()
4062            .check_transpiling("type Position <- list{ position: int };")
4063            .iter()
4064            .cloned()
4065            .collect::<Vec<_>>()
4066            .join("\n");
4067        assert!(
4068            r_str.contains("class(x) <- c(\"Position\", \"list\")"),
4069            "expected Position's annotator with no supertype, got: {r_str}"
4070        );
4071    }
4072
4073    #[test]
4074    fn test_import_from_qualifies_call_site() {
4075        // @importFrom dplyr filter should make filter(a, b) transpile to dplyr::filter(a, b).
4076        let r_str = FluentParser::new()
4077            .push("@importFrom dplyr filter;")
4078            .run()
4079            .push("@filter: (Any, Any) -> Any;")
4080            .run()
4081            .check_transpiling("filter(df, cond)")
4082            .iter()
4083            .cloned()
4084            .collect::<Vec<_>>()
4085            .join("\n");
4086        assert!(
4087            r_str.contains("dplyr::filter("),
4088            "expected dplyr::filter(...), got: {r_str}"
4089        );
4090    }
4091
4092    #[test]
4093    fn test_import_from_multiple_fns() {
4094        // Multiple functions from the same package should each be qualified independently.
4095        let r_str = FluentParser::new()
4096            .push("@importFrom dplyr filter mutate;")
4097            .run()
4098            .push("@mutate: (Any, Any) -> Any;")
4099            .run()
4100            .check_transpiling("mutate(df, z)")
4101            .iter()
4102            .cloned()
4103            .collect::<Vec<_>>()
4104            .join("\n");
4105        assert!(
4106            r_str.contains("dplyr::mutate("),
4107            "expected dplyr::mutate(...), got: {r_str}"
4108        );
4109    }
4110}