Skip to content

Commit 38fcaca

Browse files
author
Arnaud Riess
committed
refactor: fixed review findings
1 parent 90bed9e commit 38fcaca

16 files changed

Lines changed: 578 additions & 162 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
(module
2+
(memory 1 2)
3+
4+
;; Active segment 1: four known bytes at offset 0
5+
(data (i32.const 0) "\01\02\03\04")
6+
7+
;; Active segment 2: four known bytes at a different offset
8+
(data (i32.const 16) "\0A\0B\0C\0D")
9+
10+
;; Passive segment: not copied to memory at startup.
11+
;; Exercised here to verify the transpiler skips it without error.
12+
;; TODO: later, add a test that explicitly loads this segment to verify it is present and correct.
13+
(data "passive bytes that are never loaded")
14+
15+
(func (export "load_i32") (param i32) (result i32)
16+
local.get 0
17+
i32.load)
18+
19+
(func (export "load_byte") (param i32) (result i32)
20+
local.get 0
21+
i32.load8_u))

crates/herkos-tests/tests/module_wrapper.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! 2. The new() constructor initializes globals and data segments
66
//! 3. Exported functions work correctly as methods on WasmModule
77
8-
use herkos_tests::{const_global, counter, hello_data};
8+
use herkos_tests::{const_global, counter, data_segments, hello_data};
99

1010
// ── Counter: mutable global, no memory ──
1111

@@ -71,6 +71,46 @@ fn test_hello_data_second_byte() {
7171
assert_eq!(word, expected, "bytes 1..5 of 'Hello' as LE i32");
7272
}
7373

74+
// ── Data segments: active segments initialised, passive segment skipped ──
75+
76+
#[test]
77+
fn test_data_segments_active_init() {
78+
let mut module = data_segments::new().unwrap();
79+
80+
// Active segment 1: bytes [1, 2, 3, 4] at offset 0
81+
let word = module.load_i32(0).unwrap();
82+
assert_eq!(word, i32::from_le_bytes([1, 2, 3, 4]));
83+
}
84+
85+
#[test]
86+
fn test_data_segments_second_active_init() {
87+
let mut module = data_segments::new().unwrap();
88+
89+
// Active segment 2: bytes [10, 11, 12, 13] at offset 16
90+
let word = module.load_i32(16).unwrap();
91+
assert_eq!(word, i32::from_le_bytes([10, 11, 12, 13]));
92+
}
93+
94+
#[test]
95+
fn test_data_segments_byte_access() {
96+
let mut module = data_segments::new().unwrap();
97+
98+
// Individual bytes from the first active segment
99+
assert_eq!(module.load_byte(0).unwrap(), 1);
100+
assert_eq!(module.load_byte(1).unwrap(), 2);
101+
assert_eq!(module.load_byte(2).unwrap(), 3);
102+
assert_eq!(module.load_byte(3).unwrap(), 4);
103+
}
104+
105+
#[test]
106+
fn test_data_segments_passive_does_not_crash() {
107+
// Instantiating the module must succeed even though a passive data
108+
// segment is present (it is silently skipped by the transpiler).
109+
let _module = data_segments::new().unwrap();
110+
111+
// TODO: we will need a future test that checks that passive segments can be initialized via `memory.init` once we support that instruction. For now we just want to verify that the presence of a passive segment doesn't cause a constructor failure.
112+
}
113+
74114
// ── Const global: immutable global as const item ──
75115

76116
#[test]

crates/herkos/src/backend/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod safe;
77
pub use safe::SafeBackend;
88

99
use crate::ir::*;
10+
use anyhow::Result;
1011

1112
/// Code generation backend trait.
1213
///
@@ -33,7 +34,7 @@ pub trait Backend {
3334
offset: u32,
3435
width: MemoryAccessWidth,
3536
sign: Option<SignExtension>,
36-
) -> String;
37+
) -> Result<String>;
3738

3839
/// Emit Rust code for a memory store (full or sub-width).
3940
fn emit_store(
@@ -43,7 +44,7 @@ pub trait Backend {
4344
value: VarId,
4445
offset: u32,
4546
width: MemoryAccessWidth,
46-
) -> String;
47+
) -> Result<String>;
4748

4849
/// Emit Rust code for a function call (local function).
4950
fn emit_call(

crates/herkos/src/backend/safe.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ impl Backend for SafeBackend {
363363
offset: u32,
364364
width: MemoryAccessWidth,
365365
sign: Option<SignExtension>,
366-
) -> String {
366+
) -> anyhow::Result<String> {
367367
let addr_expr = if offset > 0 {
368368
format!("({addr} as usize).wrapping_add({offset} as usize)")
369369
} else {
@@ -425,12 +425,11 @@ impl Backend for SafeBackend {
425425
format!("memory.load_i32({addr_expr})? as u32 as i64")
426426
}
427427

428-
// Invalid combinations (shouldn't occur from valid Wasm)
429-
// TODO: return an error instead of silently ignoring invalid stores?
430-
_ => "0 /* unsupported load width */".to_string(),
428+
// Invalid combinations (shouldn't occur in valid Wasm)
429+
_ => anyhow::bail!("unsupported load: {ty:?} width={width:?} sign={sign:?}"),
431430
};
432431

433-
format!(" {dest} = {load_expr};")
432+
Ok(format!(" {dest} = {load_expr};"))
434433
}
435434

436435
fn emit_store(
@@ -440,7 +439,7 @@ impl Backend for SafeBackend {
440439
value: VarId,
441440
offset: u32,
442441
width: MemoryAccessWidth,
443-
) -> String {
442+
) -> anyhow::Result<String> {
444443
let addr_expr = if offset > 0 {
445444
format!("({addr} as usize).wrapping_add({offset} as usize)")
446445
} else {
@@ -477,12 +476,11 @@ impl Backend for SafeBackend {
477476
format!("memory.store_i32({addr_expr}, {value} as i32)?")
478477
}
479478

480-
// Invalid combinations
481-
// TODO: return an error instead of silently ignoring invalid stores?
482-
_ => "() /* unsupported store width */".to_string(),
479+
// Invalid combinations (shouldn't occur in valid Wasm)
480+
_ => anyhow::bail!("unsupported store: {ty:?} width={width:?}"),
483481
};
484482

485-
format!(" {store_call};")
483+
Ok(format!(" {store_call};"))
486484
}
487485

488486
fn emit_call(

crates/herkos/src/codegen/constructor.rs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
99
use crate::backend::Backend;
1010
use crate::ir::*;
11+
use anyhow::Result;
1112

1213
/// Emit preamble for generated Rust files.
1314
pub fn rust_code_preamble() -> String {
@@ -35,34 +36,58 @@ pub fn emit_const_globals<B: Backend>(_backend: &B, info: &ModuleInfo) -> String
3536

3637
/// Generate element segment initialization code for a table.
3738
///
38-
/// `table_receiver` is the expression to access the table (e.g., "module.table" or "table").
39-
pub fn emit_element_segments(info: &ModuleInfo, table_receiver: &str) -> String {
39+
/// Element segments are declared in the Wasm binary's `element` section. Each
40+
/// segment specifies a base offset into the table and a list of function
41+
/// references to write into consecutive slots starting at that offset. This
42+
/// function emits the Rust `table.set(...)` calls that perform those writes
43+
/// inside the generated module's `new()` constructor.
44+
pub fn emit_element_segments(info: &ModuleInfo, table_receiver: &str) -> Result<String> {
4045
let mut code = String::new();
46+
47+
// A Wasm module may declare multiple element segments, each covering a
48+
// contiguous slice of table slots at a different base offset.
4149
for seg in &info.element_segments {
50+
// `seg.offset` is the base table index for this segment.
51+
// `seg.func_indices` lists the local function indices to place into
52+
// consecutive slots: slot (offset+0), (offset+1), ...
53+
// All indices are already in the local index space (imports subtracted).
4254
for (i, local_func_idx) in seg.func_indices.iter().enumerate() {
55+
// Absolute table slot = segment base + position within segment.
4356
let table_idx = seg.offset + i;
57+
58+
// Each table entry records the function's canonical type index so
59+
// that `call_indirect` can validate the expected signature at the
60+
// call site before dispatching. We fetch it from the IR function.
4461
let type_idx = info
4562
.ir_function(*local_func_idx)
4663
.map(|f| f.type_idx.as_usize())
47-
.unwrap_or(0);
64+
.ok_or(anyhow::anyhow!("Invalid function index"))?;
65+
66+
// Emit one table initialisation statement per slot.
67+
//
68+
// TODO: `.unwrap()` panics if `table_idx` exceeds the table's
69+
// allocated size. The table is sized from the same Wasm module
70+
// so this should be unreachable, but it is not formally proven.
71+
// Generated code should use `?` and propagate a
72+
// `ConstructionError` to stay panic-free (required by no_std).
4873
code.push_str(&format!(
4974
" {}.set({}, Some(FuncRef {{ type_index: {}, func_index: {} }})).unwrap();\n",
50-
table_receiver,
51-
table_idx,
52-
type_idx,
53-
local_func_idx.as_usize()
75+
table_receiver, // "module.table" or "table"
76+
table_idx, // absolute slot in the table
77+
type_idx, // for type-checking on call_indirect
78+
local_func_idx.as_usize() // which function to dispatch to
5479
));
5580
}
5681
}
57-
code
82+
Ok(code)
5883
}
5984

6085
/// Generate the `pub fn new() -> WasmModule` or `pub fn new() -> WasmResult<WasmModule>` constructor.
6186
pub fn generate_constructor<B: Backend>(
6287
_backend: &B,
6388
info: &ModuleInfo,
6489
has_mut_globals: bool,
65-
) -> String {
90+
) -> Result<String> {
6691
let mut code = String::new();
6792

6893
// Simple constructor for modules with no initialization
@@ -71,10 +96,10 @@ pub fn generate_constructor<B: Backend>(
7196
&& info.data_segments.is_empty()
7297
&& info.element_segments.is_empty()
7398
{
74-
code.push_str("pub fn new() -> Result<WasmModule, herkos_runtime::ConstructionError> {\n");
99+
code.push_str("pub fn new() -> Result<WasmModule, ConstructionError> {\n");
75100
code.push_str(" Ok(WasmModule(LibraryModule::new((), Table::try_new(0)?)))\n");
76101
code.push_str("}\n");
77-
return code;
102+
return Ok(code);
78103
}
79104

80105
code.push_str("pub fn new() -> WasmResult<WasmModule> {\n");
@@ -130,13 +155,13 @@ pub fn generate_constructor<B: Backend>(
130155
}
131156

132157
// Element segment initialization
133-
code.push_str(&emit_element_segments(info, "module.table"));
158+
code.push_str(&emit_element_segments(info, "module.table")?);
134159

135160
code.push_str(" Ok(WasmModule(module))\n");
136161
} else if !info.element_segments.is_empty() {
137162
// Need mutable table for element initialization
138163
code.push_str(&format!(" let mut table = {};\n", table_init));
139-
code.push_str(&emit_element_segments(info, "table"));
164+
code.push_str(&emit_element_segments(info, "table")?);
140165
code.push_str(&format!(
141166
" Ok(WasmModule(LibraryModule::new({}, table)))\n",
142167
globals_init
@@ -149,5 +174,5 @@ pub fn generate_constructor<B: Backend>(
149174
}
150175

151176
code.push_str("}\n");
152-
code
177+
Ok(code)
153178
}

crates/herkos/src/codegen/function.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ pub fn generate_function_with_info<B: Backend>(
163163
let block = &ir_func.blocks[0];
164164
for instr in &block.instructions {
165165
let code =
166-
crate::codegen::instruction::generate_instruction_with_info(backend, instr, info);
166+
crate::codegen::instruction::generate_instruction_with_info(backend, instr, info)?;
167167
output.push_str(&code);
168168
output.push('\n');
169169
}
@@ -196,7 +196,7 @@ pub fn generate_function_with_info<B: Backend>(
196196
for instr in &block.instructions {
197197
let code = crate::codegen::instruction::generate_instruction_with_info(
198198
backend, instr, info,
199-
);
199+
)?;
200200
output.push_str(&code);
201201
output.push('\n');
202202
}

crates/herkos/src/codegen/instruction.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
66
use crate::backend::Backend;
77
use crate::ir::*;
8+
use anyhow::Result;
89
use std::collections::HashMap;
910

1011
/// Generate code for a single instruction with module info.
1112
pub fn generate_instruction_with_info<B: Backend>(
1213
backend: &B,
1314
instr: &IrInstr,
1415
info: &ModuleInfo,
15-
) -> String {
16-
match instr {
16+
) -> Result<String> {
17+
let code = match instr {
1718
IrInstr::Const { dest, value } => backend.emit_const(*dest, value),
1819

1920
IrInstr::BinOp { dest, op, lhs, rhs } => backend.emit_binop(*dest, *op, *lhs, *rhs),
@@ -27,15 +28,15 @@ pub fn generate_instruction_with_info<B: Backend>(
2728
offset,
2829
width,
2930
sign,
30-
} => backend.emit_load(*dest, *ty, *addr, *offset, *width, *sign),
31+
} => return backend.emit_load(*dest, *ty, *addr, *offset, *width, *sign),
3132

3233
IrInstr::Store {
3334
ty,
3435
addr,
3536
value,
3637
offset,
3738
width,
38-
} => backend.emit_store(*ty, *addr, *value, *offset, *width),
39+
} => return backend.emit_store(*ty, *addr, *value, *offset, *width),
3940

4041
IrInstr::Call {
4142
dest,
@@ -100,7 +101,8 @@ pub fn generate_instruction_with_info<B: Backend>(
100101
val2,
101102
condition,
102103
} => backend.emit_select(*dest, *val1, *val2, *condition),
103-
}
104+
};
105+
Ok(code)
104106
}
105107

106108
/// Generate code for a terminator with BlockId to index mapping.

0 commit comments

Comments
 (0)