Verify pass: partial application of function with negative default argument expression causes core assertion error
Emil Bay reported a bug on Zulip, the following are the details of the bug:
The following pony code causes an asserion error during the verify pass:
primitive Foo
fun test(f: I8 = -1) => None
actor Main
new create(env: Env) =>
Foo~test()
The problem comes from the -1 part. Writing 1+1 also fails (or any other operation with integers).
Compiler output:
> ponyc examples/crash
Building builtin -> /.../dev/ponyc/packages/builtin
Building examples/crash/ -> /.../dev/ponyc/examples/crash
/Users/ryan/dev/ponyc/src/libponyc/type/lookup.c:556: lookup_base: Assertion `0` failed.
Backtrace functionality not available.
Abort trap: 6
Pony version:
> ponyc --version
0.39.0-08187e28 [debug]
Compiled with: LLVM 9.0.1 -- AppleClang-11.0.0.11000033-x86_64
Defaults: pic=true
Backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
* frame #0: 0x00007fff726e42c2 libsystem_kernel.dylib`__pthread_kill + 10
frame #1: 0x00007fff7279fbf1 libsystem_pthread.dylib`pthread_kill + 284
frame #2: 0x00007fff7264e6a6 libsystem_c.dylib`abort + 127
frame #3: 0x0000000100114e0d ponyc`ponyint_assert_fail(expr="0", file="/Users/ryan/dev/ponyc/src/libponyc/type/lookup.c", line=556, func="lookup_base") at ponyassert.c:65:3
frame #4: 0x00000001000f1341 ponyc`lookup_base(opt=0x00007ffeefbff4d8, from=0x000000010b55bbc0, orig=0x000000010b557280, type=0x000000010b557280, name="sub", errors=true, allow_private=false) at lookup.c:556:3
frame #5: 0x00000001000f0fd3 ponyc`lookup(opt=0x00007ffeefbff4d8, from=0x000000010b55bbc0, type=0x000000010b557280, name="sub") at lookup.c:563:10
frame #6: 0x00000001001010c9 ponyc`check_partial_function_call(opt=0x00007ffeefbff4d8, ast=0x000000010b55bb40) at call.c:31:40
frame #7: 0x0000000100100e5f ponyc`verify_function_call(opt=0x00007ffeefbff4d8, ast=0x000000010b55bb40) at call.c:136:7
frame #8: 0x00000001000d651d ponyc`pass_verify(astp=0x00007ffeefbfed80, options=0x00007ffeefbff4d8) at verify.c:536:31
frame #9: 0x00000001000b6fe3 ponyc`ast_visit(ast=0x00007ffeefbfed80, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:457:12
frame #10: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbfee10, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #11: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbfeea0, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #12: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbfef30, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #13: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbfefc0, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #14: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbff050, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #15: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbff0e0, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #16: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbff170, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #17: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbff200, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #18: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbff290, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #19: 0x00000001000b6f1c ponyc`ast_visit(ast=0x00007ffeefbff3c8, pre=0x0000000000000000, post=(ponyc`pass_verify at verify.c:523), options=0x00007ffeefbff4d8, pass=PASS_VERIFY) at pass.c:428:14
frame #20: 0x00000001000b7afa ponyc`visit_pass(astp=0x00007ffeefbff3c8, options=0x00007ffeefbff4d8, last_pass=PASS_ALL, out_r=0x00007ffeefbff393, pass=PASS_VERIFY, pre_fn=0x0000000000000000, post_fn=(ponyc`pass_verify at verify.c:523)) at pass.c:176:6
frame #21: 0x00000001000b753e ponyc`ast_passes(astp=0x00007ffeefbff3c8, options=0x00007ffeefbff4d8, last=PASS_ALL) at pass.c:275:7
frame #22: 0x00000001000b70e2 ponyc`ast_passes_program(ast=0x000000010c7bfd00, options=0x00007ffeefbff4d8) at pass.c:318:10
frame #23: 0x00000001000dcbaf ponyc`program_load(path="examples/crash/", opt=0x00007ffeefbff4d8) at package.c:934:7
frame #24: 0x0000000100001f1f ponyc`compile_package(path="examples/crash/", opt=0x00007ffeefbff4d8, print_program_ast=false, print_package_ast=false) at main.c:56:20
frame #25: 0x0000000100001e3f ponyc`main(argc=2, argv=0x00007ffeefbff5e8) at main.c:112:15
frame #26: 0x00007fff725a93d5 libdyld.dylib`start + 1
frame #27: 0x00007fff725a93d5 libdyld.dylib`start + 1Specifically, the problem happens when verifying the desugared call to Foo.test. The compiler tries to look up the method definition of neg, applied to the literal 1. This then fails inside lookup.c.
If we change the partial function to the following:
primitive Foo
fun test(f: Bool = not true) => None
Then the code works. It might have to do with the compiler not being to infer correctly what's going on when we assign to the default parameter.
Here's the AST output for the partial application call when using f: I8 = -0 and f: Bool = not true, to show the difference in inference:
For I8:
(funref
(0 [literal])
(id neg)
[funtype box x x (nominal (id $0) (id I8) x val x x)]
)
For Bool:
(funref
(true [nominal (id $0) (id Bool) x val x x])
(id op_not)
[funtype box x x (nominal (id $0) (id Bool) x val x x)]
)
The difference is that for I8, the compiler infers that 0 is a literal, while inferring that true is of type nominal. In this case, both should be nominal, as we can see in the AST for the definition of the functionf:
For I8:
(funref
(0 [nominal (id $0) (id I8) x val x x])
(id neg)
[funtype box x x (nominal (id $0) (id I8) x val x x)]
)
And for Bool:
(funref
(true [nominal (id $0) (id Bool) x val x x])
(id op_not)
[funtype box x x (nominal (id $0) (id Bool) x val x x)]
)
Wrapping the number literal in parenthesis makes the problem go away:
primitive Foo
fun test(f: I8 = -(1)) => None
The problem isn't with inference on numbers. using any non-negative number for the default works fine. It's the negative that is tripping this up.
Specifically, this is in the handling of call:
positive integer:
(params (param (id f) (nominal (id $0) (id I8) x val x x) (seq:scope 1)))
negative integer:
(params (param (id f) (nominal (id $0) (id I8) x val x x) (seq:scope (call (. 1 (id neg)) x x x))))
The change with -(1) that @ergl points out results in:
(params (param (id f) (nominal (id $0) (id I8) x val x x) (seq:scope (call (. (seq 1) (id neg)) x x x))))
So the issue is in call with it's handling of the literal rather than (seq 1) when combined with (id neg)
in partial_application in call.c in the expression pass we have:
// `$0.f`
BUILD_NO_DECL(call_receiver, ast,
NODE(TK_DOT,
NODE(TK_REFERENCE, ID(recv_name))
TREE(method)));
the ast_type(call_receiver) result goes boom eventually as the type comes back as a literal. Which is the boom, but its on the creation of call_receiver that the problem already exists.
At the refer pass, a normal call aka no partial application to the method gives us a
(seq:scope (call (. 1 (id neg)) x x x))))
but it works, so there's no real issue there. its something about either the creation in partial application or perhaps a bad code path for this neg later in verify.
After some more poking, I'm thinking its the lambda that gets created in partial_application and that the type of the call_receiver is a literal.
With a positive integer... call receiver type is returning NULL. Ditto for the -(1).
So the issue really appears to be call receiver having an ast_type.
So really this looks like that all the code from partial application on out doesn't expect a call to have a literal as a receiver but we do here and its a chain of pony_assert(0) all the way down across multiple passes for having a TK_LITERAL as the receiver.
We only end up going down this path with partial application, not a regular call.
@jemc do you have thoughts on the proper approach here?
backtrace w/ release ponyc.
frame #2: 0x00000000007963d2 ponyc`ponyint_assert_fail + 258
frame #3: 0x000000000073ac3d ponyc`lookup_base + 1293
frame #4: 0x000000000073a729 ponyc`lookup + 25
frame #5: 0x0000000000743ff4 ponyc`verify_function_call + 532
frame #6: 0x0000000000728175 ponyc`pass_verify + 85