-
Notifications
You must be signed in to change notification settings - Fork 0
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
typeguide spike #20
base: rust
Are you sure you want to change the base?
typeguide spike #20
Conversation
It has also just crossed my mind that maybe create (new value) is not the place to be wrapping/unwrapping at all, although this does lead to some complexities around things that get returned from libcalls that are already wrapped such as strings. |
@alexswan10k First of all, I really appreciate your effort, you obviously spent a great deal of time thinking about this. Here are some random thoughts about it, in no particular order (and it's just an opinion, so don't put too much weight on it):
Again, just random thoughts, what do you think? Perhaps start (as you already did) with something that doesn't work and see what's the simplest way to make it work. |
Happy to help. I really feel that this has wings, and it is quite exciting to see it starting to come together. Just need to crack these damn low level issues and then I am sure it will become far easier. Ok, good ideas here. I am completely with you in that whatever we can push out of tracked state and into the AST would be hugely beneficial.
So I think where this is going to get difficult to statically analyze is actually with mutable values. What we would ideally like to do is one of the following:
Two other pain points are operations and pattern matching:
Saying all this though, it may well be that the mess I have got into is simply because we need to define mutables as their lowest common denominator which is statically derivable, the Rc<Cell>. This is kind of sub par though from a performance point of view, especially for trivial counters etc. Apologies for my incoherent stream of consciousness.. i guess this is just part of the process haha |
I don't think we lose any performance by keeping all mutable values everywhere uniformly wrapped in Cells. There is no panic with Cells, and numeric benchmarks with tight loops confirm that performance is good, so at least for now we can assume it's ok. So perhaps we don't need to track it in this case, since it doesn't leave scope.
Passing to function should always unwrap the cell value at call site (and it does, already, in I wonder if we can temporarily leave the perhaps complicated case of capturing mutables by closures for later, and just use your refactoring to clean up and simplify the other tracking. What do you think? |
I would love to but the problem is stuff still isn't working properly because this pattern breaks other assumptions we have made elsewhere. The double unwrap scenario seems to be the issue I just cannot work around any more, because I have attempted to pull out the inline wrapping code in Create and handle it as a post-process step. This is conflicting with the let bindings which also do the same op, but have no context of the inner expr. I had to add this to let bindings because of another assumptionelsewhere around returning mutable values, which breaks when you start wrapping them. On a side note the problem with the cell scenario is closures cannot clone cells, so they have to wrap them in I had a random idea yesterday. Rather than tear the whole program apart to change all the return statements, we could just track it on the compiler against the rust expr. You can get away with this because actual context only ever comes from a directly computed child. Let me try and push this approach a little further as this would theoretically give us a mechanism to permanently solve double clones, double wraps under certain edge cases, and a number of other frustrating inconsistencies that come about because context is assumed rather than known. |
Yes, that's what I meant by inspecting the generated Rust AST, something like this: let isCallExpr pathNames (expr: Rust.Expr): bool =
let pathExpr = mkGenericPathExpr pathNames None
match expr.kind with
| Rust.ExprKind.Call(callee, _) when callee = pathExpr -> true
| _ -> false
let isRcWrapped (value: Rust.Expr): bool =
value |> isCallExpr ["Rc";"from"]
let isCellWrapped (value: Rust.Expr): bool =
value |> isCallExpr ["Cell";"from"] (assuming values were created like this): let makeCallExpr pathNames (args: Rust.Expr list) =
let callee = mkGenericPathExpr pathNames None
mkCallExpr callee args
let makeRefValue (value: Rust.Expr) =
makeCallExpr ["Rc";"from"] [value]
let makeMutValue (value: Rust.Expr) =
makeCallExpr ["Cell";"from"] [value] |
Pulling stuff out of the rust AST is definitely worth keeping in mind, I am just not convinced we can get around a lot of the non-trivial edge cases, namely:
Just a quick update: Anyway, still not really sure where we go from here, but happy to keep pushing this if you think it is worth it. I don't want to be a blocker though so happy to park it and use as an idea spike or whatever. |
@alexswan10k Does keeping all mutable values uniformly wrapped in Cells help? They're not traveling across boundaries, except in mutable containers like arrays or records with mutable fields. |
@alexswan10k I guess I don't quite understand the actual problem with capturing mutable values, perhaps I can use some education. If Rust closures capture by reference (in this case, a reference to a cell with internal mutability), is that not what we want for some reason? Or am I completely off-base? |
So the problem in the closure example is you cannot just capture a Cell or a I should also add - closures in our implementation are capture by move, not by reference. This is the safest approach to get around the lifecycle problems you would have to deal with if you allowed non-owned references to be captured. |
8b18ae5
to
a7936ff
Compare
7ead1e3
to
88f0c4c
Compare
95f0008
to
c8b6491
Compare
03e6514
to
ab03971
Compare
c55ecba
to
e294395
Compare
9259fe7
to
1a1854a
Compare
e7afdaa
to
6ae8390
Compare
c237e91
to
291c69f
Compare
95acf2a
to
2c1fe37
Compare
43b31bb
to
573e28f
Compare
4453d24
to
122083b
Compare
31628b7
to
9cde400
Compare
96148c4
to
7c93b15
Compare
8e2fe54
to
4f8930b
Compare
8588ba5
to
8052758
Compare
Firstly, not ready yet, do not merge! (some tests are broken, although a lot of stuff does work). I am prepared to throw half of this away, but I think the new concepts are definitely progress and worth discussing. This all started when I tried to uncomment a seemingly innocuous closure test that captures a mutable value, and then tried to turn mutables into wrapped cells for capture purposes.
@ncave , I could do with some input really. This is kind of where I am going. The new concepts:
Herein lies the problem - I think we are missing a fundamental piece of the puzzle. The ScopedVarAttrs can obviously be pulled from the ScopedVarAttrs in the ident context if you are working with an ident. If you are working with a class/record field you can infer what the default is for that field. The issue is when you have hierarchies where multiple steps might call FitToGuide, it is not obvious, or perhaps even resolvable who has done what unwrapping and when. A simple example I am hitting up against is a let which defines a variable. If the let guides and the create value guides, you double wrap. The problem is you need it in both places, because let might call a function that emits an incompatible type (not a cell when you want a cell).
On a side note create value should presumably always wrap, and this is actually where we do it today, just very unstructured and ad-hoc. In this pr we can just throw the output through the guide fn, and tada, wrapped/cell/whatever.
A possible solution
What if every transform expression returned both an expression and a ScopedVarAttrs (it probably needs another name i guess). By doing this you could subsequently understand what you actually have after any expression transform. This would allow you to know not to double clone for example, or double unwrap a ref.
The problem with this is every trasnformExpr derivative changes its return signature, which is a massive ballache. An alternative pattern I was initially considering is how the Fable AST derives types - by effectively walking the subtree and pulling types out. This seemed cool in principle, but in practice I do not think the AST actually has all the info we need at this point, because it might just be an ident that actually points to something else (the context has been lost). This is what we need to track against each transformed expression (at least that is my conclusion):
Again apologies if I am holding you up with other stuff, feel free to just carry on if you need and I will have to just work around it. I am just finding the existing architecture has limits I am hitting up against which I am not sure we can solve without these extra pieces. As you can imagine fixing this has been non-trivial either as when you fix 1 thing you break 2 other things... Let me know anyway, happy to follow your lead on this.