Skip to content
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

Rust Interop - Emitting type decorators in a target language. #2887

Closed
alexswan10k opened this issue May 17, 2022 · 7 comments
Closed

Rust Interop - Emitting type decorators in a target language. #2887

alexswan10k opened this issue May 17, 2022 · 7 comments

Comments

@alexswan10k
Copy link
Contributor

alexswan10k commented May 17, 2022

Following on from #2875, I wanted to discuss possible approaches to creating bindings for languages specifically which need type information. The approach will probably be relevant for any typed target language where there is asymmetry between source and target types, otherwise import would probably suffice.

So currently you can do simple operations by using emit to bridge the impedance mismatch between the F# domain and the underlying Rust domain. Import alone is not sufficient because you effecrively have to transform the parameters and return types as they are called.

    module Vec =
        [<Erase>]
        type VecT =
            [<Emit("$0.get_mut().push($1)")>]
            abstract Push: 'a -> unit
        [<Emit("MutCell::from(std::vec::Vec::new())")>]
        let create (): VecT = jsNative
        [<Emit("$1.get_mut().push($0)")>]
        let push item (vec: VecT) = jsNative
        [<Emit("{ $1.get_mut().append(&mut vec![$0]); $1 }")>]
        let append item (vec: VecT): VecT = jsNative

This lets you use Vec as if it was a struct with static methods (which is basically how Rust models instances).

let a = Subs.Vec.create()
a |> Vec.push 3 |> Vec.push 4

This is fine for simple cases, but Rust does not have the level of type inference that F# does, and at some point, you are probably going to want to pass a Vec (or anything you are abstracting) over a boundary (function parameter or add to record etc), which will require type annotation. Currently, this will break because there is no way to define how the type is represented in the target type system. It will output the dotnet type, which is nonsense.

pub fn (v: &IrrelevantNamespace.Vec<?>)  {
   ...
}

Now notice this is a particularly problematic example because not only is there no definition, but there is also no way to define generic parameters.

Currently, Emit does not work on instances I believe, but presumably, it could be extended to emit on constructor calls, and any instances that need type decorators could perhaps use a new attribute EmitType to instruct Fable on how to generate the type decorator

[<Erase, Emit("Rc::new(MutCell::new(std::vec::Vec::new()))"), EmitType("Rc<MutCell<std::vec::Vec<$0>>>")>]
type Vec<'a>() =
    [<Emit("(*$0).get_mut().push($1)")>]
    member x.Push a = jsNative
    [<Emit("$0.clone()")>]
    member x.Clone (): Vec<'a> = jsNative
...etc

My generated code might then look like this

pub fn (v: &Rc<MutCell<std::vec::Vec<i32>>>)  {
   ...
}

let v: Rc<MutCell<std::vec::Vec<i32>>> = Rc::new(MutCell::new(std::vec::Vec::new())) // type annotation not actually needed here but for illustration purposes

In the same way that Emit allows placeholders to exist for parameter variables, perhaps EmitType would be able to do the same for generic parameters. This would allow modelling of generic collections, among other things.

I imagine this is going to require changes to the core Fable AST as the Emit concept is baked right in. Is this feasible?

One final caveat. In rust a constructor is rrally just a function called new. This means it does not have to return the same type, it might be wrapped in a result, or there might be multiple constructors. With all this in mind, perhaps static method are the way to go. The type decorator problem remains though!

Previous discussions covering dart etc #2779

#2774 (comment)

@ncave
Copy link
Collaborator

ncave commented May 24, 2022

@alexswan10k What if we use Emit on a type to (just) emit the type, but otherwise keep everything as in the module Vec above (see OP at top)?

@alfonsogarciacaro What is your opinion on emitting custom types?

@alexswan10k
Copy link
Contributor Author

@ncave, good idea. I think that might work. this prevents needing to define new attributes and pull them through the AST. Semantically I think this makes sense too because you are declaring what should be emitted for a declared type.

@alfonsogarciacaro
Copy link
Member

@alexswan10k This is a good idea! I think it can work without many code changes (probably even without any change in the AST). I like it that it consistent with the general behavior of Emit so it won't add too much complexity for users when dealing with bindings.

Actually, it's already possible to emit code for constructors, you only need to place the attribute between the type name and the constructor arguments. So as @ncave says we probably don't need a new attribute, just implement the behavior for Emit when it decorates a type. We can even allow combination of Import and Emit, as this is already possible for method calls.

I can draft a PR for this using Dart so you can easily port it to Rust.

@alexswan10k
Copy link
Contributor Author

alexswan10k commented May 26, 2022

That sounds great @alfonsogarciacaro. I am happy to roll this out to the rust version although a working example would definitely help. I Imagine the entity would need to know about the emit attribute values? I guess this could even be done as language specific as you can extend the entity registration in the bespoke ICompiler implementation to carry extra data, or register as a complimentary dictionary or something. Ultimately it needs to be available when we do a type transform.

On the topic of imports i am still torn if we even need it in rust for bindings. All external crates can be directly accessed by their fully qualified namespace, so technically you do not need to import anything as long as the crate is registered in cargo.toml. That being said, this could remove noise, and importing traits can expand the available api surface of any type. Maybe this is a separate discussion though as trait representation in bindings is still unclear to me.

@alfonsogarciacaro
Copy link
Member

@alexswan10k I was thinking to add the emit case to isGlobalOrImportedEntity but this is actually tricky because it means the types must accept expressions which, at least for the Dart implementation, is not the case.

But implementing Emit for types in the last step (Fable2Dart here) was not difficult, please see #2899. Only a few notes:

  • Don't use Erase or Fable will use Any as the type, ignore class declarations with the Emit attribute in Fable2Rust instead.
  • If the constructor is not decorated with Emit, it will be resolved in FSharp2Fable stage, so it will reference the actual class as any other declared class.
  • For instance members $0 is the self reference but for static members (and the constructor) $0 is the first argument. We may need to add a new convention like $T for self-type reference if needed.
  • I haven't implemented the Import/Emit combination ($0 becomes the imported item then). But hopefully it shouldn't be too difficult. It's done for methods here:
    // Allow combination of Import and Emit attributes
    let callInfo =
    match tryGlobalOrImportedMember com Fable.Any memb with
    | Some importExpr -> { callInfo with Fable.ThisArg = Some importExpr }
    | _ -> callInfo

@alexswan10k
Copy link
Contributor Author

This is great. Many thanks @alfonsogarciacaro, I will see if I can get this working with Rust.

@alexswan10k
Copy link
Contributor Author

I think we have a solution for this going forward. Any objections if I close?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants