restrict.go raw

   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