1use std::fmt::Write;
14
15use crate::{Def, EffectKind, Module, PrimOp, Term, Ty};
16
17pub fn module(m: &Module) -> String {
20 let mut out = String::new();
21 let _ = writeln!(out, "weft module · entry {}", m.entry);
22 let mut order: Vec<_> = m.defs.keys().collect();
23 order.sort_by_key(|h| **h != m.entry); for h in order {
25 let _ = writeln!(out, "\n{}", def(&m.defs[h]));
26 let _ = writeln!(out, " = {}", h);
27 }
28 out
29}
30
31pub fn def(d: &Def) -> String {
33 let mut out = String::new();
34 let params: Vec<String> = d
35 .params
36 .iter()
37 .enumerate()
38 .map(|(i, t)| format!("{}: {}", name(i), ty(t)))
39 .collect();
40 let _ = write!(out, "def ({}) -> {}", params.join(", "), ty(&d.ret));
41 if !d.effects.is_empty() {
42 let effs: Vec<&str> = d.effects.iter().map(|e| effect_name(*e)).collect();
43 let _ = write!(out, "\n effects: {}", effs.join(", "));
44 }
45 let depth = d.params.len();
46 if let Some(pre) = &d.pre {
47 let _ = write!(out, "\n requires {}", term(pre, depth));
48 }
49 if let Some(post) = &d.post {
50 let _ = write!(out, "\n ensures {}", term(post, depth + 1));
51 }
52 let _ = write!(out, "\n {}", term(&d.body, depth));
53 out
54}
55
56fn name(depth: usize) -> String {
58 let mut n = depth;
59 let mut s = String::new();
60 loop {
61 s.insert(0, (b'a' + (n % 26) as u8) as char);
62 if n < 26 {
63 break;
64 }
65 n = n / 26 - 1;
66 }
67 s
68}
69
70fn weft_fix_scale() -> i64 {
71 crate::FIX_SCALE
72}
73
74fn ty(t: &Ty) -> String {
75 match t {
76 Ty::Int => "Int".into(),
77 Ty::Fix => "Fix".into(),
78 Ty::Bool => "Bool".into(),
79 Ty::Text => "Text".into(),
80 Ty::Action => "Action".into(),
81 Ty::List(e) => format!("List {}", ty(e)),
82 Ty::Record(fs) => {
83 let fields: Vec<String> = fs.iter().map(|(k, v)| format!("{k}: {}", ty(v))).collect();
84 format!("{{{}}}", fields.join(", "))
85 }
86 }
87}
88
89fn effect_name(e: EffectKind) -> &'static str {
90 match e {
91 EffectKind::Notify => "notify",
92 EffectKind::Navigate => "navigate",
93 EffectKind::CodexOpen => "codex.open",
94 EffectKind::CommerceBuy => "commerce.buy",
95 EffectKind::PresenceEmit => "presence.emit",
96 EffectKind::SetState => "set_state",
97 EffectKind::GiveItem => "give_item",
98 EffectKind::Despawn => "despawn",
99 EffectKind::Spawn => "spawn",
100 }
101}
102
103fn prim_name(p: PrimOp) -> &'static str {
104 match p {
105 PrimOp::Add => "+",
106 PrimOp::Sub => "-",
107 PrimOp::Mul => "*",
108 PrimOp::Div => "/",
109 PrimOp::Lt => "<",
110 PrimOp::Le => "<=",
111 PrimOp::EqInt | PrimOp::EqText => "==",
112 PrimOp::And => "and",
113 PrimOp::Or => "or",
114 PrimOp::Not => "not",
115 PrimOp::Concat => "++",
116 PrimOp::ToText => "text",
117 PrimOp::FAdd => "+.",
118 PrimOp::FSub => "-.",
119 PrimOp::FMul => "*.",
120 PrimOp::FDiv => "/.",
121 PrimOp::FLt => "<.",
122 PrimOp::FLe => "<=.",
123 PrimOp::EqFix => "==.",
124 PrimOp::FixOfInt => "fix",
125 PrimOp::IntOfFix => "trunc",
126 PrimOp::FixToText => "text",
127 PrimOp::Len => "len",
128 PrimOp::FSin => "sin",
129 PrimOp::FCos => "cos",
130 PrimOp::ListCat => "++list",
131 }
132}
133
134fn term(t: &Term, depth: usize) -> String {
136 match t {
137 Term::Int(v) => v.to_string(),
138 Term::Fix(v) => {
139 let s = weft_fix_scale();
140 let sign = if *v < 0 { "-" } else { "" };
141 let (w, f) = ((v / s).abs(), (v % s).abs());
142 if f == 0 {
143 format!("{sign}{w}.0")
144 } else {
145 format!("{sign}{w}.{}", format!("{f:06}").trim_end_matches('0'))
146 }
147 }
148 Term::Bool(b) => b.to_string(),
149 Term::Text(s) => format!("{s:?}"),
150 Term::Var(i) => match (depth as u32).checked_sub(1 + i) {
152 Some(k) => name(k as usize),
153 None => format!("?var{i}"),
154 },
155 Term::Let(v, b) => {
156 format!(
157 "let {} = {} in\n {}",
158 name(depth),
159 term(v, depth),
160 term(b, depth + 1)
161 )
162 }
163 Term::Map { cap, list, body } => format!(
164 "map[{cap}] {} ({} -> {})",
165 term(list, depth),
166 name(depth),
167 term(body, depth + 1)
168 ),
169 Term::Iota { cap, count } => format!("iota[{cap}] {}", term(count, depth)),
170 Term::Fold {
171 cap,
172 list,
173 init,
174 body,
175 } => format!(
176 "fold[{cap}] {} from {} ({} {} -> {})",
177 term(list, depth),
178 term(init, depth),
179 name(depth),
180 name(depth + 1),
181 term(body, depth + 2)
182 ),
183 Term::If(c, a, b) => format!(
184 "if {} then {} else {}",
185 term(c, depth),
186 term(a, depth),
187 term(b, depth)
188 ),
189 Term::Prim(PrimOp::Not, args) => format!("not {}", term(&args[0], depth)),
190 Term::Prim(PrimOp::ToText, args) => format!("text({})", term(&args[0], depth)),
191 Term::Prim(op, args) if args.len() == 2 => format!(
192 "({} {} {})",
193 term(&args[0], depth),
194 prim_name(*op),
195 term(&args[1], depth)
196 ),
197 Term::Prim(op, args) => {
198 let xs: Vec<String> = args.iter().map(|a| term(a, depth)).collect();
199 format!("{}({})", prim_name(*op), xs.join(", "))
200 }
201 Term::Rec(fields) => {
202 let fs: Vec<String> = fields
203 .iter()
204 .map(|(k, v)| format!("{k}: {}", term(v, depth)))
205 .collect();
206 format!("{{{}}}", fs.join(", "))
207 }
208 Term::Get(r, k) => format!("{}.{k}", term(r, depth)),
209 Term::ListNew(items) => {
210 let xs: Vec<String> = items.iter().map(|i| term(i, depth)).collect();
211 format!("[{}]", xs.join(", "))
212 }
213 Term::Call(h, args) => {
214 let xs: Vec<String> = args.iter().map(|a| term(a, depth)).collect();
215 format!("{:?}({})", h, xs.join(", "))
216 }
217 Term::Effect(kind, fields) => {
218 let fs: Vec<String> = fields
219 .iter()
220 .map(|(k, v)| format!("{k}: {}", term(v, depth)))
221 .collect();
222 format!("{}{{{}}}", effect_name(*kind), fs.join(", "))
223 }
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::{hash_def, PrimOp, Term};
231 use std::collections::BTreeSet;
232
233 #[test]
234 fn a_projection_is_readable_and_deterministic() {
235 let inc = Def {
237 params: vec![Ty::Int],
238 ret: Ty::Int,
239 effects: BTreeSet::new(),
240 body: Term::Prim(PrimOp::Add, vec![Term::Var(0), Term::Int(1)]),
241 pre: Some(Term::Prim(PrimOp::Le, vec![Term::Int(0), Term::Var(0)])),
242 post: Some(Term::Prim(PrimOp::Lt, vec![Term::Var(1), Term::Var(0)])),
243 };
244 let h = hash_def(&inc);
245 let m = Module::build(vec![inc], 0).unwrap();
246 let text = module(&m);
247 assert!(text.contains("def (a: Int) -> Int"), "{text}");
248 assert!(text.contains("requires (0 <= a)"), "{text}");
249 assert!(text.contains("ensures (a < b)"), "{text}");
251 assert!(text.contains("(a + 1)"), "{text}");
252 assert!(text.contains(&h.to_string()), "cited by hash");
253 assert_eq!(text, module(&m), "same module, same projection, always");
254 }
255
256 #[test]
257 fn effects_and_lets_render_with_generated_names() {
258 let d = Def {
259 params: vec![Ty::Int],
260 ret: Ty::Action,
261 effects: BTreeSet::from([crate::EffectKind::Notify]),
262 body: Term::Let(
263 Box::new(Term::Prim(PrimOp::ToText, vec![Term::Var(0)])),
264 Box::new(Term::Effect(
265 crate::EffectKind::Notify,
266 [("text".to_string(), Term::Var(0))].into(),
267 )),
268 ),
269 pre: None,
270 post: None,
271 };
272 let text = def(&d);
273 assert!(text.contains("effects: notify"), "{text}");
274 assert!(text.contains("let b = text(a) in"), "{text}");
275 assert!(text.contains("notify{text: b}"), "{text}");
276 }
277}