Files
AILang/crates/ailang-surface/src/print.rs
T
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00

851 lines
27 KiB
Rust

//! Pretty-printer for form (A).
//!
//! Mirrors the parser in [`mod@crate::parse`]. Output is deterministic
//! and parseable by the parser — round-trip is the gating contract
//! (see `tests/round_trip.rs`).
//!
//! Indentation is informational only (the lexer ignores it). Two-space
//! per level. Comments are NOT emitted.
use ailang_core::ast::{
Arm, ClassDef, ClassMethod, Constraint, ConstDef, Ctor, Def, FnDef, Import, InstanceDef,
InstanceMethod, Literal, Module, ParamMode, Pattern, SuperclassRef, Suppress, Term,
Type, TypeDef,
};
/// Print a module in form (A).
pub fn print(module: &Module) -> String {
let mut out = String::new();
out.push('(');
out.push_str("module ");
out.push_str(&module.name);
for imp in &module.imports {
out.push('\n');
write_import(&mut out, imp, 1);
}
for def in &module.defs {
out.push('\n');
write_def(&mut out, def, 1);
}
out.push(')');
out.push('\n');
out
}
/// Print a single [`Term`] in form (A) — the dual of
/// [`crate::parse::parse_term`]. Used by callers that produce form-A
/// snippets out-of-band, e.g. the `suggested_rewrites` payload of
/// `ail check --json` (Iter 18c.2). The output is round-trip stable
/// (`parse_term(term_to_form_a(t)) ≡ t`) but does not include a trailing
/// newline; callers that want one append it themselves.
pub fn term_to_form_a(t: &Term) -> String {
let mut out = String::new();
write_term(&mut out, t, 0);
out
}
/// render a [`Type`] as form-A. Used by diagnostics that want
/// to suggest a relaxed signature (e.g. `over-strict-mode` shows the
/// `(fn-type ...)` with `(borrow T)` in place of `(own T)`). Output is
/// the same shape that appears as the `type` slot of a `(fn ...)` def.
pub fn type_to_form_a(t: &Type) -> String {
let mut out = String::new();
write_type(&mut out, t);
out
}
// ---- helpers --------------------------------------------------------------
fn indent(out: &mut String, level: usize) {
for _ in 0..level {
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('"');
}
// ---- imports & defs -------------------------------------------------------
fn write_import(out: &mut String, imp: &Import, level: usize) {
indent(out, level);
out.push_str("(import ");
out.push_str(&imp.module);
if let Some(alias) = &imp.alias {
out.push_str(" as ");
out.push_str(alias);
}
out.push(')');
}
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),
Def::Class(c) => write_class_def(out, c, level),
Def::Instance(i) => write_instance_def(out, i, level),
}
}
fn write_type_def(out: &mut String, td: &TypeDef, level: usize) {
indent(out, level);
out.push_str("(data ");
out.push_str(&td.name);
if !td.vars.is_empty() {
out.push_str(" (vars");
for v in &td.vars {
out.push(' ');
out.push_str(v);
}
out.push(')');
}
if let Some(doc) = &td.doc {
out.push('\n');
indent(out, level + 1);
out.push_str("(doc ");
write_string_lit(out, doc);
out.push(')');
}
for c in &td.ctors {
out.push('\n');
write_ctor(out, c, level + 1);
}
// `(drop-iterative)` annotation. Printed last (after
// every ctor) so the canonical form lands consistent with the
// design/contracts/0002-data-model.md example. Omitted when `drop_iterative == false`.
if td.drop_iterative {
out.push('\n');
indent(out, level + 1);
out.push_str("(drop-iterative)");
}
out.push(')');
}
fn write_ctor(out: &mut String, c: &Ctor, level: usize) {
indent(out, level);
out.push_str("(ctor ");
out.push_str(&c.name);
for f in &c.fields {
out.push(' ');
write_type(out, f);
}
out.push(')');
}
fn write_fn_def(out: &mut String, fd: &FnDef, level: usize) {
indent(out, level);
out.push_str("(fn ");
out.push_str(&fd.name);
if let Some(doc) = &fd.doc {
out.push('\n');
indent(out, level + 1);
out.push_str("(doc ");
write_string_lit(out, doc);
out.push(')');
}
if let Some(export) = &fd.export {
out.push('\n');
indent(out, level + 1);
out.push_str("(export ");
write_string_lit(out, export);
out.push(')');
}
// emit one `(suppress ...)` clause per entry, after the
// doc string and before the type. Round-trip stable: parser
// re-reads each clause back into [`FnDef::suppress`].
for s in &fd.suppress {
out.push('\n');
write_suppress(out, s, level + 1);
}
out.push('\n');
indent(out, level + 1);
out.push_str("(type ");
write_type(out, &fd.ty);
out.push(')');
out.push('\n');
indent(out, level + 1);
out.push_str("(params");
for p in &fd.params {
out.push(' ');
out.push_str(p);
}
out.push(')');
out.push('\n');
indent(out, level + 1);
out.push_str("(body ");
write_term(out, &fd.body, level + 2);
out.push(')');
out.push(')');
}
/// print one `(suppress (code "<c>") (because "<r>"))`
/// clause. Indentation matches the rest of the fn-def (one level
/// deeper than the `(fn ...)` head).
fn write_suppress(out: &mut String, s: &Suppress, level: usize) {
indent(out, level);
out.push_str("(suppress (code ");
write_string_lit(out, &s.code);
out.push_str(") (because ");
write_string_lit(out, &s.because);
out.push_str("))");
}
fn write_const_def(out: &mut String, cd: &ConstDef, level: usize) {
indent(out, level);
out.push_str("(const ");
out.push_str(&cd.name);
if let Some(doc) = &cd.doc {
out.push('\n');
indent(out, level + 1);
out.push_str("(doc ");
write_string_lit(out, doc);
out.push(')');
}
out.push('\n');
indent(out, level + 1);
out.push_str("(type ");
write_type(out, &cd.ty);
out.push(')');
out.push('\n');
indent(out, level + 1);
out.push_str("(body ");
write_term(out, &cd.value, level + 2);
out.push(')');
out.push(')');
}
fn write_class_def(out: &mut String, c: &ClassDef, level: usize) {
indent(out, level);
out.push_str("(class ");
out.push_str(&c.name);
out.push('\n');
indent(out, level + 1);
out.push_str("(param ");
out.push_str(&c.param);
out.push(')');
if let Some(sc) = &c.superclass {
out.push('\n');
write_superclass(out, sc, level + 1);
}
if let Some(doc) = &c.doc {
out.push('\n');
indent(out, level + 1);
out.push_str("(doc ");
write_string_lit(out, doc);
out.push(')');
}
for m in &c.methods {
out.push('\n');
write_class_method(out, m, level + 1);
}
out.push(')');
}
fn write_superclass(out: &mut String, sc: &SuperclassRef, level: usize) {
indent(out, level);
out.push_str("(superclass (class ");
out.push_str(&sc.class);
out.push_str(") (type ");
out.push_str(&sc.type_);
out.push_str("))");
}
fn write_class_method(out: &mut String, m: &ClassMethod, level: usize) {
indent(out, level);
out.push_str("(method ");
out.push_str(&m.name);
out.push('\n');
indent(out, level + 1);
out.push_str("(type ");
write_type(out, &m.ty);
out.push(')');
if let Some(default) = &m.default {
out.push('\n');
indent(out, level + 1);
out.push_str("(default ");
write_term(out, default, level + 2);
out.push(')');
}
out.push(')');
}
fn write_instance_def(out: &mut String, i: &InstanceDef, level: usize) {
indent(out, level);
out.push_str("(instance");
out.push('\n');
indent(out, level + 1);
out.push_str("(class ");
out.push_str(&i.class);
out.push(')');
out.push('\n');
indent(out, level + 1);
out.push_str("(type ");
write_type(out, &i.type_);
out.push(')');
if let Some(doc) = &i.doc {
out.push('\n');
indent(out, level + 1);
out.push_str("(doc ");
write_string_lit(out, doc);
out.push(')');
}
for m in &i.methods {
out.push('\n');
write_instance_method(out, m, level + 1);
}
out.push(')');
}
fn write_instance_method(out: &mut String, m: &InstanceMethod, level: usize) {
indent(out, level);
out.push_str("(method ");
out.push_str(&m.name);
out.push('\n');
indent(out, level + 1);
out.push_str("(body ");
write_term(out, &m.body, level + 2);
out.push(')');
out.push(')');
}
// ---- types ----------------------------------------------------------------
/// print one fn-type param/ret slot, wrapping with
/// `(borrow ...)` or `(own ...)` when the slot has an explicit
/// mode. `Implicit` is printed bare so pre-18a fixtures round-trip
/// unchanged.
fn write_fn_type_slot(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);
out.push(')');
}
ParamMode::Borrow => {
out.push_str("(borrow ");
write_type(out, t);
out.push(')');
}
}
}
/// print one `(constraint <Class> <type>)` pair, used
/// inside the optional `(constraints …)` clause of a `(forall …)`. The
/// clause itself is omitted entirely when `Type::Forall.constraints` is
/// empty so pre-22b.2 forall fixtures stay bit-identical.
fn write_constraint(out: &mut String, c: &Constraint) {
out.push_str("(constraint ");
out.push_str(&c.class);
out.push(' ');
write_type(out, &c.type_);
out.push(')');
}
fn write_type(out: &mut String, t: &Type) {
match t {
Type::Var { name } => out.push_str(name),
Type::Con { name, args } => {
out.push_str("(con ");
out.push_str(name);
for a in args {
out.push(' ');
write_type(out, a);
}
out.push(')');
}
Type::Fn {
params,
param_modes,
ret,
ret_mode,
effects,
} => {
out.push_str("(fn-type (params");
for (i, p) in params.iter().enumerate() {
out.push(' ');
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
write_fn_type_slot(out, p, mode);
}
out.push_str(") (ret ");
write_fn_type_slot(out, ret, *ret_mode);
out.push(')');
if !effects.is_empty() {
out.push_str(" (effects");
for e in effects {
out.push(' ');
out.push_str(e);
}
out.push(')');
}
out.push(')');
}
Type::Forall { vars, constraints, body } => {
out.push_str("(forall (vars");
for v in vars {
out.push(' ');
out.push_str(v);
}
out.push(')');
if !constraints.is_empty() {
out.push_str(" (constraints");
for c in constraints {
out.push(' ');
write_constraint(out, c);
}
out.push(')');
}
out.push(' ');
write_type(out, body);
out.push(')');
}
}
}
// ---- 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 } => {
// `tail-app` is the form for `App { tail: true }`;
// otherwise the regular `app` head is used. Both productions
// are positional analogues of each other — only the head
// keyword differs.
out.push_str(if *tail { "(tail-app " } else { "(app " });
write_term(out, callee, level);
for a in args {
out.push(' ');
write_term(out, a, level);
}
out.push(')');
}
Term::Let { name, value, body } => {
out.push_str("(let ");
out.push_str(name);
out.push(' ');
write_term(out, value, level);
out.push(' ');
write_term(out, body, level);
out.push(')');
}
Term::LetRec { name, ty, params, body, in_term } => {
out.push_str("(let-rec ");
out.push_str(name);
out.push_str(" (params");
for p in params {
out.push(' ');
out.push_str(p);
}
out.push_str(") (type ");
write_type(out, ty);
out.push_str(") (body ");
write_term(out, body, level);
out.push_str(") (in ");
write_term(out, in_term, level);
out.push_str("))");
}
Term::If { cond, then, else_ } => {
out.push_str("(if ");
write_term(out, cond, level);
out.push(' ');
write_term(out, then, level);
out.push(' ');
write_term(out, else_, level);
out.push(')');
}
Term::Do { op, args, tail } => {
// `tail-do` mirrors `tail-app` for effect ops.
out.push_str(if *tail { "(tail-do " } else { "(do " });
out.push_str(op);
for a in args {
out.push(' ');
write_term(out, a, level);
}
out.push(')');
}
Term::Ctor { type_name, ctor, args } => {
out.push_str("(term-ctor ");
out.push_str(type_name);
out.push(' ');
out.push_str(ctor);
for a in args {
out.push(' ');
write_term(out, a, level);
}
out.push(')');
}
Term::Match { scrutinee, arms } => {
out.push_str("(match ");
write_term(out, scrutinee, level);
for arm in arms {
out.push('\n');
indent(out, level);
write_arm(out, arm, level);
}
out.push(')');
}
Term::Lam {
params,
param_tys,
ret_ty,
effects,
body,
} => {
out.push_str("(lam (params");
for (name, ty) in params.iter().zip(param_tys.iter()) {
out.push_str(" (typed ");
out.push_str(name);
out.push(' ');
write_type(out, ty);
out.push(')');
}
out.push_str(") (ret ");
write_type(out, ret_ty);
out.push(')');
if !effects.is_empty() {
out.push_str(" (effects");
for e in effects {
out.push(' ');
out.push_str(e);
}
out.push(')');
}
out.push_str(" (body ");
write_term(out, body, level);
out.push_str("))");
}
Term::Seq { lhs, rhs } => {
out.push_str("(seq ");
write_term(out, lhs, level);
out.push(' ');
write_term(out, rhs, level);
out.push(')');
}
Term::Clone { value } => {
// print as `(clone <inner>)`. The wrapper is
// identity at typecheck/codegen in 18c.1; only authored
// intent is recorded for the future inc/dec emission pass.
out.push_str("(clone ");
write_term(out, value, level);
out.push(')');
}
Term::ReuseAs { source, body } => {
// print as `(reuse-as <source> <body>)`. The
// wrapper is identity at codegen in 18d.1 (the `body` is
// lowered, the `source` is dropped); 18d.2 will lower this
// as in-place rewrite under `--alloc=rc`.
out.push_str("(reuse-as ");
write_term(out, source, level);
out.push(' ');
write_term(out, body, level);
out.push(')');
}
Term::Loop { binders, body } => {
// loop-recur iter 1: print as
// `(loop (NAME TYPE INIT)* STMT* FINAL_EXPR)`
// — inverse of parse_loop's binder read + Seq right-fold.
out.push_str("(loop");
for b in binders {
out.push_str(" (");
out.push_str(&b.name);
out.push(' ');
write_type(out, &b.ty);
out.push(' ');
write_term(out, &b.init, level);
out.push(')');
}
let mut cursor: &Term = body;
loop {
match cursor {
Term::Seq { lhs, rhs } => {
out.push(' ');
write_term(out, lhs, level);
cursor = rhs;
}
other => {
out.push(' ');
write_term(out, other, level);
break;
}
}
}
out.push(')');
}
Term::Recur { args } => {
out.push_str("(recur");
for a in args {
out.push(' ');
write_term(out, a, level);
}
out.push(')');
}
}
}
fn write_arm(out: &mut String, arm: &Arm, level: usize) {
out.push_str("(case ");
write_pattern(out, &arm.pat);
out.push(' ');
write_term(out, &arm.body, level + 1);
out.push(')');
}
/// Render a Float literal's bit pattern as surface text. The output
/// is the shortest round-trippable decimal produced by
/// `f64::to_string` (Grisu3). If the rendered form contains
/// neither `.` nor `e`/`E` (`f64::to_string` returns "1" for
/// `1.0_f64`, "10000000000" for `1e10_f64`), append `.0` so the
/// lexer's `is_int`/`has_dot|has_exp` dispatch routes the token to
/// the Float path on re-lex. This preserves the
/// `lex(print(L)) == L` round-trip property pinned by
/// [`print_then_parse_round_trip_float_literal`].
///
/// Non-finite bit patterns (NaN, ±Inf) cannot be expressed in
/// surface form (per spec section A2 — the lexer rejects all such
/// inputs). A Float literal in the AST whose bits are non-finite
/// must therefore have arrived via Form-A directly. The printer's
/// job is surface output, not a Form-A escape hatch, so we panic
/// rather than emit a non-round-trippable token.
fn write_float_lit(out: &mut String, bits: u64) {
let f = f64::from_bits(bits);
if !f.is_finite() {
panic!(
"write_float_lit: non-finite Float literal {bits:#018x} \
cannot be expressed in surface form"
);
}
let s = f.to_string();
out.push_str(&s);
if !s.contains('.') && !s.contains('e') && !s.contains('E') {
out.push_str(".0");
}
}
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("(lit-unit)"),
Literal::Float { bits } => write_float_lit(out, *bits),
}
}
fn write_pattern(out: &mut String, p: &Pattern) {
match p {
Pattern::Wild => out.push('_'),
Pattern::Var { name } => out.push_str(name),
Pattern::Lit { lit } => {
out.push_str("(pat-lit ");
// Inside pat-lit, write the bare literal form (no `(lit-unit)`
// — Unit is not allowed in pat-lit per the grammar).
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 => {
// Unit is not a valid pat-lit; fall back to a bare unit-lit
// form. This is unreachable for any well-formed AST that
// came through the typechecker.
out.push_str("(lit-unit)");
}
Literal::Float { bits } => write_float_lit(out, *bits),
}
out.push(')');
}
Pattern::Ctor { ctor, fields } => {
out.push_str("(pat-ctor ");
out.push_str(ctor);
for f in fields {
out.push(' ');
write_pattern(out, f);
}
out.push(')');
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(m: Module, label: &str) {
let printed = print(&m);
let parsed = crate::parse::parse(&printed)
.unwrap_or_else(|e| panic!(
"{label}: re-parse failed: {e:?}\nprinted:\n{printed}"
));
let bytes_orig = ailang_core::canonical::to_bytes(&m);
let bytes_back = ailang_core::canonical::to_bytes(&parsed);
if bytes_orig != bytes_back {
let s_orig = String::from_utf8_lossy(&bytes_orig).into_owned();
let s_back = String::from_utf8_lossy(&bytes_back).into_owned();
panic!(
"{label}: canonical bytes differ.\noriginal: {s_orig}\nround: {s_back}\nprinted:\n{printed}"
);
}
}
#[test]
fn print_then_parse_round_trip_minimal_class() {
let m = Module {
schema: ailang_core::SCHEMA.to_string(),
name: "M".into(),
imports: vec![],
defs: vec![Def::Class(ClassDef {
name: "Foo".into(),
param: "a".into(),
superclass: None,
methods: vec![ClassMethod {
name: "m".into(),
ty: Type::Fn {
params: vec![Type::Var { name: "a".into() }],
param_modes: vec![ParamMode::Implicit],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
effects: vec![],
},
default: None,
}],
doc: None,
})],
};
round_trip(m, "minimal_class");
}
#[test]
fn print_then_parse_round_trip_class_with_superclass_and_default() {
let m = Module {
schema: ailang_core::SCHEMA.to_string(),
name: "M".into(),
imports: vec![],
defs: vec![Def::Class(ClassDef {
name: "Bar".into(),
param: "a".into(),
superclass: Some(SuperclassRef {
class: "Foo".into(),
type_: "a".into(),
}),
methods: vec![ClassMethod {
name: "m".into(),
ty: Type::Fn {
params: vec![Type::Var { name: "a".into() }],
param_modes: vec![ParamMode::Implicit],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
effects: vec![],
},
default: Some(Term::Lit { lit: Literal::Int { value: 0 } }),
}],
doc: Some("docline".into()),
})],
};
round_trip(m, "class_with_superclass_and_default");
}
#[test]
fn print_then_parse_round_trip_forall_with_constraint() {
let m = Module {
schema: ailang_core::SCHEMA.to_string(),
name: "M".into(),
imports: vec![],
defs: vec![Def::Fn(ailang_core::ast::FnDef {
name: "f".into(),
ty: Type::Forall {
vars: vec!["a".into()],
constraints: vec![ailang_core::ast::Constraint {
class: "Show".into(),
type_: Type::Var { name: "a".into() },
}],
body: Box::new(Type::Fn {
params: vec![Type::Var { name: "a".into() }],
param_modes: vec![ParamMode::Implicit],
ret: Box::new(Type::Con {
name: "Str".into(),
args: vec![],
}),
ret_mode: ParamMode::Implicit,
effects: vec![],
}),
},
params: vec!["x".into()],
body: Term::Var { name: "x".into() },
doc: None,
export: None,
suppress: vec![],
})],
};
round_trip(m, "forall_with_constraint");
}
#[test]
fn print_then_parse_round_trip_minimal_instance() {
let m = Module {
schema: ailang_core::SCHEMA.to_string(),
name: "M".into(),
imports: vec![],
defs: vec![Def::Instance(InstanceDef {
class: "Foo".into(),
type_: Type::int(),
methods: vec![InstanceMethod {
name: "m".into(),
body: Term::Lit { lit: Literal::Int { value: 5 } },
}],
doc: None,
})],
};
round_trip(m, "minimal_instance");
}
/// `lex(print(L)) == L` round-trip property
/// for Float literals. Six representative bit patterns:
/// `1.5`, `0.0`, `-0.0`, `10.0`, `-0.375`, `1e10`. The round-trip
/// uses the existing `round_trip(Module, &str)` helper, which
/// prints the module to surface form, re-parses it, and asserts
/// canonical bytes match. The Float arm in surface print MUST emit
/// at least one of `.` or `e`/`E` so the lexer recognises the
/// output as a Float (not an Int) — `f64::to_string` returns "1"
/// for `1.0_f64`, "10000000000" for `1e10_f64`, and the printer's
/// post-pass adds `.0` in those cases.
#[test]
fn print_then_parse_round_trip_float_literal() {
use ailang_core::ast::*;
for &bits in &[
0x3ff8_0000_0000_0000u64, // 1.5
0x0000_0000_0000_0000u64, // 0.0
0x8000_0000_0000_0000u64, // -0.0
0x4024_0000_0000_0000u64, // 10.0
0xbfd8_0000_0000_0000u64, // -0.375
0x4202_a05f_2000_0000u64, // 1e10
] {
let m = Module {
schema: ailang_core::SCHEMA.to_string(),
name: "M".into(),
imports: vec![],
defs: vec![Def::Const(ConstDef {
name: "k".into(),
ty: Type::float(),
value: Term::Lit { lit: Literal::Float { bits } },
doc: None,
})],
};
round_trip(m, &format!("float_literal_{bits:#018x}"));
}
}
}