spawn.go raw

   1  package compiler
   2  
   3  // This file implements the 'spawn' mechanism for creating isolated domains.
   4  // spawn creates a new OS process (native) or Service Worker (JS) with its
   5  // own cooperative scheduler. Arguments are serialized across the boundary.
   6  // Channels become IPC channels.
   7  
   8  import (
   9  	"fmt"
  10  	"go/constant"
  11  	"go/types"
  12  
  13  	"moxie/loader"
  14  	"golang.org/x/tools/go/ssa"
  15  	"tinygo.org/x/go-llvm"
  16  )
  17  
  18  // createSpawn handles spawn(fn, args...) builtin calls. The compiler emits
  19  // a call to runtime.spawnDomain with the function pointer and serialized
  20  // arguments.
  21  //
  22  // As a builtin, the SSA passes args directly — no MakeInterface wrapping,
  23  // no variadic slice packing.
  24  //
  25  // If the first argument is a string constant, it specifies the transport:
  26  //   spawn("local", fn, args...)  — fork+socketpair (default)
  27  //   spawn("pipe", fn, args...)   — alias for local
  28  //   spawn(fn, args...)           — implicit local
  29  func (b *builder) createSpawn(instr *ssa.CallCommon) (llvm.Value, error) {
  30  	if len(instr.Args) < 1 {
  31  		b.addError(instr.Pos(), "spawn requires at least a function argument")
  32  		return llvm.Value{}, nil
  33  	}
  34  
  35  	// Wasm target uses a completely different spawn mechanism (Workers + SAB channels).
  36  	if b.isWasmTarget() {
  37  		return b.createWasmSpawn(instr)
  38  	}
  39  	// Check if the first argument is a transport string constant.
  40  	transport := "local"
  41  	argStart := 0 // index of the function argument
  42  	if transportStr, ok := extractTransportString(instr.Args[0]); ok {
  43  		transport = transportStr
  44  		argStart = 1
  45  	}
  46  
  47  	// Validate transport.
  48  	switch transport {
  49  	case "local", "pipe":
  50  		// OK — use fork+socketpair.
  51  	default:
  52  		if len(transport) > 0 && (hasScheme(transport, "tcp") || hasScheme(transport, "ws") || hasScheme(transport, "wss")) {
  53  			b.addError(instr.Pos(), fmt.Sprintf("spawn: network transport %q not yet implemented", transport))
  54  			return llvm.Value{}, nil
  55  		}
  56  		b.addError(instr.Pos(), fmt.Sprintf("spawn: unknown transport %q (expected \"local\", \"pipe\", or a network URL)", transport))
  57  		return llvm.Value{}, nil
  58  	}
  59  
  60  	if argStart >= len(instr.Args) {
  61  		b.addError(instr.Pos(), "spawn requires a function argument after transport string")
  62  		return llvm.Value{}, nil
  63  	}
  64  
  65  	// Extract the function argument.
  66  	fnArg := instr.Args[argStart]
  67  	var targetFn *ssa.Function
  68  	switch v := fnArg.(type) {
  69  	case *ssa.Function:
  70  		targetFn = v
  71  	case *ssa.MakeClosure:
  72  		if fn, ok := v.Fn.(*ssa.Function); ok {
  73  			targetFn = fn
  74  		}
  75  	}
  76  
  77  	if targetFn == nil {
  78  		b.addError(instr.Pos(), "spawn: first argument must be a static function (dynamic function values not yet supported)")
  79  		return llvm.Value{}, nil
  80  	}
  81  
  82  	// Cross-binary spawn: target is in a .mxh package (loaded from cache).
  83  	if b.isExternalMXHSpawn(targetFn) {
  84  		return b.createCrossBinarySpawn(targetFn, instr)
  85  	}
  86  
  87  	// Data args are everything after the function.
  88  	concreteArgs := instr.Args[argStart+1:]
  89  
  90  	// Look up the moxie.Codec interface for spawn-boundary type checking.
  91  	// runtime transitively imports moxie, so this should always succeed;
  92  	// the explicit errors turn a formerly-silent permissive path into a
  93  	// clear compiler failure if the stdlib wiring is ever broken.
  94  	moxiePkg := b.program.ImportedPackage("moxie")
  95  	if moxiePkg == nil {
  96  		b.addError(instr.Pos(), "spawn: internal error — \"moxie\" package not in program (runtime wiring broken)")
  97  		return llvm.Value{}, nil
  98  	}
  99  	codecObj := moxiePkg.Pkg.Scope().Lookup("Codec")
 100  	if codecObj == nil {
 101  		b.addError(instr.Pos(), "spawn: internal error — moxie.Codec symbol missing from moxie package")
 102  		return llvm.Value{}, nil
 103  	}
 104  	codecIface, ok := codecObj.Type().Underlying().(*types.Interface)
 105  	if !ok {
 106  		b.addError(instr.Pos(), "spawn: internal error — moxie.Codec is not an interface type")
 107  		return llvm.Value{}, nil
 108  	}
 109  
 110  	// Validate that the function signature's parameters are spawn-safe
 111  	// and implement moxie.Codec.
 112  	sig := targetFn.Signature
 113  	for i := 0; i < sig.Params().Len(); i++ {
 114  		if err := validateSpawnArg(sig.Params().At(i).Type(), i, codecIface); err != nil {
 115  			b.addError(instr.Pos(), err.Error())
 116  			return llvm.Value{}, nil
 117  		}
 118  	}
 119  
 120  	if len(concreteArgs) != sig.Params().Len() {
 121  		b.addError(instr.Pos(), fmt.Sprintf("spawn: %s expects %d arguments, got %d",
 122  			targetFn.Name(), sig.Params().Len(), len(concreteArgs)))
 123  		return llvm.Value{}, nil
 124  	}
 125  
 126  	// Verify argument types match the target function's parameter types.
 127  	for i, arg := range concreteArgs {
 128  		paramType := sig.Params().At(i).Type()
 129  		argType := arg.Type()
 130  		if !types.Identical(argType.Underlying(), paramType.Underlying()) {
 131  			b.addError(instr.Pos(), fmt.Sprintf("spawn: argument %d has type %s, %s expects %s",
 132  				i+1, argType, targetFn.Name(), paramType))
 133  			return llvm.Value{}, nil
 134  		}
 135  	}
 136  
 137  	// Reject buffered channels across spawn boundaries.
 138  	for i, arg := range concreteArgs {
 139  		if _, isChan := arg.Type().Underlying().(*types.Chan); !isChan {
 140  			continue
 141  		}
 142  		if mc := traceToMakeChan(arg); mc != nil {
 143  			if c, ok := mc.Size.(*ssa.Const); ok {
 144  				if c.Value != nil && constant.Sign(c.Value) > 0 {
 145  					b.addError(instr.Pos(), fmt.Sprintf(
 146  						"spawn: argument %d is a buffered channel — only unbuffered channels allowed across spawn boundaries",
 147  						i+1))
 148  					return llvm.Value{}, nil
 149  				}
 150  			} else {
 151  				b.addError(instr.Pos(), fmt.Sprintf(
 152  					"spawn: argument %d has dynamic channel buffer size — only unbuffered channels (make(chan T)) allowed across spawn boundaries",
 153  					i+1))
 154  				return llvm.Value{}, nil
 155  			}
 156  		}
 157  	}
 158  
 159  	// Collect channel args (in declaration order) for post-spawn marking.
 160  	var chanArgs []spawnChanArg
 161  	for _, arg := range concreteArgs {
 162  		if _, isChan := arg.Type().Underlying().(*types.Chan); isChan {
 163  			chanArgs = append(chanArgs, spawnChanArg{
 164  				ssa:  arg,
 165  				llvm: b.getValue(arg, instr.Pos()),
 166  			})
 167  		}
 168  	}
 169  
 170  	var params []llvm.Value
 171  	for _, arg := range concreteArgs {
 172  		params = append(params, b.expandFormalParam(b.getValue(arg, instr.Pos()))...)
 173  	}
 174  
 175  	paramBundle := b.emitPointerPack(params)
 176  
 177  	// Get the LLVM function.
 178  	funcType, funcPtr := b.getFunction(targetFn)
 179  
 180  	// Create a goroutine-style wrapper that unpacks args and calls the function.
 181  	wrapper := b.createGoroutineStartWrapper(funcType, funcPtr, "spawn", false, false, instr.Pos())
 182  
 183  	// Call runtime.spawnDomain(wrapper, paramBundle, nChans).
 184  	nChans := llvm.ConstInt(b.ctx.Int32Type(), uint64(len(chanArgs)), false)
 185  	spawnFnType, spawnFn := b.getFunction(b.program.ImportedPackage("runtime").Members["spawnDomain"].(*ssa.Function))
 186  	b.createCall(spawnFnType, spawnFn, []llvm.Value{wrapper, paramBundle, nChans, llvm.Undef(b.dataPtrType)}, "")
 187  
 188  	// Mark every channel arg pipe-bound on the parent side. The child's
 189  	// setupNativeSpawnTargetChannels does the symmetric mark with ChildPipeFd.
 190  	if target := b.nativeSpawnTargetFor(targetFn); target != nil {
 191  		b.emitNativeSpawnChannelMark(target, chanArgs)
 192  	}
 193  
 194  	// Block until the child signals ready.
 195  	waitFnType, waitFn := b.getFunction(b.program.ImportedPackage("runtime").Members["PipeWaitChildReady"].(*ssa.Function))
 196  	b.createCall(waitFnType, waitFn, []llvm.Value{llvm.Undef(b.dataPtrType)}, "child.ready")
 197  
 198  	// Return a lifecycle channel (chan struct{}) that closes when the child exits.
 199  	lcFnType, lcFn := b.getFunction(b.program.ImportedPackage("runtime").Members["SpawnLifecycleChannel"].(*ssa.Function))
 200  	doneCh := b.createCall(lcFnType, lcFn, []llvm.Value{llvm.Undef(b.dataPtrType)}, "done.ch")
 201  	return doneCh, nil
 202  }
 203  
 204  // validateSpawnArg checks that a type is safe to pass across a spawn boundary.
 205  // Channels are allowed as bare top-level parameters (they become IPC channels);
 206  // their element types must implement moxie.Codec. Non-channel arguments must
 207  // implement moxie.Codec directly. No interface types anywhere.
 208  func validateSpawnArg(t types.Type, argIndex int, codec *types.Interface) error {
 209  	if ch, ok := t.Underlying().(*types.Chan); ok {
 210  		// Top-level channel: element type must be spawn-safe and implement Codec.
 211  		return validateSpawnElem(ch.Elem(), argIndex, codec)
 212  	}
 213  	return validateSpawnElem(t, argIndex, codec)
 214  }
 215  
 216  // validateSpawnElem rejects types that can't be serialized across a spawn
 217  // boundary. Named types that implement moxie.Codec are accepted immediately.
 218  // Channels are rejected here - they're only allowed at the top level.
 219  // codec is non-nil (createSpawn enforces moxie.Codec availability before call).
 220  func validateSpawnElem(t types.Type, argIndex int, codec *types.Interface) error {
 221  	if loader.ImplementsCodecIface(t, codec) {
 222  		return nil
 223  	}
 224  
 225  	switch ut := t.Underlying().(type) {
 226  	case *types.Basic:
 227  		if isSpawnSafeBasic(ut) {
 228  			return nil
 229  		}
 230  		return fmt.Errorf("spawn: argument %d type %s is not spawn-safe", argIndex+1, t)
 231  	case *types.Pointer:
 232  		if loader.ImplementsCodecIface(t, codec) {
 233  			return nil
 234  		}
 235  		return fmt.Errorf("spawn: argument %d is a pointer type %s without Codec; raw pointers cannot cross spawn boundaries", argIndex+1, ut)
 236  	case *types.Chan:
 237  		return fmt.Errorf("spawn: argument %d contains a nested channel (channels must be top-level parameters)", argIndex+1)
 238  	case *types.Slice:
 239  		return validateSpawnElem(ut.Elem(), argIndex, codec)
 240  	case *types.Map:
 241  		if err := validateSpawnElem(ut.Key(), argIndex, codec); err != nil {
 242  			return err
 243  		}
 244  		return validateSpawnElem(ut.Elem(), argIndex, codec)
 245  	case *types.Signature:
 246  		return fmt.Errorf("spawn: argument %d is a function type (not allowed across spawn boundary)", argIndex+1)
 247  	case *types.Interface:
 248  		return fmt.Errorf("spawn: argument %d is an interface type (not allowed across spawn boundary)", argIndex+1)
 249  	case *types.Struct:
 250  		for i := 0; i < ut.NumFields(); i++ {
 251  			if err := validateSpawnElem(ut.Field(i).Type(), argIndex, codec); err != nil {
 252  				return err
 253  			}
 254  		}
 255  		return nil
 256  	case *types.Array:
 257  		return validateSpawnElem(ut.Elem(), argIndex, codec)
 258  	}
 259  
 260  	return fmt.Errorf("spawn: argument %d type %s does not implement moxie.Codec (use moxie.Int32, moxie.Uint64, etc.)", argIndex+1, t)
 261  }
 262  
 263  // isSpawnSafeBasic returns true for basic types that are safe to copy across
 264  // a fork-based spawn boundary (same binary, same architecture). All concrete
 265  // scalar types are safe; untyped types and unsafe.Pointer are not.
 266  func isSpawnSafeBasic(b *types.Basic) bool {
 267  	switch b.Kind() {
 268  	case types.Bool, types.Int, types.Int8, types.Int16, types.Int32, types.Int64,
 269  		types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64, types.Uintptr,
 270  		types.Float32, types.Float64, types.Complex64, types.Complex128,
 271  		types.String:
 272  		return true
 273  	}
 274  	return false
 275  }
 276  
 277  // extractTransportString checks if an SSA value is a string constant
 278  // (possibly wrapped in MakeInterface) and returns it.
 279  func extractTransportString(v ssa.Value) (string, bool) {
 280  	if mi, ok := v.(*ssa.MakeInterface); ok {
 281  		v = mi.X
 282  	}
 283  	c, ok := v.(*ssa.Const)
 284  	if !ok {
 285  		return "", false
 286  	}
 287  	if c.Value == nil || c.Value.Kind() != constant.String {
 288  		return "", false
 289  	}
 290  	return constant.StringVal(c.Value), true
 291  }
 292  
 293  // traceToMakeChan walks an SSA value back to its MakeChan definition.
 294  // Returns nil if the channel origin can't be determined (parameter, load, etc).
 295  func traceToMakeChan(v ssa.Value) *ssa.MakeChan {
 296  	switch v := v.(type) {
 297  	case *ssa.MakeChan:
 298  		return v
 299  	case *ssa.MakeInterface:
 300  		return traceToMakeChan(v.X)
 301  	case *ssa.Phi:
 302  		for _, edge := range v.Edges {
 303  			if mc := traceToMakeChan(edge); mc != nil {
 304  				return mc
 305  			}
 306  		}
 307  	}
 308  	return nil
 309  }
 310  
 311  // hasScheme checks if s starts with scheme://.
 312  func hasScheme(s, scheme string) bool {
 313  	if len(s) < len(scheme)+3 {
 314  		return false
 315  	}
 316  	for i := 0; i < len(scheme); i++ {
 317  		if s[i] != scheme[i] {
 318  			return false
 319  		}
 320  	}
 321  	return s[len(scheme)] == ':' && s[len(scheme)+1] == '/' && s[len(scheme)+2] == '/'
 322  }
 323