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
Add user function definition support #2624
Add user function definition support #2624
Conversation
1b1c288
to
83f7c9b
Compare
208972c
to
d5b7bad
Compare
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.
Looks great, the implementation is pretty straightforward. Just a couple of comments.
I'd advocate for merging this before implementing support for function calls.
94dd1d4
to
b5b6c32
Compare
Resolved comments and rebased. |
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.
Love the tests!
The one main structural comment I've got is that something like checking all paths return a value should come under semantic analysis rather than codegen. Codegen should be able to assume that what it's been given is valid, and not worry about checking things itself.
Slightly off topic: We need to start caring more about the performance of the parser. Our approach previously was to not worry because it's not on the tracing hot-path, but the unit tests taking multiple seconds to run indicates that we've taken this too far. I did a quick profile and we're spending a lot of our time just copying strings around. I left a few comments on the new changes here, but we'll need to revisit the rest of ast.h at some point.
src/lexer.l
Outdated
\n buffer += '\n'; loc.lines(1); loc.step(); | ||
} | ||
<STRUCT,BRACE>{ | ||
"*"|")"|","|{space}+"$" { |
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.
Why are the space changes needed in lexer.l (this line and also in the AFTER_COLON
section)? Why is space handled differently in this section?
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.
AFTER_COLON
is an %x
section, IIUC the usual rules do not apply there. Here the space handling should be a part of the logic ensuring struct test $x
(for example) is parsed correctly.
I can take a closer look at it if that did not make much sense, that's what I remember from writing this code.
Going to set aside some time this weekend to take a look |
Thank you! I had to write thorough tests, the area of possibility of bugs here is almost unlimited :D
I also thought that initially, however, checking if all paths return a value is actually not semantic analysis, but control flow analysis, since semantic analysis operates on the AST, while this checks for a property of the CFG. Implementing it in semantic analyser would require to construct the control flow inside the AST, which would duplicate work that is already done in codegen.
Good point. |
In languages with more complex control flow (e.g. I feel like something similar to this pseudo-code should be able to detect missing return statements:
|
tests/semantic_analyser.cpp
Outdated
|
||
TEST(semantic_analyser, subprog_arguments) | ||
{ | ||
test("fn f(int64 $a): int64 { return $a; }", 0); |
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.
Sorry if this was discussed elsewhere already, but the argument syntax feels a bit odd to me.
The function syntax is fn <idents and stuff>: <ret type>
. It seems to me it would follow that argument syntax should also be <ident>: <type>
, but here it's reversed.
IIRC we also added type annotations for local variables as $var: type = <blah>
right?
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 posted the syntax in the RFC some time ago (#2516 (comment)), but I'm not against changing it - actually, it will make the lexer simpler, since currently it has to check separately if we are inside arguments or after a colon when dealing with structs (definition vs use, related to your comment above at #2624 (comment)).
$var: type = <blah>
syntax is not yet supported, #2560 did not add any new features; but it should be also handled by the AFTER_COLON
lexer logic once it is added.
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.
In that case I am in favor of $arg: type
syntax
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 wonder if this type syntax can be expanded to other cases, like:
kprobe:some_kernel_fn(a: int, b: int): int
Basically adding the option to specify function signatures in places (althoug with btf and dwarf that might be a bit late). In that case, i wonder if having ->
like rust,python will make parsing easier
I'd put something like this in its own pass. Is not semantic but also shouldn't go in the codegen. Passes should be fairly easy to add as all the boilerplate is handled (see e.g. nodecount.h). Might be a good usecase for the Might be good to do it in a separte PR to avoid some noise in this one |
That sounds like the best idea to me, that will avoid the check being in codegen and having it returning bool, and also complicating the code of semantic analyzer. Also, there is a direct benefit of doing it on the AST as @ajor and @danobi suggested: we can print the exact place where a return instruction should be and isn't, which will make it easier for the programmer.
|
The idea behind the passmanager and all the visitor stuff is that it should make new passes easy to add, to avoid increasing the complexity of existing passes, the semantic analyser is quite tricky in places. If you do want to try the dispather visitor, it will be the first usecase and its a bit hacky so it might not work that well :( Also, really nice work here :) |
Thank you all for the helpful comments, I feel like it cleared a lot of things up and will increase the quality of the PR a lot. |
b5b6c32
to
f8db42a
Compare
Resolved (hopefully) all of the comments. There are some minor issues with the code like leftover declarations, I will clean it after rebasing to current master. |
f8db42a
to
66635ae
Compare
66635ae
to
9ee2231
Compare
57b5954
to
bbe9804
Compare
Just fixed a small readability issue (moving check for void return value to |
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.
Thanks!
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.
This is really looking pretty good now, thanks!
I just found some bugs that we should fix before merging.
Crash using disallowed builtins in functions:
$ sudo ./bpftrace -e 'fn foo(): int64 { return func; }'
Segmentation fault
Crash writing to a map in a function:
$ sudo ./bpftrace -e 'fn bar(): void { @x = 1; }'
bpftrace: /home/ajor/src/bpftrace/src/ast/irbuilderbpf.cpp:444: void bpftrace::ast::IRBuilderBPF::CreateMapUpdateElem(llvm::Value*, bpftrace::ast::Map&, llvm::Value*, llvm::Value*, const bpftrace::location&): Assertion `ctx && ctx->getType() == getInt8PtrTy()' failed.
Aborted
Crash using map's value in a function:
$ sudo ./bpftrace -e 'fn bar(): uint64 { $res = @x + 1; return $res; } BEGIN { @x = 1; }'
bpftrace: /home/ajor/src/bpftrace/src/ast/irbuilderbpf.cpp:379: llvm::Value* bpftrace::ast::IRBuilderBPF::CreateMapLookupElem(llvm::Value*, bpftrace::ast::Map&, llvm::Value*, const bpftrace::location&): Assertion `ctx && ctx->getType() == getInt8PtrTy()' failed.
Aborted
Poor error message reading a map from a function (it'd be good if this could work):
$ sudo ./bpftrace -e 'fn bar(): int64 { return @x; } BEGIN { @x = 1; }'
stdin:1:20-29: ERROR: Function bar is of type int64, cannot return none
fn bar(): int64 { return @x; } BEGIN { @x = 1; }
~~~~~~~~~
FunctionPassManager fpm; | ||
FunctionAnalysisManager fam; | ||
llvm::PassBuilder pb; | ||
pb.registerFunctionAnalyses(fam); | ||
fpm.addPass(UnreachableBlockElimPass()); | ||
fpm.run(*func, fam); |
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.
Why is this code needed for subprograms specifically and not top-level probes?
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.
Unlike probes which have a default return value, subprograms might leave unreachable blocks that are not finished with a return instruction, these have to be removed for the IR to be correct.
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.
Just been thinking about this some more and I realise that I don't actually understand. How can a subprogram generate an unreachable block without a return instruction?
bbe9804
to
237f002
Compare
Rebased to solve merge conflicts. I also noticed another bug:
causing segfault for void functions. I'll fix that together with implementing the comments and expand codegen tests to cover such issues. |
f92b07f
to
6abd22b
Compare
Fixed maps and casts in subprogs. Also added more tests to cover this functionality. Subprog argument implementation in codegen was changed to include ctx as the first argument, which is needed for perf-event-based builtin functions (like print) to work properly. This was tested at runtime with the in-progress function call PR. With this all feedback should be resolved. |
Are there any outstanding work items left? Or are we missing a final round of reviews? Side note, this behavior is a bit unexpected:
I suspect this is more related to how we represent all ints as int64 internally. Not a show stopper -- IMO fixing this relaxes some rules so this is ok to do later. Also the error underline seems to be off. |
Also another bit of fix-later feedback:
Going with bpftrace's philosophy of brevity, perhaps good to allow |
No, just waiting for reviews.
I know about this, it's because types were not explicitly given anywhere previously (besides casts). Not sure why the underlining is off. |
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.
Sorry for taking so long to review this. Really appreciate the work you've put it!
The code's looking good to me - the comments are just a bunch of minor things that can be fixed later. Let's just get this one through without more delay!
The only issue I found was that we're allowing multiple functions to be defined with the same name. Again, this can be fixed later:
fn aaa(): void { print(1); }
fn aaa(): void { print(2); }
src/ast/passes/resource_analyser.cpp
Outdated
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.
There are a couple of places in this file where we do: resources_.probes_using_usym.insert(probe_);
I don't think it'll cause any problems to put a null pointer in that set, but it looks a bit odd and might cause confusion in the future. Would probably be safer to add a check on probe_
for these too
@@ -164,11 +164,11 @@ void SemanticAnalyser::builtin_args_tracepoint(AttachPoint *attach_point, | |||
} | |||
} | |||
|
|||
ProbeType SemanticAnalyser::single_provider_type(void) | |||
ProbeType SemanticAnalyser::single_provider_type(Probe *probe) |
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.
ProbeType SemanticAnalyser::single_provider_type(Probe *probe) | |
ProbeType SemanticAnalyser::single_provider_type(const Probe &probe) |
Nit: If a null value isn't allowed then use references instead of pointers
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rf-ptr-ref
@@ -1332,6 +1372,24 @@ void SemanticAnalyser::check_stack_call(Call &call, bool kernel) | |||
call.type = CreateStack(kernel, stack_type); | |||
} | |||
|
|||
Probe *SemanticAnalyser::get_probe_from_scope(Scope *scope, | |||
const location &loc, | |||
std::string name) |
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.
std::string name) | |
const std::string &name) |
ProbeType type = single_provider_type(probe); | ||
cast.type.SetAS(find_addrspace(type)); | ||
} else { | ||
// Assume kernel space for data in subprogs |
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.
This will need to be able to work with user space data in the future too
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.
Yes, the issue is this probably requires a new language feature, since in the context of a subprog, there is no way to tell if you are accessing userspace or kernel space data based on pointers alone.
[this](SubprogArg *arg) { return b_.GetType(arg->type); }); | ||
FunctionType *func_type = FunctionType::get(b_.GetType(subprog.return_type), | ||
arg_types, | ||
0); |
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.
0); | |
false); |
This function takes a boolean: https://llvm.org/doxygen/classllvm_1_1FunctionType.html#af8be7844c269f201ebcee1e15048c378
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.
Yeah I must have copied this from somewhere :D
@@ -879,7 +878,8 @@ void CodegenLLVM::visit(Call &call) | |||
expr_ = b_.CreateRegisterRead(ctx_, offset, call.func + "_" + reg_name); | |||
} else if (call.func == "printf") { | |||
// We overload printf call for iterator probe's seq_printf helper. | |||
if (probetype(current_attach_point_->provider) == ProbeType::iter) { | |||
if (!inside_subprog_ && |
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.
If an iterator probe calls a subprogram, does it not still count as being in an iterator probe?
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.
Currently subprogs do not track if they are called from an iterator probe so printf from subprog from iterator probe is treated like outside iterator probe. This should be probably fixed eventually, I will fix it in the follow-up PR for calls if it causes problems in tests.
void CodegenLLVM::createRet(Value *value) | ||
{ | ||
// If value is explicitly provided, use it | ||
if (value) { | ||
b_.CreateRet(value); | ||
return; | ||
} else if (inside_subprog_) { | ||
b_.CreateRetVoid(); |
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'm assuming this is just a temporary thing until your next PR?
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.
This is because unlike with probes, there is not any default return value for subprogs. createRet() without argument is only used to end unreachable code that is then removed in subprogs.
A new structure for defining subprograms is added on the same level as probes: fn <function_name>(<arg0> : <type0>, ...): <return_type> { <body> } The existing return statement is extended to also allow returning a specified value. SemanticAnalyser checks if the type is correct, while a new pass called ReturnPathAnalyser checks if all paths return a value for non-void functions. Features requiring accessing the probe they are used in are not supported in subprograms; using them throws an error at compilation.
Includes parser, codegen, semantic analyser, field analyser, resource analyser, and return path analyser tests.
6abd22b
to
f6c0ba1
Compare
The PR should be now rebased and ready for merge. |
Thanks @lenticularis39! |
A new structure for defining subprograms is added on the same level as probes:
The existing return statement is extended to also allow returning a specified value. SemanticAnalyser checks if the type is correct, while CodegenLLVM goes through the generated code to ensure all code flow paths return a value in a non-void function.
Only a limited number of builtins are allowed to be used inside functions due to current implementation restrictions; these can be addressed in later development.
This is the first step in implementing #2516 that can be either merged separately or together with the rest of the implementation. Note that this does not implement a way to call the functions nor a way to generate the correct BPF code for that.
(I'm opening this draft PR, since I believe there is quite a few non-trivial changes that it makes sense to review while I work on the rest.)
Checklist
Note: this checklist is only for the function definition part, excluding the BPF code generator and function calls.