prose: human-readable projection renderer (iter 20a)
New crate ailang-prose with one public fn module_to_prose(&Module) -> String. Rust-flavour with braces, =>-match-arms, mode keywords (own/borrow), effects as trailing 'with IO', Cons(1, Nil) ctor form, /// doc strings. Lossy projection where the LLM can re-derive: (con T) wrap, (fn-type ...) wrap, (term-ctor ...) wrap. Load-bearing semantic detail stays visible (modes, effects, clone, reuse-as, doc strings, type annotations, tail flag). CLI: 'ail prose <file.ail.json>' prints the projection. 3 snapshot fixtures + 28 unit tests. 215-line .ail.json reduces to ~18 lines of legible source. 20b queued: infix arithmetic, paren elision, let-inlining, do prettify.
This commit is contained in:
@@ -0,0 +1,924 @@
|
||||
//! AILang prose projection — human-readable presentation layer.
|
||||
//!
|
||||
//! This crate is the form-(B) projection of [`ailang_core::ast::Module`]:
|
||||
//! a Rust-flavoured, brace-and-comma text rendering optimised for human
|
||||
//! reading. It is a *one-way* projection — there is no parser. Editing
|
||||
//! the prose and re-integrating the changes is the round-trip-mediator's
|
||||
//! job (Iter 20d), not this crate's.
|
||||
//!
|
||||
//! The canonical authoring surface remains form (A) (`ailang-surface`)
|
||||
//! and the canonical hashable artefact remains the JSON-AST
|
||||
//! (`ailang-core`). Prose is deliberately lossy where the LLM can
|
||||
//! re-derive the dropped machinery from typecheck context (the `(con T)`
|
||||
//! wrap, the `(fn-type ...)` wrap, the `(term-ctor T C ...)` collapsing
|
||||
//! to `C(...)`); every load-bearing semantic detail (mode annotations,
|
||||
//! effects, `clone`, `reuse-as`, doc strings, type annotations on
|
||||
//! signatures and lambdas, the `tail` flag) stays visible.
|
||||
//!
|
||||
//! See `docs/JOURNAL.md` "Pinned: human-readable prose surface
|
||||
//! (Family 20 candidate)" for the design pinning, and "Iter 20a" for
|
||||
//! this iter's spec.
|
||||
//!
|
||||
//! # Public API
|
||||
//!
|
||||
//! [`module_to_prose`] is the only entry point. It is deterministic and
|
||||
//! never fails on a well-formed AST.
|
||||
|
||||
use ailang_core::ast::{
|
||||
ConstDef, Ctor, Def, FnDef, Import, Literal, Module, ParamMode, Pattern, Term, Type, TypeDef,
|
||||
};
|
||||
|
||||
/// Render a [`Module`] as human-readable prose.
|
||||
///
|
||||
/// The output is deterministic; identical input AST always yields
|
||||
/// identical bytes. The renderer never fails on a well-formed AST.
|
||||
pub fn module_to_prose(m: &Module) -> String {
|
||||
let mut out = String::new();
|
||||
write_module(&mut out, m);
|
||||
out
|
||||
}
|
||||
|
||||
// ---- module ---------------------------------------------------------------
|
||||
|
||||
fn write_module(out: &mut String, m: &Module) {
|
||||
out.push_str("// module ");
|
||||
out.push_str(&m.name);
|
||||
out.push('\n');
|
||||
|
||||
if !m.imports.is_empty() {
|
||||
out.push('\n');
|
||||
for imp in &m.imports {
|
||||
write_import(out, imp);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
for def in &m.defs {
|
||||
out.push('\n');
|
||||
write_def(out, def, 0);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn write_import(out: &mut String, imp: &Import) {
|
||||
out.push_str("import ");
|
||||
out.push_str(&imp.module);
|
||||
if let Some(alias) = &imp.alias {
|
||||
out.push_str(" as ");
|
||||
out.push_str(alias);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- defs -----------------------------------------------------------------
|
||||
|
||||
fn write_def(out: &mut String, def: &Def, level: usize) {
|
||||
match def {
|
||||
Def::Type(td) => write_type_def(out, td, level),
|
||||
Def::Fn(fd) => write_fn_def(out, fd, level),
|
||||
Def::Const(cd) => write_const_def(out, cd, level),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_doc(out: &mut String, doc: &Option<String>, level: usize) {
|
||||
if let Some(d) = doc {
|
||||
for line in d.split('\n') {
|
||||
indent(out, level);
|
||||
out.push_str("/// ");
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_type_def(out: &mut String, td: &TypeDef, level: usize) {
|
||||
write_doc(out, &td.doc, level);
|
||||
indent(out, level);
|
||||
out.push_str("data ");
|
||||
out.push_str(&td.name);
|
||||
if !td.vars.is_empty() {
|
||||
out.push('<');
|
||||
for (i, v) in td.vars.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(v);
|
||||
}
|
||||
out.push('>');
|
||||
}
|
||||
out.push_str(" = ");
|
||||
for (i, c) in td.ctors.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(" | ");
|
||||
}
|
||||
write_ctor(out, c);
|
||||
}
|
||||
if td.drop_iterative {
|
||||
out.push_str(" with drop-iterative");
|
||||
}
|
||||
}
|
||||
|
||||
fn write_ctor(out: &mut String, c: &Ctor) {
|
||||
out.push_str(&c.name);
|
||||
if !c.fields.is_empty() {
|
||||
out.push('(');
|
||||
for (i, f) in c.fields.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_type(out, f);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
|
||||
fn write_fn_def(out: &mut String, fd: &FnDef, level: usize) {
|
||||
write_doc(out, &fd.doc, level);
|
||||
// The FnDef carries `params` (names) plus the type. The signature
|
||||
// surface combines them slot-for-slot. `fd.ty` is either a
|
||||
// `Type::Fn` or a `Type::Forall` wrapping one — in the latter case
|
||||
// we render the forall on the line above and then the inner fn
|
||||
// signature.
|
||||
let (forall_vars, fn_ty) = match &fd.ty {
|
||||
Type::Forall { vars, body } => (Some(vars.clone()), body.as_ref()),
|
||||
other => (None, other),
|
||||
};
|
||||
|
||||
if let Some(vars) = &forall_vars {
|
||||
indent(out, level);
|
||||
out.push_str("forall<");
|
||||
for (i, v) in vars.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(v);
|
||||
}
|
||||
out.push_str(">\n");
|
||||
}
|
||||
|
||||
indent(out, level);
|
||||
out.push_str("fn ");
|
||||
out.push_str(&fd.name);
|
||||
out.push('(');
|
||||
if let Type::Fn {
|
||||
params,
|
||||
param_modes,
|
||||
ret,
|
||||
ret_mode,
|
||||
effects,
|
||||
} = fn_ty
|
||||
{
|
||||
for (i, ty) in params.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
// Param name from FnDef.params, type+mode from Type::Fn.
|
||||
let pname = fd.params.get(i).map(String::as_str).unwrap_or("_");
|
||||
out.push_str(pname);
|
||||
out.push_str(": ");
|
||||
let mode = param_modes
|
||||
.get(i)
|
||||
.copied()
|
||||
.unwrap_or(ParamMode::Implicit);
|
||||
write_mode_type(out, ty, mode);
|
||||
}
|
||||
out.push_str(") -> ");
|
||||
write_mode_type(out, ret, *ret_mode);
|
||||
if !effects.is_empty() {
|
||||
out.push_str(" with ");
|
||||
// Stable order: as written in the AST. Effects are
|
||||
// semantically a set, but re-sorting would erase
|
||||
// author-asserted ordering; preserve the AST order.
|
||||
for (i, e) in effects.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Defensive fallback: a non-Fn fn-type. Render the bare type so
|
||||
// nothing crashes; this path is unreachable for typechecked
|
||||
// input.
|
||||
out.push_str(") -> ");
|
||||
write_type(out, fn_ty);
|
||||
}
|
||||
out.push_str(" {\n");
|
||||
indent(out, level + 1);
|
||||
write_term(out, &fd.body, level + 1);
|
||||
out.push('\n');
|
||||
indent(out, level);
|
||||
out.push('}');
|
||||
}
|
||||
|
||||
fn write_const_def(out: &mut String, cd: &ConstDef, level: usize) {
|
||||
write_doc(out, &cd.doc, level);
|
||||
indent(out, level);
|
||||
out.push_str("const ");
|
||||
out.push_str(&cd.name);
|
||||
out.push_str(": ");
|
||||
write_type(out, &cd.ty);
|
||||
out.push_str(" = ");
|
||||
write_term(out, &cd.value, level);
|
||||
}
|
||||
|
||||
// ---- types ----------------------------------------------------------------
|
||||
|
||||
fn write_mode_type(out: &mut String, t: &Type, mode: ParamMode) {
|
||||
match mode {
|
||||
ParamMode::Implicit => write_type(out, t),
|
||||
ParamMode::Own => {
|
||||
out.push_str("own ");
|
||||
write_type(out, t);
|
||||
}
|
||||
ParamMode::Borrow => {
|
||||
out.push_str("borrow ");
|
||||
write_type(out, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_type(out: &mut String, t: &Type) {
|
||||
match t {
|
||||
Type::Var { name } => out.push_str(name),
|
||||
Type::Con { name, args } => {
|
||||
out.push_str(name);
|
||||
if !args.is_empty() {
|
||||
out.push('<');
|
||||
for (i, a) in args.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_type(out, a);
|
||||
}
|
||||
out.push('>');
|
||||
}
|
||||
}
|
||||
Type::Fn {
|
||||
params,
|
||||
param_modes,
|
||||
ret,
|
||||
ret_mode,
|
||||
effects,
|
||||
} => {
|
||||
out.push('(');
|
||||
for (i, p) in params.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
let mode = param_modes
|
||||
.get(i)
|
||||
.copied()
|
||||
.unwrap_or(ParamMode::Implicit);
|
||||
// In a bare `Type::Fn` outside a fn signature we have no
|
||||
// parameter names, so render `mode T` only (no `name:`).
|
||||
write_mode_type(out, p, mode);
|
||||
}
|
||||
out.push_str(") -> ");
|
||||
write_mode_type(out, ret, *ret_mode);
|
||||
if !effects.is_empty() {
|
||||
out.push_str(" with ");
|
||||
for (i, e) in effects.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Type::Forall { vars, body } => {
|
||||
out.push_str("forall<");
|
||||
for (i, v) in vars.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(v);
|
||||
}
|
||||
out.push_str("> ");
|
||||
write_type(out, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- terms ----------------------------------------------------------------
|
||||
|
||||
fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
match t {
|
||||
Term::Lit { lit } => write_lit(out, lit),
|
||||
Term::Var { name } => out.push_str(name),
|
||||
Term::App { callee, args, tail } => {
|
||||
if *tail {
|
||||
out.push_str("tail ");
|
||||
}
|
||||
write_term(out, callee, level);
|
||||
out.push('(');
|
||||
for (i, a) in args.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_term(out, a, level);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Term::Let { name, value, body } => {
|
||||
// `let x = value;` then body on the next line at the same
|
||||
// level. Both lines are emitted by the *enclosing* context's
|
||||
// indentation; the caller indented us once already, so for
|
||||
// the body we re-indent.
|
||||
out.push_str("let ");
|
||||
out.push_str(name);
|
||||
out.push_str(" = ");
|
||||
write_term(out, value, level);
|
||||
out.push_str(";\n");
|
||||
indent(out, level);
|
||||
write_term(out, body, level);
|
||||
}
|
||||
Term::LetRec {
|
||||
name,
|
||||
ty,
|
||||
params,
|
||||
body,
|
||||
in_term,
|
||||
} => {
|
||||
// Local recursive fn-binding. Render as a nested fn-shape
|
||||
// followed by the body.
|
||||
out.push_str("let-rec ");
|
||||
out.push_str(name);
|
||||
out.push('(');
|
||||
if let Type::Fn {
|
||||
params: ptys,
|
||||
param_modes,
|
||||
ret,
|
||||
ret_mode,
|
||||
effects,
|
||||
} = ty
|
||||
{
|
||||
for (i, pty) in ptys.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
let pname = params.get(i).map(String::as_str).unwrap_or("_");
|
||||
out.push_str(pname);
|
||||
out.push_str(": ");
|
||||
let mode = param_modes
|
||||
.get(i)
|
||||
.copied()
|
||||
.unwrap_or(ParamMode::Implicit);
|
||||
write_mode_type(out, pty, mode);
|
||||
}
|
||||
out.push_str(") -> ");
|
||||
write_mode_type(out, ret, *ret_mode);
|
||||
if !effects.is_empty() {
|
||||
out.push_str(" with ");
|
||||
for (i, e) in effects.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.push_str(") -> ");
|
||||
write_type(out, ty);
|
||||
}
|
||||
out.push_str(" {\n");
|
||||
indent(out, level + 1);
|
||||
write_term(out, body, level + 1);
|
||||
out.push('\n');
|
||||
indent(out, level);
|
||||
out.push_str("};\n");
|
||||
indent(out, level);
|
||||
write_term(out, in_term, level);
|
||||
}
|
||||
Term::If { cond, then, else_ } => {
|
||||
out.push_str("if ");
|
||||
write_term(out, cond, level);
|
||||
out.push_str(" {\n");
|
||||
indent(out, level + 1);
|
||||
write_term(out, then, level + 1);
|
||||
out.push('\n');
|
||||
indent(out, level);
|
||||
out.push_str("} else {\n");
|
||||
indent(out, level + 1);
|
||||
write_term(out, else_, level + 1);
|
||||
out.push('\n');
|
||||
indent(out, level);
|
||||
out.push('}');
|
||||
}
|
||||
Term::Do { op, args, tail } => {
|
||||
if *tail {
|
||||
out.push_str("tail ");
|
||||
}
|
||||
out.push_str("do ");
|
||||
out.push_str(op);
|
||||
out.push('(');
|
||||
for (i, a) in args.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_term(out, a, level);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Term::Ctor {
|
||||
type_name: _,
|
||||
ctor,
|
||||
args,
|
||||
} => {
|
||||
out.push_str(ctor);
|
||||
if !args.is_empty() {
|
||||
out.push('(');
|
||||
for (i, a) in args.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_term(out, a, level);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
Term::Match { scrutinee, arms } => {
|
||||
out.push_str("match ");
|
||||
write_term(out, scrutinee, level);
|
||||
out.push_str(" {\n");
|
||||
for (i, arm) in arms.iter().enumerate() {
|
||||
indent(out, level + 1);
|
||||
write_pattern(out, &arm.pat);
|
||||
out.push_str(" => ");
|
||||
write_term(out, &arm.body, level + 1);
|
||||
if i + 1 < arms.len() {
|
||||
out.push(',');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
indent(out, level);
|
||||
out.push('}');
|
||||
}
|
||||
Term::Lam {
|
||||
params,
|
||||
param_tys,
|
||||
ret_ty,
|
||||
effects,
|
||||
body,
|
||||
} => {
|
||||
// Rust-closure flavour: `|p1: T1, p2: T2| -> R with EFF { body }`
|
||||
out.push('|');
|
||||
for (i, (pname, pty)) in params.iter().zip(param_tys.iter()).enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(pname);
|
||||
out.push_str(": ");
|
||||
write_type(out, pty);
|
||||
}
|
||||
out.push_str("| -> ");
|
||||
write_type(out, ret_ty);
|
||||
if !effects.is_empty() {
|
||||
out.push_str(" with ");
|
||||
for (i, e) in effects.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(e);
|
||||
}
|
||||
}
|
||||
out.push_str(" {\n");
|
||||
indent(out, level + 1);
|
||||
write_term(out, body, level + 1);
|
||||
out.push('\n');
|
||||
indent(out, level);
|
||||
out.push('}');
|
||||
}
|
||||
Term::Seq { lhs, rhs } => {
|
||||
// Semicolon-as-discard. `lhs;` on its own line, `rhs` on
|
||||
// the next at the same indent. The caller already indented us.
|
||||
write_term(out, lhs, level);
|
||||
out.push_str(";\n");
|
||||
indent(out, level);
|
||||
write_term(out, rhs, level);
|
||||
}
|
||||
Term::Clone { value } => {
|
||||
out.push_str("clone(");
|
||||
write_term(out, value, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::ReuseAs { source, body } => {
|
||||
out.push_str("reuse-as ");
|
||||
write_term(out, source, level);
|
||||
out.push_str(" {\n");
|
||||
indent(out, level + 1);
|
||||
write_term(out, body, level + 1);
|
||||
out.push('\n');
|
||||
indent(out, level);
|
||||
out.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_pattern(out: &mut String, p: &Pattern) {
|
||||
match p {
|
||||
Pattern::Wild => out.push('_'),
|
||||
Pattern::Var { name } => out.push_str(name),
|
||||
Pattern::Lit { lit } => write_lit(out, lit),
|
||||
Pattern::Ctor { ctor, fields } => {
|
||||
out.push_str(ctor);
|
||||
if !fields.is_empty() {
|
||||
out.push('(');
|
||||
for (i, f) in fields.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_pattern(out, f);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_lit(out: &mut String, lit: &Literal) {
|
||||
match lit {
|
||||
Literal::Int { value } => out.push_str(&value.to_string()),
|
||||
Literal::Bool { value } => out.push_str(if *value { "true" } else { "false" }),
|
||||
Literal::Str { value } => write_string_lit(out, value),
|
||||
Literal::Unit => out.push_str("()"),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_string_lit(out: &mut String, s: &str) {
|
||||
out.push('"');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
// ---- helpers --------------------------------------------------------------
|
||||
|
||||
fn indent(out: &mut String, level: usize) {
|
||||
for _ in 0..level {
|
||||
out.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- unit tests -----------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ailang_core::ast::{Arm, Ctor, FnDef, Literal, Pattern, Term, Type};
|
||||
|
||||
fn render_term(t: &Term) -> String {
|
||||
let mut out = String::new();
|
||||
write_term(&mut out, t, 0);
|
||||
out
|
||||
}
|
||||
|
||||
fn render_type(t: &Type) -> String {
|
||||
let mut out = String::new();
|
||||
write_type(&mut out, t);
|
||||
out
|
||||
}
|
||||
|
||||
// ---- Lit / Var ----
|
||||
|
||||
#[test]
|
||||
fn lit_int_renders_as_decimal() {
|
||||
assert_eq!(render_term(&Term::Lit { lit: Literal::Int { value: 42 } }), "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lit_bool_renders_as_keyword() {
|
||||
assert_eq!(
|
||||
render_term(&Term::Lit { lit: Literal::Bool { value: true } }),
|
||||
"true"
|
||||
);
|
||||
assert_eq!(
|
||||
render_term(&Term::Lit { lit: Literal::Bool { value: false } }),
|
||||
"false"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lit_str_renders_with_escapes() {
|
||||
assert_eq!(
|
||||
render_term(&Term::Lit { lit: Literal::Str { value: "hi\n".into() } }),
|
||||
"\"hi\\n\""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lit_unit_renders_as_unit_pair() {
|
||||
assert_eq!(render_term(&Term::Lit { lit: Literal::Unit }), "()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_renders_as_name() {
|
||||
assert_eq!(render_term(&Term::Var { name: "x".into() }), "x");
|
||||
}
|
||||
|
||||
// ---- App ----
|
||||
|
||||
#[test]
|
||||
fn app_two_args_renders_as_call() {
|
||||
let t = Term::App {
|
||||
callee: Box::new(Term::Var { name: "f".into() }),
|
||||
args: vec![
|
||||
Term::Lit { lit: Literal::Int { value: 1 } },
|
||||
Term::Lit { lit: Literal::Int { value: 2 } },
|
||||
],
|
||||
tail: false,
|
||||
};
|
||||
assert_eq!(render_term(&t), "f(1, 2)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tail_renders_with_keyword() {
|
||||
let t = Term::App {
|
||||
callee: Box::new(Term::Var { name: "f".into() }),
|
||||
args: vec![],
|
||||
tail: true,
|
||||
};
|
||||
assert_eq!(render_term(&t), "tail f()");
|
||||
}
|
||||
|
||||
// ---- Let / If ----
|
||||
|
||||
#[test]
|
||||
fn let_renders_as_binding_and_body() {
|
||||
let t = Term::Let {
|
||||
name: "x".into(),
|
||||
value: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }),
|
||||
body: Box::new(Term::Var { name: "x".into() }),
|
||||
};
|
||||
assert_eq!(render_term(&t), "let x = 1;\nx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_renders_with_braces_and_else() {
|
||||
let t = Term::If {
|
||||
cond: Box::new(Term::Lit { lit: Literal::Bool { value: true } }),
|
||||
then: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }),
|
||||
else_: Box::new(Term::Lit { lit: Literal::Int { value: 2 } }),
|
||||
};
|
||||
assert_eq!(
|
||||
render_term(&t),
|
||||
"if true {\n 1\n} else {\n 2\n}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Match ----
|
||||
|
||||
#[test]
|
||||
fn match_two_arms_renders_with_arrows_and_commas() {
|
||||
let t = Term::Match {
|
||||
scrutinee: Box::new(Term::Var { name: "xs".into() }),
|
||||
arms: vec![
|
||||
Arm {
|
||||
pat: Pattern::Ctor { ctor: "Nil".into(), fields: vec![] },
|
||||
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
||||
},
|
||||
Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: "Cons".into(),
|
||||
fields: vec![
|
||||
Pattern::Var { name: "h".into() },
|
||||
Pattern::Var { name: "t".into() },
|
||||
],
|
||||
},
|
||||
body: Term::Var { name: "h".into() },
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
render_term(&t),
|
||||
"match xs {\n Nil => 0,\n Cons(h, t) => h\n}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Ctor ----
|
||||
|
||||
#[test]
|
||||
fn ctor_nullary_renders_as_bare_name() {
|
||||
let t = Term::Ctor {
|
||||
type_name: "List".into(),
|
||||
ctor: "Nil".into(),
|
||||
args: vec![],
|
||||
};
|
||||
assert_eq!(render_term(&t), "Nil");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctor_with_args_renders_as_call() {
|
||||
let t = Term::Ctor {
|
||||
type_name: "List".into(),
|
||||
ctor: "Cons".into(),
|
||||
args: vec![
|
||||
Term::Lit { lit: Literal::Int { value: 1 } },
|
||||
Term::Ctor {
|
||||
type_name: "List".into(),
|
||||
ctor: "Nil".into(),
|
||||
args: vec![],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(render_term(&t), "Cons(1, Nil)");
|
||||
}
|
||||
|
||||
// ---- Lam ----
|
||||
|
||||
#[test]
|
||||
fn lam_renders_as_rust_closure() {
|
||||
let t = Term::Lam {
|
||||
params: vec!["x".into()],
|
||||
param_tys: vec![Type::int()],
|
||||
ret_ty: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
body: Box::new(Term::Var { name: "x".into() }),
|
||||
};
|
||||
assert_eq!(render_term(&t), "|x: Int| -> Int {\n x\n}");
|
||||
}
|
||||
|
||||
// ---- Seq / Clone / ReuseAs / Do ----
|
||||
|
||||
#[test]
|
||||
fn seq_renders_with_semicolon_and_newline() {
|
||||
let t = Term::Seq {
|
||||
lhs: Box::new(Term::Var { name: "a".into() }),
|
||||
rhs: Box::new(Term::Var { name: "b".into() }),
|
||||
};
|
||||
assert_eq!(render_term(&t), "a;\nb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_renders_explicitly() {
|
||||
let t = Term::Clone {
|
||||
value: Box::new(Term::Var { name: "x".into() }),
|
||||
};
|
||||
assert_eq!(render_term(&t), "clone(x)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reuse_as_renders_with_keyword_and_braces() {
|
||||
let t = Term::ReuseAs {
|
||||
source: Box::new(Term::Var { name: "xs".into() }),
|
||||
body: Box::new(Term::Ctor {
|
||||
type_name: "List".into(),
|
||||
ctor: "Nil".into(),
|
||||
args: vec![],
|
||||
}),
|
||||
};
|
||||
assert_eq!(render_term(&t), "reuse-as xs {\n Nil\n}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn do_renders_with_keyword_and_op() {
|
||||
let t = Term::Do {
|
||||
op: "io/print_int".into(),
|
||||
args: vec![Term::Lit { lit: Literal::Int { value: 5 } }],
|
||||
tail: false,
|
||||
};
|
||||
assert_eq!(render_term(&t), "do io/print_int(5)");
|
||||
}
|
||||
|
||||
// ---- Types ----
|
||||
|
||||
#[test]
|
||||
fn type_con_no_args_drops_wrap() {
|
||||
assert_eq!(render_type(&Type::int()), "Int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_con_with_args_renders_as_generic() {
|
||||
let t = Type::Con {
|
||||
name: "List".into(),
|
||||
args: vec![Type::int()],
|
||||
};
|
||||
assert_eq!(render_type(&t), "List<Int>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_var_renders_as_name() {
|
||||
assert_eq!(render_type(&Type::Var { name: "a".into() }), "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_fn_no_modes_no_effects() {
|
||||
let t = Type::fn_implicit(vec![Type::int()], Type::int(), vec![]);
|
||||
assert_eq!(render_type(&t), "(Int) -> Int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_fn_with_effects() {
|
||||
let t = Type::fn_implicit(vec![], Type::unit(), vec!["IO".into()]);
|
||||
assert_eq!(render_type(&t), "() -> Unit with IO");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_fn_with_modes_renders_keywords_before_type() {
|
||||
let t = Type::Fn {
|
||||
params: vec![Type::int(), Type::bool_()],
|
||||
param_modes: vec![ParamMode::Own, ParamMode::Borrow],
|
||||
ret: Box::new(Type::int()),
|
||||
ret_mode: ParamMode::Own,
|
||||
effects: vec![],
|
||||
};
|
||||
assert_eq!(render_type(&t), "(own Int, borrow Bool) -> own Int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_forall_renders_with_angle_vars() {
|
||||
let t = Type::Forall {
|
||||
vars: vec!["a".into()],
|
||||
body: Box::new(Type::Var { name: "a".into() }),
|
||||
};
|
||||
assert_eq!(render_type(&t), "forall<a> a");
|
||||
}
|
||||
|
||||
// ---- FnDef integration: modes BEFORE the type, in fn signature ----
|
||||
|
||||
#[test]
|
||||
fn fn_def_signature_renders_modes_before_type() {
|
||||
let fd = FnDef {
|
||||
name: "head_or_zero".into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![Type::Con {
|
||||
name: "IntList".into(),
|
||||
args: vec![],
|
||||
}],
|
||||
param_modes: vec![ParamMode::Own],
|
||||
ret: Box::new(Type::int()),
|
||||
ret_mode: ParamMode::Implicit,
|
||||
effects: vec![],
|
||||
},
|
||||
params: vec!["xs".into()],
|
||||
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
||||
doc: None,
|
||||
};
|
||||
let mut out = String::new();
|
||||
write_fn_def(&mut out, &fd, 0);
|
||||
assert!(out.contains("xs: own IntList"), "got:\n{out}");
|
||||
assert!(out.contains(") -> Int "), "got:\n{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_def_with_effects_appends_with_clause() {
|
||||
let fd = FnDef {
|
||||
name: "main".into(),
|
||||
ty: Type::fn_implicit(vec![], Type::unit(), vec!["IO".into()]),
|
||||
params: vec![],
|
||||
body: Term::Lit { lit: Literal::Unit },
|
||||
doc: None,
|
||||
};
|
||||
let mut out = String::new();
|
||||
write_fn_def(&mut out, &fd, 0);
|
||||
assert!(out.contains("() -> Unit with IO {"), "got:\n{out}");
|
||||
}
|
||||
|
||||
// ---- Doc string lines ----
|
||||
|
||||
#[test]
|
||||
fn doc_string_renders_as_triple_slash_lines() {
|
||||
let td = TypeDef {
|
||||
name: "Foo".into(),
|
||||
vars: vec![],
|
||||
ctors: vec![Ctor {
|
||||
name: "MkFoo".into(),
|
||||
fields: vec![],
|
||||
}],
|
||||
doc: Some("line one\nline two".into()),
|
||||
drop_iterative: false,
|
||||
};
|
||||
let mut out = String::new();
|
||||
write_type_def(&mut out, &td, 0);
|
||||
assert!(out.starts_with("/// line one\n/// line two\n"), "got:\n{out}");
|
||||
}
|
||||
|
||||
// ---- TypeDef ----
|
||||
|
||||
#[test]
|
||||
fn type_def_renders_with_pipe_separated_ctors() {
|
||||
let td = TypeDef {
|
||||
name: "IntList".into(),
|
||||
vars: vec![],
|
||||
ctors: vec![
|
||||
Ctor { name: "Nil".into(), fields: vec![] },
|
||||
Ctor {
|
||||
name: "Cons".into(),
|
||||
fields: vec![Type::int(), Type::Con { name: "IntList".into(), args: vec![] }],
|
||||
},
|
||||
],
|
||||
doc: None,
|
||||
drop_iterative: false,
|
||||
};
|
||||
let mut out = String::new();
|
||||
write_type_def(&mut out, &td, 0);
|
||||
assert_eq!(out, "data IntList = Nil | Cons(Int, IntList)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user