Skip to content

Commit 814e336

Browse files
committed
test: lock in Phase 1 IfLet and Phase 2 codegen behaviour
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 7044637 commit 814e336

3 files changed

Lines changed: 344 additions & 0 deletions

File tree

tests/dune

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@
9090
(modules test_compound_index_assignment)
9191
(libraries kernelscript alcotest test_utils str))
9292

93+
(executable
94+
(name test_iflet)
95+
(modules test_iflet)
96+
(libraries kernelscript alcotest test_utils str))
97+
9398
(executable
9499
(name test_dynptr_bridge)
95100
(modules test_dynptr_bridge)
@@ -454,6 +459,7 @@
454459
test_map_operations.exe
455460
test_evaluator.exe
456461
test_compound_index_assignment.exe
462+
test_iflet.exe
457463
test_dynptr_bridge.exe
458464
test_global_var_ordering.exe
459465
test_string_to_array_unification.exe
@@ -590,6 +596,10 @@
590596
(alias runtest)
591597
(action (run ./test_compound_index_assignment.exe)))
592598

599+
(rule
600+
(alias runtest)
601+
(action (run ./test_iflet.exe)))
602+
593603
(rule
594604
(alias runtest)
595605
(action (run ./test_dynptr_bridge.exe)))

tests/test_compound_index_assignment.ml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,56 @@ var stats : hash<u32, Stats>(1024)
430430
(List.length (get_programs ir_multi_program) > 0);
431431
print_endline "✓ map[k].field += rhs compiles end-to-end"
432432

433+
(** Test 15: Codegen for `m[k].field op= rhs` produces the expected eBPF C
434+
shape. This locks in the Phase 2 codegen path:
435+
436+
(a) The synthetic pointer binding for the lowered IfLet is declared
437+
with a pointer type (`struct Stats* __cidx_field_N`) and is
438+
initialised from the lookup pointer directly — *not* via the
439+
deref-load statement-expression. A regression to the old shape
440+
produced a `struct Stats* x = ({ struct Stats __val = ...; __val; })`
441+
that fails clang -target bpf with a value-to-pointer mismatch.
442+
443+
(b) The body emits a presence-checked `ptr->field = ptr->field op rhs`
444+
using the underlying map lookup pointer.
445+
446+
(c) The field's type width matches the struct definition (u64) — i.e.
447+
the codegen does not default to u32 because the synthesized
448+
FieldAccess loses its `expr_type` annotation. *)
449+
let test_map_index_field_compound_codegen () =
450+
let source = {|
451+
struct Stats { count: u64, bytes: u64 }
452+
var stats : hash<u32, Stats>(1024)
453+
454+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
455+
stats[1].count += 1
456+
return XDP_PASS
457+
}
458+
|} in
459+
let ast = parse_string source in
460+
let (typed_ast, _) = type_check_and_annotate_ast_with_builtins ast in
461+
let symbol_table = Test_utils.Helpers.create_test_symbol_table ast in
462+
let ir_multi_program =
463+
Kernelscript.Ir_generator.generate_ir typed_ast symbol_table "probe" in
464+
let c = Kernelscript.Ebpf_c_codegen.generate_c_multi_program ir_multi_program in
465+
let contains s =
466+
try let _ = Str.search_forward (Str.regexp_string s) c 0 in true
467+
with Not_found -> false in
468+
(* (a) pointer-typed synthetic binding initialised from the lookup pointer *)
469+
check bool "synthetic binding declared as a struct pointer" true
470+
(contains "struct Stats* __cidx_field_");
471+
let bad_value_init = contains
472+
"struct Stats* __cidx_field_0 = ({ struct Stats __val" in
473+
check bool "synthetic binding does NOT use deref-load init" false bad_value_init;
474+
(* (b) presence-checked in-place mutation *)
475+
check bool "single map lookup" true (contains "bpf_map_lookup_elem(&stats");
476+
check bool "presence check" true (contains "!= NULL");
477+
check bool "ptr->count write" true (contains "->count =");
478+
(* (c) field width is u64, not the IRU32 default *)
479+
check bool "field access width is u64" true
480+
(contains "__u64 __field_access_");
481+
print_endline "✓ map[k].field += rhs codegen shape locked in"
482+
433483
let compound_index_assignment_tests = [
434484
"basic_parsing", `Quick, test_basic_parsing;
435485
"all_operators_parsing", `Quick, test_all_operators_parsing;
@@ -445,6 +495,7 @@ let compound_index_assignment_tests = [
445495
"ir_instruction_ordering", `Quick, test_ir_instruction_ordering;
446496
"end_to_end_compilation", `Quick, test_end_to_end_compilation;
447497
"map_index_field_compound_assignment", `Quick, test_map_index_field_compound_assignment;
498+
"map_index_field_compound_codegen", `Quick, test_map_index_field_compound_codegen;
448499
]
449500

450501
let () =

tests/test_iflet.ml

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
(*
2+
* Copyright 2026 Multikernel Technologies, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*)
16+
17+
(** Tests for the `if (var x = expr)` declaration-as-condition statement. *)
18+
19+
open Kernelscript.Ast
20+
open Kernelscript.Parse
21+
open Alcotest
22+
23+
let contains_substr str substr =
24+
try let _ = Str.search_forward (Str.regexp_string substr) str 0 in true
25+
with Not_found -> false
26+
27+
let typecheck source =
28+
let ast = parse_string source in
29+
let symbol_table = Test_utils.Helpers.create_test_symbol_table ast in
30+
let (typed_ast, _) =
31+
Kernelscript.Type_checker.type_check_and_annotate_ast
32+
~symbol_table:(Some symbol_table) ast in
33+
(ast, symbol_table, typed_ast)
34+
35+
let codegen_ebpf source =
36+
let (_ast, symbol_table, typed_ast) = typecheck source in
37+
let ir = Kernelscript.Ir_generator.generate_ir typed_ast symbol_table "test" in
38+
Kernelscript.Ebpf_c_codegen.generate_c_multi_program ir
39+
40+
let extract_first_stmt source =
41+
let ast = parse_string source in
42+
let attr_func =
43+
List.find (function AttributedFunction _ -> true | _ -> false) ast in
44+
match attr_func with
45+
| AttributedFunction af -> List.nth af.attr_function.func_body 0
46+
| _ -> failwith "no attributed function"
47+
48+
(** 1. Parse: bare `if (var x = ...)` produces an IfLet AST node. *)
49+
let test_parse_iflet_no_else () =
50+
let source = {|
51+
var counters : hash<u32, u64>(1024)
52+
53+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
54+
if (var c = counters[1]) {
55+
return XDP_DROP
56+
}
57+
return XDP_PASS
58+
}
59+
|} in
60+
let stmt = extract_first_stmt source in
61+
match stmt.stmt_desc with
62+
| IfLet (name, _, _, None) ->
63+
check string "binding name" "c" name
64+
| _ -> fail "expected IfLet without else"
65+
66+
(** 2. Parse: `if (var x = ...) { } else { }` round-trips with else. *)
67+
let test_parse_iflet_with_else () =
68+
let source = {|
69+
var counters : hash<u32, u64>(1024)
70+
71+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
72+
if (var c = counters[1]) {
73+
return XDP_DROP
74+
} else {
75+
return XDP_PASS
76+
}
77+
}
78+
|} in
79+
let stmt = extract_first_stmt source in
80+
match stmt.stmt_desc with
81+
| IfLet (_, _, _, Some _) -> ()
82+
| _ -> fail "expected IfLet with else"
83+
84+
(** 3. Parse: `else if (var ...)` chains via nested IfLet. *)
85+
let test_parse_iflet_else_iflet () =
86+
let source = {|
87+
var a : hash<u32, u64>(1024)
88+
var b : hash<u32, u64>(1024)
89+
90+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
91+
if (var x = a[1]) {
92+
return XDP_DROP
93+
} else if (var y = b[2]) {
94+
return XDP_PASS
95+
}
96+
return XDP_PASS
97+
}
98+
|} in
99+
let stmt = extract_first_stmt source in
100+
match stmt.stmt_desc with
101+
| IfLet (_, _, _, Some [{ stmt_desc = IfLet _; _ }]) -> ()
102+
| _ -> fail "expected outer IfLet whose else is a single IfLet"
103+
104+
(** 4. Type-check: struct-map binding succeeds; field access in body works. *)
105+
let test_typecheck_struct_binding () =
106+
let source = {|
107+
struct Stats { count: u64, bytes: u64 }
108+
var stats : hash<u32, Stats>(1024)
109+
110+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
111+
if (var s = stats[1]) {
112+
s.count = s.count + 1
113+
s.bytes = s.bytes + 100
114+
}
115+
return XDP_PASS
116+
}
117+
|} in
118+
let _ = typecheck source in
119+
()
120+
121+
(** 5. Type-check: scalar-map binding succeeds; value used as a value in body. *)
122+
let test_typecheck_scalar_binding () =
123+
let source = {|
124+
var counters : hash<u32, u64>(1024)
125+
126+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
127+
if (var c = counters[1]) {
128+
if (c > 100) {
129+
return XDP_DROP
130+
}
131+
}
132+
return XDP_PASS
133+
}
134+
|} in
135+
let _ = typecheck source in
136+
()
137+
138+
(** 6. Reject: binding referenced from the else-branch. *)
139+
let test_reject_binding_in_else () =
140+
let source = {|
141+
var counters : hash<u32, u64>(1024)
142+
143+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
144+
if (var c = counters[1]) {
145+
return XDP_PASS
146+
} else {
147+
var leaked : u64 = c
148+
}
149+
return XDP_PASS
150+
}
151+
|} in
152+
try
153+
let _ = typecheck source in
154+
fail "expected rejection of binding leak into else-branch"
155+
with
156+
| Kernelscript.Symbol_table.Symbol_error _ -> ()
157+
| Kernelscript.Type_checker.Type_error _ -> ()
158+
159+
(** 7. Reject: binding referenced after the if-statement (no outer shadow). *)
160+
let test_reject_binding_after_if () =
161+
let source = {|
162+
var counters : hash<u32, u64>(1024)
163+
164+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
165+
if (var c = counters[1]) {
166+
return XDP_PASS
167+
}
168+
var leaked : u64 = c
169+
return XDP_PASS
170+
}
171+
|} in
172+
try
173+
let _ = typecheck source in
174+
fail "expected rejection of binding leak past the if-statement"
175+
with
176+
| Kernelscript.Symbol_table.Symbol_error _ -> ()
177+
| Kernelscript.Type_checker.Type_error _ -> ()
178+
179+
(** 8. Codegen (struct map): single lookup + presence check + in-place mutation
180+
with no manual write-back. *)
181+
let test_codegen_struct_in_place () =
182+
let source = {|
183+
struct Stats { count: u64, bytes: u64 }
184+
var stats : hash<u32, Stats>(1024)
185+
186+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
187+
if (var s = stats[1]) {
188+
s.count = s.count + 1
189+
}
190+
return XDP_PASS
191+
}
192+
|} in
193+
let c = codegen_ebpf source in
194+
check bool "single map lookup" true (contains_substr c "bpf_map_lookup_elem(&stats");
195+
check bool "presence check" true (contains_substr c "!= NULL");
196+
check bool "in-place ptr->field write" true
197+
(contains_substr c "->count =");
198+
(* In-place mutation should mean no bpf_map_update_elem in the truthy branch.
199+
The else branch is omitted in the source, so there should be zero updates. *)
200+
let has_update =
201+
try let _ = Str.search_forward
202+
(Str.regexp_string "bpf_map_update_elem(&stats") c 0 in true
203+
with Not_found -> false in
204+
check bool "no manual write-back update" false has_update
205+
206+
(** 9. Codegen (scalar map): the binding holds the dereffed value, and the
207+
presence check uses the underlying lookup pointer. *)
208+
let test_codegen_scalar_value_binding () =
209+
let source = {|
210+
var counters : hash<u32, u64>(1024)
211+
212+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
213+
if (var c = counters[1]) {
214+
if (c > 100) {
215+
return XDP_DROP
216+
}
217+
}
218+
return XDP_PASS
219+
}
220+
|} in
221+
let c = codegen_ebpf source in
222+
check bool "scalar binding declared as value, not pointer" true
223+
(contains_substr c "__u64 c =");
224+
check bool "binding init uses the dereffed value statement-expression" true
225+
(contains_substr c "__val = *(");
226+
check bool "presence check on the underlying lookup pointer" true
227+
(contains_substr c "!= NULL")
228+
229+
(** 10. Codegen (struct map, end-to-end shape): the binding is declared with
230+
the value type (the type-checker auto-derefs `m[k]` to the struct
231+
value), but the field operations in the body lower to in-place
232+
mutation through the underlying lookup pointer rather than through
233+
the local. The local is therefore dead — clang elides it — but its
234+
declaration is still syntactically a value, not a pointer.
235+
236+
Concretely the previous codegen shape was, for user-written code:
237+
struct Stats* __map_lookup_N;
238+
__map_lookup_N = bpf_map_lookup_elem(&stats, &k);
239+
struct Stats s = ({ struct Stats __val = {0};
240+
if (__map_lookup_N) { __val = *(__map_lookup_N); }
241+
__val; });
242+
if (__map_lookup_N != NULL) {
243+
... __map_lookup_N->count = ... ;
244+
}
245+
Phase 2 only changed the synthetic-pointer-binding path (used by the
246+
lowered `m[k].field op= rhs`); user-written IfLet still produces the
247+
value-typed local above. Pinning that here so any future change to
248+
the typing rule is intentional. *)
249+
let test_codegen_struct_value_binding_shape () =
250+
let source = {|
251+
struct Stats { count: u64 }
252+
var stats : hash<u32, Stats>(1024)
253+
254+
@xdp fn probe(ctx: *xdp_md) -> xdp_action {
255+
if (var s = stats[1]) {
256+
s.count = s.count + 1
257+
}
258+
return XDP_PASS
259+
}
260+
|} in
261+
let c = codegen_ebpf source in
262+
check bool "binding declared with value type, not pointer" true
263+
(contains_substr c "struct Stats s =");
264+
check bool "value-typed binding uses deref-load init" true
265+
(contains_substr c "struct Stats __val");
266+
check bool "field write goes through the underlying lookup pointer" true
267+
(contains_substr c "->count =")
268+
269+
let suite = [
270+
"parse_iflet_no_else", `Quick, test_parse_iflet_no_else;
271+
"parse_iflet_with_else", `Quick, test_parse_iflet_with_else;
272+
"parse_iflet_else_iflet", `Quick, test_parse_iflet_else_iflet;
273+
"typecheck_struct_binding", `Quick, test_typecheck_struct_binding;
274+
"typecheck_scalar_binding", `Quick, test_typecheck_scalar_binding;
275+
"reject_binding_in_else", `Quick, test_reject_binding_in_else;
276+
"reject_binding_after_if", `Quick, test_reject_binding_after_if;
277+
"codegen_struct_in_place", `Quick, test_codegen_struct_in_place;
278+
"codegen_scalar_value_binding", `Quick, test_codegen_scalar_value_binding;
279+
"codegen_struct_value_binding_shape", `Quick, test_codegen_struct_value_binding_shape;
280+
]
281+
282+
let () =
283+
run "IfLet (declaration-as-condition)" [ "iflet", suite ]

0 commit comments

Comments
 (0)