plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


b858611 — Peter John 2026-11-02T13:57:47+05:30
T4: native host: Backend, wasmtime binding, macroquad, recorder
plum-game/src/native.rs CHANGED
@@ -45,12 +45,15 @@ fn str_bytes(store: &mut impl wasmtime::AsContextMut, s: &Rooted<AnyRef>) -> Res
45
45
  let Val::AnyRef(Some(data)) = st.field(&mut *store, 0)? else {
46
46
  return Err(wasmtime::Error::msg("Str.data is not a byte array"));
47
47
  };
48
- let start = st.field(&mut *store, 1)?.unwrap_i64() as usize;
48
+ let (Val::I64(start), Val::I64(len)) = (st.field(&mut *store, 1)?, st.field(&mut *store, 2)?) else {
49
+ return Err(wasmtime::Error::msg("Str.start/len are not Int"));
50
+ };
49
- let len = st.field(&mut *store, 2)?.unwrap_i64() as usize;
51
+ let (start, len) = (usize::try_from(start)?, usize::try_from(len)?);
52
+ let end = start.checked_add(len).ok_or_else(|| wasmtime::Error::msg("Str view out of range"))?;
50
53
  let arr = data.unwrap_array(&*store)?;
51
54
  let mut buf = vec![0u8; arr.len(&*store)? as usize];
52
55
  arr.copy_to_i8_slice(&mut *store, &mut buf)?;
53
- buf.get(start..start + len).map(|b| b.to_vec()).ok_or_else(|| wasmtime::Error::msg("Str view out of range"))
56
+ buf.get(start..end).map(|b| b.to_vec()).ok_or_else(|| wasmtime::Error::msg("Str view out of range"))
54
57
  }
55
58
 
56
59
  fn str_arg<B: Backend>(caller: &mut Caller<'_, HostState<B>>, p: &[Val], i: usize) -> Result<String> {
@@ -176,20 +179,22 @@ impl<B: Backend> PlumGame<B> {
176
179
  Ok(())
177
180
  }
178
181
 
182
+ /// Calls `name` with the state (and `extra`) inside a root scope, so the
183
+ /// per-frame `Rooted` handle is released instead of accumulating.
179
- fn state_val(&mut self) -> Result<Val> {
184
+ fn call_with_state(&mut self, name: &str, extra: &[Val]) -> Result<()> {
185
+ let func = self.func(name)?;
180
- let s = self.state.as_ref().ok_or_else(|| wasmtime::Error::msg("`init` has not run"))?;
186
+ let s = self.state.clone().ok_or_else(|| wasmtime::Error::msg("`init` has not run"))?;
187
+ let mut scope = wasmtime::RootScope::new(&mut self.store);
181
- Ok(Val::AnyRef(Some(s.to_rooted(&mut self.store))))
188
+ let mut args = vec![Val::AnyRef(Some(s.to_rooted(&mut scope)))];
189
+ args.extend_from_slice(extra);
190
+ func.call(&mut scope, &args, &mut [])
182
191
  }
183
192
 
184
193
  pub fn update(&mut self, dt: f32) -> Result<()> {
185
- let update = self.func("update")?;
194
+ self.call_with_state("update", &[fv(dt)])
186
- let g = self.state_val()?;
187
- update.call(&mut self.store, &[g, fv(dt)], &mut [])
188
195
  }
189
196
 
190
197
  pub fn draw(&mut self) -> Result<()> {
191
- let draw = self.func("draw")?;
198
+ self.call_with_state("draw", &[])
192
- let g = self.state_val()?;
193
- draw.call(&mut self.store, &[g], &mut [])
194
199
  }
195
200
  }
plum-game/tests/host_test.rs CHANGED
@@ -139,3 +139,15 @@ fn atlasAndMapThroughHost() {
139
139
  assert!(rec.map_terrain(m, 0, 0) < 512);
140
140
  assert_eq!(rec.map_terrain(m, w, 0), 0);
141
141
  }
142
+
143
+ #[test]
144
+ fn manyFramesDoNotAccumulateRoots() {
145
+ let mut g = game(Recorder::new());
146
+ for _ in 0..2000 {
147
+ g.update(0.016).unwrap();
148
+ g.draw().unwrap();
149
+ }
150
+ g.backend_mut().calls.clear();
151
+ g.draw().unwrap();
152
+ assert_eq!(g.backend().calls.last().unwrap(), "drawText(\"frames 2000\",0,0,8,1,1,1,1)");
153
+ }