Skip to content

Commit 74e0fda

Browse files
Dale-Blackclaude
andcommitted
Add array compilation: optimize=false path with SSA callee resolution
Julia arrays now compile to JS arrays: - Float64[] → [] - push!(arr, val) → arr.push(val) - sin(x) → Math.sin(x) - for i in 1:n → iterate pattern with range objects - (x, y) → [x, y] Uses optimize=false IR which preserves semantic function calls (push!, getindex, sin) instead of Julia's internal memory management. Adds SlotNumber support for local variable names in unoptimized IR. Adds SSA callee resolution for Core.Const function references. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 172f088 commit 74e0fda

1 file changed

Lines changed: 253 additions & 0 deletions

File tree

src/compiler/codegen.jl

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,27 @@ function compile_function(ctx::JSCompilationContext)
7070
print(buf, " let $(ctx.js_locals[idx]);\n")
7171
end
7272

73+
# Declare slot variables (used in optimize=false IR for local variables)
74+
if hasproperty(ctx.code_info, :slotnames) && ctx.code_info.slotnames !== nothing
75+
slot_declared = Set{String}()
76+
for i in 2:length(ctx.code_info.slotnames) # Skip slot 1 (#self#)
77+
name = string(ctx.code_info.slotnames[i])
78+
if startswith(name, "#") || startswith(name, "@") || isempty(name)
79+
name = "_tmp$(i)"
80+
end
81+
# Skip argument names (already declared as parameters)
82+
if name in ctx.arg_names || name in slot_declared
83+
continue
84+
end
85+
# Only declare slots that are actually assigned in the code
86+
is_assigned = any(s -> s isa Expr && s.head == :(=) && s.args[1] isa Core.SlotNumber && s.args[1].id == i, ctx.code_info.code)
87+
if is_assigned
88+
push!(slot_declared, name)
89+
print(buf, " let $(name);\n")
90+
end
91+
end
92+
end
93+
7394
# Emit the code body
7495
emit_structured!(ctx, buf, code, 1, n, loop_headers, backward_edges, forward_gotos, phi_info, 1)
7596

@@ -479,6 +500,26 @@ function compile_expr_stmt!(ctx, buf, idx, expr::Expr, indent::String)
479500
end
480501
elseif expr.head === :leave || expr.head === :pop_exception
481502
# Exception frame management: no-op in JS (try/catch handles this)
503+
elseif expr.head === :(=)
504+
# Slot assignment (optimize=false IR): _var = expr
505+
lhs = expr.args[1]
506+
rhs = expr.args[2]
507+
lhs_name = compile_value(ctx, lhs)
508+
# RHS may be an Expr (call, invoke, etc.) or a plain value
509+
rhs_val = if rhs isa Expr
510+
if rhs.head === :call
511+
compile_call(ctx, rhs)
512+
elseif rhs.head === :invoke
513+
compile_invoke(ctx, rhs)
514+
elseif rhs.head === :new
515+
compile_new_expr(ctx, rhs)
516+
else
517+
compile_value(ctx, rhs)
518+
end
519+
else
520+
compile_value(ctx, rhs)
521+
end
522+
print(buf, "$(indent)$(lhs_name) = $(rhs_val);\n")
482523
end
483524
end
484525

@@ -489,6 +530,148 @@ function compile_call(ctx::JSCompilationContext, expr::Expr)
489530
args = expr.args
490531
callee = args[1]
491532

533+
# ─── Resolve SSA callees (from unoptimized IR) ───
534+
# In optimize=false IR, calls like push!(x, val) appear as (%20)(%21, %22)
535+
# where %20 is an SSA holding Core.Const(push!). Resolve and dispatch.
536+
if callee isa Core.SSAValue
537+
callee_type = ctx.code_info.ssavaluetypes[callee.id]
538+
if callee_type isa Core.Const
539+
resolved_fn = callee_type.val
540+
fn_name = string(nameof(resolved_fn))
541+
call_args = [compile_value(ctx, a) for a in args[2:end]]
542+
543+
# Array creation: Float64[] → getindex(Float64) → []
544+
# Also handles array indexing: arr[i] → arr[i-1]
545+
if resolved_fn === Base.getindex
546+
# Check if first arg is a Type (array construction)
547+
if length(args) >= 2
548+
first_arg = args[2]
549+
first_type = nothing
550+
if first_arg isa Core.SSAValue
551+
first_type = try ctx.code_info.ssavaluetypes[first_arg.id] catch; nothing end
552+
elseif first_arg isa GlobalRef
553+
first_type = try Core.Const(getfield(first_arg.mod, first_arg.name)) catch; nothing end
554+
end
555+
if first_type isa Core.Const && first_type.val isa DataType
556+
return "[]"
557+
end
558+
end
559+
# Array indexing: arr[i] → arr[i-1]
560+
if length(call_args) == 2
561+
return "$(call_args[1])[($(call_args[2])) - 1]"
562+
end
563+
return "[]"
564+
end
565+
566+
# push!(arr, val) → arr.push(val)
567+
if resolved_fn === Base.push! && length(call_args) >= 2
568+
return "$(call_args[1]).push($(call_args[2]))"
569+
end
570+
571+
# length(arr) → arr.length
572+
if resolved_fn === Base.length && length(call_args) >= 1
573+
return "$(call_args[1]).length"
574+
end
575+
576+
# Math functions
577+
if resolved_fn === Base.sin || resolved_fn === sin
578+
return "Math.sin($(call_args[1]))"
579+
end
580+
if resolved_fn === Base.cos || resolved_fn === cos
581+
return "Math.cos($(call_args[1]))"
582+
end
583+
if resolved_fn === Base.sqrt || resolved_fn === sqrt
584+
return "Math.sqrt($(call_args[1]))"
585+
end
586+
if resolved_fn === Base.abs || resolved_fn === abs
587+
return "Math.abs($(call_args[1]))"
588+
end
589+
if resolved_fn === Base.max || resolved_fn === max
590+
return "Math.max($(join(call_args, ", ")))"
591+
end
592+
if resolved_fn === Base.min || resolved_fn === min
593+
return "Math.min($(join(call_args, ", ")))"
594+
end
595+
596+
# println → console.log
597+
if resolved_fn === Base.println || resolved_fn === println
598+
require_runtime!(ctx, :jl_println)
599+
return "jl_println($(join(call_args, ", ")))"
600+
end
601+
602+
# Type constructors: Float64(x) → +(x) (identity for numbers)
603+
if resolved_fn === Float64
604+
return "+($(call_args[1]))"
605+
end
606+
if resolved_fn === Float32
607+
return "Math.fround($(call_args[1]))"
608+
end
609+
if resolved_fn === Int || resolved_fn === Int64
610+
return "(($(call_args[1])) | 0)"
611+
end
612+
613+
# Colon constructor: (:)(start, stop) → range not needed, handled by iterate
614+
if resolved_fn === Base.Colon()
615+
# Range creation — this is just (:)(1, n), handled by iterate below
616+
return "({start: $(call_args[1]), stop: $(call_args[2])})"
617+
end
618+
619+
# iterate(range) and iterate(range, state) for for-loops
620+
if resolved_fn === Base.iterate
621+
if length(call_args) == 1
622+
# iterate(range) → { value: range.start, done: range.start > range.stop }
623+
r = call_args[1]
624+
return "($(r).start <= $(r).stop ? [$(r).start, $(r).start] : null)"
625+
else
626+
# iterate(range, state) → { value: state+1, done: state+1 > range.stop }
627+
r = call_args[1]
628+
s = call_args[2]
629+
return "(($(s) + 1) <= $(r).stop ? [($(s) + 1), ($(s) + 1)] : null)"
630+
end
631+
end
632+
633+
# Multiplication, addition etc (when not inlined as intrinsics)
634+
if resolved_fn === Base.:(*) && length(call_args) == 2
635+
return "($(call_args[1]) * $(call_args[2]))"
636+
end
637+
if resolved_fn === Base.:(+) && length(call_args) == 2
638+
return "($(call_args[1]) + $(call_args[2]))"
639+
end
640+
if resolved_fn === Base.:(-) && length(call_args) == 2
641+
return "($(call_args[1]) - $(call_args[2]))"
642+
end
643+
if resolved_fn === Base.:(-) && length(call_args) == 1
644+
return "(-($(call_args[1])))"
645+
end
646+
if resolved_fn === Base.:(/) && length(call_args) == 2
647+
return "($(call_args[1]) / $(call_args[2]))"
648+
end
649+
650+
# js() escape hatch
651+
if fn_name == "js"
652+
template_str = nothing
653+
if length(args) >= 2 && args[2] isa String
654+
template_str = args[2]
655+
elseif length(call_args) >= 1
656+
s = call_args[1]
657+
if length(s) >= 2 && s[1] == '"' && s[end] == '"'
658+
template_str = replace(replace(s[2:end-1], "\\\"" => "\""), "\\\\" => "\\")
659+
end
660+
end
661+
if template_str !== nothing
662+
result = template_str
663+
for i in 2:length(call_args)
664+
result = replace(result, "\$$(i-1)" => call_args[i])
665+
end
666+
return result
667+
end
668+
end
669+
670+
# Fallback: emit as function call
671+
return "$(fn_name)($(join(call_args, ", ")))"
672+
end
673+
end
674+
492675
# Check for Core.Intrinsics — may be referenced via Base.add_int etc.
493676
if callee isa GlobalRef
494677
resolved = try
@@ -540,6 +723,41 @@ function compile_call(ctx::JSCompilationContext, expr::Expr)
540723
if bname === :string && callee.mod === Base
541724
return compile_string_concat(ctx, args[2:end])
542725
end
726+
727+
# Base.getindex — array creation (Type[]) or array access (arr[i])
728+
if bname === :getindex && callee.mod === Base
729+
# Check if first arg is a Type → empty array creation
730+
if length(args) >= 2
731+
first_arg = args[2]
732+
first_type = nothing
733+
if first_arg isa Core.SSAValue
734+
first_type = try ctx.code_info.ssavaluetypes[first_arg.id] catch; nothing end
735+
elseif first_arg isa GlobalRef
736+
first_type = try Core.Const(getfield(first_arg.mod, first_arg.name)) catch; nothing end
737+
end
738+
if first_type isa Core.Const && first_type.val isa DataType
739+
return "[]"
740+
end
741+
end
742+
# Array indexing
743+
call_args_gr = [compile_value(ctx, a) for a in args[2:end]]
744+
if length(call_args_gr) == 2
745+
return "$(call_args_gr[1])[($(call_args_gr[2])) - 1]"
746+
end
747+
end
748+
749+
# Base.iterate — for-loop iteration
750+
if bname === :iterate && callee.mod === Base
751+
call_args_it = [compile_value(ctx, a) for a in args[2:end]]
752+
if length(call_args_it) == 1
753+
r = call_args_it[1]
754+
return "($(r).start <= $(r).stop ? [$(r).start, $(r).start] : null)"
755+
else
756+
r = call_args_it[1]
757+
s = call_args_it[2]
758+
return "(($(s) + 1) <= $(r).stop ? [($(s) + 1), ($(s) + 1)] : null)"
759+
end
760+
end
543761
end
544762

545763
# Handle Core.=== (egal)
@@ -1382,6 +1600,12 @@ function compile_value(ctx::JSCompilationContext, val)
13821600
if stmt isa Core.PiNode
13831601
# PiNode is a type narrowing no-op: pass through the value
13841602
return compile_value(ctx, stmt.val)
1603+
elseif stmt isa Core.SlotNumber
1604+
# Slot read in unoptimized IR — resolve to slot variable name
1605+
return compile_value(ctx, stmt)
1606+
elseif stmt isa GlobalRef
1607+
# Global reference — resolve directly
1608+
return compile_value(ctx, stmt)
13851609
elseif stmt isa Expr && stmt.head === :call
13861610
return compile_call(ctx, stmt)
13871611
elseif stmt isa Expr && stmt.head === :invoke
@@ -1390,9 +1614,38 @@ function compile_value(ctx::JSCompilationContext, val)
13901614
return compile_new_expr(ctx, stmt)
13911615
elseif stmt isa Expr && stmt.head === :boundscheck
13921616
return "false"
1617+
elseif stmt isa Expr && stmt.head === :(=)
1618+
# Slot assignment — the SSA value is the RHS result
1619+
rhs = stmt.args[2]
1620+
if rhs isa Expr
1621+
if rhs.head === :call
1622+
return compile_call(ctx, rhs)
1623+
elseif rhs.head === :invoke
1624+
return compile_invoke(ctx, rhs)
1625+
end
1626+
end
1627+
return compile_value(ctx, rhs)
13931628
end
13941629
# Fallback: create a local
13951630
return get_local!(ctx, id)
1631+
elseif val isa Core.SlotNumber
1632+
# Local variable (used in optimize=false IR)
1633+
slot_id = val.id
1634+
if slot_id == 1
1635+
# Slot 1 is #self# (the function)
1636+
return "null"
1637+
end
1638+
# Use slot name from CodeInfo if available
1639+
slot_names = ctx.code_info.slotnames
1640+
if slot_id <= length(slot_names)
1641+
name = string(slot_names[slot_id])
1642+
# Clean up generated names (remove # prefixes, @ etc.)
1643+
if startswith(name, "#") || startswith(name, "@") || isempty(name)
1644+
return "_tmp$(slot_id)"
1645+
end
1646+
return name
1647+
end
1648+
return "_tmp$(slot_id)"
13961649
elseif val isa Core.Argument
13971650
idx = val.n - 1
13981651
if idx >= 1 && idx <= length(ctx.arg_names)

0 commit comments

Comments
 (0)