Conversation
75628fc to
917c74e
Compare
|
def CB(x: i32) -> i32:
return 0
@blue
def make_adder(n: i32) -> type(CB):
def adder(x: i32) -> i32:
return x + n
return adder |
|
Talking with @antocuni, it is likely that |
antocuni
left a comment
There was a problem hiding this comment.
it's mostly good, but with a lot of nitpicks here and there that overall degrade code quality and should be fixed.
The biggest thing to solve is the change to SPyVM.make_fqn_const which is plainly wrong, see the inline comment.
| if d != fqn and d not in seen: | ||
| seen.add(d) | ||
| deps.append(d) | ||
| return deps |
There was a problem hiding this comment.
I think this deserves its own unit test.
See e.g.:
spy/spy/tests/test_backend_c.py
Lines 111 to 140 in 0f2192b
| 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});") |
There was a problem hiding this comment.
this works and it's good enough for this PR.
But note that it produces an incredibly ugly C type name.
This happens because function types get an FQN which depends on their "structural" shape. For example for a functype def(i32, i32) -> f64 the fqn is:
FQN('builtins::def[i32, i32, f64]')
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 builtins shorter, which maybe it's a good idea). The nice property of this is that if you compile two spy programs separately (e.g. a main executable and a lib) their function types are automatically compatible at the C level.
Random ideas for how to improve it:
- give a unique numering and call them
FuncType1,FuncType2, etc. They are more opaque, but complex function types will have unreadable names anyway. The problem is that separate compilation becomes harder because you need to agree on numbering. - do it at the spy level. Declare e.g. that
@functypedefines a NEW type with its own fqn:
@functype
def BinOp(x: int, y: int) -> int:
passHere, we would define a fresh functype whose fqn is mod::BinOp: the advantage is that the redshift output and the C output become very clear. The new type would have conversion functions to convert to and from def(int, int) -> int.
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.
| # 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] | ||
|
|
There was a problem hiding this comment.
this seems the wrong place where to put this test code, as it's not about out-of-tree at all.
I'd put it in vm/modules/_testing_helpers.py
| """) | ||
| assert mod.foo() == "hello from mymod" | ||
|
|
||
| def test_c_callback(self): |
There was a problem hiding this comment.
and these tests should go to test_functype.py
| 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" |
There was a problem hiding this comment.
if you want to simplify this test, you can probably just assert w_CB.name == 'def (i32, i32) -> i32'.
| 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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
these have nothing to do with func types.
Typechecking function calls is already tested elsewhere.
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) |
There was a problem hiding this comment.
this is an interesting case, we can keep it.
But expect_errors should be MORE precise and check that we get a reasonable explanation/error annotation.
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) | ||
| """) |
There was a problem hiding this comment.
kill, we already have an end-to-end test.
| if fqn in self.globals_w: | ||
| return fqn |
There was a problem hiding this comment.
this is VERY wrong and it's a code smell. Bad claude 😡.
Early in this function we do a reverse lookup:
fqn = self.reverse_lookup_global(w_val)
if fqn is not None:
return fqnso, if w_val is ALREADY in the globals, we returned the FQN.
If we reach this stage, it means that we need to add w_val to the globals, and the assert is a sanity check to ensure that for some reason the FQN doesn't already exist.
It turns out that the FQN builtins::functype is already taken by W_FuncType (see
function.py):
W_FuncType._w = W_Type.declare(FQN("builtins::functype"))
So the failing assert here was catching a real bug. This means that we should probably rename the @functype decorator.
@JeffersGlass
Yes, I think that now it works by chance, but we could make it working with @functype
def CB(x: int, y: int) -> float: ...Speaking of which, I'm not 100% sure about the name @def_functype
def CB(x: i32, y: i32) -> i32: ...Other possibilities:
|
|
My personal favorites among the names you listed @antocuni are |
This is a redo of #607 based on out-of-band discussion with @antocuni. In this version, we don't need a new type for C callbacks, we use the existing
W_FuncType, and make the minimum changes required to allow red functions to be passed around as arguments. The end goal is the same: to enable external modules to accept SPy functions as C callbacks. Calling a function passed as an argument is still not allowed in this PR.This PR adds a builtin
@functypedecorator to make it easier to define a new function type, so you can write stuff like:(This PR was created with AI assistance.)