Skip to content

Commit f8e956f

Browse files
authored
Merge pull request #7 from Point72/tkp/cov
Add additional test coverage, add fix for duplicate name validation
2 parents 22a3d74 + 874445f commit f8e956f

4 files changed

Lines changed: 1082 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ A compilation framework that transforms Python functions into native C-callable
1313

1414
- **Stateful variables** — declare persistent state with natural syntax that survives across calls with automatic lifecycle management (start/execute/stop)
1515
- **Standalone typed containers** — lists and dicts that work inside compiled functions without Numba's runtime overhead
16-
- **Extensible type resolution** — register custom types that map Python syntax to numba-compatible lowering
16+
- **Extensible type resolution** — register custom types that map Python syntax to numba-compatible lowering
1717
- **Plugin architecture** — all domain-specific behavior (input handlers, output handlers, AST transforms, type inference) is injected through registration APIs
1818

1919
The distribution is published as `numba-cfunc-compiler` and imported in Python as `numba_cfunc_compiler`.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import ast
2+
from typing import Annotated
3+
4+
import pytest
5+
from llvmlite import ir
6+
from numba.core import types
7+
8+
from numba_cfunc_compiler.compilation_context import CompilationContext
9+
from numba_cfunc_compiler.defaults import register_all
10+
from numba_cfunc_compiler.output_utils import (
11+
collect_return_nodes,
12+
parse_annotated_metadata_dict,
13+
validate_return_arity,
14+
validate_single_return,
15+
)
16+
from numba_cfunc_compiler.post_compilation import (
17+
CompilationOptions,
18+
_force_inline,
19+
_rename_exported_symbol,
20+
apply_post_compilation,
21+
link_ffi_bitcode,
22+
)
23+
from numba_cfunc_compiler.standalone.dict import StandaloneDictType
24+
from numba_cfunc_compiler.standalone.list import StandaloneListType
25+
from numba_cfunc_compiler.standalone.utils import (
26+
alloc_value_buffer,
27+
convert_to_i8,
28+
convert_to_i64,
29+
f64,
30+
get_llvm_type_for_numba_dtype,
31+
get_or_declare_function,
32+
i8,
33+
i8ptr,
34+
i32,
35+
i64,
36+
prepare_int_key_for_lookup,
37+
store_value_to_buffer,
38+
)
39+
from numba_cfunc_compiler.state_ast import (
40+
append_state_values_to_return,
41+
inject_state_params,
42+
is_state_annotation,
43+
state_annotation_target,
44+
)
45+
46+
47+
def unparse(node: ast.AST) -> str:
48+
ast.fix_missing_locations(node)
49+
return ast.unparse(node)
50+
51+
52+
def make_builder():
53+
module = ir.Module(name="helpers")
54+
func = ir.Function(module, ir.FunctionType(ir.VoidType(), []), name="test_func")
55+
block = func.append_basic_block("entry")
56+
return module, ir.IRBuilder(block)
57+
58+
59+
def test_output_utils_validate_returns_and_annotated_metadata():
60+
tree = ast.parse(
61+
"""
62+
def f(x):
63+
if x:
64+
return 1
65+
if x < 0:
66+
return (1, 2)
67+
return
68+
"""
69+
)
70+
returns = collect_return_nodes(tree)
71+
assert len(returns) == 2
72+
with pytest.raises(ValueError, match="returns 2 values"):
73+
validate_single_return(tree)
74+
with pytest.raises(ValueError, match="expects 1 output"):
75+
validate_single_return(ast.parse("def f():\n pass"))
76+
with pytest.raises(ValueError, match="annotation expects 3"):
77+
validate_return_arity(tree, 3)
78+
79+
single = ast.parse("def f():\n return 1")
80+
validate_single_return(single)
81+
validate_return_arity(ast.parse("def f():\n return 1, 2"), 2)
82+
83+
class SignalSet:
84+
pass
85+
86+
metadata = parse_annotated_metadata_dict(Annotated[SignalSet, {"bid": int}], SignalSet, "SignalSet", "Annotated[SignalSet, {'bid': int}]")
87+
assert metadata == {"bid": int}
88+
assert parse_annotated_metadata_dict(int, SignalSet, "SignalSet", "example") is None
89+
with pytest.raises(TypeError, match="must use SignalSet"):
90+
parse_annotated_metadata_dict(Annotated[int, {"bid": int}], SignalSet, "SignalSet", "example")
91+
with pytest.raises(TypeError, match="single dict metadata"):
92+
parse_annotated_metadata_dict(Annotated[SignalSet, ("bad",)], SignalSet, "SignalSet", "example")
93+
94+
95+
def test_state_ast_helpers_identify_and_rewrite_state_nodes():
96+
ann = ast.parse("state: State[int] = 1").body[0]
97+
assert is_state_annotation(ann)
98+
assert state_annotation_target(ann) == "state"
99+
assert not is_state_annotation(ast.parse("value: int = 1").body[0])
100+
with pytest.raises(ValueError, match="not a State"):
101+
state_annotation_target(ast.parse("value: int = 1").body[0])
102+
103+
func = ast.parse("def f(x):\n return x").body[0]
104+
inject_state_params(func, ["state_a", "state_b"])
105+
assert [arg.arg for arg in func.args.args] == ["x", "state_a", "state_b"]
106+
107+
assert unparse(append_state_values_to_return(ast.Return(value=ast.Constant(1)), ["s"])) == "return (1, s)"
108+
assert (
109+
unparse(append_state_values_to_return(ast.Return(value=ast.Tuple([ast.Constant(1), ast.Constant(2)], ast.Load())), ["s"]))
110+
== "return (1, 2, s)"
111+
)
112+
assert unparse(append_state_values_to_return(ast.Return(value=None), ["s"])) == "return (s,)"
113+
114+
115+
def test_post_compilation_symbol_rewrite_and_fallback_linking(caplog):
116+
assert _force_inline("attributes #0 = { noinline }") == "attributes #0 = { alwaysinline }"
117+
assert _rename_exported_symbol("define void @old(i8* %x) { ret void }", "old", "new").startswith("define void @new")
118+
same_ir = "define void @same() { ret void }"
119+
assert _rename_exported_symbol(same_ir, "same", "same") == same_ir
120+
with pytest.raises(ValueError, match="Failed to find"):
121+
_rename_exported_symbol("define void @other() { ret void }", "old", "new")
122+
123+
class Library:
124+
def get_llvm_str(self):
125+
return "define void @raw_name() { ret void }\nattributes #0 = { noinline }"
126+
127+
class CompiledFunc:
128+
native_name = "raw_name"
129+
_library = Library()
130+
131+
ir_text, exported = apply_post_compilation(CompiledFunc(), "abc123", CompilationOptions(force_inline=True))
132+
assert exported == "_gc_numba_abc123"
133+
assert "@_gc_numba_abc123" in ir_text
134+
assert "alwaysinline" in ir_text
135+
136+
module = ir.Module(name="bad_link")
137+
assert link_ffi_bitcode(module, b"not valid bitcode") is module
138+
assert "Failed to link FFI bitcode" in caplog.text
139+
140+
141+
def test_standalone_type_classes_and_llvm_utility_branches():
142+
with CompilationContext():
143+
register_all()
144+
list_type = StandaloneListType(types.int64)
145+
assert list_type.item_size == 8
146+
assert list_type.key is types.int64
147+
dict_type = StandaloneDictType(types.int64, types.float64)
148+
assert dict_type.key_size == 8
149+
assert dict_type.value_size == 8
150+
assert dict_type.key == (types.int64, types.float64)
151+
with pytest.raises(TypeError, match="Unsupported element type"):
152+
StandaloneListType(types.unicode_type)
153+
with pytest.raises(TypeError, match="Unsupported type"):
154+
StandaloneDictType(types.unicode_type, types.int64)
155+
156+
assert isinstance(i8(), ir.IntType) and i8().width == 8
157+
assert isinstance(i32(), ir.IntType) and i32().width == 32
158+
assert isinstance(i64(), ir.IntType) and i64().width == 64
159+
assert isinstance(i8ptr(), ir.PointerType)
160+
assert isinstance(f64(), ir.DoubleType)
161+
assert isinstance(get_llvm_type_for_numba_dtype(types.int64), ir.IntType)
162+
assert isinstance(get_llvm_type_for_numba_dtype(types.float64), ir.DoubleType)
163+
assert isinstance(get_llvm_type_for_numba_dtype(types.int8), ir.IntType)
164+
with pytest.raises(TypeError, match="Unsupported dtype"):
165+
get_llvm_type_for_numba_dtype(types.unicode_type)
166+
167+
168+
def test_standalone_llvm_builder_helpers_emit_expected_ir():
169+
module, builder = make_builder()
170+
fnty = ir.FunctionType(i64(), [])
171+
declared = get_or_declare_function(module, "external_func", fnty)
172+
assert get_or_declare_function(module, "external_func", fnty) is declared
173+
174+
small = ir.Constant(ir.IntType(32), 7)
175+
wide = ir.Constant(ir.IntType(64), 7)
176+
tiny = ir.Constant(ir.IntType(1), 1)
177+
byte = ir.Constant(ir.IntType(8), 1)
178+
assert convert_to_i64(builder, small).type.width == 64
179+
assert convert_to_i64(builder, wide) is wide
180+
assert convert_to_i8(builder, wide).type.width == 8
181+
assert convert_to_i8(builder, tiny).type.width == 8
182+
assert convert_to_i8(builder, byte) is byte
183+
184+
key_ptr, hash_val = prepare_int_key_for_lookup(builder, byte, types.int8)
185+
assert isinstance(key_ptr.type, ir.PointerType)
186+
assert hash_val.type.width == 64
187+
key_ptr, hash_val = prepare_int_key_for_lookup(builder, wide, types.int64)
188+
assert isinstance(key_ptr.type, ir.PointerType)
189+
assert hash_val is wide
190+
191+
val_ptr, val_char_ptr = alloc_value_buffer(builder, types.float64)
192+
assert isinstance(val_ptr.type, ir.PointerType)
193+
assert isinstance(val_char_ptr.type, ir.PointerType)
194+
int_val_ptr, int_val_char_ptr = store_value_to_buffer(builder, small, types.int64)
195+
assert isinstance(int_val_ptr.type, ir.PointerType)
196+
assert isinstance(int_val_char_ptr.type, ir.PointerType)
197+
bool_val_ptr, _ = store_value_to_buffer(builder, wide, types.int8)
198+
assert isinstance(bool_val_ptr.type, ir.PointerType)
199+
float_value = ir.Constant(ir.DoubleType(), 1.5)
200+
float_val_ptr, _ = store_value_to_buffer(builder, float_value, types.float64)
201+
assert isinstance(float_val_ptr.type, ir.PointerType)
202+
203+
builder.ret_void()
204+
assert "external_func" in str(module)

0 commit comments

Comments
 (0)