-
Notifications
You must be signed in to change notification settings - Fork 62
allow red functions to be passed as arguments and return values #608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| from spy.backend.c.context import C_Type, Context | ||
| from spy.fqn import FQN | ||
| from spy.textbuilder import TextBuilder | ||
| from spy.vm.function import W_FuncType | ||
| from spy.vm.modules.unsafe.ptr import W_PtrType, W_RefType | ||
| from spy.vm.object import W_Type | ||
| from spy.vm.struct import W_StructType | ||
|
|
@@ -98,6 +99,8 @@ def emit_content(self) -> None: | |
| assert fqn == w_type.fqn # sanity check | ||
| if isinstance(w_type, W_StructType): | ||
| self.emit_StructType(fqn, w_type) | ||
| elif isinstance(w_type, W_FuncType): | ||
| self.emit_FuncType(fqn, w_type) | ||
| elif isinstance(w_type, W_PtrType): | ||
| self.emit_PtrType(fqn, w_type) | ||
| elif isinstance(w_type, W_RefType): | ||
|
|
@@ -155,6 +158,15 @@ def emit_StructType(self, fqn: FQN, w_st: W_StructType) -> None: | |
| tb.wl("};") | ||
| tb.wl("") | ||
|
|
||
| def emit_FuncType(self, fqn: FQN, w_ft: W_FuncType) -> None: | ||
| c_name = w_ft.fqn.c_name | ||
| c_ret = self.ctx.w2c(w_ft.w_restype) | ||
| if w_ft.params: | ||
| c_params = ", ".join(str(self.ctx.w2c(p.w_T)) for p in w_ft.params) | ||
| else: | ||
| c_params = "void" | ||
| self.tbh_fwdecl.wl(f"typedef {c_ret} (*{c_name})({c_params});") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this works and it's good enough for this PR. This happens because function types get an FQN which depends on their "structural" shape. For example for a functype And the C typedef is: typedef double (*spy_builtins$def__builtins$i32_builtins$i32_builtins$f64)(int32_t, int32_t);It's unclear how to improve it though (apart making Random ideas for how to improve it:
@functype
def BinOp(x: int, y: int) -> int:
passHere, we would define a fresh functype whose fqn is I kind of like option 2, but my only doubt is that most PLs do NOT treat function types as new types. The only exception seems to be go, from what I understand. |
||
|
|
||
| def emit_PtrType(self, fqn: FQN, w_ptrtype: W_PtrType) -> None: | ||
| c_ptrtype = C_Type(w_ptrtype.fqn.c_name) | ||
| w_itemT = w_ptrtype.w_itemT | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING | ||
| from typing import TYPE_CHECKING, Annotated | ||
|
|
||
| from spy.build.build_info import BuildInfo, BuildTarget, BuildType | ||
| from spy.vm.b import B | ||
| from spy.vm.function import FuncParam, W_Func, W_FuncType | ||
| from spy.vm.primitive import W_I32 | ||
| from spy.vm.registry import ModuleRegistry | ||
| from spy.vm.str import W_Str | ||
|
|
||
|
|
@@ -12,6 +15,10 @@ | |
|
|
||
| MODULE = ModuleRegistry("mymod") | ||
|
|
||
| # red def(i32) -> i32: a callback that takes one i32 and returns i32 | ||
| _w_cb_type = W_FuncType.new([FuncParam(B.w_i32, "simple")], B.w_i32) | ||
| CB = Annotated[W_Func, _w_cb_type] | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems the wrong place where to put this test code, as it's not about out-of-tree at all. |
||
|
|
||
| def build_info(target: BuildTarget, build_type: BuildType) -> BuildInfo: | ||
| return BuildInfo( | ||
|
|
@@ -24,3 +31,10 @@ def build_info(target: BuildTarget, build_type: BuildType) -> BuildInfo: | |
| @MODULE.builtin_func | ||
| def w_get_name(vm: "SPyVM") -> W_Str: | ||
| return vm.wrap("hello from mymod") | ||
|
|
||
|
|
||
| @MODULE.builtin_func | ||
| def w_run_callback(vm: "SPyVM", w_cb: CB, w_x: W_I32) -> W_I32: | ||
| # At interp level, w_cb is the W_ASTFunc itself. Call it directly. | ||
| assert isinstance(w_cb, W_Func) | ||
| return vm.fast_call(w_cb, [w_x]) # type: ignore[return-value] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,3 +37,43 @@ def foo() -> str: | |
| return get_name() | ||
| """) | ||
| assert mod.foo() == "hello from mymod" | ||
|
|
||
| def test_c_callback(self): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and these tests should go to |
||
| self.vm = SPyVM(extra_vm_modules=[str(MYMOD_PATH)]) | ||
| self.vm.path.append(str(self.tmpdir)) | ||
| mod = self.compile(""" | ||
| from mymod import run_callback | ||
|
|
||
| def double(x: i32) -> i32: | ||
| return x * 2 | ||
|
|
||
| def run() -> i32: | ||
| return run_callback(double, 21) | ||
| """) | ||
| assert mod.run() == 42 | ||
|
|
||
| def test_c_callback_blue_factory(self): | ||
| # A @blue function generates a specialised red callback per compile-time | ||
| # constant; each becomes a distinct C symbol that C can call back through. | ||
| self.vm = SPyVM(extra_vm_modules=[str(MYMOD_PATH)]) | ||
| self.vm.path.append(str(self.tmpdir)) | ||
| mod = self.compile(""" | ||
| from mymod import run_callback | ||
|
|
||
| @functype | ||
| def CB(x: i32) -> i32: | ||
| pass | ||
|
|
||
| @blue | ||
| def make_adder(n: i32) -> CB: | ||
| def adder(x: i32) -> i32: | ||
| return x + n | ||
| return adder | ||
|
|
||
| add5 = make_adder(5) | ||
| add10 = make_adder(10) | ||
|
|
||
| def run() -> i32: | ||
| return run_callback(add5, 1) + run_callback(add10, 1) | ||
| """) | ||
| assert mod.run() == 17 # (1+5) + (1+10) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| from spy.tests.support import CompilerTest, expect_errors, only_interp | ||
| from spy.vm.b import TYPES, B | ||
| from spy.vm.function import FuncParam, W_FuncType | ||
|
|
||
|
|
||
| class TestFuncType(CompilerTest): | ||
| @only_interp | ||
| def test_type_construction(self): | ||
| # After compiling a @functype-decorated def, the named variable holds | ||
| # the W_FuncType for that signature. | ||
| mod = self.compile(""" | ||
| @functype | ||
| def CB(a: i32, b: i32) -> i32: | ||
| pass | ||
| """) | ||
| w_CB = mod.w_mod.getattr("CB") | ||
| assert isinstance(w_CB, W_FuncType) | ||
| assert w_CB.w_restype is B.w_i32 | ||
| assert [p.w_T for p in w_CB.params] == [B.w_i32, B.w_i32] | ||
| assert w_CB.color == "red" | ||
| assert w_CB.kind == "plain" | ||
|
Comment on lines
+18
to
+21
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you want to simplify this test, you can probably just assert |
||
| # FQN puts params first, restype last | ||
| assert str(w_CB.fqn) == "builtins::def[i32, i32, i32]" | ||
|
|
||
| @only_interp | ||
| def test_type_identical_to_red_def(self): | ||
| # The W_FuncType from @functype must be the SAME interned object as a | ||
| # directly constructed W_FuncType with the same signature — so passing | ||
| # a matching red function where that type is expected works by identity. | ||
| mod = self.compile(""" | ||
| @functype | ||
| def CB(x: i32) -> i32: | ||
| pass | ||
| """) | ||
| w_CB = mod.w_mod.getattr("CB") | ||
| w_T_direct = W_FuncType.new( | ||
| [FuncParam(B.w_i32, "simple")], B.w_i32, color="red", kind="plain" | ||
| ) | ||
| assert w_CB is w_T_direct | ||
|
Comment on lines
+25
to
+39
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems a VERY defensive test, claude style :). |
||
|
|
||
| def test_functype_decorator(self): | ||
| self.compile(""" | ||
| @functype | ||
| def CB(x: i32, y: i32) -> i32: | ||
| pass | ||
|
|
||
| def apply(cb: CB, x: i32, y: i32) -> i32: | ||
| return x + y | ||
|
|
||
| def my_add(a: i32, b: i32) -> i32: | ||
| return a + b | ||
|
|
||
| def run() -> i32: | ||
| return apply(my_add, 3, 4) | ||
| """) | ||
|
Comment on lines
+41
to
+55
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really understand what this test is supposed to test. |
||
|
|
||
| def test_functype_at_call_site(self): | ||
| mod = self.compile(""" | ||
| @functype | ||
| def CB(x: i32, y: i32) -> i32: | ||
| pass | ||
|
|
||
| def apply(cb: CB, x: i32, y: i32) -> i32: | ||
| return x + y | ||
|
|
||
| def my_add(a: i32, b: i32) -> i32: | ||
| return a + b | ||
|
|
||
| def run() -> i32: | ||
| return apply(my_add, 3, 4) | ||
| """) | ||
| assert mod.run() == 7 | ||
|
Comment on lines
+57
to
+72
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and this seems to be the exact same test as above? |
||
|
|
||
| def test_signature_mismatch_wrong_ret(self): | ||
| src = """ | ||
| @functype | ||
| def CB(x: i32) -> bool: | ||
| pass | ||
|
|
||
| def my_fn(x: i32) -> i32: | ||
| return x | ||
|
|
||
| def bad() -> CB: | ||
| return my_fn | ||
| """ | ||
| errors = expect_errors("mismatched types") | ||
| self.compile_raises(src, "bad", errors) | ||
|
|
||
| def test_signature_mismatch_wrong_argcount(self): | ||
| src = """ | ||
| @functype | ||
| def CB(x: i32, y: i32) -> i32: | ||
| pass | ||
|
|
||
| def my_fn(x: i32) -> i32: | ||
| return x | ||
|
|
||
| def bad() -> CB: | ||
| return my_fn | ||
| """ | ||
| errors = expect_errors("mismatched types") | ||
| self.compile_raises(src, "bad", errors) | ||
|
|
||
| def test_signature_mismatch_wrong_arg_type(self): | ||
| src = """ | ||
| @functype | ||
| def CB(x: f64) -> i32: | ||
| pass | ||
|
|
||
| def my_fn(x: i32) -> i32: | ||
| return x | ||
|
|
||
| def bad() -> CB: | ||
| return my_fn | ||
| """ | ||
| errors = expect_errors("mismatched types") | ||
| self.compile_raises(src, "bad", errors) | ||
|
Comment on lines
+74
to
+117
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these have nothing to do with func types. Kill. |
||
|
|
||
| def test_blue_func_rejected(self): | ||
| # A @blue function's functype has a different FQN than a red functype, | ||
| # so passing it where a @functype type is expected fails with a type mismatch. | ||
| src = """ | ||
| @functype | ||
| def CB(x: i32) -> i32: | ||
| pass | ||
|
|
||
| @blue | ||
| def my_fn(x: i32) -> i32: | ||
| return x | ||
|
|
||
| def get_cb() -> CB: | ||
| return my_fn | ||
| """ | ||
| errors = expect_errors("mismatched types") | ||
| self.compile_raises(src, "get_cb", errors) | ||
|
Comment on lines
+119
to
+135
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is an interesting case, we can keep it. I have noticed multiple times that claude tends to be very bad at writing these tests. |
||
|
|
||
| def test_blue_factory(self): | ||
| # A @blue function can generate a red callback that captures compile-time | ||
| # constants. After redshifting, captures are inlined so each becomes a | ||
| # standalone C symbol. | ||
| self.compile(""" | ||
| @functype | ||
| def CB(x: i32) -> i32: | ||
| pass | ||
|
|
||
| @blue | ||
| def make_adder(n: i32) -> CB: | ||
| def adder(x: i32) -> i32: | ||
| return x + n | ||
| return adder | ||
|
|
||
| add5: CB = make_adder(5) | ||
| add10: CB = make_adder(10) | ||
| """) | ||
|
Comment on lines
+137
to
+154
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kill, we already have an end-to-end test. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -514,10 +514,9 @@ def make_fqn_const(self, w_val: W_Object) -> FQN: | |
| assert w_val.fqn not in self.globals_w | ||
|
|
||
| elif isinstance(w_val, W_Type): | ||
| # for now types are only builtin so they must have an unique fqn, | ||
| # we might need to change this when we introduce custom types | ||
| fqn = w_val.fqn | ||
| assert w_val.fqn not in self.globals_w | ||
| if fqn in self.globals_w: | ||
| return fqn | ||
|
Comment on lines
+518
to
+519
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is VERY wrong and it's a code smell. Bad claude 😡. fqn = self.reverse_lookup_global(w_val)
if fqn is not None:
return fqnso, if So the failing assert here was catching a real bug. This means that we should probably rename the |
||
| else: | ||
| w_T = self.dynamic_type(w_val) | ||
| T = w_T.fqn.human_name(self) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this deserves its own unit test.
See e.g.:
spy/spy/tests/test_backend_c.py
Lines 111 to 140 in 0f2192b