Skip to main content

typr_core/processes/transpiling/
mod.rs

1pub mod translatable;
2
3use crate::components::context::config::Environment;
4use crate::components::context::Context;
5use crate::components::error_message::help_data::HelpData;
6use crate::components::language::argument_value::ArgumentValue;
7use crate::components::language::format_backtick;
8use crate::components::language::function_lang::Function;
9use crate::components::language::operators::Op;
10use crate::components::language::set_related_type_if_variable;
11use crate::components::language::var::Var;
12use crate::components::language::Lang;
13use crate::components::language::ModulePosition;
14use crate::components::r#type::argument_type::ArgumentType;
15use crate::components::r#type::array_type::ArrayType;
16use crate::components::r#type::function_type::FunctionType;
17use crate::components::r#type::type_operator::TypeOperator;
18use crate::components::r#type::type_system::TypeSystem;
19use crate::components::r#type::vector_type::VecType;
20use crate::components::r#type::Type;
21use crate::processes::transpiling::translatable::Translatable;
22use crate::processes::type_checking::flatten_operator_union;
23use crate::processes::type_checking::resolve_module_member_type;
24use crate::processes::type_checking::type_comparison::reduce_type;
25use crate::processes::type_checking::typing;
26use translatable::RTranslatable;
27
28#[cfg(not(feature = "wasm"))]
29use std::fs::File;
30#[cfg(not(feature = "wasm"))]
31use std::io::Write;
32#[cfg(not(feature = "wasm"))]
33use std::path::PathBuf;
34
35use std::cell::RefCell;
36use std::collections::HashMap;
37
38/// Render a string value as an R double-quoted literal (R's canonical string
39/// form). The value is assumed to be already decoded (see
40/// `parsing::elements::decode_escapes`), so this is the single place that knows
41/// how to escape for the R target: backslashes and double quotes must be
42/// escaped, control characters are emitted as escape sequences.
43pub fn escape_r_string(s: &str) -> String {
44    let escaped = s
45        .replace('\\', "\\\\")
46        .replace('"', "\\\"")
47        .replace('\n', "\\n")
48        .replace('\t', "\\t");
49    format!("\"{}\"", escaped)
50}
51
52// Thread-local storage for generated files (used in WASM mode)
53thread_local! {
54    static GENERATED_FILES: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
55}
56
57// Thread-local stack of roxygen2 `@include` dependencies, scoped per output file.
58//
59// In Project mode, `mod foo;` dependencies must surface as top-level
60// `#' @include foo.R` tags in the *header* of the file that references them — a
61// `#'` comment buried inside a `local({ ... })` block is not attached to any
62// top-level object, so roxygen2 ignores it. Instead of emitting the tag inline,
63// each external module registers its filename into the current frame; the file
64// that owns that frame drains it into its header.
65//
66// The stack mirrors the `to_r` recursion: a new frame is pushed before
67// transpiling the body of a module that writes its own file, and drained when
68// that file is written. The bottom frame collects top-level (`main`) includes.
69thread_local! {
70    static INCLUDE_STACK: RefCell<Vec<Vec<String>>> = RefCell::new(vec![Vec::new()]);
71}
72
73/// Reset the include stack to a single empty bottom frame (call before a build).
74pub fn reset_include_stack() {
75    INCLUDE_STACK.with(|s| *s.borrow_mut() = vec![Vec::new()]);
76}
77
78/// Push a new frame for the body of a module that writes its own file.
79fn push_include_frame() {
80    INCLUDE_STACK.with(|s| s.borrow_mut().push(Vec::new()));
81}
82
83/// Pop the current frame, returning the includes collected within it.
84fn pop_include_frame() -> Vec<String> {
85    INCLUDE_STACK.with(|s| s.borrow_mut().pop().unwrap_or_default())
86}
87
88/// Register an `@include` target (e.g. "foo.R") into the current frame.
89fn register_include(file: &str) {
90    INCLUDE_STACK.with(|s| {
91        if let Some(top) = s.borrow_mut().last_mut() {
92            top.push(file.to_string());
93        }
94    });
95}
96
97/// Drain the bottom (main) frame — the top-level includes for `main.R`.
98pub fn take_main_includes() -> Vec<String> {
99    INCLUDE_STACK.with(|s| {
100        let mut stack = s.borrow_mut();
101        match stack.first_mut() {
102            Some(bottom) => std::mem::take(bottom),
103            None => Vec::new(),
104        }
105    })
106}
107
108/// Register a generated file (used for WASM mode to capture file outputs)
109pub fn register_generated_file(path: &str, content: &str) {
110    GENERATED_FILES.with(|files| {
111        files
112            .borrow_mut()
113            .insert(path.to_string(), content.to_string());
114    });
115}
116
117/// Get all generated files
118pub fn get_generated_files() -> HashMap<String, String> {
119    GENERATED_FILES.with(|files| files.borrow().clone())
120}
121
122/// Clear all generated files
123pub fn clear_generated_files() {
124    GENERATED_FILES.with(|files| {
125        files.borrow_mut().clear();
126    });
127}
128
129/// Write a file - in native mode writes to filesystem, in WASM mode stores in memory
130#[cfg(not(feature = "wasm"))]
131fn write_output_file(path: &str, content: &str) -> Result<(), String> {
132    use std::fs;
133
134    // Also register in memory for consistency
135    register_generated_file(path, content);
136
137    let path_buf = PathBuf::from(path);
138    if let Some(parent) = path_buf.parent() {
139        fs::create_dir_all(parent).map_err(|e| e.to_string())?;
140    }
141    let mut file = File::create(&path_buf).map_err(|e| e.to_string())?;
142    file.write_all(content.as_bytes())
143        .map_err(|e| e.to_string())?;
144    Ok(())
145}
146
147#[cfg(feature = "wasm")]
148fn write_output_file(path: &str, content: &str) -> Result<(), String> {
149    register_generated_file(path, content);
150    Ok(())
151}
152
153pub trait ToSome {
154    fn to_some(self) -> Option<Self>
155    where
156        Self: Sized;
157}
158
159impl<T: Sized> ToSome for T {
160    fn to_some(self) -> Option<Self> {
161        Some(self)
162    }
163}
164
165trait AndIf {
166    fn and_if<F>(self, condition: F) -> Option<Self>
167    where
168        F: Fn(Self) -> bool,
169        Self: Sized;
170}
171
172impl<T: Clone> AndIf for T {
173    fn and_if<F>(self, condition: F) -> Option<Self>
174    where
175        F: Fn(Self) -> bool,
176    {
177        if condition(self.clone()) {
178            Some(self)
179        } else {
180            None
181        }
182    }
183}
184
185const JS_HEADER: &str = "";
186
187fn to_pattern_match_statement(
188    exp: Lang,
189    branches: &[(Lang, Box<Lang>)],
190    context: &Context,
191) -> String {
192    let match_var = "match_val__";
193    let res = branches
194        .iter()
195        .enumerate()
196        .map(|(id, (pattern, body))| {
197            let (cond, bindings) = pattern_to_condition(pattern, match_var, context);
198            let body_str = body.to_r(context).0;
199            let body_with_bindings = if bindings.is_empty() {
200                body_str
201            } else {
202                format!("{}\n{}", bindings, body_str)
203            };
204            if cond == "TRUE" {
205                // wildcard pattern: always matches
206                if id == 0 {
207                    format!("{{\n{}\n}}", body_with_bindings)
208                } else {
209                    format!("else {{\n{}\n}}", body_with_bindings)
210                }
211            } else if id == 0 {
212                format!("if ({}) {{\n{}\n}}", cond, body_with_bindings)
213            } else {
214                format!("else if ({}) {{\n{}\n}}", cond, body_with_bindings)
215            }
216        })
217        .collect::<Vec<_>>()
218        .join(" ");
219    format!("{{\n{} <- {}\n{}\n}}", match_var, exp.to_r(context).0, res)
220}
221
222/// Map a Type to its corresponding R type-check function name.
223fn type_to_r_check(typ: &Type) -> Option<&'static str> {
224    match typ {
225        Type::Integer(_, _) => Some("is.integer"),
226        Type::Boolean(_, _) => Some("is.logical"),
227        Type::Number(_, _) => Some("is.numeric"),
228        Type::Char(_, _) => Some("is.character"),
229        Type::Null(_) => Some("is.null"),
230        _ => None,
231    }
232}
233
234/// R class a value of `typ` is expected to carry at runtime, used by record
235/// validators to check field types via `inherits`. Relies on monomorphisation:
236/// every value already carries its type's class. Returns `None` for types with
237/// no reliable nominal class (generics, functions, unions…), in which case the
238/// field is only checked for presence.
239fn record_field_class(typ: &Type, cont: &Context) -> Option<String> {
240    match typ {
241        Type::Integer(_, _) => Some("integer".to_string()),
242        Type::Number(_, _) => Some("numeric".to_string()),
243        Type::Char(_, _) => Some("character".to_string()),
244        Type::Boolean(_, _) => Some("logical".to_string()),
245        Type::Alias(name, _, _, _) => match cont
246            .aliases()
247            .find(|(var, _)| var.get_name() == *name)
248            .map(|(_, t)| t)
249        {
250            // Record aliases carry their alias name as the S3 class.
251            Some(Type::Record(_, _)) => Some(name.clone()),
252            // Primitive aliases (e.g. `type Meters <- int`) carry the underlying
253            // R class — no constructor adds the alias name as a class.
254            Some(inner) => record_field_class(inner, cont),
255            None => None,
256        },
257        _ => None,
258    }
259}
260
261/// Find the name of a union alias that declares a tag variant called
262/// `tag_name`. Used by the `Lang::Tag` literal to enrich its runtime class
263/// with the union name (canonical representation, see
264/// `validation_variant_d_union.md` §2). Returns `None` for standalone tags
265/// (no declared union).
266fn find_union_for_tag(tag_name: &str, cont: &Context) -> Option<String> {
267    cont.aliases().find_map(|(var, typ)| {
268        let is_union = matches!(
269            typ,
270            Type::Operator(
271                crate::components::r#type::type_operator::TypeOperator::Union,
272                _,
273                _,
274                _
275            )
276        );
277        if !is_union {
278            return None;
279        }
280        let declares_tag = flatten_operator_union(typ)
281            .iter()
282            .any(|m| matches!(m, Type::Tag(n, _, _) if n == tag_name));
283        if declares_tag {
284            Some(var.get_name())
285        } else {
286            None
287        }
288    })
289}
290
291/// Build the structural body-validation block for a tag's payload, shared by
292/// standalone tag aliases (`type Hello <- .Hello(char)`) and union variants.
293/// `name` is the type/variant name used in error messages; `inner_type` is the
294/// declared payload type. An empty payload (`.Nothing`) yields an empty block.
295fn tag_body_validation(name: &str, inner_type: &Type) -> String {
296    match inner_type {
297        Type::Empty(_) => String::new(),
298        Type::Integer(tint, _) => {
299            use crate::components::r#type::tint::Tint;
300            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\")");
301            match tint {
302                Tint::Val(i) => format!("{null_check}\n  if (x[[\"body\"]] != {i}L) stop(\"Validation failed for type {name}: body must be literal {i}\")"),
303                Tint::Unknown => null_check,
304            }
305        }
306        Type::Char(tchar, _) => {
307            use crate::components::r#type::tchar::Tchar;
308            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\")");
309            match tchar {
310                Tchar::Val(s) => format!("{null_check}\n  if (x[[\"body\"]] != '{s}') stop(\"Validation failed for type {name}: body must be literal '{s}'\")"),
311                Tchar::Unknown => null_check,
312            }
313        }
314        Type::Boolean(tbool, _) => {
315            use crate::components::r#type::tbool::Tbool;
316            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\")");
317            match tbool {
318                Tbool::Val(b) => {
319                    let r_val = if *b { "TRUE" } else { "FALSE" };
320                    format!("{null_check}\n  if (x[[\"body\"]] != {r_val}) stop(\"Validation failed for type {name}: body must be literal {r_val}\")")
321                }
322                Tbool::Unknown => null_check,
323            }
324        }
325        Type::Number(tnum, _) => {
326            use crate::components::r#type::tnumber::Tnum;
327            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\")");
328            match tnum {
329                Tnum::Val(v) => format!("{null_check}\n  if (x[[\"body\"]] != {v}) stop(\"Validation failed for type {name}: body must be literal {v}\")"),
330                Tnum::Unknown => null_check,
331            }
332        }
333        Type::Alias(alias_name, _, _, _) => format!(
334            "\n  if (is.null(x[[\"body\"]])) stop(\"Validation failed for type {name}: missing 'body' field\")\n  validate_{alias_name}(x[[\"body\"]])"
335        ),
336        _ => format!(
337            "\n  if (is.null(x[[\"body\"]])) stop(\"Validation failed for type {name}: missing 'body' field\")"
338        ),
339    }
340}
341
342/// Emit the full constructor/annotator/validator pipeline for a single tag
343/// variant `V` of union `U`, in the canonical representation
344/// (`structure(list("V", body = p), class = c("V", "U", "Tag", "list"))`).
345/// Mirrors the record pipeline (see `Lang::Alias` / `Type::Record`).
346fn tag_variant_pipeline(variant_name: &str, union_name: &str, inner_type: &Type) -> String {
347    let is_empty = matches!(inner_type, Type::Empty(_));
348    // Constructor: build the raw value, then delegate entirely to the
349    // annotator. It neither sets the class nor validates.
350    let constructor = if is_empty {
351        format!(
352            "{variant_name} <- function() {{\n  x <- list(\"{variant_name}\")\n  as.{variant_name}(x)\n}}"
353        )
354    } else {
355        format!(
356            "{variant_name} <- function(x) {{\n  v <- list(\"{variant_name}\", body = x)\n  as.{variant_name}(v)\n}}"
357        )
358    };
359    // Annotator: single entry point. Sets the class idempotently, then runs
360    // the internal validator and the user validator (`validate` S3 generic,
361    // which dispatches to `validate.{variant_name}` then `validate.{union_name}`).
362    let annotator = format!(
363        "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}}"
364    );
365    // Internal validator: pure structural invariants (tag identity + payload).
366    let body_validation = tag_body_validation(variant_name, inner_type);
367    let validator = format!(
368        "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}}"
369    );
370    format!("{constructor}\n{annotator}\n{validator}")
371}
372
373fn pattern_to_condition(pattern: &Lang, match_var: &str, _context: &Context) -> (String, String) {
374    match pattern {
375        // Tag with a binding variable: .Some(a)
376        Lang::Tag {
377            name, value: inner, ..
378        } => {
379            let cond = format!("{}[[1]] == '{}'", match_var, name);
380            match inner.as_ref() {
381                Lang::Variable { name: var_name, .. } => {
382                    let binding = format!("{} <- {}[[\"body\"]]", var_name, match_var);
383                    (cond, binding)
384                }
385                Lang::Empty(_) => (cond, String::new()),
386                _ => (cond, String::new()),
387            }
388        }
389        // Type pattern: x as int
390        Lang::TypePattern {
391            variable_name: var_name,
392            matched_type: typ,
393            ..
394        } => {
395            let check_fn = type_to_r_check(typ).unwrap_or("is.logical");
396            let cond = format!("{}({})", check_fn, match_var);
397            let binding = format!("{} <- {}", var_name, match_var);
398            (cond, binding)
399        }
400        // Tuple pattern: :{a, b, c}
401        Lang::Tuple {
402            value: elements, ..
403        } => {
404            let cond = format!(
405                "inherits({}, 'Tuple') && length({}) == {}",
406                match_var,
407                match_var,
408                elements.len()
409            );
410            let bindings: Vec<String> = elements
411                .iter()
412                .enumerate()
413                .filter_map(|(i, elem)| {
414                    if let Lang::Variable { name: var_name, .. } = elem {
415                        if var_name == "_" {
416                            None
417                        } else {
418                            Some(format!("{} <- {}[[{}]]", var_name, match_var, i + 1))
419                        }
420                    } else {
421                        None
422                    }
423                })
424                .collect();
425            (cond, bindings.join("\n"))
426        }
427        // List/record pattern: :{nom: n, age: a}
428        Lang::List { value: fields, .. } => {
429            let conditions: Vec<String> = fields
430                .iter()
431                .map(|arg_val: &ArgumentValue| {
432                    format!("!is.null({}[[\"{}\"]])", match_var, arg_val.get_argument())
433                })
434                .collect();
435            let cond = if conditions.is_empty() {
436                "is.list(".to_string() + match_var + ")"
437            } else {
438                format!("is.list({}) && {}", match_var, conditions.join(" && "))
439            };
440            let bindings: Vec<String> = fields
441                .iter()
442                .filter_map(|arg_val| {
443                    if let Lang::Variable { name: var_name, .. } = &arg_val.get_value() {
444                        Some(format!(
445                            "{} <- {}[[\"{}\"]]",
446                            var_name,
447                            match_var,
448                            arg_val.get_argument()
449                        ))
450                    } else {
451                        None
452                    }
453                })
454                .collect();
455            (cond, bindings.join("\n"))
456        }
457        // DataFrame pattern: data__frame(col1 = x, col2 = y)
458        Lang::DataFrame { value: fields, .. } => {
459            let conditions: Vec<String> = fields
460                .iter()
461                .map(|arg_val: &ArgumentValue| {
462                    format!("!is.null({}[[\"{}\"]])", match_var, arg_val.get_argument())
463                })
464                .collect();
465            let cond = if conditions.is_empty() {
466                "is.data.frame(".to_string() + match_var + ")"
467            } else {
468                format!(
469                    "is.data.frame({}) && {}",
470                    match_var,
471                    conditions.join(" && ")
472                )
473            };
474            let bindings: Vec<String> = fields
475                .iter()
476                .filter_map(|arg_val| {
477                    if let Lang::Variable { name: var_name, .. } = &arg_val.get_value() {
478                        Some(format!(
479                            "{} <- {}[[\"{}\"]]",
480                            var_name,
481                            match_var,
482                            arg_val.get_argument()
483                        ))
484                    } else {
485                        None
486                    }
487                })
488                .collect();
489            (cond, bindings.join("\n"))
490        }
491        // Wildcard: _
492        Lang::Variable { name, .. } if name == "_" => ("TRUE".to_string(), String::new()),
493        // Other variable: bind the whole value
494        Lang::Variable { name, .. } => {
495            let binding = format!("{} <- {}", name, match_var);
496            ("TRUE".to_string(), binding)
497        }
498        _ => ("TRUE".to_string(), String::new()),
499    }
500}
501
502impl RTranslatable<(String, Context)> for Lang {
503    fn to_r(&self, cont: &Context) -> (String, Context) {
504        let result = match self {
505            Lang::Bool { value: b, .. } => {
506                let (typ, _, _) = typing(cont, self).to_tuple();
507                let anotation = cont.get_type_anotation(&typ);
508                (
509                    format!("{} |> {}", b.to_string().to_uppercase(), anotation),
510                    cont.clone(),
511                )
512            }
513            Lang::Number { value: n, .. } => {
514                let (typ, _, _) = typing(cont, self).to_tuple();
515                let anotation = cont.get_type_anotation(&typ);
516                (format!("{} |> {}", n, anotation), cont.clone())
517            }
518            Lang::Integer { value: i, .. } => {
519                let (typ, _, _) = typing(cont, self).to_tuple();
520                let anotation = cont.get_type_anotation(&typ);
521                (format!("{}L |> {}", i, anotation), cont.clone())
522            }
523            Lang::Char { value: s, .. } => {
524                let (typ, _, _) = typing(cont, self).to_tuple();
525                let anotation = cont.get_type_anotation(&typ);
526                (
527                    format!("{} |> {}", escape_r_string(s), anotation),
528                    cont.clone(),
529                )
530            }
531            Lang::Operator {
532                operator: op @ (Op::Dot(_) | Op::Pipe(_)),
533                rhs: e1,
534                lhs: e2,
535                ..
536            } => {
537                // `rhs` is the syntactic-left operand, `lhs` is the
538                // syntactic-right operand (same convention as `Op::Dollar`
539                // and the generic operator case below: `e1.to_r() <op>
540                // e2.to_r()`). For plain field access (`p.x`, `e2` a bare
541                // `Lang::Variable` field name and `e1` not an `Integer`),
542                // that means the receiver `e1` must render first:
543                // `e1[['e2']]`, not `e2[['e1']]`. The `Lang::Integer`
544                // sub-case is the synthetic tuple-destructuring node built
545                // in `parsing/mod.rs` (`rhs: Integer(index), lhs: tmp_var`),
546                // which already has the receiver/index roles swapped
547                // relative to that convention on purpose — left as-is, and
548                // `Op::Pipe` keeps its prior (separate, untouched) ordering
549                // since a bare-variable pipe target (`a |> f`) means
550                // something else entirely (`f(a)`, not field indexing).
551                let is_dot = matches!(op, Op::Dot(_));
552                let e1 = (**e1).clone();
553                let e2 = (**e2).clone();
554                match e2.clone() {
555                    Lang::Variable { .. } => match e1 {
556                        Lang::Integer { .. } => Translatable::from(cont.clone())
557                            .to_r(&e2)
558                            .add("[[")
559                            .to_r(&e1)
560                            .add("]]")
561                            .into(),
562                        _ if is_dot => Translatable::from(cont.clone())
563                            .to_r(&e1)
564                            .add("[['")
565                            .to_r(&e2)
566                            .add("']]")
567                            .into(),
568                        _ => Translatable::from(cont.clone())
569                            .to_r(&e2)
570                            .add("[['")
571                            .to_r(&e1)
572                            .add("']]")
573                            .into(),
574                    },
575                    Lang::List { value: fields, .. } => {
576                        let at = fields[0].clone();
577                        Translatable::from(cont.clone())
578                            .add("within(")
579                            .to_r(&e2)
580                            .add(", { ")
581                            .add(&at.get_argument())
582                            .add(" <- ")
583                            .to_r(&at.get_value())
584                            .add(" })")
585                            .into()
586                    }
587                    Lang::DataFrame { value: fields, .. } => {
588                        let at = fields[0].clone();
589                        Translatable::from(cont.clone())
590                            .add("within(")
591                            .to_r(&e2)
592                            .add(", { ")
593                            .add(&at.get_argument())
594                            .add(" <- ")
595                            .to_r(&at.get_value())
596                            .add(" })")
597                            .into()
598                    }
599                    Lang::FunctionApp {
600                        identifier: var,
601                        arguments: v,
602                        help_data: h,
603                    } => {
604                        let v = [e1].iter().chain(v.iter()).cloned().collect();
605                        Lang::FunctionApp {
606                            identifier: var,
607                            arguments: v,
608                            help_data: h,
609                        }
610                        .to_r(cont)
611                    }
612                    _ => Translatable::from(cont.clone())
613                        .to_r(&e2)
614                        .add("[[")
615                        .add("]]")
616                        .to_r(&e1)
617                        .into(),
618                }
619            }
620            Lang::Operator {
621                operator: Op::Dollar(_),
622                rhs: e1,
623                lhs: e2,
624                ..
625            } => {
626                let e1 = (**e1).clone();
627                let e2 = (**e2).clone();
628                let t1 = typing(cont, &e1).value;
629                let val = match (t1.clone(), e2.clone()) {
630                    (Type::Vec(vtype, _, _, _), Lang::Variable { name, .. })
631                        if vtype.is_array() =>
632                    {
633                        format!("vec_apply(get, {}, typed_vec('{}'))", e1.to_r(cont).0, name)
634                    }
635                    (Type::Vec(VecType::S3, _, _, _), Lang::Variable { name, .. }) => {
636                        let name_str = name.replace("__", ".");
637                        format!("get({}, '{}')", e1.to_r(cont).0, name_str)
638                    }
639                    (_, Lang::Variable { name, .. }) => format!("{}${}", e1.to_r(cont).0, name),
640                    _ => format!("{}${}", e1.to_r(cont).0, e2.to_r(cont).0),
641                };
642                (val, cont.clone())
643            }
644            Lang::Operator {
645                operator: op,
646                rhs: e1,
647                lhs: e2,
648                ..
649            } => {
650                let op_str = format!(" {} ", op);
651                Translatable::from(cont.clone())
652                    .to_r(e1)
653                    .add(&op_str)
654                    .to_r(e2)
655                    .into()
656            }
657            Lang::Scope { body: exps, .. } => Translatable::from(cont.clone())
658                .add("{\n")
659                .join(exps, "\n")
660                .add("\n}")
661                .into(),
662            Lang::Function {
663                parameters: params,
664                body,
665                ..
666            } => {
667                let fn_type = FunctionType::try_from(typing(cont, self).value.clone())
668                    .expect("function expression should have a function type");
669                let return_type = fn_type.get_return_type();
670
671                // Record alias constructors take specific named fields — calling
672                // TypeName(single_value) would fail.  The body already constructs
673                // the correct type (via ConstructorCall or List), so skip the
674                // output conversion for record aliases.
675                let is_record_alias_return = match &return_type {
676                    Type::Alias(alias_name, _, _, _) => cont
677                        .aliases()
678                        .find(|(var, _)| var.get_name() == *alias_name)
679                        .map(|(_, t)| matches!(t, Type::Record(_, _)))
680                        .unwrap_or(false),
681                    _ => false,
682                };
683                let output_conversion = if is_record_alias_return {
684                    "".to_string()
685                } else {
686                    cont.get_type_anotation(&return_type)
687                };
688
689                let has_variadic = params.last().map(|p| p.is_variadic()).unwrap_or(false);
690                let list_of_types = params
691                    .iter()
692                    .map(ArgumentType::body_type)
693                    .collect::<Vec<_>>();
694                let sub_context = params
695                    .iter()
696                    .map(|arg_typ| arg_typ.clone().set_type(arg_typ.body_type()).to_var(cont))
697                    .zip(list_of_types.clone())
698                    .fold(cont.clone(), |context: Context, (var, typ)| {
699                        context.clone().push_var_type(var, typ, &context)
700                    });
701                let res = if output_conversion.is_empty() {
702                    "".to_string()
703                } else {
704                    " |> ".to_owned() + &output_conversion
705                };
706                let body_r = body.to_r(&sub_context).0;
707                let final_body_r = if has_variadic {
708                    let vname = params.last().unwrap().get_argument_str();
709                    // The body sees the variadic param as `[#N, T]`, so collect
710                    // the R `...` into a `typed_vec` to match the S3 dispatch the
711                    // stdlib array functions (`sum`, `map`, `length`, …) rely on.
712                    let collector = "typed_vec(..., dim = c(...length()))";
713                    // inject `vname <- typed_vec(...)` after opening `{`
714                    if let Some(rest) = body_r.strip_prefix('{') {
715                        format!("{{\n{} <- {}{}", vname, collector, rest)
716                    } else {
717                        body_r
718                    }
719                } else {
720                    body_r
721                };
722                (
723                    format!(
724                        "(function({}) {}{}) |> {}",
725                        params
726                            .iter()
727                            .map(|x| x.to_r(cont))
728                            .collect::<Vec<_>>()
729                            .join(", "),
730                        final_body_r,
731                        res,
732                        cont.get_type_anotation(&fn_type.into())
733                    ),
734                    cont.clone(),
735                )
736            }
737            Lang::Variable { .. } => {
738                //Here we only keep the variable name, the path and the type
739                let var = Var::from_language(self.clone()).unwrap();
740                let name = if var.contains("__") {
741                    var.replace("__", ".").get_name()
742                } else {
743                    var.display_type(cont).get_name()
744                };
745                (name.to_string(), cont.clone())
746            }
747            Lang::FunctionApp {
748                identifier: exp,
749                arguments: vals,
750                ..
751            } => {
752                let var = Var::try_from(exp.clone()).unwrap();
753
754                let (exp_str, cont1) = exp.to_r(cont);
755                let fn_t = FunctionType::try_from(
756                    cont1
757                        .get_type_from_variable(&var)
758                        .unwrap_or_else(|_| panic!("variable {} don't have a related type", var)),
759                )
760                .map(|ft| ft.adjust_nb_parameters(vals.len()))
761                .expect("function application identifier should have a function type");
762                let new_args = fn_t
763                    .get_param_types()
764                    .iter()
765                    .map(|arg| reduce_type(&cont1, arg))
766                    .collect::<Vec<_>>();
767                let new_vals = vals
768                    .iter()
769                    .zip(new_args.iter())
770                    .map(set_related_type_if_variable)
771                    .collect::<Vec<_>>();
772                let (args, current_cont) = Translatable::from(cont1).join(&new_vals, ", ").into();
773                Var::from_language(*exp.clone())
774                    .map(|var| {
775                        let name = var.get_name();
776                        let new_name = if &name[0..1] == "%" {
777                            format!("`{}`", name.replace("__", "."))
778                        } else {
779                            name.replace("__", ".")
780                        };
781                        (format!("{}({})", new_name, args), current_cont.clone())
782                    })
783                    .unwrap_or((format!("{}({})", exp_str, args), current_cont))
784            }
785            Lang::VecFunctionApp {
786                vector_type,
787                identifier: exp,
788                arguments: vals,
789                ..
790            } => {
791                let var = Var::try_from(exp.clone()).unwrap();
792                let name = var.get_name();
793                let str_vals = vals
794                    .iter()
795                    .map(|x| x.to_r(cont).0)
796                    .collect::<Vec<_>>()
797                    .join(", ");
798                if *vector_type == VecType::Vector {
799                    // `Vec[N, T]` values transpile to plain R atomic vectors (see
800                    // vectors.md): R already vectorizes arithmetic/comparison
801                    // operators and ordinary scalar functions over them natively,
802                    // so no `vec_apply` (typed_vec normalization + manual
803                    // recycling, needed for the `[N, T]` / S3-array mechanism) is
804                    // required here — just call the function plainly.
805                    if cont.is_an_untyped_function(&name) {
806                        let name = name.replace("__", ".");
807                        let new_name = if &name[0..1] == "%" {
808                            format!("`{}`", name)
809                        } else {
810                            name.to_string()
811                        };
812                        (format!("{}({})", new_name, str_vals), cont.clone())
813                    } else {
814                        let (exp_str, cont1) = exp.to_r(cont);
815                        let fn_t = FunctionType::try_from(
816                            cont1.get_type_from_variable(&var).unwrap_or_else(|_| {
817                                panic!("variable {} don't have a related type", var)
818                            }),
819                        )
820                        .expect(
821                            "vector function application identifier should have a function type",
822                        );
823                        let new_args = fn_t
824                            .get_param_types()
825                            .iter()
826                            .map(|arg| reduce_type(&cont1, arg))
827                            .collect::<Vec<_>>();
828                        let new_vals = vals
829                            .iter()
830                            .zip(new_args.iter())
831                            .map(set_related_type_if_variable)
832                            .collect::<Vec<_>>();
833                        let (args, current_cont) =
834                            Translatable::from(cont1).join(&new_vals, ", ").into();
835                        Var::from_language(*exp.clone())
836                            .map(|var| {
837                                let name = var.get_name();
838                                let new_name = if &name[0..1] == "%" {
839                                    format!("`{}`", name.replace("__", "."))
840                                } else {
841                                    name.replace("__", ".")
842                                };
843                                (format!("{}({})", new_name, args), current_cont.clone())
844                            })
845                            .unwrap_or((format!("{}({})", exp_str, args), current_cont))
846                    }
847                } else if name == "reduce" {
848                    (format!("vec_reduce({})", str_vals), cont.clone())
849                } else if name == "extend" {
850                    (format!("vec_extend({})", str_vals), cont.clone())
851                } else if cont.is_an_untyped_function(&name) {
852                    let name = name.replace("__", ".");
853                    let new_name = if &name[0..1] == "%" {
854                        format!("`{}`", name)
855                    } else {
856                        name.to_string()
857                    };
858                    let s = format!("vec_apply({}, {})", new_name, str_vals);
859                    (s, cont.clone())
860                } else {
861                    let (exp_str, cont1) = exp.to_r(cont);
862                    let fn_t = FunctionType::try_from(
863                        cont1.get_type_from_variable(&var).unwrap_or_else(|_| {
864                            panic!("variable {} don't have a related type", var)
865                        }),
866                    )
867                    .expect("vector function application identifier should have a function type");
868                    let new_args = fn_t
869                        .get_param_types()
870                        .iter()
871                        .map(|arg| reduce_type(&cont1, arg))
872                        .collect::<Vec<_>>();
873                    let new_vals = vals
874                        .iter()
875                        .zip(new_args.iter())
876                        .map(set_related_type_if_variable)
877                        .collect::<Vec<_>>();
878                    let (args, current_cont) =
879                        Translatable::from(cont1).join(&new_vals, ", ").into();
880                    Var::from_language(*exp.clone())
881                        .map(|var| {
882                            let name = var.get_name();
883                            let new_name = if &name[0..1] == "%" {
884                                format!("`{}`", name.replace("__", "."))
885                            } else {
886                                name.replace("__", ".")
887                            };
888                            (
889                                format!("vec_apply({}, {})", new_name, args),
890                                current_cont.clone(),
891                            )
892                        })
893                        .unwrap_or((format!("vec_apply({}, {})", exp_str, args), current_cont))
894                }
895            }
896            Lang::ArrayIndexing {
897                identifier: exp,
898                indexing: val,
899                ..
900            } => {
901                let (exp_str, _) = exp.to_r(cont);
902                // v[-n] → v[[length(v) + (1 - n)]] (count from end)
903                let negative_idx = val.get_members_if_array().and_then(|members| {
904                    if members.len() == 1 {
905                        if let Lang::Integer { value: i, .. } = &members[0] {
906                            if *i < 0 {
907                                Some(*i)
908                            } else {
909                                None
910                            }
911                        } else {
912                            None
913                        }
914                    } else {
915                        None
916                    }
917                });
918                let res = if let Some(neg) = negative_idx {
919                    let offset = 1 + neg; // e.g. -1 → 0, -2 → -1
920                    if offset == 0 {
921                        format!("{}[[length({})]]", exp_str, exp_str)
922                    } else if offset < 0 {
923                        format!("{}[[length({}) - {}L]]", exp_str, exp_str, -offset)
924                    } else {
925                        format!("{}[[length({}) + {}L]]", exp_str, exp_str, offset)
926                    }
927                } else {
928                    let (val_str, _) = val.to_simple_r(cont);
929                    format!("{}[[{}]]", exp_str, val_str)
930                };
931                (res, cont.clone())
932            }
933            Lang::GenFunc { name: func, .. } => (
934                format!("function(x, ...) UseMethod('{}')", func),
935                cont.clone(),
936            ),
937            Lang::Let {
938                variable: expr,
939                r#type: ttype,
940                expression: body,
941                is_public: _,
942                is_testable: _,
943                is_export,
944                help_data: _,
945            } => {
946                let (body_str, new_cont) = body.to_r(cont);
947                let new_name = format_backtick(expr.clone().to_r(cont).0);
948
949                let (r_code, _new_name2) = Function::try_from((**body).clone())
950                    .map(|_| {
951                        let related_type = Var::try_from(expr)
952                            .ok()
953                            .map(|v| v.get_type())
954                            .filter(|t| !matches!(t, Type::Empty(_) | Type::UnknownFunction(_)))
955                            .unwrap_or_else(|| typing(cont, expr).value);
956                        let method = match cont.get_environment() {
957                            Environment::Project => format!(
958                                "#' @method {}\n",
959                                new_name.replace(".", " ").replace("`", "")
960                            ),
961                            _ => "".to_string(),
962                        };
963                        match related_type {
964                            Type::Empty(_) => {
965                                (format!("{} <- {}", new_name, body_str), new_name.clone())
966                            }
967                            // `Type::Any` is the one case `display_type` (which produced
968                            // `new_name` above) deliberately leaves unsuffixed — see its
969                            // own `Type::Empty(_) | Type::Any(_) => ""` arm — so `.default`
970                            // must be appended explicitly here. Bare/kinded generics
971                            // (`Type::Generic`, `Type::KindedGen`) are NOT special-cased:
972                            // `display_type` already resolved them to `name.default` via
973                            // `get_class`'s "default" fallback, so they fall through to
974                            // the catch-all `_` arm below and use `new_name` as-is.
975                            Type::Any(_) => (
976                                format!("{}.default <- {}", new_name, body_str),
977                                new_name.clone(),
978                            ),
979                            _ => (
980                                format!("{}{} <- {}", method, new_name, body_str),
981                                new_name.clone(),
982                            ),
983                        }
984                    })
985                    .unwrap_or((format!("{} <- {}", new_name, body_str), new_name));
986                let code = if !ttype.is_empty() {
987                    let type_annotation = new_cont.get_type_anotation(ttype);
988                    format!("{} |> {}\n", r_code, type_annotation)
989                } else {
990                    r_code + "\n"
991                };
992                // RFC-TR-032: @export prepends `#' @export` for R package API
993                let code = if *is_export {
994                    format!("#' @export\n{}", code)
995                } else {
996                    code
997                };
998                (code, new_cont)
999            }
1000            Lang::Array { .. } => {
1001                let typ = self.typing(cont).value;
1002
1003                let dimension = ArrayType::try_from(typ.clone())
1004                    .expect("array literal should have an array type")
1005                    .get_shape()
1006                    .map(|sha| format!("c({})", sha))
1007                    .unwrap_or_else(|| "c(0)".to_string());
1008
1009                let array = &self
1010                    .linearize_array()
1011                    .iter()
1012                    .map(|lang| lang.to_r(cont).0)
1013                    .collect::<Vec<_>>()
1014                    .join(", ")
1015                    .and_if(|lin_array| !lin_array.is_empty())
1016                    .map(|lin_array| format!("typed_vec({}, dim = {})", lin_array, dimension))
1017                    .unwrap_or_else(|| format!("typed_vec(dim = {})", dimension));
1018
1019                (
1020                    format!("{} |> {}", array, cont.get_type_anotation(&typ)),
1021                    cont.to_owned(),
1022                )
1023            }
1024            Lang::List {
1025                value: args,
1026                spreads,
1027                ..
1028            } if spreads.is_empty() => {
1029                let (body, current_cont) = Translatable::from(cont.clone())
1030                    .join_arg_val(args, ",\n ")
1031                    .into();
1032                let (typ, _, _) = typing(cont, self).to_tuple();
1033                // For record-alias types use the constructor directly
1034                if let Type::Alias(alias_name, _, _, _) = &typ {
1035                    let is_record = cont
1036                        .aliases()
1037                        .find(|(var, _)| var.get_name() == *alias_name)
1038                        .map(|(_, t)| matches!(t, Type::Record(_, _)))
1039                        .unwrap_or(false);
1040                    if is_record {
1041                        return (format!("{}({})", alias_name, body), current_cont);
1042                    }
1043                }
1044                let anotation = cont.get_type_anotation(&typ);
1045                cont.get_classes(&typ)
1046                    .map(|_| format!("list({}) |> {}", body, anotation))
1047                    .unwrap_or(format!("list({}) |> {}", body, anotation))
1048                    .to_some()
1049                    .map(|s| (s, current_cont))
1050                    .unwrap()
1051            }
1052            // Record literal with one or more `...source` spreads
1053            // (spread_operator2.md): merge spreads sequentially into `base`
1054            // with the runtime `spread()` helper, then apply explicit
1055            // fields as the final override — never via static field
1056            // expansion, so unknown/row-polymorphic fields carried by the
1057            // runtime value of `source` are preserved (§5-§7).
1058            Lang::List {
1059                value: args,
1060                spreads,
1061                ..
1062            } => {
1063                let mut spreads_iter = spreads.iter();
1064                let first = spreads_iter.next().expect("checked non-empty above");
1065                let (mut base, mut current_cont) = first.to_r(cont);
1066                for spread_expr in spreads_iter {
1067                    let (next, next_cont) = spread_expr.to_r(&current_cont);
1068                    base = format!("spread({}, {})", base, next);
1069                    current_cont = next_cont;
1070                }
1071                if args.is_empty() {
1072                    return (base, current_cont);
1073                }
1074                let (overrides, current_cont) = Translatable::from(current_cont)
1075                    .join_arg_val(args, ", ")
1076                    .into();
1077                (
1078                    format!("spread({}, list({}))", base, overrides),
1079                    current_cont,
1080                )
1081            }
1082            Lang::DataFrame { value: args, .. } => {
1083                let (body, current_cont) = Translatable::from(cont.clone())
1084                    .join_arg_val(args, ",\n ")
1085                    .into();
1086                let (typ, _, _) = typing(cont, self).to_tuple();
1087                let anotation = cont.get_type_anotation(&typ);
1088                cont.get_classes(&typ)
1089                    .map(|_| format!("data.frame({}) |> {}", body, anotation))
1090                    .unwrap_or(format!("data.frame({}) |> {}", body, anotation))
1091                    .to_some()
1092                    .map(|s| (s, current_cont))
1093                    .unwrap()
1094            }
1095            Lang::If {
1096                condition: cond,
1097                if_block: exp,
1098                else_block: els,
1099                ..
1100            } if els == &Box::new(Lang::Empty(HelpData::default())) => {
1101                Translatable::from(cont.clone())
1102                    .add("if(")
1103                    .to_r(cond)
1104                    .add(") {\n")
1105                    .to_r(exp)
1106                    .add(" \n}")
1107                    .into()
1108            }
1109            Lang::If {
1110                condition: cond,
1111                if_block: exp,
1112                else_block: els,
1113                help_data: _,
1114            } => Translatable::from(cont.clone())
1115                .add("if(")
1116                .to_r(cond)
1117                .add(") {\n")
1118                .to_r(exp)
1119                .add(" \n} else ")
1120                .to_r(els)
1121                .into(),
1122            Lang::Tuple { value: vals, .. } => Translatable::from(cont.clone())
1123                .add("struct(list(")
1124                .join(vals, ", ")
1125                .add("), 'Tuple')")
1126                .into(),
1127            Lang::Assign {
1128                identifier: var,
1129                expression: exp,
1130                ..
1131            } => Translatable::from(cont.clone())
1132                .to_r(var)
1133                .add(" <- ")
1134                .to_r(exp)
1135                .into(),
1136            Lang::Comment { value: txt, .. } => ("#".to_string() + txt, cont.clone()),
1137            Lang::Tag {
1138                name: s, value: t, ..
1139            } => {
1140                let (t_str, new_cont) = t.to_r(cont);
1141                let is_empty = matches!(t.as_ref(), Lang::Empty(_));
1142                // Canonical representation (see validation_variant_d_union.md §2):
1143                // tag identity in position 1, payload under `body`, class enriched
1144                // with the union name (when the tag belongs to a declared union)
1145                // plus `Tag`/`list`. This makes the literal interchangeable with
1146                // the value produced by the variant constructor `V(...)`, so
1147                // `match` and the variant validators apply to both origins.
1148                let class = match find_union_for_tag(s, cont) {
1149                    Some(union_name) => format!("c('{}', '{}', 'Tag', 'list')", s, union_name),
1150                    None => format!("c('{}', 'Tag', 'list')", s),
1151                };
1152                let value = if is_empty {
1153                    format!("structure(list('{}'), class = {})", s, class)
1154                } else {
1155                    format!(
1156                        "structure(list('{}', body = {}), class = {})",
1157                        s, t_str, class
1158                    )
1159                };
1160                (value, new_cont)
1161            }
1162            Lang::Null(_) => ("NULL".to_string(), cont.clone()),
1163            Lang::Empty(_) => ("NA".to_string(), cont.clone()),
1164            Lang::Lines { value: exps, .. } => {
1165                Translatable::from(cont.clone()).join(exps, "\n").into()
1166            }
1167            Lang::Return { value: exp, .. } => Translatable::from(cont.clone())
1168                .add("return ")
1169                .to_r(exp)
1170                .into(),
1171            Lang::Lambda {
1172                parameters: params,
1173                body: bloc,
1174                ..
1175            } => {
1176                let param_names: Vec<String> = params
1177                    .iter()
1178                    .map(|p: &Lang| match p {
1179                        Lang::Variable { name, .. } => name.clone(),
1180                        _ => "x".to_string(),
1181                    })
1182                    .collect();
1183                (
1184                    format!(
1185                        "function({}) {{ {} }}",
1186                        param_names.join(", "),
1187                        bloc.to_r(cont).0
1188                    ),
1189                    cont.clone(),
1190                )
1191            }
1192            Lang::VecBlock { value: bloc, .. } => (bloc.to_string(), cont.clone()),
1193            Lang::Library { value: name, .. } => (format!("library({})", name), cont.clone()),
1194            Lang::Match {
1195                target: exp,
1196                branches,
1197                ..
1198            } => (
1199                to_pattern_match_statement((**exp).clone(), branches, cont),
1200                cont.clone(),
1201            ),
1202            Lang::Exp { value: exp, .. } => (exp.clone(), cont.clone()),
1203            Lang::ForLoop {
1204                identifier: var,
1205                expression: iterator,
1206                body,
1207                ..
1208            } => Translatable::from(cont.clone())
1209                .add("for (")
1210                .to_r_safe(var)
1211                .add(" in ")
1212                .to_r_safe(iterator)
1213                .add(") {\n")
1214                .to_r_safe(body)
1215                .add("\n}")
1216                .into(),
1217            Lang::RFunction {
1218                parameters: vars,
1219                body,
1220                ..
1221            } => Translatable::from(cont.clone())
1222                .add("function (")
1223                .join(vars, ", ")
1224                .add(") \n")
1225                .add(body)
1226                .add("\n")
1227                .into(),
1228            Lang::Signature { .. } => ("".to_string(), cont.clone()),
1229            Lang::TypeConstructor { .. } => ("".to_string(), cont.clone()),
1230            Lang::Alias {
1231                identifier: ident,
1232                target_type: typ,
1233                ..
1234            } => {
1235                let name = Var::from_language(*ident.clone())
1236                    .map(|v| v.get_name())
1237                    .unwrap_or_default();
1238                // An alias mixing a record-kinded generic with a concrete
1239                // record (e.g. `type Animator<%T> <- %T & list { animations:
1240                // [Animation] }`) has no fixed runtime shape on the generic
1241                // side — TypR doesn't monomorphize, and R has no class for
1242                // "any record" — so only the concrete `Record` side carries
1243                // fields worth validating. Substitute the alias's type with
1244                // that concrete side so it falls into the existing
1245                // `Type::Record` pipeline below (constructor/annotator/
1246                // validator), instead of the unrelated union-alias
1247                // `Type::Operator` catch-all, which doesn't apply here and
1248                // previously produced no R code at all for this shape,
1249                // leaving `validate_Animator`/`as.Animator` referenced
1250                // elsewhere but never defined.
1251                let typ_for_dispatch: Type = match typ {
1252                    Type::Operator(TypeOperator::Intersection, t1, t2, _) => {
1253                        match (t1.reduce(cont), t2.reduce(cont)) {
1254                            (record @ Type::Record(_, _), other) if other.has_generic() => record,
1255                            (other, record @ Type::Record(_, _)) if other.has_generic() => record,
1256                            _ => typ.clone(),
1257                        }
1258                    }
1259                    _ => typ.clone(),
1260                };
1261                match &typ_for_dispatch {
1262                    Type::Record(fields, _) => {
1263                        let mut sorted_fields: Vec<&ArgumentType> = fields.iter().collect();
1264                        sorted_fields.sort_by_key(|f| f.get_argument_str());
1265                        let params = sorted_fields
1266                            .iter()
1267                            .map(|f| f.get_argument_str())
1268                            .collect::<Vec<_>>()
1269                            .join(", ");
1270                        // Each field is only added to `explicit` when the caller
1271                        // actually supplied it (`missing()`, not a NULL default):
1272                        // a record-typed `.spread` (spread_operator3.md) may cover
1273                        // the field instead, and `missing()` never forces the
1274                        // argument promise, so unsupplied fields stay lazy/unevaluated.
1275                        let explicit_lines = sorted_fields
1276                            .iter()
1277                            .map(|f| {
1278                                let n = f.get_argument_str();
1279                                format!("  if (!missing({n})) explicit[[\"{n}\"]] <- {n}")
1280                            })
1281                            .collect::<Vec<_>>()
1282                            .join("\n");
1283                        // Constructor: collect the explicitly-supplied fields, merge
1284                        // them over `.spread` (explicit wins, extra spread fields are
1285                        // kept), then delegate entirely to the annotator. It neither
1286                        // adds classes nor validates.
1287                        let constructor = format!(
1288                            "{name} <- function({params}, .spread = NULL) {{\n  explicit <- list()\n{explicit_lines}\n  x <- typr_spread_record(explicit, .spread)\n  as.{name}(x)\n}}"
1289                        );
1290                        // Structural supertypes: any record alias whose fields are a
1291                        // strict subset of this alias's fields. They are included in
1292                        // the S3 class vector so that methods defined on the supertype
1293                        // dispatch correctly to subtype values.
1294                        //
1295                        // Candidates come from the whole-program `record_aliases`
1296                        // registry, not just `cont.aliases()`: the latter is scoped
1297                        // to the current module body, so a supertype declared in a
1298                        // sibling `mod` file (e.g. `Position` while transpiling
1299                        // `Circle` in another file) would otherwise never be found,
1300                        // even though R's S3 classes have no module privacy.
1301                        // Built as an ordered Vec (not a HashMap) and deduplicated by
1302                        // first occurrence, so candidate order — and therefore the
1303                        // final sort below — stays deterministic across runs.
1304                        let mut seen_names: std::collections::HashSet<String> =
1305                            std::collections::HashSet::new();
1306                        let candidates: Vec<(String, Type)> = cont
1307                            .aliases()
1308                            .map(|(var, typ)| (var.get_name(), typ.clone()))
1309                            .chain(cont.record_aliases.iter().cloned())
1310                            .filter(|(other_name, _)| seen_names.insert(other_name.clone()))
1311                            .collect();
1312                        let mut supertype_entries: Vec<(String, usize)> = candidates
1313                            .into_iter()
1314                            .filter_map(|(other_name, typ)| {
1315                                if other_name == name {
1316                                    return None;
1317                                }
1318                                if let Type::Record(other_fields, _) = typ {
1319                                    if fields.is_superset(&other_fields) && other_fields != *fields
1320                                    {
1321                                        Some((other_name, other_fields.len()))
1322                                    } else {
1323                                        None
1324                                    }
1325                                } else {
1326                                    None
1327                                }
1328                            })
1329                            .collect();
1330                        // More-specific supertypes (more fields) first for correct S3
1331                        // dispatch order; ties broken by name for determinism.
1332                        supertype_entries.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
1333                        let supertype_class_str = if supertype_entries.is_empty() {
1334                            String::new()
1335                        } else {
1336                            let names = supertype_entries
1337                                .iter()
1338                                .map(|(n, _)| format!("\"{n}\""))
1339                                .collect::<Vec<_>>()
1340                                .join(", ");
1341                            format!(", {names}")
1342                        };
1343                        // Annotator: the single entry point that adds the class
1344                        // (idempotently), runs the internal validator, then the
1345                        // user validator (`validate` S3 generic, default = identity).
1346                        let annotator = format!(
1347                            "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}}"
1348                        );
1349                        let fields_quoted = sorted_fields
1350                            .iter()
1351                            .map(|f| format!("\"{}\"", f.get_argument_str()))
1352                            .collect::<Vec<_>>()
1353                            .join(", ");
1354                        // Per-field type invariants, checked by class (`inherits`).
1355                        // Fields whose type has no reliable nominal class are only
1356                        // checked for presence (above).
1357                        let field_checks = sorted_fields
1358                            .iter()
1359                            .filter_map(|f| {
1360                                let n = f.get_argument_str();
1361                                record_field_class(&f.body_type(), cont).map(|cls| {
1362                                    format!(
1363                                        "  if (!inherits(x[[\"{n}\"]], \"{cls}\")) stop(\"Validation failed for type {name}: field '{n}' must be of class {cls}\")"
1364                                    )
1365                                })
1366                            })
1367                            .collect::<Vec<_>>()
1368                            .join("\n");
1369                        let field_checks_block = if field_checks.is_empty() {
1370                            String::new()
1371                        } else {
1372                            format!("{field_checks}\n")
1373                        };
1374                        // Internal validator: pure structural invariants only.
1375                        // It must not reconstruct the value (that would recurse
1376                        // back through the constructor / annotator).
1377                        let validator = format!(
1378                            "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}}"
1379                        );
1380                        (
1381                            format!("{constructor}\n{annotator}\n{validator}"),
1382                            cont.clone(),
1383                        )
1384                    }
1385                    // Dataframe alias (`type Df <- dataframe[#N]{ ... }` /
1386                    // `df[3]{ ... }`): same constructor/annotator/validator
1387                    // pipeline as a record alias, but columns are assembled
1388                    // into a `data.frame` instead of a `list`, the class
1389                    // chain carries `"data.frame"`, and a concrete size
1390                    // index (`df[3]{...}`) additionally checks `nrow(x)`.
1391                    Type::Vec(VecType::DataFrame, size, fields_type, _)
1392                        if matches!(fields_type.as_ref(), Type::Record(_, _)) =>
1393                    {
1394                        use crate::components::r#type::tint::Tint;
1395                        let fields = match fields_type.as_ref() {
1396                            Type::Record(fields, _) => fields,
1397                            _ => unreachable!(),
1398                        };
1399                        let mut sorted_fields: Vec<&ArgumentType> = fields.iter().collect();
1400                        sorted_fields.sort_by_key(|f| f.get_argument_str());
1401                        let params = sorted_fields
1402                            .iter()
1403                            .map(|f| f.get_argument_str())
1404                            .collect::<Vec<_>>()
1405                            .join(", ");
1406                        let explicit_lines = sorted_fields
1407                            .iter()
1408                            .map(|f| {
1409                                let n = f.get_argument_str();
1410                                format!("  if (!missing({n})) explicit[[\"{n}\"]] <- {n}")
1411                            })
1412                            .collect::<Vec<_>>()
1413                            .join("\n");
1414                        // Constructor: collect the explicitly-supplied columns,
1415                        // merge them over `.spread`, assemble into a
1416                        // `data.frame`, then delegate to the annotator.
1417                        let constructor = format!(
1418                            "{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}}"
1419                        );
1420                        // Annotator: adds the class (idempotently), runs the
1421                        // internal validator, then the user validator.
1422                        let annotator = format!(
1423                            "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}}"
1424                        );
1425                        let fields_quoted = sorted_fields
1426                            .iter()
1427                            .map(|f| format!("\"{}\"", f.get_argument_str()))
1428                            .collect::<Vec<_>>()
1429                            .join(", ");
1430                        // Per-column type invariants, checked by class
1431                        // (`inherits`) on the column vector.
1432                        let field_checks = sorted_fields
1433                            .iter()
1434                            .filter_map(|f| {
1435                                let n = f.get_argument_str();
1436                                record_field_class(&f.body_type(), cont).map(|cls| {
1437                                    format!(
1438                                        "  if (!inherits(x[[\"{n}\"]], \"{cls}\")) stop(\"Validation failed for type {name}: column '{n}' must be of class {cls}\")"
1439                                    )
1440                                })
1441                            })
1442                            .collect::<Vec<_>>()
1443                            .join("\n");
1444                        let field_checks_block = if field_checks.is_empty() {
1445                            String::new()
1446                        } else {
1447                            format!("{field_checks}\n")
1448                        };
1449                        // Row-count check: only emitted when the size index is
1450                        // a concrete literal (`df[3]{...}`); a generic (`#N`)
1451                        // or unconstrained (`df{...}`) size imposes no check.
1452                        let size_check = if let Type::Integer(Tint::Val(n), _) = size.as_ref() {
1453                            format!(
1454                                "  if (nrow(x) != {n}) stop(paste0(\"Validation failed for type {name}: expected {n} rows, got \", nrow(x)))\n"
1455                            )
1456                        } else {
1457                            String::new()
1458                        };
1459                        let validator = format!(
1460                            "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}}"
1461                        );
1462                        (
1463                            format!("{constructor}\n{annotator}\n{validator}"),
1464                            cont.clone(),
1465                        )
1466                    }
1467                    // Vector alias (`type V <- Vec[#N, T]` / `Vec[3, T]` /
1468                    // `Vec[T]`): no class is added (R already distinguishes
1469                    // atomic vector kinds via `is.*`/implicit class), so the
1470                    // pipeline is the same shape as a primitive alias —
1471                    // constructor delegates straight to the validator — plus
1472                    // an optional length check when the size index is a
1473                    // concrete literal.
1474                    Type::Vec(VecType::Vector, size, elem_type, _) => {
1475                        use crate::components::r#type::tint::Tint;
1476                        let constructor =
1477                            format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
1478                        let elem_check = record_field_class(elem_type.as_ref(), cont).map(|cls| {
1479                            format!(
1480                                "  if (!inherits(x, \"{cls}\")) stop(\"Validation failed for type {name}: expected vector of {cls}\")\n"
1481                            )
1482                        }).unwrap_or_default();
1483                        let size_check = if let Type::Integer(Tint::Val(n), _) = size.as_ref() {
1484                            format!(
1485                                "  if (length(x) != {n}) stop(paste0(\"Validation failed for type {name}: expected length {n}, got \", length(x)))\n"
1486                            )
1487                        } else {
1488                            String::new()
1489                        };
1490                        let validator = format!(
1491                            "validate_{name} <- function(x) {{\n{elem_check}{size_check}  x\n}}"
1492                        );
1493                        (format!("{constructor}\n{validator}"), cont.clone())
1494                    }
1495                    // Array alias (`type A <- [#N, T]` / `[3, T]` / bare
1496                    // brackets, and the explicit `Array[#N, T]` spelling —
1497                    // both `VecType::S3` and `VecType::Array` produce the
1498                    // same runtime shape): the literal/constructor-call
1499                    // transpilation already wraps elements in `typed_vec`
1500                    // (see `Lang::Array`/`Lang::ArrayConstructorCall`), so
1501                    // this alias just needs to register the alias name as a
1502                    // `typed_vec` subclass — same annotator/validator shape
1503                    // as a record alias — so generic functions written
1504                    // against `typed_vec` dispatch on it.
1505                    Type::Vec(VecType::S3, size, elem_type, _)
1506                    | Type::Vec(VecType::Array, size, elem_type, _) => {
1507                        use crate::components::r#type::tint::Tint;
1508                        let constructor = format!(
1509                            "{name} <- function(x) {{\n  if (!inherits(x, \"typed_vec\")) x <- typed_vec(x)\n  as.{name}(x)\n}}"
1510                        );
1511                        let annotator = format!(
1512                            "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}}"
1513                        );
1514                        let elem_check = record_field_class(elem_type.as_ref(), cont).map(|cls| {
1515                            format!(
1516                                "  if (!all(vapply(x$data, inherits, logical(1), \"{cls}\"))) stop(\"Validation failed for type {name}: expected elements of class {cls}\")\n"
1517                            )
1518                        }).unwrap_or_default();
1519                        let size_check = if let Type::Integer(Tint::Val(n), _) = size.as_ref() {
1520                            format!(
1521                                "  if (length(x) != {n}) stop(paste0(\"Validation failed for type {name}: expected length {n}, got \", length(x)))\n"
1522                            )
1523                        } else {
1524                            String::new()
1525                        };
1526                        let validator = format!(
1527                            "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}}"
1528                        );
1529                        (
1530                            format!("{constructor}\n{annotator}\n{validator}"),
1531                            cont.clone(),
1532                        )
1533                    }
1534                    Type::Operator(_, _, _, _) => {
1535                        // Union alias: generate the full constructor/annotator/
1536                        // validator pipeline for each variant (see
1537                        // validation_variant_d_union.md).
1538                        let union_name = &name;
1539                        let members = flatten_operator_union(typ);
1540                        // Sort for deterministic output
1541                        let mut members_vec: Vec<Type> = members.into_iter().collect();
1542                        members_vec.sort_by_key(|t| t.pretty2());
1543                        let constructors: Vec<String> = members_vec
1544                            .iter()
1545                            .filter_map(|member| match member {
1546                                Type::Tag(variant_name, inner, _) => Some(tag_variant_pipeline(
1547                                    variant_name,
1548                                    union_name,
1549                                    inner.as_ref(),
1550                                )),
1551                                Type::Alias(alias_name, _, _, _) => {
1552                                    // Look up the record fields for this alias
1553                                    let record_fields = cont
1554                                        .aliases()
1555                                        .find(|(var, _)| var.get_name() == *alias_name)
1556                                        .and_then(|(_, t)| {
1557                                            if let Type::Record(fields, _) = t {
1558                                                Some(fields.clone())
1559                                            } else {
1560                                                None
1561                                            }
1562                                        });
1563                                    if let Some(fields) = record_fields {
1564                                        let mut sorted: Vec<&ArgumentType> =
1565                                            fields.iter().collect();
1566                                        sorted.sort_by_key(|f| f.get_argument_str());
1567                                        let params = sorted
1568                                            .iter()
1569                                            .map(|f| f.get_argument_str())
1570                                            .collect::<Vec<_>>()
1571                                            .join(", ");
1572                                        let field_args = sorted
1573                                            .iter()
1574                                            .map(|f| {
1575                                                let n = f.get_argument_str();
1576                                                format!("{n} = {n}")
1577                                            })
1578                                            .collect::<Vec<_>>()
1579                                            .join(", ");
1580                                        Some(format!(
1581                                            "{alias_name} <- function({params}) {{\n  structure(list({field_args}), class = c(\"{alias_name}\", \"{union_name}\", \"list\"))\n}}"
1582                                        ))
1583                                    } else {
1584                                        None
1585                                    }
1586                                }
1587                                _ => None,
1588                            })
1589                            .collect();
1590                        (constructors.join("\n"), cont.clone())
1591                    }
1592                    Type::Integer(tint, _) => {
1593                        use crate::components::r#type::tint::Tint;
1594                        let validator = match tint {
1595                            Tint::Val(i) => format!(
1596                                "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}}"
1597                            ),
1598                            Tint::Unknown => format!(
1599                                "validate_{name} <- function(x) {{\n  if (!is.integer(x)) stop(\"Validation failed for type {name}: expected int\")\n  x\n}}"
1600                            ),
1601                        };
1602                        let constructor =
1603                            format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
1604                        (format!("{constructor}\n{validator}"), cont.clone())
1605                    }
1606                    Type::Char(tchar, _) => {
1607                        use crate::components::r#type::tchar::Tchar;
1608                        let validator = match tchar {
1609                            Tchar::Val(s) => format!(
1610                                "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}}"
1611                            ),
1612                            Tchar::Unknown => format!(
1613                                "validate_{name} <- function(x) {{\n  if (!is.character(x)) stop(\"Validation failed for type {name}: expected char\")\n  x\n}}"
1614                            ),
1615                        };
1616                        let constructor =
1617                            format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
1618                        (format!("{constructor}\n{validator}"), cont.clone())
1619                    }
1620                    Type::Boolean(tbool, _) => {
1621                        use crate::components::r#type::tbool::Tbool;
1622                        let validator = match tbool {
1623                            Tbool::Val(b) => {
1624                                let r_val = if *b { "TRUE" } else { "FALSE" };
1625                                format!(
1626                                    "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}}"
1627                                )
1628                            }
1629                            Tbool::Unknown => format!(
1630                                "validate_{name} <- function(x) {{\n  if (!is.logical(x)) stop(\"Validation failed for type {name}: expected bool\")\n  x\n}}"
1631                            ),
1632                        };
1633                        let constructor =
1634                            format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
1635                        (format!("{constructor}\n{validator}"), cont.clone())
1636                    }
1637                    Type::Number(tnum, _) => {
1638                        use crate::components::r#type::tnumber::Tnum;
1639                        let validator = match tnum {
1640                            Tnum::Val(v) => format!(
1641                                "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}}"
1642                            ),
1643                            Tnum::Unknown => format!(
1644                                "validate_{name} <- function(x) {{\n  if (!is.numeric(x)) stop(\"Validation failed for type {name}: expected num\")\n  x\n}}"
1645                            ),
1646                        };
1647                        let constructor =
1648                            format!("{name} <- function(x) {{\n  validate_{name}(x)\n}}");
1649                        (format!("{constructor}\n{validator}"), cont.clone())
1650                    }
1651                    Type::Tag(tag_name, inner_type, _) => {
1652                        let body_validation = tag_body_validation(&name, inner_type.as_ref());
1653                        let validator = format!(
1654                            "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}}"
1655                        );
1656                        (validator, cont.clone())
1657                    }
1658                    // Alias-to-alias (`type Object <- Circle;`): transparent, so reuse
1659                    // the target's own constructor/validator pipeline under this name
1660                    // instead of generating nothing — otherwise the module export step
1661                    // below (`module$Object <- Object`) would reference a binding that
1662                    // was never created.
1663                    Type::Alias(target_name, ..) => {
1664                        (format!("{name} <- {target_name}"), cont.clone())
1665                    }
1666                    _ => ("".to_string(), cont.clone()),
1667                }
1668            }
1669            Lang::UnionConstructor {
1670                variant_name,
1671                fields,
1672                ..
1673            } => {
1674                if fields.is_empty() {
1675                    (format!("{}()", variant_name), cont.clone())
1676                } else {
1677                    let (body, current_cont) = Translatable::from(cont.clone())
1678                        .join_arg_val(fields, ", ")
1679                        .into();
1680                    (format!("{}({})", variant_name, body), current_cont)
1681                }
1682            }
1683            Lang::KeyValue {
1684                key: k, value: v, ..
1685            } => (format!("{} = {}", k, v.to_r(cont).0), cont.clone()),
1686            Lang::Vector { value: vals, .. } => {
1687                let res = "c(".to_string()
1688                    + &vals
1689                        .iter()
1690                        .map(|x: &Lang| x.to_r(cont).0)
1691                        .collect::<Vec<_>>()
1692                        .join(", ")
1693                    + ")";
1694                (res, cont.to_owned())
1695            }
1696            Lang::Not { value: exp, .. } => (format!("!{}", exp.to_r(cont).0), cont.clone()),
1697            Lang::Sequence { body: vals, .. } => {
1698                let res = if !vals.is_empty() {
1699                    "c(".to_string()
1700                        + &vals
1701                            .iter()
1702                            .map(|x: &Lang| "list(".to_string() + &x.to_r(cont).0 + ")")
1703                            .collect::<Vec<_>>()
1704                            .join(", ")
1705                        + ")"
1706                } else {
1707                    "c(list())".to_string()
1708                };
1709                (res, cont.to_owned())
1710            }
1711            Lang::TestBlock {
1712                value: body,
1713                help_data: h,
1714            } => {
1715                let file_name = h
1716                    .get_file_data()
1717                    .map(|(name, _)| format!("test-{}", name))
1718                    .unwrap_or_else(|| "test-unknown".to_string())
1719                    .replace("TypR/", "")
1720                    .replace(".ty", ".R");
1721
1722                let file_path = format!("tests/testthat/{}", file_name);
1723                let body_str = body.to_r(cont).0;
1724                // RFC-TR-031: bring `@testable` private members of the enclosing
1725                // module into scope (e.g. `sq <- Math$.test_sq`) so the test body
1726                // can call them by their bare names.
1727                let content = if cont.test_preamble.is_empty() {
1728                    body_str
1729                } else {
1730                    format!("{}\n{}", cont.test_preamble.join("\n"), body_str)
1731                };
1732
1733                let _ = write_output_file(&file_path, &content);
1734                ("".to_string(), cont.clone())
1735            }
1736            Lang::JSBlock(exp, _id, _h) => {
1737                let js_cont = Context::default(); //TODO get js context from memory
1738                let res = exp.to_js(&js_cont).0;
1739                (format!("'{}{}'", JS_HEADER, res), cont.clone())
1740            }
1741            Lang::WhileLoop {
1742                condition, body, ..
1743            } => (
1744                format!(
1745                    "while ({}) {{\n{}\n}}",
1746                    condition.to_r(cont).0,
1747                    body.to_r(cont).0
1748                ),
1749                cont.clone(),
1750            ),
1751            Lang::Loop { body, .. } => (
1752                format!("while (TRUE) {{\n{}\n}}", body.to_r(cont).0),
1753                cont.clone(),
1754            ),
1755            Lang::Break(_) => ("break".to_string(), cont.clone()),
1756            Lang::Next(_) => ("next".to_string(), cont.clone()),
1757            Lang::NA(_) => ("NA".to_string(), cont.clone()),
1758            Lang::Module {
1759                name,
1760                body,
1761                module_position: position,
1762                config,
1763                ..
1764            } => {
1765                let name_str = if (name == "main") && (config.environment == Environment::Project) {
1766                    "a_main"
1767                } else {
1768                    name
1769                };
1770
1771                // A module that writes its own roxygen2 file (External in Project)
1772                // gets a dedicated frame so the `@include` deps generated inside its
1773                // body land in *its* header rather than the enclosing file's.
1774                let writes_own_file = matches!(position, ModulePosition::External)
1775                    && config.environment == Environment::Project;
1776                if writes_own_file {
1777                    push_include_frame();
1778                }
1779
1780                // Re-derive the module's internal typing context so body elements
1781                // can resolve sibling definitions (e.g. a `Test { ... }` block or a
1782                // top-level call referencing an internal `let`). The outer `cont`
1783                // only knows the module itself, not its private members.
1784                let module_expr = if body.len() > 1 {
1785                    Lang::Lines {
1786                        value: body.to_vec(),
1787                        help_data: HelpData::default(),
1788                    }
1789                } else {
1790                    body.first()
1791                        .cloned()
1792                        .unwrap_or(Lang::Empty(HelpData::default()))
1793                };
1794                let mut inner_cont =
1795                    typing(&cont.clone().set_in_module_body(), &module_expr).context;
1796
1797                // RFC-TR-031: in a test build, give any `Test { ... }` block in this
1798                // module access to its `@testable` private members by binding their
1799                // bare names to the exposed `M$.test_<name>` aliases at the top of the
1800                // generated test file (see `Lang::TestBlock` below).
1801                if cont.get_test_mode() {
1802                    let preamble: Vec<String> = body
1803                        .iter()
1804                        .filter_map(|lang| match lang {
1805                            Lang::Let {
1806                                variable: var,
1807                                is_testable: true,
1808                                ..
1809                            } => Var::from_language(*var.clone()).map(|v| {
1810                                let raw = v.get_name();
1811                                format!("{} <- {}$`.test_{}`", raw, name_str, raw)
1812                            }),
1813                            _ => None,
1814                        })
1815                        .collect();
1816                    inner_cont = inner_cont.set_test_preamble(preamble);
1817                }
1818
1819                // C1: partition body into file-level imports (mod foo; / External sub-modules)
1820                // and runtime content. Imports are processed first so their side-effects
1821                // (file writes, register_include) happen before the new.env binding, and
1822                // their output is emitted outside the local({}) block.
1823                let (import_langs, runtime_langs): (Vec<_>, Vec<_>) =
1824                    body.iter().partition(|lang| {
1825                        matches!(
1826                            lang,
1827                            Lang::ModuleImport { .. }
1828                                | Lang::Module {
1829                                    module_position: ModulePosition::External,
1830                                    ..
1831                                }
1832                        )
1833                    });
1834
1835                let imports_parts: Vec<String> = import_langs
1836                    .iter()
1837                    .map(|lang| lang.to_r(&inner_cont).0)
1838                    .filter(|s| !s.is_empty())
1839                    .collect();
1840                let imports_preamble = if imports_parts.is_empty() {
1841                    String::new()
1842                } else {
1843                    imports_parts.join("\n") + "\n"
1844                };
1845
1846                let body_content = runtime_langs
1847                    .iter()
1848                    .map(|lang| lang.to_r(&inner_cont).0)
1849                    .collect::<Vec<_>>()
1850                    .join("\n");
1851
1852                // Build exports (inside local) and generics (outside local) for @pub/@export members
1853                let mut exports: Vec<String> = Vec::new();
1854                let mut generics: Vec<String> = Vec::new();
1855                let mut generic_exports: Vec<String> = Vec::new();
1856                // RFC-TR-032: @export members also get a top-level #' @export re-export
1857                let mut package_exports: Vec<String> = Vec::new();
1858
1859                for lang in body.iter() {
1860                    if let Lang::Let {
1861                        variable: var,
1862                        is_public: true,
1863                        is_export,
1864                        ..
1865                    } = lang
1866                    {
1867                        if let Some(v) = Var::from_language(*var.clone()) {
1868                            let raw_name = v.get_name();
1869                            // Use `inner_cont`, not `cont`: `body_content` above rendered
1870                            // this same `Let` via `inner_cont` (the re-typed module-body
1871                            // context), so `display_type`'s alias lookup must use the same
1872                            // context here or it can resolve the same structural type to a
1873                            // *different* auto-named alias (e.g. the function gets defined
1874                            // as `animate_move.Record4` but exported/registered as
1875                            // `animate_move.Record1` — a dangling reference at R runtime).
1876                            let typed_name = v.clone().display_type(&inner_cont).get_name();
1877
1878                            // Export the (possibly type-suffixed) member into the module env
1879                            exports.push(format!("{}${} <- {}", name_str, typed_name, typed_name));
1880
1881                            // RFC-TR-032: @export also surfaces as a package-level function
1882                            if *is_export {
1883                                package_exports.push(format!(
1884                                    "#' @export\n{} <- {}${}",
1885                                    raw_name, name_str, typed_name
1886                                ));
1887                            }
1888
1889                            // For typed functions: register as S3 method and create generic
1890                            let var_type = v.get_type();
1891                            if !var_type.is_empty() && typed_name != raw_name {
1892                                let class_name = inner_cont.get_class_unquoted(&var_type);
1893                                exports.push(format!(
1894                                    "registerS3method(\"{}\", \"{}\", {})",
1895                                    raw_name, class_name, typed_name
1896                                ));
1897
1898                                let generic_def = format!(
1899                                    "{} <- function(x, ...) UseMethod(\"{}\")",
1900                                    raw_name, raw_name
1901                                );
1902                                if !generics.contains(&generic_def) {
1903                                    generics.push(generic_def);
1904                                    generic_exports
1905                                        .push(format!("{}${} <- {}", name_str, raw_name, raw_name));
1906                                }
1907
1908                                // The method implementation itself (e.g. `do.Object`) is only
1909                                // bound inside `local({...})`, so it never becomes a top-level
1910                                // binding that load_module.R's dependency-copy step can see.
1911                                // `registerS3method` alone doesn't help here either: it relies
1912                                // on a real package namespace, which a `sys.source`'d module env
1913                                // is not. Re-expose it at top level (outside local) from the
1914                                // module env so UseMethod can find `typed_name` via normal
1915                                // lexical scoping from any file that imports this module.
1916                                generic_exports
1917                                    .push(format!("{} <- {}${}", typed_name, name_str, typed_name));
1918                            }
1919                        }
1920                    }
1921                    // RFC-TR-032: in a test build, expose `@testable` (and implied-testable
1922                    // `@pub`/`@export`) members as `M$.test_<name>` while keeping them
1923                    // private in the module's regular API.
1924                    if cont.get_test_mode() {
1925                        if let Lang::Let {
1926                            variable: var,
1927                            is_testable: true,
1928                            ..
1929                        } = lang
1930                        {
1931                            if let Some(v) = Var::from_language(*var.clone()) {
1932                                let raw_name = v.get_name();
1933                                // Reference the actual (possibly type-suffixed)
1934                                // binding emitted in the module body. Must use
1935                                // `inner_cont` for the same reason as above.
1936                                let typed_name = v.clone().display_type(&inner_cont).get_name();
1937                                exports.push(format!(
1938                                    "{}$`.test_{}` <- {}",
1939                                    name_str, raw_name, typed_name
1940                                ));
1941                            }
1942                        }
1943                    }
1944                    // Export @pub opaque type constructors into the module environment
1945                    if let Lang::Alias {
1946                        identifier: var,
1947                        is_public: true,
1948                        ..
1949                    } = lang
1950                    {
1951                        if let Some(v) = Var::from_language(*var.clone()) {
1952                            let alias_name = v.get_name();
1953                            exports.push(format!("{}${} <- {}", name_str, alias_name, alias_name));
1954                        }
1955                    }
1956                }
1957
1958                let exports_str = if exports.is_empty() {
1959                    String::new()
1960                } else {
1961                    "\n".to_string() + &exports.join("\n")
1962                };
1963
1964                let generics_defs_str = if generics.is_empty() {
1965                    String::new()
1966                } else {
1967                    generics.join("\n") + "\n"
1968                };
1969
1970                let generic_exports_str = if generic_exports.is_empty() {
1971                    String::new()
1972                } else {
1973                    "\n".to_string() + &generic_exports.join("\n")
1974                };
1975
1976                let package_exports_str = if package_exports.is_empty() {
1977                    String::new()
1978                } else {
1979                    "\n".to_string() + &package_exports.join("\n")
1980                };
1981
1982                let content = format!(
1983                    "{}{}{} <- new.env(parent = emptyenv())\nlocal({{\n{}{}\n}}){}{}",
1984                    generics_defs_str,
1985                    imports_preamble,
1986                    name_str,
1987                    body_content,
1988                    exports_str,
1989                    generic_exports_str,
1990                    package_exports_str
1991                );
1992
1993                match (position, config.environment) {
1994                    (ModulePosition::Internal, _) => (content, cont.clone()),
1995                    // In WASM mode, inline all external modules instead of writing files
1996                    (ModulePosition::External, Environment::Wasm) => {
1997                        let file_path = format!("{}.R", name_str);
1998                        let _ = write_output_file(&file_path, &content);
1999                        (content, cont.clone())
2000                    }
2001                    (ModulePosition::External, Environment::StandAlone)
2002                    | (ModulePosition::External, Environment::Repl) => {
2003                        let file_path = format!("{}.R", name_str);
2004                        let _ = write_output_file(&file_path, &content);
2005                        (format!("source('{}')", file_path), cont.clone())
2006                    }
2007                    (ModulePosition::External, Environment::Project) => {
2008                        let file_path = format!("R/{}.R", name_str);
2009                        // Drain the deps collected within this module's body and emit
2010                        // them as top-level `@include` tags in this file's header.
2011                        let nested = pop_include_frame();
2012                        let nested_includes = nested
2013                            .iter()
2014                            .map(|f| format!("#' @include {}\n", f))
2015                            .collect::<String>();
2016                        let project_preamble = "#' @include std.R\n#' @include generic_functions.R\n#' @include types.R\n";
2017                        let _ = write_output_file(
2018                            &file_path,
2019                            &format!("{}{}{}", project_preamble, nested_includes, content),
2020                        );
2021                        // The enclosing file depends on this one: hoist the tag to its
2022                        // header instead of emitting it inline inside a `local({...})`.
2023                        register_include(&format!("{}.R", name_str));
2024                        (String::new(), cont.clone())
2025                    }
2026                }
2027            }
2028            Lang::UseModule {
2029                module_path,
2030                selector,
2031                ..
2032            } => {
2033                use crate::components::language::use_lang::UseSelector;
2034
2035                // Build the R accessor prefix: A::B::C → A$B$C
2036                let r_path = module_path.join("$");
2037
2038                // Resolve the module type from context to enumerate public members for wildcards
2039                let mod_type_opt = (|| {
2040                    let root = cont
2041                        .get_type_from_variable(&Var::from_name(&module_path[0]))
2042                        .ok()?;
2043                    let mut current = root;
2044                    for seg in module_path.iter().skip(1) {
2045                        current = current
2046                            .to_module_type()
2047                            .ok()?
2048                            .get_type_from_name(seg)
2049                            .ok()?;
2050                    }
2051                    current.to_module_type().ok()
2052                })();
2053
2054                let bindings: Vec<String> = match selector {
2055                    UseSelector::Wildcard => mod_type_opt
2056                        .map(|mt| {
2057                            mt.get_public_members()
2058                                .iter()
2059                                .map(|m| {
2060                                    let name = m.get_argument_str();
2061                                    format!("{} <- {}${}", name, r_path, name)
2062                                })
2063                                .collect()
2064                        })
2065                        .unwrap_or_default(),
2066                    UseSelector::Items(items) => items
2067                        .iter()
2068                        .map(|item| {
2069                            let local_name = item.alias.as_deref().unwrap_or(&item.name);
2070                            format!("{} <- {}${}", local_name, r_path, item.name)
2071                        })
2072                        .collect(),
2073                };
2074
2075                (bindings.join("\n"), cont.clone())
2076            }
2077            Lang::ModuleImport { .. } => ("".to_string(), cont.clone()),
2078            // `Self:{ field = expr, ...base }` (generic_constructor.md §5):
2079            // resolve the actual R constructor straight from `base`'s type
2080            // rather than from the literal name "Self" — by the
2081            // type-checking rule `base`'s type is always Self or a subtype
2082            // of it, so this is exactly the constructor that should run.
2083            Lang::ConstructorCall {
2084                type_name,
2085                fields,
2086                spreads,
2087                ..
2088            } if type_name == "Self" => {
2089                let base_typ = spreads
2090                    .first()
2091                    .map(|e| typing(cont, e).value)
2092                    .unwrap_or_else(|| Type::Any(HelpData::default()));
2093                let resolved_name = match &base_typ {
2094                    Type::Alias(alias_name, ..) => cont
2095                        .aliases()
2096                        .find(|(var, _)| var.get_name() == *alias_name)
2097                        .map(|(_, t)| matches!(t, Type::Record(_, _)))
2098                        .unwrap_or(false)
2099                        .then(|| alias_name.clone()),
2100                    _ => None,
2101                };
2102                match (resolved_name, spreads.first()) {
2103                    (Some(name), Some(spread_expr)) => {
2104                        // Same codegen as the plain runtime-spread
2105                        // ConstructorCall path below, with the resolved
2106                        // alias name substituted for "Self".
2107                        let (spread_r, current_cont) = spread_expr.to_r(cont);
2108                        let (body, current_cont) = if fields.is_empty() {
2109                            (format!(".spread = {}", spread_r), current_cont)
2110                        } else {
2111                            let (overrides, next_cont) = Translatable::from(current_cont)
2112                                .join_arg_val(fields, ", ")
2113                                .into();
2114                            (format!("{}, .spread = {}", overrides, spread_r), next_cont)
2115                        };
2116                        (format!("{}({})", name, body), current_cont)
2117                    }
2118                    (None, Some(spread_expr)) => {
2119                        // No nominal constructor to call: fall back to the
2120                        // generic spread()-merge used for plain `{ f=e, ...x }`
2121                        // record literals.
2122                        let (base, current_cont) = spread_expr.to_r(cont);
2123                        if fields.is_empty() {
2124                            (base, current_cont)
2125                        } else {
2126                            let (overrides, current_cont) = Translatable::from(current_cont)
2127                                .join_arg_val(fields, ", ")
2128                                .into();
2129                            (
2130                                format!("spread({}, list({}))", base, overrides),
2131                                current_cont,
2132                            )
2133                        }
2134                    }
2135                    // No spread: ill-typed (type-checking already reports
2136                    // SelfOutsideContext); nothing sensible to emit.
2137                    (_, None) => ("NULL".to_string(), cont.clone()),
2138                }
2139            }
2140            Lang::ConstructorCall {
2141                module_path,
2142                type_name,
2143                fields,
2144                spreads,
2145                ..
2146            } if !spreads.is_empty() => {
2147                // Single runtime `...source` spread (spread_operator3.md): pass the
2148                // explicit fields plus the spread source straight to the generated
2149                // constructor's `.spread` parameter — it merges them at runtime via
2150                // `typr_spread_record` (explicit fields win, extra fields on the
2151                // source are preserved, unlike the old do.call/spread/select).
2152                let spread_expr = spreads.first().expect("checked non-empty above");
2153                let (spread_r, current_cont) = spread_expr.to_r(cont);
2154                let qualified = if module_path.is_empty() {
2155                    type_name.clone()
2156                } else {
2157                    format!("{}${}", module_path.join("$"), type_name)
2158                };
2159                let (body, current_cont) = if fields.is_empty() {
2160                    (format!(".spread = {}", spread_r), current_cont)
2161                } else {
2162                    let (overrides, next_cont) = Translatable::from(current_cont)
2163                        .join_arg_val(fields, ", ")
2164                        .into();
2165                    (format!("{}, .spread = {}", overrides, spread_r), next_cont)
2166                };
2167                (format!("{}({})", qualified, body), current_cont)
2168            }
2169            Lang::ConstructorCall {
2170                module_path,
2171                type_name,
2172                fields,
2173                spread,
2174                help_data: h,
2175                ..
2176            } => {
2177                // With a spread present, statically expand every record field absent
2178                // from `fields` into a `source$field` access (RFC-TR-033 §5: static
2179                // expansion, no runtime merge so the record's R class is preserved).
2180                let all_fields: Vec<ArgumentValue> = match spread {
2181                    Some((spread_path, spread_var, _)) => {
2182                        let resolved_alias = if module_path.is_empty() {
2183                            cont.get_type_from_aliases(&Var::from_name(type_name))
2184                        } else {
2185                            resolve_module_member_type(cont, module_path, type_name)
2186                        };
2187                        let record_fields = resolved_alias.and_then(|t| match t.reduce(cont) {
2188                            Type::Record(fields, _) => Some(fields),
2189                            _ => None,
2190                        });
2191                        let receiver = {
2192                            let qualifier = spread_path.split_first().map(|(first, rest)| {
2193                                rest.iter()
2194                                    .fold(Var::from_name(first).to_language(), |acc, seg| {
2195                                        Lang::Operator {
2196                                            operator: Op::Dollar(h.clone()),
2197                                            rhs: Box::new(acc),
2198                                            lhs: Box::new(Var::from_name(seg).to_language()),
2199                                            help_data: h.clone(),
2200                                        }
2201                                    })
2202                            });
2203                            match qualifier {
2204                                Some(qualifier) => Lang::Operator {
2205                                    operator: Op::Dollar(h.clone()),
2206                                    rhs: Box::new(qualifier),
2207                                    lhs: Box::new(Var::from_name(spread_var).to_language()),
2208                                    help_data: h.clone(),
2209                                },
2210                                None => Var::from_name(spread_var).to_language(),
2211                            }
2212                        };
2213                        let provided: std::collections::HashSet<String> =
2214                            fields.iter().map(|f| f.get_argument()).collect();
2215                        let synthetic = record_fields
2216                            .into_iter()
2217                            .flatten()
2218                            .filter(|rf| !provided.contains(&rf.get_argument_str()))
2219                            .map(|rf| {
2220                                let field_access = Lang::Operator {
2221                                    operator: Op::Dollar(h.clone()),
2222                                    rhs: Box::new(receiver.clone()),
2223                                    lhs: Box::new(
2224                                        Var::from_name(&rf.get_argument_str()).to_language(),
2225                                    ),
2226                                    help_data: h.clone(),
2227                                };
2228                                ArgumentValue(rf.get_argument_str(), field_access)
2229                            });
2230                        fields.iter().cloned().chain(synthetic).collect()
2231                    }
2232                    None => fields.clone(),
2233                };
2234                let (body, current_cont) = Translatable::from(cont.clone())
2235                    .join_arg_val(&all_fields, ", ")
2236                    .into();
2237                let qualified = if module_path.is_empty() {
2238                    type_name.clone()
2239                } else {
2240                    format!("{}${}", module_path.join("$"), type_name)
2241                };
2242                (format!("{}({})", qualified, body), current_cont)
2243            }
2244            Lang::ArrayConstructorCall {
2245                type_name,
2246                elements,
2247                help_data: h,
2248            } => {
2249                let resolved_alias = cont
2250                    .get_type_from_aliases(&Var::from_name(type_name))
2251                    .map(|t| t.reduce(cont));
2252                if let Some(Type::Vec(VecType::Vector, ..)) = resolved_alias {
2253                    // Plain vector alias (`type V <- Vec[#N, T]`): the runtime
2254                    // value is a bare R vector (no `dim`/`typed_vec` wrapper, see
2255                    // the Vec constructor pipeline in `Lang::Alias`), so the
2256                    // elements are simply collected with `c(...)`.
2257                    let inner = elements
2258                        .iter()
2259                        .map(|el| el.to_r(cont).0)
2260                        .collect::<Vec<_>>()
2261                        .join(", ");
2262                    (format!("{}(c({}))", type_name, inner), cont.clone())
2263                } else {
2264                    let temp_array = Lang::Array {
2265                        value: elements.clone(),
2266                        help_data: h.clone(),
2267                    };
2268                    let typ = temp_array.typing(cont).value;
2269                    let dimension = ArrayType::try_from(typ)
2270                        .expect("array constructor call should have an array type")
2271                        .get_shape()
2272                        .map(|sha| format!("c({})", sha))
2273                        .unwrap_or_else(|| "c(0)".to_string());
2274                    let lin_array = temp_array
2275                        .linearize_array()
2276                        .iter()
2277                        .map(|lang| lang.to_r(cont).0)
2278                        .collect::<Vec<_>>()
2279                        .join(", ");
2280                    let inner = if lin_array.is_empty() {
2281                        format!("typed_vec(dim = {})", dimension)
2282                    } else {
2283                        format!("typed_vec({}, dim = {})", lin_array, dimension)
2284                    };
2285                    (format!("{}({})", type_name, inner), cont.clone())
2286                }
2287            }
2288            Lang::Import { .. } | Lang::Test { .. } | Lang::Use { .. } => {
2289                ("".to_string(), cont.clone())
2290            }
2291            Lang::ValidatingCast {
2292                expression,
2293                type_name,
2294                literal_type,
2295                ..
2296            } => {
2297                let expr_r = expression.to_r(cont).0;
2298                match literal_type {
2299                    // Inline structural type (`as! [Any, int]` and friends):
2300                    // call the auto-generated `as.ArrayN`-style cast (see
2301                    // `Context::get_type_anotations`) registered for it at
2302                    // typing time, instead of a named `validate_<name>`.
2303                    Some(t) => (
2304                        format!("{} |> {}", expr_r, cont.get_type_anotation(t)),
2305                        cont.clone(),
2306                    ),
2307                    None => (format!("validate_{}({})", type_name, expr_r), cont.clone()),
2308                }
2309            }
2310            _ => ("".to_string(), cont.clone()),
2311        };
2312
2313        result
2314    }
2315}
2316
2317#[cfg(test)]
2318mod tests {
2319    use crate::components::context::config::{Config, Environment};
2320    use crate::components::context::Context;
2321    use crate::components::error_message::help_data::HelpData;
2322    use crate::components::language::{Lang, ModulePosition};
2323    use crate::processes::transpiling::translatable::RTranslatable;
2324    use crate::utils::fluent_parser::FluentParser;
2325
2326    #[test]
2327    fn test_escape_r_string() {
2328        use super::escape_r_string;
2329        assert_eq!(escape_r_string("hello"), r#""hello""#);
2330        assert_eq!(escape_r_string(r#"say "hi""#), r#""say \"hi\"""#);
2331        assert_eq!(escape_r_string(r"a\b"), r#""a\\b""#);
2332        assert_eq!(escape_r_string("line1\nline2"), r#""line1\nline2""#);
2333    }
2334
2335    #[test]
2336    fn test_validating_cast_transpiles_to_validate_call() {
2337        let r_code = FluentParser::new()
2338            .push("type Person <- list { name: char, age: int };")
2339            .run()
2340            .check_transpiling("x as! Person");
2341        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2342        assert!(
2343            r_str.contains("validate_Person(x)"),
2344            "expected validate_Person(x), got: {}",
2345            r_str
2346        );
2347    }
2348
2349    #[test]
2350    fn test_validating_cast_type_is_alias() {
2351        let typ = FluentParser::new()
2352            .push("type Person <- list { name: char, age: int };")
2353            .run()
2354            .check_typing("x as! Person");
2355        assert!(
2356            typ.pretty2().contains("Person"),
2357            "expected Alias(Person), got: {}",
2358            typ.pretty2()
2359        );
2360    }
2361
2362    #[test]
2363    fn test_validating_cast_literal_array_type() {
2364        let r_code = FluentParser::new().check_transpiling("c() as! [Any, int]");
2365        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2366        assert!(
2367            r_str.contains("c() |> as.Array0()"),
2368            "expected c() |> as.Array0(), got: {}",
2369            r_str
2370        );
2371    }
2372
2373    #[test]
2374    fn test_validating_cast_literal_vec_and_array_keywords() {
2375        // `Vec[...]` / `Array[...]` prefixes are equivalent to the bare
2376        // `[...]` form for an inline (non-aliased) cast target.
2377        let r_code = FluentParser::new().check_transpiling("c() as! Vec[Any, int]");
2378        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2379        assert!(
2380            r_str.contains("c() |> as.Array0()"),
2381            "expected c() |> as.Array0(), got: {}",
2382            r_str
2383        );
2384    }
2385
2386    #[test]
2387    fn test_validating_cast_literal_type_dedup() {
2388        // Two casts to the same structural type reuse the same auto-generated
2389        // alias instead of registering a new one each time.
2390        let r_code = FluentParser::new()
2391            .push("let a <- c() as! [Any, int];")
2392            .run()
2393            .push("let b <- c() as! [Any, int];")
2394            .run()
2395            .get_r_code();
2396        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2397        assert_eq!(
2398            r_str.matches("as.Array0()").count(),
2399            2,
2400            "expected both casts to reuse as.Array0, got: {}",
2401            r_str
2402        );
2403    }
2404
2405    #[test]
2406    fn test_alias_record_generates_validator() {
2407        let r_code =
2408            FluentParser::new().check_transpiling("type Person <- list { name: char, age: int };");
2409        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2410        assert!(
2411            r_str.contains("validate_Person <- function(x)"),
2412            "expected validator function, got: {}",
2413            r_str
2414        );
2415        assert!(
2416            r_str.contains("required_fields"),
2417            "expected field validation, got: {}",
2418            r_str
2419        );
2420    }
2421
2422    #[test]
2423    fn test_record_kinded_generic_param_transpiles_without_panic() {
2424        // Regression for a `get_class` panic ("%T has no class equivalent")
2425        // when a function parameter/alias is typed with a record-kinded
2426        // generic (`%T`), e.g. `type Animator<%T> <- %T & list {...}` plus
2427        // a function spreading a `%T`-typed parameter. `%T` has no fixed
2428        // runtime R class, so it must fall back to the same `Generic`
2429        // convention used for plain `T`, never panic.
2430        let fp = FluentParser::new()
2431            .push("type Animator<%T> <- %T & list { extra: int };")
2432            .run()
2433            .push("let combine <- fn(target: %T): %T { let more <- :{ extra = 1 }; :{ ...target, ...more } };")
2434            .run();
2435        assert_eq!(fp.get_last_log(), "The logs are empty");
2436        let r_code = fp
2437            .get_r_code()
2438            .iter()
2439            .cloned()
2440            .collect::<Vec<_>>()
2441            .join("\n");
2442        assert!(
2443            r_code.contains("spread(target, more)"),
2444            "expected the spread merge to transpile, got:\n{}",
2445            r_code
2446        );
2447    }
2448
2449    #[test]
2450    fn test_generic_intersection_alias_generates_validator() {
2451        // An alias mixing a record-kinded generic with a concrete record
2452        // (`%T & list {...}`) used to produce no R code at all for itself
2453        // (it fell into the union-alias `Type::Operator` catch-all), while
2454        // `as! Animator` elsewhere still emitted a call to `validate_Animator`
2455        // — a dangling reference at actual R runtime. The concrete side's
2456        // fields are the only part with a fixed runtime shape, so the
2457        // constructor/annotator/validator pipeline should be generated from
2458        // them, exactly like a plain `Type::Record` alias.
2459        let r_code = FluentParser::new()
2460            .check_transpiling("type Animator<%T> <- %T & list { animations: int };");
2461        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2462        assert!(
2463            r_str.contains("Animator <- function(animations, .spread = NULL)"),
2464            "expected a constructor for the concrete side's fields, got:\n{}",
2465            r_str
2466        );
2467        assert!(
2468            r_str.contains("as.Animator <- function(x)"),
2469            "expected an annotator, got:\n{}",
2470            r_str
2471        );
2472        assert!(
2473            r_str.contains("validate_Animator <- function(x)")
2474                && r_str.contains("required_fields <- c(\"animations\")"),
2475            "expected a validator checking the concrete field, got:\n{}",
2476            r_str
2477        );
2478    }
2479
2480    #[test]
2481    fn test_external_module_project_generates_include() {
2482        use super::{reset_include_stack, take_main_includes};
2483        reset_include_stack();
2484        let module = Lang::Module {
2485            name: "MyModule".to_string(),
2486            body: vec![],
2487            module_position: ModulePosition::External,
2488            config: Config::default().set_environment(Environment::Project),
2489            help_data: HelpData::default(),
2490        };
2491        let context = Context::default().set_environment(Environment::Project);
2492        let (r_code, _) = module.to_r(&context);
2493        // The include is no longer emitted inline; it is hoisted to the enclosing
2494        // file's header via the include stack.
2495        assert_eq!(r_code, "", "got: {}", r_code);
2496        let includes = take_main_includes();
2497        assert!(
2498            includes.contains(&"MyModule.R".to_string()),
2499            "expected MyModule.R to be registered, got: {:?}",
2500            includes
2501        );
2502    }
2503
2504    #[test]
2505    fn test_module_transpilation_with_pub() {
2506        let r_code = FluentParser::new()
2507            .push("module Math { let sq <- 2; @pub let pi <- 3; };")
2508            .run()
2509            .get_r_code()
2510            .iter()
2511            .cloned()
2512            .collect::<Vec<_>>()
2513            .join("\n");
2514        assert!(
2515            r_code.contains("Math <- new.env(parent = emptyenv())"),
2516            "missing env init: {}",
2517            r_code
2518        );
2519        assert!(
2520            r_code.contains("local({"),
2521            "missing local block: {}",
2522            r_code
2523        );
2524        assert!(
2525            r_code.contains("Math$pi <- pi"),
2526            "missing public export: {}",
2527            r_code
2528        );
2529        assert!(
2530            !r_code.contains("Math$sq"),
2531            "private member should not be exported: {}",
2532            r_code
2533        );
2534    }
2535
2536    #[test]
2537    fn test_testable_member_hidden_in_normal_build() {
2538        // Without --test, a @testable member stays fully private.
2539        let r_code = FluentParser::new()
2540            .push("module Math { @testable let sq <- fn(x: int): int { x * x }; };")
2541            .run()
2542            .get_r_code()
2543            .iter()
2544            .cloned()
2545            .collect::<Vec<_>>()
2546            .join("\n");
2547        assert!(
2548            !r_code.contains(".test_sq"),
2549            "testable member must not be exposed in a normal build: {}",
2550            r_code
2551        );
2552    }
2553
2554    #[test]
2555    fn test_testable_member_exposed_in_test_build() {
2556        // With test_mode on, a @testable member is exposed as M$.test_<name>.
2557        let r_code = FluentParser::new()
2558            .set_context(Context::empty().set_test_mode(true))
2559            .push("module Math { @testable let sq <- fn(x: int): int { x * x }; };")
2560            .run()
2561            .get_r_code()
2562            .iter()
2563            .cloned()
2564            .collect::<Vec<_>>()
2565            .join("\n");
2566        assert!(
2567            r_code.contains("Math$`.test_sq` <- sq"),
2568            "testable member must be exposed in a test build: {}",
2569            r_code
2570        );
2571    }
2572
2573    #[test]
2574    fn test_module_transpilation_no_pub() {
2575        let r_code = FluentParser::new()
2576            .push("module Empty { let x <- 1; };")
2577            .run()
2578            .get_r_code()
2579            .iter()
2580            .cloned()
2581            .collect::<Vec<_>>()
2582            .join("\n");
2583        assert!(
2584            r_code.contains("Empty <- new.env(parent = emptyenv())"),
2585            "missing env init: {}",
2586            r_code
2587        );
2588        assert!(
2589            r_code.contains("local({"),
2590            "missing local block: {}",
2591            r_code
2592        );
2593        assert!(
2594            !r_code.contains("Empty$x"),
2595            "private member should not be exported: {}",
2596            r_code
2597        );
2598    }
2599
2600    #[test]
2601    fn test_module_s3_registration_for_typed_pub_fn() {
2602        let r_code = FluentParser::new()
2603            .push("module Math { @pub let double <- fn(x: Integer): Integer { x }; };")
2604            .run()
2605            .get_r_code()
2606            .iter()
2607            .cloned()
2608            .collect::<Vec<_>>()
2609            .join("\n");
2610        assert!(
2611            r_code.contains("Math <- new.env(parent = emptyenv())"),
2612            "missing env init: {}",
2613            r_code
2614        );
2615        assert!(
2616            r_code.contains("registerS3method(\"double\", \"integer\", double.integer)"),
2617            "missing S3 registration: {}",
2618            r_code
2619        );
2620        assert!(
2621            r_code.contains("double <- function(x, ...) UseMethod(\"double\")"),
2622            "missing generic: {}",
2623            r_code
2624        );
2625        assert!(
2626            r_code.contains("Math$double <- double"),
2627            "missing generic export: {}",
2628            r_code
2629        );
2630    }
2631
2632    #[test]
2633    fn test_module_no_trailing_semicolon() {
2634        let r_code = FluentParser::new()
2635            .push("module Geo { @pub let pi <- 3; }")
2636            .run()
2637            .get_r_code()
2638            .iter()
2639            .cloned()
2640            .collect::<Vec<_>>()
2641            .join("\n");
2642        assert!(
2643            r_code.contains("Geo <- new.env(parent = emptyenv())"),
2644            "missing env init: {}",
2645            r_code
2646        );
2647        assert!(
2648            r_code.contains("Geo$pi <- pi"),
2649            "missing public export: {}",
2650            r_code
2651        );
2652    }
2653
2654    #[test]
2655    fn test_import_module() {
2656        let r_code = FluentParser::new()
2657            .push("module Math { @pub let pi <- 3; }")
2658            .push("import Math")
2659            .run()
2660            .run()
2661            .get_r_code()
2662            .iter()
2663            .cloned()
2664            .collect::<Vec<_>>()
2665            .join("\n");
2666        assert!(
2667            r_code.contains("Math <- new.env(parent = emptyenv())"),
2668            "missing env init: {}",
2669            r_code
2670        );
2671    }
2672
2673    #[test]
2674    fn test_import_module_as_alias() {
2675        let r_code = FluentParser::new()
2676            .push("module Math { @pub let pi <- 3; }")
2677            .push("import Math as Maths")
2678            .run()
2679            .run()
2680            .get_r_code()
2681            .iter()
2682            .cloned()
2683            .collect::<Vec<_>>()
2684            .join("\n");
2685        assert!(
2686            r_code.contains("Math <- new.env(parent = emptyenv())"),
2687            "missing env init: {}",
2688            r_code
2689        );
2690        assert!(
2691            r_code.contains("Maths <- Math") || r_code.contains("`Maths` <- Math"),
2692            "missing alias assignment: {}",
2693            r_code
2694        );
2695    }
2696
2697    #[test]
2698    fn test_use_items_transpiles() {
2699        let r_code = FluentParser::new()
2700            .push("module Math { @pub let pi <- 3; @pub let e <- 2; };")
2701            .push("use Math::{pi, e as euler};")
2702            .run()
2703            .run()
2704            .get_r_code()
2705            .iter()
2706            .cloned()
2707            .collect::<Vec<_>>()
2708            .join("\n");
2709        assert!(
2710            r_code.contains("pi <- Math$pi"),
2711            "missing pi binding: {}",
2712            r_code
2713        );
2714        assert!(
2715            r_code.contains("euler <- Math$e"),
2716            "missing euler binding: {}",
2717            r_code
2718        );
2719    }
2720
2721    #[test]
2722    fn test_use_wildcard_transpiles() {
2723        let r_code = FluentParser::new()
2724            .push("module Math { @pub let pi <- 3; @pub let e <- 2; let secret <- 0; };")
2725            .push("use Math::*;")
2726            .run()
2727            .run()
2728            .get_r_code()
2729            .iter()
2730            .cloned()
2731            .collect::<Vec<_>>()
2732            .join("\n");
2733        assert!(
2734            r_code.contains("pi <- Math$pi"),
2735            "missing pi binding: {}",
2736            r_code
2737        );
2738        assert!(
2739            r_code.contains("e <- Math$e"),
2740            "missing e binding: {}",
2741            r_code
2742        );
2743        assert!(
2744            !r_code.contains("secret <- Math$secret"),
2745            "private member must not be imported: {}",
2746            r_code
2747        );
2748    }
2749
2750    // RFC-TR-032 @export tests
2751
2752    #[test]
2753    fn test_export_at_top_level_prepends_roxygen_tag() {
2754        let r_code = FluentParser::new()
2755            .push("@export let answer <- 42;")
2756            .run()
2757            .get_r_code()
2758            .iter()
2759            .cloned()
2760            .collect::<Vec<_>>()
2761            .join("\n");
2762        assert!(
2763            r_code.contains("#' @export"),
2764            "missing #' @export tag: {}",
2765            r_code
2766        );
2767        assert!(r_code.contains("answer"), "missing assignment: {}", r_code);
2768    }
2769
2770    #[test]
2771    fn test_export_in_module_is_public_and_package_exported() {
2772        let r_code = FluentParser::new()
2773            .push("module Math { @export let norm <- fn(x: int): int { x }; };")
2774            .run()
2775            .get_r_code()
2776            .iter()
2777            .cloned()
2778            .collect::<Vec<_>>()
2779            .join("\n");
2780        assert!(
2781            r_code.contains("Math$norm <- norm"),
2782            "missing module export: {}",
2783            r_code
2784        );
2785        assert!(
2786            r_code.contains("#' @export"),
2787            "missing roxygen export tag: {}",
2788            r_code
2789        );
2790        assert!(
2791            r_code.contains("norm <- Math$norm"),
2792            "missing package-level re-export: {}",
2793            r_code
2794        );
2795    }
2796
2797    #[test]
2798    fn test_export_in_module_test_build_adds_test_alias() {
2799        let r_code = FluentParser::new()
2800            .set_context(Context::empty().set_test_mode(true))
2801            .push("module Math { @export let norm <- fn(x: int): int { x }; };")
2802            .run()
2803            .get_r_code()
2804            .iter()
2805            .cloned()
2806            .collect::<Vec<_>>()
2807            .join("\n");
2808        assert!(
2809            r_code.contains("Math$`.test_norm` <- norm"),
2810            "export member must get .test_ alias in test build: {}",
2811            r_code
2812        );
2813    }
2814
2815    #[test]
2816    fn test_pub_in_module_test_build_adds_test_alias() {
2817        let r_code = FluentParser::new()
2818            .set_context(Context::empty().set_test_mode(true))
2819            .push("module Math { @pub let pi <- 3; };")
2820            .run()
2821            .get_r_code()
2822            .iter()
2823            .cloned()
2824            .collect::<Vec<_>>()
2825            .join("\n");
2826        assert!(
2827            r_code.contains("Math$`.test_pi` <- pi"),
2828            "@pub member must get .test_ alias in test build (RFC-TR-032 §3.2): {}",
2829            r_code
2830        );
2831    }
2832
2833    #[test]
2834    fn test_array_constructor_call_transpilation() {
2835        let r_code = FluentParser::new()
2836            .push("type Bits <- [Any, int];")
2837            .run()
2838            .push("let b <- Bits:[1, 2, 3];")
2839            .run()
2840            .get_r_code()
2841            .iter()
2842            .cloned()
2843            .collect::<Vec<_>>()
2844            .join("\n");
2845        assert!(
2846            r_code.contains("Bits(typed_vec("),
2847            "expected Bits(...) constructor: {}",
2848            r_code
2849        );
2850        assert!(
2851            r_code.contains("dim = c(3)"),
2852            "expected dimension annotation: {}",
2853            r_code
2854        );
2855    }
2856
2857    #[test]
2858    fn test_record_alias_return_no_constructor_pipe() {
2859        // A function returning a record alias must NOT get `|> TypeName()` in
2860        // its body — the constructor takes specific named fields, not a single
2861        // value, so piping the body result through it would fail at runtime.
2862        let r_code = FluentParser::new()
2863            .push("type Point <- list { x: int, y: int };")
2864            .run()
2865            .push("let incr <- fn(p: Point): Point { Point:{x: (p$x+1), y: (p$y+1)} };")
2866            .run()
2867            .get_r_code()
2868            .iter()
2869            .cloned()
2870            .collect::<Vec<_>>()
2871            .join("\n");
2872        // The method definition must not contain `|> Point()` inside the body
2873        assert!(
2874            !r_code.contains("}) |> Point()"),
2875            "record alias output conversion should not be added: {}",
2876            r_code
2877        );
2878        // The method should still be wrapped by as.Generic() or as.FunctionN() for S3 dispatch
2879        assert!(
2880            r_code.contains("|> as.Generic()") || r_code.contains("|> Function"),
2881            "function type annotation should still be applied: {}",
2882            r_code
2883        );
2884    }
2885
2886    #[test]
2887    fn test_alias_int_generates_validator() {
2888        let r_code = FluentParser::new().check_transpiling("type Meters <- int;");
2889        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2890        assert!(
2891            r_str.contains("validate_Meters <- function(x)"),
2892            "expected validator function, got: {}",
2893            r_str
2894        );
2895        assert!(
2896            r_str.contains("is.integer"),
2897            "expected is.integer check, got: {}",
2898            r_str
2899        );
2900    }
2901
2902    #[test]
2903    fn test_alias_char_generates_validator() {
2904        let r_code = FluentParser::new().check_transpiling("type Name <- char;");
2905        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2906        assert!(
2907            r_str.contains("validate_Name <- function(x)"),
2908            "expected validator function, got: {}",
2909            r_str
2910        );
2911        assert!(
2912            r_str.contains("is.character"),
2913            "expected is.character check, got: {}",
2914            r_str
2915        );
2916    }
2917
2918    #[test]
2919    fn test_alias_bool_generates_validator() {
2920        let r_code = FluentParser::new().check_transpiling("type Flag <- bool;");
2921        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2922        assert!(
2923            r_str.contains("validate_Flag <- function(x)"),
2924            "expected validator function, got: {}",
2925            r_str
2926        );
2927        assert!(
2928            r_str.contains("is.logical"),
2929            "expected is.logical check, got: {}",
2930            r_str
2931        );
2932    }
2933
2934    #[test]
2935    fn test_alias_num_generates_validator() {
2936        let r_code = FluentParser::new().check_transpiling("type Real <- num;");
2937        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2938        assert!(
2939            r_str.contains("validate_Real <- function(x)"),
2940            "expected validator function, got: {}",
2941            r_str
2942        );
2943        assert!(
2944            r_str.contains("is.numeric"),
2945            "expected is.numeric check, got: {}",
2946            r_str
2947        );
2948    }
2949
2950    #[test]
2951    fn test_validating_cast_int() {
2952        let r_code = FluentParser::new()
2953            .push("type Meters <- int;")
2954            .run()
2955            .check_transpiling("x as! Meters");
2956        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2957        assert!(
2958            r_str.contains("validate_Meters(x)"),
2959            "expected validate_Meters(x), got: {}",
2960            r_str
2961        );
2962    }
2963
2964    #[test]
2965    fn test_tag_alias_char_generates_validator() {
2966        let r_code = FluentParser::new().check_transpiling("type Hello <- .Hello(char);");
2967        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2968        assert!(
2969            r_str.contains("validate_Hello <- function(x)"),
2970            "expected validator function, got: {}",
2971            r_str
2972        );
2973        assert!(
2974            r_str.contains("x[[1]] != 'Hello'"),
2975            "expected tag name check, got: {}",
2976            r_str
2977        );
2978        assert!(
2979            r_str.contains("x[[\"body\"]]"),
2980            "expected body field check, got: {}",
2981            r_str
2982        );
2983        assert!(
2984            r_str.contains("is.character"),
2985            "expected is.character check on body, got: {}",
2986            r_str
2987        );
2988    }
2989
2990    #[test]
2991    fn test_tag_alias_int_generates_validator() {
2992        let r_code = FluentParser::new().check_transpiling("type Count <- .Count(int);");
2993        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
2994        assert!(
2995            r_str.contains("validate_Count <- function(x)"),
2996            "expected validator, got: {}",
2997            r_str
2998        );
2999        assert!(
3000            r_str.contains("x[[1]] != 'Count'"),
3001            "expected tag name check, got: {}",
3002            r_str
3003        );
3004        assert!(
3005            r_str.contains("is.integer"),
3006            "expected is.integer check on body, got: {}",
3007            r_str
3008        );
3009    }
3010
3011    #[test]
3012    fn test_tag_alias_with_alias_body_calls_nested_validator() {
3013        let r_code = FluentParser::new()
3014            .push("type Name <- char;")
3015            .run()
3016            .check_transpiling("type Tagged <- .Tagged(Name);");
3017        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3018        assert!(
3019            r_str.contains("validate_Tagged <- function(x)"),
3020            "expected validator, got: {}",
3021            r_str
3022        );
3023        assert!(
3024            r_str.contains("validate_Name(x[[\"body\"]])"),
3025            "expected nested validator call, got: {}",
3026            r_str
3027        );
3028    }
3029
3030    #[test]
3031    fn test_union_variant_generates_full_pipeline() {
3032        // Each union variant gets the same constructor/annotator/validator
3033        // contract as records (validation_variant_d_union.md §3).
3034        let r_str = FluentParser::new()
3035            .check_transpiling("type Shape <- .Circle(num) | .Nothing;")
3036            .iter()
3037            .cloned()
3038            .collect::<Vec<_>>()
3039            .join("\n");
3040        // Constructor (payload variant) builds the canonical value then delegates.
3041        assert!(
3042            r_str.contains("Circle <- function(x) {")
3043                && r_str.contains("v <- list(\"Circle\", body = x)")
3044                && r_str.contains("as.Circle(v)"),
3045            "expected Circle constructor, got: {r_str}"
3046        );
3047        // Annotator sets the enriched class idempotently and validates.
3048        assert!(
3049            r_str.contains("as.Circle <- function(x) {")
3050                && r_str.contains("class(x) <- c(\"Circle\", \"Shape\", \"Tag\", \"list\")")
3051                && r_str.contains("x <- validate_Circle(x)")
3052                && r_str.contains("x <- validate(x)"),
3053            "expected Circle annotator, got: {r_str}"
3054        );
3055        // Internal validator checks tag identity and payload type.
3056        assert!(
3057            r_str.contains("validate_Circle <- function(x) {")
3058                && r_str.contains("x[[1]] != 'Circle'")
3059                && r_str.contains("is.numeric(x[[\"body\"]])"),
3060            "expected Circle validator, got: {r_str}"
3061        );
3062        // Empty variant has a zero-arg constructor and no body.
3063        assert!(
3064            r_str.contains("Nothing <- function() {") && r_str.contains("x <- list(\"Nothing\")"),
3065            "expected Nothing constructor, got: {r_str}"
3066        );
3067    }
3068
3069    #[test]
3070    fn test_tag_literal_canonical_representation() {
3071        // A `.Circle(..)` literal must produce the same runtime shape as the
3072        // variant constructor: tag in position 1, payload under `body`, class
3073        // enriched with the union name (validation_variant_d_union.md §2).
3074        let r_str = FluentParser::new()
3075            .push("type Shape <- .Circle(num) | .Square(num);")
3076            .run()
3077            .check_transpiling(".Circle(3.14)")
3078            .iter()
3079            .cloned()
3080            .collect::<Vec<_>>()
3081            .join("\n");
3082        assert!(
3083            r_str.contains("structure(list('Circle', body =")
3084                && r_str.contains("class = c('Circle', 'Shape', 'Tag', 'list')"),
3085            "expected canonical tag literal with union class, got: {r_str}"
3086        );
3087    }
3088
3089    #[test]
3090    fn test_tag_literal_without_union_omits_union_class() {
3091        // A tag with no declared union still uses the canonical shape, but the
3092        // class carries no union name.
3093        let r_str = FluentParser::new()
3094            .check_transpiling(".Loose(1)")
3095            .iter()
3096            .cloned()
3097            .collect::<Vec<_>>()
3098            .join("\n");
3099        assert!(
3100            r_str.contains("structure(list('Loose', body =")
3101                && r_str.contains("class = c('Loose', 'Tag', 'list')"),
3102            "expected canonical tag literal without union class, got: {r_str}"
3103        );
3104    }
3105
3106    #[test]
3107    fn test_literal_char_alias_generates_exact_validator() {
3108        let r_code = FluentParser::new().check_transpiling("type Hello <- \"hello\";");
3109        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3110        assert!(
3111            r_str.contains("validate_Hello <- function(x)"),
3112            "expected validator function, got: {}",
3113            r_str
3114        );
3115        assert!(
3116            r_str.contains("x != 'hello'"),
3117            "expected literal equality check, got: {}",
3118            r_str
3119        );
3120    }
3121
3122    #[test]
3123    fn test_literal_int_alias_generates_exact_validator() {
3124        let r_code = FluentParser::new().check_transpiling("type Byte <- 89;");
3125        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3126        assert!(
3127            r_str.contains("validate_Byte <- function(x)"),
3128            "expected validator function, got: {}",
3129            r_str
3130        );
3131        assert!(
3132            r_str.contains("x != 89L"),
3133            "expected literal equality check, got: {}",
3134            r_str
3135        );
3136    }
3137
3138    #[test]
3139    fn test_literal_num_alias_generates_exact_validator() {
3140        let r_code = FluentParser::new().check_transpiling("type Pi <- 3.14;");
3141        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3142        assert!(
3143            r_str.contains("validate_Pi <- function(x)"),
3144            "expected validator function, got: {}",
3145            r_str
3146        );
3147        assert!(
3148            r_str.contains("x != 3.14"),
3149            "expected literal equality check, got: {}",
3150            r_str
3151        );
3152    }
3153
3154    #[test]
3155    fn test_literal_bool_true_alias_generates_exact_validator() {
3156        let r_code = FluentParser::new().check_transpiling("type Yes <- true;");
3157        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3158        assert!(
3159            r_str.contains("validate_Yes <- function(x)"),
3160            "expected validator function, got: {}",
3161            r_str
3162        );
3163        assert!(
3164            r_str.contains("x != TRUE"),
3165            "expected literal TRUE check, got: {}",
3166            r_str
3167        );
3168    }
3169
3170    #[test]
3171    fn test_literal_bool_false_alias_generates_exact_validator() {
3172        let r_code = FluentParser::new().check_transpiling("type No <- false;");
3173        let r_str = r_code.iter().cloned().collect::<Vec<_>>().join("\n");
3174        assert!(
3175            r_str.contains("validate_No <- function(x)"),
3176            "expected validator function, got: {}",
3177            r_str
3178        );
3179        assert!(
3180            r_str.contains("x != FALSE"),
3181            "expected literal FALSE check, got: {}",
3182            r_str
3183        );
3184    }
3185
3186    #[test]
3187    fn test_record_subtype_includes_supertype_in_s3_class() {
3188        // Person has all fields of Position, so Person <: Position.
3189        // The annotator for Person must include "Position" in its class vector
3190        // so that S3 methods defined on Position dispatch for Person values.
3191        let r_str = FluentParser::new()
3192            .push("type Position <- list{ position: int };")
3193            .run()
3194            .push("type Person <- list{ name: char, age: int, position: int };")
3195            .run()
3196            .get_r_code()
3197            .iter()
3198            .cloned()
3199            .collect::<Vec<_>>()
3200            .join("\n");
3201        assert!(
3202            r_str.contains("class(x) <- c(\"Person\", \"Position\", \"list\")"),
3203            "expected Person's annotator to include Position, got: {r_str}"
3204        );
3205    }
3206
3207    #[test]
3208    fn test_record_without_supertype_keeps_plain_class() {
3209        // Position has no other record supertype, so its class stays c("Position", "list").
3210        let r_str = FluentParser::new()
3211            .check_transpiling("type Position <- list{ position: int };")
3212            .iter()
3213            .cloned()
3214            .collect::<Vec<_>>()
3215            .join("\n");
3216        assert!(
3217            r_str.contains("class(x) <- c(\"Position\", \"list\")"),
3218            "expected Position's annotator with no supertype, got: {r_str}"
3219        );
3220    }
3221}