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  	"golang.org/x/tools/go/ssa"
  14  	"tinygo.org/x/go-llvm"
  15  )
  16  
  17  // createSpawn handles spawn(fn, args...) builtin calls. The compiler emits
  18  // a call to runtime.spawnDomain with the function pointer and serialized
  19  // arguments.
  20  //
  21  // As a builtin, the SSA passes args directly — no MakeInterface wrapping,
  22  // no variadic slice packing.
  23  //
  24  // If the first argument is a string constant, it specifies the transport:
  25  //   spawn("local", fn, args...)  — fork+socketpair (default)
  26  //   spawn("pipe", fn, args...)   — alias for local
  27  //   spawn(fn, args...)           — implicit local
  28  func (b *builder) createSpawn(instr *ssa.CallCommon) (llvm.Value, error) {
  29  	if len(instr.Args) < 1 {
  30  		b.addError(instr.Pos(), "spawn requires at least a function argument")
  31  		return llvm.Value{}, nil
  32  	}
  33  
  34  	// Check if the first argument is a transport string constant.
  35  	transport := "local"
  36  	argStart := 0 // index of the function argument
  37  	if transportStr, ok := extractTransportString(instr.Args[0]); ok {
  38  		transport = transportStr
  39  		argStart = 1
  40  	}
  41  
  42  	// Validate transport.
  43  	switch transport {
  44  	case "local", "pipe":
  45  		// OK — use fork+socketpair.
  46  	default:
  47  		if len(transport) > 0 && (hasScheme(transport, "tcp") || hasScheme(transport, "ws") || hasScheme(transport, "wss")) {
  48  			b.addError(instr.Pos(), fmt.Sprintf("spawn: network transport %q not yet implemented", transport))
  49  			return llvm.Value{}, nil
  50  		}
  51  		b.addError(instr.Pos(), fmt.Sprintf("spawn: unknown transport %q (expected \"local\", \"pipe\", or a network URL)", transport))
  52  		return llvm.Value{}, nil
  53  	}
  54  
  55  	if argStart >= len(instr.Args) {
  56  		b.addError(instr.Pos(), "spawn requires a function argument after transport string")
  57  		return llvm.Value{}, nil
  58  	}
  59  
  60  	// Extract the function argument.
  61  	fnArg := instr.Args[argStart]
  62  	var targetFn *ssa.Function
  63  	switch v := fnArg.(type) {
  64  	case *ssa.Function:
  65  		targetFn = v
  66  	case *ssa.MakeClosure:
  67  		if fn, ok := v.Fn.(*ssa.Function); ok {
  68  			targetFn = fn
  69  		}
  70  	}
  71  
  72  	if targetFn == nil {
  73  		b.addError(instr.Pos(), "spawn: first argument must be a static function (dynamic function values not yet supported)")
  74  		return llvm.Value{}, nil
  75  	}
  76  
  77  	// Data args are everything after the function.
  78  	concreteArgs := instr.Args[argStart+1:]
  79  
  80  	// Look up the moxie.Codec interface for spawn-boundary type checking.
  81  	var codecIface *types.Interface
  82  	if moxiePkg := b.program.ImportedPackage("moxie"); moxiePkg != nil {
  83  		if obj := moxiePkg.Pkg.Scope().Lookup("Codec"); obj != nil {
  84  			if iface, ok := obj.Type().Underlying().(*types.Interface); ok {
  85  				codecIface = iface
  86  			}
  87  		}
  88  	}
  89  
  90  	// Validate that the function signature's parameters are spawn-safe
  91  	// and implement moxie.Codec.
  92  	sig := targetFn.Signature
  93  	for i := 0; i < sig.Params().Len(); i++ {
  94  		if err := validateSpawnArg(sig.Params().At(i).Type(), i, codecIface); err != nil {
  95  			b.addError(instr.Pos(), err.Error())
  96  			return llvm.Value{}, nil
  97  		}
  98  	}
  99  
 100  	if len(concreteArgs) != sig.Params().Len() {
 101  		b.addError(instr.Pos(), fmt.Sprintf("spawn: %s expects %d arguments, got %d",
 102  			targetFn.Name(), sig.Params().Len(), len(concreteArgs)))
 103  		return llvm.Value{}, nil
 104  	}
 105  
 106  	// Verify argument types match the target function's parameter types.
 107  	for i, arg := range concreteArgs {
 108  		paramType := sig.Params().At(i).Type()
 109  		argType := arg.Type()
 110  		if !types.Identical(argType.Underlying(), paramType.Underlying()) {
 111  			b.addError(instr.Pos(), fmt.Sprintf("spawn: argument %d has type %s, %s expects %s",
 112  				i+1, argType, targetFn.Name(), paramType))
 113  			return llvm.Value{}, nil
 114  		}
 115  	}
 116  
 117  	// Reject buffered channels across spawn boundaries.
 118  	for i, arg := range concreteArgs {
 119  		if _, isChan := arg.Type().Underlying().(*types.Chan); !isChan {
 120  			continue
 121  		}
 122  		if mc := traceToMakeChan(arg); mc != nil {
 123  			if c, ok := mc.Size.(*ssa.Const); ok {
 124  				if c.Value != nil && constant.Sign(c.Value) > 0 {
 125  					b.addError(instr.Pos(), fmt.Sprintf(
 126  						"spawn: argument %d is a buffered channel — only unbuffered channels allowed across spawn boundaries",
 127  						i+1))
 128  					return llvm.Value{}, nil
 129  				}
 130  			} else {
 131  				b.addError(instr.Pos(), fmt.Sprintf(
 132  					"spawn: argument %d has dynamic channel buffer size — only unbuffered channels (make(chan T)) allowed across spawn boundaries",
 133  					i+1))
 134  				return llvm.Value{}, nil
 135  			}
 136  		}
 137  	}
 138  
 139  	var params []llvm.Value
 140  	for _, arg := range concreteArgs {
 141  		params = append(params, b.expandFormalParam(b.getValue(arg, instr.Pos()))...)
 142  	}
 143  
 144  	paramBundle := b.emitPointerPack(params)
 145  
 146  	// Get the LLVM function.
 147  	funcType, funcPtr := b.getFunction(targetFn)
 148  
 149  	// Create a goroutine-style wrapper that unpacks args and calls the function.
 150  	wrapper := b.createGoroutineStartWrapper(funcType, funcPtr, "spawn", false, false, instr.Pos())
 151  
 152  	// Call runtime.spawnDomain(wrapper, paramBundle).
 153  	spawnFnType, spawnFn := b.getFunction(b.program.ImportedPackage("runtime").Members["spawnDomain"].(*ssa.Function))
 154  	b.createCall(spawnFnType, spawnFn, []llvm.Value{wrapper, paramBundle, llvm.Undef(b.dataPtrType)}, "")
 155  
 156  	// Return a lifecycle channel (chan struct{}).
 157  	// TODO: wire this to the socketpair so close propagates.
 158  	chanType := b.getLLVMRuntimeType("channel")
 159  	return llvm.ConstPointerNull(llvm.PointerType(chanType, 0)), nil
 160  }
 161  
 162  // validateSpawnArg checks that a type is safe to pass across a spawn boundary.
 163  // Channels are allowed as bare top-level parameters (they become IPC channels);
 164  // their element types must implement moxie.Codec. Non-channel arguments must
 165  // implement moxie.Codec directly. No interface types anywhere.
 166  func validateSpawnArg(t types.Type, argIndex int, codec *types.Interface) error {
 167  	if ch, ok := t.Underlying().(*types.Chan); ok {
 168  		// Top-level channel: element type must be spawn-safe and implement Codec.
 169  		return validateSpawnElem(ch.Elem(), argIndex, codec)
 170  	}
 171  	return validateSpawnElem(t, argIndex, codec)
 172  }
 173  
 174  // validateSpawnElem rejects types that can't be serialized across a spawn
 175  // boundary. Named types that implement moxie.Codec are accepted immediately.
 176  // Channels are rejected here — they're only allowed at the top level.
 177  func validateSpawnElem(t types.Type, argIndex int, codec *types.Interface) error {
 178  	// Named type that implements Codec — accept directly.
 179  	// This handles moxie.Int32, moxie.Bytes, user structs with Codec methods, etc.
 180  	if codec != nil && implementsCodec(t, codec) {
 181  		return nil
 182  	}
 183  
 184  	switch ut := t.Underlying().(type) {
 185  	case *types.Pointer:
 186  		return fmt.Errorf("spawn: argument %d is a pointer type %s (not allowed across spawn boundary)", argIndex+1, ut)
 187  	case *types.Chan:
 188  		return fmt.Errorf("spawn: argument %d contains a nested channel (channels must be top-level parameters)", argIndex+1)
 189  	case *types.Slice:
 190  		return validateSpawnElem(ut.Elem(), argIndex, codec)
 191  	case *types.Map:
 192  		if err := validateSpawnElem(ut.Key(), argIndex, codec); err != nil {
 193  			return err
 194  		}
 195  		return validateSpawnElem(ut.Elem(), argIndex, codec)
 196  	case *types.Signature:
 197  		return fmt.Errorf("spawn: argument %d is a function type (not allowed across spawn boundary)", argIndex+1)
 198  	case *types.Interface:
 199  		return fmt.Errorf("spawn: argument %d is an interface type (not allowed across spawn boundary)", argIndex+1)
 200  	case *types.Struct:
 201  		for i := 0; i < ut.NumFields(); i++ {
 202  			if err := validateSpawnElem(ut.Field(i).Type(), argIndex, codec); err != nil {
 203  				return err
 204  			}
 205  		}
 206  		return nil
 207  	case *types.Array:
 208  		return validateSpawnElem(ut.Elem(), argIndex, codec)
 209  	}
 210  
 211  	// Leaf type that doesn't implement Codec.
 212  	if codec != nil {
 213  		return fmt.Errorf("spawn: argument %d type %s does not implement moxie.Codec (use moxie.Int32, moxie.Uint64, etc.)", argIndex+1, t)
 214  	}
 215  	return nil
 216  }
 217  
 218  // implementsCodec checks whether t or *t satisfies the Codec interface.
 219  func implementsCodec(t types.Type, codec *types.Interface) bool {
 220  	return types.Implements(t, codec) || types.Implements(types.NewPointer(t), codec)
 221  }
 222  
 223  // extractTransportString checks if an SSA value is a string constant
 224  // (possibly wrapped in MakeInterface) and returns it.
 225  func extractTransportString(v ssa.Value) (string, bool) {
 226  	if mi, ok := v.(*ssa.MakeInterface); ok {
 227  		v = mi.X
 228  	}
 229  	c, ok := v.(*ssa.Const)
 230  	if !ok {
 231  		return "", false
 232  	}
 233  	if c.Value == nil || c.Value.Kind() != constant.String {
 234  		return "", false
 235  	}
 236  	return constant.StringVal(c.Value), true
 237  }
 238  
 239  // traceToMakeChan walks an SSA value back to its MakeChan definition.
 240  // Returns nil if the channel origin can't be determined (parameter, load, etc).
 241  func traceToMakeChan(v ssa.Value) *ssa.MakeChan {
 242  	switch v := v.(type) {
 243  	case *ssa.MakeChan:
 244  		return v
 245  	case *ssa.MakeInterface:
 246  		return traceToMakeChan(v.X)
 247  	case *ssa.Phi:
 248  		for _, edge := range v.Edges {
 249  			if mc := traceToMakeChan(edge); mc != nil {
 250  				return mc
 251  			}
 252  		}
 253  	}
 254  	return nil
 255  }
 256  
 257  // hasScheme checks if s starts with scheme://.
 258  func hasScheme(s, scheme string) bool {
 259  	if len(s) < len(scheme)+3 {
 260  		return false
 261  	}
 262  	for i := 0; i < len(scheme); i++ {
 263  		if s[i] != scheme[i] {
 264  			return false
 265  		}
 266  	}
 267  	return s[len(scheme)] == ':' && s[len(scheme)+1] == '/' && s[len(scheme)+2] == '/'
 268  }
 269