1use std::collections::BTreeMap;
18
19use weft::{EffectKind, Module, Ty, Value};
20
21use crate::behavior::Action;
22
23pub struct WeftBehavior {
26 module: Module,
27 state: Value,
28 fuel: u64,
29}
30
31impl WeftBehavior {
32 pub fn from_json(text: &str) -> Result<Self, String> {
35 let module: Module = serde_json::from_str(text).map_err(|e| e.to_string())?;
36 let certs = weft::verify_module(&module).map_err(|e| e.to_string())?;
37 let cert = certs
38 .get(&module.entry)
39 .ok_or("module has no entry certificate")?;
40 let entry = module
41 .defs
42 .get(&module.entry)
43 .ok_or("module has no entry def")?;
44 if entry.params.len() != 2 {
45 return Err("a weft behavior's entry must take (state, event)".into());
46 }
47 let state = default_value(&entry.params[0]);
48 Ok(Self {
50 fuel: cert.fuel_bound + 1024,
51 module,
52 state,
53 })
54 }
55
56 pub fn from_package_json(text: &str, export: &str) -> Result<Self, String> {
61 let pkg: weft::pack::Package = serde_json::from_str(text).map_err(|e| e.to_string())?;
62 pkg.verify().map_err(|e| e.to_string())?;
63 let hash = pkg
64 .export(export)
65 .ok_or_else(|| format!("package '{}' has no export '{export}'", pkg.name))?;
66 let entry_def = pkg
67 .defs
68 .get(&hash)
69 .cloned()
70 .ok_or("export verified to exist")?;
71 let module = weft::pack::link(&[pkg], vec![entry_def], 0).map_err(|e| e.to_string())?;
72 Self::from_json(&serde_json::to_string(&module).map_err(|e| e.to_string())?)
73 }
74
75 pub fn effects(&self) -> Vec<EffectKind> {
78 self.module.defs[&self.module.entry]
79 .effects
80 .iter()
81 .copied()
82 .collect()
83 }
84
85 pub fn on_interact(&mut self, name: &str, slug: Option<&str>) -> Vec<Action> {
88 let entry = &self.module.defs[&self.module.entry];
89 let event = project_event(
90 &entry.params[1],
91 &[
92 ("name", Value::Text(name.to_string())),
93 ("slug", Value::Text(slug.unwrap_or("").to_string())),
94 ],
95 );
96 let out = match weft::eval_call(
97 &self.module,
98 self.module.entry,
99 vec![self.state.clone(), event],
100 self.fuel,
101 ) {
102 Ok(o) => o,
103 Err(e) => {
104 tracing::warn!("weft behavior: {e}");
105 return Vec::new();
106 }
107 };
108 let Value::Rec(mut rec) = out.value else {
109 return Vec::new();
110 };
111 if let Some(st) = rec.remove("state") {
112 self.state = st;
113 }
114 match rec.remove("actions") {
115 Some(Value::List(actions)) => actions.into_iter().filter_map(to_action).collect(),
116 _ => Vec::new(),
117 }
118 }
119
120 pub fn on_tick(&mut self, dt_ms: i64) -> Vec<Action> {
126 let entry = &self.module.defs[&self.module.entry];
127 let event = project_event(
128 &entry.params[1],
129 &[
130 ("kind", Value::Text("tick".into())),
131 ("dt_ms", Value::Int(dt_ms)),
132 ],
133 );
134 let out = match weft::eval_call(
135 &self.module,
136 self.module.entry,
137 vec![self.state.clone(), event],
138 self.fuel,
139 ) {
140 Ok(o) => o,
141 Err(e) => {
142 tracing::warn!("weft tick: {e}");
143 return Vec::new();
144 }
145 };
146 let Value::Rec(mut rec) = out.value else {
147 return Vec::new();
148 };
149 if let Some(st) = rec.remove("state") {
150 self.state = st;
151 }
152 match rec.remove("actions") {
153 Some(Value::List(actions)) => actions.into_iter().filter_map(to_action).collect(),
154 _ => Vec::new(),
155 }
156 }
157}
158
159fn default_value(ty: &Ty) -> Value {
161 match ty {
162 Ty::Int => Value::Int(0),
163 Ty::Fix => Value::Fix(0),
164 Ty::Bool => Value::Bool(false),
165 Ty::Text => Value::Text(String::new()),
166 Ty::Action => Value::List(Vec::new()), Ty::List(_) => Value::List(Vec::new()),
168 Ty::Record(fields) => Value::Rec(
169 fields
170 .iter()
171 .map(|(k, t)| (k.clone(), default_value(t)))
172 .collect(),
173 ),
174 }
175}
176
177fn project_event(ty: &Ty, available: &[(&str, Value)]) -> Value {
181 let Ty::Record(fields) = ty else {
182 return default_value(ty);
183 };
184 let mut out = BTreeMap::new();
185 for (k, t) in fields {
186 let supplied = available
187 .iter()
188 .find(|(name, v)| name == k && type_of(v) == *t)
189 .map(|(_, v)| v.clone());
190 out.insert(k.clone(), supplied.unwrap_or_else(|| default_value(t)));
191 }
192 Value::Rec(out)
193}
194
195fn type_of(v: &Value) -> Ty {
196 match v {
197 Value::Int(_) => Ty::Int,
198 Value::Fix(_) => Ty::Fix,
199 Value::Bool(_) => Ty::Bool,
200 Value::Text(_) => Ty::Text,
201 Value::Action { .. } => Ty::Action,
202 Value::List(_) => Ty::List(Box::new(Ty::Action)),
203 Value::Rec(fs) => Ty::Record(fs.iter().map(|(k, x)| (k.clone(), type_of(x))).collect()),
204 }
205}
206
207fn to_action(v: Value) -> Option<Action> {
209 let Value::Action { kind, fields } = v else {
210 return None;
211 };
212 let text = |k: &str| match fields.get(k) {
213 Some(Value::Text(s)) => s.clone(),
214 _ => String::new(),
215 };
216 let int = |k: &str| match fields.get(k) {
217 Some(Value::Int(i)) => *i,
218 _ => 0,
219 };
220 Some(match kind {
221 EffectKind::Notify => Action::Notify {
222 text: text("text"),
223 level: None,
224 },
225 EffectKind::Navigate => Action::Navigate { to: text("to") },
226 EffectKind::CodexOpen => Action::CodexOpen { slug: text("slug") },
227 EffectKind::CommerceBuy => Action::CommerceBuy {
228 item: text("item"),
229 price_ref: serde_json::Value::from(int("price_ref")),
230 },
231 EffectKind::GiveItem => Action::GiveItem {
232 item: int("item") as u16,
233 count: int("count").max(1) as u32,
234 },
235 EffectKind::Despawn => Action::Despawn {
236 position: [0.0, 0.0, 0.0],
237 },
238 EffectKind::SetState => {
242 let placement = match fields.get("placement") {
243 Some(Value::Text(s)) if !s.is_empty() => Some(s.clone()),
244 _ => None,
245 };
246 let mut patch = serde_json::Map::new();
247 for (k, v) in &fields {
248 if k != "placement" {
249 patch.insert(k.clone(), value_to_json(v));
250 }
251 }
252 Action::SetState {
253 placement,
254 patch: serde_json::Value::Object(patch),
255 }
256 }
257 EffectKind::Spawn => {
260 let fix = |k: &str| match fields.get(k) {
263 Some(Value::Fix(raw)) => Some(*raw as f32 / weft::FIX_SCALE as f32),
264 _ => None,
265 };
266 let axis = |m: &str, cm: &str| fix(m).unwrap_or_else(|| int(cm) as f32 / 100.0);
267 Action::Spawn {
268 builtin: {
269 let b = text("builtin");
270 if b.is_empty() {
271 "sphere".to_string()
272 } else {
273 b
274 }
275 },
276 position: [axis("x", "x_cm"), axis("y", "y_cm"), axis("z", "z_cm")],
277 scale: fix("scale")
278 .unwrap_or_else(|| int("scale_cm").max(1) as f32 / 100.0)
279 .clamp(0.01, 50.0),
280 color: [
281 (int("r").clamp(0, 255) as f32) / 255.0,
282 (int("g").clamp(0, 255) as f32) / 255.0,
283 (int("b").clamp(0, 255) as f32) / 255.0,
284 ],
285 name: text("name"),
286 animate: text("animate"),
287 anim_speed: fix("speed").unwrap_or(1.0),
288 anim_amp: fix("amp").unwrap_or(0.25),
289 }
290 }
291 EffectKind::PresenceEmit => return None,
293 })
294}
295
296fn value_to_json(v: &Value) -> serde_json::Value {
298 match v {
299 Value::Int(i) => serde_json::Value::from(*i),
300 Value::Fix(raw) => serde_json::Value::from(*raw as f64 / weft::FIX_SCALE as f64),
303 Value::Bool(b) => serde_json::Value::Bool(*b),
304 Value::Text(s) => serde_json::Value::String(s.clone()),
305 Value::List(xs) => serde_json::Value::Array(xs.iter().map(value_to_json).collect()),
306 Value::Rec(fs) => serde_json::Value::Object(
307 fs.iter()
308 .map(|(k, x)| (k.clone(), value_to_json(x)))
309 .collect(),
310 ),
311 Value::Action { .. } => serde_json::Value::Null,
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use std::collections::BTreeSet;
319 use weft::{Def, PrimOp, Term};
320
321 #[test]
325 fn tick_dispatch_counts_and_rewrites() {
326 use weft::EffectKind as EK;
327 let state_ty = rec_ty(vec![("beats", Ty::Int)]);
328 let out_ty = rec_ty(vec![
329 ("actions", Ty::List(Box::new(Ty::Action))),
330 ("state", state_ty.clone()),
331 ]);
332 let body = Term::Let(
333 Box::new(Term::Prim(
334 PrimOp::Add,
335 vec![
336 Term::Get(Box::new(Term::Var(1)), "beats".into()),
337 Term::Int(1),
338 ],
339 )),
340 Box::new(Term::Rec(
341 [
342 (
343 "actions".to_string(),
344 Term::ListNew(vec![Term::Effect(
345 EK::SetState,
346 [
347 ("placement".to_string(), Term::Text("clock".into())),
348 (
349 "text".to_string(),
350 Term::Prim(
351 PrimOp::Concat,
352 vec![
353 Term::Text("Beats: ".into()),
354 Term::Prim(PrimOp::ToText, vec![Term::Var(0)]),
355 ],
356 ),
357 ),
358 ]
359 .into(),
360 )]),
361 ),
362 (
363 "state".to_string(),
364 Term::Rec([("beats".to_string(), Term::Var(0))].into()),
365 ),
366 ]
367 .into(),
368 )),
369 );
370 let def = weft::Def {
371 params: vec![state_ty, rec_ty(vec![])],
372 ret: out_ty,
373 effects: BTreeSet::from([EK::SetState]),
374 body,
375 pre: None,
376 post: None,
377 };
378 let module = weft::Module::build(vec![def], 0).unwrap();
379 let json = serde_json::to_string(&module).unwrap();
380 let mut b = WeftBehavior::from_json(&json).expect("verifies");
381 let a1 = b.on_tick(250);
382 let a2 = b.on_tick(250);
383 let patch_text = |a: &[Action]| match &a[0] {
384 Action::SetState { placement, patch } => {
385 assert_eq!(placement.as_deref(), Some("clock"));
386 patch["text"].as_str().unwrap().to_string()
387 }
388 other => panic!("wrong action {other:?}"),
389 };
390 assert_eq!(patch_text(&a1), "Beats: 1");
391 assert_eq!(patch_text(&a2), "Beats: 2", "state persists across ticks");
392 }
393
394 #[test]
397 fn set_state_effects_map_to_scene_patches() {
398 let v = Value::Action {
399 kind: EffectKind::SetState,
400 fields: [
401 ("placement".to_string(), Value::Text("visitors".into())),
402 (
403 "text".to_string(),
404 Value::Text("Visitors this session\n3".into()),
405 ),
406 ("count".to_string(), Value::Int(3)),
407 ]
408 .into(),
409 };
410 match to_action(v).expect("maps") {
411 Action::SetState { placement, patch } => {
412 assert_eq!(placement.as_deref(), Some("visitors"));
413 assert_eq!(patch["text"], "Visitors this session\n3");
414 assert_eq!(patch["count"], 3);
415 assert!(patch.get("placement").is_none());
416 }
417 other => panic!("wrong action: {other:?}"),
418 }
419 }
420
421 fn rec_ty(fields: Vec<(&str, Ty)>) -> Ty {
422 Ty::Record(
423 fields
424 .into_iter()
425 .map(|(k, v)| (k.to_string(), v))
426 .collect(),
427 )
428 }
429
430 fn greeter_json() -> String {
433 let state_ty = rec_ty(vec![("visits", Ty::Int)]);
434 let event_ty = rec_ty(vec![("name", Ty::Text)]);
435 let out_ty = rec_ty(vec![
436 ("actions", Ty::List(Box::new(Ty::Action))),
437 ("state", state_ty.clone()),
438 ]);
439 let body = Term::Rec(
441 [
442 (
443 "actions".to_string(),
444 Term::ListNew(vec![Term::Effect(
445 EffectKind::Notify,
446 [(
447 "text".to_string(),
448 Term::Prim(
449 PrimOp::Concat,
450 vec![
451 Term::Text("Welcome to ".into()),
452 Term::Get(Box::new(Term::Var(0)), "name".into()),
453 ],
454 ),
455 )]
456 .into(),
457 )]),
458 ),
459 (
460 "state".to_string(),
461 Term::Rec(
462 [(
463 "visits".to_string(),
464 Term::Prim(
465 PrimOp::Add,
466 vec![
467 Term::Get(Box::new(Term::Var(1)), "visits".into()),
468 Term::Int(1),
469 ],
470 ),
471 )]
472 .into(),
473 ),
474 ),
475 ]
476 .into(),
477 );
478 let def = Def {
479 params: vec![state_ty, event_ty],
480 ret: out_ty,
481 effects: BTreeSet::from([EffectKind::Notify]),
482 body,
483 pre: None,
484 post: None,
485 };
486 serde_json::to_string(&Module::build(vec![def], 0).unwrap()).unwrap()
487 }
488
489 #[test]
490 fn a_weft_behavior_verifies_projects_and_acts() {
491 let mut b = WeftBehavior::from_json(&greeter_json()).expect("verifies + loads");
492 assert_eq!(b.effects(), vec![EffectKind::Notify]);
493 let actions = b.on_interact("The Waystone", None);
494 assert_eq!(
495 actions,
496 vec![Action::Notify {
497 text: "Welcome to The Waystone".into(),
498 level: None
499 }]
500 );
501 b.on_interact("The Waystone", None);
504 let Value::Rec(st) = &b.state else { panic!() };
505 assert_eq!(st["visits"], Value::Int(2));
506 }
507
508 #[test]
512 #[ignore]
513 fn bench_weft_events_per_second() {
514 let mut b = WeftBehavior::from_json(&greeter_json()).unwrap();
515 let n = 200_000u32;
516 let t0 = std::time::Instant::now();
517 let mut total_actions = 0usize;
518 for _ in 0..n {
519 total_actions += b.on_interact("The Waystone", None).len();
520 }
521 let secs = t0.elapsed().as_secs_f64();
522 println!(
523 "BENCH weft events/sec = {:.0} ({n} events in {secs:.3}s, {total_actions} actions)",
524 n as f64 / secs
525 );
526 assert_eq!(total_actions, n as usize);
527 }
528
529 #[cfg(feature = "behaviors")]
535 #[test]
536 #[ignore]
537 fn bench_tier2_weft_vs_wasmi() {
538 use crate::behavior::{Actor, Behavior as _, InteractEvent};
539 use crate::behavior_wasm::WasmBehavior;
540
541 let n = 200_000u32;
542
543 let notify_def = Def {
545 params: vec![Ty::Record(BTreeMap::new()), Ty::Record(BTreeMap::new())],
546 ret: Ty::Record(
547 [
548 ("actions".to_string(), Ty::List(Box::new(Ty::Action))),
549 ("state".to_string(), Ty::Record(BTreeMap::new())),
550 ]
551 .into(),
552 ),
553 effects: BTreeSet::from([EffectKind::Notify]),
554 body: Term::Rec(
555 [
556 (
557 "actions".to_string(),
558 Term::ListNew(vec![Term::Effect(
559 EffectKind::Notify,
560 [("text".to_string(), Term::Text("The waystone hums.".into()))].into(),
561 )]),
562 ),
563 ("state".to_string(), Term::Rec(BTreeMap::new())),
564 ]
565 .into(),
566 ),
567 pre: None,
568 post: None,
569 };
570 let module = Module::build(vec![notify_def], 0).unwrap();
571 let mut weft_b = WeftBehavior::from_json(&serde_json::to_string(&module).unwrap()).unwrap();
572
573 let t0 = std::time::Instant::now();
574 let mut weft_actions = 0usize;
575 for _ in 0..n {
576 weft_actions += weft_b.on_interact("waystone", None).len();
577 }
578 let weft_secs = t0.elapsed().as_secs_f64();
579
580 let reply = r#"{"actions":[{"action":"notify","text":"The waystone hums."}]}"#;
583 let wat = format!(
587 r#"(module
588 (memory (export "memory") 1)
589 (data (i32.const 0) "{data}")
590 (func (export "thread_alloc") (param i32) (result i32)
591 i32.const 4096)
592 (func (export "thread_on_interact") (param i32 i32) (result i64)
593 i64.const {len}))"#,
594 data = reply.replace('"', "\\\""),
595 len = reply.len(),
596 );
597 let wasm = wat::parse_str(&wat).unwrap();
598 let mut wasm_b = WasmBehavior::load(&wasm).unwrap();
599 let event = InteractEvent {
600 placement: "waystone".into(),
601 actor: Actor { passport_sub: None },
602 world: "meadow".into(),
603 data: serde_json::Value::Null,
604 };
605 let t0 = std::time::Instant::now();
606 let mut wasm_actions = 0usize;
607 for _ in 0..n {
608 wasm_actions += wasm_b.on_interact(&event).len();
609 }
610 let wasm_secs = t0.elapsed().as_secs_f64();
611
612 assert_eq!(weft_actions, n as usize);
613 assert_eq!(wasm_actions, n as usize);
614 println!(
615 "BENCH tier2 matched notify · weft {:.0} ev/s · wasmi {:.0} ev/s · ratio {:.2}x",
616 n as f64 / weft_secs,
617 n as f64 / wasm_secs,
618 wasm_secs / weft_secs,
619 );
620 }
621
622 #[test]
623 fn a_tampered_module_is_refused_at_the_door() {
624 let bad = {
628 let mut m: Module = serde_json::from_str(&greeter_json()).unwrap();
629 let mut defs: Vec<Def> = m.defs.values().cloned().collect();
630 defs[0].effects.clear();
631 m = Module::build(defs, 0).unwrap();
632 serde_json::to_string(&m).unwrap()
633 };
634 assert!(WeftBehavior::from_json(&bad).is_err());
635 }
636}