instructions.go raw

   1  package jsbackend
   2  
   3  import (
   4  	"fmt"
   5  	"go/constant"
   6  	"go/token"
   7  	"go/types"
   8  	"strings"
   9  
  10  	"golang.org/x/tools/go/ssa"
  11  )
  12  
  13  // FunctionCompiler handles the translation of a single SSA function to JS.
  14  type FunctionCompiler struct {
  15  	pc     *ProgramCompiler
  16  	fn     *ssa.Function
  17  	e      *Emitter
  18  	locals map[ssa.Value]string // SSA value -> JS variable name
  19  	varGen int                  // variable name counter
  20  	// Track which blocks need phi variable assignments.
  21  	phis []*ssa.Phi
  22  }
  23  
  24  // freshVar generates a unique local variable name.
  25  func (fc *FunctionCompiler) freshVar(hint string) string {
  26  	fc.varGen++
  27  	if hint != "" {
  28  		return fmt.Sprintf("$%s_%d", JsIdentifier(hint), fc.varGen)
  29  	}
  30  	return fmt.Sprintf("$v_%d", fc.varGen)
  31  }
  32  
  33  // compile generates JS for the entire function.
  34  func (fc *FunctionCompiler) compile() {
  35  	fn := fc.fn
  36  	e := fc.e
  37  
  38  	name := functionJsName(fn)
  39  	params := functionParams(fn)
  40  
  41  	// Determine if function needs to be async (contains channel ops, goroutine spawns, etc.).
  42  	async := fc.needsAsync()
  43  	hasDefer := fc.hasDefer()
  44  
  45  	prefix := "export "
  46  	if fn.Parent() != nil {
  47  		prefix = "" // nested/anonymous functions are not exported
  48  	}
  49  
  50  	asyncStr := ""
  51  	if async {
  52  		asyncStr = "async "
  53  	}
  54  
  55  	e.Block("%s%sfunction %s(%s)", prefix, asyncStr, name, params)
  56  
  57  	// Emit local variable declarations for all SSA values.
  58  	fc.emitLocals()
  59  
  60  	if hasDefer {
  61  		e.Line("const $defers = [];")
  62  		e.Line("let $panicValue = null;")
  63  		e.Block("try")
  64  	}
  65  
  66  	// Emit blocks.
  67  	// Use a block-label scheme: while(true) { switch($block) { case 0: ... } }
  68  	if len(fn.Blocks) == 1 {
  69  		// Simple function — no control flow needed.
  70  		fc.emitBlock(fn.Blocks[0], false)
  71  	} else if len(fn.Blocks) > 1 {
  72  		e.Line("let $block = 0;")
  73  		e.Block("while (true)")
  74  		e.Block("switch ($block)")
  75  
  76  		for _, block := range fn.Blocks {
  77  			// Skip the recover block — it's emitted after the try/catch/finally.
  78  			if hasDefer && fn.Recover != nil && block == fn.Recover {
  79  				continue
  80  			}
  81  			e.Line("case %d: {", block.Index)
  82  			e.Indent()
  83  			fc.emitBlock(block, true)
  84  			e.Line("break;")
  85  			e.Dedent()
  86  			e.Line("}")
  87  		}
  88  
  89  		e.EndBlock() // switch
  90  		e.EndBlock() // while
  91  	}
  92  
  93  	if hasDefer {
  94  		e.EndBlock() // try
  95  		e.Block("catch ($e)")
  96  		e.Line("$panicValue = $e;")
  97  		e.EndBlock() // catch
  98  		e.Block("finally")
  99  		e.Line("$rt.runtime.runDeferStack($defers, $panicValue);")
 100  		e.EndBlock() // finally
 101  
 102  		// If a panic was recovered, execution continues here.
 103  		// Emit the recover block which reads named return values.
 104  		if fn.Recover != nil {
 105  			e.Comment("Recover block — reached after successful recovery.")
 106  			fc.emitBlock(fn.Recover, false)
 107  		}
 108  	}
 109  
 110  	e.EndBlock() // function
 111  	e.Newline()
 112  
 113  	// Compile anonymous (closure) functions.
 114  	for _, anon := range fn.AnonFuncs {
 115  		fc.pc.compileFunction(e, anon)
 116  	}
 117  }
 118  
 119  // hasDefer checks if the function contains any defer statements.
 120  func (fc *FunctionCompiler) hasDefer() bool {
 121  	for _, block := range fc.fn.Blocks {
 122  		for _, instr := range block.Instrs {
 123  			if _, ok := instr.(*ssa.Defer); ok {
 124  				return true
 125  			}
 126  		}
 127  	}
 128  	return false
 129  }
 130  
 131  // emitLocals declares JS variables for all SSA values used in the function.
 132  func (fc *FunctionCompiler) emitLocals() {
 133  	// Parameters get their names from the SSA params.
 134  	for _, p := range fc.fn.Params {
 135  		fc.locals[p] = JsIdentifier(p.Name())
 136  	}
 137  
 138  	// Free variables (closures).
 139  	for _, fv := range fc.fn.FreeVars {
 140  		fc.locals[fv] = JsIdentifier(fv.Name())
 141  	}
 142  
 143  	// Scan all instructions for values that need variables.
 144  	var varDecls []string
 145  	for _, block := range fc.fn.Blocks {
 146  		for _, instr := range block.Instrs {
 147  			val, ok := instr.(ssa.Value)
 148  			if !ok {
 149  				continue
 150  			}
 151  			if _, exists := fc.locals[val]; exists {
 152  				continue
 153  			}
 154  			name := fc.freshVar(val.Name())
 155  			fc.locals[val] = name
 156  			varDecls = append(varDecls, name)
 157  		}
 158  	}
 159  	if len(varDecls) > 0 {
 160  		fc.e.Line("let %s;", strings.Join(varDecls, ", "))
 161  	}
 162  }
 163  
 164  // emitBlock emits JS for all instructions in a basic block.
 165  func (fc *FunctionCompiler) emitBlock(block *ssa.BasicBlock, inSwitch bool) {
 166  	for _, instr := range block.Instrs {
 167  		fc.emitInstruction(instr, inSwitch)
 168  	}
 169  }
 170  
 171  // emitInstruction dispatches a single SSA instruction to JS code.
 172  func (fc *FunctionCompiler) emitInstruction(instr ssa.Instruction, inSwitch bool) {
 173  	e := fc.e
 174  
 175  	switch instr := instr.(type) {
 176  	case *ssa.Alloc:
 177  		dst := fc.local(instr)
 178  		elemType := instr.Type().(*types.Pointer).Elem()
 179  		zero := fc.pc.typeMapper.ZeroExpr(elemType)
 180  		// Alloc creates a pointer. We model it as an accessor object.
 181  		if IsValueType(elemType) {
 182  			e.Line("%s = { $value: %s, $get() { return this.$value; }, $set(v) { this.$value = v; } };", dst, zero)
 183  		} else {
 184  			e.Line("%s = { $value: %s, $get() { return this.$value; }, $set(v) { this.$value = v; } };", dst, zero)
 185  		}
 186  
 187  	case *ssa.BinOp:
 188  		dst := fc.local(instr)
 189  		x := fc.value(instr.X)
 190  		y := fc.value(instr.Y)
 191  		op := fc.binOp(instr.Op, instr.X.Type())
 192  		e.Line("%s = %s;", dst, op(x, y))
 193  
 194  	case *ssa.Call:
 195  		dst := fc.local(instr)
 196  		callExpr := fc.emitCall(instr.Common())
 197  		if dst != "" && instr.Type() != nil {
 198  			e.Line("%s = %s;", dst, callExpr)
 199  		} else {
 200  			e.Line("%s;", callExpr)
 201  		}
 202  
 203  	case *ssa.ChangeInterface:
 204  		dst := fc.local(instr)
 205  		e.Line("%s = %s;", dst, fc.value(instr.X))
 206  
 207  	case *ssa.ChangeType:
 208  		dst := fc.local(instr)
 209  		e.Line("%s = %s;", dst, fc.value(instr.X))
 210  
 211  	case *ssa.Convert:
 212  		dst := fc.local(instr)
 213  		e.Line("%s = %s;", dst, fc.emitConvert(instr))
 214  
 215  	case *ssa.DebugRef:
 216  		// Skip.
 217  
 218  	case *ssa.Defer:
 219  		fc.emitDefer(instr)
 220  
 221  	case *ssa.Extract:
 222  		dst := fc.local(instr)
 223  		tuple := fc.value(instr.Tuple)
 224  		e.Line("%s = %s[%d];", dst, tuple, instr.Index)
 225  
 226  	case *ssa.Field:
 227  		dst := fc.local(instr)
 228  		x := fc.value(instr.X)
 229  		fieldName := fieldJsName(instr.X.Type(), instr.Field)
 230  		e.Line("%s = %s.%s;", dst, x, fieldName)
 231  
 232  	case *ssa.FieldAddr:
 233  		dst := fc.local(instr)
 234  		x := fc.value(instr.X)
 235  		fieldName := fieldJsName(instr.X.Type().(*types.Pointer).Elem(), instr.Field)
 236  		// Return an accessor that reads/writes the field.
 237  		e.Line("%s = { $get() { return %s.$get().%s; }, $set(v) { const obj = %s.$get(); obj.%s = v; %s.$set(obj); } };",
 238  			dst, x, fieldName, x, fieldName, x)
 239  
 240  	case *ssa.Go:
 241  		fc.emitGo(instr)
 242  
 243  	case *ssa.If:
 244  		cond := fc.value(instr.Cond)
 245  		block := instr.Block()
 246  		if inSwitch {
 247  			// Emit phi assignments before each branch target.
 248  			thenPhis := fc.phiAssignmentStmts(block, block.Succs[0])
 249  			elsePhis := fc.phiAssignmentStmts(block, block.Succs[1])
 250  			e.Block("if (%s)", cond)
 251  			for _, s := range thenPhis {
 252  				e.Line("%s", s)
 253  			}
 254  			e.Line("$block = %d; break;", block.Succs[0].Index)
 255  			e.EndBlock()
 256  			e.Block("else")
 257  			for _, s := range elsePhis {
 258  				e.Line("%s", s)
 259  			}
 260  			e.Line("$block = %d; break;", block.Succs[1].Index)
 261  			e.EndBlock()
 262  		} else {
 263  			e.Line("if (%s) { /* block %d */ } else { /* block %d */ }",
 264  				cond, block.Succs[0].Index, block.Succs[1].Index)
 265  		}
 266  
 267  	case *ssa.Index:
 268  		dst := fc.local(instr)
 269  		x := fc.value(instr.X)
 270  		idx := fc.value(instr.Index)
 271  		switch instr.X.Type().Underlying().(type) {
 272  		case *types.Basic: // string — use UTF-8 byte semantics
 273  			e.Line("$rt.runtime.boundsCheck(%s, $rt.builtin.byteLen(%s));", idx, x)
 274  			e.Line("%s = $rt.builtin.stringByteAt(%s, %s);", dst, x, idx)
 275  		default: // array/slice
 276  			e.Line("%s = %s.get(%s);", dst, x, idx)
 277  		}
 278  
 279  	case *ssa.IndexAddr:
 280  		dst := fc.local(instr)
 281  		x := fc.value(instr.X)
 282  		idx := fc.value(instr.Index)
 283  		// X is a pointer to an array or slice. Dereference through $get() for pointer types.
 284  		if _, ok := instr.X.Type().Underlying().(*types.Pointer); ok {
 285  			e.Line("%s = %s.$get().addr(%s);", dst, x, idx)
 286  		} else {
 287  			e.Line("%s = %s.addr(%s);", dst, x, idx)
 288  		}
 289  
 290  	case *ssa.Jump:
 291  		if inSwitch {
 292  			block := instr.Block()
 293  			// Emit phi assignments before jumping.
 294  			fc.emitPhiAssignments(block, block.Succs[0])
 295  			e.Line("$block = %d; break;", block.Succs[0].Index)
 296  		}
 297  
 298  	case *ssa.Lookup:
 299  		dst := fc.local(instr)
 300  		x := fc.value(instr.X)
 301  		idx := fc.value(instr.Index)
 302  
 303  		if _, ok := instr.X.Type().Underlying().(*types.Map); ok {
 304  			if instr.CommaOk {
 305  				e.Line("{ const $r = $rt.builtin.mapLookup(%s, %s); %s = [$r.value, $r.ok]; }", x, idx, dst)
 306  			} else {
 307  				e.Line("%s = $rt.builtin.mapLookup(%s, %s).value;", dst, x, idx)
 308  			}
 309  		} else {
 310  			// String index — UTF-8 byte semantics.
 311  			e.Line("%s = $rt.builtin.stringByteAt(%s, %s);", dst, x, idx)
 312  		}
 313  
 314  	case *ssa.MakeChan:
 315  		dst := fc.local(instr)
 316  		size := fc.value(instr.Size)
 317  		e.Line("%s = new $rt.channel.Channel(%s);", dst, size)
 318  
 319  	case *ssa.MakeClosure:
 320  		dst := fc.local(instr)
 321  		fnRef := fc.value(instr.Fn.(*ssa.Function))
 322  		var bindings []string
 323  		for _, b := range instr.Bindings {
 324  			bindings = append(bindings, fc.value(b))
 325  		}
 326  		e.Line("%s = %s.bind(null, %s);", dst, fnRef, strings.Join(bindings, ", "))
 327  
 328  	case *ssa.MakeInterface:
 329  		dst := fc.local(instr)
 330  		typeID := fc.pc.typeMapper.TypeID(instr.X.Type())
 331  		x := fc.value(instr.X)
 332  		e.Line("%s = $rt.types.makeInterface(%s, %s);", dst, JsString(typeID), x)
 333  
 334  	case *ssa.MakeMap:
 335  		dst := fc.local(instr)
 336  		mapType := instr.Type().Underlying().(*types.Map)
 337  		keyKind := fc.pc.typeMapper.KeyKind(mapType.Key())
 338  		e.Line("%s = $rt.builtin.makeMap(%s);", dst, JsString(keyKind))
 339  
 340  	case *ssa.MakeSlice:
 341  		dst := fc.local(instr)
 342  		length := fc.value(instr.Len)
 343  		capacity := fc.value(instr.Cap)
 344  		elemType := instr.Type().Underlying().(*types.Slice).Elem()
 345  		zero := fc.pc.typeMapper.ZeroExpr(elemType)
 346  		e.Line("%s = $rt.builtin.makeSlice(%s, %s, %s);", dst, length, capacity, zero)
 347  
 348  	case *ssa.MapUpdate:
 349  		m := fc.value(instr.Map)
 350  		k := fc.value(instr.Key)
 351  		v := fc.value(instr.Value)
 352  		e.Line("$rt.builtin.mapUpdate(%s, %s, %s);", m, k, v)
 353  
 354  	case *ssa.Next:
 355  		dst := fc.local(instr)
 356  		iter := fc.value(instr.Iter)
 357  		if instr.IsString {
 358  			e.Line("%s = %s.next();", dst, iter)
 359  		} else {
 360  			e.Line("%s = %s.next();", dst, iter)
 361  		}
 362  
 363  	case *ssa.Panic:
 364  		x := fc.value(instr.X)
 365  		e.Line("$rt.runtime.panic(%s);", x)
 366  
 367  	case *ssa.Phi:
 368  		// Phi nodes are handled via assignments at the end of predecessor blocks.
 369  		fc.phis = append(fc.phis, instr)
 370  
 371  	case *ssa.Range:
 372  		dst := fc.local(instr)
 373  		x := fc.value(instr.X)
 374  		switch instr.X.Type().Underlying().(type) {
 375  		case *types.Basic: // string range — use UTF-8 byte semantics
 376  			e.Line("%s = $rt.builtin.stringRange(%s);", dst, x)
 377  		case *types.Map:
 378  			e.Line("%s = { $entries: [...%s.entries()], $pos: 0, next() { if (this.$pos >= this.$entries.length) return [false, null, null]; const [k, v] = this.$entries[this.$pos++]; return [true, k, v]; } };", dst, x)
 379  		}
 380  
 381  	case *ssa.Return:
 382  		if len(instr.Results) == 0 {
 383  			e.Line("return;")
 384  		} else if len(instr.Results) == 1 {
 385  			e.Line("return %s;", fc.value(instr.Results[0]))
 386  		} else {
 387  			var vals []string
 388  			for _, r := range instr.Results {
 389  				vals = append(vals, fc.value(r))
 390  			}
 391  			e.Line("return [%s];", strings.Join(vals, ", "))
 392  		}
 393  
 394  	case *ssa.RunDefers:
 395  		e.Line("// RunDefers handled by try/finally")
 396  
 397  	case *ssa.Select:
 398  		fc.emitSelect(instr)
 399  
 400  	case *ssa.Send:
 401  		ch := fc.value(instr.Chan)
 402  		x := fc.value(instr.X)
 403  		e.Line("await %s.send(%s);", ch, x)
 404  
 405  	case *ssa.Slice:
 406  		dst := fc.local(instr)
 407  		x := fc.value(instr.X)
 408  		low := "undefined"
 409  		high := "undefined"
 410  		max := "undefined"
 411  		if instr.Low != nil {
 412  			low = fc.value(instr.Low)
 413  		}
 414  		if instr.High != nil {
 415  			high = fc.value(instr.High)
 416  		}
 417  		if instr.Max != nil {
 418  			max = fc.value(instr.Max)
 419  		}
 420  		xType := instr.X.Type().Underlying()
 421  		// Dereference pointer-to-array.
 422  		if ptr, ok := xType.(*types.Pointer); ok {
 423  			xType = ptr.Elem().Underlying()
 424  			x = x + ".$get()"
 425  		}
 426  		switch xType.(type) {
 427  		case *types.Basic: // string slice
 428  			e.Line("%s = $rt.builtin.stringSlice(%s, %s, %s);", dst, x, low, high)
 429  		default:
 430  			e.Line("%s = $rt.builtin.sliceSlice(%s, %s, %s, %s);", dst, x, low, high, max)
 431  		}
 432  
 433  	case *ssa.Store:
 434  		addr := fc.value(instr.Addr)
 435  		val := fc.value(instr.Val)
 436  		if needsClone(instr.Val.Type()) {
 437  			e.Line("%s.$set($rt.builtin.cloneValue(%s));", addr, val)
 438  		} else {
 439  			e.Line("%s.$set(%s);", addr, val)
 440  		}
 441  
 442  	case *ssa.TypeAssert:
 443  		dst := fc.local(instr)
 444  		x := fc.value(instr.X)
 445  		targetType := fc.pc.typeMapper.TypeID(instr.AssertedType)
 446  
 447  		if instr.CommaOk {
 448  			if types.IsInterface(instr.AssertedType) {
 449  				e.Line("{ try { %s = [$rt.types.interfaceAssert(%s, %s).$value, true]; } catch(e) { %s = [null, false]; } }",
 450  					dst, x, JsString(targetType), dst)
 451  			} else {
 452  				e.Line("%s = $rt.types.typeAssertOk(%s, %s);", dst, x, JsString(targetType))
 453  			}
 454  		} else {
 455  			if types.IsInterface(instr.AssertedType) {
 456  				e.Line("%s = $rt.types.interfaceAssert(%s, %s);", dst, x, JsString(targetType))
 457  			} else {
 458  				e.Line("%s = $rt.types.typeAssert(%s, %s);", dst, x, JsString(targetType))
 459  			}
 460  		}
 461  
 462  	case *ssa.UnOp:
 463  		fc.emitUnOp(instr)
 464  
 465  	default:
 466  		e.Comment("TODO: unhandled instruction: %T %s", instr, instr.String())
 467  	}
 468  }
 469  
 470  // phiAssignmentStmts returns JS assignment statements for phi nodes when jumping from src to dst.
 471  // Uses temporaries to avoid sequential assignment corruption (all phis read old values simultaneously).
 472  func (fc *FunctionCompiler) phiAssignmentStmts(src, dst *ssa.BasicBlock) []string {
 473  	type phiPair struct {
 474  		dst, val string
 475  	}
 476  	var pairs []phiPair
 477  	for _, instr := range dst.Instrs {
 478  		phi, ok := instr.(*ssa.Phi)
 479  		if !ok {
 480  			break
 481  		}
 482  		for i, pred := range dst.Preds {
 483  			if pred == src {
 484  				pairs = append(pairs, phiPair{fc.local(phi), fc.value(phi.Edges[i])})
 485  				break
 486  			}
 487  		}
 488  	}
 489  	if len(pairs) <= 1 {
 490  		// Single phi or none — no reordering issue.
 491  		var stmts []string
 492  		for _, p := range pairs {
 493  			stmts = append(stmts, fmt.Sprintf("%s = %s;", p.dst, p.val))
 494  		}
 495  		return stmts
 496  	}
 497  	// Check if any phi destination appears as a source in another phi.
 498  	dstSet := make(map[string]bool)
 499  	for _, p := range pairs {
 500  		dstSet[p.dst] = true
 501  	}
 502  	needsTemps := false
 503  	for _, p := range pairs {
 504  		if dstSet[p.val] {
 505  			needsTemps = true
 506  			break
 507  		}
 508  	}
 509  	if !needsTemps {
 510  		var stmts []string
 511  		for _, p := range pairs {
 512  			stmts = append(stmts, fmt.Sprintf("%s = %s;", p.dst, p.val))
 513  		}
 514  		return stmts
 515  	}
 516  	// Use temporaries: save all sources, then assign.
 517  	var stmts []string
 518  	for i, p := range pairs {
 519  		stmts = append(stmts, fmt.Sprintf("let $phi%d = %s;", i, p.val))
 520  	}
 521  	for i, p := range pairs {
 522  		stmts = append(stmts, fmt.Sprintf("%s = $phi%d;", p.dst, i))
 523  	}
 524  	return stmts
 525  }
 526  
 527  // emitPhiAssignments emits variable assignments for phi nodes when jumping from src to dst.
 528  func (fc *FunctionCompiler) emitPhiAssignments(src, dst *ssa.BasicBlock) {
 529  	stmts := fc.phiAssignmentStmts(src, dst)
 530  	for _, s := range stmts {
 531  		fc.e.Line("%s", s)
 532  	}
 533  }
 534  
 535  // emitCall generates JS for a function call.
 536  func (fc *FunctionCompiler) emitCall(call *ssa.CallCommon) string {
 537  	if call.IsInvoke() {
 538  		// Interface method call — await if caller is async (method may be async).
 539  		recv := fc.value(call.Value)
 540  		var args []string
 541  		for _, a := range call.Args {
 542  			args = append(args, fc.value(a))
 543  		}
 544  		awaitPrefix := ""
 545  		if fc.needsAsync() {
 546  			awaitPrefix = "await "
 547  		}
 548  		return fmt.Sprintf("%s$rt.types.methodCall(%s, %s, [%s])",
 549  			awaitPrefix, recv, JsString(call.Method.Name()), strings.Join(args, ", "))
 550  	}
 551  
 552  	callee := call.StaticCallee()
 553  	if callee != nil && len(callee.FreeVars) == 0 {
 554  		return fc.emitStaticCall(callee, call.Args)
 555  	}
 556  	// Closure call: callee has FreeVars that were pre-bound via MakeClosure.
 557  	// Fall through to the dynamic call path so we invoke the bound value
 558  	// instead of the bare function name (which would skip the bindings).
 559  
 560  	// Builtin call (println, len, cap, append, etc.).
 561  	if builtin, ok := call.Value.(*ssa.Builtin); ok {
 562  		return fc.emitBuiltinCall(builtin.Name(), call.Args)
 563  	}
 564  
 565  	// Dynamic call (function value) — await if caller is async.
 566  	fnVal := fc.value(call.Value)
 567  	var args []string
 568  	for _, a := range call.Args {
 569  		args = append(args, fc.value(a))
 570  	}
 571  	if fc.needsAsync() {
 572  		return fmt.Sprintf("await %s(%s)", fnVal, strings.Join(args, ", "))
 573  	}
 574  	return fmt.Sprintf("%s(%s)", fnVal, strings.Join(args, ", "))
 575  }
 576  
 577  // emitStaticCall generates JS for a static function call.
 578  func (fc *FunctionCompiler) emitStaticCall(callee *ssa.Function, args []ssa.Value) string {
 579  	var jsArgs []string
 580  	for _, a := range args {
 581  		jsArgs = append(jsArgs, fc.value(a))
 582  	}
 583  	argStr := strings.Join(jsArgs, ", ")
 584  
 585  	fullName := callee.String()
 586  
 587  	// Map well-known functions to JS equivalents.
 588  	switch fullName {
 589  	case "println":
 590  		return fmt.Sprintf("$rt.runtime.println(%s)", argStr)
 591  	case "print":
 592  		return fmt.Sprintf("$rt.runtime.print(%s)", argStr)
 593  	case "len":
 594  		return fmt.Sprintf("$rt.builtin.len(%s)", argStr)
 595  	case "cap":
 596  		return fmt.Sprintf("$rt.builtin.cap(%s)", argStr)
 597  	case "append":
 598  		return fmt.Sprintf("$rt.builtin.append(%s)", argStr)
 599  	case "copy":
 600  		return fmt.Sprintf("$rt.builtin.copy(%s)", argStr)
 601  	case "delete":
 602  		return fmt.Sprintf("$rt.builtin.mapDelete(%s)", argStr)
 603  	case "close":
 604  		if len(jsArgs) > 0 {
 605  			return fmt.Sprintf("%s.close()", jsArgs[0])
 606  		}
 607  	case "panic":
 608  		return fmt.Sprintf("$rt.runtime.panic(%s)", argStr)
 609  	case "recover":
 610  		return "$rt.runtime.recover()"
 611  	case "spawn":
 612  		return fc.emitSpawn(callee, args)
 613  	}
 614  
 615  	// Package-qualified call.
 616  	fnName := functionJsName(callee)
 617  	pkg := callee.Package()
 618  
 619  	// Determine if the call needs to be awaited.
 620  	awaitPrefix := ""
 621  	if fc.needsAsync() && fc.pc.asyncFuncs[callee] {
 622  		awaitPrefix = "await "
 623  	}
 624  
 625  	if pkg != nil && pkg.Pkg.Path() != fc.fn.Package().Pkg.Path() {
 626  		// Cross-package call.
 627  		pkgAlias := JsIdentifier(pkg.Pkg.Path())
 628  		if shouldSkipPackage(pkg.Pkg.Path()) {
 629  			// Runtime function — map to JS runtime.
 630  			return fc.mapRuntimeCall(fullName, argStr)
 631  		}
 632  		return fmt.Sprintf("%s%s.%s(%s)", awaitPrefix, pkgAlias, fnName, argStr)
 633  	}
 634  
 635  	// Same-package call.
 636  	return fmt.Sprintf("%s%s(%s)", awaitPrefix, fnName, argStr)
 637  }
 638  
 639  // mapRuntimeCall maps Go runtime function calls to JS runtime equivalents.
 640  func (fc *FunctionCompiler) mapRuntimeCall(fullName string, argStr string) string {
 641  	switch {
 642  	case strings.HasPrefix(fullName, "fmt.Println"), fullName == "fmt.Println":
 643  		return fmt.Sprintf("$rt.runtime.println(%s)", argStr)
 644  	case strings.HasPrefix(fullName, "fmt.Printf"), fullName == "fmt.Printf":
 645  		return fmt.Sprintf("$rt.runtime.print(%s)", argStr)
 646  	case strings.HasPrefix(fullName, "fmt.Sprintf"):
 647  		return fmt.Sprintf("String(%s)", argStr)
 648  	default:
 649  		return fmt.Sprintf("/* runtime: %s */ undefined", fullName)
 650  	}
 651  }
 652  
 653  // emitBuiltinCall handles calls to Go builtin functions.
 654  func (fc *FunctionCompiler) emitBuiltinCall(name string, args []ssa.Value) string {
 655  	var jsArgs []string
 656  	for _, a := range args {
 657  		jsArgs = append(jsArgs, fc.value(a))
 658  	}
 659  	argStr := strings.Join(jsArgs, ", ")
 660  
 661  	switch name {
 662  	case "println":
 663  		return fmt.Sprintf("$rt.runtime.println(%s)", argStr)
 664  	case "print":
 665  		return fmt.Sprintf("$rt.runtime.print(%s)", argStr)
 666  	case "len":
 667  		return fmt.Sprintf("$rt.builtin.len(%s)", argStr)
 668  	case "cap":
 669  		return fmt.Sprintf("$rt.builtin.cap(%s)", argStr)
 670  	case "append":
 671  		// SSA append: first arg is the slice, second arg is also a slice (append(s1, s2...)).
 672  		if len(args) == 2 {
 673  			_, isSlice := args[1].Type().Underlying().(*types.Slice)
 674  			if isSlice {
 675  				return fmt.Sprintf("$rt.builtin.appendSlice(%s, %s)", jsArgs[0], jsArgs[1])
 676  			}
 677  			// append([]byte, string...) — spread string bytes.
 678  			if basic, ok := args[1].Type().Underlying().(*types.Basic); ok && basic.Info()&types.IsString != 0 {
 679  				return fmt.Sprintf("$rt.builtin.appendString(%s, %s)", jsArgs[0], jsArgs[1])
 680  			}
 681  		}
 682  		return fmt.Sprintf("$rt.builtin.append(%s)", argStr)
 683  	case "copy":
 684  		return fmt.Sprintf("$rt.builtin.copy(%s)", argStr)
 685  	case "delete":
 686  		return fmt.Sprintf("$rt.builtin.mapDelete(%s)", argStr)
 687  	case "close":
 688  		if len(jsArgs) > 0 {
 689  			return fmt.Sprintf("%s.close()", jsArgs[0])
 690  		}
 691  		return "undefined"
 692  	case "panic":
 693  		return fmt.Sprintf("$rt.runtime.panic(%s)", argStr)
 694  	case "recover":
 695  		return "$rt.runtime.recover()"
 696  	case "real":
 697  		if len(jsArgs) > 0 {
 698  			return fmt.Sprintf("(%s).re", jsArgs[0])
 699  		}
 700  		return "0"
 701  	case "imag":
 702  		if len(jsArgs) > 0 {
 703  			return fmt.Sprintf("(%s).im", jsArgs[0])
 704  		}
 705  		return "0"
 706  	case "complex":
 707  		if len(jsArgs) >= 2 {
 708  			return fmt.Sprintf("{ re: %s, im: %s }", jsArgs[0], jsArgs[1])
 709  		}
 710  		return "{ re: 0, im: 0 }"
 711  	case "min":
 712  		if len(jsArgs) == 2 {
 713  			return fmt.Sprintf("Math.min(%s, %s)", jsArgs[0], jsArgs[1])
 714  		}
 715  		return fmt.Sprintf("Math.min(%s)", argStr)
 716  	case "max":
 717  		if len(jsArgs) == 2 {
 718  			return fmt.Sprintf("Math.max(%s, %s)", jsArgs[0], jsArgs[1])
 719  		}
 720  		return fmt.Sprintf("Math.max(%s)", argStr)
 721  	case "make":
 722  		// Make is handled by MakeChan, MakeMap, MakeSlice SSA nodes, not as a call.
 723  		return fmt.Sprintf("/* make(%s) */", argStr)
 724  	case "new":
 725  		return fmt.Sprintf("/* new(%s) */", argStr)
 726  	default:
 727  		return fmt.Sprintf("/* builtin %s(%s) */", name, argStr)
 728  	}
 729  }
 730  
 731  // emitUnOp handles unary operations.
 732  func (fc *FunctionCompiler) emitUnOp(instr *ssa.UnOp) {
 733  	dst := fc.local(instr)
 734  	x := fc.value(instr.X)
 735  
 736  	switch instr.Op {
 737  	case token.NOT:
 738  		fc.e.Line("%s = !%s;", dst, x)
 739  	case token.SUB:
 740  		fc.e.Line("%s = -%s;", dst, x)
 741  	case token.XOR:
 742  		fc.e.Line("%s = ~%s;", dst, x)
 743  	case token.MUL: // pointer dereference
 744  		fc.e.Line("%s = %s.$get();", dst, x)
 745  	case token.ARROW: // channel receive
 746  		if instr.CommaOk {
 747  			fc.e.Line("{ const $r = await %s.recv(); %s = [$r.value, $r.ok]; }", x, dst)
 748  		} else {
 749  			fc.e.Line("%s = (await %s.recv()).value;", dst, x)
 750  		}
 751  	default:
 752  		fc.e.Comment("TODO: unhandled UnOp: %s", instr.Op)
 753  	}
 754  }
 755  
 756  // emitDefer generates JS for a defer statement.
 757  func (fc *FunctionCompiler) emitDefer(instr *ssa.Defer) {
 758  	call := &instr.Call
 759  	if call.IsInvoke() {
 760  		recv := fc.value(call.Value)
 761  		var args []string
 762  		for _, a := range call.Args {
 763  			args = append(args, fc.value(a))
 764  		}
 765  		fc.e.Line("$defers.push(() => $rt.types.methodCall(%s, %s, [%s]));",
 766  			recv, JsString(call.Method.Name()), strings.Join(args, ", "))
 767  	} else if builtin, ok := call.Value.(*ssa.Builtin); ok {
 768  		// Builtin call (println, etc.).
 769  		callExpr := fc.emitBuiltinCall(builtin.Name(), call.Args)
 770  		fc.e.Line("$defers.push(() => %s);", callExpr)
 771  	} else if _, ok := call.Value.(*ssa.MakeClosure); ok {
 772  		// Closure — use the bound closure variable.
 773  		fnVal := fc.value(call.Value)
 774  		var args []string
 775  		for _, a := range call.Args {
 776  			args = append(args, fc.value(a))
 777  		}
 778  		fc.e.Line("$defers.push(() => %s(%s));", fnVal, strings.Join(args, ", "))
 779  	} else if callee := call.StaticCallee(); callee != nil {
 780  		callExpr := fc.emitStaticCall(callee, call.Args)
 781  		fc.e.Line("$defers.push(() => %s);", callExpr)
 782  	} else {
 783  		fnVal := fc.value(call.Value)
 784  		var args []string
 785  		for _, a := range call.Args {
 786  			args = append(args, fc.value(a))
 787  		}
 788  		fc.e.Line("$defers.push(() => %s(%s));", fnVal, strings.Join(args, ", "))
 789  	}
 790  }
 791  
 792  // emitGo generates JS for a go statement (goroutine spawn).
 793  func (fc *FunctionCompiler) emitGo(instr *ssa.Go) {
 794  	call := &instr.Call
 795  	if call.IsInvoke() {
 796  		recv := fc.value(call.Value)
 797  		var args []string
 798  		for _, a := range call.Args {
 799  			args = append(args, fc.value(a))
 800  		}
 801  		fc.e.Line("$rt.goroutine.spawn(async () => $rt.types.methodCall(%s, %s, [%s]));",
 802  			recv, JsString(call.Method.Name()), strings.Join(args, ", "))
 803  	} else if _, isClosure := call.Value.(*ssa.MakeClosure); isClosure {
 804  		// For closures, spawn the closure variable (which has bindings already applied).
 805  		fnVal := fc.value(call.Value)
 806  		var args []string
 807  		for _, a := range call.Args {
 808  			args = append(args, fc.value(a))
 809  		}
 810  		fc.e.Line("$rt.goroutine.spawn(async () => %s(%s));", fnVal, strings.Join(args, ", "))
 811  	} else if callee := call.StaticCallee(); callee != nil {
 812  		callExpr := fc.emitStaticCall(callee, call.Args)
 813  		fc.e.Line("$rt.goroutine.spawn(async () => %s);", callExpr)
 814  	} else {
 815  		fnVal := fc.value(call.Value)
 816  		var args []string
 817  		for _, a := range call.Args {
 818  			args = append(args, fc.value(a))
 819  		}
 820  		fc.e.Line("$rt.goroutine.spawn(async () => %s(%s));", fnVal, strings.Join(args, ", "))
 821  	}
 822  }
 823  
 824  // emitSpawn generates JS for spawn — creates a new isolated browser domain.
 825  //
 826  // The spawn builtin's SSA signature is func(interface{}, ...interface{}).
 827  // Args[0] is MakeInterface(targetFn), Args[1] is a []interface{} slice
 828  // created by the SSA builder. We walk the SSA graph to recover the
 829  // concrete typed values (same approach as the native backend).
 830  func (fc *FunctionCompiler) emitSpawn(callee *ssa.Function, args []ssa.Value) string {
 831  	if len(args) < 1 {
 832  		return "/* spawn: no function argument */ undefined"
 833  	}
 834  
 835  	// Check if the first positional argument (fn) is a transport string.
 836  	// SSA: spawn("pipe", worker, ch) → args[0]=MakeInterface("pipe"), args[1]=variadic
 837  	transport := "local"
 838  	hasTransport := false
 839  	if ts, ok := extractTransportStringJS(args[0]); ok {
 840  		transport = ts
 841  		hasTransport = true
 842  	}
 843  
 844  	// Validate transport.
 845  	switch transport {
 846  	case "local", "pipe":
 847  		// OK.
 848  	default:
 849  		if len(transport) > 6 && (transport[:6] == "tcp://" || transport[:5] == "ws://" || transport[:6] == "wss://") {
 850  			return fmt.Sprintf("/* spawn: network transport %q not yet implemented */ undefined", transport)
 851  		}
 852  		return fmt.Sprintf("/* spawn: unknown transport %q */ undefined", transport)
 853  	}
 854  
 855  	// Extract all concrete values from the variadic slice.
 856  	allConcrete := extractSpawnSSAArgsJS(args)
 857  
 858  	// When transport is present, the function is allConcrete[0], args are [1:].
 859  	var fnArg ssa.Value
 860  	var concreteArgs []ssa.Value
 861  	if hasTransport {
 862  		if len(allConcrete) < 1 {
 863  			return "/* spawn: no function argument after transport string */ undefined"
 864  		}
 865  		fnArg = allConcrete[0]
 866  		concreteArgs = allConcrete[1:]
 867  	} else {
 868  		fnArg = args[0]
 869  		if mi, ok := fnArg.(*ssa.MakeInterface); ok {
 870  			fnArg = mi.X
 871  		}
 872  		concreteArgs = allConcrete
 873  	}
 874  
 875  	// Unwrap MakeInterface on fnArg.
 876  	if mi, ok := fnArg.(*ssa.MakeInterface); ok {
 877  		fnArg = mi.X
 878  	}
 879  	fnVal := fc.value(fnArg)
 880  
 881  	// Find the actual target function to determine its signature.
 882  	var targetFn *ssa.Function
 883  	switch v := fnArg.(type) {
 884  	case *ssa.Function:
 885  		targetFn = v
 886  	case *ssa.MakeClosure:
 887  		if fn, ok := v.Fn.(*ssa.Function); ok {
 888  			targetFn = fn
 889  		}
 890  	}
 891  
 892  	// Verify argument count and types match the target function.
 893  	if targetFn != nil {
 894  		sig := targetFn.Signature
 895  		if len(concreteArgs) != sig.Params().Len() {
 896  			return fmt.Sprintf("/* spawn: %s expects %d arguments, got %d */ undefined",
 897  				targetFn.Name(), sig.Params().Len(), len(concreteArgs))
 898  		}
 899  		for i, arg := range concreteArgs {
 900  			paramType := sig.Params().At(i).Type()
 901  			argType := arg.Type()
 902  			if !types.Identical(argType.Underlying(), paramType.Underlying()) {
 903  				return fmt.Sprintf("/* spawn: argument %d has type %s, %s expects %s */ undefined",
 904  					i+1, argType, targetFn.Name(), paramType)
 905  			}
 906  		}
 907  	}
 908  
 909  	var jsArgs []string
 910  	for i, arg := range concreteArgs {
 911  		jsVal := fc.value(arg)
 912  		// Emit type-appropriate JS serialization for the spawn boundary.
 913  		if targetFn != nil && i < targetFn.Signature.Params().Len() {
 914  			jsVal = fc.emitSpawnArgSerialize(arg, targetFn.Signature.Params().At(i).Type(), jsVal)
 915  		}
 916  		jsArgs = append(jsArgs, jsVal)
 917  	}
 918  
 919  	awaitPrefix := ""
 920  	if fc.needsAsync() {
 921  		awaitPrefix = "await "
 922  	}
 923  
 924  	if len(jsArgs) > 0 {
 925  		return fmt.Sprintf("%s$rt.domain.spawn(async () => %s(%s))", awaitPrefix, fnVal, strings.Join(jsArgs, ", "))
 926  	}
 927  	return fmt.Sprintf("%s$rt.domain.spawn(async () => %s())", awaitPrefix, fnVal)
 928  }
 929  
 930  // emitSpawnArgSerialize wraps a JS value in the appropriate serialization
 931  // for crossing a spawn boundary. Slices become Array copies, maps become
 932  // Object copies. Value types pass through unchanged.
 933  func (fc *FunctionCompiler) emitSpawnArgSerialize(arg ssa.Value, goType types.Type, jsVal string) string {
 934  	switch goType.Underlying().(type) {
 935  	case *types.Slice:
 936  		// Deep copy the slice so child gets independent data.
 937  		return fmt.Sprintf("[...%s]", jsVal)
 938  	case *types.Map:
 939  		// Shallow copy the map so child gets independent entries.
 940  		return fmt.Sprintf("Object.assign({}, %s)", jsVal)
 941  	default:
 942  		return jsVal
 943  	}
 944  }
 945  
 946  // extractSpawnSSAArgsJS recovers concrete values from spawn's variadic slice.
 947  // Same logic as extractSpawnSSAArgs in compiler/spawn.go but accessible from
 948  // the jsbackend package.
 949  // extractTransportStringJS checks if an SSA value is a string constant
 950  // (possibly wrapped in MakeInterface) and returns it.
 951  func extractTransportStringJS(v ssa.Value) (string, bool) {
 952  	if mi, ok := v.(*ssa.MakeInterface); ok {
 953  		v = mi.X
 954  	}
 955  	c, ok := v.(*ssa.Const)
 956  	if !ok {
 957  		return "", false
 958  	}
 959  	if c.Value == nil || c.Value.Kind() != constant.String {
 960  		return "", false
 961  	}
 962  	return constant.StringVal(c.Value), true
 963  }
 964  
 965  func extractSpawnSSAArgsJS(args []ssa.Value) []ssa.Value {
 966  	if len(args) < 2 {
 967  		return nil
 968  	}
 969  
 970  	variadicArg := args[1]
 971  	slice, ok := variadicArg.(*ssa.Slice)
 972  	if !ok {
 973  		return nil
 974  	}
 975  	alloc, ok := slice.X.(*ssa.Alloc)
 976  	if !ok {
 977  		return nil
 978  	}
 979  	refs := alloc.Referrers()
 980  	if refs == nil {
 981  		return nil
 982  	}
 983  
 984  	type indexedValue struct {
 985  		index int64
 986  		value ssa.Value
 987  	}
 988  	var indexed []indexedValue
 989  
 990  	for _, ref := range *refs {
 991  		ia, ok := ref.(*ssa.IndexAddr)
 992  		if !ok {
 993  			continue
 994  		}
 995  		iaRefs := ia.Referrers()
 996  		if iaRefs == nil {
 997  			continue
 998  		}
 999  		for _, iaRef := range *iaRefs {
1000  			store, ok := iaRef.(*ssa.Store)
1001  			if !ok || store.Addr != ia {
1002  				continue
1003  			}
1004  			idx := int64(0)
1005  			if constIdx, ok := ia.Index.(*ssa.Const); ok {
1006  				if v, ok := constant.Int64Val(constIdx.Value); ok {
1007  					idx = v
1008  				}
1009  			}
1010  			val := store.Val
1011  			if mi, ok := val.(*ssa.MakeInterface); ok {
1012  				val = mi.X
1013  			}
1014  			indexed = append(indexed, indexedValue{idx, val})
1015  		}
1016  	}
1017  
1018  	for i := 1; i < len(indexed); i++ {
1019  		for j := i; j > 0 && indexed[j].index < indexed[j-1].index; j-- {
1020  			indexed[j], indexed[j-1] = indexed[j-1], indexed[j]
1021  		}
1022  	}
1023  
1024  	result := make([]ssa.Value, len(indexed))
1025  	for i, iv := range indexed {
1026  		result[i] = iv.value
1027  	}
1028  	return result
1029  }
1030  
1031  // emitSelect generates JS for a select statement.
1032  func (fc *FunctionCompiler) emitSelect(instr *ssa.Select) {
1033  	dst := fc.local(instr)
1034  
1035  	var cases []string
1036  	for i, state := range instr.States {
1037  		ch := fc.value(state.Chan)
1038  		if state.Dir == types.SendOnly {
1039  			val := fc.value(state.Send)
1040  			cases = append(cases, fmt.Sprintf("{ ch: %s, dir: 'send', value: %s, id: %d }", ch, val, i))
1041  		} else {
1042  			cases = append(cases, fmt.Sprintf("{ ch: %s, dir: 'recv', id: %d }", ch, i))
1043  		}
1044  	}
1045  
1046  	hasDefault := "false"
1047  	if !instr.Blocking {
1048  		hasDefault = "true"
1049  	}
1050  
1051  	fc.e.Line("%s = await $rt.channel.select([%s], %s);",
1052  		dst, strings.Join(cases, ", "), hasDefault)
1053  }
1054  
1055  // emitConvert handles type conversions.
1056  func (fc *FunctionCompiler) emitConvert(instr *ssa.Convert) string {
1057  	x := fc.value(instr.X)
1058  	fromType := instr.X.Type().Underlying()
1059  	toType := instr.Type().Underlying()
1060  
1061  	fromBasic, fromIsBasic := fromType.(*types.Basic)
1062  	toBasic, toIsBasic := toType.(*types.Basic)
1063  
1064  	// String conversions.
1065  	if toIsBasic && toBasic.Kind() == types.String {
1066  		if fromIsBasic && fromBasic.Info()&types.IsInteger != 0 {
1067  			return fmt.Sprintf("String.fromCodePoint(%s)", x)
1068  		}
1069  		if _, ok := fromType.(*types.Slice); ok {
1070  			return fmt.Sprintf("$rt.builtin.bytesToString(%s)", x)
1071  		}
1072  	}
1073  
1074  	if fromIsBasic && fromBasic.Kind() == types.String {
1075  		if _, ok := toType.(*types.Slice); ok {
1076  			return fmt.Sprintf("$rt.builtin.stringToBytes(%s)", x)
1077  		}
1078  	}
1079  
1080  	// Numeric conversions.
1081  	if fromIsBasic && toIsBasic {
1082  		from64 := fromBasic.Kind() == types.Int64 || fromBasic.Kind() == types.Uint64
1083  		to64 := toBasic.Kind() == types.Int64 || toBasic.Kind() == types.Uint64
1084  
1085  		// BigInt (64-bit) → Number (float/smaller int).
1086  		if from64 && toBasic.Info()&types.IsFloat != 0 {
1087  			return fmt.Sprintf("Number(%s)", x)
1088  		}
1089  		if from64 && toBasic.Info()&types.IsInteger != 0 && !to64 {
1090  			return fc.intTruncate(fmt.Sprintf("Number(%s)", x), toBasic)
1091  		}
1092  
1093  		// Number → BigInt (64-bit).
1094  		if !from64 && to64 {
1095  			if toBasic.Kind() == types.Uint64 {
1096  				return fmt.Sprintf("BigInt.asUintN(64, BigInt(%s))", x)
1097  			}
1098  			return fmt.Sprintf("BigInt(%s)", x)
1099  		}
1100  
1101  		// float → int (both Number).
1102  		if toBasic.Info()&types.IsFloat != 0 && fromBasic.Info()&types.IsInteger != 0 {
1103  			if from64 {
1104  				return fmt.Sprintf("Number(%s)", x)
1105  			}
1106  			return x // JS numbers are already float64
1107  		}
1108  		if toBasic.Info()&types.IsInteger != 0 && fromBasic.Info()&types.IsFloat != 0 {
1109  			if to64 {
1110  				return fmt.Sprintf("BigInt(Math.trunc(%s))", x)
1111  			}
1112  			return fmt.Sprintf("Math.trunc(%s)", x)
1113  		}
1114  		if toBasic.Info()&types.IsInteger != 0 && fromBasic.Info()&types.IsInteger != 0 {
1115  			return fc.intTruncate(x, toBasic)
1116  		}
1117  	}
1118  
1119  	return x
1120  }
1121  
1122  // intTruncate wraps a value to fit a specific integer type.
1123  func (fc *FunctionCompiler) intTruncate(x string, t *types.Basic) string {
1124  	switch t.Kind() {
1125  	case types.Int8:
1126  		return fmt.Sprintf("((%s << 24) >> 24)", x)
1127  	case types.Int16:
1128  		return fmt.Sprintf("((%s << 16) >> 16)", x)
1129  	case types.Int32:
1130  		return fmt.Sprintf("(%s | 0)", x)
1131  	case types.Uint8:
1132  		return fmt.Sprintf("(%s & 0xFF)", x)
1133  	case types.Uint16:
1134  		return fmt.Sprintf("(%s & 0xFFFF)", x)
1135  	case types.Uint32:
1136  		return fmt.Sprintf("(%s >>> 0)", x)
1137  	case types.Int64:
1138  		return fmt.Sprintf("BigInt.asIntN(64, BigInt(%s))", x)
1139  	case types.Uint64:
1140  		return fmt.Sprintf("BigInt.asUintN(64, BigInt(%s))", x)
1141  	default:
1142  		return x
1143  	}
1144  }
1145  
1146  // value returns the JS expression for an SSA value.
1147  func (fc *FunctionCompiler) value(v ssa.Value) string {
1148  	if v == nil {
1149  		return "undefined"
1150  	}
1151  
1152  	// Constants.
1153  	if c, ok := v.(*ssa.Const); ok {
1154  		return fc.constValue(c)
1155  	}
1156  
1157  	// Functions.
1158  	if fn, ok := v.(*ssa.Function); ok {
1159  		return functionJsName(fn)
1160  	}
1161  
1162  	// Globals.
1163  	if g, ok := v.(*ssa.Global); ok {
1164  		pkg := g.Package()
1165  		if pkg != nil && pkg.Pkg.Path() != fc.fn.Package().Pkg.Path() {
1166  			return fmt.Sprintf("%s.%s", JsIdentifier(pkg.Pkg.Path()), JsIdentifier(g.Name()))
1167  		}
1168  		return JsIdentifier(g.Name())
1169  	}
1170  
1171  	// Builtins.
1172  	if _, ok := v.(*ssa.Builtin); ok {
1173  		return "/* builtin */"
1174  	}
1175  
1176  	// Local variable.
1177  	if name, ok := fc.locals[v]; ok {
1178  		return name
1179  	}
1180  
1181  	return fmt.Sprintf("/* unknown: %T */", v)
1182  }
1183  
1184  // local returns the JS variable name for an SSA value (creating one if needed).
1185  func (fc *FunctionCompiler) local(v ssa.Value) string {
1186  	if name, ok := fc.locals[v]; ok {
1187  		return name
1188  	}
1189  	name := fc.freshVar(v.Name())
1190  	fc.locals[v] = name
1191  	return name
1192  }
1193  
1194  // is64bit returns true if the type is int64 or uint64.
1195  func is64bit(t types.Type) bool {
1196  	basic, ok := t.Underlying().(*types.Basic)
1197  	if !ok {
1198  		return false
1199  	}
1200  	return basic.Kind() == types.Int64 || basic.Kind() == types.Uint64
1201  }
1202  
1203  // constValue returns the JS literal for an SSA constant.
1204  func (fc *FunctionCompiler) constValue(c *ssa.Const) string {
1205  	if c.Value == nil {
1206  		// Typed nil or zero value.
1207  		if is64bit(c.Type()) {
1208  			return "0n"
1209  		}
1210  		return fc.pc.typeMapper.ZeroExpr(c.Type())
1211  	}
1212  
1213  	switch c.Value.Kind() {
1214  	case constant.Bool:
1215  		if constant.BoolVal(c.Value) {
1216  			return "true"
1217  		}
1218  		return "false"
1219  	case constant.Int:
1220  		// 64-bit integers use BigInt (suffix n) for full precision.
1221  		if is64bit(c.Type()) {
1222  			if v, exact := constant.Int64Val(c.Value); exact {
1223  				return fmt.Sprintf("%dn", v)
1224  			}
1225  			if v, exact := constant.Uint64Val(c.Value); exact {
1226  				return fmt.Sprintf("%dn", v)
1227  			}
1228  			return c.Value.ExactString() + "n"
1229  		}
1230  		if v, exact := constant.Int64Val(c.Value); exact {
1231  			return fmt.Sprintf("%d", v)
1232  		}
1233  		if v, exact := constant.Uint64Val(c.Value); exact {
1234  			return fmt.Sprintf("%d", v)
1235  		}
1236  		return c.Value.ExactString()
1237  	case constant.Float:
1238  		f, _ := constant.Float64Val(c.Value)
1239  		return fmt.Sprintf("%g", f)
1240  	case constant.String:
1241  		return JsString(constant.StringVal(c.Value))
1242  	case constant.Complex:
1243  		re, _ := constant.Float64Val(constant.Real(c.Value))
1244  		im, _ := constant.Float64Val(constant.Imag(c.Value))
1245  		return fmt.Sprintf("{ re: %g, im: %g }", re, im)
1246  	default:
1247  		return "null"
1248  	}
1249  }
1250  
1251  // intTypeInfo returns (bitSize, isUnsigned) for an integer type.
1252  func intTypeInfo(t types.Type) (int, bool) {
1253  	basic, ok := t.Underlying().(*types.Basic)
1254  	if !ok {
1255  		return 0, false
1256  	}
1257  	switch basic.Kind() {
1258  	case types.Uint8:
1259  		return 8, true
1260  	case types.Uint16:
1261  		return 16, true
1262  	case types.Uint32:
1263  		return 32, true
1264  	case types.Uint64:
1265  		return 64, true
1266  	case types.Uint, types.Uintptr:
1267  		return 32, true // Moxie: int/uint are always 32-bit
1268  	case types.Int8:
1269  		return 8, false
1270  	case types.Int16:
1271  		return 16, false
1272  	case types.Int32:
1273  		return 32, false
1274  	case types.Int64:
1275  		return 64, false
1276  	case types.Int:
1277  		return 32, false // Moxie: int/uint are always 32-bit
1278  	}
1279  	return 0, false
1280  }
1281  
1282  // wrapUint wraps an expression to the correct unsigned bit width.
1283  func wrapUint(expr string, bits int) string {
1284  	switch bits {
1285  	case 8:
1286  		return fmt.Sprintf("((%s) & 0xFF)", expr)
1287  	case 16:
1288  		return fmt.Sprintf("((%s) & 0xFFFF)", expr)
1289  	case 32:
1290  		return fmt.Sprintf("((%s) >>> 0)", expr)
1291  	case 64:
1292  		return fmt.Sprintf("BigInt.asUintN(64, %s)", expr)
1293  	default:
1294  		return expr
1295  	}
1296  }
1297  
1298  // binOp returns a function that generates the JS binary expression.
1299  func (fc *FunctionCompiler) binOp(op token.Token, t types.Type) func(x, y string) string {
1300  	isString := false
1301  	if basic, ok := t.Underlying().(*types.Basic); ok {
1302  		isString = basic.Info()&types.IsString != 0
1303  	}
1304  	// Moxie: string=[]byte — a []byte slice is also a string.
1305  	if !isString {
1306  		if sl, ok := t.Underlying().(*types.Slice); ok {
1307  			if basic, ok := sl.Elem().Underlying().(*types.Basic); ok {
1308  				isString = basic.Kind() == types.Byte
1309  			}
1310  		}
1311  	}
1312  
1313  	bits, unsigned := intTypeInfo(t)
1314  
1315  	// 64-bit integer operations use BigInt for full precision.
1316  	if bits == 64 {
1317  		wrap := "BigInt.asUintN"
1318  		if !unsigned {
1319  			wrap = "BigInt.asIntN"
1320  		}
1321  		switch op {
1322  		case token.ADD:
1323  			return func(x, y string) string { return fmt.Sprintf("%s(64, %s + %s)", wrap, x, y) }
1324  		case token.SUB:
1325  			return func(x, y string) string { return fmt.Sprintf("%s(64, %s - %s)", wrap, x, y) }
1326  		case token.MUL:
1327  			return func(x, y string) string { return fmt.Sprintf("%s(64, %s * %s)", wrap, x, y) }
1328  		case token.QUO:
1329  			return func(x, y string) string { return fmt.Sprintf("%s(64, %s / %s)", wrap, x, y) }
1330  		case token.REM:
1331  			return func(x, y string) string { return fmt.Sprintf("(%s %% %s)", x, y) }
1332  		case token.AND:
1333  			return func(x, y string) string { return fmt.Sprintf("(%s & %s)", x, y) }
1334  		case token.OR:
1335  			return func(x, y string) string { return fmt.Sprintf("(%s | %s)", x, y) }
1336  		case token.XOR:
1337  			return func(x, y string) string { return fmt.Sprintf("(%s ^ %s)", x, y) }
1338  		case token.SHL:
1339  			// Shift amount may be a Number (e.g. uint); coerce to BigInt.
1340  			return func(x, y string) string { return fmt.Sprintf("%s(64, %s << BigInt(%s))", wrap, x, y) }
1341  		case token.SHR:
1342  			if unsigned {
1343  				return func(x, y string) string { return fmt.Sprintf("(%s >> BigInt(%s))", x, y) }
1344  			}
1345  			return func(x, y string) string {
1346  				return fmt.Sprintf("BigInt.asIntN(64, %s >> BigInt(%s))", x, y)
1347  			}
1348  		case token.AND_NOT:
1349  			return func(x, y string) string { return fmt.Sprintf("(%s & ~%s)", x, y) }
1350  		case token.EQL:
1351  			return func(x, y string) string { return fmt.Sprintf("(%s === %s)", x, y) }
1352  		case token.NEQ:
1353  			return func(x, y string) string { return fmt.Sprintf("(%s !== %s)", x, y) }
1354  		case token.LSS:
1355  			return func(x, y string) string { return fmt.Sprintf("(%s < %s)", x, y) }
1356  		case token.LEQ:
1357  			return func(x, y string) string { return fmt.Sprintf("(%s <= %s)", x, y) }
1358  		case token.GTR:
1359  			return func(x, y string) string { return fmt.Sprintf("(%s > %s)", x, y) }
1360  		case token.GEQ:
1361  			return func(x, y string) string { return fmt.Sprintf("(%s >= %s)", x, y) }
1362  		}
1363  	}
1364  
1365  	switch op {
1366  	case token.ADD:
1367  		if isString {
1368  			return func(x, y string) string { return fmt.Sprintf("(%s + %s)", x, y) }
1369  		}
1370  		if unsigned && bits <= 32 {
1371  			return func(x, y string) string { return wrapUint(fmt.Sprintf("%s + %s", x, y), bits) }
1372  		}
1373  		return func(x, y string) string { return fmt.Sprintf("(%s + %s)", x, y) }
1374  	case token.SUB:
1375  		if unsigned && bits <= 32 {
1376  			return func(x, y string) string { return wrapUint(fmt.Sprintf("%s - %s", x, y), bits) }
1377  		}
1378  		return func(x, y string) string { return fmt.Sprintf("(%s - %s)", x, y) }
1379  	case token.MUL:
1380  		if unsigned && bits == 32 {
1381  			// Math.imul gives low 32 bits of integer multiply, then >>> 0 for unsigned.
1382  			return func(x, y string) string { return fmt.Sprintf("(Math.imul(%s, %s) >>> 0)", x, y) }
1383  		}
1384  		if unsigned && bits < 32 {
1385  			return func(x, y string) string { return wrapUint(fmt.Sprintf("%s * %s", x, y), bits) }
1386  		}
1387  		return func(x, y string) string { return fmt.Sprintf("(%s * %s)", x, y) }
1388  	case token.QUO:
1389  		// Integer division in Go truncates toward zero.
1390  		if basic, ok := t.Underlying().(*types.Basic); ok && basic.Info()&types.IsInteger != 0 {
1391  			if unsigned && bits <= 32 {
1392  				return func(x, y string) string { return wrapUint(fmt.Sprintf("Math.trunc(%s / %s)", x, y), bits) }
1393  			}
1394  			return func(x, y string) string { return fmt.Sprintf("Math.trunc(%s / %s)", x, y) }
1395  		}
1396  		return func(x, y string) string { return fmt.Sprintf("(%s / %s)", x, y) }
1397  	case token.REM:
1398  		if unsigned && bits <= 32 {
1399  			return func(x, y string) string { return wrapUint(fmt.Sprintf("%s %% %s", x, y), bits) }
1400  		}
1401  		return func(x, y string) string { return fmt.Sprintf("(%s %% %s)", x, y) }
1402  	case token.AND:
1403  		if unsigned && bits == 32 {
1404  			return func(x, y string) string { return fmt.Sprintf("((%s & %s) >>> 0)", x, y) }
1405  		}
1406  		return func(x, y string) string { return fmt.Sprintf("(%s & %s)", x, y) }
1407  	case token.OR:
1408  		if unsigned && bits == 32 {
1409  			return func(x, y string) string { return fmt.Sprintf("((%s | %s) >>> 0)", x, y) }
1410  		}
1411  		return func(x, y string) string { return fmt.Sprintf("(%s | %s)", x, y) }
1412  	case token.XOR:
1413  		if unsigned && bits == 32 {
1414  			return func(x, y string) string { return fmt.Sprintf("((%s ^ %s) >>> 0)", x, y) }
1415  		}
1416  		return func(x, y string) string { return fmt.Sprintf("(%s ^ %s)", x, y) }
1417  	case token.SHL:
1418  		if unsigned && bits <= 32 {
1419  			return func(x, y string) string { return wrapUint(fmt.Sprintf("%s << %s", x, y), bits) }
1420  		}
1421  		return func(x, y string) string { return fmt.Sprintf("(%s << %s)", x, y) }
1422  	case token.SHR:
1423  		if unsigned {
1424  			if bits == 32 {
1425  				return func(x, y string) string { return fmt.Sprintf("(%s >>> %s)", x, y) }
1426  			}
1427  			if bits < 32 {
1428  				return func(x, y string) string { return fmt.Sprintf("((%s & %s) >> %s)", x, wrapMask(bits), y) }
1429  			}
1430  			return func(x, y string) string { return fmt.Sprintf("(%s >>> %s)", x, y) }
1431  		}
1432  		return func(x, y string) string { return fmt.Sprintf("(%s >> %s)", x, y) }
1433  	case token.AND_NOT:
1434  		if unsigned && bits == 32 {
1435  			return func(x, y string) string { return fmt.Sprintf("((%s & ~%s) >>> 0)", x, y) }
1436  		}
1437  		return func(x, y string) string { return fmt.Sprintf("(%s & ~%s)", x, y) }
1438  	case token.EQL:
1439  		if isString {
1440  			return func(x, y string) string { return fmt.Sprintf("$rt.builtin.stringEqual(%s, %s)", x, y) }
1441  		}
1442  		return func(x, y string) string { return fmt.Sprintf("(%s === %s)", x, y) }
1443  	case token.NEQ:
1444  		if isString {
1445  			return func(x, y string) string { return fmt.Sprintf("!$rt.builtin.stringEqual(%s, %s)", x, y) }
1446  		}
1447  		return func(x, y string) string { return fmt.Sprintf("(%s !== %s)", x, y) }
1448  	case token.LSS:
1449  		return func(x, y string) string { return fmt.Sprintf("(%s < %s)", x, y) }
1450  	case token.LEQ:
1451  		return func(x, y string) string { return fmt.Sprintf("(%s <= %s)", x, y) }
1452  	case token.GTR:
1453  		return func(x, y string) string { return fmt.Sprintf("(%s > %s)", x, y) }
1454  	case token.GEQ:
1455  		return func(x, y string) string { return fmt.Sprintf("(%s >= %s)", x, y) }
1456  	default:
1457  		return func(x, y string) string { return fmt.Sprintf("/* TODO: %s */ (%s ? %s)", op, x, y) }
1458  	}
1459  }
1460  
1461  func wrapMask(bits int) string {
1462  	switch bits {
1463  	case 8:
1464  		return "0xFF"
1465  	case 16:
1466  		return "0xFFFF"
1467  	default:
1468  		return "0xFFFFFFFF"
1469  	}
1470  }
1471  
1472  // needsClone returns true if a Go type needs deep-cloning when stored via pointer.
1473  // Arrays and structs are value types in Go; in JS they're reference types.
1474  func needsClone(t types.Type) bool {
1475  	switch t.Underlying().(type) {
1476  	case *types.Array:
1477  		return true
1478  	case *types.Struct:
1479  		return true
1480  	}
1481  	return false
1482  }
1483  
1484  // needsAsync checks if this function needs to be declared async.
1485  // Uses the precomputed transitive async set from ProgramCompiler.
1486  func (fc *FunctionCompiler) needsAsync() bool {
1487  	return fc.pc.asyncFuncs[fc.fn]
1488  }
1489  
1490  // fieldJsName returns the JS property name for a struct field by index.
1491  func fieldJsName(t types.Type, index int) string {
1492  	switch t := t.Underlying().(type) {
1493  	case *types.Struct:
1494  		if index < t.NumFields() {
1495  			return JsIdentifier(t.Field(index).Name())
1496  		}
1497  	}
1498  	return fmt.Sprintf("$field_%d", index)
1499  }
1500