tshort / WebAssemblyCompiler.jl

Create WebAssembly with Julia
https://tshort.github.io/WebAssemblyCompiler.jl/
MIT License
67 stars 4 forks source link

Trying to implement basic signals #17

Open Dale-Black opened 2 weeks ago

Dale-Black commented 2 weeks ago

I am trying to implement a basic signal-style reactivity (e.g. SolidJS) with Julia and WebAssemblyCompiler.jl

TLDR

Why is this function not compiling?

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

function create_counter_signal()
    return create_signal(0)
end

compile(
    (create_counter_signal,),
    filepath = "counter/counter.wasm",
    validate = true
)

Details

This is the signal which works as expected in pure Julia:

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

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

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.
"""

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

Which outputs:

Value changed to: Hello, World!
Value changed to: Signals are reactive!
Value changed to: Updating the value again...

Now, I want to create a simple Counter App using this but I am having trouble compiling one of the functions. Here is the full code:

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

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

function create_counter_signal()
    return create_signal(0)
end

function increment(count_signal::Signal{Int})
    count_signal.value += 1
    update_signal(count_signal)
    nothing
end

function update_signal(count_signal::Signal{Int})
    @jscall(
        "(id, src)=> document.getElementById(id).innerHTML=src",
        Nothing,
        Tuple{Externref,Externref},
        JS.object("counter"),
        """<p>Count: $(count_signal.value)</p>"""
    )
    nothing
end

compile(
    (create_counter_signal,),
    (increment, Signal{Int}),
    (update_signal, Signal{Int}),
    filepath = "counter/counter.wasm",
    validate = true
)

And specifically this line (create_counter_signal,), inside of the compile block errors like so:

MethodError: no constructors have been defined for Function

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

default(::Type{Function}) @ utils.jl:282
(::WebAssemblyCompiler.var"#84#181"{WebAssemblyCompiler.CompilerContext, Int64})(args::Vector{Any}) @ compile_block.jl:521
matchforeigncall(fun::WebAssemblyCompiler.var"#84#181"{WebAssemblyCompiler.CompilerContext, Int64}, node::Expr, sym::Symbol) @ utils.jl:88
compile_block(ctx::WebAssemblyCompiler.CompilerContext, cfg::Core.Compiler.CFG, phis::Dict{Int64, Any}, idx::Int64) @ compile_block.jl:517
(::WebAssemblyCompiler.var"#283#284"{WebAssemblyCompiler.CompilerContext, Dict{Int64, Any}, Ptr{WebAssemblyCompiler.LibBinaryen.Relooper}, Core.Compiler.CFG})(idx::Int64) @ 
iterate(::Base.Generator{Base.OneTo{Int64}, WebAssemblyCompiler.var"#283#284"{WebAssemblyCompiler.CompilerContext, Dict{Int64, Any}, Ptr{WebAssemblyCompiler.LibBinaryen.Relooper}, Core.Compiler.CFG}}) @ [generator.jl:47](https://github.com/JuliaLang/julia/tree/48d4fd48430af58502699fdf3504b90589df3852/base/generator.jl#L42)
collect(itr::Base.Generator{Base.OneTo{Int64}, WebAssemblyCompiler.var"#283#284"{WebAssemblyCompiler.CompilerContext, Dict{Int64, Any}, Ptr{WebAssemblyCompiler.LibBinaryen.Relooper}, Core.Compiler.CFG}}) @ [array.jl:834](https://github.com/JuliaLang/julia/tree/48d4fd48430af58502699fdf3504b90589df3852/base/array.jl#L827)
compile_method_body(ctx::WebAssemblyCompiler.CompilerContext) @ compiler.jl:145
compile_method(ctx::WebAssemblyCompiler.CompilerContext, funname::String; sig::Type, exported::Bool) @ compiler.jl:101
compile(::Tuple{typeof(Main.var"workspace#138".create_counter_signal)}, ::Vararg{Tuple}; filepath::String, jspath::String, validate::Bool, optimize::Bool, experimental::Bool, names::Nothing) @ compiler.jl:58
[This cell: line 1](http://localhost:1234/edit?id=353ab39a-29d7-11ef-0548-6d249aabf9fe#03b24a0d-7512-4920-b2ac-14c8500c0299)
compile(
    (create_counter_signal,),
Dale-Black commented 2 weeks ago

Let me know if you see what I am doing wrong. Thank you!

tshort commented 2 weeks ago

I don't think Vector{Function} can work. WebAssemblyCompiler supports vectors of concrete objects but not functions.

I've explored this problem area some in trying to get Makie and Observables to work. There, I tried to compile a set of Observables. The idea is to bake the Observables after they are set up. See here for more info. If you need truly dynamic signalling, I'm not sure how to tackle that.

tshort commented 2 weeks ago

Maybe you could move the signals to the JavaScript side.

Dale-Black commented 2 weeks ago

I'll have to look more into the observables examples and see if I can come up with something creative. Moving to JS seems like it might be more useful to use SolidJS at that point? I am looking at something like leptos in rust (which uses WASM to build a SolidJS style framework in pure rust) and trying to see how WebAssemblyCompiler.jl could be used to recreate it in Julia