AssemblyScript / assemblyscript

A TypeScript-like language for WebAssembly.
https://www.assemblyscript.org
Apache License 2.0
16.72k stars 653 forks source link

Support custom types atop of primitive wasm types #2253

Open leighmcculloch opened 2 years ago

leighmcculloch commented 2 years ago

Something that would be useful for some applications of wasm modules is to be able to define types that are mapped to primitive wasm types. This is useful in cases where a functions input value is a u64 handle to something on the host side. This is also useful in cases when it is desirable to avoid use of the heap/linear-memory. The user defined type would have an underlying type of that primitive value on the stack, but it could have functions attached to it. From the wasm runtimes POV the type would just be a i64/i32.

This is possible today in Go and Rust that builds to wasm:

type Val uint64
func (v *Val) isX() bool {
    // call host function on v to find out if it is X
    return __host_is_x(uint64(v))
}
#[repr(transparent)]
pub struct Val(u64);
impl Val {
    fn is_x(&self) -> bool {
        // call host function on v to find out if it is X
        return __host_is_x(v.0)
    }

AssemblyScript already defines new types and makes them to wasm types, since it makes u64 to i64, and u32 to i32. This would be extending that capability to the user being able to define types that make to a primitive type.

To some degree this is already possible since TypeScript (and AssemblyScript) support type aliases. With type aliases we can say the following, which means Val is a u64.

type Val = u64;

However, using a type alias it is not possible to attach functions to the Val type, and there is no type safety, because it is truly just an alias and not a new type. Any u64 can be set on a variable of type Val without an explicit cast.

dcodeIO commented 2 years ago

For a bit of background, the compiler does something along these lines for the Number wrappers already, i.e. I8, I16, I32 etc. are attached to the basic types i8, i16, i32 etc. This works because TypeScript tooling can be informed that all these types are basically aliases of number, and there is the Number class attached to these in JS that can be mimicked. As such these wrappers are limited to the functionality provided by Number in JS. This same mechanism would, however, not work with custom methods, since TS tooling would not recognize these. The tricky part hence is to implement this in a TS-compatible way. Open to ideas :)

leighmcculloch commented 2 years ago

I think we could take advantage of the fact that TS supports extending primitive types like Number.

Consider the following example. Val can be created with any number value. Once it is created, the underlying number value being stored within is immutable. In TS this is assumedly an object stored on the heap, but it doesn't really matter in contexts where the TS is being run such as a browser. Functionally it is still immutable.

In the AS context, AS could optimize this type definition. It could identify that this class extends a primitive type, and has no other fields. Therefore, it can be treated as an on the stack i32/i64/f32/f64. Ideally it would be possible for the definition to be class Val extends u64 rather than Number, but I'm a bit fuzzy on if that is possible. Maybe it would need to be class Val extends U64?

class Val extends Number {
    isX(): boolean {
        return this.valueOf() == 1;
    }
}

let n1 = new Val(1);
let n2 = new Val(2);

console.log(n1.valueOf(), n1.isX())
console.log(n2.valueOf(), n2.isX())
console.log(n1.valueOf(), n1.isX())
console.log(n2.valueOf(), n2.isX())

Ref: TS Playground