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