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

Grain Runtime Tag Matching #2208

Open
spotandjake opened this issue Nov 21, 2024 · 0 comments
Open

Grain Runtime Tag Matching #2208

spotandjake opened this issue Nov 21, 2024 · 0 comments
Labels

Comments

@spotandjake
Copy link
Member

spotandjake commented Nov 21, 2024

Currently in grain we use Tags to identify the runtime type of variables in functions like toString, marshal, numberUtils this has a major problem of when we add a new type to the language we need to manually go to all these locations and update these which can lead to issues like #2184

My suggestion for fixing this is pattern matching basically we will introduce a new type to the compiler GrainRuntimeValue<a> though we can workshop the names, and then you can pattern match on the type like so:

module Main

primitive magic = "@magic"
// Note: We need to add a check to well_formed to ensure that users cannot create constructors with the runtime values, it won't do anything too bad but they won't print correctly because they are not implemented in toString and they won't pattern match correctly even though you could pass them to it.
// Note: if you alias one of the patterns it won't give the enum value back so were fine.
// Note: This could break gc currently especially if it were used with a stack type
// Note: We should probably make a way to relate all the tags to values so that we can maintain the mapping from a single place.
let main = (x: a) => { // This is actually safe now (Though we may want to tweak that)
  // This syntax was added with about 10 lines of code in builtin_types.re and 20 lines in matchcomp.re
  // This introduces no runtime complexity as the type is transparent at runtime
  // We can use the typevar info to not break garbage collection (I don't currently do this)
  match (magic(x): GrainRuntimeValue<a>) { // Currently using magic to do the type conversion
    GrainRuntimeString(s) => { // s is of type String
      print("String: " ++ toString(s)) // This could now call RuntimeString.toString(s) or something
    },
    GrainRuntimeInt32(s) => { // s is of type int32
      print("Int: " ++ toString(s))
    },
    _ => print("Unknown"),
  }
  ignore(x)
}
main("test")
main(1l)
main(1.0f)

Proof Of Concept

A very rough implementation that gets the code above working can be found here, this is based of my #1185 pr so I could make the pattern matching changes quicker though I think it would be easy to build off main without the changes in that pr.

The proof of concept is just meant to be rough while I think the underlying logic is the way we should do this as in an enum type a cast and new pattern matching logic, the current implementation does not make it easy to add new types as it requires updating the enum, and pattern matching code seperatly I think we could unify this into a tuple of (ident_name, tag) and then forEach over those for the logic. Some additional things that we need to change relate to the raw enum values as currently a user could initialize a GrainRuntimeString("s") itself which while this would not break any of the current runtime the pattern matching for that type would not be correct, additionally I think if you were to cast an unmanaged gc type to the enum value our gc would break a little as the compiler would think its managed, we should either make this type specifically unmanaged or have it take on the management properties of the type we are inheriting from, I think this change shouldn't be that hard.

Aliases, one thing I was watching while implementing this is what happens under various different types of pattern syntax, different types of destructuring, aliases and I think it all works as we do not add a new runtime type for this.

Unsafe, currently this approach is considered safe but I think the dynamics should be unsafe to keep users away from it. Additionally the current approach uses the magic keyword todo the cast I think we should add a new little runtime library with utilities like GrainValue.fromGrain(a) => GrainValue<A> and then GrainValue.toGrain(GrainValue<a>) => a to allow for explicit and searchable conversions this also let's us use the type variable a to preserve the type inference and initial type.

Design

The basic design try's to non invasively introduce safer runtime features. My initial design aims at being non invasive this is not a feature of the language as such we do not want to add a bunch of new code and surface area to support, as we are just doing a type cast and new logic this shouldn't be too heavily dependent on our underlying runtime implementation as long as we can use equality comparisons to check the runtime type (We need to see how this plays with wasm gc but I think it should be a non issue considering print will need the runtime type info anyways).

Design Considerations

I think this will work with wasm-gc but I think it really depends on how we end up deciding to represent the values in wasm-gc themselves, all we really need for this though is some sort of polymorphism which we will need for our print function anyways and a way to check the runtime type of the value.

Does this make the runtime to easy

Grain has powerful abstractions in regular code and there is really no need in grain for this type of runtime type checking in regular code. Currently we we discourage this low level runtime code by keeping it somewhat niche to use. Will adding this syntax makes things too easy / too high level and accidentally create a new user feature? I think that if we keep this limited to unsafe contexts, we should be fine one api change that might be worse discussing to further discourage this relates to the patterns themselves currently a match on GrainRuntimeValueString will contain a string maybe we make this return a pointer to the string which helps to discourage it still. Though we should certainly discuss this.
We want people writing high level grain code so we need to discuss weather this makes things to easy and would hurt the ecosystem.

Future thought

While I think we should focus on the core problem itself while implementing and designing this I think it is worth noting some things we could easily derive from this high level abstraction in its current form.

  • Monomorphization, If you were to have one of these patterns as the only expression in your function, it would not be very hard to monomorphize the code, we do not have the analysis or a phase that can handle this right now but the process is easy to visualize and this creates a good base for it in the future.
  • GrainValue creates a good type home for safer runtime interactions. Especially as we keep track of the initial type in the variable we could say something like GrainValue.getSimpleNumber(GrainValue<Number>) => WasmI32.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant