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