Skip to content

Commit 07800ee

Browse files
Dale-Blackclaude
andcommitted
Add package compilation registry + Plotly mappings
Registry system: register_package_compilation! maps Julia package functions to JS code generators. Enables JST to compile calls like scatter(x=x, y=y) to {type:"scatter", x:x, y:y} instead of trying to compile the package's internal implementation. Includes: - Core.kwcall handling for keyword argument dispatch - _extract_kwargs: reads NamedTuple construction from IR - PlotlyBase mappings: scatter, bar, heatmap, Layout, plot - build_js_object helper for JS object literal generation - register_plotly_compilations! for any module with Plotly API Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 861b62b commit 07800ee

4 files changed

Lines changed: 292 additions & 0 deletions

File tree

src/JavaScriptTarget.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@ export compile, compile_module
77
export JSOutput
88
export build_inference_tables
99
export build_playground
10+
export register_package_compilation!, register_package_compilations!
11+
export lookup_package_compilation, build_js_object, build_js_object_from_kwargs
12+
export register_plotly_compilations!
1013

1114
# === Types ===
1215
include("compiler/types.jl")
1316

1417
# === IR Extraction ===
1518
include("compiler/ir.jl")
1619

20+
# === Package Registry ===
21+
include("compiler/packages.jl")
22+
include("compiler/packages_plotly.jl")
23+
1724
# === Code Generation ===
1825
include("compiler/codegen.jl")
1926

src/compiler/codegen.jl

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,69 @@ end
542542
"""
543543
Compile a :call expression (intrinsics, builtins, generic calls).
544544
"""
545+
# Extract keyword arguments from a NamedTuple SSA in unoptimized IR.
546+
# The IR pattern is: Core.apply_type(NamedTuple, (:x,:y,:mode)), Core.tuple(vals...), NamedTupleType(tuple)
547+
# Returns Dict{Symbol, String} mapping kwarg names to compiled JS expressions.
548+
function _extract_kwargs(ctx::JSCompilationContext, kwargs_ssa)
549+
kwargs = Dict{Symbol, String}()
550+
551+
if !(kwargs_ssa isa Core.SSAValue)
552+
return kwargs
553+
end
554+
555+
# The kwargs SSA points to a NamedTuple constructor call
556+
nt_stmt = ctx.code_info.code[kwargs_ssa.id]
557+
558+
# Handle (:=) wrapping
559+
if nt_stmt isa Expr && nt_stmt.head === :(=)
560+
nt_stmt = nt_stmt.args[2]
561+
end
562+
563+
# Pattern: (%apply_type_result)(%tuple_of_values)
564+
# Where apply_type_result = Core.apply_type(NamedTuple, (:x, :y, :mode))
565+
if !(nt_stmt isa Expr && nt_stmt.head === :call)
566+
return kwargs
567+
end
568+
569+
# Get the NamedTuple type (has field names) from the callee
570+
type_ssa = nt_stmt.args[1]
571+
values_ssa = length(nt_stmt.args) >= 2 ? nt_stmt.args[2] : nothing
572+
573+
# Resolve field names from the type
574+
field_names = Symbol[]
575+
if type_ssa isa Core.SSAValue
576+
type_type = ctx.code_info.ssavaluetypes[type_ssa.id]
577+
if type_type isa Core.Const && type_type.val isa DataType
578+
nt_type = type_type.val
579+
if nt_type <: NamedTuple
580+
field_names = collect(nt_type.parameters[1])
581+
end
582+
end
583+
end
584+
585+
if isempty(field_names) || values_ssa === nothing
586+
return kwargs
587+
end
588+
589+
# Resolve values from the tuple
590+
if values_ssa isa Core.SSAValue
591+
vals_stmt = ctx.code_info.code[values_ssa.id]
592+
if vals_stmt isa Expr && vals_stmt.head === :call
593+
vals_callee = vals_stmt.args[1]
594+
if vals_callee isa GlobalRef && vals_callee.name === :tuple
595+
vals = vals_stmt.args[2:end]
596+
for (i, name) in enumerate(field_names)
597+
if i <= length(vals)
598+
kwargs[name] = compile_value(ctx, vals[i])
599+
end
600+
end
601+
end
602+
end
603+
end
604+
605+
return kwargs
606+
end
607+
545608
# Compile Base.materialize(broadcasted_ssa) to JS .map() chains.
546609
# sin.(x) -> x.map(_b => Math.sin(_b)), x.*f -> x.map(_b => _b*f), etc.
547610
function _compile_broadcast_materialize(ctx::JSCompilationContext, bc_arg)
@@ -716,6 +779,16 @@ function compile_call(ctx::JSCompilationContext, expr::Expr)
716779
fn_name = string(nameof(resolved_fn))
717780
call_args = [compile_value(ctx, a) for a in args[2:end]]
718781

782+
# Check package registry for positional calls
783+
if resolved_fn isa Function
784+
fn_mod = parentmodule(resolved_fn)
785+
fn_sym = nameof(resolved_fn)
786+
compiler_fn = lookup_package_compilation(fn_mod, fn_sym)
787+
if compiler_fn !== nothing
788+
return compiler_fn(ctx, Dict{Symbol,String}(), call_args)
789+
end
790+
end
791+
719792
# Array creation: Float64[] → getindex(Float64) → []
720793
# Also handles array indexing: arr[i] → arr[i-1]
721794
if resolved_fn === Base.getindex
@@ -848,6 +921,49 @@ function compile_call(ctx::JSCompilationContext, expr::Expr)
848921
end
849922
end
850923

924+
# ─── Handle Core.kwcall (keyword argument function calls) ───
925+
# IR: Core.kwcall(NamedTuple{(:x,:y,:mode)}(vals...), func, pos_args...)
926+
# Check package registry for the target function
927+
if callee isa typeof(Core.kwcall) || (callee isa GlobalRef && callee.name === :kwcall && callee.mod === Core)
928+
if length(args) >= 3
929+
# args[2] = NamedTuple with kwargs, args[3] = function, args[4:end] = positional
930+
kwargs_ssa = args[2]
931+
func_ssa = args[3]
932+
pos_raw = args[4:end]
933+
934+
# Resolve the function being called
935+
func_type = nothing
936+
if func_ssa isa Core.SSAValue
937+
func_type = ctx.code_info.ssavaluetypes[func_ssa.id]
938+
elseif func_ssa isa GlobalRef
939+
func_type = try Core.Const(getfield(func_ssa.mod, func_ssa.name)) catch; nothing end
940+
end
941+
942+
if func_type isa Core.Const
943+
fn = func_type.val
944+
fn_mod = parentmodule(fn)
945+
fn_name = nameof(fn)
946+
947+
# Check package registry
948+
compiler_fn = lookup_package_compilation(fn_mod, fn_name)
949+
if compiler_fn !== nothing
950+
# Extract kwargs from NamedTuple construction
951+
kwargs = _extract_kwargs(ctx, kwargs_ssa)
952+
pos_args = [compile_value(ctx, a) for a in pos_raw]
953+
return compiler_fn(ctx, kwargs, pos_args)
954+
end
955+
end
956+
end
957+
# Fallback: compile as regular call (strip kwargs)
958+
if length(args) >= 3
959+
func_js = compile_value(ctx, args[3])
960+
pos_args = [compile_value(ctx, a) for a in args[4:end]]
961+
kwargs = _extract_kwargs(ctx, args[2])
962+
all_args = vcat(pos_args, ["$(k)=$(v)" for (k, v) in kwargs])
963+
return "$(func_js)($(join(all_args, ", ")))"
964+
end
965+
end
966+
851967
# Check for Core.Intrinsics — may be referenced via Base.add_int etc.
852968
if callee isa GlobalRef
853969
resolved = try

src/compiler/packages.jl

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# packages.jl — Package compilation registry
2+
#
3+
# Enables JST to compile calls to registered Julia packages into JS equivalents.
4+
# Instead of compiling a package's internal implementation (which often uses
5+
# complex Julia internals), registered packages have their API functions
6+
# mapped directly to JS code generators.
7+
#
8+
# Example:
9+
# register_package_compilation!(PlotlyBase, :scatter) do ctx, kwargs, args
10+
# # kwargs is Dict{Symbol, String} of compiled kwarg values
11+
# # args is Vector{String} of compiled positional args
12+
# build_js_object(Dict("type" => "\"scatter\"", kwargs...))
13+
# end
14+
15+
# Registry: (Module, Symbol) → compiler function
16+
# Compiler function signature: (ctx, kwargs::Dict{Symbol,String}, args::Vector{String}) → String
17+
const PACKAGE_COMPILATIONS = Dict{Tuple{Module, Symbol}, Function}()
18+
19+
"""
20+
register_package_compilation!(compiler_fn, mod::Module, name::Symbol)
21+
22+
Register a JS compilation mapping for a Julia package function.
23+
24+
The `compiler_fn` receives:
25+
- `ctx::JSCompilationContext` — compilation context
26+
- `kwargs::Dict{Symbol, String}` — keyword arguments (name → compiled JS expression)
27+
- `pos_args::Vector{String}` — positional arguments (compiled JS expressions)
28+
29+
And should return a JS code string.
30+
31+
# Example
32+
```julia
33+
register_package_compilation!(MyPlotLib, :scatter) do ctx, kwargs, pos_args
34+
pairs = ["\\"\\$(k)\\": \\$(v)" for (k, v) in kwargs]
35+
push!(pairs, "\\"type\\": \\"scatter\\"")
36+
return "{\\$(join(pairs, ", "))}"
37+
end
38+
```
39+
"""
40+
function register_package_compilation!(compiler_fn::Function, mod::Module, name::Symbol)
41+
PACKAGE_COMPILATIONS[(mod, name)] = compiler_fn
42+
end
43+
44+
"""
45+
register_package_compilations!(mod::Module, mappings::Dict{Symbol, Function})
46+
47+
Register multiple compilation mappings for a package at once.
48+
"""
49+
function register_package_compilations!(mod::Module, mappings::Dict{Symbol, Function})
50+
for (name, compiler_fn) in mappings
51+
PACKAGE_COMPILATIONS[(mod, name)] = compiler_fn
52+
end
53+
end
54+
55+
"""
56+
lookup_package_compilation(mod::Module, name::Symbol)
57+
58+
Look up a registered compilation mapping. Returns the compiler function or nothing.
59+
"""
60+
function lookup_package_compilation(mod::Module, name::Symbol)
61+
get(PACKAGE_COMPILATIONS, (mod, name), nothing)
62+
end
63+
64+
# ─── Helper: build JS object literal from key-value pairs ───
65+
66+
"""
67+
build_js_object(pairs::Dict{String, String}) → String
68+
69+
Build a JS object literal from string key → compiled JS value pairs.
70+
"""
71+
function build_js_object(pairs)
72+
if isempty(pairs)
73+
return "{}"
74+
end
75+
entries = ["$(repr(k)): $(v)" for (k, v) in pairs]
76+
return "{$(join(entries, ", "))}"
77+
end
78+
79+
"""
80+
build_js_object_from_kwargs(kwargs::Dict{Symbol, String}; extras...) → String
81+
82+
Build a JS object from compiled kwargs, with optional extra static fields.
83+
"""
84+
function build_js_object_from_kwargs(kwargs::Dict{Symbol, String}; extras...)
85+
pairs = Dict{String, String}()
86+
for (k, v) in kwargs
87+
pairs[string(k)] = v
88+
end
89+
for (k, v) in extras
90+
pairs[string(k)] = v isa String ? repr(v) : string(v)
91+
end
92+
return build_js_object(pairs)
93+
end

src/compiler/packages_plotly.jl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# packages_plotly.jl — Plotly compilation mappings
2+
#
3+
# Maps PlotlyBase/PlotlyJS API functions to Plotly.js browser calls.
4+
# Users write standard Julia plotting code, JST compiles it to JS.
5+
#
6+
# Supported:
7+
# scatter(x=x, y=y, mode="lines") → {type:"scatter", x:x, y:y, mode:"lines"}
8+
# bar(x=x, y=y) → {type:"bar", x:x, y:y}
9+
# Layout(title="...", xaxis=...) → {title:"...", xaxis:...}
10+
# plot(traces, layout; id="plot") → Plotly.newPlot(el, traces, layout)
11+
12+
# ─── Trace constructors: scatter, bar, heatmap, etc. ───
13+
14+
function _plotly_trace_compiler(trace_type::String)
15+
return (ctx, kwargs, pos_args) -> begin
16+
pairs = Dict{String, String}()
17+
pairs["type"] = repr(trace_type)
18+
for (k, v) in kwargs
19+
pairs[string(k)] = v
20+
end
21+
return build_js_object(pairs)
22+
end
23+
end
24+
25+
# ─── Layout constructor ───
26+
27+
function _plotly_layout_compiler(ctx, kwargs, pos_args)
28+
pairs = Dict{String, String}()
29+
for (k, v) in kwargs
30+
pairs[string(k)] = v
31+
end
32+
return build_js_object(pairs)
33+
end
34+
35+
# ─── Plot function: creates/updates a Plotly chart ───
36+
37+
function _plotly_plot_compiler(ctx, kwargs, pos_args)
38+
# pos_args[1] = traces (array), pos_args[2] = layout (optional)
39+
# kwargs may include: divid (element ID)
40+
traces_js = length(pos_args) >= 1 ? pos_args[1] : "[]"
41+
layout_js = length(pos_args) >= 2 ? pos_args[2] : "{}"
42+
43+
# Get target element ID from kwargs or default
44+
el_id = get(kwargs, :divid, "\"therapy-plot\"")
45+
46+
return "(function() { var _el = document.getElementById($(el_id)); if (_el && typeof Plotly !== 'undefined') { Plotly.react(_el, $(traces_js), $(layout_js), {responsive: true, displayModeBar: false}); } else if (_el) { var _s = document.createElement('script'); _s.src = 'https://cdn.plot.ly/plotly-2.35.2.min.js'; _s.onload = function() { Plotly.newPlot(_el, $(traces_js), $(layout_js), {responsive: true, displayModeBar: false}); }; document.head.appendChild(_s); } }())"
47+
end
48+
49+
# ─── Registration function (called when a module that has these names is available) ───
50+
51+
"""
52+
register_plotly_compilations!(mod::Module)
53+
54+
Register Plotly trace/layout/plot compilation mappings for a module.
55+
Call with the module that exports scatter, bar, Layout, plot etc.
56+
"""
57+
function register_plotly_compilations!(mod::Module)
58+
# Trace types
59+
for trace_type in [:scatter, :bar, :heatmap, :contour, :surface,
60+
:histogram, :box, :violin, :pie, :scatter3d,
61+
:scattergl, :scatterpolar, :choropleth, :mesh3d]
62+
if isdefined(mod, trace_type)
63+
register_package_compilation!(_plotly_trace_compiler(string(trace_type)), mod, trace_type)
64+
end
65+
end
66+
67+
# Layout
68+
if isdefined(mod, :Layout)
69+
register_package_compilation!(_plotly_layout_compiler, mod, :Layout)
70+
end
71+
72+
# Plot
73+
if isdefined(mod, :plot)
74+
register_package_compilation!(_plotly_plot_compiler, mod, :plot)
75+
end
76+
end

0 commit comments

Comments
 (0)