1
//! Tests that span multiple context submodules. Each test verifies a
2
//! property of the assembled `CompileContext` produced by `new()` —
3
//! type registrations, helper-function registrations, and concrete-vs-
4
//! abstract heap-type accessors.
5

            
6
use super::CompileContext;
7
use crate::ast::EntityKind;
8
use wasm_encoder::{Function, HeapType, Instruction, ValType};
9

            
10
#[test]
11
1
fn test_ratio_type_registered() {
12
1
    let ctx = CompileContext::new().unwrap();
13
    // Type indices live in the typed `WasmIds` (poison `u32::MAX` means
14
    // unresolved); a real registered index is below the module's type count.
15
1
    assert!(ctx.ids.ty_ratio < ctx.type_count);
16
1
    assert!(ctx.ids.ty_i8_array < ctx.type_count);
17
1
}
18

            
19
#[test]
20
1
fn test_entity_types_registered() {
21
1
    let ctx = CompileContext::new().unwrap();
22
7
    for kind in [
23
1
        EntityKind::Account,
24
1
        EntityKind::Commodity,
25
1
        EntityKind::Transaction,
26
1
        EntityKind::Split,
27
1
        EntityKind::Tag,
28
1
        EntityKind::Price,
29
1
        EntityKind::SshKey,
30
1
    ] {
31
7
        let name = kind.type_name();
32
        // Every entity kind resolves to a registered struct-type index via the
33
        // exhaustive `ids.entity_type` match.
34
7
        assert!(
35
7
            ctx.ids.entity_type(kind) < ctx.type_count,
36
            "missing entity wasm struct: ${name}"
37
        );
38
        // Accessor returns a `(ref null $<kind>)` resolving to that index —
39
        // regression check that `entity_ref` and `ids.entity_type` stay in sync.
40
7
        let ty = ctx.entity_ref(kind);
41
7
        match ty {
42
7
            ValType::Ref(r) => assert!(r.nullable),
43
            _ => panic!("entity_ref for {name} must produce Ref ValType"),
44
        }
45
    }
46
1
}
47

            
48
#[test]
49
1
fn test_pair_type_registered() {
50
1
    let ctx = CompileContext::new().unwrap();
51
1
    assert!(
52
1
        ctx.ids.ty_pair < ctx.type_count,
53
        "missing $pair WasmGC type"
54
    );
55
1
    assert!(
56
1
        ctx.func_names.contains_key("pair_new"),
57
        "missing pair_new helper"
58
    );
59
1
}
60

            
61
#[test]
62
1
fn test_pair_ref_type_is_concrete_pair() {
63
1
    let ctx = CompileContext::new().unwrap();
64
1
    let pair_ref = ctx.pair_ref();
65
1
    match pair_ref {
66
1
        ValType::Ref(r) => {
67
1
            assert!(r.nullable);
68
1
            assert_eq!(r.heap_type, HeapType::Concrete(ctx.ids.ty_pair));
69
        }
70
        _ => panic!("expected Ref type"),
71
    }
72
1
}
73

            
74
#[test]
75
1
fn test_anyref_is_abstract_any() {
76
1
    let ctx = CompileContext::new().unwrap();
77
1
    match ctx.anyref() {
78
1
        ValType::Ref(r) => {
79
1
            assert!(r.nullable);
80
1
            assert_eq!(r.heap_type, HeapType::ANY);
81
        }
82
        _ => panic!("expected Ref type"),
83
    }
84
1
}
85

            
86
#[test]
87
1
fn test_ratio_helpers_registered() {
88
1
    let ctx = CompileContext::new().unwrap();
89
9
    for name in [
90
1
        "gcd",
91
1
        "ratio_new",
92
1
        "ratio_add",
93
1
        "ratio_sub",
94
1
        "ratio_mul",
95
1
        "ratio_div",
96
1
        "ratio_eq",
97
1
        "ratio_lt",
98
1
        "ratio_from_i64",
99
1
    ] {
100
9
        assert!(
101
9
            ctx.func_names.contains_key(name),
102
            "missing helper function: {name}"
103
        );
104
    }
105
1
}
106

            
107
#[test]
108
1
fn test_host_imports_registered() {
109
1
    let ctx = CompileContext::new().unwrap();
110
10
    for name in [
111
1
        "get_output_offset",
112
1
        "get_input_offset",
113
1
        "get_strings_offset",
114
1
        "get_input_entities_count",
115
1
        "get_timestamp",
116
1
        "generate_uuid",
117
1
        "write_string",
118
1
        "write_bytes",
119
1
        "symbol_resolve",
120
1
        "log",
121
1
    ] {
122
10
        assert!(
123
10
            ctx.func_names.contains_key(name),
124
            "missing host import: {name}"
125
        );
126
    }
127
1
}
128

            
129
#[test]
130
1
fn test_ratio_ref_type() {
131
1
    let ctx = CompileContext::new().unwrap();
132
1
    let ratio_ref = ctx.ratio_ref();
133
1
    match ratio_ref {
134
1
        ValType::Ref(r) => {
135
1
            assert!(r.nullable);
136
1
            assert_eq!(r.heap_type, HeapType::Concrete(ctx.ids.ty_ratio));
137
        }
138
        _ => panic!("expected Ref type"),
139
    }
140
1
}
141

            
142
/// Asserts every common `WasmIds` field resolved to a real index (not the
143
/// `u32::MAX` poison) in BOTH module modes. This is the regression that would
144
/// have caught the original `log`-missing-in-eval panic at construction time:
145
/// `log` is now declared in both modes, so `ids.log` is resolved in both.
146
2
fn assert_common_ids_resolved(ctx: &CompileContext) {
147
2
    let ids = &ctx.ids;
148
72
    for (label, idx) in [
149
2
        ("gcd", ids.gcd),
150
2
        ("ratio_new", ids.ratio_new),
151
2
        ("ratio_add", ids.ratio_add),
152
2
        ("ratio_sub", ids.ratio_sub),
153
2
        ("ratio_mul", ids.ratio_mul),
154
2
        ("ratio_div", ids.ratio_div),
155
2
        ("ratio_eq", ids.ratio_eq),
156
2
        ("ratio_lt", ids.ratio_lt),
157
2
        ("ratio_from_i64", ids.ratio_from_i64),
158
2
        ("ratio_to_i64", ids.ratio_to_i64),
159
2
        ("unit_singleton", ids.unit_singleton),
160
2
        ("unit_mul", ids.unit_mul),
161
2
        ("unit_negate", ids.unit_negate),
162
2
        ("unit_eq", ids.unit_eq),
163
2
        ("materialize_unit", ids.materialize_unit),
164
2
        ("commodity_add", ids.commodity_add),
165
2
        ("commodity_sub", ids.commodity_sub),
166
2
        ("commodity_mul", ids.commodity_mul),
167
2
        ("commodity_div", ids.commodity_div),
168
2
        ("commodity_mul_by_ratio", ids.commodity_mul_by_ratio),
169
2
        ("commodity_div_by_ratio", ids.commodity_div_by_ratio),
170
2
        ("commodity_neg", ids.commodity_neg),
171
2
        ("commodity_eq", ids.commodity_eq),
172
2
        ("commodity_lt", ids.commodity_lt),
173
2
        ("commodity_assert_atomic", ids.commodity_assert_atomic),
174
2
        ("commodity_new_with_term", ids.commodity_new_with_term),
175
2
        ("pair_new", ids.pair_new),
176
2
        ("string_eq", ids.string_eq),
177
2
        ("nomi_raise", ids.nomi_raise),
178
2
        ("log", ids.log),
179
2
        ("ty_i8_array", ids.ty_i8_array),
180
2
        ("ty_ratio", ids.ty_ratio),
181
2
        ("ty_pair", ids.ty_pair),
182
2
        ("ty_commodity", ids.ty_commodity),
183
2
        ("ty_unit_term", ids.ty_unit_term),
184
2
        ("ty_nomi_condition", ids.ty_nomi_condition),
185
2
    ] {
186
72
        assert_ne!(idx, u32::MAX, "WasmIds field {label} left unresolved");
187
    }
188
18
    for kind in [
189
2
        EntityKind::Account,
190
2
        EntityKind::Commodity,
191
2
        EntityKind::Transaction,
192
2
        EntityKind::Split,
193
2
        EntityKind::Tag,
194
2
        EntityKind::Price,
195
2
        EntityKind::SshKey,
196
2
        EntityKind::ReportNode,
197
2
        EntityKind::Condition,
198
2
    ] {
199
18
        assert_ne!(
200
18
            ids.entity_type(kind),
201
            u32::MAX,
202
            "entity_type({kind:?}) left unresolved"
203
        );
204
    }
205
2
}
206

            
207
#[test]
208
1
fn wasm_ids_fully_resolved_script_mode() {
209
1
    let ctx = CompileContext::new().unwrap();
210
1
    assert_common_ids_resolved(&ctx);
211
    // Script-mode-only env imports resolve; eval-only catch_each does not.
212
1
    assert!(ctx.ids.get_output_offset().is_ok());
213
1
    assert!(ctx.ids.get_input_offset().is_ok());
214
1
    assert!(ctx.ids.get_input_entities_count().is_ok());
215
1
    assert!(ctx.ids.nomi_catch_each().is_err());
216
1
}
217

            
218
#[test]
219
1
fn wasm_ids_fully_resolved_eval_mode() {
220
1
    let ctx = CompileContext::new_eval_with_host_fns(&[]).unwrap();
221
1
    assert_common_ids_resolved(&ctx);
222
    // Eval-mode-only catch_each resolves; script-only env imports do not.
223
1
    assert!(ctx.ids.nomi_catch_each().is_ok());
224
1
    assert!(ctx.ids.get_output_offset().is_err());
225
1
    assert!(ctx.ids.get_input_offset().is_err());
226
1
    assert!(ctx.ids.get_input_entities_count().is_err());
227
1
}
228

            
229
#[test]
230
1
fn host_fn_named_log_is_rejected_not_silently_rebound() {
231
    // A user `HostFnSpec` whose import_name collides with the reserved `env.log`
232
    // (or any built-in) must error at construction, not silently overwrite the
233
    // resolved index and misroute PRINT/DISPLAY or emit invalid wasm.
234
1
    let spec = crate::host_fn::HostFnSpec::new("user-log", "env", "log");
235
1
    let msg = match CompileContext::new_eval_with_host_fns(&[spec]) {
236
        Ok(_) => panic!("reserved-name collision must be rejected"),
237
1
        Err(e) => e.to_string(),
238
    };
239
1
    assert!(msg.contains("already registered"), "got: {msg}");
240
1
    assert!(msg.contains("log"), "got: {msg}");
241
1
}
242

            
243
#[test]
244
1
fn test_valid_wasm_with_ratio_helpers() {
245
1
    let mut ctx = CompileContext::new().unwrap();
246
1
    ctx.add_should_apply(CompileContext::default_should_apply(), usize::MAX);
247
1
    let mut f = Function::new([]);
248
1
    f.instruction(&Instruction::End);
249
1
    ctx.add_process(f);
250
1
    let wasm = ctx.finish();
251
1
    assert_eq!(&wasm[0..4], b"\0asm");
252
1
}