Pangoraw / WasmCompiler.jl

:jigsaw: it's just bytes
7 stars 0 forks source link

Create signals based reactivitiy (like leptos.rs) #1

Open Dale-Black opened 1 month ago

Dale-Black commented 1 month ago

Can you see a potential path for being able to compile Function types using WASM? I am interested in creating a Signals based reactive frontend, similar to leptos.rs but both WasmCompiler.jl and WebAssemblyCompiler.jl do not allow for this

Something like:

# ╔═╡ daa402d1-b23d-499d-af9b-bee91c31a4ed
md"""
## Basic Signals
"""

# ╔═╡ dbe725ba-ef60-4e2e-b6e3-dcd1109f2f6a
begin
    mutable struct Signal{T}
        _value::T
        subscribers::Vector{Function}
    end

    function Base.getproperty(s::Signal, name::Symbol)
        if name === :value
            return getfield(s, :_value)
        else
            return getfield(s, name)
        end
    end

    function Base.setproperty!(s::Signal, name::Symbol, value)
        if name === :value
            setfield!(s, :_value, value)
            notify(s)
        else
            setfield!(s, name, value)
        end
    end
end

# ╔═╡ 06d8be6e-bbf0-4fe1-8a1a-9515ee8945c2
begin
    function notify(s::Signal)
        for subscriber in s.subscribers
            subscriber(s._value)
        end
    end

    function subscribe(s::Signal, subscriber::Function)
        push!(s.subscribers, subscriber)
    end

    function create_signal(initial_value)
        return Signal(initial_value, Function[])
    end
end

# ╔═╡ e3f50388-e8ce-4ee2-b7af-9660f1fb023a
md"""
This example demonstrates the reactivity of the signal. Whenever the value of my_signal is updated, the subscribed function is automatically called, printing the updated value. This showcases how the signal can be used to trigger reactions and update values in a reactive manner.

You can extend this example further by creating more complex subscribers or using the signal values to perform specific actions or update other parts of your Julia program.
"""

# ╔═╡ 9cfd3651-dff8-4274-aa7d-1e0db80661d5
begin
    my_signal = create_signal("")

    subscribe(my_signal, x -> println("Value changed to: $x"))

    my_signal.value = "Hello, World!"
    my_signal.value = "Signals are reactive!"
    my_signal.value = "Updating the value again..."
end

But fails to compile like:

# ╔═╡ 269baa16-48c3-4941-8ae9-1ee1eecc0d30
function create_counter_signal()
    return create_signal(0)
end

# ╔═╡ b0b14f0f-eee6-4a6f-8b91-bf5bcf9fc6f2
@code_wasm create_counter_signal()

With this error:

compiling

create_counter_signal(): ErrorException("type Function cannot be represented in wasm")

Stack trace
Here is what happened, the most recent locations are first:

emit_func!(ctx::WasmCompiler.CodegenContext, types::Type) @ [compiler.jl:1378](https://github.com/Pangoraw/WasmCompiler.jl/tree/6f38c79e6f6e50cc5b2e79e371c597bd085a0550//src/compiler.jl#L1349)
emit_func! @ compiler.jl:1347
emit_func(f::Function, types::Type; debug::Bool) @ [compiler.jl:1345](https://github.com/Pangoraw/WasmCompiler.jl/tree/6f38c79e6f6e50cc5b2e79e371c597bd085a0550//src/compiler.jl#L1345)
emit_func @ compiler.jl:1345
macro expansion @ [This cell: line 114](http://localhost:1234/edit?id=f145b294-4dca-11ef-0369-a38da8221f0d#b0b14f0f-eee6-4a6f-8b91-bf5bcf9fc6f2)
[This cell: line 1](http://localhost:1234/edit?id=f145b294-4dca-11ef-0369-a38da8221f0d#b0b14f0f-eee6-4a6f-8b91-bf5bcf9fc6f2)
[@code_wasm create_counter_signal()](http://localhost:1234/edit?id=f145b294-4dca-11ef-0369-a38da8221f0d#b0b14f0f-eee6-4a6f-8b91-bf5bcf9fc6f2)
Pangoraw commented 1 month ago

The issue is the same as the one mentioned in https://github.com/tshort/WebAssemblyCompiler.jl/issues/17#issuecomment-2170996205. Basically, the function signature is unknown and therefore it is uncallable/unrepresentable.

I can see two workaround here:

  1. to have a custom ABI for calling functions to support varargs and emit a wrapper which calls the specialized function signature similarly to the virtual calling convention in http://wingolog.org/archives/2023/03/20/a-world-to-win-webassembly-for-the-rest-of-us and use call_indirect on func (param i32) (result i32)
  2. write a specialized struct whose calling signature is known and emit call_indirect values accordingly. I played around having a special wasmcall intrinsic which allows users to inject custom wasm in the compiled module, here we would have call_indirect instead: https://github.com/Pangoraw/WasmCompiler.jl/blob/6f38c79e6f6e50cc5b2e79e371c597bd085a0550/llama.jl#L16-L17

The remaining problem would be to know which functions can be called since we donot want to compile the entire Base.