Skip to main content

ic_wasm/
instrumentation.rs

1use walrus::ir::*;
2use walrus::*;
3
4use crate::utils::*;
5use std::collections::HashSet;
6
7const METADATA_SIZE: i32 = 24;
8const DEFAULT_PAGE_LIMIT: i32 = 16 * 256; // 256M
9const LOG_ITEM_SIZE: i32 = 12;
10const MAX_ITEMS_PER_QUERY: i32 = 174758; // (2M - 40) / LOG_ITEM_SIZE;
11
12struct InjectionPoint {
13    position: usize,
14    cost: i64,
15    kind: InjectionKind,
16}
17impl InjectionPoint {
18    fn new() -> Self {
19        InjectionPoint {
20            position: 0,
21            cost: 0,
22            kind: InjectionKind::Static,
23        }
24    }
25}
26
27struct Variables {
28    total_counter: GlobalId,
29    log_size: GlobalId,
30    page_size: GlobalId,
31    is_init: GlobalId,
32    is_entry: GlobalId,
33    dynamic_counter_func: FunctionId,
34    dynamic_counter64_func: FunctionId,
35    /// Base address of heap trace buffer (only used when heap_trace is enabled)
36    heap_base: GlobalId,
37}
38
39pub struct Config {
40    pub trace_only_funcs: Vec<String>,
41    pub start_address: Option<i64>,
42    pub page_limit: Option<i32>,
43    /// Store traces in heap memory instead of stable memory
44    pub heap_trace: bool,
45    /// Number of WASM pages (64KB each) for heap trace buffer
46    pub heap_pages: i32,
47    pub stub_wasi: bool,
48}
49impl Config {
50    pub fn is_preallocated(&self) -> bool {
51        self.start_address.is_some()
52    }
53    pub fn log_start_address(&self) -> i64 {
54        self.start_address.unwrap_or(0) + METADATA_SIZE as i64
55    }
56    pub fn metadata_start_address(&self) -> i64 {
57        self.start_address.unwrap_or(0)
58    }
59    pub fn page_limit(&self) -> i64 {
60        i64::from(
61            self.page_limit
62                .map(|x| x - 1)
63                .unwrap_or(DEFAULT_PAGE_LIMIT - 1),
64        ) // minus 1 because of metadata
65    }
66    /// Size limit for heap trace buffer in bytes
67    pub fn heap_trace_size_limit(&self) -> i32 {
68        self.heap_pages * 65536 - METADATA_SIZE
69    }
70}
71
72/// When trace_only_funcs is not empty, counting and tracing is only enabled for those listed functions per update call.
73/// TODO: doesn't handle recursive entry functions. Need to create a wrapper for the recursive entry function.
74pub fn instrument(m: &mut Module, config: Config) -> Result<(), String> {
75    if config.stub_wasi {
76        stub_wasi_imports(m);
77    }
78    let mut trace_only_ids = HashSet::new();
79    for name in config.trace_only_funcs.iter() {
80        let id = match m.funcs.by_name(name) {
81            Some(id) => id,
82            None => return Err(format!("func \"{name}\" not found")),
83        };
84        trace_only_ids.insert(id);
85    }
86    let is_partial_tracing = !trace_only_ids.is_empty();
87    let func_cost = FunctionCost::new(m);
88    let total_counter =
89        m.globals
90            .add_local(ValType::I64, true, false, ConstExpr::Value(Value::I64(0)));
91    let log_size = m
92        .globals
93        .add_local(ValType::I32, true, false, ConstExpr::Value(Value::I32(0)));
94    let page_size = m
95        .globals
96        .add_local(ValType::I32, true, false, ConstExpr::Value(Value::I32(0)));
97    let is_init = m
98        .globals
99        .add_local(ValType::I32, true, false, ConstExpr::Value(Value::I32(1)));
100    let is_entry = m
101        .globals
102        .add_local(ValType::I32, true, false, ConstExpr::Value(Value::I32(0)));
103    // heap_base is initialized to 0 here, will be set at runtime in canister_init
104    let heap_base = m
105        .globals
106        .add_local(ValType::I32, true, false, ConstExpr::Value(Value::I32(0)));
107    let opt_init = if is_partial_tracing {
108        Some(is_init)
109    } else {
110        None
111    };
112    let dynamic_counter_func = make_dynamic_counter(m, total_counter, &opt_init);
113    let dynamic_counter64_func = make_dynamic_counter64(m, total_counter, &opt_init);
114    let vars = Variables {
115        total_counter,
116        log_size,
117        is_init,
118        is_entry,
119        dynamic_counter_func,
120        dynamic_counter64_func,
121        page_size,
122        heap_base,
123    };
124
125    for (id, func) in m.funcs.iter_local_mut() {
126        if id != dynamic_counter_func && id != dynamic_counter64_func {
127            inject_metering(
128                func,
129                func.entry_block(),
130                &vars,
131                &func_cost,
132                is_partial_tracing,
133            );
134        }
135    }
136    let writer = if config.heap_trace {
137        make_heap_writer(m, &vars, &config)
138    } else {
139        make_stable_writer(m, &vars, &config)
140    };
141    let printer = make_printer(m, &vars, writer);
142    for (id, func) in m.funcs.iter_local_mut() {
143        if id != printer
144            && id != writer
145            && id != dynamic_counter_func
146            && id != dynamic_counter64_func
147        {
148            let is_partial_tracing = trace_only_ids.contains(&id);
149            inject_profiling_prints(&m.types, printer, id, func, is_partial_tracing, &vars);
150        }
151    }
152    if !is_partial_tracing {
153        //inject_start(m, vars.is_init);
154        if config.heap_trace {
155            // Increase memory maximum to accommodate heap trace buffer
156            let memory_id = get_memory_id(m);
157            let memory = m.memories.get_mut(memory_id);
158            let current_max = memory.maximum.unwrap_or(memory.initial);
159            memory.maximum = Some(current_max + config.heap_pages as u64);
160            inject_init_heap_trace(m, &vars, &config);
161        } else {
162            inject_init(m, vars.is_init);
163        }
164    }
165    // Persist globals (skip for heap_trace since heap doesn't persist across upgrades)
166    if !config.heap_trace {
167        inject_pre_upgrade(m, &vars, &config);
168        inject_post_upgrade(m, &vars, &config);
169    }
170
171    inject_canister_methods(m, &vars);
172    let leb = make_leb128_encoder(m);
173    if config.heap_trace {
174        make_heap_getter(m, &vars, leb, &config);
175    } else {
176        make_stable_getter(m, &vars, leb, &config);
177    }
178    make_getter(m, &vars);
179    make_toggle_func(m, "__toggle_tracing", vars.is_init);
180    make_toggle_func(m, "__toggle_entry", vars.is_entry);
181    let name = make_name_section(m);
182    m.customs.add(name);
183    Ok(())
184}
185
186fn inject_metering(
187    func: &mut LocalFunction,
188    start: InstrSeqId,
189    vars: &Variables,
190    func_cost: &FunctionCost,
191    is_partial_tracing: bool,
192) {
193    use InjectionKind::*;
194    let mut stack = vec![start];
195    while let Some(seq_id) = stack.pop() {
196        let seq = func.block(seq_id);
197        // Finding injection points
198        let mut injection_points = vec![];
199        let mut curr = InjectionPoint::new();
200        // each function has at least a unit cost
201        if seq_id == start {
202            curr.cost += 1;
203        }
204        for (pos, (instr, _)) in seq.instrs.iter().enumerate() {
205            curr.position = pos;
206            match instr {
207                Instr::Block(Block { seq }) | Instr::Loop(Loop { seq }) => {
208                    match func.block(*seq).ty {
209                        InstrSeqType::Simple(Some(_)) => curr.cost += instr_cost(instr),
210                        InstrSeqType::Simple(None) => (),
211                        InstrSeqType::MultiValue(_) => unreachable!("Multivalue not supported"),
212                    }
213                    stack.push(*seq);
214                    injection_points.push(curr);
215                    curr = InjectionPoint::new();
216                }
217                Instr::IfElse(IfElse {
218                    consequent,
219                    alternative,
220                }) => {
221                    curr.cost += instr_cost(instr);
222                    stack.push(*consequent);
223                    stack.push(*alternative);
224                    injection_points.push(curr);
225                    curr = InjectionPoint::new();
226                }
227                Instr::Br(_) | Instr::BrIf(_) | Instr::BrTable(_) => {
228                    // br always points to a block, so we don't need to push the br block to stack for traversal
229                    curr.cost += instr_cost(instr);
230                    injection_points.push(curr);
231                    curr = InjectionPoint::new();
232                }
233                Instr::Return(_) | Instr::Unreachable(_) => {
234                    curr.cost += instr_cost(instr);
235                    injection_points.push(curr);
236                    curr = InjectionPoint::new();
237                }
238                Instr::Call(Call { func }) => {
239                    curr.cost += instr_cost(instr);
240                    match func_cost.get_cost(*func) {
241                        Some((cost, InjectionKind::Static)) => curr.cost += cost,
242                        Some((cost, kind @ InjectionKind::Dynamic))
243                        | Some((cost, kind @ InjectionKind::Dynamic64)) => {
244                            curr.cost += cost;
245                            let dynamic = InjectionPoint {
246                                position: pos,
247                                cost: 0,
248                                kind,
249                            };
250                            injection_points.push(dynamic);
251                        }
252                        None => {}
253                    }
254                }
255                Instr::MemoryFill(_)
256                | Instr::MemoryCopy(_)
257                | Instr::MemoryInit(_)
258                | Instr::TableCopy(_)
259                | Instr::TableInit(_) => {
260                    curr.cost += instr_cost(instr);
261                    let dynamic = InjectionPoint {
262                        position: pos,
263                        cost: 0,
264                        kind: InjectionKind::Dynamic,
265                    };
266                    injection_points.push(dynamic);
267                }
268                _ => {
269                    curr.cost += instr_cost(instr);
270                }
271            }
272        }
273        injection_points.push(curr);
274        // Reconstruct instructions
275        let injection_points = injection_points
276            .iter()
277            .filter(|point| point.cost > 0 || point.kind != Static);
278        let mut builder = func.builder_mut().instr_seq(seq_id);
279        let original = builder.instrs_mut();
280        let mut instrs = vec![];
281        let mut last_injection_position = 0;
282        for point in injection_points {
283            instrs.extend_from_slice(&original[last_injection_position..point.position]);
284            // injection happens one instruction before the injection_points, so the cost contains
285            // the control flow instruction.
286            match point.kind {
287                Static => {
288                    #[rustfmt::skip]
289                    instrs.extend_from_slice(&[
290                        (GlobalGet { global: vars.total_counter }.into(), Default::default()),
291                        (Const { value: Value::I64(point.cost) }.into(), Default::default()),
292                    ]);
293                    if is_partial_tracing {
294                        #[rustfmt::skip]
295                        instrs.extend_from_slice(&[
296                            (GlobalGet { global: vars.is_init }.into(), Default::default()),
297                            (Const { value: Value::I32(1) }.into(), Default::default()),
298                            (Binop { op: BinaryOp::I32Xor }.into(), Default::default()),
299                            (Unop { op: UnaryOp::I64ExtendUI32 }.into(), Default::default()),
300                            (Binop { op: BinaryOp::I64Mul }.into(), Default::default()),
301                        ]);
302                    }
303                    #[rustfmt::skip]
304                    instrs.extend_from_slice(&[
305                        (Binop { op: BinaryOp::I64Add }.into(), Default::default()),
306                        (GlobalSet { global: vars.total_counter }.into(), Default::default()),
307                    ]);
308                }
309                Dynamic => {
310                    // Assume top of the stack is the i32 size parameter
311                    #[rustfmt::skip]
312                    instrs.push((Call { func: vars.dynamic_counter_func }.into(), Default::default()));
313                }
314                Dynamic64 => {
315                    #[rustfmt::skip]
316                    instrs.push((Call { func: vars.dynamic_counter64_func }.into(), Default::default()));
317                }
318            };
319            last_injection_position = point.position;
320        }
321        instrs.extend_from_slice(&original[last_injection_position..]);
322        *original = instrs;
323    }
324}
325
326fn inject_profiling_prints(
327    types: &ModuleTypes,
328    printer: FunctionId,
329    id: FunctionId,
330    func: &mut LocalFunction,
331    is_partial_tracing: bool,
332    vars: &Variables,
333) {
334    // Put the original function body inside a block, so that if the code
335    // use br_if/br_table to exit the function, we can still output the exit signal.
336    let start_id = func.entry_block();
337    let original_block = func.block_mut(start_id);
338    let start_instrs = original_block.instrs.split_off(0);
339    let start_ty = match original_block.ty {
340        InstrSeqType::MultiValue(id) => {
341            let valtypes = types.results(id);
342            InstrSeqType::Simple(match valtypes.len() {
343                0 => None,
344                1 => Some(valtypes[0]),
345                _ => unreachable!("Multivalue return not supported"),
346            })
347        }
348        // top-level block is using the function signature
349        InstrSeqType::Simple(_) => unreachable!(),
350    };
351    let mut inner_start = func.builder_mut().dangling_instr_seq(start_ty);
352    *(inner_start.instrs_mut()) = start_instrs;
353    let inner_start_id = inner_start.id();
354    let mut start_builder = func.builder_mut().func_body();
355    if is_partial_tracing {
356        start_builder.i32_const(0).global_set(vars.is_init);
357    }
358    start_builder
359        .i32_const(id.index() as i32)
360        .call(printer)
361        .instr(Block {
362            seq: inner_start_id,
363        })
364        // TOOD fix when id == 0
365        .i32_const(-(id.index() as i32))
366        .call(printer);
367    // TODO this only works for non-recursive entry function
368    if is_partial_tracing {
369        start_builder.i32_const(1).global_set(vars.is_init);
370    }
371    let mut stack = vec![inner_start_id];
372    while let Some(seq_id) = stack.pop() {
373        let mut builder = func.builder_mut().instr_seq(seq_id);
374        let original = builder.instrs_mut();
375        let mut instrs = vec![];
376        for (instr, loc) in original.iter() {
377            match instr {
378                Instr::Block(Block { seq }) | Instr::Loop(Loop { seq }) => {
379                    stack.push(*seq);
380                    instrs.push((instr.clone(), *loc));
381                }
382                Instr::IfElse(IfElse {
383                    consequent,
384                    alternative,
385                }) => {
386                    stack.push(*alternative);
387                    stack.push(*consequent);
388                    instrs.push((instr.clone(), *loc));
389                }
390                Instr::Return(_) => {
391                    instrs.push((
392                        Instr::Br(Br {
393                            block: inner_start_id,
394                        }),
395                        *loc,
396                    ));
397                }
398                // redirect br,br_if,br_table to inner seq id
399                Instr::Br(Br { block }) if *block == start_id => {
400                    instrs.push((
401                        Instr::Br(Br {
402                            block: inner_start_id,
403                        }),
404                        *loc,
405                    ));
406                }
407                Instr::BrIf(BrIf { block }) if *block == start_id => {
408                    instrs.push((
409                        Instr::BrIf(BrIf {
410                            block: inner_start_id,
411                        }),
412                        *loc,
413                    ));
414                }
415                Instr::BrTable(BrTable { blocks, default }) => {
416                    let mut blocks = blocks.clone();
417                    for i in 0..blocks.len() {
418                        if let Some(id) = blocks.get_mut(i) {
419                            if *id == start_id {
420                                *id = inner_start_id
421                            };
422                        }
423                    }
424                    let default = if *default == start_id {
425                        inner_start_id
426                    } else {
427                        *default
428                    };
429                    instrs.push((Instr::BrTable(BrTable { blocks, default }), *loc));
430                }
431                _ => instrs.push((instr.clone(), *loc)),
432            }
433        }
434        *original = instrs;
435    }
436}
437
438fn make_dynamic_counter(
439    m: &mut Module,
440    total_counter: GlobalId,
441    opt_init: &Option<GlobalId>,
442) -> FunctionId {
443    let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I32], &[ValType::I32]);
444    let size = m.locals.add(ValType::I32);
445    let mut seq = builder.func_body();
446    seq.local_get(size);
447    if let Some(is_init) = opt_init {
448        seq.global_get(*is_init)
449            .i32_const(1)
450            .binop(BinaryOp::I32Xor)
451            .binop(BinaryOp::I32Mul);
452    }
453    seq.unop(UnaryOp::I64ExtendUI32)
454        .global_get(total_counter)
455        .binop(BinaryOp::I64Add)
456        .global_set(total_counter)
457        .local_get(size);
458    builder.finish(vec![size], &mut m.funcs)
459}
460fn make_dynamic_counter64(
461    m: &mut Module,
462    total_counter: GlobalId,
463    opt_init: &Option<GlobalId>,
464) -> FunctionId {
465    let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64], &[ValType::I64]);
466    let size = m.locals.add(ValType::I64);
467    let mut seq = builder.func_body();
468    seq.local_get(size);
469    if let Some(is_init) = opt_init {
470        seq.global_get(*is_init)
471            .i32_const(1)
472            .binop(BinaryOp::I32Xor)
473            .unop(UnaryOp::I64ExtendUI32)
474            .binop(BinaryOp::I64Mul);
475    }
476    seq.global_get(total_counter)
477        .binop(BinaryOp::I64Add)
478        .global_set(total_counter)
479        .local_get(size);
480    builder.finish(vec![size], &mut m.funcs)
481}
482fn make_stable_writer(m: &mut Module, vars: &Variables, config: &Config) -> FunctionId {
483    let writer = get_ic_func_id(m, "stable64_write");
484    let grow = get_ic_func_id(m, "stable64_grow");
485    let mut builder = FunctionBuilder::new(
486        &mut m.types,
487        &[ValType::I64, ValType::I64, ValType::I64],
488        &[],
489    );
490    let start_address = config.log_start_address();
491    let size_limit = config.page_limit() * 65536;
492    let is_preallocated = config.is_preallocated();
493    let offset = m.locals.add(ValType::I64);
494    let src = m.locals.add(ValType::I64);
495    let size = m.locals.add(ValType::I64);
496    builder
497        .func_body()
498        .local_get(offset)
499        .local_get(size)
500        .binop(BinaryOp::I64Add);
501    if is_preallocated {
502        builder.func_body().i64_const(size_limit);
503    } else {
504        builder
505            .func_body()
506            .global_get(vars.page_size)
507            .i32_const(65536)
508            .binop(BinaryOp::I32Mul)
509            .i32_const(METADATA_SIZE)
510            .binop(BinaryOp::I32Sub)
511            // SI because it can be negative
512            .unop(UnaryOp::I64ExtendSI32);
513    }
514    builder
515        .func_body()
516        .binop(BinaryOp::I64GtS)
517        .if_else(
518            None,
519            |then| {
520                if is_preallocated {
521                    then.return_();
522                } else {
523                    // This assumes user code doesn't use stable memory
524                    then.global_get(vars.page_size)
525                        .i32_const(DEFAULT_PAGE_LIMIT)
526                        .binop(BinaryOp::I32GtS) // trace > default_page_limit
527                        .if_else(
528                            None,
529                            |then| {
530                                then.return_();
531                            },
532                            |else_| {
533                                else_
534                                    .i64_const(1)
535                                    .call(grow)
536                                    .drop()
537                                    .global_get(vars.page_size)
538                                    .i32_const(1)
539                                    .binop(BinaryOp::I32Add)
540                                    .global_set(vars.page_size);
541                            },
542                        );
543                }
544            },
545            |_| {},
546        )
547        .i64_const(start_address)
548        .local_get(offset)
549        .binop(BinaryOp::I64Add)
550        .local_get(src)
551        .local_get(size)
552        .call(writer)
553        .global_get(vars.log_size)
554        .i32_const(1)
555        .binop(BinaryOp::I32Add)
556        .global_set(vars.log_size);
557    builder.finish(vec![offset, src, size], &mut m.funcs)
558}
559
560fn make_printer(m: &mut Module, vars: &Variables, writer: FunctionId) -> FunctionId {
561    let memory = get_memory_id(m);
562    let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I32], &[]);
563    let func_id = m.locals.add(ValType::I32);
564    let a = m.locals.add(ValType::I32);
565    let b = m.locals.add(ValType::I64);
566    builder.func_body().global_get(vars.is_init).if_else(
567        None,
568        |then| {
569            then.return_();
570        },
571        |else_| {
572            #[rustfmt::skip]
573            else_
574                // backup memory
575                .i32_const(0)
576                .load(memory, LoadKind::I32 { atomic: false }, MemArg { offset: 0, align: 4})
577                .local_set(a)
578                .i32_const(4)
579                .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8})
580                .local_set(b)
581                // print
582                .i32_const(0)
583                .local_get(func_id)
584                .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 })
585                .i32_const(4)
586                .global_get(vars.total_counter)
587                .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
588                .global_get(vars.log_size)
589                .unop(UnaryOp::I64ExtendUI32)
590                .i64_const(LOG_ITEM_SIZE as i64)
591                .binop(BinaryOp::I64Mul)
592                .i64_const(0)
593                .i64_const(LOG_ITEM_SIZE as i64)
594                .call(writer)
595                // restore memory
596                .i32_const(0)
597                .local_get(a)
598                .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 })
599                .i32_const(4)
600                .local_get(b)
601                .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 });
602        },
603    );
604    builder.finish(vec![func_id], &mut m.funcs)
605}
606/*
607// We can use this function once we have a system memroy for logs.
608// Otherwise, we cannot call stable_write in canister_init
609fn inject_start(m: &mut Module, is_init: GlobalId) {
610    if let Some(id) = m.start {
611        let mut builder = get_builder(m, id);
612        #[rustfmt::skip]
613        builder
614            .instr(Const { value: Value::I32(0) })
615            .instr(GlobalSet { global: is_init });
616    }
617}
618*/
619fn inject_canister_methods(m: &mut Module, vars: &Variables) {
620    let methods: Vec<_> = m
621        .exports
622        .iter()
623        .filter_map(|e| match e.item {
624            ExportItem::Function(id)
625                if e.name != "canister_update __motoko_async_helper"
626                    && (e.name.starts_with("canister_update")
627                    || e.name.starts_with("canister_query")
628                    || e.name.starts_with("canister_composite_query")
629                    || e.name.starts_with("canister_heartbeat")
630                    // don't clear logs for timer and post_upgrade, as they are trigger by other signals
631                    //|| e.name == "canister_global_timer"
632                    //|| e.name == "canister_post_upgrade"
633                    || e.name == "canister_pre_upgrade") =>
634            {
635                Some(id)
636            }
637            _ => None,
638        })
639        .collect();
640    for id in methods.iter() {
641        let mut builder = get_builder(m, *id);
642        #[rustfmt::skip]
643        inject_top(
644            &mut builder,
645            vec![
646                // log_size = is_entry ? log_size : 0
647                GlobalGet { global: vars.is_entry }.into(),
648                GlobalGet { global: vars.log_size }.into(),
649                Binop { op: BinaryOp::I32Mul }.into(),
650                GlobalSet { global: vars.log_size }.into(),
651            ],
652        );
653    }
654}
655fn inject_init(m: &mut Module, is_init: GlobalId) {
656    let mut builder = get_or_create_export_func(m, "canister_init");
657    // canister_init in Motoko use stable_size to decide if there is stable memory to deserialize.
658    // Region initialization in Motoko is also done here.
659    // We can only enable profiling at the end of init, otherwise stable.grow breaks this check.
660    builder.i32_const(0).global_set(is_init);
661}
662fn inject_pre_upgrade(m: &mut Module, vars: &Variables, config: &Config) {
663    let writer = get_ic_func_id(m, "stable64_write");
664    let memory = get_memory_id(m);
665    let a = m.locals.add(ValType::I64);
666    let b = m.locals.add(ValType::I64);
667    let c = m.locals.add(ValType::I64);
668    let mut builder = get_or_create_export_func(m, "canister_pre_upgrade");
669    #[rustfmt::skip]
670    builder
671        // backup memory. This is not strictly needed, since it's at the end of pre-upgrade.
672        .i32_const(0)
673        .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8})
674        .local_set(a)
675        .i32_const(8)
676        .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8})
677        .local_set(b)
678        .i32_const(16)
679        .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8})
680        .local_set(c)
681        // persist globals
682        .i32_const(0)
683        .global_get(vars.total_counter)
684        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
685        .i32_const(8)
686        .global_get(vars.log_size)
687        .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 })
688        .i32_const(12)
689        .global_get(vars.page_size)
690        .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 })
691        .i32_const(16)
692        .global_get(vars.is_init)
693        .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 })
694        .i32_const(20)
695        .global_get(vars.is_entry)
696        .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 })
697        .i64_const(config.metadata_start_address())
698        .i64_const(0)
699        .i64_const(METADATA_SIZE as i64)
700        .call(writer)
701        // restore memory
702        .i32_const(0)
703        .local_get(a)
704        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
705        .i32_const(8)
706        .local_get(b)
707        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
708        .i32_const(16)
709        .local_get(c)
710        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 });
711}
712fn inject_post_upgrade(m: &mut Module, vars: &Variables, config: &Config) {
713    let reader = get_ic_func_id(m, "stable64_read");
714    let memory = get_memory_id(m);
715    let a = m.locals.add(ValType::I64);
716    let b = m.locals.add(ValType::I64);
717    let c = m.locals.add(ValType::I64);
718    let mut builder = get_or_create_export_func(m, "canister_post_upgrade");
719    #[rustfmt::skip]
720    inject_top(&mut builder, vec![
721        // backup
722        Const { value: Value::I32(0) }.into(),
723        Load { memory, kind: LoadKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(),
724        LocalSet { local: a }.into(),
725        Const { value: Value::I32(8) }.into(),
726        Load { memory, kind: LoadKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(),
727        LocalSet { local: b }.into(),
728        Const { value: Value::I32(16) }.into(),
729        Load { memory, kind: LoadKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(),
730        LocalSet { local: c }.into(),
731        // load from stable memory
732        Const { value: Value::I64(0) }.into(),
733        Const { value: Value::I64(config.metadata_start_address()) }.into(),
734        Const { value: Value::I64(METADATA_SIZE as i64) }.into(),
735        Call { func: reader }.into(),
736        Const { value: Value::I32(0) }.into(),
737        Load { memory, kind: LoadKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(),
738        GlobalSet { global: vars.total_counter }.into(),
739        Const { value: Value::I32(8) }.into(),
740        Load { memory, kind: LoadKind::I32 { atomic: false }, arg: MemArg { offset: 0, align: 4 } }.into(),
741        GlobalSet { global: vars.log_size }.into(),
742        Const { value: Value::I32(12) }.into(),
743        Load { memory, kind: LoadKind::I32 { atomic: false }, arg: MemArg { offset: 0, align: 4 } }.into(),
744        GlobalSet { global: vars.page_size }.into(),
745        Const { value: Value::I32(16) }.into(),
746        Load { memory, kind: LoadKind::I32 { atomic: false }, arg: MemArg { offset: 0, align: 4 } }.into(),
747        GlobalSet { global: vars.is_init }.into(),
748        Const { value: Value::I32(20) }.into(),
749        Load { memory, kind: LoadKind::I32 { atomic: false }, arg: MemArg { offset: 0, align: 4 } }.into(),
750        GlobalSet { global: vars.is_entry }.into(),
751        // restore
752        Const { value: Value::I32(0) }.into(),
753        LocalGet { local: a }.into(),
754        Store { memory, kind: StoreKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(),
755        Const { value: Value::I32(8) }.into(),
756        LocalGet { local: b }.into(),
757        Store { memory, kind: StoreKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(),
758        Const { value: Value::I32(16) }.into(),
759        LocalGet { local: c }.into(),
760        Store { memory, kind: StoreKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(),
761    ]);
762}
763
764fn make_stable_getter(m: &mut Module, vars: &Variables, leb: FunctionId, config: &Config) {
765    let memory = get_memory_id(m);
766    let arg_size = get_ic_func_id(m, "msg_arg_data_size");
767    let arg_copy = get_ic_func_id(m, "msg_arg_data_copy");
768    let reply_data = get_ic_func_id(m, "msg_reply_data_append");
769    let reply = get_ic_func_id(m, "msg_reply");
770    let trap = get_ic_func_id(m, "trap");
771    let reader = get_ic_func_id(m, "stable64_read");
772    let idx = m.locals.add(ValType::I32);
773    let len = m.locals.add(ValType::I32);
774    let next_idx = m.locals.add(ValType::I32);
775    let mut builder = FunctionBuilder::new(&mut m.types, &[], &[]);
776    builder.name("__get_profiling".to_string());
777    #[rustfmt::skip]
778    builder.func_body()
779        // allocate 2M of heap memory, it's a query call, the system will give back the memory.
780        .memory_size(memory)
781        .i32_const(32)
782        .binop(BinaryOp::I32LtU)
783        .if_else(
784            None,
785            |then| {
786                then
787                    .i32_const(32)
788                    .memory_grow(memory)
789                    .drop();
790            },
791            |_| {}
792        )
793        // parse input idx
794        .call(arg_size)
795        .i32_const(11)
796        .binop(BinaryOp::I32Ne)
797        .if_else(
798            None,
799            |then| {
800                then.i32_const(0)
801                    .i32_const(0)
802                    .call(trap);
803            },
804            |_| {},
805        )
806        .i32_const(0)
807        .i32_const(7)
808        .i32_const(4)
809        .call(arg_copy)
810        .i32_const(0)
811        .load(memory, LoadKind::I32 { atomic: false }, MemArg { offset: 0, align: 4})
812        .local_set(idx)
813        // write header (vec { record { int32; int64 } }, opt int32)
814        .i32_const(0)
815        .i64_const(0x6c016d034c444944) // "DIDL036d016c"
816        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
817        .i32_const(8)
818        .i64_const(0x02756e7401750002)  // "02007501746e7502"
819        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
820        .i32_const(16)
821        .i32_const(0x0200) // "0002"
822        .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4})
823        .i32_const(0)
824        .i32_const(18)
825        .call(reply_data)
826        // if log_size - idx > MAX_ITEMS_PER_QUERY
827        .global_get(vars.log_size)
828        .local_get(idx)
829        .binop(BinaryOp::I32Sub)
830        .local_tee(len)
831        .i32_const(MAX_ITEMS_PER_QUERY)
832        .binop(BinaryOp::I32GtU)
833        .if_else(
834            None,
835            |then| {
836                then.i32_const(MAX_ITEMS_PER_QUERY)
837                    .local_set(len)
838                    .local_get(idx)
839                    .i32_const(MAX_ITEMS_PER_QUERY)
840                    .binop(BinaryOp::I32Add)
841                    .local_set(next_idx);
842            },
843            |else_| {
844                else_.i32_const(0)
845                    .local_set(next_idx);
846            },
847        )
848        .local_get(len)
849        .call(leb)
850        .i32_const(0)
851        .i32_const(5)
852        .call(reply_data)
853        // read stable logs
854        .i64_const(0)
855        .i64_const(config.log_start_address())
856        .local_get(idx)
857        .unop(UnaryOp::I64ExtendUI32)
858        .i64_const(LOG_ITEM_SIZE as i64)
859        .binop(BinaryOp::I64Mul)
860        .binop(BinaryOp::I64Add)
861        .local_get(len)
862        .unop(UnaryOp::I64ExtendUI32)
863        .i64_const(LOG_ITEM_SIZE as i64)
864        .binop(BinaryOp::I64Mul)
865        .call(reader)
866        .i32_const(0)
867        .local_get(len)
868        .i32_const(LOG_ITEM_SIZE)
869        .binop(BinaryOp::I32Mul)
870        .call(reply_data)
871        // opt next idx
872        .local_get(next_idx)
873        .unop(UnaryOp::I32Eqz)
874        .if_else(
875            None,
876            |then| {
877                then.i32_const(0)
878                    .i32_const(0)
879                    .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4})
880                    .i32_const(0)
881                    .i32_const(1)
882                    .call(reply_data);
883            },
884            |else_| {
885                else_.i32_const(0)
886                    .i32_const(1)
887                    .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 1})
888                    .i32_const(1)
889                    .local_get(next_idx)
890                    .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4})
891                    .i32_const(0)
892                    .i32_const(5)
893                    .call(reply_data);
894            },
895        )
896        .call(reply);
897    let getter = builder.finish(vec![], &mut m.funcs);
898    m.exports.add("canister_query __get_profiling", getter);
899}
900// Generate i32 to 5-byte LEB128 encoding at memory address 0..5
901fn make_leb128_encoder(m: &mut Module) -> FunctionId {
902    let memory = get_memory_id(m);
903    let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I32], &[]);
904    let value = m.locals.add(ValType::I32);
905    let mut instrs = builder.func_body();
906    for i in 0..5 {
907        instrs
908            .i32_const(i)
909            .local_get(value)
910            .i32_const(0x7f)
911            .binop(BinaryOp::I32And);
912        if i < 4 {
913            instrs.i32_const(0x80).binop(BinaryOp::I32Or);
914        }
915        #[rustfmt::skip]
916        instrs
917            .store(memory, StoreKind::I32_8 { atomic: false }, MemArg { offset: 0, align: 1 })
918            .local_get(value)
919            .i32_const(7)
920            .binop(BinaryOp::I32ShrU)
921            .local_set(value);
922    }
923    builder.finish(vec![value], &mut m.funcs)
924}
925fn make_name_section(m: &Module) -> RawCustomSection {
926    use candid::Encode;
927    let name: Vec<_> = m
928        .funcs
929        .iter()
930        .filter_map(|f| {
931            if matches!(f.kind, FunctionKind::Local(_)) {
932                use rustc_demangle::demangle;
933                let name = f.name.as_ref()?;
934                let demangled = format!("{:#}", demangle(name));
935                Some((f.id().index() as u16, demangled))
936            } else {
937                None
938            }
939        })
940        .collect();
941    let data = Encode!(&name).unwrap();
942    RawCustomSection {
943        name: "icp:public name".to_string(),
944        data,
945    }
946}
947
948fn make_getter(m: &mut Module, vars: &Variables) {
949    let memory = get_memory_id(m);
950    let reply_data = get_ic_func_id(m, "msg_reply_data_append");
951    let reply = get_ic_func_id(m, "msg_reply");
952    let mut getter = FunctionBuilder::new(&mut m.types, &[], &[]);
953    getter.name("__get_cycles".to_string());
954    #[rustfmt::skip]
955    getter
956        .func_body()
957        // It's a query call, so we can arbitrarily change the memory without restoring them afterwards.
958        .i32_const(0)
959        .i64_const(0x007401004c444944)  // "DIDL000174xx" in little endian
960        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
961        .i32_const(7)
962        .global_get(vars.total_counter)
963        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
964        .i32_const(0)
965        .i32_const(15)
966        .call(reply_data)
967        .call(reply);
968    let getter = getter.finish(vec![], &mut m.funcs);
969    m.exports.add("canister_query __get_cycles", getter);
970}
971fn make_toggle_func(m: &mut Module, name: &str, var: GlobalId) {
972    let memory = get_memory_id(m);
973    let reply_data = get_ic_func_id(m, "msg_reply_data_append");
974    let reply = get_ic_func_id(m, "msg_reply");
975    let tmp = m.locals.add(ValType::I64);
976    let mut builder = FunctionBuilder::new(&mut m.types, &[], &[]);
977    builder.name(name.to_string());
978    #[rustfmt::skip]
979    builder
980        .func_body()
981        .global_get(var)
982        .i32_const(1)
983        .binop(BinaryOp::I32Xor)
984        .global_set(var)
985        .i32_const(0)
986        .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
987        .local_set(tmp)
988        .i32_const(0)
989        .i64_const(0x4c444944) // "DIDL0000xxxx"
990        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
991        .i32_const(0)
992        .i32_const(6)
993        .call(reply_data)
994        .i32_const(0)
995        .local_get(tmp)
996        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
997        .call(reply);
998    let id = builder.finish(vec![], &mut m.funcs);
999    m.exports.add(&format!("canister_update {name}"), id);
1000}
1001
1002// ============================================================================
1003// Heap-based tracing (for canisters that use stable memory)
1004// ============================================================================
1005
1006/// Initialize heap trace buffer in canister_init
1007/// Grows memory and sets heap_base to the start of the trace buffer
1008fn inject_init_heap_trace(m: &mut Module, vars: &Variables, config: &Config) {
1009    let memory = get_memory_id(m);
1010    let mut builder = get_or_create_export_func(m, "canister_init");
1011    #[rustfmt::skip]
1012    builder
1013        // heap_base = memory.size * 65536
1014        .memory_size(memory)
1015        .i32_const(65536)
1016        .binop(BinaryOp::I32Mul)
1017        .global_set(vars.heap_base)
1018        // memory.grow(heap_pages)
1019        .i32_const(config.heap_pages)
1020        .memory_grow(memory)
1021        .drop()
1022        // Enable profiling (is_init = 0)
1023        .i32_const(0)
1024        .global_set(vars.is_init);
1025}
1026
1027/// Write log entry to heap memory instead of stable memory
1028fn make_heap_writer(m: &mut Module, vars: &Variables, config: &Config) -> FunctionId {
1029    let memory = get_memory_id(m);
1030    let size_limit = config.heap_trace_size_limit();
1031    let mut builder = FunctionBuilder::new(
1032        &mut m.types,
1033        &[ValType::I64, ValType::I64, ValType::I64],
1034        &[],
1035    );
1036    let offset = m.locals.add(ValType::I64);
1037    let src = m.locals.add(ValType::I64);
1038    let size = m.locals.add(ValType::I64);
1039    let dest_addr = m.locals.add(ValType::I32);
1040    let tmp_i32 = m.locals.add(ValType::I32);
1041    let tmp_i64 = m.locals.add(ValType::I64);
1042
1043    // Check if we have space: offset + size <= size_limit
1044    #[rustfmt::skip]
1045    builder
1046        .func_body()
1047        .local_get(offset)
1048        .local_get(size)
1049        .binop(BinaryOp::I64Add)
1050        .i64_const(size_limit as i64)
1051        .binop(BinaryOp::I64GtS)
1052        .if_else(
1053            None,
1054            |then| {
1055                // Out of space, just return without writing
1056                then.return_();
1057            },
1058            |_| {},
1059        )
1060        // Calculate destination address: heap_base + METADATA_SIZE + offset
1061        .global_get(vars.heap_base)
1062        .i32_const(METADATA_SIZE)
1063        .binop(BinaryOp::I32Add)
1064        .local_get(offset)
1065        .unop(UnaryOp::I32WrapI64)
1066        .binop(BinaryOp::I32Add)
1067        .local_set(dest_addr)
1068        // Load the 4-byte func_id from src (address 0) and store it
1069        .i32_const(0)
1070        .load(memory, LoadKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 })
1071        .local_set(tmp_i32)
1072        .local_get(dest_addr)
1073        .local_get(tmp_i32)
1074        .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 })
1075        // Load the 8-byte counter from src+4 (address 4) and store it
1076        .i32_const(4)
1077        .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
1078        .local_set(tmp_i64)
1079        .local_get(dest_addr)
1080        .i32_const(4)
1081        .binop(BinaryOp::I32Add)
1082        .local_get(tmp_i64)
1083        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
1084        // Increment log_size
1085        .global_get(vars.log_size)
1086        .i32_const(1)
1087        .binop(BinaryOp::I32Add)
1088        .global_set(vars.log_size);
1089
1090    builder.finish(vec![offset, src, size], &mut m.funcs)
1091}
1092
1093/// Create __get_profiling that reads from heap memory instead of stable memory
1094fn make_heap_getter(m: &mut Module, vars: &Variables, leb: FunctionId, _config: &Config) {
1095    let memory = get_memory_id(m);
1096    let arg_size = get_ic_func_id(m, "msg_arg_data_size");
1097    let arg_copy = get_ic_func_id(m, "msg_arg_data_copy");
1098    let reply_data = get_ic_func_id(m, "msg_reply_data_append");
1099    let reply = get_ic_func_id(m, "msg_reply");
1100    let trap = get_ic_func_id(m, "trap");
1101    let idx = m.locals.add(ValType::I32);
1102    let len = m.locals.add(ValType::I32);
1103    let next_idx = m.locals.add(ValType::I32);
1104    let log_addr = m.locals.add(ValType::I32);
1105    let mut builder = FunctionBuilder::new(&mut m.types, &[], &[]);
1106    builder.name("__get_profiling".to_string());
1107    #[rustfmt::skip]
1108    builder.func_body()
1109        // parse input idx
1110        .call(arg_size)
1111        .i32_const(11)
1112        .binop(BinaryOp::I32Ne)
1113        .if_else(
1114            None,
1115            |then| {
1116                then.i32_const(0)
1117                    .i32_const(0)
1118                    .call(trap);
1119            },
1120            |_| {},
1121        )
1122        .i32_const(0)
1123        .i32_const(7)
1124        .i32_const(4)
1125        .call(arg_copy)
1126        .i32_const(0)
1127        .load(memory, LoadKind::I32 { atomic: false }, MemArg { offset: 0, align: 4})
1128        .local_set(idx)
1129        // write header (vec { record { int32; int64 } }, opt int32)
1130        .i32_const(0)
1131        .i64_const(0x6c016d034c444944) // "DIDL036d016c"
1132        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
1133        .i32_const(8)
1134        .i64_const(0x02756e7401750002)  // "02007501746e7502"
1135        .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 })
1136        .i32_const(16)
1137        .i32_const(0x0200) // "0002"
1138        .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4})
1139        .i32_const(0)
1140        .i32_const(18)
1141        .call(reply_data)
1142        // if log_size - idx > MAX_ITEMS_PER_QUERY
1143        .global_get(vars.log_size)
1144        .local_get(idx)
1145        .binop(BinaryOp::I32Sub)
1146        .local_tee(len)
1147        .i32_const(MAX_ITEMS_PER_QUERY)
1148        .binop(BinaryOp::I32GtU)
1149        .if_else(
1150            None,
1151            |then| {
1152                then.i32_const(MAX_ITEMS_PER_QUERY)
1153                    .local_set(len)
1154                    .local_get(idx)
1155                    .i32_const(MAX_ITEMS_PER_QUERY)
1156                    .binop(BinaryOp::I32Add)
1157                    .local_set(next_idx);
1158            },
1159            |else_| {
1160                else_.i32_const(0)
1161                    .local_set(next_idx);
1162            },
1163        )
1164        .local_get(len)
1165        .call(leb)
1166        .i32_const(0)
1167        .i32_const(5)
1168        .call(reply_data)
1169        // Calculate log address: heap_base + METADATA_SIZE + idx * LOG_ITEM_SIZE
1170        .global_get(vars.heap_base)
1171        .i32_const(METADATA_SIZE)
1172        .binop(BinaryOp::I32Add)
1173        .local_get(idx)
1174        .i32_const(LOG_ITEM_SIZE)
1175        .binop(BinaryOp::I32Mul)
1176        .binop(BinaryOp::I32Add)
1177        .local_set(log_addr)
1178        // Reply with log data directly from heap
1179        .local_get(log_addr)
1180        .local_get(len)
1181        .i32_const(LOG_ITEM_SIZE)
1182        .binop(BinaryOp::I32Mul)
1183        .call(reply_data)
1184        // opt next idx
1185        .local_get(next_idx)
1186        .unop(UnaryOp::I32Eqz)
1187        .if_else(
1188            None,
1189            |then| {
1190                then.i32_const(0)
1191                    .i32_const(0)
1192                    .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4})
1193                    .i32_const(0)
1194                    .i32_const(1)
1195                    .call(reply_data);
1196            },
1197            |else_| {
1198                else_.i32_const(0)
1199                    .i32_const(1)
1200                    .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 1})
1201                    .i32_const(1)
1202                    .local_get(next_idx)
1203                    .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4})
1204                    .i32_const(0)
1205                    .i32_const(5)
1206                    .call(reply_data);
1207            },
1208        )
1209        .call(reply);
1210    let getter = builder.finish(vec![], &mut m.funcs);
1211    m.exports.add("canister_query __get_profiling", getter);
1212}
1213
1214// ============================================================================
1215// WASI import stubbing (for modules compiled with Emscripten/wasi-sdk)
1216// ============================================================================
1217
1218/// Replace WASI imports with stub functions that return 0 (success) or trap for proc_exit
1219fn stub_wasi_imports(m: &mut Module) {
1220    use walrus::FunctionBuilder;
1221
1222    // Find all WASI imports
1223    let wasi_imports: Vec<_> = m
1224        .imports
1225        .iter()
1226        .filter(|i| i.module == "wasi_snapshot_preview1")
1227        .filter_map(|i| {
1228            if let ImportKind::Function(func_id) = i.kind {
1229                Some((i.id(), i.name.clone(), func_id))
1230            } else {
1231                None
1232            }
1233        })
1234        .collect();
1235
1236    let memory = m.memories.iter().next().map(|mem| mem.id());
1237
1238    for (import_id, name, old_func_id) in wasi_imports {
1239        // Get the function type
1240        let func = m.funcs.get(old_func_id);
1241        let ty_id = func.ty();
1242        let ty = m.types.get(ty_id);
1243        let params: Vec<_> = ty.params().to_vec();
1244        let results: Vec<_> = ty.results().to_vec();
1245
1246        // Create stub function
1247        let mut builder = FunctionBuilder::new(&mut m.types, &params, &results);
1248        builder.name(format!("__wasi_{name}_stub"));
1249
1250        // Create locals for parameters
1251        let param_locals: Vec<_> = params.iter().map(|t| m.locals.add(*t)).collect();
1252
1253        match name.as_str() {
1254            "fd_write" => {
1255                // fd_write(fd: i32, iovs: i32, iovs_len: i32, nwritten: i32) -> i32
1256                // Write 0 to nwritten and return 0
1257                if let Some(mem) = memory {
1258                    if param_locals.len() >= 4 {
1259                        builder
1260                            .func_body()
1261                            .local_get(param_locals[3]) // nwritten ptr
1262                            .i32_const(0)
1263                            .store(
1264                                mem,
1265                                StoreKind::I32 { atomic: false },
1266                                MemArg {
1267                                    offset: 0,
1268                                    align: 4,
1269                                },
1270                            )
1271                            .i32_const(0);
1272                    } else {
1273                        builder.func_body().i32_const(0);
1274                    }
1275                } else {
1276                    builder.func_body().i32_const(0);
1277                }
1278            }
1279            "fd_read" => {
1280                // fd_read(fd: i32, iovs: i32, iovs_len: i32, nread: i32) -> i32
1281                // Write 0 to nread and return 0
1282                if let Some(mem) = memory {
1283                    if param_locals.len() >= 4 {
1284                        builder
1285                            .func_body()
1286                            .local_get(param_locals[3]) // nread ptr
1287                            .i32_const(0)
1288                            .store(
1289                                mem,
1290                                StoreKind::I32 { atomic: false },
1291                                MemArg {
1292                                    offset: 0,
1293                                    align: 4,
1294                                },
1295                            )
1296                            .i32_const(0);
1297                    } else {
1298                        builder.func_body().i32_const(0);
1299                    }
1300                } else {
1301                    builder.func_body().i32_const(0);
1302                }
1303            }
1304            "fd_seek" => {
1305                // fd_seek(fd: i32, offset: i64, whence: i32, newoffset: i32) -> i32
1306                // Write 0 to newoffset and return 0
1307                if let Some(mem) = memory {
1308                    if param_locals.len() >= 4 {
1309                        builder
1310                            .func_body()
1311                            .local_get(param_locals[3]) // newoffset ptr
1312                            .i64_const(0)
1313                            .store(
1314                                mem,
1315                                StoreKind::I64 { atomic: false },
1316                                MemArg {
1317                                    offset: 0,
1318                                    align: 8,
1319                                },
1320                            )
1321                            .i32_const(0);
1322                    } else {
1323                        builder.func_body().i32_const(0);
1324                    }
1325                } else {
1326                    builder.func_body().i32_const(0);
1327                }
1328            }
1329            "fd_close" => {
1330                // fd_close(fd: i32) -> i32
1331                // Just return 0 (success)
1332                builder.func_body().i32_const(0);
1333            }
1334            "environ_sizes_get" => {
1335                // environ_sizes_get(count: i32, buf_size: i32) -> i32
1336                // Write 0 to both pointers and return 0
1337                if let Some(mem) = memory {
1338                    if param_locals.len() >= 2 {
1339                        builder
1340                            .func_body()
1341                            .local_get(param_locals[0]) // count ptr
1342                            .i32_const(0)
1343                            .store(
1344                                mem,
1345                                StoreKind::I32 { atomic: false },
1346                                MemArg {
1347                                    offset: 0,
1348                                    align: 4,
1349                                },
1350                            )
1351                            .local_get(param_locals[1]) // buf_size ptr
1352                            .i32_const(0)
1353                            .store(
1354                                mem,
1355                                StoreKind::I32 { atomic: false },
1356                                MemArg {
1357                                    offset: 0,
1358                                    align: 4,
1359                                },
1360                            )
1361                            .i32_const(0);
1362                    } else {
1363                        builder.func_body().i32_const(0);
1364                    }
1365                } else {
1366                    builder.func_body().i32_const(0);
1367                }
1368            }
1369            "environ_get" => {
1370                // environ_get(environ: i32, environ_buf: i32) -> i32
1371                // Just return 0 (no environment variables)
1372                builder.func_body().i32_const(0);
1373            }
1374            "proc_exit" => {
1375                // proc_exit(code: i32) -> !
1376                // Trap unconditionally
1377                builder.func_body().unreachable();
1378            }
1379            _ => {
1380                // Default: just return 0 for i32 result, or appropriate zero values
1381                for result in &results {
1382                    match result {
1383                        ValType::I32 => {
1384                            builder.func_body().i32_const(0);
1385                        }
1386                        ValType::I64 => {
1387                            builder.func_body().i64_const(0);
1388                        }
1389                        ValType::F32 => {
1390                            builder.func_body().f32_const(0.0);
1391                        }
1392                        ValType::F64 => {
1393                            builder.func_body().f64_const(0.0);
1394                        }
1395                        _ => {}
1396                    }
1397                }
1398            }
1399        }
1400
1401        let stub_func_id = builder.finish(param_locals, &mut m.funcs);
1402
1403        // Replace all calls to old_func_id with stub_func_id
1404        for (_, func) in m.funcs.iter_local_mut() {
1405            replace_calls_in_func(func, old_func_id, stub_func_id);
1406        }
1407
1408        // Remove the import
1409        m.imports.delete(import_id);
1410    }
1411}
1412
1413fn replace_calls_in_func(func: &mut LocalFunction, old_id: FunctionId, new_id: FunctionId) {
1414    let mut stack = vec![func.entry_block()];
1415    while let Some(seq_id) = stack.pop() {
1416        let mut builder = func.builder_mut().instr_seq(seq_id);
1417        for (instr, _) in builder.instrs_mut().iter_mut() {
1418            match instr {
1419                Instr::Call(Call { func }) if *func == old_id => {
1420                    *func = new_id;
1421                }
1422                Instr::Block(Block { seq }) | Instr::Loop(Loop { seq }) => {
1423                    stack.push(*seq);
1424                }
1425                Instr::IfElse(IfElse {
1426                    consequent,
1427                    alternative,
1428                }) => {
1429                    stack.push(*consequent);
1430                    stack.push(*alternative);
1431                }
1432                _ => {}
1433            }
1434        }
1435    }
1436}