plum

#treesitter#compiler#wasm

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

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


db00bd9Peter John 2026-07-20T21:52:28+05:30
feat(plum-wasm-codegen): add wasm function table + element section support
plum-wasm-codegen/src/lib.rs CHANGED
@@ -19,6 +19,9 @@ pub struct WasmModule {
19
19
  memories: Vec<MemoryType>,
20
20
  globals: Vec<(ValType, bool, Vec<u8>)>,
21
21
  data_segments: Vec<(u32, Vec<u8>)>,
22
+ /// Function indices, in table order — the single funcref table used for
23
+ /// closure `call_indirect` dispatch. Index into this vec IS the table index.
24
+ table_elements: Vec<u32>,
22
25
  pub func_import_count: u32,
23
26
  pub func_count: u32,
24
27
  global_count: u32,
@@ -34,6 +37,7 @@ impl WasmModule {
34
37
  memories: Vec::new(),
35
38
  globals: Vec::new(),
36
39
  data_segments: Vec::new(),
40
+ table_elements: Vec::new(),
37
41
  func_import_count: 0,
38
42
  func_count: 0,
39
43
  global_count: 0,
@@ -87,6 +91,14 @@ impl WasmModule {
87
91
  self.data_segments.push((offset, data.to_vec()));
88
92
  }
89
93
 
94
+ /// Registers `func_idx` as the next slot in the single funcref table used for
95
+ /// closure `call_indirect` dispatch, returning its table index.
96
+ pub fn add_table_element(&mut self, func_idx: u32) -> u32 {
97
+ let table_idx = self.table_elements.len() as u32;
98
+ self.table_elements.push(func_idx);
99
+ table_idx
100
+ }
101
+
90
102
  pub fn finish(&mut self) -> Vec<u8> {
91
103
  let mut module = wasm_encoder::Module::new();
92
104
 
@@ -115,6 +127,19 @@ impl WasmModule {
115
127
  module.section(&funcs);
116
128
  }
117
129
 
130
+ // Table section
131
+ if !self.table_elements.is_empty() {
132
+ let mut tables = TableSection::new();
133
+ tables.table(TableType {
134
+ element_type: RefType::FUNCREF,
135
+ minimum: self.table_elements.len() as u64,
136
+ maximum: Some(self.table_elements.len() as u64),
137
+ table64: false,
138
+ shared: false,
139
+ });
140
+ module.section(&tables);
141
+ }
142
+
118
143
  // Memory section
119
144
  if !self.memories.is_empty() {
120
145
  let mut mem = MemorySection::new();
@@ -146,6 +171,14 @@ impl WasmModule {
146
171
  module.section(&exports);
147
172
  }
148
173
 
174
+ // Element section
175
+ if !self.table_elements.is_empty() {
176
+ let mut elements = ElementSection::new();
177
+ let offset = ConstExpr::i32_const(0);
178
+ elements.active(Some(0), &offset, Elements::Functions(std::borrow::Cow::Borrowed(&self.table_elements)));
179
+ module.section(&elements);
180
+ }
181
+
149
182
  // Code section
150
183
  if !self.functions.is_empty() {
151
184
  let mut code = CodeSection::new();
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -1,5 +1,6 @@
1
1
  use plum_wasm_codegen::compile_source;
2
2
  use plum_core::AstParser;
3
+ use wasm_encoder::Encode;
3
4
 
4
5
  fn parse(src: &str) -> plum_core::ast::Source {
5
6
  let mut parser = tree_sitter::Parser::new();
@@ -858,3 +859,44 @@ main() -> Int =
858
859
  let bytes = compile_source(&source).expect("compile failed");
859
860
  assert_eq!(run_main(&bytes), 11);
860
861
  }
862
+
863
+ #[test]
864
+ fn wasm_module_with_a_table_element_validates_and_call_indirect_works() {
865
+ // Exercises WasmModule's new table/element support directly, independent of any
866
+ // closure-compiling logic (which doesn't exist yet) — builds a tiny module by
867
+ // hand: one function that returns 42, registered as table element 0, called via
868
+ // `call_indirect` from `main` using a runtime-computed (not compile-time-constant)
869
+ // table index, to prove the table/element wiring is real, not coincidentally
870
+ // skipped by validation.
871
+ let mut module = plum_wasm_codegen::WasmModule::new();
872
+ let ret42_type = module.add_type(&[], &[wasm_encoder::ValType::I64]);
873
+ let ret42_idx = module.add_function(ret42_type, &{
874
+ let mut body = vec![0u8]; // 0 local-decl groups
875
+ wasm_encoder::Instruction::I64Const(42).encode(&mut body);
876
+ wasm_encoder::Instruction::End.encode(&mut body);
877
+ body
878
+ });
879
+ let table_idx = module.add_table_element(ret42_idx);
880
+ assert_eq!(table_idx, 0);
881
+
882
+ let main_type = module.add_type(&[], &[wasm_encoder::ValType::I64]);
883
+ let main_idx = module.add_function(main_type, &{
884
+ let mut body = vec![0u8]; // 0 local-decl groups
885
+ wasm_encoder::Instruction::I32Const(0).encode(&mut body); // table index operand
886
+ wasm_encoder::Instruction::CallIndirect { type_index: ret42_type, table_index: 0 }.encode(&mut body);
887
+ wasm_encoder::Instruction::End.encode(&mut body);
888
+ body
889
+ });
890
+ module.add_export("main", wasm_encoder::ExportKind::Func, main_idx);
891
+
892
+ let bytes = module.finish();
893
+ let result = wasmparser::validate(&bytes);
894
+ assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
895
+
896
+ let engine = wasmtime::Engine::default();
897
+ let wasm_module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
898
+ let mut store = wasmtime::Store::new(&engine, ());
899
+ let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
900
+ let main = instance.get_typed_func::<(), i64>(&mut store, "main").expect("main should have signature () -> i64");
901
+ assert_eq!(main.call(&mut store, ()).expect("main should not trap"), 42);
902
+ }