-
Notifications
You must be signed in to change notification settings - Fork 225
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
feat(experimental): try to infer lambda argument types inside calls #7088
base: master
Are you sure you want to change the base?
feat(experimental): try to infer lambda argument types inside calls #7088
Conversation
Changes to Brillig bytecode sizes
🧾 Summary (10% most significant diffs)
Full diff report 👇
|
Changes to number of Brillig opcodes executed
🧾 Summary (10% most significant diffs)
Full diff report 👇
|
Compilation Report
|
Execution Report
|
I didn't expect this to have any repercussion in SSA 😮 Lambdas that involve math operations somehow compile before this change without type annotations: let myarray: [i32; 3] = [1, 2, 3];
// Compiles fine without saying `n: i32`
assert(myarray.any(|n| n > 2)); I think it's because of the But in the end |
Execution Memory Report
|
Compilation Memory Report
|
It seems with this PR some functions get inlined right from the beginning, while in master some are not. I'm not sure why... but if this only happens for higher-order functions (maybe uncommon?) and if it leads to an optimization, maybe it's good. |
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.
Instead of unifying afterward we can push the type down and that should be sufficient. This is what Rust does as well since it uses a different algorithm called "bidirectional type inference" which splits up type checking functions into fn infer(T) -> Type
which we have, and fn check(T, Type)
which we don't have. Certain constructs like literals are always inferred, and others like lambdas are usually checked.
For our purposes though we can just use your existing check arg type function but pass down the expected type instead of the entire function type and argument index.
Some weeks ago I tried something similar to what I did here but I can't remember what (it didn't work so I deleted the branch). I thought I was assigning types instead of unifying, and getting errors like "can't find method foo for T" and that's why I tried unifying here. But it's probably the case that I had a bug or something else and that's why it wasn't working. I'll try pushing the type down and using it when the type is unspecified 👍 |
Hm, now we get a failure on code like this: struct U60Repr<let N: u32, let NumSegments: u32> {}
impl<let N: u32, let NumSegments: u32> U60Repr<N, NumSegments> {
fn new<let NumFieldSegments: u32>(_: [Field; N * NumFieldSegments]) -> Self {
U60Repr {}
}
}
fn main() {
let input: [Field; 6] = [0; 6];
let _: U60Repr<3, 6> = U60Repr::new(input);
} The error is this:
Looking at the types that are compares, these are the ones before this PR:
And these are the ones in this PR:
It seems the compiler is trying to solve the equation and that's why there's a division. I don't know if this is a bug in the unification code or if it's a bug introduced in this PR. |
So what I found is happening is... When checking Code: // Handle cases like `4 = a + b` by trying to solve to `a = 4 - b`
let new_type = InfixExpr(
Box::new(Constant(*value, kind.clone())),
inverse,
rhs.clone(),
);
new_type.try_unify(lhs, bindings)?;
Ok(()) That will eventually fail because of an // Check if the target id occurs within `this` before binding. Otherwise this could
// cause infinitely recursive types
if this.occurs(target_id) {
Err(UnificationError)
} else {
bindings.insert(target_id, (var.clone(), this.kind(), this.clone()));
Ok(())
} Maybe it's failing because we are binding these types multiple times now? Something that caught my attention is the new_type.try_unify(lhs, bindings)?;
Ok(()) That could just be: new_type.try_unify(lhs, bindings) I wonder if the intention of that code was trying to solve the math equation, but always returning If in the @jfecher Thoughts? |
@asterite we can't remove the occurs check, that'd break the check for infinite types like |
I see, thanks.
In the code before this PR, I think now it's checking |
I don't see anything wrong with that case aside from the fact the last
This looks incorrect to me since as you mentioned it'd require |
Coming back to this, I found that it's trying to unify a
So it's unifying What ends up happening, because of
is trying to unify Now I'm trying to see whre that |
The 6 / x * x is definitely a result of trying to solve the equation previously. It's unfortunate we can't just optimize that to I wonder if we added a version of division that tells the compiler to ignore rounding if that'd fix this. E.g. assuming the original is |
Yeah... that's what I tried at first but found that a test about that failed. I wonder where that I tried gating Some commits ago this PR worked, I guess because we didn't type-check as much as we do now (previously we'd only do it if there were lambdas). We could go back to that version, though I guess there would still be code that failed because of this if it was similar to the snippet that fails but also has a lambda argument in it (maybe uncommon). That is, I wonder if this bugs exists regardless of this feature and we could procede with this feature by only applying these changes if there are lambda arguments. |
That code is currently erroring on I wanted to try to call |
Description
Problem
Fixes #6802
Summary
I've been thinking for days how we could have lambda parameter types be "inferred" from the call they are being passed to.
The first thing that came to my mind is that lambdas are most commonly passed as callbacks after invoking a method on
self
, where eitherself
or a part ofself
is given to the lambda (likeOption::map
,BoundedVec::map
, etc.). So the first thing that I tried in this PR is to eagerly unify a method call's function type with the object type. Then, when elaborating a lambda as a method call argument we pass the potential parameter types of a function type that is in that call position:Then these types are unified, without erroring because later we'll check the type of the lambda against the argument anyway.
And... that worked! And that already covers a lot of cases.
Then I did the same thing for function calls, except that there's no
self
, but at least it now works if a callback has a concrete type, like if it's:And that worked too! Though I'm not sure there are many uses of that...
BUT: it didn't work in the code Nico shared in Slack, because it's a function call where the first argument given is like a self type, except that it's not a method all:
So the final thing I did was to eagerly try to unify argument types as we elaborate them against the target function type. And that made that example work!
It won't work if the lambda comes before the argument (which works in Rust) but I think that pattern is uncommon (though we could try to make it work in the future).
Additional Context
I don't know if this is the right way to approach this.
I also don't know if unifying eagerly would cause any issues. One thing that's not done here is using "unify_with_coercions", but given that we don't issue the errors that happens in these eager checks, maybe it's fine (maybe it won't work in cases where an array is automatically converted to a slice, though I guess we could make it work in the future).
One more thing: the changes in the stdlib and programs aren't really necessary, but I wanted to see if the code compiles with those changes... and in many cases the code is simplified a bit.
And finally: the code is not the best as I was just experimenting. We should clean it up.
Documentation
Check one:
PR Checklist
cargo fmt
on default settings.