1use std::collections::{BTreeMap, BTreeSet};
27use std::fmt;
28
29use serde::{Deserialize, Serialize};
30use sha2::{Digest, Sha256};
31
32pub mod draft_lib;
33pub mod model_lib;
34pub mod pack;
35pub mod project;
36
37#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub struct WeftHash(pub [u8; 32]);
46
47impl Serialize for WeftHash {
48 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
49 ser.serialize_str(&self.to_string())
50 }
51}
52
53impl<'de> Deserialize<'de> for WeftHash {
54 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
55 let s = String::deserialize(de)?;
56 let hex = s
57 .strip_prefix("weft:")
58 .ok_or_else(|| serde::de::Error::custom("weft hash must start with 'weft:'"))?;
59 if hex.len() != 64 {
60 return Err(serde::de::Error::custom("weft hash must be 64 hex chars"));
61 }
62 let mut out = [0u8; 32];
63 for (i, byte) in out.iter_mut().enumerate() {
64 *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
65 .map_err(|_| serde::de::Error::custom("invalid hex in weft hash"))?;
66 }
67 Ok(WeftHash(out))
68 }
69}
70
71impl fmt::Display for WeftHash {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "weft:")?;
74 for b in &self.0 {
75 write!(f, "{b:02x}")?;
76 }
77 Ok(())
78 }
79}
80
81impl fmt::Debug for WeftHash {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 write!(f, "weft:")?;
85 for b in &self.0[..4] {
86 write!(f, "{b:02x}")?;
87 }
88 write!(f, "…")
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub enum Ty {
99 Int,
100 Bool,
101 Text,
102 Fix,
106 Action,
108 List(Box<Ty>),
109 Record(BTreeMap<String, Ty>),
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum EffectKind {
124 Notify,
125 Navigate,
126 CodexOpen,
127 CommerceBuy,
128 PresenceEmit,
129 SetState,
130 GiveItem,
131 Despawn,
132 Spawn,
134}
135
136impl EffectKind {
137 fn tag(self) -> u8 {
139 match self {
140 EffectKind::Notify => 0,
141 EffectKind::Navigate => 1,
142 EffectKind::CodexOpen => 2,
143 EffectKind::CommerceBuy => 3,
144 EffectKind::PresenceEmit => 4,
145 EffectKind::SetState => 5,
146 EffectKind::GiveItem => 6,
147 EffectKind::Despawn => 7,
148 EffectKind::Spawn => 8,
149 }
150 }
151}
152
153pub const FIX_SCALE: i64 = 1_000_000;
159
160pub const MAX_LIST: usize = 8192;
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case")]
169pub enum PrimOp {
170 Add,
171 Sub,
172 Mul,
173 Div,
175 Lt,
176 Le,
177 EqInt,
178 EqText,
179 And,
180 Or,
181 Not,
182 Concat,
183 ToText,
185 FAdd,
188 FSub,
190 FMul,
192 FDiv,
194 FLt,
195 FLe,
196 EqFix,
197 FixOfInt,
199 IntOfFix,
201 FixToText,
204 Len,
206 FSin,
210 FCos,
212 ListCat,
218}
219
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223pub enum Term {
224 Int(i64),
225 Fix(i64),
227 Bool(bool),
228 Text(String),
229 Var(u32),
231 Let(Box<Term>, Box<Term>),
233 If(Box<Term>, Box<Term>, Box<Term>),
234 Prim(PrimOp, Vec<Term>),
235 Rec(BTreeMap<String, Term>),
237 Get(Box<Term>, String),
239 ListNew(Vec<Term>),
240 Call(WeftHash, Vec<Term>),
242 Effect(EffectKind, BTreeMap<String, Term>),
244 Map {
248 cap: u32,
249 list: Box<Term>,
250 body: Box<Term>,
251 },
252 Fold {
255 cap: u32,
256 list: Box<Term>,
257 init: Box<Term>,
258 body: Box<Term>,
259 },
260 Iota {
263 cap: u32,
264 count: Box<Term>,
265 },
266}
267
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275pub struct Def {
276 pub params: Vec<Ty>,
277 pub ret: Ty,
278 pub effects: BTreeSet<EffectKind>,
280 pub body: Term,
281 #[serde(default)]
283 pub pre: Option<Term>,
284 #[serde(default)]
286 pub post: Option<Term>,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct Module {
293 pub defs: BTreeMap<WeftHash, Def>,
294 pub entry: WeftHash,
295}
296
297impl Module {
298 pub fn build(defs: Vec<Def>, entry_index: usize) -> Result<Self, WeftError> {
301 let mut map = BTreeMap::new();
302 let mut entry = None;
303 for (i, d) in defs.into_iter().enumerate() {
304 let h = hash_def(&d);
305 if i == entry_index {
306 entry = Some(h);
307 }
308 map.insert(h, d);
309 }
310 let entry = entry.ok_or(WeftError::UnknownEntry)?;
311 Ok(Module { defs: map, entry })
312 }
313}
314
315fn enc_u32(out: &mut Vec<u8>, v: u32) {
320 out.extend_from_slice(&v.to_le_bytes());
321}
322fn enc_i64(out: &mut Vec<u8>, v: i64) {
323 out.extend_from_slice(&v.to_le_bytes());
324}
325fn enc_str(out: &mut Vec<u8>, s: &str) {
326 enc_u32(out, s.len() as u32);
327 out.extend_from_slice(s.as_bytes());
328}
329
330fn enc_ty(out: &mut Vec<u8>, ty: &Ty) {
331 match ty {
332 Ty::Int => out.push(0),
333 Ty::Bool => out.push(1),
334 Ty::Text => out.push(2),
335 Ty::Action => out.push(3),
336 Ty::List(t) => {
337 out.push(4);
338 enc_ty(out, t);
339 }
340 Ty::Fix => out.push(6),
341 Ty::Record(fields) => {
342 out.push(5);
343 enc_u32(out, fields.len() as u32);
344 for (k, t) in fields {
345 enc_str(out, k);
346 enc_ty(out, t);
347 }
348 }
349 }
350}
351
352fn enc_term(out: &mut Vec<u8>, t: &Term) {
353 match t {
354 Term::Int(v) => {
355 out.push(0);
356 enc_i64(out, *v);
357 }
358 Term::Bool(b) => {
359 out.push(1);
360 out.push(*b as u8);
361 }
362 Term::Text(s) => {
363 out.push(2);
364 enc_str(out, s);
365 }
366 Term::Var(i) => {
367 out.push(3);
368 enc_u32(out, *i);
369 }
370 Term::Let(v, b) => {
371 out.push(4);
372 enc_term(out, v);
373 enc_term(out, b);
374 }
375 Term::If(c, a, b) => {
376 out.push(5);
377 enc_term(out, c);
378 enc_term(out, a);
379 enc_term(out, b);
380 }
381 Term::Prim(op, args) => {
382 out.push(6);
383 out.push(*op as u8);
384 enc_u32(out, args.len() as u32);
385 for a in args {
386 enc_term(out, a);
387 }
388 }
389 Term::Rec(fields) => {
390 out.push(7);
391 enc_u32(out, fields.len() as u32);
392 for (k, v) in fields {
393 enc_str(out, k);
394 enc_term(out, v);
395 }
396 }
397 Term::Get(r, k) => {
398 out.push(8);
399 enc_term(out, r);
400 enc_str(out, k);
401 }
402 Term::ListNew(items) => {
403 out.push(9);
404 enc_u32(out, items.len() as u32);
405 for i in items {
406 enc_term(out, i);
407 }
408 }
409 Term::Call(h, args) => {
410 out.push(10);
411 out.extend_from_slice(&h.0);
412 enc_u32(out, args.len() as u32);
413 for a in args {
414 enc_term(out, a);
415 }
416 }
417 Term::Fix(v) => {
418 out.push(12);
419 enc_i64(out, *v);
420 }
421 Term::Map { cap, list, body } => {
422 out.push(13);
423 enc_u32(out, *cap);
424 enc_term(out, list);
425 enc_term(out, body);
426 }
427 Term::Fold {
428 cap,
429 list,
430 init,
431 body,
432 } => {
433 out.push(14);
434 enc_u32(out, *cap);
435 enc_term(out, list);
436 enc_term(out, init);
437 enc_term(out, body);
438 }
439 Term::Iota { cap, count } => {
440 out.push(15);
441 enc_u32(out, *cap);
442 enc_term(out, count);
443 }
444 Term::Effect(kind, fields) => {
445 out.push(11);
446 out.push(kind.tag());
447 enc_u32(out, fields.len() as u32);
448 for (k, v) in fields {
449 enc_str(out, k);
450 enc_term(out, v);
451 }
452 }
453 }
454}
455
456pub fn canonical_bytes(def: &Def) -> Vec<u8> {
458 let mut out = Vec::new();
459 out.extend_from_slice(b"weft/0.1\0");
460 enc_u32(&mut out, def.params.len() as u32);
461 for p in &def.params {
462 enc_ty(&mut out, p);
463 }
464 enc_ty(&mut out, &def.ret);
465 enc_u32(&mut out, def.effects.len() as u32);
466 for e in &def.effects {
467 out.push(e.tag());
468 }
469 enc_term(&mut out, &def.body);
470 match &def.pre {
471 None => out.push(0),
472 Some(t) => {
473 out.push(1);
474 enc_term(&mut out, t);
475 }
476 }
477 match &def.post {
478 None => out.push(0),
479 Some(t) => {
480 out.push(1);
481 enc_term(&mut out, t);
482 }
483 }
484 out
485}
486
487pub fn hash_def(def: &Def) -> WeftHash {
489 let digest = Sha256::digest(canonical_bytes(def));
490 let mut h = [0u8; 32];
491 h.copy_from_slice(&digest);
492 WeftHash(h)
493}
494
495#[derive(Debug, Clone, PartialEq, Eq)]
500pub enum WeftError {
501 Type(String),
503 EffectNotDeclared(EffectKind),
505 UnknownCall(String),
507 UnknownEntry,
508 FuelExhausted,
510 ContractViolated(&'static str),
512}
513
514impl fmt::Display for WeftError {
515 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516 match self {
517 WeftError::Type(m) => write!(f, "type error: {m}"),
518 WeftError::EffectNotDeclared(k) => write!(f, "effect not declared: {k:?}"),
519 WeftError::UnknownCall(h) => write!(f, "unknown call target {h}"),
520 WeftError::UnknownEntry => write!(f, "entry hash not in module"),
521 WeftError::FuelExhausted => write!(f, "fuel exhausted"),
522 WeftError::ContractViolated(w) => write!(f, "contract violated: {w}"),
523 }
524 }
525}
526impl std::error::Error for WeftError {}
527
528#[derive(Debug, Clone, PartialEq, Eq)]
535pub struct Verified {
536 pub effects: BTreeSet<EffectKind>,
537 pub fuel_bound: u64,
538}
539
540pub fn verify_module(m: &Module) -> Result<BTreeMap<WeftHash, Verified>, WeftError> {
544 let mut done: BTreeMap<WeftHash, Verified> = BTreeMap::new();
545 let mut remaining: Vec<WeftHash> = m.defs.keys().copied().collect();
548 while !remaining.is_empty() {
549 let before = remaining.len();
550 let mut next = Vec::new();
551 for h in remaining {
552 let def = &m.defs[&h];
553 if callees(&def.body).iter().all(|c| done.contains_key(c)) {
554 let v = verify_def(def, &done, m)?;
555 done.insert(h, v);
556 } else {
557 next.push(h);
558 }
559 }
560 if next.len() == before {
561 let missing = next
563 .iter()
564 .flat_map(|h| callees(&m.defs[h].body))
565 .find(|c| !m.defs.contains_key(c))
566 .map(|c| c.to_string())
567 .unwrap_or_else(|| "<cycle?>".into());
568 return Err(WeftError::UnknownCall(missing));
569 }
570 remaining = next;
571 }
572 if !done.contains_key(&m.entry) {
573 return Err(WeftError::UnknownEntry);
574 }
575 Ok(done)
576}
577
578pub fn callees_of(t: &Term) -> BTreeSet<WeftHash> {
580 callees(t)
581}
582
583fn callees(t: &Term) -> BTreeSet<WeftHash> {
584 let mut out = BTreeSet::new();
585 fn walk(t: &Term, out: &mut BTreeSet<WeftHash>) {
586 match t {
587 Term::Call(h, args) => {
588 out.insert(*h);
589 args.iter().for_each(|a| walk(a, out));
590 }
591 Term::Let(a, b) => {
592 walk(a, out);
593 walk(b, out);
594 }
595 Term::If(a, b, c) => {
596 walk(a, out);
597 walk(b, out);
598 walk(c, out);
599 }
600 Term::Prim(_, xs) | Term::ListNew(xs) => xs.iter().for_each(|x| walk(x, out)),
601 Term::Map { list, body, .. } => {
602 walk(list, out);
603 walk(body, out);
604 }
605 Term::Fold {
606 list, init, body, ..
607 } => {
608 walk(list, out);
609 walk(init, out);
610 walk(body, out);
611 }
612 Term::Iota { count, .. } => walk(count, out),
613 Term::Rec(fs) | Term::Effect(_, fs) => fs.values().for_each(|x| walk(x, out)),
614 Term::Get(r, _) => walk(r, out),
615 _ => {}
616 }
617 }
618 walk(t, &mut out);
619 out
620}
621
622fn verify_def(
623 def: &Def,
624 done: &BTreeMap<WeftHash, Verified>,
625 m: &Module,
626) -> Result<Verified, WeftError> {
627 let mut ctx: Vec<Ty> = def.params.clone();
628 let mut used = BTreeSet::new();
629 let mut fuel: u64 = 0;
630 let ty = check(&def.body, &mut ctx, &mut used, &mut fuel, done, m)?;
631 if ty != def.ret {
632 return Err(WeftError::Type(format!(
633 "body is {ty:?}, declared {:?}",
634 def.ret
635 )));
636 }
637 for k in &used {
638 if !def.effects.contains(k) {
639 return Err(WeftError::EffectNotDeclared(*k));
640 }
641 }
642 if let Some(pre) = &def.pre {
644 let mut c = def.params.clone();
645 let (mut u, mut f) = (BTreeSet::new(), 0u64);
646 let t = check(pre, &mut c, &mut u, &mut f, done, m)?;
647 if t != Ty::Bool || !u.is_empty() {
648 return Err(WeftError::Type("pre must be a pure Bool".into()));
649 }
650 fuel += f;
651 }
652 if let Some(post) = &def.post {
653 let mut c = def.params.clone();
654 c.push(def.ret.clone());
655 let (mut u, mut f) = (BTreeSet::new(), 0u64);
656 let t = check(post, &mut c, &mut u, &mut f, done, m)?;
657 if t != Ty::Bool || !u.is_empty() {
658 return Err(WeftError::Type("post must be a pure Bool".into()));
659 }
660 fuel += f;
661 }
662 Ok(Verified {
663 effects: used,
664 fuel_bound: fuel,
665 })
666}
667
668fn check(
670 t: &Term,
671 ctx: &mut Vec<Ty>,
672 used: &mut BTreeSet<EffectKind>,
673 fuel: &mut u64,
674 done: &BTreeMap<WeftHash, Verified>,
675 m: &Module,
676) -> Result<Ty, WeftError> {
677 *fuel += 1;
678 match t {
679 Term::Int(_) => Ok(Ty::Int),
680 Term::Fix(_) => Ok(Ty::Fix),
681 Term::Bool(_) => Ok(Ty::Bool),
682 Term::Text(_) => Ok(Ty::Text),
683 Term::Var(i) => {
684 let idx = ctx.len().checked_sub(1 + *i as usize);
685 idx.and_then(|k| ctx.get(k).cloned())
686 .ok_or_else(|| WeftError::Type(format!("unbound var {i}")))
687 }
688 Term::Let(v, b) => {
689 let vt = check(v, ctx, used, fuel, done, m)?;
690 ctx.push(vt);
691 let bt = check(b, ctx, used, fuel, done, m);
692 ctx.pop();
693 bt
694 }
695 Term::If(c, a, b) => {
696 if check(c, ctx, used, fuel, done, m)? != Ty::Bool {
697 return Err(WeftError::Type("if condition must be Bool".into()));
698 }
699 let at = check(a, ctx, used, fuel, done, m)?;
700 let bt = check(b, ctx, used, fuel, done, m)?;
701 if at != bt {
702 return Err(WeftError::Type("if branches differ".into()));
703 }
704 Ok(at)
705 }
706 Term::Prim(op, args) => {
707 let ats: Result<Vec<Ty>, _> = args
708 .iter()
709 .map(|a| check(a, ctx, used, fuel, done, m))
710 .collect();
711 let ats = ats?;
712 use PrimOp::*;
713 let (want, out): (Vec<Ty>, Ty) = match op {
714 Add | Sub | Mul | Div => (vec![Ty::Int, Ty::Int], Ty::Int),
715 Lt | Le | EqInt => (vec![Ty::Int, Ty::Int], Ty::Bool),
716 EqText => (vec![Ty::Text, Ty::Text], Ty::Bool),
717 And | Or => (vec![Ty::Bool, Ty::Bool], Ty::Bool),
718 Not => (vec![Ty::Bool], Ty::Bool),
719 Concat => (vec![Ty::Text, Ty::Text], Ty::Text),
720 ToText => (vec![Ty::Int], Ty::Text),
721 FAdd | FSub | FMul | FDiv => (vec![Ty::Fix, Ty::Fix], Ty::Fix),
722 FLt | FLe | EqFix => (vec![Ty::Fix, Ty::Fix], Ty::Bool),
723 FixOfInt => (vec![Ty::Int], Ty::Fix),
724 IntOfFix => (vec![Ty::Fix], Ty::Int),
725 FixToText => (vec![Ty::Fix], Ty::Text),
726 FSin | FCos => (vec![Ty::Fix], Ty::Fix),
727 Len => {
729 return match ats.as_slice() {
730 [Ty::List(_)] => Ok(Ty::Int),
731 other => Err(WeftError::Type(format!(
732 "len expects a list, got {other:?}"
733 ))),
734 };
735 }
736 ListCat => {
738 return match ats.as_slice() {
739 [Ty::List(a), Ty::List(b)] if a == b => Ok(Ty::List(a.clone())),
740 other => Err(WeftError::Type(format!(
741 "list-cat expects two lists of the same element type, got {other:?}"
742 ))),
743 };
744 }
745 };
746 if ats != want {
747 return Err(WeftError::Type(format!(
748 "{op:?} expects {want:?}, got {ats:?}"
749 )));
750 }
751 Ok(out)
752 }
753 Term::Rec(fields) => {
754 let mut tys = BTreeMap::new();
755 for (k, v) in fields {
756 tys.insert(k.clone(), check(v, ctx, used, fuel, done, m)?);
757 }
758 Ok(Ty::Record(tys))
759 }
760 Term::Get(r, k) => match check(r, ctx, used, fuel, done, m)? {
761 Ty::Record(fs) => fs
762 .get(k)
763 .cloned()
764 .ok_or_else(|| WeftError::Type(format!("no field '{k}'"))),
765 other => Err(WeftError::Type(format!("get on non-record {other:?}"))),
766 },
767 Term::ListNew(items) => {
768 let mut elem: Option<Ty> = None;
769 for i in items {
770 let t = check(i, ctx, used, fuel, done, m)?;
771 match &elem {
772 None => elem = Some(t),
773 Some(e) if *e == t => {}
774 Some(e) => return Err(WeftError::Type(format!("list mixes {e:?} and {t:?}"))),
775 }
776 }
777 Ok(Ty::List(Box::new(elem.unwrap_or(Ty::Action))))
778 }
779 Term::Call(h, args) => {
780 let callee = m
781 .defs
782 .get(h)
783 .ok_or_else(|| WeftError::UnknownCall(h.to_string()))?;
784 let cert = done
785 .get(h)
786 .ok_or_else(|| WeftError::UnknownCall(h.to_string()))?;
787 if args.len() != callee.params.len() {
788 return Err(WeftError::Type("call arity".into()));
789 }
790 for (a, want) in args.iter().zip(&callee.params) {
791 let at = check(a, ctx, used, fuel, done, m)?;
792 if at != *want {
793 return Err(WeftError::Type(format!("call arg {at:?} vs {want:?}")));
794 }
795 }
796 used.extend(cert.effects.iter().copied());
798 *fuel += cert.fuel_bound;
799 Ok(callee.ret.clone())
800 }
801 Term::Effect(kind, fields) => {
802 for v in fields.values() {
803 check(v, ctx, used, fuel, done, m)?;
804 }
805 used.insert(*kind);
806 Ok(Ty::Action)
807 }
808 Term::Map { cap, list, body } => {
809 let lt = check(list, ctx, used, fuel, done, m)?;
810 let Ty::List(elem) = lt else {
811 return Err(WeftError::Type(format!("map over non-list {lt:?}")));
812 };
813 let mut body_fuel: u64 = 0;
816 ctx.push((*elem).clone());
817 let bt = check(body, ctx, used, &mut body_fuel, done, m);
818 ctx.pop();
819 let bt = bt?;
820 *fuel = fuel.saturating_add(body_fuel.saturating_mul(*cap as u64) + *cap as u64);
821 Ok(Ty::List(Box::new(bt)))
822 }
823 Term::Fold {
824 cap,
825 list,
826 init,
827 body,
828 } => {
829 let lt = check(list, ctx, used, fuel, done, m)?;
830 let Ty::List(elem) = lt else {
831 return Err(WeftError::Type(format!("fold over non-list {lt:?}")));
832 };
833 let it = check(init, ctx, used, fuel, done, m)?;
834 let mut body_fuel: u64 = 0;
835 ctx.push(it.clone()); ctx.push((*elem).clone()); let bt = check(body, ctx, used, &mut body_fuel, done, m);
838 ctx.pop();
839 ctx.pop();
840 let bt = bt?;
841 if bt != it {
842 return Err(WeftError::Type(format!(
843 "fold body is {bt:?}, acc is {it:?}"
844 )));
845 }
846 *fuel = fuel.saturating_add(body_fuel.saturating_mul(*cap as u64) + *cap as u64);
847 Ok(it)
848 }
849 Term::Iota { cap, count } => {
850 let ct = check(count, ctx, used, fuel, done, m)?;
851 if ct != Ty::Int {
852 return Err(WeftError::Type(format!(
853 "iota count must be Int, got {ct:?}"
854 )));
855 }
856 *fuel = fuel.saturating_add(*cap as u64);
857 Ok(Ty::List(Box::new(Ty::Int)))
858 }
859 }
860}
861
862#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
868pub enum Value {
869 Int(i64),
870 Fix(i64),
872 Bool(bool),
873 Text(String),
874 List(Vec<Value>),
875 Rec(BTreeMap<String, Value>),
876 Action {
878 kind: EffectKind,
879 fields: BTreeMap<String, Value>,
880 },
881}
882
883#[derive(Debug, Clone, PartialEq)]
887pub struct Evaluated {
888 pub value: Value,
889 pub fuel_spent: u64,
890}
891
892pub fn eval_call(
893 m: &Module,
894 target: WeftHash,
895 args: Vec<Value>,
896 max_fuel: u64,
897) -> Result<Evaluated, WeftError> {
898 let mut fuel = max_fuel;
899 let def = m
900 .defs
901 .get(&target)
902 .ok_or_else(|| WeftError::UnknownCall(target.to_string()))?;
903 if let Some(pre) = &def.pre {
905 let mut env = args.clone();
906 if eval(pre, &mut env, m, &mut fuel)? != Value::Bool(true) {
907 return Err(WeftError::ContractViolated("pre"));
908 }
909 }
910 let mut env = args.clone();
911 let value = eval(&def.body, &mut env, m, &mut fuel)?;
912 if let Some(post) = &def.post {
914 let mut env = args;
915 env.push(value.clone());
916 if eval(post, &mut env, m, &mut fuel)? != Value::Bool(true) {
917 return Err(WeftError::ContractViolated("post"));
918 }
919 }
920 Ok(Evaluated {
921 value,
922 fuel_spent: max_fuel - fuel,
923 })
924}
925
926fn eval(t: &Term, env: &mut Vec<Value>, m: &Module, fuel: &mut u64) -> Result<Value, WeftError> {
927 if *fuel == 0 {
928 return Err(WeftError::FuelExhausted);
929 }
930 *fuel -= 1;
931 Ok(match t {
932 Term::Int(v) => Value::Int(*v),
933 Term::Fix(v) => Value::Fix(*v),
934 Term::Bool(b) => Value::Bool(*b),
935 Term::Text(s) => Value::Text(s.clone()),
936 Term::Var(i) => env[env.len() - 1 - *i as usize].clone(),
937 Term::Let(v, b) => {
938 let val = eval(v, env, m, fuel)?;
939 env.push(val);
940 let out = eval(b, env, m, fuel);
941 env.pop();
942 out?
943 }
944 Term::If(c, a, b) => {
945 if eval(c, env, m, fuel)? == Value::Bool(true) {
946 eval(a, env, m, fuel)?
947 } else {
948 eval(b, env, m, fuel)?
949 }
950 }
951 Term::Prim(op, args) => {
952 let vals: Result<Vec<Value>, _> = args.iter().map(|a| eval(a, env, m, fuel)).collect();
953 let vals = vals?;
954 if *op == PrimOp::ListCat {
960 let n = match (vals.first(), vals.get(1)) {
961 (Some(Value::List(a)), Some(Value::List(b))) => a.len() + b.len(),
962 _ => 0,
963 } as u64;
964 if n > *fuel {
965 return Err(WeftError::FuelExhausted);
966 }
967 *fuel -= n;
968 if n as usize > MAX_LIST {
969 return Err(WeftError::FuelExhausted);
970 }
971 }
972 prim(*op, vals)
973 }
974 Term::Rec(fields) => {
975 let mut out = BTreeMap::new();
976 for (k, v) in fields {
977 out.insert(k.clone(), eval(v, env, m, fuel)?);
978 }
979 Value::Rec(out)
980 }
981 Term::Get(r, k) => match eval(r, env, m, fuel)? {
982 Value::Rec(fs) => fs[k].clone(),
983 _ => unreachable!("verified"),
984 },
985 Term::ListNew(items) => {
986 let vals: Result<Vec<Value>, _> = items.iter().map(|i| eval(i, env, m, fuel)).collect();
987 Value::List(vals?)
988 }
989 Term::Call(h, args) => {
990 let vals: Result<Vec<Value>, _> = args.iter().map(|a| eval(a, env, m, fuel)).collect();
991 let def = m
992 .defs
993 .get(h)
994 .ok_or_else(|| WeftError::UnknownCall(h.to_string()))?;
995 let mut callee_env = vals?;
996 eval(&def.body, &mut callee_env, m, fuel)?
997 }
998 Term::Effect(kind, fields) => {
999 let mut out = BTreeMap::new();
1000 for (k, v) in fields {
1001 out.insert(k.clone(), eval(v, env, m, fuel)?);
1002 }
1003 Value::Action {
1004 kind: *kind,
1005 fields: out,
1006 }
1007 }
1008 Term::Map { cap, list, body } => {
1009 let Value::List(items) = eval(list, env, m, fuel)? else {
1010 unreachable!("verified")
1011 };
1012 let mut out = Vec::new();
1013 for item in items.into_iter().take(*cap as usize) {
1014 env.push(item);
1015 let v = eval(body, env, m, fuel);
1016 env.pop();
1017 out.push(v?);
1018 }
1019 Value::List(out)
1020 }
1021 Term::Fold {
1022 cap,
1023 list,
1024 init,
1025 body,
1026 } => {
1027 let Value::List(items) = eval(list, env, m, fuel)? else {
1028 unreachable!("verified")
1029 };
1030 let mut acc = eval(init, env, m, fuel)?;
1031 for item in items.into_iter().take(*cap as usize) {
1032 env.push(acc);
1033 env.push(item);
1034 let v = eval(body, env, m, fuel);
1035 env.pop();
1036 env.pop();
1037 acc = v?;
1038 }
1039 acc
1040 }
1041 Term::Iota { cap, count } => {
1042 let Value::Int(n) = eval(count, env, m, fuel)? else {
1043 unreachable!("verified")
1044 };
1045 let n = n.max(0).min(*cap as i64);
1046 Value::List((0..n).map(Value::Int).collect())
1047 }
1048 })
1049}
1050
1051fn prim(op: PrimOp, vals: Vec<Value>) -> Value {
1052 use PrimOp::*;
1053 use Value::*;
1054 match (op, vals.as_slice()) {
1055 (Add, [Int(a), Int(b)]) => Int(a.wrapping_add(*b)),
1056 (Sub, [Int(a), Int(b)]) => Int(a.wrapping_sub(*b)),
1057 (Mul, [Int(a), Int(b)]) => Int(a.wrapping_mul(*b)),
1058 (Div, [Int(a), Int(b)]) => Int(if *b == 0 { 0 } else { a.wrapping_div(*b) }),
1060 (Lt, [Int(a), Int(b)]) => Bool(a < b),
1061 (Le, [Int(a), Int(b)]) => Bool(a <= b),
1062 (EqInt, [Int(a), Int(b)]) => Bool(a == b),
1063 (EqText, [Text(a), Text(b)]) => Bool(a == b),
1064 (And, [Bool(a), Bool(b)]) => Bool(*a && *b),
1065 (Or, [Bool(a), Bool(b)]) => Bool(*a || *b),
1066 (Not, [Bool(a)]) => Bool(!a),
1067 (Concat, [Text(a), Text(b)]) => Text(format!("{a}{b}")),
1068 (ToText, [Int(a)]) => Text(a.to_string()),
1069 (FAdd, [Fix(a), Fix(b)]) => Fix(a.wrapping_add(*b)),
1070 (FSub, [Fix(a), Fix(b)]) => Fix(a.wrapping_sub(*b)),
1071 (FMul, [Fix(a), Fix(b)]) => Fix(((*a as i128 * *b as i128) / FIX_SCALE as i128) as i64),
1072 (FDiv, [Fix(a), Fix(b)]) => Fix(if *b == 0 {
1073 0
1074 } else {
1075 ((*a as i128 * FIX_SCALE as i128) / *b as i128) as i64
1076 }),
1077 (FLt, [Fix(a), Fix(b)]) => Bool(a < b),
1078 (FLe, [Fix(a), Fix(b)]) => Bool(a <= b),
1079 (EqFix, [Fix(a), Fix(b)]) => Bool(a == b),
1080 (FixOfInt, [Int(a)]) => Fix(a.wrapping_mul(FIX_SCALE)),
1081 (IntOfFix, [Fix(a)]) => Int(a / FIX_SCALE),
1082 (FixToText, [Fix(a)]) => Text(fix_to_text(*a)),
1083 (Len, [List(xs)]) => Int(xs.len() as i64),
1084 (FSin, [Fix(a)]) => Fix(fix_sin(*a)),
1085 (FCos, [Fix(a)]) => Fix(fix_sin(a.wrapping_add(FIX_PI / 2))),
1086 (ListCat, [List(a), List(b)]) => {
1087 let mut out = a.clone();
1088 out.extend(b.iter().cloned());
1089 List(out)
1090 }
1091 _ => unreachable!("verified"),
1092 }
1093}
1094
1095pub const FIX_PI: i64 = 3_141_593;
1097pub const FIX_TAU: i64 = 6_283_185;
1099
1100fn fix_sin(x: i64) -> i64 {
1104 let mut r = x % FIX_TAU;
1105 if r < 0 {
1106 r += FIX_TAU;
1107 }
1108 let (r, sign) = if r > FIX_PI {
1109 (r - FIX_PI, -1i128)
1110 } else {
1111 (r, 1i128)
1112 };
1113 let x = r as i128;
1114 let pi = FIX_PI as i128;
1115 let scale = FIX_SCALE as i128;
1116 let num = 16 * x * (pi - x); let den = 5 * pi * pi - 4 * x * (pi - x);
1118 if den == 0 {
1119 return 0;
1120 }
1121 ((sign * num * scale) / den) as i64
1122}
1123
1124fn fix_to_text(raw: i64) -> String {
1128 let neg = raw < 0;
1129 let mag = (raw as i128).unsigned_abs();
1130 let whole = mag / FIX_SCALE as u128;
1131 let frac = (mag % FIX_SCALE as u128) as u64;
1132 let mut s = String::new();
1133 if neg {
1134 s.push('-');
1135 }
1136 s.push_str(&whole.to_string());
1137 if frac != 0 {
1138 let f = format!("{frac:06}");
1139 s.push('.');
1140 s.push_str(f.trim_end_matches('0'));
1141 }
1142 s
1143}
1144
1145#[cfg(test)]
1150mod tests {
1151 use super::*;
1152
1153 #[test]
1156 fn bounded_map_and_fold_stay_total() {
1157 let body = Term::Rec(
1158 [
1159 (
1160 "doubled".to_string(),
1161 Term::Map {
1162 cap: 8,
1163 list: Box::new(Term::ListNew(vec![
1164 Term::Int(1),
1165 Term::Int(2),
1166 Term::Int(3),
1167 ])),
1168 body: Box::new(Term::Prim(PrimOp::Mul, vec![Term::Var(0), Term::Int(2)])),
1169 },
1170 ),
1171 (
1172 "sum".to_string(),
1173 Term::Fold {
1174 cap: 8,
1175 list: Box::new(Term::ListNew(vec![
1176 Term::Int(10),
1177 Term::Int(20),
1178 Term::Int(30),
1179 ])),
1180 init: Box::new(Term::Int(0)),
1181 body: Box::new(Term::Prim(PrimOp::Add, vec![Term::Var(1), Term::Var(0)])),
1182 },
1183 ),
1184 (
1185 "capped".to_string(),
1186 Term::Fold {
1187 cap: 2, list: Box::new(Term::ListNew(vec![
1189 Term::Int(1),
1190 Term::Int(1),
1191 Term::Int(1),
1192 ])),
1193 init: Box::new(Term::Int(0)),
1194 body: Box::new(Term::Prim(PrimOp::Add, vec![Term::Var(1), Term::Var(0)])),
1195 },
1196 ),
1197 (
1198 "count".to_string(),
1199 Term::Prim(
1200 PrimOp::Len,
1201 vec![Term::ListNew(vec![Term::Int(7), Term::Int(7)])],
1202 ),
1203 ),
1204 ]
1205 .into(),
1206 );
1207 let ret = Ty::Record(
1208 [
1209 ("doubled".to_string(), Ty::List(Box::new(Ty::Int))),
1210 ("sum".to_string(), Ty::Int),
1211 ("capped".to_string(), Ty::Int),
1212 ("count".to_string(), Ty::Int),
1213 ]
1214 .into(),
1215 );
1216 let def = Def {
1217 params: vec![],
1218 ret,
1219 effects: BTreeSet::new(),
1220 body,
1221 pre: None,
1222 post: None,
1223 };
1224 let m = Module::build(vec![def], 0).unwrap();
1225 let certs = verify_module(&m).expect("bounded iteration verifies");
1226 let bound = certs[&m.entry].fuel_bound;
1227 let out = eval_call(&m, m.entry, vec![], bound + 8).unwrap();
1228 assert!(
1229 out.fuel_spent <= bound,
1230 "static bound covers the run ({} <= {bound})",
1231 out.fuel_spent
1232 );
1233 let Value::Rec(r) = out.value else { panic!() };
1234 assert_eq!(
1235 r["doubled"],
1236 Value::List(vec![Value::Int(2), Value::Int(4), Value::Int(6)])
1237 );
1238 assert_eq!(r["sum"], Value::Int(60));
1239 assert_eq!(r["capped"], Value::Int(2), "cap truncates");
1240 assert_eq!(r["count"], Value::Int(2));
1241 }
1242
1243 #[test]
1246 fn fix_arithmetic_is_exact_total_and_typed() {
1247 let fx = |m: i64| Term::Fix(m);
1248 let body = Term::Rec(
1249 [
1250 (
1251 "sum".to_string(),
1252 Term::Prim(PrimOp::FAdd, vec![fx(100_000), fx(200_000)]),
1253 ),
1254 (
1255 "prod".to_string(),
1256 Term::Prim(PrimOp::FMul, vec![fx(1_500_000), fx(2_000_000)]),
1257 ),
1258 (
1259 "quot".to_string(),
1260 Term::Prim(PrimOp::FDiv, vec![fx(1_000_000), fx(0)]),
1261 ),
1262 (
1263 "text".to_string(),
1264 Term::Prim(
1265 PrimOp::FixToText,
1266 vec![Term::Prim(PrimOp::FSub, vec![fx(0), fx(250_000)])],
1267 ),
1268 ),
1269 (
1270 "trunc".to_string(),
1271 Term::Prim(PrimOp::IntOfFix, vec![fx(2_900_000)]),
1272 ),
1273 ]
1274 .into(),
1275 );
1276 let ret = Ty::Record(
1277 [
1278 ("sum".to_string(), Ty::Fix),
1279 ("prod".to_string(), Ty::Fix),
1280 ("quot".to_string(), Ty::Fix),
1281 ("text".to_string(), Ty::Text),
1282 ("trunc".to_string(), Ty::Int),
1283 ]
1284 .into(),
1285 );
1286 let def = Def {
1287 params: vec![],
1288 ret,
1289 effects: BTreeSet::new(),
1290 body,
1291 pre: None,
1292 post: None,
1293 };
1294 let m = Module::build(vec![def], 0).unwrap();
1295 verify_module(&m).expect("fix ops verify");
1296 let out = eval_call(&m, m.entry, vec![], 10_000).unwrap();
1297 let Value::Rec(r) = out.value else { panic!() };
1298 assert_eq!(r["sum"], Value::Fix(300_000)); assert_eq!(r["prod"], Value::Fix(3_000_000)); assert_eq!(r["quot"], Value::Fix(0)); assert_eq!(r["text"], Value::Text("-0.25".into()));
1302 assert_eq!(r["trunc"], Value::Int(2));
1303 }
1304
1305 #[test]
1308 fn fix_and_int_do_not_mix_silently() {
1309 let body = Term::Prim(PrimOp::FAdd, vec![Term::Fix(1), Term::Int(1)]);
1310 let def = Def {
1311 params: vec![],
1312 ret: Ty::Fix,
1313 effects: BTreeSet::new(),
1314 body,
1315 pre: None,
1316 post: None,
1317 };
1318 let m = Module::build(vec![def], 0).unwrap();
1319 assert!(matches!(verify_module(&m), Err(WeftError::Type(_))));
1320 }
1321
1322 fn rec(fields: Vec<(&str, Term)>) -> Term {
1323 Term::Rec(
1324 fields
1325 .into_iter()
1326 .map(|(k, v)| (k.to_string(), v))
1327 .collect(),
1328 )
1329 }
1330 fn rec_ty(fields: Vec<(&str, Ty)>) -> Ty {
1331 Ty::Record(
1332 fields
1333 .into_iter()
1334 .map(|(k, v)| (k.to_string(), v))
1335 .collect(),
1336 )
1337 }
1338
1339 fn chop_module() -> Module {
1343 let state_ty = rec_ty(vec![("hits", Ty::Int)]);
1344 let out_ty = rec_ty(vec![
1345 ("actions", Ty::List(Box::new(Ty::Action))),
1346 ("state", state_ty.clone()),
1347 ]);
1348 let hits_plus_one = Term::Prim(
1350 PrimOp::Add,
1351 vec![
1352 Term::Get(Box::new(Term::Var(1)), "hits".into()),
1353 Term::Int(1),
1354 ],
1355 );
1356 let body = Term::Let(
1357 Box::new(hits_plus_one), Box::new(Term::If(
1359 Box::new(Term::Prim(PrimOp::Le, vec![Term::Int(3), Term::Var(0)])),
1360 Box::new(rec(vec![
1362 (
1363 "actions",
1364 Term::ListNew(vec![
1365 Term::Effect(
1366 EffectKind::GiveItem,
1367 [
1368 ("item".to_string(), Term::Int(20100001)),
1369 ("count".to_string(), Term::Int(3)),
1370 ]
1371 .into(),
1372 ),
1373 Term::Effect(
1374 EffectKind::Notify,
1375 [("text".to_string(), Term::Text("The tree falls.".into()))].into(),
1376 ),
1377 Term::Effect(EffectKind::Despawn, BTreeMap::new()),
1378 ]),
1379 ),
1380 ("state", rec(vec![("hits", Term::Int(0))])),
1381 ])),
1382 Box::new(rec(vec![
1384 ("actions", Term::ListNew(vec![])),
1385 ("state", rec(vec![("hits", Term::Var(0))])),
1386 ])),
1387 )),
1388 );
1389 let def = Def {
1390 params: vec![state_ty, rec_ty(vec![])],
1391 ret: out_ty,
1392 effects: [
1393 EffectKind::GiveItem,
1394 EffectKind::Notify,
1395 EffectKind::Despawn,
1396 ]
1397 .into(),
1398 body,
1399 pre: None,
1400 post: None,
1401 };
1402 Module::build(vec![def], 0).unwrap()
1403 }
1404
1405 fn state(hits: i64) -> Value {
1406 Value::Rec([("hits".to_string(), Value::Int(hits))].into())
1407 }
1408 fn event() -> Value {
1409 Value::Rec(BTreeMap::new())
1410 }
1411
1412 #[test]
1413 fn identity_is_content_and_only_content() {
1414 let a = chop_module();
1415 let b = chop_module();
1416 assert_eq!(a.entry, b.entry, "same structure, same hash — always");
1417 let mut defs: Vec<Def> = a.defs.values().cloned().collect();
1419 if let Term::Let(_, body) = &mut defs[0].body {
1420 if let Term::If(cond, _, _) = body.as_mut() {
1421 **cond = Term::Prim(PrimOp::Le, vec![Term::Int(4), Term::Var(0)]);
1422 }
1423 }
1424 let c = Module::build(defs, 0).unwrap();
1425 assert_ne!(a.entry, c.entry);
1426 assert!(a.entry.to_string().starts_with("weft:"));
1428 }
1429
1430 #[test]
1431 fn the_chop_behavior_verifies_and_runs() {
1432 let m = chop_module();
1433 let certs = verify_module(&m).expect("verifies");
1434 let cert = &certs[&m.entry];
1435 assert_eq!(
1436 cert.effects,
1437 [
1438 EffectKind::GiveItem,
1439 EffectKind::Notify,
1440 EffectKind::Despawn
1441 ]
1442 .into()
1443 );
1444 let r = eval_call(&m, m.entry, vec![state(1), event()], cert.fuel_bound).unwrap();
1446 let Value::Rec(out) = &r.value else { panic!() };
1447 assert_eq!(out["actions"], Value::List(vec![]));
1448 assert_eq!(out["state"], state(2));
1449 let r = eval_call(&m, m.entry, vec![state(2), event()], cert.fuel_bound).unwrap();
1451 let Value::Rec(out) = &r.value else { panic!() };
1452 let Value::List(actions) = &out["actions"] else {
1453 panic!()
1454 };
1455 assert_eq!(actions.len(), 3);
1456 assert!(matches!(
1457 &actions[0],
1458 Value::Action {
1459 kind: EffectKind::GiveItem,
1460 ..
1461 }
1462 ));
1463 assert_eq!(out["state"], state(0));
1464 }
1465
1466 #[test]
1467 fn the_effect_row_is_the_permission() {
1468 let m = chop_module();
1469 let mut defs: Vec<Def> = m.defs.values().cloned().collect();
1470 defs[0].effects.remove(&EffectKind::Despawn);
1472 let m2 = Module::build(defs, 0).unwrap();
1473 assert_eq!(
1474 verify_module(&m2).unwrap_err(),
1475 WeftError::EffectNotDeclared(EffectKind::Despawn),
1476 );
1477 let sneaky = Def {
1480 params: vec![],
1481 ret: Ty::Action,
1482 effects: BTreeSet::new(),
1483 body: Term::Effect(EffectKind::CommerceBuy, BTreeMap::new()),
1484 pre: None,
1485 post: None,
1486 };
1487 let m3 = Module::build(vec![sneaky], 0).unwrap();
1488 assert_eq!(
1489 verify_module(&m3).unwrap_err(),
1490 WeftError::EffectNotDeclared(EffectKind::CommerceBuy),
1491 );
1492 }
1493
1494 #[test]
1495 fn type_errors_are_rejected() {
1496 let bad = Def {
1498 params: vec![],
1499 ret: Ty::Int,
1500 effects: BTreeSet::new(),
1501 body: Term::If(
1502 Box::new(Term::Int(1)),
1503 Box::new(Term::Int(2)),
1504 Box::new(Term::Int(3)),
1505 ),
1506 pre: None,
1507 post: None,
1508 };
1509 let m = Module::build(vec![bad], 0).unwrap();
1510 assert!(matches!(verify_module(&m), Err(WeftError::Type(_))));
1511 }
1512
1513 #[test]
1514 fn static_fuel_bound_dominates_actual_cost() {
1515 let m = chop_module();
1516 let cert = &verify_module(&m).unwrap()[&m.entry];
1517 for hits in 0..5 {
1518 let r = eval_call(&m, m.entry, vec![state(hits), event()], cert.fuel_bound).unwrap();
1519 assert!(
1520 r.fuel_spent <= cert.fuel_bound,
1521 "spent {} > bound {}",
1522 r.fuel_spent,
1523 cert.fuel_bound
1524 );
1525 }
1526 let a = eval_call(&m, m.entry, vec![state(2), event()], cert.fuel_bound).unwrap();
1528 let b = eval_call(&m, m.entry, vec![state(2), event()], cert.fuel_bound).unwrap();
1529 assert_eq!(a.value, b.value);
1530 assert_eq!(a.fuel_spent, b.fuel_spent);
1531 }
1532
1533 #[test]
1534 fn calls_compose_by_hash_and_effects_flow_transitively() {
1535 let double = Def {
1537 params: vec![Ty::Int],
1538 ret: Ty::Int,
1539 effects: BTreeSet::new(),
1540 body: Term::Prim(PrimOp::Add, vec![Term::Var(0), Term::Var(0)]),
1541 pre: None,
1542 post: None,
1543 };
1544 let dh = hash_def(&double);
1545 let shout = Def {
1547 params: vec![Ty::Int],
1548 ret: Ty::Action,
1549 effects: [EffectKind::Notify].into(),
1550 body: Term::Effect(
1551 EffectKind::Notify,
1552 [("n".to_string(), Term::Call(dh, vec![Term::Var(0)]))].into(),
1553 ),
1554 pre: None,
1555 post: None,
1556 };
1557 let m = Module::build(vec![double, shout], 1).unwrap();
1558 let certs = verify_module(&m).unwrap();
1559 assert!(certs[&m.entry].effects.contains(&EffectKind::Notify));
1560 let r = eval_call(
1561 &m,
1562 m.entry,
1563 vec![Value::Int(21)],
1564 certs[&m.entry].fuel_bound,
1565 )
1566 .unwrap();
1567 assert_eq!(
1568 r.value,
1569 Value::Action {
1570 kind: EffectKind::Notify,
1571 fields: [("n".to_string(), Value::Int(42))].into()
1572 }
1573 );
1574 let sneaky = Def {
1577 params: vec![Ty::Int],
1578 ret: Ty::Action,
1579 effects: BTreeSet::new(), body: Term::Call(m.entry, vec![Term::Var(0)]),
1581 pre: None,
1582 post: None,
1583 };
1584 let mut defs: Vec<Def> = m.defs.values().cloned().collect();
1585 defs.push(sneaky);
1586 let m2 = Module::build(defs, 2).unwrap();
1587 assert_eq!(
1588 verify_module(&m2).unwrap_err(),
1589 WeftError::EffectNotDeclared(EffectKind::Notify)
1590 );
1591 }
1592
1593 #[test]
1594 fn contracts_gate_execution() {
1595 let inc = Def {
1597 params: vec![Ty::Int],
1598 ret: Ty::Int,
1599 effects: BTreeSet::new(),
1600 body: Term::Prim(PrimOp::Add, vec![Term::Var(0), Term::Int(1)]),
1601 pre: Some(Term::Prim(PrimOp::Le, vec![Term::Int(0), Term::Var(0)])),
1602 post: Some(Term::Prim(PrimOp::Lt, vec![Term::Var(1), Term::Var(0)])),
1604 };
1605 let m = Module::build(vec![inc], 0).unwrap();
1606 let cert = &verify_module(&m).unwrap()[&m.entry];
1607 let ok = eval_call(&m, m.entry, vec![Value::Int(5)], cert.fuel_bound + 16).unwrap();
1608 assert_eq!(ok.value, Value::Int(6));
1609 let bad = eval_call(&m, m.entry, vec![Value::Int(-1)], cert.fuel_bound + 16);
1610 assert_eq!(bad.unwrap_err(), WeftError::ContractViolated("pre"));
1611 }
1612
1613 #[test]
1614 fn list_cat_composes_and_stays_bounded() {
1615 let cat = Def {
1617 params: vec![Ty::List(Box::new(Ty::Int)), Ty::List(Box::new(Ty::Int))],
1618 ret: Ty::List(Box::new(Ty::Int)),
1619 effects: BTreeSet::new(),
1620 body: Term::Prim(PrimOp::ListCat, vec![Term::Var(1), Term::Var(0)]),
1621 pre: None,
1622 post: None,
1623 };
1624 let m = Module::build(vec![cat], 0).unwrap();
1625 let out = eval_call(
1626 &m,
1627 m.entry,
1628 vec![
1629 Value::List(vec![Value::Int(1), Value::Int(2)]),
1630 Value::List(vec![Value::Int(3)]),
1631 ],
1632 10_000,
1633 )
1634 .unwrap();
1635 assert_eq!(
1636 out.value,
1637 Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)])
1638 );
1639
1640 let bad = Def {
1642 params: vec![Ty::List(Box::new(Ty::Int)), Ty::List(Box::new(Ty::Text))],
1643 ret: Ty::List(Box::new(Ty::Int)),
1644 effects: BTreeSet::new(),
1645 body: Term::Prim(PrimOp::ListCat, vec![Term::Var(1), Term::Var(0)]),
1646 pre: None,
1647 post: None,
1648 };
1649 assert!(
1650 Module::build(vec![bad], 0).is_err()
1651 || verify_module(
1652 &Module::build(
1653 vec![Def {
1654 params: vec![Ty::List(Box::new(Ty::Int)), Ty::List(Box::new(Ty::Text))],
1655 ret: Ty::List(Box::new(Ty::Int)),
1656 effects: BTreeSet::new(),
1657 body: Term::Prim(PrimOp::ListCat, vec![Term::Var(1), Term::Var(0)]),
1658 pre: None,
1659 post: None,
1660 }],
1661 0
1662 )
1663 .unwrap()
1664 )
1665 .is_err()
1666 );
1667
1668 let doubling = Def {
1672 params: vec![Ty::List(Box::new(Ty::Int))],
1673 ret: Ty::List(Box::new(Ty::Int)),
1674 effects: BTreeSet::new(),
1675 body: Term::Fold {
1676 cap: 16,
1677 list: Term::Iota {
1678 cap: 16,
1679 count: Box::new(Term::Int(16)),
1680 }
1681 .into(),
1682 init: Box::new(Term::Var(0)),
1683 body: Box::new(Term::Prim(
1685 PrimOp::ListCat,
1686 vec![Term::Var(1), Term::Var(1)],
1687 )),
1688 },
1689 pre: None,
1690 post: None,
1691 };
1692 let m = Module::build(vec![doubling], 0).unwrap();
1693 let seed = Value::List(vec![Value::Int(0); 4]);
1694 let r = eval_call(&m, m.entry, vec![seed], 5_000);
1695 assert!(
1696 matches!(r, Err(WeftError::FuelExhausted)),
1697 "runaway list growth is stopped by fuel, not by memory: {r:?}"
1698 );
1699 }
1700
1701 #[test]
1702 fn division_is_total() {
1703 let d = Def {
1704 params: vec![Ty::Int, Ty::Int],
1705 ret: Ty::Int,
1706 effects: BTreeSet::new(),
1707 body: Term::Prim(PrimOp::Div, vec![Term::Var(1), Term::Var(0)]),
1708 pre: None,
1709 post: None,
1710 };
1711 let m = Module::build(vec![d], 0).unwrap();
1712 let cert = &verify_module(&m).unwrap()[&m.entry];
1713 let r = eval_call(
1714 &m,
1715 m.entry,
1716 vec![Value::Int(7), Value::Int(0)],
1717 cert.fuel_bound,
1718 )
1719 .unwrap();
1720 assert_eq!(r.value, Value::Int(0), "x/0 = 0 — no traps exist in Weft");
1721 }
1722}