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  	"moxie/loader"
  17  
  18  	"golang.org/x/tools/go/ssa"
  19  )
  20  
  21  // restrictedImports maps package paths that should not be used in Moxie code.
  22  var restrictedImports = map[string]string{
  23  	"strings": `use "bytes" instead of "strings" (string=[]byte)`,
  24  }
  25  
  26  // restrictedBuiltins maps Go builtins to the Moxie alternative.
  27  // The loader rewrites Moxie literal syntax to (make)(...) — parenthesized
  28  // form that the AST check skips. User-written make(...) has a bare Ident
  29  // which this check catches.
  30  var restrictedBuiltins = map[string]string{
  31  	"make":    "use literal syntax: []T{:n}, chan T{}, chan T{n}",
  32  	"new":     "use composite literal with & or var declaration",
  33  	"complex": "complex numbers not supported in moxie",
  34  	"real":    "complex numbers not supported in moxie",
  35  	"imag":    "complex numbers not supported in moxie",
  36  	"append":  "use push(s, v...) instead: no allocation, panics on capacity overflow",
  37  }
  38  
  39  // restrictedTypes maps type names to rejection reasons.
  40  // Note: string is NOT restricted — with string=[]byte type unification,
  41  // the types are interchangeable. The + operator restriction on strings
  42  // still enforces Moxie's | concatenation syntax.
  43  var restrictedTypes = map[string]string{
  44  	"int":        "use explicit width: int8, int16, int32, int64",
  45  	"uint":       "use explicit width: uint8, uint16, uint32, uint64",
  46  	"complex64":  "complex numbers not supported in moxie",
  47  	"complex128": "complex numbers not supported in moxie",
  48  	"uintptr":    "use explicit pointer types",
  49  }
  50  
  51  // isUserPackage returns true if a package should be subject to Moxie
  52  // language restrictions. Stdlib and stdlib-vendored packages are exempt.
  53  // User packages (identified by dots in the import path from the module
  54  // path) get full restrictions. "internal/" has no special semantics.
  55  func isUserPackage(pkg *ssa.Package) bool {
  56  	if pkg == nil {
  57  		return false
  58  	}
  59  	path := pkg.Pkg.Path()
  60  	if path == "main" || path == "command-line-arguments" {
  61  		return true
  62  	}
  63  	if strings.HasPrefix(path, "golang.org/x/") {
  64  		return false
  65  	}
  66  	// Compiler infrastructure packages (pkg/types, pkg/syntax, pkg/mxutil, etc.)
  67  	// are exempt from user restrictions.
  68  	if strings.Contains(path, "/pkg/") {
  69  		return false
  70  	}
  71  	return strings.Contains(path, ".")
  72  }
  73  
  74  // packageImportsUnsafe returns true if the package imports "unsafe".
  75  // Packages using unsafe.Pointer legitimately need uintptr for pointer arithmetic.
  76  func packageImportsUnsafe(pkg *ssa.Package) bool {
  77  	for _, imp := range pkg.Pkg.Imports() {
  78  		if imp.Path() == "unsafe" {
  79  			return true
  80  		}
  81  	}
  82  	return false
  83  }
  84  
  85  // isInitFunc returns true if fn is an init function (package init, synthetic
  86  // init, or the init$N variants created by the Go SSA builder).
  87  func isInitFunc(fn *ssa.Function) bool {
  88  	name := fn.Name()
  89  	if name == "init" {
  90  		return true
  91  	}
  92  	// Init-like functions: init$guard, init$N, initFoo, etc.
  93  	// Functions prefixed with "init" are treated as initialization
  94  	// code that may set up package globals.
  95  	if len(name) > 4 && name[:4] == "init" {
  96  		return true
  97  	}
  98  	// Package-level var initializer (synthetic).
  99  	if fn.Synthetic != "" && fn.Name() == "init" {
 100  		return true
 101  	}
 102  	return false
 103  }
 104  
 105  // isGlobalStore returns true if addr is a direct global variable.
 106  // Only flags `global = value`, not `global.field = value` or
 107  // `(*global).field = value`. Field mutations through a global pointer
 108  // are fine - the pointer itself is immutable after init(), and the
 109  // pointed-to struct is heap-local state.
 110  func isGlobalStore(addr ssa.Value) bool {
 111  	_, isGlobal := addr.(*ssa.Global)
 112  	return isGlobal
 113  }
 114  
 115  // globalName extracts the global variable name from an address, if any.
 116  func globalName(addr ssa.Value) string {
 117  	switch v := addr.(type) {
 118  	case *ssa.Global:
 119  		return v.Name()
 120  	case *ssa.FieldAddr:
 121  		return globalName(v.X)
 122  	case *ssa.IndexAddr:
 123  		return globalName(v.X)
 124  	case *ssa.UnOp:
 125  		if v.Op == token.MUL {
 126  			return globalName(v.X)
 127  		}
 128  	}
 129  	return ""
 130  }
 131  
 132  // checkMoxieRestrictions scans a function for uses of restricted features.
 133  // Only applies to user code — runtime packages are exempt.
 134  func (b *builder) checkMoxieRestrictions() {
 135  	if !isUserPackage(b.fn.Pkg) {
 136  		return
 137  	}
 138  	// Check for restricted imports (only on the init function to avoid per-function spam).
 139  	if b.fn.Name() == "init" || (b.fn.Name() == "main" && b.fn.Pkg.Pkg.Name() == "main") {
 140  		for _, imp := range b.fn.Pkg.Pkg.Imports() {
 141  			if reason, restricted := restrictedImports[imp.Path()]; restricted {
 142  				b.addError(b.fn.Pos(), "moxie: import \""+imp.Path()+"\" is not allowed: "+reason)
 143  			}
 144  		}
 145  	}
 146  	// Ban functions named after restricted builtins.
 147  	if _, restricted := restrictedBuiltins[b.fn.Name()]; restricted {
 148  		b.addError(b.fn.Pos(), "moxie: function name '"+b.fn.Name()+"' shadows a restricted builtin")
 149  	}
 150  	for _, block := range b.fn.Blocks {
 151  		for _, instr := range block.Instrs {
 152  			b.checkInstrRestrictions(instr)
 153  		}
 154  	}
 155  	// Check function signature for restricted types.
 156  	b.checkSignatureRestrictions(b.fn)
 157  	// Check for fallthrough (lowered away by SSA, must check AST).
 158  	b.checkFallthroughRestriction()
 159  	// Check for restricted types and builtins in AST (SSA may optimize them away).
 160  	b.checkASTRestrictions()
 161  	// Check that non-constant spawn args aren't used after the spawn call.
 162  	b.checkSpawnMoveRestriction()
 163  	// Check that every channel has both a sender and a listener.
 164  	b.checkChannelCompleteness()
 165  }
 166  
 167  func (b *builder) checkInstrRestrictions(instr ssa.Instruction) {
 168  	switch v := instr.(type) {
 169  	case *ssa.Call:
 170  		if builtin, ok := v.Call.Value.(*ssa.Builtin); ok {
 171  			if reason, restricted := restrictedBuiltins[builtin.Name()]; restricted {
 172  				b.addError(v.Pos(), "moxie: '"+builtin.Name()+"' is not allowed: "+reason)
 173  			}
 174  		}
 175  
 176  	// MakeChan, MakeMap, MakeSlice are NOT checked at SSA level — the loader
 177  	// rewrites Moxie literal syntax to (make)() calls. User-written make() is
 178  	// caught at AST level (restrictedBuiltins) where the bare Ident is visible.
 179  
 180  	case *ssa.BinOp:
 181  		// Reject + on non-numeric types (strings use | for concatenation).
 182  		if v.Op == token.ADD {
 183  			if basic, ok := v.X.Type().Underlying().(*types.Basic); ok {
 184  				if basic.Info()&types.IsString != 0 {
 185  					b.addError(v.Pos(), "moxie: '+' is not allowed for text concatenation: use | operator")
 186  				}
 187  			}
 188  		}
 189  
 190  	case *ssa.Store:
 191  		// Package globals are immutable after init(). Flag any Store
 192  		// to a global (or into a global's fields) outside init functions.
 193  		// Currently a warning (printed to stderr) while stage4 is being
 194  		// refactored. Will become a compile error once all mutable
 195  		// globals are moved to function-local state.
 196  		if isUserPackage(b.fn.Pkg) && !isInitFunc(b.fn) {
 197  			if isGlobalStore(v.Addr) {
 198  				msg := "moxie: assignment to package-level var outside init() is not allowed"
 199  				if g := globalName(v.Addr); g != "" {
 200  					msg = "moxie: assignment to package-level var '" + g + "' outside init() is not allowed"
 201  				}
 202  				b.addError(v.Pos(), msg)
 203  			}
 204  		}
 205  
 206  	case *ssa.MakeClosure:
 207  		// Moxie closures cannot capture variables from enclosing scopes.
 208  		// All inputs must be explicit parameters. Package-level globals
 209  		// are accessed directly (not via FreeVars), so they're fine.
 210  		if len(v.Bindings) > 0 {
 211  			fn := v.Fn.(*ssa.Function)
 212  			var names []string
 213  			for _, fv := range fn.FreeVars {
 214  				names = append(names, fv.Name())
 215  			}
 216  			b.addError(v.Pos(), "moxie: closure captures outer variable(s) ["+strings.Join(names, ", ")+"]; pass as explicit parameter(s)")
 217  		}
 218  
 219  	// Note: new(T) and make() are caught by AST check (restrictedBuiltins),
 220  		// not SSA. Restricted types (uintptr, complex) are also AST-checked.
 221  	}
 222  }
 223  
 224  func (b *builder) checkTypeAtPos(t types.Type, pos token.Pos) {
 225  	if t == nil {
 226  		return
 227  	}
 228  	// Unwrap pointer types — SSA wraps local var types in pointers.
 229  	if ptr, ok := t.(*types.Pointer); ok {
 230  		t = ptr.Elem()
 231  	}
 232  	basic, ok := t.(*types.Basic)
 233  	if !ok {
 234  		return
 235  	}
 236  	if reason, restricted := restrictedTypes[basic.Name()]; restricted {
 237  		// uintptr is allowed in packages that import unsafe — needed for
 238  		// pointer arithmetic with unsafe.Pointer.
 239  		if basic.Name() == "uintptr" && packageImportsUnsafe(b.fn.Pkg) {
 240  			return
 241  		}
 242  		b.addError(pos, "moxie: type '"+basic.Name()+"' is not allowed: "+reason)
 243  	}
 244  }
 245  
 246  func (b *builder) checkSignatureRestrictions(fn *ssa.Function) {
 247  	sig := fn.Signature
 248  	for i := 0; i < sig.Params().Len(); i++ {
 249  		b.checkTypeAtPos(sig.Params().At(i).Type(), fn.Pos())
 250  	}
 251  	if sig.Results() != nil {
 252  		for i := 0; i < sig.Results().Len(); i++ {
 253  			b.checkTypeAtPos(sig.Results().At(i).Type(), fn.Pos())
 254  		}
 255  	}
 256  }
 257  
 258  // checkFallthroughRestriction walks the function's AST looking for
 259  // fallthrough statements. SSA eliminates these, so we must check the
 260  // raw syntax tree.
 261  func (b *builder) checkFallthroughRestriction() {
 262  	syntax := b.fn.Syntax()
 263  	if syntax == nil {
 264  		return
 265  	}
 266  	ast.Inspect(syntax, func(n ast.Node) bool {
 267  		if br, ok := n.(*ast.BranchStmt); ok && br.Tok == token.FALLTHROUGH {
 268  			b.addError(br.Pos(), "moxie: 'fallthrough' is not allowed: each case must be self-contained")
 269  			return false
 270  		}
 271  		return true
 272  	})
 273  }
 274  
 275  // checkASTRestrictions walks the function's AST to catch restricted types
 276  // and builtins that SSA may optimize away (dead code elimination removes
 277  // unused complex64 vars, unused complex() calls, etc).
 278  func (b *builder) checkASTRestrictions() {
 279  	syntax := b.fn.Syntax()
 280  	if syntax == nil {
 281  		return
 282  	}
 283  	ast.Inspect(syntax, func(n ast.Node) bool {
 284  		switch node := n.(type) {
 285  		case *ast.Ident:
 286  			if reason, restricted := restrictedTypes[node.Name]; restricted {
 287  				if node.Name == "uintptr" && packageImportsUnsafe(b.fn.Pkg) {
 288  					return true
 289  				}
 290  				b.addError(node.Pos(), "moxie: type '"+node.Name+"' is not allowed: "+reason)
 291  			}
 292  			// Step 4: reject bare "any" keyword (alias for interface{}).
 293  			if node.Name == "any" {
 294  				b.addError(node.Pos(), "moxie: 'any' (empty interface) is not allowed: use a concrete type or named interface")
 295  			}
 296  		case *ast.GoStmt:
 297  			// Step 3: reject go keyword at AST level (redundant with SSA check, fires earlier).
 298  			b.addError(node.Pos(), "moxie: 'go' keyword is not allowed: use channels+select or spawn")
 299  		case *ast.InterfaceType:
 300  			// Step 4: reject empty interface literals.
 301  			if node.Methods == nil || len(node.Methods.List) == 0 {
 302  				b.addError(node.Pos(), "moxie: empty interface (interface{}) is not allowed: use a concrete type or named interface")
 303  			}
 304  		case *ast.CallExpr:
 305  			// Ban parenthesized call targets: (f)(args) is not valid Moxie.
 306  			// Exempt: type conversions like (*byte)(expr) where the paren wraps a type.
 307  			if paren, ok := node.Fun.(*ast.ParenExpr); ok {
 308  				if !isTypeExpr(paren.X) {
 309  					b.addError(paren.Pos(), "moxie: parenthesized call target is not allowed: write f(x), not (f)(x)")
 310  				}
 311  			}
 312  			if ident, ok := node.Fun.(*ast.Ident); ok {
 313  				if reason, restricted := restrictedBuiltins[ident.Name]; restricted {
 314  					if ident.Name == "make" {
 315  						if b.isMakeExempt(ident.Pos()) {
 316  							return true
 317  						}
 318  					}
 319  					b.addError(ident.Pos(), "moxie: '"+ident.Name+"' is not allowed: "+reason)
 320  				}
 321  			}
 322  		case *ast.FuncDecl:
 323  			// Exempt compiler-generated builtins (__moxie_*).
 324  			if node.Name != nil && strings.HasPrefix(node.Name.Name, "__moxie_") {
 325  				return true
 326  			}
 327  			// Step 6: reject value receivers (must use pointer receiver).
 328  			if node.Recv != nil && len(node.Recv.List) > 0 {
 329  				recvType := node.Recv.List[0].Type
 330  				if _, isStar := recvType.(*ast.StarExpr); !isStar {
 331  					b.addError(node.Recv.Pos(), "moxie: value receiver is not allowed: use pointer receiver (*T)")
 332  				}
 333  			}
 334  			// Step 7: reject unnamed return values.
 335  			if node.Type.Results != nil {
 336  				for _, field := range node.Type.Results.List {
 337  					if len(field.Names) == 0 {
 338  						b.addError(field.Pos(), "moxie: unnamed return values are not allowed: name all return values")
 339  					}
 340  				}
 341  			}
 342  		}
 343  		return true
 344  	})
 345  }
 346  
 347  // isTypeExpr returns true if the AST expression is syntactically a type
 348  // (pointer type, selector, array type, etc.) rather than a value expression.
 349  func isTypeExpr(expr ast.Expr) bool {
 350  	switch expr.(type) {
 351  	case *ast.StarExpr:
 352  		return true
 353  	case *ast.ArrayType:
 354  		return true
 355  	case *ast.MapType:
 356  		return true
 357  	case *ast.ChanType:
 358  		return true
 359  	case *ast.FuncType:
 360  		return true
 361  	case *ast.StructType:
 362  		return true
 363  	case *ast.InterfaceType:
 364  		return true
 365  	}
 366  	return false
 367  }
 368  
 369  // isMakeExempt returns true if a make() call at the given position was
 370  // generated by the loader's literal-syntax rewriter. Also exempts .go
 371  // files (stdlib) and stdlib .mx overlays.
 372  func (b *builder) isMakeExempt(pos token.Pos) bool {
 373  	position := b.program.Fset.Position(pos)
 374  	if !strings.HasSuffix(position.Filename, ".mx") {
 375  		return true
 376  	}
 377  	path := b.fn.Pkg.Pkg.Path()
 378  	if path == "main" || path == "command-line-arguments" {
 379  		return b.isLoaderGeneratedMake(position)
 380  	}
 381  	if !strings.Contains(path, ".") {
 382  		return true
 383  	}
 384  	if strings.HasPrefix(path, "golang.org/x/") {
 385  		return true
 386  	}
 387  	return b.isLoaderGeneratedMake(position)
 388  }
 389  
 390  func (b *builder) isLoaderGeneratedMake(position token.Position) bool {
 391  	offsets := b.makeExemptOffsets[position.Filename]
 392  	for _, off := range offsets {
 393  		if position.Offset == off {
 394  			return true
 395  		}
 396  	}
 397  	return false
 398  }
 399  
 400  // checkSpawnMoveRestriction enforces move semantics for spawn arguments.
 401  // Non-constant values passed to spawn must not be used after the call —
 402  // ownership moves to the child domain. No shared memory.
 403  func (b *builder) checkSpawnMoveRestriction() {
 404  	for _, block := range b.fn.Blocks {
 405  		for spawnIdx, instr := range block.Instrs {
 406  			call, ok := instr.(*ssa.Call)
 407  			if !ok {
 408  				continue
 409  			}
 410  			// Detect spawn builtin calls.
 411  			isSpawn := false
 412  			var spawnArgs []ssa.Value
 413  			if builtin, ok := call.Call.Value.(*ssa.Builtin); ok && builtin.Name() == "spawn" {
 414  				// Builtin spawn: args are direct in call.Call.Args.
 415  				// Skip the function arg (index 0, or 1 if transport string).
 416  				args := call.Call.Args
 417  				start := 0
 418  				if len(args) > 0 {
 419  					if _, ok := args[0].(*ssa.Const); ok {
 420  						// Could be transport string — skip it + fn.
 421  						start = 2
 422  					} else {
 423  						// fn is args[0], data starts at 1.
 424  						start = 1
 425  					}
 426  				}
 427  				if start < len(args) {
 428  					spawnArgs = args[start:]
 429  				}
 430  				isSpawn = true
 431  			}
 432  			if !isSpawn {
 433  				continue
 434  			}
 435  
 436  			// Collect every block reachable from spawn's block via successor
 437  			// edges. A referrer in any such block is "after" spawn in the
 438  			// control-flow sense, even if it's in a different basic block
 439  			// (branch, loop body, later sequential block). Same-block refs
 440  			// are handled separately by instruction-index comparison.
 441  			reachableAfter := make(map[*ssa.BasicBlock]bool)
 442  			var work []*ssa.BasicBlock
 443  			for _, succ := range block.Succs {
 444  				if !reachableAfter[succ] {
 445  					reachableAfter[succ] = true
 446  					work = append(work, succ)
 447  				}
 448  			}
 449  			for wi := 0; wi < len(work); wi++ {
 450  				bb := work[wi]
 451  				for _, succ := range bb.Succs {
 452  					if !reachableAfter[succ] {
 453  						reachableAfter[succ] = true
 454  						work = append(work, succ)
 455  					}
 456  				}
 457  			}
 458  			work = work[:0]
 459  
 460  			for _, arg := range spawnArgs {
 461  				if _, isConst := arg.(*ssa.Const); isConst {
 462  					continue
 463  				}
 464  				// Channels are exempt — both parent and child hold
 465  				// endpoints for IPC communication.
 466  				if _, isChan := arg.Type().Underlying().(*types.Chan); isChan {
 467  					continue
 468  				}
 469  				refs := arg.Referrers()
 470  				if refs == nil {
 471  					continue
 472  				}
 473  				for _, ref := range *refs {
 474  					ri, ok := ref.(ssa.Instruction)
 475  					if !ok {
 476  						continue
 477  					}
 478  					refBlock := ri.Block()
 479  					if refBlock == block {
 480  						// Same block: referrer must come after spawn.
 481  						// If the block has a back-edge to itself
 482  						// (reachableAfter[block] == true), any referrer
 483  						// in this block is reachable-after on the next
 484  						// iteration — flag it regardless of instruction
 485  						// position.
 486  						if reachableAfter[block] {
 487  							b.addError(ri.Pos(), "moxie: variable used after spawn — ownership moved to child domain")
 488  							break
 489  						}
 490  						for j := spawnIdx + 1; j < len(block.Instrs); j++ {
 491  							if block.Instrs[j] == ri {
 492  								b.addError(ri.Pos(), "moxie: variable used after spawn — ownership moved to child domain")
 493  								break
 494  							}
 495  						}
 496  					} else if reachableAfter[refBlock] {
 497  						b.addError(ri.Pos(), "moxie: variable used after spawn — ownership moved to child domain")
 498  						break
 499  					}
 500  				}
 501  			}
 502  		}
 503  	}
 504  }
 505  
 506  // checkChannelCompleteness ensures every channel created in a function has
 507  // both a sender and a listener (select case or receive). Channels that escape
 508  // the function (passed to calls, stored, returned) are exempt — the other end
 509  // is elsewhere.
 510  func (b *builder) checkChannelCompleteness() {
 511  	for _, block := range b.fn.Blocks {
 512  		for _, instr := range block.Instrs {
 513  			mc, ok := instr.(*ssa.MakeChan)
 514  			if !ok {
 515  				continue
 516  			}
 517  			refs := mc.Referrers()
 518  			if refs == nil || len(*refs) == 0 {
 519  				b.addError(mc.Pos(), "moxie: channel created but never used")
 520  				continue
 521  			}
 522  
 523  			hasSend := false
 524  			hasRecv := false
 525  			escapes := false
 526  
 527  			for _, ref := range *refs {
 528  				switch r := ref.(type) {
 529  				case *ssa.Send:
 530  					if r.Chan == mc {
 531  						hasSend = true
 532  					} else {
 533  						escapes = true // channel sent as a value
 534  					}
 535  				case *ssa.Select:
 536  					found := false
 537  					for _, state := range r.States {
 538  						if state.Chan == mc {
 539  							found = true
 540  							if state.Dir == types.SendOnly {
 541  								hasSend = true
 542  							} else {
 543  								hasRecv = true
 544  							}
 545  						}
 546  					}
 547  					if !found {
 548  						escapes = true // mc used as send value in select
 549  					}
 550  				case *ssa.UnOp:
 551  					if r.Op == token.ARROW {
 552  						hasRecv = true
 553  					}
 554  				case *ssa.Call:
 555  					if bi, ok := r.Call.Value.(*ssa.Builtin); ok {
 556  						if bi.Name() == "spawn" {
 557  							// A channel passed to spawn escapes to the spawned
 558  							// domain, which provides the other end.
 559  							for _, arg := range r.Call.Args {
 560  								if arg == mc {
 561  									escapes = true
 562  									break
 563  								}
 564  							}
 565  						}
 566  						// close, len, cap are fine — not an escape
 567  					} else {
 568  						escapes = true
 569  					}
 570  				case *ssa.DebugRef:
 571  					// Debug info, not real usage.
 572  				default:
 573  					escapes = true
 574  				}
 575  			}
 576  
 577  			if escapes {
 578  				continue
 579  			}
 580  			if !hasSend && !hasRecv {
 581  				b.addError(mc.Pos(), "moxie: channel created but has no send or select/receive")
 582  			} else if !hasSend {
 583  				b.addError(mc.Pos(), "moxie: channel has no sender — every channel needs both a sender and a listener")
 584  			} else if !hasRecv {
 585  				b.addError(mc.Pos(), "moxie: channel has no listener — every channel needs both a sender and a select/receive")
 586  			}
 587  		}
 588  	}
 589  }
 590  
 591  // isUserPackageByPath returns true if the import path looks like a user
 592  // package (contains a dot, or is "main"/"command-line-arguments").
 593  func isUserPackageByPath(path string) bool {
 594  	if path == "main" || path == "command-line-arguments" {
 595  		return true
 596  	}
 597  	if strings.HasPrefix(path, "golang.org/x/") {
 598  		return false
 599  	}
 600  	return strings.Contains(path, ".")
 601  }
 602  
 603  // checkPackageRestrictions runs package-scope checks that aren't per-function:
 604  // - reject var X = expr at package scope (step 8)
 605  // - reject named slice/map types (step 9)
 606  // - reject scope shadowing including builtins (step 10)
 607  // - enforce init() rules: main pkg can't have init, library max one, no select/spawn
 608  func (c *compilerContext) checkPackageRestrictions(pkg *loader.Package) {
 609  	if !isUserPackageByPath(pkg.Pkg.Path()) {
 610  		return
 611  	}
 612  	isMainPkg := pkg.Pkg.Name() == "main"
 613  	initCount := 0
 614  	for _, f := range pkg.Files {
 615  		for _, decl := range f.Decls {
 616  			switch d := decl.(type) {
 617  			case *ast.FuncDecl:
 618  				if d.Name.Name == "init" && d.Recv == nil {
 619  					initCount++
 620  					if isMainPkg {
 621  						c.addError(d.Pos(), "moxie: init() is not allowed in main package: use main() for program entry")
 622  					}
 623  					if initCount > 1 {
 624  						c.addError(d.Pos(), "moxie: at most one init() per package")
 625  					}
 626  					// Body restrictions: no select or spawn.
 627  					if d.Body != nil {
 628  						checkInitBodyRestrictions(c, d.Body)
 629  					}
 630  				}
 631  			case *ast.GenDecl:
 632  				switch d.Tok {
 633  				case token.VAR:
 634  					for _, spec := range d.Specs {
 635  						vs := spec.(*ast.ValueSpec)
 636  						if len(vs.Values) > 0 && !isDemotedStringConst(vs) {
 637  							c.addError(vs.Pos(), "moxie: package-level var with initializer is not allowed: use zero-value declaration and init()")
 638  						}
 639  					}
 640  				case token.TYPE:
 641  					for _, spec := range d.Specs {
 642  						ts := spec.(*ast.TypeSpec)
 643  						switch ts.Type.(type) {
 644  						case *ast.ArrayType:
 645  							at := ts.Type.(*ast.ArrayType)
 646  							if at.Len == nil {
 647  								c.addError(ts.Pos(), "moxie: named slice type '"+ts.Name.Name+"' is not allowed: use []T directly")
 648  							}
 649  						case *ast.MapType:
 650  							c.addError(ts.Pos(), "moxie: named map type '"+ts.Name.Name+"' is not allowed: use map[K]V directly")
 651  						}
 652  					}
 653  				}
 654  			}
 655  		}
 656  	}
 657  	// Step 10: reject scope shadowing (including builtins from Universe).
 658  	c.checkScopeShadowing(pkg)
 659  }
 660  
 661  // checkScopeShadowing walks all scopes in the package and rejects any
 662  // declaration that shadows a name in an enclosing scope, including Universe.
 663  func (c *compilerContext) checkScopeShadowing(pkg *loader.Package) {
 664  	info := pkg.TypeInfo()
 665  	if info == nil || info.Scopes == nil {
 666  		return
 667  	}
 668  	pkgScope := pkg.Pkg.Scope()
 669  	for _, scope := range info.Scopes {
 670  		if scope == nil || scope == pkgScope {
 671  			continue
 672  		}
 673  		for _, name := range scope.Names() {
 674  			if name == "_" {
 675  				continue
 676  			}
 677  			obj := scope.Lookup(name)
 678  			if obj == nil {
 679  				continue
 680  			}
 681  			// Walk parent scopes up to (and including) Universe.
 682  			for parent := scope.Parent(); parent != nil; parent = parent.Parent() {
 683  				if shadowedObj := parent.Lookup(name); shadowedObj != nil {
 684  					c.addError(obj.Pos(), "moxie: '"+name+"' shadows declaration in enclosing scope")
 685  					break
 686  				}
 687  			}
 688  		}
 689  	}
 690  }
 691  
 692  // checkInitBodyRestrictions walks an init() function body for banned constructs:
 693  // select statements and spawn() calls.
 694  // isDemotedStringConst returns true if a var spec was originally a string
 695  // constant that was demoted to var by splitConstBlocks (string->[]byte).
 696  // These have initializers like []byte("literal") and should not be flagged
 697  // as "package-level var with initializer".
 698  func isDemotedStringConst(vs *ast.ValueSpec) bool {
 699  	for _, val := range vs.Values {
 700  		call, ok := val.(*ast.CallExpr)
 701  		if !ok {
 702  			return false
 703  		}
 704  		// Check for []byte(...) call
 705  		if _, ok := call.Fun.(*ast.ArrayType); !ok {
 706  			return false
 707  		}
 708  		if len(call.Args) != 1 {
 709  			return false
 710  		}
 711  		if lit, ok := call.Args[0].(*ast.BasicLit); !ok || lit.Kind != token.STRING {
 712  			return false
 713  		}
 714  	}
 715  	return true
 716  }
 717  
 718  func checkInitBodyRestrictions(c *compilerContext, body *ast.BlockStmt) {
 719  	ast.Inspect(body, func(n ast.Node) bool {
 720  		switch node := n.(type) {
 721  		case *ast.SelectStmt:
 722  			c.addError(node.Pos(), "moxie: select is not allowed in init(): event dispatch begins in main()")
 723  		case *ast.CallExpr:
 724  			if ident, ok := node.Fun.(*ast.Ident); ok && ident.Name == "spawn" {
 725  				c.addError(ident.Pos(), "moxie: spawn is not allowed in init(): domains can only be created after main() starts")
 726  			}
 727  		}
 728  		return true
 729  	})
 730  }
 731  
 732