1 package compiler
2 3 // This file implements Moxie language restrictions.
4 // Moxie removes features that are incompatible with domain-isolated
5 // cooperative concurrency or that obstruct the programming model.
6 //
7 // Restrictions apply to user code and converted stdlib packages.
8 // Runtime/internal packages are permanently exempt.
9 10 import (
11 "go/ast"
12 "go/token"
13 "go/types"
14 "strings"
15 16 "golang.org/x/tools/go/ssa"
17 )
18 19 // restrictedImports maps package paths that should not be used in Moxie code.
20 var restrictedImports = map[string]string{
21 "strings": `use "bytes" instead of "strings" (string=[]byte)`,
22 }
23 24 // restrictedBuiltins maps Go builtins to the Moxie alternative.
25 // The loader rewrites Moxie literal syntax to (make)(...) — parenthesized
26 // form that the AST check skips. User-written make(...) has a bare Ident
27 // which this check catches.
28 var restrictedBuiltins = map[string]string{
29 "make": "use literal syntax: []T{:n}, chan T{}, chan T{n}",
30 "new": "use composite literal with & or var declaration",
31 "complex": "complex numbers not supported in moxie",
32 "real": "complex numbers not supported in moxie",
33 "imag": "complex numbers not supported in moxie",
34 }
35 36 // restrictedTypes maps type names to rejection reasons.
37 // Note: string is NOT restricted — with string=[]byte type unification,
38 // the types are interchangeable. The + operator restriction on strings
39 // still enforces Moxie's | concatenation syntax.
40 var restrictedTypes = map[string]string{
41 "complex64": "complex numbers not supported in moxie",
42 "complex128": "complex numbers not supported in moxie",
43 "uintptr": "use explicit pointer types",
44 }
45 46 // isUserPackage returns true if a package should be subject to Moxie
47 // language restrictions. All permanently exempt packages implement
48 // low-level primitives or syscall interfaces.
49 func isUserPackage(pkg *ssa.Package) bool {
50 if pkg == nil {
51 return false
52 }
53 path := pkg.Pkg.Path()
54 if strings.HasPrefix(path, "internal/") || // internal packages
55 strings.Contains(path, "/internal/") || // nested internals
56 strings.HasPrefix(path, "runtime") || // language primitives
57 strings.HasPrefix(path, "unsafe") || // language primitive
58 strings.HasPrefix(path, "os") || // FDs, syscall wrappers
59 strings.HasPrefix(path, "syscall") { // syscall interfaces
60 return false
61 }
62 return true
63 }
64 65 // packageImportsUnsafe returns true if the package imports "unsafe".
66 // Packages using unsafe.Pointer legitimately need uintptr for pointer arithmetic.
67 func packageImportsUnsafe(pkg *ssa.Package) bool {
68 for _, imp := range pkg.Pkg.Imports() {
69 if imp.Path() == "unsafe" {
70 return true
71 }
72 }
73 return false
74 }
75 76 // checkMoxieRestrictions scans a function for uses of restricted features.
77 // Only applies to user code — runtime packages are exempt.
78 func (b *builder) checkMoxieRestrictions() {
79 if !isUserPackage(b.fn.Pkg) {
80 return
81 }
82 // Check for restricted imports (only on the init function to avoid per-function spam).
83 if b.fn.Name() == "init" || (b.fn.Name() == "main" && b.fn.Pkg.Pkg.Name() == "main") {
84 for _, imp := range b.fn.Pkg.Pkg.Imports() {
85 if reason, restricted := restrictedImports[imp.Path()]; restricted {
86 b.addError(b.fn.Pos(), "moxie: import \""+imp.Path()+"\" is not allowed: "+reason)
87 }
88 }
89 }
90 for _, block := range b.fn.Blocks {
91 for _, instr := range block.Instrs {
92 b.checkInstrRestrictions(instr)
93 }
94 }
95 // Check function signature for restricted types.
96 b.checkSignatureRestrictions(b.fn)
97 // Check for fallthrough (lowered away by SSA, must check AST).
98 b.checkFallthroughRestriction()
99 // Check for restricted types and builtins in AST (SSA may optimize them away).
100 b.checkASTRestrictions()
101 // Check that non-constant spawn args aren't used after the spawn call.
102 b.checkSpawnMoveRestriction()
103 // Check that every channel has both a sender and a listener.
104 b.checkChannelCompleteness()
105 }
106 107 func (b *builder) checkInstrRestrictions(instr ssa.Instruction) {
108 switch v := instr.(type) {
109 case *ssa.Call:
110 if builtin, ok := v.Call.Value.(*ssa.Builtin); ok {
111 if reason, restricted := restrictedBuiltins[builtin.Name()]; restricted {
112 b.addError(v.Pos(), "moxie: '"+builtin.Name()+"' is not allowed: "+reason)
113 }
114 }
115 116 // MakeChan, MakeMap, MakeSlice are NOT checked at SSA level — the loader
117 // rewrites Moxie literal syntax to (make)() calls. User-written make() is
118 // caught at AST level (restrictedBuiltins) where the bare Ident is visible.
119 120 case *ssa.BinOp:
121 // Reject + on non-numeric types (strings use | for concatenation).
122 if v.Op == token.ADD {
123 if basic, ok := v.X.Type().Underlying().(*types.Basic); ok {
124 if basic.Info()&types.IsString != 0 {
125 b.addError(v.Pos(), "moxie: '+' is not allowed for text concatenation: use | operator")
126 }
127 }
128 }
129 130 // Note: new(T) and make() are caught by AST check (restrictedBuiltins),
131 // not SSA. Restricted types (uintptr, complex) are also AST-checked.
132 }
133 }
134 135 func (b *builder) checkTypeAtPos(t types.Type, pos token.Pos) {
136 if t == nil {
137 return
138 }
139 // Unwrap pointer types — SSA wraps local var types in pointers.
140 if ptr, ok := t.(*types.Pointer); ok {
141 t = ptr.Elem()
142 }
143 basic, ok := t.(*types.Basic)
144 if !ok {
145 return
146 }
147 if reason, restricted := restrictedTypes[basic.Name()]; restricted {
148 // uintptr is allowed in packages that import unsafe — needed for
149 // pointer arithmetic with unsafe.Pointer.
150 if basic.Name() == "uintptr" && packageImportsUnsafe(b.fn.Pkg) {
151 return
152 }
153 b.addError(pos, "moxie: type '"+basic.Name()+"' is not allowed: "+reason)
154 }
155 }
156 157 func (b *builder) checkSignatureRestrictions(fn *ssa.Function) {
158 sig := fn.Signature
159 for i := 0; i < sig.Params().Len(); i++ {
160 b.checkTypeAtPos(sig.Params().At(i).Type(), fn.Pos())
161 }
162 if sig.Results() != nil {
163 for i := 0; i < sig.Results().Len(); i++ {
164 b.checkTypeAtPos(sig.Results().At(i).Type(), fn.Pos())
165 }
166 }
167 }
168 169 // checkFallthroughRestriction walks the function's AST looking for
170 // fallthrough statements. SSA eliminates these, so we must check the
171 // raw syntax tree.
172 func (b *builder) checkFallthroughRestriction() {
173 syntax := b.fn.Syntax()
174 if syntax == nil {
175 return
176 }
177 ast.Inspect(syntax, func(n ast.Node) bool {
178 if br, ok := n.(*ast.BranchStmt); ok && br.Tok == token.FALLTHROUGH {
179 b.addError(br.Pos(), "moxie: 'fallthrough' is not allowed: each case must be self-contained")
180 return false
181 }
182 return true
183 })
184 }
185 186 // checkASTRestrictions walks the function's AST to catch restricted types
187 // and builtins that SSA may optimize away (dead code elimination removes
188 // unused complex64 vars, unused complex() calls, etc).
189 func (b *builder) checkASTRestrictions() {
190 syntax := b.fn.Syntax()
191 if syntax == nil {
192 return
193 }
194 ast.Inspect(syntax, func(n ast.Node) bool {
195 switch node := n.(type) {
196 case *ast.Ident:
197 if reason, restricted := restrictedTypes[node.Name]; restricted {
198 // uintptr is allowed in packages that import unsafe.
199 if node.Name == "uintptr" && packageImportsUnsafe(b.fn.Pkg) {
200 return true
201 }
202 b.addError(node.Pos(), "moxie: type '"+node.Name+"' is not allowed: "+reason)
203 }
204 case *ast.CallExpr:
205 if ident, ok := node.Fun.(*ast.Ident); ok {
206 if reason, restricted := restrictedBuiltins[ident.Name]; restricted {
207 // Bare make() — exempt .go files and stdlib .mx overlays.
208 // Loader-generated (make)() has ParenExpr as Fun, not bare Ident.
209 if ident.Name == "make" {
210 if isMakeExempt(b.fn.Pkg, b.program.Fset, ident.Pos()) {
211 return true
212 }
213 }
214 b.addError(ident.Pos(), "moxie: '"+ident.Name+"' is not allowed: "+reason)
215 }
216 }
217 }
218 return true
219 })
220 }
221 222 // isMakeExempt returns true if a make() call at the given position should
223 // be allowed. Exempt: .go files (stdlib), stdlib .mx overlays (package
224 // import path has no dots), and vendored stdlib dependencies (golang.org/x/).
225 func isMakeExempt(pkg *ssa.Package, fset *token.FileSet, pos token.Pos) bool {
226 position := fset.Position(pos)
227 if !strings.HasSuffix(position.Filename, ".mx") {
228 return true // .go files always allowed
229 }
230 // Stdlib packages have simple paths (no dots): bytes, fmt, net/http.
231 // The main package also has a simple path ("main") but IS user code.
232 path := pkg.Pkg.Path()
233 if path == "main" || path == "command-line-arguments" {
234 return false // user code
235 }
236 if !strings.Contains(path, ".") {
237 return true // stdlib
238 }
239 // Vendored stdlib dependencies.
240 if strings.HasPrefix(path, "golang.org/x/") {
241 return true
242 }
243 return false
244 }
245 246 // checkSpawnMoveRestriction enforces move semantics for spawn arguments.
247 // Non-constant values passed to spawn must not be used after the call —
248 // ownership moves to the child domain. No shared memory.
249 func (b *builder) checkSpawnMoveRestriction() {
250 for _, block := range b.fn.Blocks {
251 for spawnIdx, instr := range block.Instrs {
252 call, ok := instr.(*ssa.Call)
253 if !ok {
254 continue
255 }
256 // Detect spawn builtin calls.
257 isSpawn := false
258 var spawnArgs []ssa.Value
259 if builtin, ok := call.Call.Value.(*ssa.Builtin); ok && builtin.Name() == "spawn" {
260 // Builtin spawn: args are direct in call.Call.Args.
261 // Skip the function arg (index 0, or 1 if transport string).
262 args := call.Call.Args
263 start := 0
264 if len(args) > 0 {
265 if _, ok := args[0].(*ssa.Const); ok {
266 // Could be transport string — skip it + fn.
267 start = 2
268 } else {
269 // fn is args[0], data starts at 1.
270 start = 1
271 }
272 }
273 if start < len(args) {
274 spawnArgs = args[start:]
275 }
276 isSpawn = true
277 }
278 if !isSpawn {
279 continue
280 }
281 282 for _, arg := range spawnArgs {
283 if _, isConst := arg.(*ssa.Const); isConst {
284 continue
285 }
286 // Channels are exempt — both parent and child hold
287 // endpoints for IPC communication.
288 if _, isChan := arg.Type().Underlying().(*types.Chan); isChan {
289 continue
290 }
291 refs := arg.Referrers()
292 if refs == nil {
293 continue
294 }
295 for _, ref := range *refs {
296 ri, ok := ref.(ssa.Instruction)
297 if !ok {
298 continue
299 }
300 // Same block: check if the referrer comes after spawn.
301 if ri.Block() == block {
302 for j := spawnIdx + 1; j < len(block.Instrs); j++ {
303 if block.Instrs[j] == ri {
304 b.addError(ri.Pos(), "moxie: variable used after spawn — ownership moved to child domain")
305 break
306 }
307 }
308 }
309 }
310 }
311 }
312 }
313 }
314 315 // checkChannelCompleteness ensures every channel created in a function has
316 // both a sender and a listener (select case or receive). Channels that escape
317 // the function (passed to calls, stored, returned) are exempt — the other end
318 // is elsewhere.
319 func (b *builder) checkChannelCompleteness() {
320 for _, block := range b.fn.Blocks {
321 for _, instr := range block.Instrs {
322 mc, ok := instr.(*ssa.MakeChan)
323 if !ok {
324 continue
325 }
326 refs := mc.Referrers()
327 if refs == nil || len(*refs) == 0 {
328 b.addError(mc.Pos(), "moxie: channel created but never used")
329 continue
330 }
331 332 hasSend := false
333 hasRecv := false
334 escapes := false
335 336 for _, ref := range *refs {
337 switch r := ref.(type) {
338 case *ssa.Send:
339 if r.Chan == mc {
340 hasSend = true
341 } else {
342 escapes = true // channel sent as a value
343 }
344 case *ssa.Select:
345 found := false
346 for _, state := range r.States {
347 if state.Chan == mc {
348 found = true
349 if state.Dir == types.SendOnly {
350 hasSend = true
351 } else {
352 hasRecv = true
353 }
354 }
355 }
356 if !found {
357 escapes = true // mc used as send value in select
358 }
359 case *ssa.UnOp:
360 if r.Op == token.ARROW {
361 hasRecv = true
362 }
363 case *ssa.Call:
364 if _, ok := r.Call.Value.(*ssa.Builtin); !ok {
365 escapes = true
366 }
367 // close, len, cap are fine — not an escape
368 case *ssa.DebugRef:
369 // Debug info, not real usage.
370 default:
371 escapes = true
372 }
373 }
374 375 if escapes {
376 continue
377 }
378 if !hasSend && !hasRecv {
379 b.addError(mc.Pos(), "moxie: channel created but has no send or select/receive")
380 } else if !hasSend {
381 b.addError(mc.Pos(), "moxie: channel has no sender — every channel needs both a sender and a listener")
382 } else if !hasRecv {
383 b.addError(mc.Pos(), "moxie: channel has no listener — every channel needs both a sender and a select/receive")
384 }
385 }
386 }
387 }
388 389