600 lines
21 KiB
Rust
600 lines
21 KiB
Rust
//! cordial source decomposer -- produces self-contained Rust from Cordial.
|
|
|
|
use acord_core::interp::{parse_program, Expr, Op, Stmt};
|
|
|
|
/// extension point for external projects adding custom decomposition rules.
|
|
pub trait DecomposeHook {
|
|
/// extra Rust source appended after the value prelude, before generated code.
|
|
fn preamble(&self) -> Option<String> { None }
|
|
|
|
/// intercepts a function call by name before the default decomposition.
|
|
fn call(&self, _name: &str, _args: &[Expr]) -> Option<String> { None }
|
|
|
|
/// intercepts an expression before the default decomposition.
|
|
fn expr(&self, _expr: &Expr) -> Option<String> { None }
|
|
|
|
/// intercepts a statement before the default decomposition.
|
|
fn stmt(&self, _stmt: &Stmt) -> Option<String> { None }
|
|
}
|
|
|
|
struct NoHook;
|
|
impl DecomposeHook for NoHook {}
|
|
|
|
/// external module dependency discovered during decomposition.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub struct Dependency {
|
|
/// module name (note filename stem, hyphen-form).
|
|
pub module: String,
|
|
/// specific item imported, or None / Some("*") for the whole module.
|
|
pub item: Option<String>,
|
|
}
|
|
|
|
/// result of decomposing a single cordial source.
|
|
pub struct Decomposed {
|
|
/// self-contained rust source for this module.
|
|
pub code: String,
|
|
/// external modules referenced by use declarations.
|
|
pub deps: Vec<Dependency>,
|
|
}
|
|
|
|
/// decomposes cordial source into standalone rust + a dependency list.
|
|
pub fn decompose(source: &str) -> Result<Decomposed, String> {
|
|
decompose_with(source, &NoHook)
|
|
}
|
|
|
|
/// decomposes with a custom hook for external extensions.
|
|
pub fn decompose_with(source: &str, hook: &dyn DecomposeHook) -> Result<Decomposed, String> {
|
|
let stmts = parse_program(source)?;
|
|
let mut deps = Vec::new();
|
|
let mut out = String::new();
|
|
out.push_str(GENERATED_PRELUDE);
|
|
if let Some(extra) = hook.preamble() {
|
|
out.push_str(&extra);
|
|
out.push('\n');
|
|
}
|
|
emit_program(&mut out, &stmts, &mut deps, hook)?;
|
|
Ok(Decomposed { code: out, deps })
|
|
}
|
|
|
|
fn emit_program(out: &mut String, stmts: &[Stmt], deps: &mut Vec<Dependency>, hook: &dyn DecomposeHook) -> Result<(), String> {
|
|
let mut uses = Vec::new();
|
|
let mut fns = Vec::new();
|
|
let mut rest = Vec::new();
|
|
for s in stmts {
|
|
match s {
|
|
Stmt::Use(..) => uses.push(s),
|
|
Stmt::FnDef { .. } => fns.push(s),
|
|
_ => rest.push(s),
|
|
}
|
|
}
|
|
|
|
for s in &uses {
|
|
emit_stmt(out, s, 0, deps, hook)?;
|
|
}
|
|
if !uses.is_empty() { out.push('\n'); }
|
|
|
|
for s in &fns {
|
|
emit_stmt(out, s, 0, deps, hook)?;
|
|
out.push('\n');
|
|
}
|
|
indent(out, 0, "pub fn run() -> V {");
|
|
for s in &rest {
|
|
emit_stmt(out, s, 1, deps, hook)?;
|
|
}
|
|
indent(out, 1, "V::Void");
|
|
indent(out, 0, "}");
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_stmt(out: &mut String, stmt: &Stmt, depth: usize, deps: &mut Vec<Dependency>, hook: &dyn DecomposeHook) -> Result<(), String> {
|
|
if let Some(custom) = hook.stmt(stmt) {
|
|
indent(out, depth, &custom);
|
|
return Ok(());
|
|
}
|
|
match stmt {
|
|
Stmt::Let(name, _ann, expr) => {
|
|
let rhs = emit_expr(expr, hook)?;
|
|
indent(out, depth, &format!("let mut {} = {};", ident(name), rhs));
|
|
}
|
|
Stmt::Assign(name, expr) => {
|
|
let rhs = emit_expr(expr, hook)?;
|
|
indent(out, depth, &format!("{} = {};", ident(name), rhs));
|
|
}
|
|
Stmt::PathAssign(lhs, expr) => {
|
|
let lhs_s = emit_expr(lhs, hook)?;
|
|
let rhs = emit_expr(expr, hook)?;
|
|
indent(out, depth, &format!("v_path_assign(&mut {}, &{});", lhs_s, rhs));
|
|
}
|
|
Stmt::Return(expr) => {
|
|
let rhs = emit_expr(expr, hook)?;
|
|
indent(out, depth, &format!("return {};", rhs));
|
|
}
|
|
Stmt::ExprStmt(expr) => {
|
|
let rendered = emit_expr(expr, hook)?;
|
|
indent(out, depth, &format!("let _ = {};", rendered));
|
|
}
|
|
Stmt::FnDef { name, params, body, .. } => {
|
|
let param_list: String = params.iter()
|
|
.map(|(p, _)| format!("mut {}: V", ident(p)))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
indent(out, depth, &format!("pub fn {}({}) -> V {{", ident(name), param_list));
|
|
for s in body {
|
|
emit_stmt(out, s, depth + 1, deps, hook)?;
|
|
}
|
|
indent(out, depth + 1, "V::Void");
|
|
indent(out, depth, "}");
|
|
}
|
|
Stmt::While(cond, body) => {
|
|
let c = emit_expr(cond, hook)?;
|
|
indent(out, depth, &format!("while v_truthy(&{}) {{", c));
|
|
for s in body {
|
|
emit_stmt(out, s, depth + 1, deps, hook)?;
|
|
}
|
|
indent(out, depth, "}");
|
|
}
|
|
Stmt::IfElse(cond, then_body, else_body) => {
|
|
let c = emit_expr(cond, hook)?;
|
|
indent(out, depth, &format!("if v_truthy(&{}) {{", c));
|
|
for s in then_body {
|
|
emit_stmt(out, s, depth + 1, deps, hook)?;
|
|
}
|
|
if let Some(els) = else_body {
|
|
indent(out, depth, "} else {");
|
|
for s in els {
|
|
emit_stmt(out, s, depth + 1, deps, hook)?;
|
|
}
|
|
}
|
|
indent(out, depth, "}");
|
|
}
|
|
Stmt::ForLoop(var, iter_expr, body) => {
|
|
let iter_s = emit_expr(iter_expr, hook)?;
|
|
indent(out, depth, &format!("for {} in v_iter(&{}) {{", ident(var), iter_s));
|
|
for s in body {
|
|
emit_stmt(out, s, depth + 1, deps, hook)?;
|
|
}
|
|
indent(out, depth, "}");
|
|
}
|
|
Stmt::Use(module, item) => {
|
|
let mod_ident = module.replace('-', "_");
|
|
deps.push(Dependency { module: module.clone(), item: item.clone() });
|
|
indent(out, depth, &format!("mod {};", mod_ident));
|
|
match item.as_deref() {
|
|
None | Some("*") => indent(out, depth, &format!("use {}::*;", mod_ident)),
|
|
Some(name) => indent(out, depth, &format!("use {}::{};", mod_ident, ident(name))),
|
|
}
|
|
}
|
|
Stmt::CellAssign { table, cell, value, .. } => {
|
|
let v = emit_expr(value, hook)?;
|
|
indent(out, depth, &format!(
|
|
"v_cell_set({:?}, {}, {}, &{});",
|
|
table, cell.0, cell.1, v
|
|
));
|
|
}
|
|
Stmt::SolveDef { name, params, target_var, source_fn, .. } => {
|
|
let param_list: String = params.iter()
|
|
.map(|p| format!("mut {}: V", ident(p)))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
indent(out, depth, &format!("pub fn {}({}) -> V {{", ident(name), param_list));
|
|
indent(out, depth + 1, &format!(
|
|
"v_solve_newton({:?}, {:?})",
|
|
target_var, source_fn
|
|
));
|
|
indent(out, depth, "}");
|
|
}
|
|
Stmt::TraitDef { name, methods } => {
|
|
indent(out, depth, &format!("pub trait {} {{", ident(name)));
|
|
for m in methods {
|
|
indent(out, depth + 1, &format!("fn {}(&self) -> V;", ident(m)));
|
|
}
|
|
indent(out, depth, "}");
|
|
}
|
|
Stmt::ImplBlock { type_name, trait_name, methods } => {
|
|
let header = match trait_name {
|
|
Some(t) => format!("impl {} for {} {{", ident(t), ident(type_name)),
|
|
None => format!("impl {} {{", ident(type_name)),
|
|
};
|
|
indent(out, depth, &header);
|
|
for m in methods {
|
|
emit_stmt(out, m, depth + 1, deps, hook)?;
|
|
}
|
|
indent(out, depth, "}");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_expr(expr: &Expr, hook: &dyn DecomposeHook) -> Result<String, String> {
|
|
if let Some(custom) = hook.expr(expr) {
|
|
return Ok(custom);
|
|
}
|
|
Ok(match expr {
|
|
Expr::Num(n) => format!("V::Num({})", fmt_num(*n)),
|
|
Expr::Bool(b) => format!("V::Bool({})", b),
|
|
Expr::Str(s) => format!("V::Str({:?}.into())", s),
|
|
Expr::Ident(name) => format!("{}.clone()", ident(name)),
|
|
Expr::UnaryOp(op, inner) => {
|
|
let inner_s = emit_expr(inner, hook)?;
|
|
match op {
|
|
Op::Neg => format!("v_neg(&{})", inner_s),
|
|
Op::Not => format!("v_not(&{})", inner_s),
|
|
Op::Strip => format!("v_strip(&{})", inner_s),
|
|
_ => format!("v_neg(&{})", inner_s),
|
|
}
|
|
}
|
|
Expr::BinOp(op, l, r) => {
|
|
let ls = emit_expr(l, hook)?;
|
|
let rs = emit_expr(r, hook)?;
|
|
let func = match op {
|
|
Op::Add => "v_add",
|
|
Op::Sub => "v_sub",
|
|
Op::Mul => "v_mul",
|
|
Op::Div => "v_div",
|
|
Op::Mod => "v_rem",
|
|
Op::Pow => "v_pow",
|
|
Op::Eq => "v_eq",
|
|
Op::Neq => "v_neq",
|
|
Op::Lt => "v_lt",
|
|
Op::Gt => "v_gt",
|
|
Op::Lte => "v_lte",
|
|
Op::Gte => "v_gte",
|
|
Op::And => "v_and",
|
|
Op::Or => "v_or",
|
|
_ => "v_add",
|
|
};
|
|
format!("{}(&{}, &{})", func, ls, rs)
|
|
}
|
|
Expr::Call(name, args) => {
|
|
if let Some(custom) = hook.call(name, args) {
|
|
custom
|
|
} else {
|
|
let arg_list: Vec<String> = args.iter()
|
|
.map(|a| emit_expr(a, hook))
|
|
.collect::<Result<_, _>>()?;
|
|
format!("{}({})", ident(name), arg_list.join(", "))
|
|
}
|
|
}
|
|
Expr::Array(items) => {
|
|
let parts: Vec<String> = items.iter()
|
|
.map(|e| emit_expr(e, hook))
|
|
.collect::<Result<_, _>>()?;
|
|
format!("V::Array(vec![{}])", parts.join(", "))
|
|
}
|
|
Expr::Index(base, idx) => {
|
|
let b = emit_expr(base, hook)?;
|
|
let i = emit_expr(idx, hook)?;
|
|
format!("v_index(&{}, &{})", b, i)
|
|
}
|
|
Expr::Range(start, end) => {
|
|
let s = emit_expr(start, hook)?;
|
|
let e = emit_expr(end, hook)?;
|
|
format!("v_range(&{}, &{})", s, e)
|
|
}
|
|
Expr::IsCheck(inner, type_name) => {
|
|
let i = emit_expr(inner, hook)?;
|
|
format!("v_is(&{}, {:?})", i, type_name)
|
|
}
|
|
Expr::CellRef { table, target, .. } => {
|
|
let t = table.as_deref().unwrap_or("_default");
|
|
match target {
|
|
acord_core::interp::CellRefTarget::Cell(col, row) => {
|
|
format!("v_cell_get({:?}, {}, {})", t, col, row)
|
|
}
|
|
acord_core::interp::CellRefTarget::Range(c1, r1, c2, r2) => {
|
|
format!("v_cell_range({:?}, {}, {}, {}, {})", t, c1, r1, c2, r2)
|
|
}
|
|
acord_core::interp::CellRefTarget::Whole => {
|
|
format!("v_cell_table({:?})", t)
|
|
}
|
|
}
|
|
}
|
|
Expr::SolveMacro { var, source_fn } => {
|
|
format!("v_solve_call({:?}, {:?})", var, source_fn)
|
|
}
|
|
Expr::Struct(fields) => {
|
|
let entries: Vec<String> = fields.iter()
|
|
.map(|(k, v)| {
|
|
let val = emit_expr(v, hook)?;
|
|
Ok(format!("({:?}.into(), {})", k, val))
|
|
})
|
|
.collect::<Result<Vec<_>, String>>()?;
|
|
format!("V::Struct(vec![{}].into_iter().collect())", entries.join(", "))
|
|
}
|
|
Expr::Field(base, field) => {
|
|
let b = emit_expr(base, hook)?;
|
|
format!("v_field(&{}, {:?})", b, field)
|
|
}
|
|
Expr::MethodCall(recv, method, args) => {
|
|
let r = emit_expr(recv, hook)?;
|
|
let arg_list: Vec<String> = args.iter()
|
|
.map(|a| emit_expr(a, hook))
|
|
.collect::<Result<_, _>>()?;
|
|
if arg_list.is_empty() {
|
|
format!("v_method_call(&{}, {:?}, &[])", r, method)
|
|
} else {
|
|
format!("v_method_call(&{}, {:?}, &[{}])", r, method, arg_list.join(", "))
|
|
}
|
|
}
|
|
Expr::StaticCall(type_name, method, args) => {
|
|
let arg_list: Vec<String> = args.iter()
|
|
.map(|a| emit_expr(a, hook))
|
|
.collect::<Result<_, _>>()?;
|
|
format!("{}__{}({})", ident(type_name), ident(method), arg_list.join(", "))
|
|
}
|
|
})
|
|
}
|
|
|
|
fn indent(out: &mut String, depth: usize, line: &str) {
|
|
for _ in 0..depth { out.push_str(" "); }
|
|
out.push_str(line);
|
|
out.push('\n');
|
|
}
|
|
|
|
fn ident(name: &str) -> String {
|
|
if RESERVED.contains(&name) { format!("r#{}", name) } else { name.replace('-', "_") }
|
|
}
|
|
|
|
fn fmt_num(n: f64) -> String {
|
|
if n.is_nan() { return "f64::NAN".into(); }
|
|
if n.is_infinite() {
|
|
return if n > 0.0 { "f64::INFINITY".into() } else { "f64::NEG_INFINITY".into() };
|
|
}
|
|
let s = format!("{}", n);
|
|
if s.contains('.') || s.contains('e') || s.contains('E') { s } else { format!("{}.0", s) }
|
|
}
|
|
|
|
const RESERVED: &[&str] = &[
|
|
"as", "break", "const", "continue", "crate", "else", "enum", "extern",
|
|
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
|
|
"move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
|
|
"super", "trait", "true", "type", "unsafe", "use", "where", "while",
|
|
"async", "await", "dyn",
|
|
];
|
|
|
|
const GENERATED_PRELUDE: &str = r#"#![allow(unused_variables, unused_mut, dead_code, unused_imports, non_snake_case)]
|
|
use std::collections::BTreeMap;
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum V {
|
|
Num(f64),
|
|
Str(String),
|
|
Bool(bool),
|
|
Array(Vec<V>),
|
|
Struct(BTreeMap<String, V>),
|
|
Void,
|
|
}
|
|
|
|
impl std::fmt::Display for V {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
match self {
|
|
V::Num(n) => write!(f, "{}", n),
|
|
V::Str(s) => write!(f, "{}", s),
|
|
V::Bool(b) => write!(f, "{}", b),
|
|
V::Array(a) => {
|
|
write!(f, "[")?;
|
|
for (i, v) in a.iter().enumerate() {
|
|
if i > 0 { write!(f, ", ")?; }
|
|
write!(f, "{}", v)?;
|
|
}
|
|
write!(f, "]")
|
|
}
|
|
V::Struct(m) => {
|
|
write!(f, "{{")?;
|
|
for (i, (k, v)) in m.iter().enumerate() {
|
|
if i > 0 { write!(f, ", ")?; }
|
|
write!(f, "{}: {}", k, v)?;
|
|
}
|
|
write!(f, "}}")
|
|
}
|
|
V::Void => write!(f, "()"),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn v_num(v: &V) -> f64 { match v { V::Num(n) => *n, V::Bool(b) => if *b { 1.0 } else { 0.0 }, _ => 0.0 } }
|
|
fn v_truthy(v: &V) -> bool { match v { V::Bool(b) => *b, V::Num(n) => *n != 0.0, V::Str(s) => !s.is_empty(), V::Array(a) => !a.is_empty(), _ => false } }
|
|
|
|
fn v_neg(a: &V) -> V { V::Num(-v_num(a)) }
|
|
fn v_not(a: &V) -> V { V::Bool(!v_truthy(a)) }
|
|
fn v_strip(a: &V) -> V { match a { V::Str(s) => V::Str(s.trim().into()), _ => a.clone() } }
|
|
|
|
fn v_add(a: &V, b: &V) -> V { match (a, b) { (V::Str(s1), V::Str(s2)) => V::Str(format!("{}{}", s1, s2)), (V::Array(a1), V::Array(a2)) => V::Array([a1.as_slice(), a2.as_slice()].concat()), _ => V::Num(v_num(a) + v_num(b)) } }
|
|
fn v_sub(a: &V, b: &V) -> V { V::Num(v_num(a) - v_num(b)) }
|
|
fn v_mul(a: &V, b: &V) -> V { V::Num(v_num(a) * v_num(b)) }
|
|
fn v_div(a: &V, b: &V) -> V { let d = v_num(b); V::Num(if d == 0.0 { f64::NAN } else { v_num(a) / d }) }
|
|
fn v_rem(a: &V, b: &V) -> V { V::Num(v_num(a) % v_num(b)) }
|
|
fn v_pow(a: &V, b: &V) -> V { V::Num(v_num(a).powf(v_num(b))) }
|
|
|
|
fn v_eq(a: &V, b: &V) -> V { V::Bool(a == b) }
|
|
fn v_neq(a: &V, b: &V) -> V { V::Bool(a != b) }
|
|
fn v_lt(a: &V, b: &V) -> V { V::Bool(v_num(a) < v_num(b)) }
|
|
fn v_gt(a: &V, b: &V) -> V { V::Bool(v_num(a) > v_num(b)) }
|
|
fn v_lte(a: &V, b: &V) -> V { V::Bool(v_num(a) <= v_num(b)) }
|
|
fn v_gte(a: &V, b: &V) -> V { V::Bool(v_num(a) >= v_num(b)) }
|
|
fn v_and(a: &V, b: &V) -> V { V::Bool(v_truthy(a) && v_truthy(b)) }
|
|
fn v_or(a: &V, b: &V) -> V { V::Bool(v_truthy(a) || v_truthy(b)) }
|
|
|
|
fn v_index(base: &V, idx: &V) -> V {
|
|
match (base, idx) {
|
|
(V::Array(arr), V::Num(n)) => arr.get(*n as usize).cloned().unwrap_or(V::Void),
|
|
(V::Struct(m), V::Str(k)) => m.get(k).cloned().unwrap_or(V::Void),
|
|
_ => V::Void,
|
|
}
|
|
}
|
|
|
|
fn v_range(start: &V, end: &V) -> V {
|
|
let s = v_num(start) as i64;
|
|
let e = v_num(end) as i64;
|
|
V::Array((s..=e).map(|i| V::Num(i as f64)).collect())
|
|
}
|
|
|
|
fn v_is(val: &V, type_name: &str) -> V {
|
|
let matches = match (val, type_name) {
|
|
(V::Num(_), "number") => true,
|
|
(V::Str(_), "string") => true,
|
|
(V::Bool(_), "bool") => true,
|
|
(V::Array(_), "array") => true,
|
|
(V::Struct(_), "struct") => true,
|
|
(V::Void, "void") => true,
|
|
_ => false,
|
|
};
|
|
V::Bool(matches)
|
|
}
|
|
|
|
fn v_field(base: &V, name: &str) -> V {
|
|
match base {
|
|
V::Struct(m) => m.get(name).cloned().unwrap_or(V::Void),
|
|
V::Array(a) => match name {
|
|
"len" => V::Num(a.len() as f64),
|
|
_ => V::Void,
|
|
},
|
|
_ => V::Void,
|
|
}
|
|
}
|
|
|
|
fn v_field_set(base: &mut V, _val: &V) {
|
|
let _ = base; // path assignment placeholder
|
|
}
|
|
|
|
fn v_iter(val: &V) -> Vec<V> {
|
|
match val {
|
|
V::Array(a) => a.clone(),
|
|
V::Num(n) => (0..(*n as i64).max(0)).map(|i| V::Num(i as f64)).collect(),
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn v_path_assign(target: &mut V, val: &V) { *target = val.clone(); }
|
|
fn v_cell_get(_table: &str, _col: u32, _row: u32) -> V { V::Void }
|
|
fn v_cell_range(_table: &str, _c1: u32, _r1: u32, _c2: u32, _r2: u32) -> V { V::Array(Vec::new()) }
|
|
fn v_cell_table(_table: &str) -> V { V::Array(Vec::new()) }
|
|
fn v_cell_set(_table: &str, _col: u32, _row: u32, _val: &V) {}
|
|
fn v_solve_newton(_target_var: &str, _source_fn: &str) -> V { V::Void }
|
|
fn v_solve_call(_var: &str, _source_fn: &str) -> V { V::Void }
|
|
fn v_method_call(_recv: &V, _method: &str, _args: &[V]) -> V { V::Void }
|
|
|
|
// --- generated code below ---
|
|
|
|
"#;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn dec(src: &str) -> String {
|
|
decompose(src).expect("decompose failed").code
|
|
}
|
|
|
|
fn dec_full(src: &str) -> Decomposed {
|
|
decompose(src).expect("decompose failed")
|
|
}
|
|
|
|
#[test]
|
|
fn empty_produces_run() {
|
|
let out = dec("");
|
|
assert!(out.contains("pub fn run() -> V"));
|
|
}
|
|
|
|
#[test]
|
|
fn let_binding() {
|
|
let out = dec("let x = 5");
|
|
assert!(out.contains("let mut x = V::Num(5.0)"));
|
|
}
|
|
|
|
#[test]
|
|
fn binop() {
|
|
let out = dec("let x = 1 + 2");
|
|
assert!(out.contains("v_add("));
|
|
}
|
|
|
|
#[test]
|
|
fn fn_def() {
|
|
let out = dec("fn add(a, b) {\n return a + b\n}");
|
|
assert!(out.contains("pub fn add(mut a: V, mut b: V) -> V"));
|
|
}
|
|
|
|
#[test]
|
|
fn if_else() {
|
|
let out = dec("if true {\n let x = 1\n} else {\n let x = 2\n}");
|
|
assert!(out.contains("if v_truthy("));
|
|
assert!(out.contains("} else {"));
|
|
}
|
|
|
|
#[test]
|
|
fn while_loop() {
|
|
let out = dec("let i = 0\nwhile i < 10 {\n i = i + 1\n}");
|
|
assert!(out.contains("while v_truthy("));
|
|
}
|
|
|
|
#[test]
|
|
fn for_loop() {
|
|
let out = dec("for x in [1, 2, 3] {\n let _ = x\n}");
|
|
assert!(out.contains("for x in v_iter("));
|
|
}
|
|
|
|
#[test]
|
|
fn array_literal() {
|
|
let out = dec("let a = [1, 2, 3]");
|
|
assert!(out.contains("V::Array(vec!["));
|
|
}
|
|
|
|
#[test]
|
|
fn index_access() {
|
|
let out = dec("let a = [1, 2]\nlet b = a[0]");
|
|
assert!(out.contains("v_index("));
|
|
}
|
|
|
|
#[test]
|
|
fn struct_literal() {
|
|
let out = dec("let p = {x: 1, y: 2}");
|
|
assert!(out.contains("V::Struct("));
|
|
}
|
|
|
|
#[test]
|
|
fn field_access() {
|
|
let out = dec("let p = {x: 1}\nlet v = p.x");
|
|
assert!(out.contains("v_field("));
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_word_raw_ident() {
|
|
let out = dec("let type = 1");
|
|
assert!(out.contains("r#type"));
|
|
}
|
|
|
|
#[test]
|
|
fn self_contained_compiles() {
|
|
let out = dec("fn double(x) {\n return x * 2\n}\nlet r = double(5)");
|
|
assert!(out.contains("pub fn double("));
|
|
assert!(out.contains("pub fn run()"));
|
|
assert!(!out.contains("acord"));
|
|
}
|
|
|
|
#[test]
|
|
fn use_emits_mod_and_reports_dep() {
|
|
let r = dec_full("use math");
|
|
assert!(r.code.contains("mod math;"));
|
|
assert!(r.code.contains("use math::*;"));
|
|
assert_eq!(r.deps.len(), 1);
|
|
assert_eq!(r.deps[0].module, "math");
|
|
assert_eq!(r.deps[0].item, None);
|
|
}
|
|
|
|
#[test]
|
|
fn use_specific_item() {
|
|
let r = dec_full("use utils::double");
|
|
assert!(r.code.contains("mod utils;"));
|
|
assert!(r.code.contains("use utils::double;"));
|
|
assert_eq!(r.deps[0].item.as_deref(), Some("double"));
|
|
}
|
|
|
|
#[test]
|
|
fn use_with_underscored_name() {
|
|
let r = dec_full("use my_lib");
|
|
assert!(r.code.contains("mod my_lib;"));
|
|
assert!(r.code.contains("use my_lib::*;"));
|
|
assert_eq!(r.deps[0].module, "my_lib");
|
|
}
|
|
}
|