Repository: google/starlark-go
Pull request #427 (2022-10-05 15:31:17)

fix a few function names on comments


Patch:
diff --git a/README.md b/README.md
index 5eafad9..b097749 100644
--- a/README.md
+++ b/README.md
@@ -154,6 +154,10 @@ stage in the project, although we will endeavor to keep them to a minimum.
 Once the Bazel team has finalized the version 1 language specification,
 we will be more rigorous with interface stability.
 
+We aim to support the most recent four (go1.x) releases of the Go
+toolchain. For example, if the latest release is go1.20, we support it
+along with go1.19, go1.18, and go1.17, but not go1.16.
+
 ### Credits
 
 Starlark was designed and implemented in Java by
diff --git a/doc/spec.md b/doc/spec.md
index 90afccc..c98ee17 100644
--- a/doc/spec.md
+++ b/doc/spec.md
@@ -104,7 +104,7 @@ reproducibility is paramount, such as build tools.
     * [For loops](#for-loops)
     * [Break and Continue](#break-and-continue)
     * [Load statements](#load-statements)
-    * [Module execution](#module-execution)
+  * [Module execution](#module-execution)
   * [Built-in constants and functions](#built-in-constants-and-functions)
     * [None](#none)
     * [True and False](#true-and-false)
@@ -153,6 +153,11 @@ reproducibility is paramount, such as build tools.
     * [list·insert](#list·insert)
     * [list·pop](#list·pop)
     * [list·remove](#list·remove)
+    * [set·add](#set·add)
+    * [set·clear](#set·clear)
+    * [set·discard](#set·discard)
+    * [set·pop](#set·pop)
+    * [set·remove](#set·remove)
     * [set·union](#set·union)
     * [string·capitalize](#string·capitalize)
     * [string·codepoint_ords](#string·codepoint_ords)
@@ -260,12 +265,14 @@ appear in the grammar; they are reserved as possible future keywords:
 <!-- and to remain a syntactic subset of Python -->
 
 ```text
-as             finally        nonlocal
-assert         from           raise
-class          global         try
-del            import         with
-except         is             yield
+as              except          nonlocal
+assert          finally         raise
+async           from            try
+await           global          with
+class           import          yield
+del             is   
 ```
+<!-- NB: bazelbuild/starlark puts `while` in the second list -->
 
 <b>Implementation note:</b>
 The Go implementation permits `assert` to be used as an identifier,
@@ -741,11 +748,9 @@ The concatenation operation `x + y` yields a new list containing all
 the elements of the two lists x and y.
 
 For most types, `x += y` is equivalent to `x = x + y`, except that it
-evaluates `x` only once, that is, it allocates a new list to hold
-the concatenation of `x` and `y`.
-However, if `x` refers to a list, the statement does not allocate a
-new list but instead mutates the original list in place, similar to
-`x.extend(y)`.
+evaluates `x` only once. However, if `x` refers to a list, the statement
+`x += y` does not allocate a new list as `x = x + y` would, but instead 
+mutates the original list in place, similar to `x.extend(y)`.
 
 Lists are not hashable, so may not be used in the keys of a dictionary.
 
@@ -966,7 +971,15 @@ Sets are instantiated by calling the built-in `set` function, which
 returns a set containing all the elements of its optional argument,
 which must be an iterable sequence.  Sets have no literal syntax.
 
-The only method of a set is `union`, which is equivalent to the `|` operator.
+A set has these methods:
+
+* [`add`](#set·add)
+* [`clear`](#set·clear)
+* [`discard`](#set·discard)
+* [`pop`](#set·pop)
+* [`remove`](#set·remove)
+* [`union`](#set·union)
+
 
 A set used in a Boolean context is considered true if it is non-empty.
 
@@ -1192,8 +1205,8 @@ The [type](#type) of a built-in function is `"builtin_function_or_method"`.
 
 A built-in function value used in a Boolean context is always considered true.
 
-Many built-in functions are predeclared in the environment
-(see [Name Resolution](#name-resolution)).
+Many [built-in functions](#built-in-constants-and-functions) are predeclared 
+in the environment (see [Name binding and variables](#name-binding-and-variables)).
 Some built-in functions such as `len` are _universal_, that is,
 available to all Starlark programs.
 The host application may predeclare additional built-in functions
@@ -1439,7 +1452,7 @@ on the value returned by `get_filename()`.
 ## Value concepts
 
 Starlark has eleven core [data types](#data-types).  An application
-that embeds the Starlark intepreter may define additional types that
+that embeds the Starlark interpreter may define additional types that
 behave like Starlark values.  All values, whether core or
 application-defined, implement a few basic behaviors:
 
@@ -2522,7 +2535,7 @@ Example:
 def map(f, list):
     return [f(x) for x in list]
 
-map(lambda x: 2*x, range(3))    # [2, 4, 6]
+map(lambda x: 2*x, range(3))    # [0, 2, 4]
 ```
 
 As with functions created by a `def` statement, a lambda function
@@ -3462,14 +3475,14 @@ type(0.0)               # "float"
 `zip()` returns a new list of n-tuples formed from corresponding
 elements of each of the n iterable sequences provided as arguments to
 `zip`.  That is, the first tuple contains the first element of each of
-the sequences, the second element contains the second element of each
+the sequences, the second tuple contains the second element of each
 of the sequences, and so on.  The result list is only as long as the
 shortest of the input sequences.
 
 ```python
 zip()                                   # []
 zip(range(5))                           # [(0,), (1,), (2,), (3,), (4,)]
-zip(range(5), "abc")                    # [(0, "a"), (1, "b"), (2, "c")]
+zip(range(5), "abc".elems())            # [(0, "a"), (1, "b"), (2, "c")]
 ```
 
 ## Built-in methods
@@ -3740,6 +3753,79 @@ x.remove(2)                             # None (x == [1, 3])
 x.remove(2)                             # error: element not found
 ```
 
+<a id='set·add'></a>
+### set·add
+
+If `x` is not an element of set `S`, `S.add(x)` adds it to the set or fails if the set is frozen.
+If `x` already an element of the set, `add(x)` has no effect.
+
+It returns None.
+
+```python
+x = set([1, 2])
+x.add(3)                             # None
+x                                    # set([1, 2, 3])
+x.add(3)                             # None
+x                                    # set([1, 2, 3])
+```
+
+<a id='set·clear'></a>
+### set·clear
+
+`S.clear()` removes all items from the set or fails if the set is non-empty and frozen.
+
+It returns None.
+
+```python
+x = set([1, 2, 3])
+x.clear(2)                               # None
+x                                        # set([])
+```
+
+<a id='set·discard'></a>
+### set·discard
+
+If `x` is an element of set `S`, `S.discard(x)` removes `x` from the set, or fails if the
+set is frozen. If `x` is not an element of the set, discard has no effect.
+
+It returns None.
+
+```python
+x = set([1, 2, 3])
+x.discard(2)                             # None
+x                                        # set([1, 3])
+x.discard(2)                             # None
+x                                        # set([1, 3])
+```
+
+<a id='set·pop'></a>
+### set·pop
+
+`S.pop()` removes the first inserted item from the set and returns it.
+
+`pop` fails if the set is empty or frozen.
+
+```python
+x = set([1, 2])
+x.pop()                                 # 1
+x.pop()                                 # 2
+x.pop()                                 # error: empty set
+```
+
+<a id='set·remove'></a>
+### set·remove
+
+`S.remove(x)` removes `x` from the set and returns None.
+
+`remove` fails if the set does not contain `x` or is frozen.
+
+```python
+x = set([1, 2, 3])
+x.remove(2)                             # None
+x                                       # set([1, 3])
+x.remove(2)                             # error: element not found
+```
+
 <a id='set·union'></a>
 ### set·union
 
@@ -3903,7 +3989,7 @@ Currently it must be empty, but it is reserved for future use.
 "a{x}b{y}c{}".format(1, x=2, y=3)               # "a2b3c1"
 "a{}b{}c".format(1, 2)                          # "a1b2c"
 "({1}, {0})".format("zero", "one")              # "(one, zero)"
-"Is {0!r} {0!s}?".format('heterological')       # 'is "heterological" heterological?'
+"Is {0!r} {0!s}?".format('heterological')       # 'Is "heterological" heterological?'
 ```
 
 <a id='string·index'></a>
diff --git a/go.mod b/go.mod
index 99a5585..719cff6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,12 +1,12 @@
 module go.starlark.net
 
-go 1.13
+go 1.16
 
 require (
 	github.com/chzyer/logex v1.1.10 // indirect
 	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
 	github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
-	github.com/google/go-cmp v0.5.1 // indirect
+	github.com/google/go-cmp v0.5.1
 	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
 	golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
diff --git a/internal/compile/compile.go b/internal/compile/compile.go
index 888d95c..ecf689f 100644
--- a/internal/compile/compile.go
+++ b/internal/compile/compile.go
@@ -23,7 +23,6 @@
 //
 // Operands, logically uint32s, are encoded using little-endian 7-bit
 // varints, the top bit indicating that more bytes follow.
-//
 package compile // import "go.starlark.net/internal/compile"
 
 import (
@@ -47,7 +46,7 @@ var Disassemble = false
 const debug = false // make code generation verbose, for debugging the compiler
 
 // Increment this to force recompilation of saved bytecode files.
-const Version = 13
+const Version = 14
 
 type Opcode uint8
 
@@ -317,6 +316,7 @@ type Program struct {
 	Functions []*Funcode
 	Globals   []Binding // for error messages and tracing
 	Toplevel  *Funcode  // module initialization function
+	Recursion bool      // disable recursion check for functions in this file
 }
 
 // The type of a bytes literal value, to distinguish from text string.
@@ -486,17 +486,20 @@ func bindings(bindings []*resolve.Binding) []Binding {
 }
 
 // Expr compiles an expression to a program whose toplevel function evaluates it.
-func Expr(expr syntax.Expr, name string, locals []*resolve.Binding) *Program {
+// The options must be consistent with those used when parsing expr.
+func Expr(opts *syntax.FileOptions, expr syntax.Expr, name string, locals []*resolve.Binding) *Program {
 	pos := syntax.Start(expr)
 	stmts := []syntax.Stmt{&syntax.ReturnStmt{Result: expr}}
-	return File(stmts, pos, name, locals, nil)
+	return File(opts, stmts, pos, name, locals, nil)
 }
 
 // File compiles the statements of a file into a program.
-func File(stmts []syntax.Stmt, pos syntax.Position, name string, locals, globals []*resolve.Binding) *Program {
+// The options must be consistent with those used when parsing stmts.
+func File(opts *syntax.FileOptions, stmts []syntax.Stmt, pos syntax.Position, name string, locals, globals []*resolve.Binding) *Program {
 	pcomp := &pcomp{
 		prog: &Program{
-			Globals: bindings(globals),
+			Globals:   bindings(globals),
+			Recursion: opts.Recursion,
 		},
 		names:     make(map[string]uint32),
 		constants: make(map[interface{}]uint32),
diff --git a/internal/compile/serial.go b/internal/compile/serial.go
index adadabf..4d71738 100644
--- a/internal/compile/serial.go
+++ b/internal/compile/serial.go
@@ -25,6 +25,7 @@ package compile
 //	toplevel	Funcode
 //	numfuncs	varint
 //	funcs		[]Funcode
+//	recursion	varint (0 or 1)
 //	<strings>	[]byte		# concatenation of all referenced strings
 //	EOF
 //
@@ -130,6 +131,7 @@ func (prog *Program) Encode() []byte {
 	for _, fn := range prog.Functions {
 		e.function(fn)
 	}
+	e.int(b2i(prog.Recursion))
 
 	// Patch in the offset of the string data section.
 	binary.LittleEndian.PutUint32(e.p[4:8], uint32(len(e.p)))
@@ -270,6 +272,7 @@ func DecodeProgram(data []byte) (_ *Program, err error) {
 	for i := range funcs {
 		funcs[i] = d.function()
 	}
+	recursion := d.int() != 0
 
 	prog := &Program{
 		Loads:     loads,
@@ -278,6 +281,7 @@ func DecodeProgram(data []byte) (_ *Program, err error) {
 		Globals:   globals,
 		Functions: funcs,
 		Toplevel:  toplevel,
+		Recursion: recursion,
 	}
 	toplevel.Prog = prog
 	for _, f := range funcs {
diff --git a/lib/json/json.go b/lib/json/json.go
index e893a97..5979b6f 100644
--- a/lib/json/json.go
+++ b/lib/json/json.go
@@ -11,13 +11,14 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"log"
 	"math"
 	"math/big"
+	"reflect"
 	"sort"
 	"strconv"
 	"strings"
 	"unicode/utf8"
+	"unsafe"
 
 	"go.starlark.net/starlark"
 	"go.starlark.net/starlarkstruct"
@@ -25,42 +26,45 @@ import (
 
 // Module json is a Starlark module of JSON-related functions.
 //
-//   json = module(
-//      encode,
-//      decode,
-//      indent,
-//   )
+//	json = module(
+//	   encode,
+//	   decode,
+//	   indent,
+//	)
 //
 // def encode(x):
 //
 // The encode function accepts one required positional argument,
 // which it converts to JSON by cases:
-// - A Starlark value that implements Go's standard json.Marshal
-//   interface defines its own JSON encoding.
-// - None, True, and False are converted to null, true, and false, respectively.
-// - Starlark int values, no matter how large, are encoded as decimal integers.
-//   Some decoders may not be able to decode very large integers.
-// - Starlark float values are encoded using decimal point notation,
-//   even if the value is an integer.
-//   It is an error to encode a non-finite floating-point value.
-// - Starlark strings are encoded as JSON strings, using UTF-16 escapes.
-// - a Starlark IterableMapping (e.g. dict) is encoded as a JSON object.
-//   It is an error if any key is not a string.
-// - any other Starlark Iterable (e.g. list, tuple) is encoded as a JSON array.
-// - a Starlark HasAttrs (e.g. struct) is encoded as a JSON object.
+//   - A Starlark value that implements Go's standard json.Marshal
+//     interface defines its own JSON encoding.
+//   - None, True, and False are converted to null, true, and false, respectively.
+//   - Starlark int values, no matter how large, are encoded as decimal integers.
+//     Some decoders may not be able to decode very large integers.
+//   - Starlark float values are encoded using decimal point notation,
+//     even if the value is an integer.
+//     It is an error to encode a non-finite floating-point value.
+//   - Starlark strings are encoded as JSON strings, using UTF-16 escapes.
+//   - a Starlark IterableMapping (e.g. dict) is encoded as a JSON object.
+//     It is an error if any key is not a string.
+//   - any other Starlark Iterable (e.g. list, tuple) is encoded as a JSON array.
+//   - a Starlark HasAttrs (e.g. struct) is encoded as a JSON object.
+//
 // It an application-defined type matches more than one the cases describe above,
 // (e.g. it implements both Iterable and HasFields), the first case takes precedence.
 // Encoding any other value yields an error.
 //
-// def decode(x):
+// def decode(x[, default]):
 //
-// The decode function accepts one positional parameter, a JSON string.
+// The decode function has one required positional parameter, a JSON string.
 // It returns the Starlark value that the string denotes.
-// - Numbers are parsed as int or float, depending on whether they
-//   contain a decimal point.
-// - JSON objects are parsed as new unfrozen Starlark dicts.
-// - JSON arrays are parsed as new unfrozen Starlark lists.
-// Decoding fails if x is not a valid JSON string.
+//   - Numbers are parsed as int or float, depending on whether they
+//     contain a decimal point.
+//   - JSON objects are parsed as new unfrozen Starlark dicts.
+//   - JSON arrays are parsed as new unfrozen Starlark lists.
+//
+// If x is not a valid JSON string, the behavior depends on the "default"
+// parameter: if present, Decode returns its value; otherwise, Decode fails.
 //
 // def indent(str, *, prefix="", indent="\t"):
 //
@@ -69,7 +73,6 @@ import (
 // It accepts one required positional parameter, the JSON string,
 // and two optional keyword-only string parameters, prefix and indent,
 // that specify a prefix of each new line, and the unit of indentation.
-//
 var Module = &starlarkstruct.Module{
 	Name: "json",
 	Members: starlark.StringDict{
@@ -100,8 +103,23 @@ func encode(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, k
 		}
 	}
 
+	path := make([]unsafe.Pointer, 0, 8)
+
 	var emit func(x starlark.Value) error
 	emit = func(x starlark.Value) error {
+
+		// It is only necessary to push/pop the item when it might contain
+		// itself (i.e. the last three switch cases), but omitting it in the other
+		// cases did not show significant improvement on the benchmarks.
+		if ptr := pointer(x); ptr != nil {
+			if pathContains(path, ptr) {
+				return fmt.Errorf("cycle in JSON structure")
+			}
+
+			path = append(path, ptr)
+			defer func() { path = path[0 : len(path)-1] }()
+		}
+
 		switch x := x.(type) {
 		case json.Marshaler:
 			// Application-defined starlark.Value types
@@ -183,8 +201,13 @@ func encode(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, k
 			sort.Strings(names)
 			for i, name := range names {
 				v, err := x.Attr(name)
-				if err != nil || v == nil {
-					log.Fatalf("internal error: dir(%s) includes %q but value has no .%s field", x.Type(), name, name)
+				if err != nil {
+					return fmt.Errorf("cannot access attribute %s.%s: %w", x.Type(), name, err)
+				}
+				if v == nil {
+					// x.AttrNames() returned name, but x.Attr(name) returned nil, stating
+					// that the field doesn't exist.
+					return fmt.Errorf("missing attribute %s.%s (despite %q appearing in dir()", x.Type(), name, name)
 				}
 				if i > 0 {
 					buf.WriteByte(',')
@@ -209,6 +232,27 @@ func encode(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, k
 	return starlark.String(buf.String()), nil
 }
 
+func pointer(i interface{}) unsafe.Pointer {
+	v := reflect.ValueOf(i)
+	switch v.Kind() {
+	case reflect.Ptr, reflect.Chan, reflect.Map, reflect.UnsafePointer, reflect.Slice:
+		// TODO(adonovan): use v.Pointer() when we drop go1.17.
+		return unsafe.Pointer(v.Pointer())
+	default:
+		return nil
+	}
+}
+
+func pathContains(path []unsafe.Pointer, item unsafe.Pointer) bool {
+	for _, p := range path {
+		if p == item {
+			return true
+		}
+	}
+
+	return false
+}
+
 // isPrintableASCII reports whether s contains only printable ASCII.
 func isPrintableASCII(s string) bool {
 	for i := 0; i < len(s); i++ {
@@ -246,11 +290,17 @@ func indent(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, k
 	return starlark.String(buf.String()), nil
 }
 
-func decode(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (_ starlark.Value, err error) {
+func decode(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (v starlark.Value, err error) {
 	var s string
-	if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &s); err != nil {
+	var d starlark.Value
+	if err := starlark.UnpackArgs(b.Name(), args, kwargs, "x", &s, "default?", &d); err != nil {
 		return nil, err
 	}
+	if len(args) < 1 {
+		// "x" parameter is positional only; UnpackArgs does not allow us to
+		// directly express "def decode(x, *, default)"
+		return nil, fmt.Errorf("%s: unexpected keyword argument x", b.Name())
+	}
 
 	// The decoder necessarily makes certain representation choices
 	// such as list vs tuple, struct vs dict, int vs float.
@@ -258,6 +308,11 @@ func decode(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, k
 	// control the returned types, but there's no compelling need yet.
 
 	// Use panic/recover with a distinguished type (failure) for error handling.
+	// If "default" is set, we only want to return it when encountering invalid
+	// json - not for any other possible causes of panic.
+	// In particular, if we ever extend the json.decode API to take a callback,
+	// a distinguished, private failure type prevents the possibility of
+	// json.decode with "default" becoming abused as a try-catch mechanism.
 	type failure string
 	fail := func(format string, args ...interface{}) {
 		panic(failure(fmt.Sprintf(format, args...)))
@@ -459,18 +514,22 @@ func decode(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, k
 		x := recover()
 		switch x := x.(type) {
 		case failure:
-			err = fmt.Errorf("json.decode: at offset %d, %s", i, x)
+			if d != nil {
+				v = d
+			} else {
+				err = fmt.Errorf("json.decode: at offset %d, %s", i, x)
+			}
 		case nil:
 			// nop
 		default:
 			panic(x) // unexpected panic
 		}
 	}()
-	x := parse()
+	v = parse()
 	if skipSpace() {
 		fail("unexpected character %q after value", s[i])
 	}
-	return x, nil
+	return v, nil
 }
 
 func isdigit(b byte) bool {
diff --git a/lib/math/math.go b/lib/math/math.go
index ba2fe26..07eb4a7 100644
--- a/lib/math/math.go
+++ b/lib/math/math.go
@@ -71,7 +71,7 @@ var Module = &starlarkstruct.Module{
 		"copysign":  newBinaryBuiltin("copysign", math.Copysign),
 		"fabs":      newUnaryBuiltin("fabs", math.Abs),
 		"floor":     starlark.NewBuiltin("floor", floor),
-		"mod":       newBinaryBuiltin("round", math.Mod),
+		"mod":       newBinaryBuiltin("mod", math.Mod),
 		"pow":       newBinaryBuiltin("pow", math.Pow),
 		"remainder": newBinaryBuiltin("remainder", math.Remainder),
 		"round":     newUnaryBuiltin("round", math.Round),
diff --git a/lib/proto/cmd/star2proto/star2proto.go b/lib/proto/cmd/star2proto/star2proto.go
index 24b5a0e..791f207 100644
--- a/lib/proto/cmd/star2proto/star2proto.go
+++ b/lib/proto/cmd/star2proto/star2proto.go
@@ -40,8 +40,10 @@ var (
 
 // Starlark dialect flags
 func init() {
-	flag.BoolVar(&resolve.AllowFloat, "fp", true, "allow floating-point numbers")
 	flag.BoolVar(&resolve.AllowSet, "set", resolve.AllowSet, "allow set data type")
+
+	// obsolete, no effect:
+	flag.BoolVar(&resolve.AllowFloat, "fp", true, "allow floating-point numbers")
 	flag.BoolVar(&resolve.AllowLambda, "lambda", resolve.AllowLambda, "allow lambda expressions")
 	flag.BoolVar(&resolve.AllowNestedDef, "nesteddef", resolve.AllowNestedDef, "allow nested def statements")
 }
diff --git a/lib/proto/proto.go b/lib/proto/proto.go
index 149162d..36f0023 100644
--- a/lib/proto/proto.go
+++ b/lib/proto/proto.go
@@ -395,8 +395,8 @@ func setField(msg protoreflect.Message, fdesc protoreflect.FieldDescriptor, valu
 		}
 		defer iter.Done()
 
-		// TODO(adonovan): handle maps
 		list := msg.Mutable(fdesc).List()
+		list.Truncate(0)
 		var x starlark.Value
 		for i := 0; iter.Next(&x); i++ {
 			v, err := toProto(fdesc, x)
@@ -408,6 +408,45 @@ func setField(msg protoreflect.Message, fdesc protoreflect.FieldDescriptor, valu
 		return nil
 	}
 
+	if fdesc.IsMap() {
+		mapping, ok := value.(starlark.IterableMapping)
+		if !ok {
+			return fmt.Errorf("in map field %s: expected mappable type, but got %s", fdesc.Name(), value.Type())
+		}
+
+		iter := mapping.Iterate()
+		defer iter.Done()
+
+		// Each value is converted using toProto as usual, passing the key/value
+		// field descriptors to check their types.
+		mutMap := msg.Mutable(fdesc).Map()
+		var k starlark.Value
+		for iter.Next(&k) {
+			kproto, err := toProto(fdesc.MapKey(), k)
+			if err != nil {
+				return fmt.Errorf("in key of map field %s: %w", fdesc.Name(), err)
+			}
+
+			// `found` is discarded, as the presence of the key in the
+			// iterator guarantees the presence of some value (even if it is
+			// starlark.None). Mismatching values will be caught in toProto
+			// below.
+			v, _, err := mapping.Get(k)
+			if err != nil {
+				return fmt.Errorf("in map field %s, at key %s: %w", fdesc.Name(), k.String(), err)
+			}
+
+			vproto, err := toProto(fdesc.MapValue(), v)
+			if err != nil {
+				return fmt.Errorf("in map field %s, at key %s: %w", fdesc.Name(), k.String(), err)
+			}
+
+			mutMap.Set(kproto.MapKey(), vproto)
+		}
+
+		return nil
+	}
+
 	v, err := toProto(fdesc, value)
 	if err != nil {
 		return fmt.Errorf("in field %s: %v", fdesc.Name(), err)
diff --git a/lib/time/time.go b/lib/time/time.go
index de8bf53..0f78142 100644
--- a/lib/time/time.go
+++ b/lib/time/time.go
@@ -29,7 +29,7 @@ import (
 //     parse_duration(d) - Parses the given duration string. For more details, refer to
 //                         https://pkg.go.dev/time#ParseDuration.
 //
-//     parseTime(x, format, location) - Parses the given time string using a specific time format and location.
+//     parse_time(x, format, location) - Parses the given time string using a specific time format and location.
 //                                      The expected arguments are a time string (mandatory), a time format
 //                                      (optional, set to RFC3339 by default, e.g. "2021-03-22T23:20:50.52Z")
 //                                      and a name of location (optional, set to UTC by default). For more details,
@@ -209,16 +209,15 @@ func (d Duration) AttrNames() []string {
 	}
 }
 
-// CompareSameType implements comparison of two Duration values. required by
-// starlark.Comparable interface.
-func (d Duration) CompareSameType(op syntax.Token, v starlark.Value, depth int) (bool, error) {
-	cmp := 0
+// Cmp implements comparison of two Duration values. required by
+// starlark.TotallyOrdered interface.
+func (d Duration) Cmp(v starlark.Value, depth int) (int, error) {
 	if x, y := d, v.(Duration); x < y {
-		cmp = -1
+		return -1, nil
 	} else if x > y {
-		cmp = 1
+		return 1, nil
 	}
-	return threeway(op, cmp), nil
+	return 0, nil
 }
 
 // Binary implements binary operators, which satisfies the starlark.HasBinary
@@ -348,7 +347,7 @@ func (t Time) Hash() (uint32, error) {
 
 // Truth returns the truth value of an object required by starlark.Value
 // interface.
-func (t Time) Truth() starlark.Bool { return starlark.Bool(time.Time(t).IsZero()) }
+func (t Time) Truth() starlark.Bool { return !starlark.Bool(time.Time(t).IsZero()) }
 
 // Attr gets a value for a string attribute, implementing dot expression support
 // in starklark. required by starlark.HasAttrs interface.
@@ -392,18 +391,17 @@ func (t Time) AttrNames() []string {
 	)
 }
 
-// CompareSameType implements comparison of two Time values. required by
-// starlark.Comparable interface.
-func (t Time) CompareSameType(op syntax.Token, yV starlark.Value, depth int) (bool, error) {
+// Cmp implements comparison of two Time values. Required by
+// starlark.TotallyOrdered interface.
+func (t Time) Cmp(yV starlark.Value, depth int) (int, error) {
 	x := time.Time(t)
 	y := time.Time(yV.(Time))
-	cmp := 0
 	if x.Before(y) {
-		cmp = -1
+		return -1, nil
 	} else if x.After(y) {
-		cmp = 1
+		return 1, nil
 	}
-	return threeway(op, cmp), nil
+	return 0, nil
 }
 
 // Binary implements binary operators, which satisfies the starlark.HasBinary
@@ -485,23 +483,3 @@ func builtinAttrNames(methods map[string]builtinMethod) []string {
 	sort.Strings(names)
 	return names
 }
-
-// Threeway interprets a three-way comparison value cmp (-1, 0, +1)
-// as a boolean comparison (e.g. x < y).
-func threeway(op syntax.Token, cmp int) bool {
-	switch op {
-	case syntax.EQL:
-		return cmp == 0
-	case syntax.NEQ:
-		return cmp != 0
-	case syntax.LE:
-		return cmp <= 0
-	case syntax.LT:
-		return cmp < 0
-	case syntax.GE:
-		return cmp >= 0
-	case syntax.GT:
-		return cmp > 0
-	}
-	panic(op)
-}
diff --git a/repl/repl.go b/repl/repl.go
index 94f8947..6bb7f56 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -20,21 +20,25 @@ import (
 	"os/signal"
 
 	"github.com/chzyer/readline"
-	"go.starlark.net/resolve"
 	"go.starlark.net/starlark"
 	"go.starlark.net/syntax"
 )
 
 var interrupted = make(chan os.Signal, 1)
 
-// REPL executes a read, eval, print loop.
+// REPL calls [REPLOptions] using [syntax.LegacyFileOptions].
+// Deprecated: relies on legacy global variables.
+func REPL(thread *starlark.Thread, globals starlark.StringDict) {
+	REPLOptions(syntax.LegacyFileOptions(), thread, globals)
+}
+
+// REPLOptions executes a read, eval, print loop.
 //
 // Before evaluating each expression, it sets the Starlark thread local
 // variable named "context" to a context.Context that is cancelled by a
 // SIGINT (Control-C). Client-supplied global functions may use this
 // context to make long-running operations interruptable.
-//
-func REPL(thread *starlark.Thread, globals starlark.StringDict) {
+func REPLOptions(opts *syntax.FileOptions, thread *starlark.Thread, globals starlark.StringDict) {
 	signal.Notify(interrupted, os.Interrupt)
 	defer signal.Stop(interrupted)
 
@@ -45,7 +49,7 @@ func REPL(thread *starlark.Thread, globals starlark.StringDict) {
 	}
 	defer rl.Close()
 	for {
-		if err := rep(rl, thread, globals); err != nil {
+		if err := rep(opts, rl, thread, globals); err != nil {
 			if err == readline.ErrInterrupt {
 				fmt.Println(err)
 				continue
@@ -59,7 +63,7 @@ func REPL(thread *starlark.Thread, globals starlark.StringDict) {
 //
 // It returns an error (possibly readline.ErrInterrupt)
 // only if readline failed. Starlark errors are printed.
-func rep(rl *readline.Instance, thread *starlark.Thread, globals starlark.StringDict) error {
+func rep(opts *syntax.FileOptions, rl *readline.Instance, thread *starlark.Thread, globals starlark.StringDict) error {
 	// Each item gets its own context,
 	// which is cancelled by a SIGINT.
 	//
@@ -93,8 +97,14 @@ func rep(rl *readline.Instance, thread *starlark.Thread, globals starlark.String
 		return []byte(line + "\n"), nil
 	}
 
+	// Treat load bindings as global (like they used to be) in the REPL.
+	// Fixes github.com/google/starlark-go/issues/224.
+	opts2 := *opts
+	opts2.LoadBindsGlobally = true
+	opts = &opts2
+
 	// parse
-	f, err := syntax.ParseCompoundStmt("<stdin>", readline)
+	f, err := opts.ParseCompoundStmt("<stdin>", readline)
 	if err != nil {
 		if eof {
 			return io.EOF
@@ -103,16 +113,9 @@ func rep(rl *readline.Instance, thread *starlark.Thread, globals starlark.String
 		return nil
 	}
 
-	// Treat load bindings as global (like they used to be) in the REPL.
-	// This is a workaround for github.com/google/starlark-go/issues/224.
-	// TODO(adonovan): not safe wrt concurrent interpreters.
-	// Come up with a more principled solution (or plumb options everywhere).
-	defer func(prev bool) { resolve.LoadBindsGlobally = prev }(resolve.LoadBindsGlobally)
-	resolve.LoadBindsGlobally = true
-
 	if expr := soleExpr(f); expr != nil {
 		// eval
-		v, err := starlark.EvalExpr(thread, expr, globals)
+		v, err := starlark.EvalExprOptions(f.Options, thread, expr, globals)
 		if err != nil {
 			PrintError(err)
 			return nil
@@ -149,10 +152,16 @@ func PrintError(err error) {
 	}
 }
 
-// MakeLoad returns a simple sequential implementation of module loading
-// suitable for use in the REPL.
-// Each function returned by MakeLoad accesses a distinct private cache.
+// MakeLoad calls [MakeLoadOptions] using [syntax.LegacyFileOptions].
+// Deprecated: relies on legacy global variables.
 func MakeLoad() func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
+	return MakeLoadOptions(syntax.LegacyFileOptions())
+}
+
+// MakeLoadOptions returns a simple sequential implementation of module loading
+// suitable for use in the REPL.
+// Each function returned by MakeLoadOptions accesses a distinct private cache.
+func MakeLoadOptions(opts *syntax.FileOptions) func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
 	type entry struct {
 		globals starlark.StringDict
 		err     error
@@ -173,7 +182,7 @@ func MakeLoad() func(thread *starlark.Thread, module string) (starlark.StringDic
 
 			// Load it.
 			thread := &starlark.Thread{Name: "exec " + module, Load: thread.Load}
-			globals, err := starlark.ExecFile(thread, module, nil, nil)
+			globals, err := starlark.ExecFileOptions(opts, thread, module, nil, nil)
 			e = &entry{globals, err}
 
 			// Update the cache.
diff --git a/resolve/binding.go b/resolve/binding.go
index 6b99f4b..8507e64 100644
--- a/resolve/binding.go
+++ b/resolve/binding.go
@@ -10,7 +10,7 @@ import "go.starlark.net/syntax"
 // We cannot guarantee API stability for these types
 // as they are closely tied to the implementation.
 
-// A Binding contains resolver information about an identifer.
+// A Binding contains resolver information about an identifier.
 // The resolver populates the Binding field of each syntax.Identifier.
 // The Binding ties together all identifiers that denote the same variable.
 type Binding struct {
diff --git a/resolve/resolve.go b/resolve/resolve.go
index 56e33ba..8c780d8 100644
--- a/resolve/resolve.go
+++ b/resolve/resolve.go
@@ -97,6 +97,9 @@ const doesnt = "this Starlark dialect does not "
 // global options
 // These features are either not standard Starlark (yet), or deprecated
 // features of the BUILD language, so we put them behind flags.
+//
+// Deprecated: use an explicit [syntax.FileOptions] argument instead,
+// as it avoids all the usual problems of global variables.
 var (
 	AllowSet            = false // allow the 'set' built-in
 	AllowGlobalReassign = false // allow reassignment to top-level names; also, allow if/for/while at top-level
@@ -130,7 +133,7 @@ func File(file *syntax.File, isPredeclared, isUniversal func(name string) bool)
 // REPLChunk is a generalization of the File function that supports a
 // non-empty initial global block, as occurs in a REPL.
 func REPLChunk(file *syntax.File, isGlobal, isPredeclared, isUniversal func(name string) bool) error {
-	r := newResolver(isGlobal, isPredeclared, isUniversal)
+	r := newResolver(file.Options, isGlobal, isPredeclared, isUniversal)
 	r.stmts(file.Stmts)
 
 	r.env.resolveLocalUses()
@@ -151,12 +154,18 @@ func REPLChunk(file *syntax.File, isGlobal, isPredeclared, isUniversal func(name
 	return nil
 }
 
-// Expr resolves the specified expression.
+// Expr calls [ExprOptions] using [syntax.LegacyFileOptions].
+// Deprecated: relies on legacy global variables.
+func Expr(expr syntax.Expr, isPredeclared, isUniversal func(name string) bool) ([]*Binding, error) {
+	return ExprOptions(syntax.LegacyFileOptions(), expr, isPredeclared, isUniversal)
+}
+
+// ExprOptions resolves the specified expression.
 // It returns the local variables bound within the expression.
 //
-// The isPredeclared and isUniversal predicates behave as for the File function.
-func Expr(expr syntax.Expr, isPredeclared, isUniversal func(name string) bool) ([]*Binding, error) {
-	r := newResolver(nil, isPredeclared, isUniversal)
+// The isPredeclared and isUniversal predicates behave as for the File function
+func ExprOptions(opts *syntax.FileOptions, expr syntax.Expr, isPredeclared, isUniversal func(name string) bool) ([]*Binding, error) {
+	r := newResolver(opts, nil, isPredeclared, isUniversal)
 	r.expr(expr)
 	r.env.resolveLocalUses()
 	r.resolveNonLocalUses(r.env) // globals & universals
@@ -179,9 +188,10 @@ type Error struct {
 
 func (e Error) Error() string { return e.Pos.String() + ": " + e.Msg }
 
-func newResolver(isGlobal, isPredeclared, isUniversal func(name string) bool) *resolver {
+func newResolver(options *syntax.FileOptions, isGlobal, isPredeclared, isUniversal func(name string) bool) *resolver {
 	file := new(block)
 	return &resolver{
+		options:       options,
 		file:          file,
 		env:           file,
 		isGlobal:      isGlobal,
@@ -193,6 +203,8 @@ func newResolver(isGlobal, isPredeclared, isUniversal func(name string) bool) *r
 }
 
 type resolver struct {
+	options *syntax.FileOptions
+
 	// env is the current local environment:
 	// a linked list of blocks, innermost first.
 	// The tail of the list is the file block.
@@ -314,7 +326,7 @@ func (r *resolver) bind(id *syntax.Ident) bool {
 				r.moduleGlobals = append(r.moduleGlobals, bind)
 			}
 		}
-		if ok && !AllowGlobalReassign {
+		if ok && !r.options.GlobalReassign {
 			r.errorf(id.NamePos, "cannot reassign %s %s declared at %s",
 				bind.Scope, id.Name, bind.First.NamePos)
 		}
@@ -382,7 +394,7 @@ func (r *resolver) use(id *syntax.Ident) {
 	// We will piggyback support for the legacy semantics on the
 	// AllowGlobalReassign flag, which is loosely related and also
 	// required for Bazel.
-	if AllowGlobalReassign && r.env == r.file {
+	if r.options.GlobalReassign && r.env == r.file {
 		r.useToplevel(use)
 		return
 	}
@@ -420,7 +432,7 @@ func (r *resolver) useToplevel(use use) (bind *Binding) {
 		r.predeclared[id.Name] = bind // save it
 	} else if r.isUniversal(id.Name) {
 		// use of universal name
-		if !AllowSet && id.Name == "set" {
+		if !r.options.Set && id.Name == "set" {
 			r.errorf(id.NamePos, doesnt+"support sets")
 		}
 		bind = &Binding{Scope: Universal}
@@ -493,7 +505,7 @@ func (r *resolver) stmt(stmt syntax.Stmt) {
 		}
 
 	case *syntax.IfStmt:
-		if !AllowGlobalReassign && r.container().function == nil {
+		if !r.options.GlobalReassign && r.container().function == nil {
 			r.errorf(stmt.If, "if statement not within a function")
 		}
 		r.expr(stmt.Cond)
@@ -519,7 +531,7 @@ func (r *resolver) stmt(stmt syntax.Stmt) {
 		r.function(fn, stmt.Def)
 
 	case *syntax.ForStmt:
-		if !AllowGlobalReassign && r.container().function == nil {
+		if !r.options.GlobalReassign && r.container().function == nil {
 			r.errorf(stmt.For, "for loop not within a function")
 		}
 		r.expr(stmt.X)
@@ -530,10 +542,10 @@ func (r *resolver) stmt(stmt syntax.Stmt) {
 		r.loops--
 
 	case *syntax.WhileStmt:
-		if !AllowRecursion {
+		if !r.options.While {
 			r.errorf(stmt.While, doesnt+"support while loops")
 		}
-		if !AllowGlobalReassign && r.container().function == nil {
+		if !r.options.GlobalReassign && r.container().function == nil {
 			r.errorf(stmt.While, "while loop not within a function")
 		}
 		r.expr(stmt.Cond)
@@ -569,9 +581,9 @@ func (r *resolver) stmt(stmt syntax.Stmt) {
 			}
 
 			id := stmt.To[i]
-			if LoadBindsGlobally {
+			if r.options.LoadBindsGlobally {
 				r.bind(id)
-			} else if r.bindLocal(id) && !AllowGlobalReassign {
+			} else if r.bindLocal(id) && !r.options.GlobalReassign {
 				// "Global" in AllowGlobalReassign is a misnomer for "toplevel".
 				// Sadly we can't report the previous declaration
 				// as id.Binding may not be set yet.
@@ -746,7 +758,7 @@ func (r *resolver) expr(e syntax.Expr) {
 				}
 				x := binop.X.(*syntax.Ident)
 				if seenName[x.Name] {
-					r.errorf(x.NamePos, "keyword argument %s repeated", x.Name)
+					r.errorf(x.NamePos, "keyword argument %q is repeated", x.Name)
 				} else {
 					if seenName == nil {
 						seenName = make(map[string]bool)
diff --git a/starlark/eval.go b/starlark/eval.go
index 4941d44..706c624 100644
--- a/starlark/eval.go
+++ b/starlark/eval.go
@@ -51,8 +51,13 @@ type Thread struct {
 	// The default behavior is to call thread.Cancel("too many steps").
 	OnMaxSteps func(thread *Thread)
 
-	// steps counts abstract computation steps executed by this thread.
-	steps, maxSteps uint64
+	// Steps a count of abstract computation steps executed
+	// by this thread. It is incremented by the interpreter. It may be used
+	// as a measure of the approximate cost of Starlark execution, by
+	// computing the difference in its value before and after a computation.
+	//
+	// The precise meaning of "step" is not specified and may change.
+	Steps, maxSteps uint64
 
 	// cancelReason records the reason from the first call to Cancel.
 	cancelReason *string
@@ -65,14 +70,9 @@ type Thread struct {
 	proftime time.Duration
 }
 
-// ExecutionSteps returns a count of abstract computation steps executed
-// by this thread. It is incremented by the interpreter. It may be used
-// as a measure of the approximate cost of Starlark execution, by
-// computing the difference in its value before and after a computation.
-//
-// The precise meaning of "step" is not specified and may change.
+// ExecutionSteps returns the current value of Steps.
 func (thread *Thread) ExecutionSteps() uint64 {
-	return thread.steps
+	return thread.Steps
 }
 
 // SetMaxExecutionSteps sets a limit on the number of Starlark
@@ -84,12 +84,20 @@ func (thread *Thread) SetMaxExecutionSteps(max uint64) {
 	thread.maxSteps = max
 }
 
+// Uncancel resets the cancellation state.
+//
+// Unlike most methods of Thread, it is safe to call Uncancel from any
+// goroutine, even if the thread is actively executing.
+func (thread *Thread) Uncancel() {
+	atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&thread.cancelReason)), nil)
+}
+
 // Cancel causes execution of Starlark code in the specified thread to
 // promptly fail with an EvalError that includes the specified reason.
 // There may be a delay before the interpreter observes the cancellation
 // if the thread is currently in a call to a built-in function.
 //
-// Cancellation cannot be undone.
+// Call [Uncancel] to reset the cancellation state.
 //
 // Unlike most methods of Thread, it is safe to call Cancel from any
 // goroutine, even if the thread is actively executing.
@@ -317,7 +325,13 @@ func (prog *Program) Write(out io.Writer) error {
 	return err
 }
 
-// ExecFile parses, resolves, and executes a Starlark file in the
+// ExecFile calls [ExecFileOptions] using [syntax.LegacyFileOptions].
+// Deprecated: relies on legacy global variables.
+func ExecFile(thread *Thread, filename string, src interface{}, predeclared StringDict) (StringDict, error) {
+	return ExecFileOptions(syntax.LegacyFileOptions(), thread, filename, src, predeclared)
+}
+
+// ExecFileOptions parses, resolves, and executes a Starlark file in the
 // specified global environment, which may be modified during execution.
 //
 // Thread is the state associated with the Starlark thread.
@@ -332,11 +346,11 @@ func (prog *Program) Write(out io.Writer) error {
 // Execution does not modify this dictionary, though it may mutate
 // its values.
 //
-// If ExecFile fails during evaluation, it returns an *EvalError
+// If ExecFileOptions fails during evaluation, it returns an *EvalError
 // containing a backtrace.
-func ExecFile(thread *Thread, filename string, src interface{}, predeclared StringDict) (StringDict, error) {
+func ExecFileOptions(opts *syntax.FileOptions, thread *Thread, filename string, src interface{}, predeclared StringDict) (StringDict, error) {
 	// Parse, resolve, and compile a Starlark source file.
-	_, mod, err := SourceProgram(filename, src, predeclared.Has)
+	_, mod, err := SourceProgramOptions(opts, filename, src, predeclared.Has)
 	if err != nil {
 		return nil, err
 	}
@@ -346,7 +360,13 @@ func ExecFile(thread *Thread, filename string, src interface{}, predeclared Stri
 	return g, err
 }
 
-// SourceProgram produces a new program by parsing, resolving,
+// SourceProgram calls [SourceProgramOptions] using [syntax.LegacyFileOptions].
+// Deprecated: relies on legacy global variables.
+func SourceProgram(filename string, src interface{}, isPredeclared func(string) bool) (*syntax.File, *Program, error) {
+	return SourceProgramOptions(syntax.LegacyFileOptions(), filename, src, isPredeclared)
+}
+
+// SourceProgramOptions produces a new program by parsing, resolving,
 // and compiling a Starlark source file.
 // On success, it returns the parsed file and the compiled program.
 // The filename and src parameters are as for syntax.Parse.
@@ -355,8 +375,8 @@ func ExecFile(thread *Thread, filename string, src interface{}, predeclared Stri
 // a pre-declared identifier of the current module.
 // Its typical value is predeclared.Has,
 // where predeclared is a StringDict of pre-declared values.
-func SourceProgram(filename string, src interface{}, isPredeclared func(string) bool) (*syntax.File, *Program, error) {
-	f, err := syntax.Parse(filename, src, 0)
+func SourceProgramOptions(opts *syntax.FileOptions, filename string, src interface{}, isPredeclared func(string) bool) (*syntax.File, *Program, error) {
+	f, err := opts.Parse(filename, src, 0)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -388,7 +408,7 @@ func FileProgram(f *syntax.File, isPredeclared func(string) bool) (*Program, err
 	}
 
 	module := f.Module.(*resolve.Module)
-	compiled := compile.File(f.Stmts, pos, "<toplevel>", module.Locals, module.Globals)
+	compiled := compile.File(f.Options, f.Stmts, pos, "<toplevel>", module.Locals, module.Globals)
 
 	return &Program{compiled}, nil
 }
@@ -445,7 +465,7 @@ func ExecREPLChunk(f *syntax.File, thread *Thread, globals StringDict) error {
 	}
 
 	module := f.Module.(*resolve.Module)
-	compiled := compile.File(f.Stmts, pos, "<toplevel>", module.Locals, module.Globals)
+	compiled := compile.File(f.Options, f.Stmts, pos, "<toplevel>", module.Locals, module.Globals)
 	prog := &Program{compiled}
 
 	// -- variant of Program.Init --
@@ -504,7 +524,13 @@ func makeToplevelFunction(prog *compile.Program, predeclared StringDict) *Functi
 	}
 }
 
-// Eval parses, resolves, and evaluates an expression within the
+// Eval calls [EvalOptions] using [syntax.LegacyFileOptions].
+// Deprecated: relies on legacy global variables.
+func Eval(thread *Thread, filename string, src interface{}, env StringDict) (Value, error) {
+	return EvalOptions(syntax.LegacyFileOptions(), thread, filename, src, env)
+}
+
+// EvalOptions parses, resolves, and evaluates an expression within the
 // specified (predeclared) environment.
 //
 // Evaluation cannot mutate the environment dictionary itself,
@@ -512,58 +538,71 @@ func makeToplevelFunction(prog *compile.Program, predeclared StringDict) *Functi
 //
 // The filename and src parameters are as for syntax.Parse.
 //
-// If Eval fails during evaluation, it returns an *EvalError
+// If EvalOptions fails during evaluation, it returns an *EvalError
 // containing a backtrace.
-func Eval(thread *Thread, filename string, src interface{}, env StringDict) (Value, error) {
-	expr, err := syntax.ParseExpr(filename, src, 0)
+func EvalOptions(opts *syntax.FileOptions, thread *Thread, filename string, src interface{}, env StringDict) (Value, error) {
+	expr, err := opts.ParseExpr(filename, src, 0)
 	if err != nil {
 		return nil, err
 	}
-	f, err := makeExprFunc(expr, env)
+	f, err := makeExprFunc(opts, expr, env)
 	if err != nil {
 		return nil, err
 	}
 	return Call(thread, f, nil, nil)
 }
 
-// EvalExpr resolves and evaluates an expression within the
+// EvalExpr calls [EvalExprOptions] using [syntax.LegacyFileOptions].
+// Deprecated: relies on legacy global variables.
+func EvalExpr(thread *Thread, expr syntax.Expr, env StringDict) (Value, error) {
+	return EvalExprOptions(syntax.LegacyFileOptions(), thread, expr, env)
+}
+
+// EvalExprOptions resolves and evaluates an expression within the
 // specified (predeclared) environment.
 // Evaluating a comma-separated list of expressions yields a tuple value.
 //
 // Resolving an expression mutates it.
-// Do not call EvalExpr more than once for the same expression.
+// Do not call EvalExprOptions more than once for the same expression.
 //
 // Evaluation cannot mutate the environment dictionary itself,
 // though it may modify variables reachable from the dictionary.
 //
-// If Eval fails during evaluation, it returns an *EvalError
+// If EvalExprOptions fails during evaluation, it returns an *EvalError
 // containing a backtrace.
-func EvalExpr(thread *Thread, expr syntax.Expr, env StringDict) (Value, error) {
-	fn, err := makeExprFunc(expr, env)
+func EvalExprOptions(opts *syntax.FileOptions, thread *Thread, expr syntax.Expr, env StringDict) (Value, error) {
+	fn, err := makeExprFunc(opts, expr, env)
 	if err != nil {
 		return nil, err
 	}
 	return Call(thread, fn, nil, nil)
 }
 
+// ExprFunc calls [ExprFuncOptions] using [syntax.LegacyFileOptions].
+// Deprecated: relies on legacy global variables.
+func ExprFunc(filename string, src interface{}, env StringDict) (*Function, error) {
+	return ExprFuncOptions(syntax.LegacyFileOptions(), filename, src, env)
+}
+
 // ExprFunc returns a no-argument function
 // that evaluates the expression whose source is src.
-func ExprFunc(filename string, src interface{}, env StringDict) (*Function, error) {
-	expr, err := syntax.ParseExpr(filename, src, 0)
+func ExprFuncOptions(options *syntax.FileOptions, filename string, src interface{}, env StringDict) (*Function, error) {
+	expr, err := options.ParseExpr(filename, src, 0)
 	if err != nil {
 		return nil, err
 	}
-	return makeExprFunc(expr, env)
+	return makeExprFunc(options, expr, env)
 }
 
 // makeExprFunc returns a no-argument function whose body is expr.
-func makeExprFunc(expr syntax.Expr, env StringDict) (*Function, error) {
-	locals, err := resolve.Expr(expr, env.Has, Universe.Has)
+// The options must be consistent with those used when parsing expr.
+func makeExprFunc(opts *syntax.FileOptions, expr syntax.Expr, env StringDict) (*Function, error) {
+	locals, err := resolve.ExprOptions(opts, expr, env.Has, Universe.Has)
 	if err != nil {
 		return nil, err
 	}
 
-	return makeToplevelFunction(compile.Expr(expr, "<expr>", locals), env), nil
+	return makeToplevelFunction(compile.Expr(opts, expr, "<expr>", locals), env), nil
 }
 
 // The following functions are primitive operations of the byte code interpreter.
@@ -1062,10 +1101,10 @@ func Binary(op syntax.Token, x, y Value) (Value, error) {
 				if x.Len() > y.Len() {
 					x, y = y, x // opt: range over smaller set
 				}
-				for _, xelem := range x.elems() {
+				for xe := x.ht.head; xe != nil; xe = xe.next {
 					// Has, Insert cannot fail here.
-					if found, _ := y.Has(xelem); found {
-						set.Insert(xelem)
+					if found, _ := y.Has(xe.key); found {
+						set.Insert(xe.key)
 					}
 				}
 				return set, nil
@@ -1081,14 +1120,14 @@ func Binary(op syntax.Token, x, y Value) (Value, error) {
 		case *Set: // symmetric difference
 			if y, ok := y.(*Set); ok {
 				set := new(Set)
-				for _, xelem := range x.elems() {
-					if found, _ := y.Has(xelem); !found {
-						set.Insert(xelem)
+				for xe := x.ht.head; xe != nil; xe = xe.next {
+					if found, _ := y.Has(xe.key); !found {
+						set.Insert(xe.key)
 					}
 				}
-				for _, yelem := range y.elems() {
-					if found, _ := x.Has(yelem); !found {
-						set.Insert(yelem)
+				for ye := y.ht.head; ye != nil; ye = ye.next {
+					if found, _ := x.Has(ye.key); !found {
+						set.Insert(ye.key)
 					}
 				}
 				return set, nil
@@ -1225,8 +1264,22 @@ func Call(thread *Thread, fn Value, args Tuple, kwargs []Tuple) (Value, error) {
 	fr.callable = c
 
 	thread.beginProfSpan()
+
+	// Use defer to ensure that panics from built-ins
+	// pass through the interpreter without leaving
+	// it in a bad state.
+	defer func() {
+		thread.endProfSpan()
+
+		// clear out any references
+		// TODO(adonovan): opt: zero fr.Locals and
+		// reuse it if it is large enough.
+		*fr = frame{}
+
+		thread.stack = thread.stack[:len(thread.stack)-1] // pop
+	}()
+
 	result, err := c.CallInternal(thread, args, kwargs)
-	thread.endProfSpan()
 
 	// Sanity check: nil is not a valid Starlark value.
 	if result == nil && err == nil {
@@ -1240,9 +1293,6 @@ func Call(thread *Thread, fn Value, args Tuple, kwargs []Tuple) (Value, error) {
 		}
 	}
 
-	*fr = frame{}                                     // clear out any references
-	thread.stack = thread.stack[:len(thread.stack)-1] // pop
-
 	return result, err
 }
 
diff --git a/starlark/hashtable.go b/starlark/hashtable.go
index 9fab60e..252d21d 100644
--- a/starlark/hashtable.go
+++ b/starlark/hashtable.go
@@ -66,16 +66,9 @@ func (ht *hashtable) init(size int) {
 func (ht *hashtable) freeze() {
 	if !ht.frozen {
 		ht.frozen = true
-		for i := range ht.table {
-			for p := &ht.table[i]; p != nil; p = p.next {
-				for i := range p.entries {
-					e := &p.entries[i]
-					if e.hash != 0 {
-						e.key.Freeze()
-						e.value.Freeze()
-					}
-				}
-			}
+		for e := ht.head; e != nil; e = e.next {
+			e.key.Freeze()
+			e.value.Freeze()
 		}
 	}
 }
@@ -162,13 +155,12 @@ func overloaded(elems, buckets int) bool {
 
 func (ht *hashtable) grow() {
 	// Double the number of buckets and rehash.
-	// TODO(adonovan): opt:
-	// - avoid reentrant calls to ht.insert, and specialize it.
-	//   e.g. we know the calls to Equals will return false since
-	//   there are no duplicates among the old keys.
-	// - saving the entire hash in the bucket would avoid the need to
-	//   recompute the hash.
-	// - save the old buckets on a free list.
+	//
+	// Even though this makes reentrant calls to ht.insert,
+	// calls Equals unnecessarily (since there can't be duplicate keys),
+	// and recomputes the hash unnecessarily, the gains from
+	// avoiding these steps were found to be too small to justify
+	// the extra logic: -2% on hashtable benchmark.
 	ht.table = make([]bucket, len(ht.table)<<1)
 	oldhead := ht.head
 	ht.head = nil
@@ -372,6 +364,8 @@ func (it *keyIterator) Done() {
 	}
 }
 
+// TODO(adonovan): use go1.19's maphash.String.
+
 // hashString computes the hash of s.
 func hashString(s string) uint32 {
 	if len(s) >= 12 {
diff --git a/starlark/int.go b/starlark/int.go
index 398811b..93567c5 100644
--- a/starlark/int.go
+++ b/starlark/int.go
@@ -190,20 +190,30 @@ func (i Int) Hash() (uint32, error) {
 	}
 	return 12582917 * uint32(lo+3), nil
 }
-func (x Int) CompareSameType(op syntax.Token, v Value, depth int) (bool, error) {
-	y := v.(Int)
-	xSmall, xBig := x.get()
-	ySmall, yBig := y.get()
-	if xBig != nil || yBig != nil {
-		return threeway(op, x.bigInt().Cmp(y.bigInt())), nil
+
+// Cmp implements comparison of two Int values.
+// Required by the TotallyOrdered interface.
+func (i Int) Cmp(v Value, depth int) (int, error) {
+	j := v.(Int)
+	iSmall, iBig := i.get()
+	jSmall, jBig := j.get()
+	if iBig != nil || jBig != nil {
+		return i.bigInt().Cmp(j.bigInt()), nil
 	}
-	return threeway(op, signum64(xSmall-ySmall)), nil
+	return signum64(iSmall - jSmall), nil // safe: int32 operands
 }
 
 // Float returns the float value nearest i.
 func (i Int) Float() Float {
 	iSmall, iBig := i.get()
 	if iBig != nil {
+		// Fast path for hardware int-to-float conversions.
+		if iBig.IsUint64() {
+			return Float(iBig.Uint64())
+		} else if iBig.IsInt64() {
+			return Float(iBig.Int64())
+		}
+
 		f, _ := new(big.Float).SetInt(iBig).Float64()
 		return Float(f)
 	}
diff --git a/starlark/int_generic.go b/starlark/int_generic.go
index f2adf45..d54bc2a 100644
--- a/starlark/int_generic.go
+++ b/starlark/int_generic.go
@@ -1,5 +1,5 @@
-//go:build (!linux && !darwin && !dragonfly && !freebsd && !netbsd && !solaris) || (!amd64 && !arm64 && !mips64x && !ppc64x && !loong64)
-// +build !linux,!darwin,!dragonfly,!freebsd,!netbsd,!solaris !amd64,!arm64,!mips64x,!ppc64x,!loong64
+//go:build (!linux && !darwin && !dragonfly && !freebsd && !netbsd && !solaris) || (!amd64 && !arm64 && !mips64x && !ppc64 && !ppc64le && !loong64 && !s390x)
+// +build !linux,!darwin,!dragonfly,!freebsd,!netbsd,!solaris !amd64,!arm64,!mips64x,!ppc64,!ppc64le,!loong64,!s390x
 
 package starlark
 
diff --git a/starlark/int_posix64.go b/starlark/int_posix64.go
index 2ab0bed..36c56e5 100644
--- a/starlark/int_posix64.go
+++ b/starlark/int_posix64.go
@@ -1,6 +1,6 @@
-//go:build (linux || darwin || dragonfly || freebsd || netbsd || solaris) && (amd64 || arm64 || mips64x || ppc64x || loong64)
+//go:build (linux || darwin || dragonfly || freebsd || netbsd || solaris) && (amd64 || arm64 || mips64x || ppc64 || ppc64le || loong64 || s390x)
 // +build linux darwin dragonfly freebsd netbsd solaris
-// +build amd64 arm64 mips64x ppc64x loong64
+// +build amd64 arm64 mips64x ppc64 ppc64le loong64 s390x
 
 package starlark
 
diff --git a/starlark/interp.go b/starlark/interp.go
index 9961e2f..d29e525 100644
--- a/starlark/interp.go
+++ b/starlark/interp.go
@@ -10,7 +10,6 @@ import (
 
 	"go.starlark.net/internal/compile"
 	"go.starlark.net/internal/spell"
-	"go.starlark.net/resolve"
 	"go.starlark.net/syntax"
 )
 
@@ -24,19 +23,19 @@ func (fn *Function) CallInternal(thread *Thread, args Tuple, kwargs []Tuple) (Va
 	// Postcondition: args is not mutated. This is stricter than required by Callable,
 	// but allows CALL to avoid a copy.
 
-	if !resolve.AllowRecursion {
+	f := fn.funcode
+	if !f.Prog.Recursion {
 		// detect recursion
 		for _, fr := range thread.stack[:len(thread.stack)-1] {
 			// We look for the same function code,
 			// not function value, otherwise the user could
 			// defeat the check by writing the Y combinator.
-			if frfn, ok := fr.Callable().(*Function); ok && frfn.funcode == fn.funcode {
+			if frfn, ok := fr.Callable().(*Function); ok && frfn.funcode == f {
 				return nil, fmt.Errorf("function %s called recursively", fn.Name())
 			}
 		}
 	}
 
-	f := fn.funcode
 	fr := thread.frameAt(0)
 
 	// Allocate space for stack and locals.
@@ -81,14 +80,25 @@ func (fn *Function) CallInternal(thread *Thread, args Tuple, kwargs []Tuple) (Va
 
 	var iterstack []Iterator // stack of active iterators
 
+	// Use defer so that application panics can pass through
+	// interpreter without leaving thread in a bad state.
+	defer func() {
+		// ITERPOP the rest of the iterator stack.
+		for _, iter := range iterstack {
+			iter.Done()
+		}
+
+		fr.locals = nil
+	}()
+
 	sp := 0
 	var pc uint32
 	var result Value
 	code := f.Code
 loop:
 	for {
-		thread.steps++
-		if thread.steps >= thread.maxSteps {
+		thread.Steps++
+		if thread.Steps >= thread.maxSteps {
 			if thread.OnMaxSteps != nil {
 				thread.OnMaxSteps(thread)
 			} else {
@@ -646,14 +656,7 @@ loop:
 			break loop
 		}
 	}
-
-	// ITERPOP the rest of the iterator stack.
-	for _, iter := range iterstack {
-		iter.Done()
-	}
-
-	fr.locals = nil
-
+	// (deferred cleanup runs here)
 	return result, err
 }
 
diff --git a/starlark/library.go b/starlark/library.go
index 1c801be..0e3c7bf 100644
--- a/starlark/library.go
+++ b/starlark/library.go
@@ -140,7 +140,12 @@ var (
 	}
 
 	setMethods = map[string]*Builtin{
-		"union": NewBuiltin("union", set_union),
+		"add":     NewBuiltin("add", set_add),
+		"clear":   NewBuiltin("clear", set_clear),
+		"discard": NewBuiltin("discard", set_discard),
+		"pop":     NewBuiltin("pop", set_pop),
+		"remove":  NewBuiltin("remove", set_remove),
+		"union":   NewBuiltin("union", set_union),
 	}
 )
 
@@ -2168,6 +2173,85 @@ func string_splitlines(_ *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value
 	return NewList(list), nil
 }
 
+// https://github.com/google/starlark-go/blob/master/doc/spec.md#set·add.
+func set_add(_ *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error) {
+	var elem Value
+	if err := UnpackPositionalArgs(b.Name(), args, kwargs, 1, &elem); err != nil {
+		return nil, err
+	}
+	if found, err := b.Receiver().(*Set).Has(elem); err != nil {
+		return nil, nameErr(b, err)
+	} else if found {
+		return None, nil
+	}
+	err := b.Receiver().(*Set).Insert(elem)
+	if err != nil {
+		return nil, nameErr(b, err)
+	}
+	return None, nil
+}
+
+// https://github.com/google/starlark-go/blob/master/doc/spec.md#set·clear.
+func set_clear(_ *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error) {
+	if err := UnpackPositionalArgs(b.Name(), args, kwargs, 0); err != nil {
+		return nil, err
+	}
+	if b.Receiver().(*Set).Len() > 0 {
+		if err := b.Receiver().(*Set).Clear(); err != nil {
+			return nil, nameErr(b, err)
+		}
+	}
+	return None, nil
+}
+
+// https://github.com/google/starlark-go/blob/master/doc/spec.md#set·discard.
+func set_discard(_ *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error) {
+	var k Value
+	if err := UnpackPositionalArgs(b.Name(), args, kwargs, 1, &k); err != nil {
+		return nil, err
+	}
+	if found, err := b.Receiver().(*Set).Has(k); err != nil {
+		return nil, nameErr(b, err)
+	} else if !found {
+		return None, nil
+	}
+	if _, err := b.Receiver().(*Set).Delete(k); err != nil {
+		return nil, nameErr(b, err) // set is frozen
+	}
+	return None, nil
+}
+
+// https://github.com/google/starlark-go/blob/master/doc/spec.md#set·pop.
+func set_pop(_ *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error) {
+	if err := UnpackPositionalArgs(b.Name(), args, kwargs, 0); err != nil {
+		return nil, err
+	}
+	recv := b.Receiver().(*Set)
+	k, ok := recv.ht.first()
+	if !ok {
+		return nil, nameErr(b, "empty set")
+	}
+	_, err := recv.Delete(k)
+	if err != nil {
+		return nil, nameErr(b, err) // set is frozen
+	}
+	return k, nil
+}
+
+// https://github.com/google/starlark-go/blob/master/doc/spec.md#set·remove.
+func set_remove(_ *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error) {
+	var k Value
+	if err := UnpackPositionalArgs(b.Name(), args, kwargs, 1, &k); err != nil {
+		return nil, err
+	}
+	if found, err := b.Receiver().(*Set).Delete(k); err != nil {
+		return nil, nameErr(b, err) // dict is frozen or key is unhashable
+	} else if found {
+		return None, nil
+	}
+	return nil, nameErr(b, "missing key")
+}
+
 // https://github.com/google/starlark-go/blob/master/doc/spec.md#set·union.
 func set_union(_ *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error) {
 	var iterable Iterable
diff --git a/starlark/profile.go b/starlark/profile.go
index 38da2b2..590a4e2 100644
--- a/starlark/profile.go
+++ b/starlark/profile.go
@@ -101,11 +101,11 @@ func StartProfile(w io.Writer) error {
 	return nil
 }
 
-// StopProfiler stops the profiler started by a prior call to
+// StopProfile stops the profiler started by a prior call to
 // StartProfile and finalizes the profile. It returns an error if the
 // profile could not be completed.
 //
-// StopProfiler must not be called concurrently with Starlark execution.
+// StopProfile must not be called concurrently with Starlark execution.
 func StopProfile() error {
 	// Terminate the profiler goroutine and get its result.
 	close(profiler.events)
diff --git a/starlark/unpack.go b/starlark/unpack.go
index abc71b8..3168589 100644
--- a/starlark/unpack.go
+++ b/starlark/unpack.go
@@ -300,7 +300,9 @@ func unpackOneArg(v Value, ptr interface{}) error {
 			// Attempt to call Value.Type method.
 			func() {
 				defer func() { recover() }()
-				paramType = paramVar.MethodByName("Type").Call(nil)[0].String()
+				if typer, _ := paramVar.Interface().(interface{ Type() string }); typer != nil {
+					paramType = typer.Type()
+				}
 			}()
 			return fmt.Errorf("got %s, want %s", v.Type(), paramType)
 		}
diff --git a/starlark/value.go b/starlark/value.go
index b51d4ca..3ceacc6 100644
--- a/starlark/value.go
+++ b/starlark/value.go
@@ -7,35 +7,35 @@
 // Starlark values are represented by the Value interface.
 // The following built-in Value types are known to the evaluator:
 //
-//      NoneType        -- NoneType
-//      Bool            -- bool
-//      Bytes           -- bytes
-//      Int             -- int
-//      Float           -- float
-//      String          -- string
-//      *List           -- list
-//      Tuple           -- tuple
-//      *Dict           -- dict
-//      *Set            -- set
-//      *Function       -- function (implemented in Starlark)
-//      *Builtin        -- builtin_function_or_method (function or method implemented in Go)
+//	NoneType        -- NoneType
+//	Bool            -- bool
+//	Bytes           -- bytes
+//	Int             -- int
+//	Float           -- float
+//	String          -- string
+//	*List           -- list
+//	Tuple           -- tuple
+//	*Dict           -- dict
+//	*Set            -- set
+//	*Function       -- function (implemented in Starlark)
+//	*Builtin        -- builtin_function_or_method (function or method implemented in Go)
 //
 // Client applications may define new data types that satisfy at least
 // the Value interface.  Such types may provide additional operations by
 // implementing any of these optional interfaces:
 //
-//      Callable        -- value is callable like a function
-//      Comparable      -- value defines its own comparison operations
-//      Iterable        -- value is iterable using 'for' loops
-//      Sequence        -- value is iterable sequence of known length
-//      Indexable       -- value is sequence with efficient random access
-//      Mapping         -- value maps from keys to values, like a dictionary
-//      HasBinary       -- value defines binary operations such as * and +
-//      HasAttrs        -- value has readable fields or methods x.f
-//      HasSetField     -- value has settable fields x.f
-//      HasSetIndex     -- value supports element update using x[i]=y
-//      HasSetKey       -- value supports map update using x[k]=v
-//      HasUnary        -- value defines unary operations such as + and -
+//	Callable        -- value is callable like a function
+//	Comparable      -- value defines its own comparison operations
+//	Iterable        -- value is iterable using 'for' loops
+//	Sequence        -- value is iterable sequence of known length
+//	Indexable       -- value is sequence with efficient random access
+//	Mapping         -- value maps from keys to values, like a dictionary
+//	HasBinary       -- value defines binary operations such as * and +
+//	HasAttrs        -- value has readable fields or methods x.f
+//	HasSetField     -- value has settable fields x.f
+//	HasSetIndex     -- value supports element update using x[i]=y
+//	HasSetKey       -- value supports map update using x[k]=v
+//	HasUnary        -- value defines unary operations such as + and -
 //
 // Client applications may also define domain-specific functions in Go
 // and make them available to Starlark programs.  Use NewBuiltin to
@@ -63,7 +63,6 @@
 // through Starlark code and into callbacks.  When evaluation fails it
 // returns an EvalError from which the application may obtain a
 // backtrace of active Starlark calls.
-//
 package starlark // import "go.starlark.net/starlark"
 
 // This file defines the data types of Starlark and their basic operations.
@@ -132,15 +131,41 @@ type Comparable interface {
 	CompareSameType(op syntax.Token, y Value, depth int) (bool, error)
 }
 
+// A TotallyOrdered is a type whose values form a total order:
+// if x and y are of the same TotallyOrdered type, then x must be less than y,
+// greater than y, or equal to y.
+//
+// It is simpler than Comparable and should be preferred in new code,
+// but if a type implements both interfaces, Comparable takes precedence.
+type TotallyOrdered interface {
+	Value
+	// Cmp compares two values x and y of the same totally ordered type.
+	// It returns negative if x < y, positive if x > y, and zero if the values are equal.
+	//
+	// Implementations that recursively compare subcomponents of
+	// the value should use the CompareDepth function, not Cmp, to
+	// avoid infinite recursion on cyclic structures.
+	//
+	// The depth parameter is used to bound comparisons of cyclic
+	// data structures.  Implementations should decrement depth
+	// before calling CompareDepth and should return an error if depth
+	// < 1.
+	//
+	// Client code should not call this method.  Instead, use the
+	// standalone Compare or Equals functions, which are defined for
+	// all pairs of operands.
+	Cmp(y Value, depth int) (int, error)
+}
+
 var (
-	_ Comparable = Int{}
-	_ Comparable = False
-	_ Comparable = Float(0)
-	_ Comparable = String("")
-	_ Comparable = (*Dict)(nil)
-	_ Comparable = (*List)(nil)
-	_ Comparable = Tuple(nil)
-	_ Comparable = (*Set)(nil)
+	_ TotallyOrdered = Int{}
+	_ TotallyOrdered = Float(0)
+	_ Comparable     = False
+	_ Comparable     = String("")
+	_ Comparable     = (*Dict)(nil)
+	_ Comparable     = (*List)(nil)
+	_ Comparable     = Tuple(nil)
+	_ Comparable     = (*Set)(nil)
 )
 
 // A Callable value f may be the operand of a function call, f(x).
@@ -229,13 +254,12 @@ var (
 //
 // Example usage:
 //
-// 	iter := iterable.Iterator()
+//	iter := iterable.Iterator()
 //	defer iter.Done()
 //	var x Value
 //	for iter.Next(&x) {
 //		...
 //	}
-//
 type Iterator interface {
 	// If the iterator is exhausted, Next returns false.
 	// Otherwise it sets *p to the current element of the sequence,
@@ -276,7 +300,7 @@ type HasSetKey interface {
 var _ HasSetKey = (*Dict)(nil)
 
 // A HasBinary value may be used as either operand of these binary operators:
-//     +   -   *   /   //   %   in   not in   |   &   ^   <<   >>
+// +   -   *   /   //   %   in   not in   |   &   ^   <<   >>
 //
 // The Side argument indicates whether the receiver is the left or right operand.
 //
@@ -296,7 +320,7 @@ const (
 )
 
 // A HasUnary value may be used as the operand of these unary operators:
-//     +   -   ~
+// +   -   ~
 //
 // An implementation may decline to handle an operation by returning (nil, nil).
 // For this reason, clients should always call the standalone Unary(op, x)
@@ -441,9 +465,11 @@ func isFinite(f float64) bool {
 	return math.Abs(f) <= math.MaxFloat64
 }
 
-func (x Float) CompareSameType(op syntax.Token, y_ Value, depth int) (bool, error) {
-	y := y_.(Float)
-	return threeway(op, floatCmp(x, y)), nil
+// Cmp implements comparison of two Float values.
+// Required by the TotallyOrdered interface.
+func (f Float) Cmp(v Value, depth int) (int, error) {
+	g := v.(Float)
+	return floatCmp(f, g), nil
 }
 
 // floatCmp performs a three-valued comparison on floats,
@@ -711,6 +737,34 @@ func (fn *Function) Param(i int) (string, syntax.Position) {
 	id := fn.funcode.Locals[i]
 	return id.Name, id.Pos
 }
+
+// ParamDefault returns the default value of the specified parameter
+// (0 <= i < NumParams()), or nil if the parameter is not optional.
+func (fn *Function) ParamDefault(i int) Value {
+	if i < 0 || i >= fn.NumParams() {
+		panic(i)
+	}
+
+	// fn.defaults omits all required params up to the first optional param. It
+	// also does not include *args or **kwargs at the end.
+	firstOptIdx := fn.NumParams() - len(fn.defaults)
+	if fn.HasVarargs() {
+		firstOptIdx--
+	}
+	if fn.HasKwargs() {
+		firstOptIdx--
+	}
+	if i < firstOptIdx || i >= firstOptIdx+len(fn.defaults) {
+		return nil
+	}
+
+	dflt := fn.defaults[i-firstOptIdx]
+	if _, ok := dflt.(mandatory); ok {
+		return nil
+	}
+	return dflt
+}
+
 func (fn *Function) HasVarargs() bool { return fn.funcode.HasVarargs }
 func (fn *Function) HasKwargs() bool  { return fn.funcode.HasKwargs }
 
@@ -754,13 +808,12 @@ func NewBuiltin(name string, fn func(thread *Thread, fn *Builtin, args Tuple, kw
 // In the example below, the value of f is the string.index
 // built-in method bound to the receiver value "abc":
 //
-//     f = "abc".index; f("a"); f("b")
+//	f = "abc".index; f("a"); f("b")
 //
 // In the common case, the receiver is bound only during the call,
 // but this still results in the creation of a temporary method closure:
 //
-//     "abc".index("a")
-//
+//	"abc".index("a")
 func (b *Builtin) BindReceiver(recv Value) *Builtin {
 	return &Builtin{name: b.name, fn: b.fn, recv: recv}
 }
@@ -1065,7 +1118,6 @@ func (s *Set) Len() int                               { return int(s.ht.len) }
 func (s *Set) Iterate() Iterator                      { return s.ht.iterate() }
 func (s *Set) String() string                         { return toString(s) }
 func (s *Set) Type() string                           { return "set" }
-func (s *Set) elems() []Value                         { return s.ht.keys() }
 func (s *Set) Freeze()                                { s.ht.freeze() }
 func (s *Set) Hash() (uint32, error)                  { return 0, fmt.Errorf("unhashable type: set") }
 func (s *Set) Truth() Bool                            { return s.Len() > 0 }
@@ -1091,8 +1143,8 @@ func setsEqual(x, y *Set, depth int) (bool, error) {
 	if x.Len() != y.Len() {
 		return false, nil
 	}
-	for _, elem := range x.elems() {
-		if found, _ := y.Has(elem); !found {
+	for e := x.ht.head; e != nil; e = e.next {
+		if found, _ := y.Has(e.key); !found {
 			return false, nil
 		}
 	}
@@ -1101,8 +1153,8 @@ func setsEqual(x, y *Set, depth int) (bool, error) {
 
 func (s *Set) Union(iter Iterator) (Value, error) {
 	set := new(Set)
-	for _, elem := range s.elems() {
-		set.Insert(elem) // can't fail
+	for e := s.ht.head; e != nil; e = e.next {
+		set.Insert(e.key) // can't fail
 	}
 	var x Value
 	for iter.Next(&x) {
@@ -1206,11 +1258,11 @@ func writeValue(out *strings.Builder, x Value, path []Value) {
 
 	case *Set:
 		out.WriteString("set([")
-		for i, elem := range x.elems() {
-			if i > 0 {
+		for e := x.ht.head; e != nil; e = e.next {
+			if e != x.ht.head {
 				out.WriteString(", ")
 			}
-			writeValue(out, elem, path)
+			writeValue(out, e.key, path)
 		}
 		out.WriteString("])")
 
@@ -1275,6 +1327,14 @@ func CompareDepth(op syntax.Token, x, y Value, depth int) (bool, error) {
 			return xcomp.CompareSameType(op, y, depth)
 		}
 
+		if xcomp, ok := x.(TotallyOrdered); ok {
+			t, err := xcomp.Cmp(y, depth)
+			if err != nil {
+				return false, err
+			}
+			return threeway(op, t), nil
+		}
+
 		// use identity comparison
 		switch op {
 		case syntax.EQL:
@@ -1393,7 +1453,7 @@ func Iterate(x Value) Iterator {
 // Bytes is the type of a Starlark binary string.
 //
 // A Bytes encapsulates an immutable sequence of bytes.
-// It is comparable, indexable, and sliceable, but not direcly iterable;
+// It is comparable, indexable, and sliceable, but not directly iterable;
 // use bytes.elems() for an iterable view.
 //
 // In this Go implementation, the elements of 'string' and 'bytes' are
diff --git a/starlarkjson/json.go b/starlarkjson/json.go
index 6579c7e..4bc208e 100644
--- a/starlarkjson/json.go
+++ b/starlarkjson/json.go
@@ -6,7 +6,7 @@
 // backwards compatibility
 //
 // Deprecated: use go.starlark.net/lib/json instead
-package starlarkjson // import "go.starlark.net/stalarkjson"
+package starlarkjson // import "go.starlark.net/starlarkjson"
 
 import (
 	"go.starlark.net/lib/json"
diff --git a/starlarkstruct/struct.go b/starlarkstruct/struct.go
index 7285bfa..ea2b1f6 100644
--- a/starlarkstruct/struct.go
+++ b/starlarkstruct/struct.go
@@ -132,11 +132,12 @@ func (s *Struct) ToStringDict(d starlark.StringDict) {
 
 func (s *Struct) String() string {
 	buf := new(strings.Builder)
-	if s.constructor == Default {
+	switch constructor := s.constructor.(type) {
+	case starlark.String:
 		// NB: The Java implementation always prints struct
 		// even for Bazel provider instances.
-		buf.WriteString("struct") // avoid String()'s quotation
-	} else {
+		buf.WriteString(constructor.GoString()) // avoid String()'s quotation
+	default:
 		buf.WriteString(s.constructor.String())
 	}
 	buf.WriteByte('(')
diff --git a/syntax/options.go b/syntax/options.go
new file mode 100644
index 0000000..51b2638
--- /dev/null
+++ b/syntax/options.go
@@ -0,0 +1,63 @@
+// Copyright 2023 The Bazel Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package syntax
+
+import _ "unsafe" // for linkname
+
+// FileOptions specifies various per-file options that affect static
+// aspects of an individual file such as parsing, name resolution, and
+// code generation. (Options that affect global dynamics are typically
+// controlled through [starlark.Thread].)
+//
+// The zero value of FileOptions is the default behavior.
+//
+// Many functions in this package come in two versions: the legacy
+// standalone function (such as [Parse]) uses [LegacyFileOptions],
+// whereas the more recent method (such as [Options.Parse]) honors the
+// provided options. The second form is preferred. In other packages,
+// the modern version is a standalone function with a leading
+// FileOptions parameter and the name suffix "Options", such as
+// [starlark.ExecFileOptions].
+type FileOptions struct {
+	// resolver
+	Set               bool // allow references to the 'set' built-in function
+	While             bool // allow 'while' statements
+	TopLevelControl   bool // allow if/for/while statements at top-level
+	GlobalReassign    bool // allow reassignment to top-level names
+	LoadBindsGlobally bool // load creates global not file-local bindings (deprecated)
+
+	// compiler
+	Recursion bool // disable recursion check for functions in this file
+}
+
+// TODO(adonovan): provide a canonical flag parser for FileOptions.
+// (And use it in the testdata "options:" strings.)
+
+// LegacyFileOptions returns a new FileOptions containing the current
+// values of the resolver package's legacy global variables such as
+// [resolve.AllowRecursion], etc.
+// These variables may be associated with command-line flags.
+func LegacyFileOptions() *FileOptions {
+	return &FileOptions{
+		Set:               resolverAllowSet,
+		While:             resolverAllowGlobalReassign,
+		TopLevelControl:   resolverAllowGlobalReassign,
+		GlobalReassign:    resolverAllowGlobalReassign,
+		Recursion:         resolverAllowRecursion,
+		LoadBindsGlobally: resolverLoadBindsGlobally,
+	}
+}
+
+// Access resolver (legacy) flags, if they are linked in; false otherwise.
+var (
+	//go:linkname resolverAllowSet go.starlark.net/resolve.AllowSet
+	resolverAllowSet bool
+	//go:linkname resolverAllowGlobalReassign go.starlark.net/resolve.AllowGlobalReassign
+	resolverAllowGlobalReassign bool
+	//go:linkname resolverAllowRecursion go.starlark.net/resolve.AllowRecursion
+	resolverAllowRecursion bool
+	//go:linkname resolverLoadBindsGlobally go.starlark.net/resolve.LoadBindsGlobally
+	resolverLoadBindsGlobally bool
+)
diff --git a/syntax/parse.go b/syntax/parse.go
index f4c8fff..260b91e 100644
--- a/syntax/parse.go
+++ b/syntax/parse.go
@@ -23,19 +23,25 @@ const (
 	RetainComments Mode = 1 << iota // retain comments in AST; see Node.Comments
 )
 
+// Parse calls the Parse method of LegacyFileOptions().
+// Deprecated: relies on legacy global variables.
+func Parse(filename string, src interface{}, mode Mode) (f *File, err error) {
+	return LegacyFileOptions().Parse(filename, src, mode)
+}
+
 // Parse parses the input data and returns the corresponding parse tree.
 //
-// If src != nil, ParseFile parses the source from src and the filename
+// If src != nil, Parse parses the source from src and the filename
 // is only used when recording position information.
 // The type of the argument for the src parameter must be string,
 // []byte, io.Reader, or FilePortion.
-// If src == nil, ParseFile parses the file specified by filename.
-func Parse(filename string, src interface{}, mode Mode) (f *File, err error) {
+// If src == nil, Parse parses the file specified by filename.
+func (opts *FileOptions) Parse(filename string, src interface{}, mode Mode) (f *File, err error) {
 	in, err := newScanner(filename, src, mode&RetainComments != 0)
 	if err != nil {
 		return nil, err
 	}
-	p := parser{in: in}
+	p := parser{options: opts, in: in}
 	defer p.in.recover(&err)
 
 	p.nextToken() // read first lookahead token
@@ -47,6 +53,12 @@ func Parse(filename string, src interface{}, mode Mode) (f *File, err error) {
 	return f, nil
 }
 
+// ParseCompoundStmt calls the ParseCompoundStmt method of LegacyFileOptions().
+// Deprecated: relies on legacy global variables.
+func ParseCompoundStmt(filename string, readline func() ([]byte, error)) (f *File, err error) {
+	return LegacyFileOptions().ParseCompoundStmt(filename, readline)
+}
+
 // ParseCompoundStmt parses a single compound statement:
 // a blank line, a def, for, while, or if statement, or a
 // semicolon-separated list of simple statements followed
@@ -54,13 +66,13 @@ func Parse(filename string, src interface{}, mode Mode) (f *File, err error) {
 // ParseCompoundStmt does not consume any following input.
 // The parser calls the readline function each
 // time it needs a new line of input.
-func ParseCompoundStmt(filename string, readline func() ([]byte, error)) (f *File, err error) {
+func (opts *FileOptions) ParseCompoundStmt(filename string, readline func() ([]byte, error)) (f *File, err error) {
 	in, err := newScanner(filename, readline, false)
 	if err != nil {
 		return nil, err
 	}
 
-	p := parser{in: in}
+	p := parser{options: opts, in: in}
 	defer p.in.recover(&err)
 
 	p.nextToken() // read first lookahead token
@@ -79,18 +91,24 @@ func ParseCompoundStmt(filename string, readline func() ([]byte, error)) (f *Fil
 		}
 	}
 
-	return &File{Path: filename, Stmts: stmts}, nil
+	return &File{Options: opts, Path: filename, Stmts: stmts}, nil
+}
+
+// ParseExpr calls the ParseExpr method of LegacyFileOptions().
+// Deprecated: relies on legacy global variables.
+func ParseExpr(filename string, src interface{}, mode Mode) (expr Expr, err error) {
+	return LegacyFileOptions().ParseExpr(filename, src, mode)
 }
 
 // ParseExpr parses a Starlark expression.
 // A comma-separated list of expressions is parsed as a tuple.
 // See Parse for explanation of parameters.
-func ParseExpr(filename string, src interface{}, mode Mode) (expr Expr, err error) {
+func (opts *FileOptions) ParseExpr(filename string, src interface{}, mode Mode) (expr Expr, err error) {
 	in, err := newScanner(filename, src, mode&RetainComments != 0)
 	if err != nil {
 		return nil, err
 	}
-	p := parser{in: in}
+	p := parser{options: opts, in: in}
 	defer p.in.recover(&err)
 
 	p.nextToken() // read first lookahead token
@@ -112,9 +130,10 @@ func ParseExpr(filename string, src interface{}, mode Mode) (expr Expr, err erro
 }
 
 type parser struct {
-	in     *scanner
-	tok    Token
-	tokval tokenValue
+	options *FileOptions
+	in      *scanner
+	tok     Token
+	tokval  tokenValue
 }
 
 // nextToken advances the scanner and returns the position of the
@@ -139,7 +158,7 @@ func (p *parser) parseFile() *File {
 		}
 		stmts = p.parseStmt(stmts)
 	}
-	return &File{Stmts: stmts}
+	return &File{Options: p.options, Stmts: stmts}
 }
 
 func (p *parser) parseStmt(stmts []Stmt) []Stmt {
@@ -275,10 +294,11 @@ func (p *parser) parseSimpleStmt(stmts []Stmt, consumeNL bool) []Stmt {
 }
 
 // small_stmt = RETURN expr?
-//            | PASS | BREAK | CONTINUE
-//            | LOAD ...
-//            | expr ('=' | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=') expr   // assign
-//            | expr
+//
+//	| PASS | BREAK | CONTINUE
+//	| LOAD ...
+//	| expr ('=' | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=') expr   // assign
+//	| expr
 func (p *parser) parseSmallStmt() Stmt {
 	switch p.tok {
 	case RETURN:
@@ -415,21 +435,23 @@ func (p *parser) consume(t Token) Position {
 }
 
 // params = (param COMMA)* param COMMA?
-//        |
+//
+//	|
 //
 // param = IDENT
-//       | IDENT EQ test
-//       | STAR
-//       | STAR IDENT
-//       | STARSTAR IDENT
+//
+//	| IDENT EQ test
+//	| STAR
+//	| STAR IDENT
+//	| STARSTAR IDENT
 //
 // parseParams parses a parameter list.  The resulting expressions are of the form:
 //
-//      *Ident                                          x
-//      *Binary{Op: EQ, X: *Ident, Y: Expr}             x=y
-//      *Unary{Op: STAR}                                *
-//      *Unary{Op: STAR, X: *Ident}                     *args
-//      *Unary{Op: STARSTAR, X: *Ident}                 **kwargs
+//	*Ident                                          x
+//	*Binary{Op: EQ, X: *Ident, Y: Expr}             x=y
+//	*Unary{Op: STAR}                                *
+//	*Unary{Op: STAR, X: *Ident}                     *args
+//	*Unary{Op: STARSTAR, X: *Ident}                 **kwargs
 func (p *parser) parseParams() []Expr {
 	var params []Expr
 	for p.tok != RPAREN && p.tok != COLON && p.tok != EOF {
@@ -651,9 +673,10 @@ func init() {
 }
 
 // primary_with_suffix = primary
-//                     | primary '.' IDENT
-//                     | primary slice_suffix
-//                     | primary call_suffix
+//
+//	| primary '.' IDENT
+//	| primary slice_suffix
+//	| primary call_suffix
 func (p *parser) parsePrimaryWithSuffix() Expr {
 	x := p.parsePrimary()
 	for {
@@ -770,12 +793,13 @@ func (p *parser) parseArgs() []Expr {
 	return args
 }
 
-//  primary = IDENT
-//          | INT | FLOAT | STRING | BYTES
-//          | '[' ...                    // list literal or comprehension
-//          | '{' ...                    // dict literal or comprehension
-//          | '(' ...                    // tuple or parenthesized expression
-//          | ('-'|'+'|'~') primary_with_suffix
+// primary = IDENT
+//
+//	| INT | FLOAT | STRING | BYTES
+//	| '[' ...                    // list literal or comprehension
+//	| '{' ...                    // dict literal or comprehension
+//	| '(' ...                    // tuple or parenthesized expression
+//	| ('-'|'+'|'~') primary_with_suffix
 func (p *parser) parsePrimary() Expr {
 	switch p.tok {
 	case IDENT:
@@ -836,9 +860,10 @@ func (p *parser) parsePrimary() Expr {
 }
 
 // list = '[' ']'
-//      | '[' expr ']'
-//      | '[' expr expr_list ']'
-//      | '[' expr (FOR loop_variables IN expr)+ ']'
+//
+//	| '[' expr ']'
+//	| '[' expr expr_list ']'
+//	| '[' expr (FOR loop_variables IN expr)+ ']'
 func (p *parser) parseList() Expr {
 	lbrack := p.nextToken()
 	if p.tok == RBRACK {
@@ -865,8 +890,9 @@ func (p *parser) parseList() Expr {
 }
 
 // dict = '{' '}'
-//      | '{' dict_entry_list '}'
-//      | '{' dict_entry FOR loop_variables IN expr '}'
+//
+//	| '{' dict_entry_list '}'
+//	| '{' dict_entry FOR loop_variables IN expr '}'
 func (p *parser) parseDict() Expr {
 	lbrace := p.nextToken()
 	if p.tok == RBRACE {
@@ -904,8 +930,9 @@ func (p *parser) parseDictEntry() *DictEntry {
 }
 
 // comp_suffix = FOR loopvars IN expr comp_suffix
-//             | IF expr comp_suffix
-//             | ']'  or  ')'                              (end)
+//
+//	| IF expr comp_suffix
+//	| ']'  or  ')'                              (end)
 //
 // There can be multiple FOR/IF clauses; the first is always a FOR.
 func (p *parser) parseComprehensionSuffix(lbrace Position, body Expr, endBrace Token) Expr {
diff --git a/syntax/scan.go b/syntax/scan.go
index bb4165e..b080202 100644
--- a/syntax/scan.go
+++ b/syntax/scan.go
@@ -1107,6 +1107,8 @@ var keywordToken = map[string]Token{
 	// reserved words:
 	"as": ILLEGAL,
 	// "assert":   ILLEGAL, // heavily used by our tests
+	"async":    ILLEGAL,
+	"await":    ILLEGAL,
 	"class":    ILLEGAL,
 	"del":      ILLEGAL,
 	"except":   ILLEGAL,
diff --git a/syntax/syntax.go b/syntax/syntax.go
index 3756637..0911f54 100644
--- a/syntax/syntax.go
+++ b/syntax/syntax.go
@@ -70,7 +70,8 @@ type File struct {
 	Path  string
 	Stmts []Stmt
 
-	Module interface{} // a *resolve.Module, set by resolver
+	Module  interface{} // a *resolve.Module, set by resolver
+	Options *FileOptions
 }
 
 func (x *File) Span() (start, end Position) {
@@ -99,9 +100,10 @@ func (*LoadStmt) stmt()   {}
 func (*ReturnStmt) stmt() {}
 
 // An AssignStmt represents an assignment:
+//
 //	x = 0
 //	x, y = y, x
-// 	x += 1
+//	x += 1
 type AssignStmt struct {
 	commentsRef
 	OpPos Position
diff --git a/syntax/walk.go b/syntax/walk.go
index 1491149..5e6c805 100644
--- a/syntax/walk.go
+++ b/syntax/walk.go
@@ -119,9 +119,7 @@ func Walk(n Node, f func(Node) bool) {
 
 	case *DictExpr:
 		for _, entry := range n.List {
-			entry := entry.(*DictEntry)
-			Walk(entry.Key, f)
-			Walk(entry.Value, f)
+			Walk(entry, f)
 		}
 
 	case *UnaryExpr:


Test patch:
diff --git a/internal/compile/codegen_test.go b/internal/compile/codegen_test.go
index f67204f..c5954c4 100644
--- a/internal/compile/codegen_test.go
+++ b/internal/compile/codegen_test.go
@@ -64,7 +64,7 @@ func TestPlusFolding(t *testing.T) {
 			t.Errorf("#%d: %v", i, err)
 			continue
 		}
-		got := disassemble(Expr(expr, "<expr>", locals).Toplevel)
+		got := disassemble(Expr(syntax.LegacyFileOptions(), expr, "<expr>", locals).Toplevel)
 		if test.want != got {
 			t.Errorf("expression <<%s>> generated <<%s>>, want <<%s>>",
 				test.src, got, test.want)
diff --git a/resolve/resolve_test.go b/resolve/resolve_test.go
index 23bee21..95a9445 100644
--- a/resolve/resolve_test.go
+++ b/resolve/resolve_test.go
@@ -14,11 +14,20 @@ import (
 	"go.starlark.net/syntax"
 )
 
-func setOptions(src string) {
-	resolve.AllowGlobalReassign = option(src, "globalreassign")
-	resolve.AllowRecursion = option(src, "recursion")
-	resolve.AllowSet = option(src, "set")
-	resolve.LoadBindsGlobally = option(src, "loadbindsglobally")
+// A test may enable non-standard options by containing (e.g.) "option:recursion".
+func getOptions(src string) *syntax.FileOptions {
+	// TODO(adonovan): use new fine-grained names.
+	// And share with eval_test.go
+	allowGlobalReassign := option(src, "globalreassign")
+	recursion := option(src, "recursion")
+	return &syntax.FileOptions{
+		Set:               option(src, "set"),
+		While:             allowGlobalReassign || recursion,
+		TopLevelControl:   allowGlobalReassign,
+		GlobalReassign:    allowGlobalReassign,
+		LoadBindsGlobally: option(src, "loadbindsglobally"),
+		Recursion:         recursion,
+	}
 }
 
 func option(chunk, name string) bool {
@@ -26,18 +35,17 @@ func option(chunk, name string) bool {
 }
 
 func TestResolve(t *testing.T) {
-	defer setOptions("")
 	filename := starlarktest.DataFile("resolve", "testdata/resolve.star")
 	for _, chunk := range chunkedfile.Read(filename, t) {
-		f, err := syntax.Parse(filename, chunk.Source, 0)
+		// A chunk may set options by containing e.g. "option:recursion".
+		opts := getOptions(chunk.Source)
+
+		f, err := opts.Parse(filename, chunk.Source, 0)
 		if err != nil {
 			t.Error(err)
 			continue
 		}
 
-		// A chunk may set options by containing e.g. "option:recursion".
-		setOptions(chunk.Source)
-
 		if err := resolve.File(f, isPredeclared, isUniversal); err != nil {
 			for _, err := range err.(resolve.ErrorList) {
 				chunk.GotError(int(err.Pos.Line), err.Msg)
diff --git a/resolve/testdata/resolve.star b/resolve/testdata/resolve.star
index ce67110..4fca831 100644
--- a/resolve/testdata/resolve.star
+++ b/resolve/testdata/resolve.star
@@ -340,7 +340,7 @@ U = 1 # ok (legacy)
 ---
 # https://github.com/bazelbuild/starlark/starlark/issues/21
 def f(**kwargs): pass
-f(a=1, a=1) ### `keyword argument a repeated`
+f(a=1, a=1) ### `keyword argument "a" is repeated`
 
 
 ---
diff --git a/starlark/bench_test.go b/starlark/bench_test.go
index e62cf59..46be890 100644
--- a/starlark/bench_test.go
+++ b/starlark/bench_test.go
@@ -12,12 +12,13 @@ import (
 	"strings"
 	"testing"
 
+	"go.starlark.net/lib/json"
 	"go.starlark.net/starlark"
 	"go.starlark.net/starlarktest"
 )
 
 func BenchmarkStarlark(b *testing.B) {
-	defer setOptions("")
+	starlark.Universe["json"] = json.Module
 
 	testdata := starlarktest.DataFile("starlark", ".")
 	thread := new(starlark.Thread)
@@ -33,10 +34,10 @@ func BenchmarkStarlark(b *testing.B) {
 			b.Error(err)
 			continue
 		}
-		setOptions(string(src))
+		opts := getOptions(string(src))
 
 		// Evaluate the file once.
-		globals, err := starlark.ExecFile(thread, filename, src, nil)
+		globals, err := starlark.ExecFileOptions(opts, thread, filename, src, nil)
 		if err != nil {
 			reportEvalError(b, err)
 		}
@@ -60,9 +61,9 @@ func BenchmarkStarlark(b *testing.B) {
 // It provides b.n, the number of iterations that must be executed by the function,
 // which is typically of the form:
 //
-//   def bench_foo(b):
-//      for _ in range(b.n):
-//         ...work...
+//	def bench_foo(b):
+//	   for _ in range(b.n):
+//	      ...work...
 //
 // It also provides stop, start, and restart methods to stop the clock in case
 // there is significant set-up work that should not count against the measured
diff --git a/starlark/eval_test.go b/starlark/eval_test.go
index 5974579..4b3c576 100644
--- a/starlark/eval_test.go
+++ b/starlark/eval_test.go
@@ -18,20 +18,31 @@ import (
 	"go.starlark.net/internal/chunkedfile"
 	"go.starlark.net/lib/json"
 	starlarkmath "go.starlark.net/lib/math"
+	"go.starlark.net/lib/proto"
 	"go.starlark.net/lib/time"
-	"go.starlark.net/resolve"
 	"go.starlark.net/starlark"
 	"go.starlark.net/starlarkstruct"
 	"go.starlark.net/starlarktest"
 	"go.starlark.net/syntax"
+	"google.golang.org/protobuf/reflect/protoregistry"
+
+	_ "google.golang.org/protobuf/types/descriptorpb" // example descriptor needed for lib/proto tests
 )
 
 // A test may enable non-standard options by containing (e.g.) "option:recursion".
-func setOptions(src string) {
-	resolve.AllowGlobalReassign = option(src, "globalreassign")
-	resolve.LoadBindsGlobally = option(src, "loadbindsglobally")
-	resolve.AllowRecursion = option(src, "recursion")
-	resolve.AllowSet = option(src, "set")
+func getOptions(src string) *syntax.FileOptions {
+	// TODO(adonovan): use new fine-grained names.
+	// And share with resolve_test.go
+	allowGlobalReassign := option(src, "globalreassign")
+	recursion := option(src, "recursion")
+	return &syntax.FileOptions{
+		Set:               option(src, "set"),
+		While:             allowGlobalReassign || recursion,
+		TopLevelControl:   allowGlobalReassign,
+		GlobalReassign:    allowGlobalReassign,
+		LoadBindsGlobally: option(src, "loadbindsglobally"),
+		Recursion:         recursion,
+	}
 }
 
 func option(chunk, name string) bool {
@@ -109,10 +120,10 @@ func TestEvalExpr(t *testing.T) {
 }
 
 func TestExecFile(t *testing.T) {
-	defer setOptions("")
 	testdata := starlarktest.DataFile("starlark", ".")
 	thread := &starlark.Thread{Load: load}
 	starlarktest.SetReporter(thread, t)
+	proto.SetPool(thread, protoregistry.GlobalFiles)
 	for _, file := range []string{
 		"testdata/assign.star",
 		"testdata/bool.star",
@@ -127,6 +138,7 @@ func TestExecFile(t *testing.T) {
 		"testdata/list.star",
 		"testdata/math.star",
 		"testdata/misc.star",
+		"testdata/proto.star",
 		"testdata/set.star",
 		"testdata/string.star",
 		"testdata/time.star",
@@ -142,9 +154,8 @@ func TestExecFile(t *testing.T) {
 				"struct":    starlark.NewBuiltin("struct", starlarkstruct.Make),
 			}
 
-			setOptions(chunk.Source)
-
-			_, err := starlark.ExecFile(thread, filename, chunk.Source, predeclared)
+			opts := getOptions(chunk.Source)
+			_, err := starlark.ExecFileOptions(opts, thread, filename, chunk.Source, predeclared)
 			switch err := err.(type) {
 			case *starlark.EvalError:
 				found := false
@@ -202,6 +213,9 @@ func load(thread *starlark.Thread, module string) (starlark.StringDict, error) {
 	if module == "math.star" {
 		return starlark.StringDict{"math": starlarkmath.Module}, nil
 	}
+	if module == "proto.star" {
+		return starlark.StringDict{"proto": proto.Module}, nil
+	}
 
 	// TODO(adonovan): test load() using this execution path.
 	filename := filepath.Join(filepath.Dir(thread.CallFrame(0).Pos.Filename()), module)
@@ -972,6 +986,13 @@ func TestExecutionSteps(t *testing.T) {
 	if fmt.Sprint(err) != "Starlark computation cancelled: too many steps" {
 		t.Errorf("execution returned error %q, want cancellation", err)
 	}
+
+	thread.Steps = 0
+	thread.Uncancel()
+	_, err = countSteps(1)
+	if err != nil {
+		t.Errorf("execution returned error %q, want nil", err)
+	}
 }
 
 // TestDeps fails if the interpreter proper (not the REPL, etc) sprouts new external dependencies.
@@ -995,3 +1016,52 @@ func TestDeps(t *testing.T) {
 		}
 	}
 }
+
+// TestPanicSafety ensures that a panic from an application-defined
+// built-in may traverse the interpreter safely; see issue #411.
+func TestPanicSafety(t *testing.T) {
+	predeclared := starlark.StringDict{
+		"panic": starlark.NewBuiltin("panic", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
+			panic(args[0])
+		}),
+		"list": starlark.NewList([]starlark.Value{starlark.MakeInt(0)}),
+	}
+
+	// This program is executed twice, using the same Thread,
+	// and panics both times, with values 1 and 2, while
+	// main is on the stack and a for-loop is active.
+	//
+	// It mutates list, a predeclared variable.
+	// This operation would fail if the previous
+	// for-loop failed to close its iterator during the panic.
+	//
+	// It also calls main a second time without recursion enabled.
+	// This operation would fail if the previous
+	// call failed to pop main from the stack during the panic.
+	const src = `
+list[0] += 1
+
+def main():
+    for x in list:
+        panic(x)
+
+main()
+`
+	thread := new(starlark.Thread)
+	for _, i := range []int{1, 2} {
+		// Use a func to limit the scope of recover.
+		func() {
+			defer func() {
+				if got := fmt.Sprint(recover()); got != fmt.Sprint(i) {
+					t.Fatalf("recover: got %v, want %v", got, i)
+				}
+			}()
+			v, err := starlark.ExecFile(thread, "panic.star", src, predeclared)
+			if err != nil {
+				t.Fatalf("ExecFile returned error %q, expected panic", err)
+			} else {
+				t.Fatalf("ExecFile returned %v, expected panic", v)
+			}
+		}()
+	}
+}
diff --git a/starlark/int_test.go b/starlark/int_test.go
index 8c20a31..2a75ae9 100644
--- a/starlark/int_test.go
+++ b/starlark/int_test.go
@@ -118,8 +118,8 @@ func TestIntFallback(t *testing.T) {
 		t.Fatalf("can't find file name of executable: %v", err)
 	}
 	// ulimit -v limits the address space in KB. Not portable.
-	// 1GB is enough for the Go runtime but not for the optimization.
-	cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("ulimit -v 1000000 && %q --entry=intfallback", exe))
+	// 4GB is enough for the Go runtime but not for the optimization.
+	cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("ulimit -v 4000000 && %q --entry=intfallback", exe))
 	out, err := cmd.CombinedOutput()
 	if err != nil {
 		t.Fatalf("intfallback subcommand failed: %v\n%s", err, out)
@@ -140,16 +140,18 @@ func intfallback() {
 }
 
 // The --entry flag invokes an alternate entry point, for use in subprocess tests.
+var testEntry = flag.String("entry", "", "child process entry-point")
+
 func TestMain(m *testing.M) {
-	var entry string
-	flag.StringVar(&entry, "entry", "", "child process entry-point")
+	// In some build systems, notably Blaze, flag.Parse is called before TestMain,
+	// in violation of the TestMain contract, making this second call a no-op.
 	flag.Parse()
-	switch entry {
+	switch *testEntry {
 	case "":
 		os.Exit(m.Run()) // normal case
 	case "intfallback":
 		intfallback()
 	default:
-		log.Fatalf("unknown entry point: %s", entry)
+		log.Fatalf("unknown entry point: %s", *testEntry)
 	}
 }
diff --git a/starlark/profile_test.go b/starlark/profile_test.go
index 2781833..515d7d4 100644
--- a/starlark/profile_test.go
+++ b/starlark/profile_test.go
@@ -31,10 +31,10 @@ func TestProfile(t *testing.T) {
 
 	const src = `
 def fibonacci(n):
-	res = list(range(n))
-	for i in res[2:]:
-		res[i] = res[i-2] + res[i-1]
-	return res
+	x, y = 1, 1
+	for i in range(n):
+		x, y = y, x+y
+	return y
 
 fibonacci(100000)
 `
diff --git a/starlark/testdata/benchmark.star b/starlark/testdata/benchmark.star
index 79614f0..5d9af10 100644
--- a/starlark/testdata/benchmark.star
+++ b/starlark/testdata/benchmark.star
@@ -1,4 +1,5 @@
 # Benchmarks of Starlark execution
+# option:set
 
 def bench_range_construction(b):
     for _ in range(b.n):
@@ -68,3 +69,60 @@ def bench_dict_equal(b):
     for _ in range(b.n):
         if largedict != largedict:
             fail("invalid comparison")
+
+largeset = set([v for v in range(1000)])
+
+def bench_set_equal(b):
+    "Benchmark of set union operation."
+    for _ in range(b.n):
+        if largeset != largeset:
+            fail("invalid comparison")
+
+flat = { "int": 1, "float": 0.2, "string": "string", "list": [], "bool": True, "nil": None, "tuple": (1, 2, 3) }
+deep = {
+    "type": "int",
+    "value": 1, 
+    "next": {
+        "type": "float",
+        "value": 0.2,
+        "next": {
+            "type": "string", 
+            "value": "string",
+            "next": {
+                "type": "list", 
+                "value": [ 1, "", True, None, (1, 2) ],
+                "next": {
+                    "type": "bool",
+                    "value": True,
+                    "next": {
+                        "type": "tuple",
+                        "value": (1, 2.0, "3"),
+                        "next": None
+                    }
+                }
+            }
+        }
+    }
+}
+
+deep_list = [ deep for _ in range(100) ]
+
+def bench_to_json_flat_mixed(b):
+    "Benchmark json.encode builtin with flat mixed input"
+    for _ in range(b.n):
+        json.encode(flat)
+
+def bench_to_json_flat_big(b):
+    "Benchmark json.encode builtin with big flat integer input"
+    for _ in range(b.n):
+        json.encode(largedict)
+
+def bench_to_json_deep(b):
+    "Benchmark json.encode builtin with deep input"
+    for _ in range(b.n):
+        json.encode(deep)
+
+def bench_to_json_deep_list(b):
+    "Benchmark json.encode builtin with a list of deep input"
+    for _ in range(b.n):
+        json.encode(deep)
diff --git a/starlark/testdata/builtins.star b/starlark/testdata/builtins.star
index b55428e..c350c96 100644
--- a/starlark/testdata/builtins.star
+++ b/starlark/testdata/builtins.star
@@ -196,7 +196,7 @@ assert.eq(getattr(hf, "x"), 2)
 assert.eq(hf.x, 2)
 # built-in types can have attributes (methods) too.
 myset = set([])
-assert.eq(dir(myset), ["union"])
+assert.eq(dir(myset), ["add", "clear", "discard", "pop", "remove", "union"])
 assert.true(hasattr(myset, "union"))
 assert.true(not hasattr(myset, "onion"))
 assert.eq(str(getattr(myset, "union")), "<built-in method union of set value>")
diff --git a/starlark/testdata/function_param.star b/starlark/testdata/function_param.star
new file mode 100644
index 0000000..6c94961
--- /dev/null
+++ b/starlark/testdata/function_param.star
@@ -0,0 +1,10 @@
+# Functions used in starlark_test.TestParamDefault().
+
+def all_required(a, b, c): pass
+def all_opt(a="a", b=None, c=""): pass
+def mix_required_opt(a, b, c="c", d="d"): pass
+def with_varargs(a, b="b", *args): pass
+def with_varargs_kwonly(a, b="b", *args, c="c", d): pass
+def with_kwonly(a, b="b", *, c="c", d): pass
+def with_kwargs(a, b="b", c="c", **kwargs): pass
+def with_varargs_kwonly_kwargs(a, b="b", *args, c="c", d, e="e", **kwargs): pass
diff --git a/starlark/testdata/json.star b/starlark/testdata/json.star
index 7c7b316..62a457b 100644
--- a/starlark/testdata/json.star
+++ b/starlark/testdata/json.star
@@ -37,6 +37,18 @@ encode_error(struct(x=[1, {"x": len}]), # nested failure
              'in field .x: at list index 1: in dict key "x": cannot encode...')
 encode_error({1: 2}, 'dict has int key, want string')
 
+recursive_map = {}
+recursive_map["r"] = recursive_map
+encode_error(recursive_map, 'json.encode: in dict key "r": cycle in JSON structure')
+
+recursive_list = []
+recursive_list.append(recursive_list)
+encode_error(recursive_list, 'json.encode: at list index 0: cycle in JSON structure')
+
+recursive_tuple = (1, 2, [])
+recursive_tuple[2].append(recursive_tuple)
+encode_error(recursive_tuple, 'json.encode: at tuple index 2: at list index 0: cycle in JSON structure')
+
 ## json.decode
 
 assert.eq(json.decode("null"), None)
@@ -99,6 +111,20 @@ decode_error('{"one": 1,', "unexpected end of file")
 decode_error('{"one": 1, }', "unexpected character '}'")
 decode_error('{"one": 1]', "in object, got ']', want ',' or '}'")
 
+## json.decode with default specified
+
+assert.eq(json.decode('{"valid": "json"}', default = "default value"), {"valid": "json"})
+assert.eq(json.decode('{"valid": "json"}', "default value"), {"valid": "json"})
+assert.eq(json.decode('{"invalid": "json"', default = "default value"), "default value")
+assert.eq(json.decode('{"invalid": "json"', "default value"), "default value")
+assert.eq(json.decode('{"invalid": "json"', default = None), None)
+assert.eq(json.decode('{"invalid": "json"', None), None)
+
+assert.fails(
+    lambda: json.decode(x = '{"invalid": "json"', default = "default value"),
+    "unexpected keyword argument x"
+)
+
 def codec(x):
     return json.decode(json.encode(x))
 
diff --git a/starlark/testdata/proto.star b/starlark/testdata/proto.star
new file mode 100644
index 0000000..f140082
--- /dev/null
+++ b/starlark/testdata/proto.star
@@ -0,0 +1,14 @@
+# Tests of the experimental 'lib/proto' module.
+
+load("assert.star", "assert")
+load("proto.star", "proto")
+
+schema = proto.file("google/protobuf/descriptor.proto")
+
+m = schema.FileDescriptorProto(name = "somename.proto", dependency = ["a", "b", "c"])
+assert.eq(type(m), "proto.Message")
+assert.eq(m.name, "somename.proto")
+assert.eq(list(m.dependency), ["a", "b", "c"])
+m.dependency = ["d", "e"]
+assert.eq(list(m.dependency), ["d", "e"])
+
diff --git a/starlark/testdata/set.star b/starlark/testdata/set.star
index bca4144..3dcde3c 100644
--- a/starlark/testdata/set.star
+++ b/starlark/testdata/set.star
@@ -15,7 +15,7 @@
 # - set += iterable, perhaps?
 # Test iterator invalidation.
 
-load("assert.star", "assert")
+load("assert.star", "assert", "freeze")
 
 # literals
 # Parser does not currently support {1, 2, 3}.
@@ -116,3 +116,52 @@ assert.eq(iter(), [1, 2, 3])
 
 # sets are not indexable
 assert.fails(lambda : x[0], "unhandled.*operation")
+
+# adding and removing
+add_set = set([1,2,3])
+add_set.add(4)
+assert.true(4 in add_set)
+freeze(add_set) # no mutation of frozen set because key already present
+add_set.add(4)
+assert.fails(lambda: add_set.add(5), "add: cannot insert into frozen hash table")
+
+# remove
+remove_set = set([1,2,3])
+remove_set.remove(3)
+assert.true(3 not in remove_set)
+assert.fails(lambda: remove_set.remove(3), "remove: missing key")
+freeze(remove_set)
+assert.fails(lambda: remove_set.remove(3), "remove: cannot delete from frozen hash table")
+
+# discard
+discard_set = set([1,2,3])
+discard_set.discard(3)
+assert.true(3 not in discard_set)
+assert.eq(discard_set.discard(3), None)
+freeze(discard_set)
+assert.eq(discard_set.discard(3), None)  # no mutation of frozen set because key doesn't exist
+assert.fails(lambda: discard_set.discard(1), "discard: cannot delete from frozen hash table")
+
+
+# pop
+pop_set = set([1,2,3])
+assert.eq(pop_set.pop(), 1)
+assert.eq(pop_set.pop(), 2)
+assert.eq(pop_set.pop(), 3)
+assert.fails(lambda: pop_set.pop(), "pop: empty set")
+pop_set.add(1)
+pop_set.add(2)
+freeze(pop_set)
+assert.fails(lambda: pop_set.pop(), "pop: cannot delete from frozen hash table")
+
+
+# clear
+clear_set = set([1,2,3])
+clear_set.clear()
+assert.eq(len(clear_set), 0)
+freeze(clear_set) # no mutation of frozen set because its already empty
+assert.eq(clear_set.clear(), None) 
+
+other_clear_set = set([1,2,3])
+freeze(other_clear_set)
+assert.fails(lambda: other_clear_set.clear(), "clear: cannot clear frozen hash table")
diff --git a/starlark/testdata/time.star b/starlark/testdata/time.star
index f6ec026..0c46096 100644
--- a/starlark/testdata/time.star
+++ b/starlark/testdata/time.star
@@ -116,6 +116,8 @@ assert.eq(10, t1.second)
 assert.eq(99, t1.nanosecond)
 assert.eq(1244822770, t1.unix)
 assert.eq(1244822770000000099, t1.unix_nano)
+assert.true(not time.parse_time("0001-01-01T00:00:00Z"))
+assert.true(time.parse_time("2022-01-01T00:00:00Z"))
 
 # time type
 assert.eq("time.time", type(refTime))
diff --git a/starlark/value_test.go b/starlark/value_test.go
index 6420a95..25d6003 100644
--- a/starlark/value_test.go
+++ b/starlark/value_test.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"go.starlark.net/starlark"
 )
 
@@ -44,3 +45,113 @@ func TestListAppend(t *testing.T) {
 		t.Errorf("failed list.Append() got: %+v, want: hello", res)
 	}
 }
+
+func TestParamDefault(t *testing.T) {
+	tests := []struct {
+		desc         string
+		starFn       string
+		wantDefaults []starlark.Value
+	}{
+		{
+			desc:         "function with all required params",
+			starFn:       "all_required",
+			wantDefaults: []starlark.Value{nil, nil, nil},
+		},
+		{
+			desc:   "function with all optional params",
+			starFn: "all_opt",
+			wantDefaults: []starlark.Value{
+				starlark.String("a"),
+				starlark.None,
+				starlark.String(""),
+			},
+		},
+		{
+			desc:   "function with required and optional params",
+			starFn: "mix_required_opt",
+			wantDefaults: []starlark.Value{
+				nil,
+				nil,
+				starlark.String("c"),
+				starlark.String("d"),
+			},
+		},
+		{
+			desc:   "function with required, optional, and varargs params",
+			starFn: "with_varargs",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				nil,
+			},
+		},
+		{
+			desc:   "function with required, optional, varargs, and keyword-only params",
+			starFn: "with_varargs_kwonly",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				starlark.String("c"),
+				nil,
+				nil,
+			},
+		},
+		{
+			desc:   "function with required, optional, and keyword-only params",
+			starFn: "with_kwonly",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				starlark.String("c"),
+				nil,
+			},
+		},
+		{
+			desc:   "function with required, optional, and kwargs params",
+			starFn: "with_kwargs",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				starlark.String("c"),
+				nil,
+			},
+		},
+		{
+			desc:   "function with required, optional, varargs, kw-only, and kwargs params",
+			starFn: "with_varargs_kwonly_kwargs",
+			wantDefaults: []starlark.Value{
+				nil,
+				starlark.String("b"),
+				starlark.String("c"),
+				nil,
+				starlark.String("e"),
+				nil,
+				nil,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			thread := &starlark.Thread{}
+			filename := "testdata/function_param.star"
+			globals, err := starlark.ExecFile(thread, filename, nil, nil)
+			if err != nil {
+				t.Fatalf("ExecFile(%v, %q) failed: %v", thread, filename, err)
+			}
+
+			fn, ok := globals[tt.starFn].(*starlark.Function)
+			if !ok {
+				t.Fatalf("value %v is not a Starlark function", globals[tt.starFn])
+			}
+
+			var paramDefaults []starlark.Value
+			for i := 0; i < fn.NumParams(); i++ {
+				paramDefaults = append(paramDefaults, fn.ParamDefault(i))
+			}
+			if diff := cmp.Diff(tt.wantDefaults, paramDefaults); diff != "" {
+				t.Errorf("param defaults got diff (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/starlarktest/assert.go b/starlarktest/assert.star
similarity index 82%
rename from starlarktest/assert.go
rename to starlarktest/assert.star
index f18719c..9a699df 100644
--- a/starlarktest/assert.go
+++ b/starlarktest/assert.star
@@ -1,9 +1,3 @@
-package starlarktest
-
-// assertStar contains the logical data file assert.star.
-// TODO(adonovan): move it back into an actual file,
-// when fs.Embed is more than two releases old.
-const assertStar = `
 # Predeclared built-ins for this module:
 #
 # error(msg): report an error in Go's test framework without halting execution.
@@ -12,12 +6,20 @@ const assertStar = `
 # matches(str, pattern): report whether str matches regular expression pattern.
 # module(**kwargs): a constructor for a module.
 # _freeze(x): freeze the value x and everything reachable from it.
+# _floateq(x, y): reports floating point equality (within 1 ULP).
 #
 # Clients may use these functions to define their own testing abstractions.
 
+_num = ("float", "int")
+
 def _eq(x, y):
     if x != y:
-        error("%r != %r" % (x, y))
+	if (type(x) == "float" and type(y) in _num or
+	    type(y) == "float" and type(x) in _num):
+	    if not _floateq(float(x), float(y)):
+		error("floats: %r != %r (delta > 1 ulp)" % (x, y))
+	else:
+            error("%r != %r" % (x, y))
 
 def _ne(x, y):
     if x == y:
@@ -55,4 +57,3 @@ assert = module(
     contains = _contains,
     fails = _fails,
 )
-`
diff --git a/starlarktest/starlarktest.go b/starlarktest/starlarktest.go
index afd1e90..88e8548 100644
--- a/starlarktest/starlarktest.go
+++ b/starlarktest/starlarktest.go
@@ -13,7 +13,9 @@
 package starlarktest // import "go.starlark.net/starlarktest"
 
 import (
+	_ "embed"
 	"fmt"
+	"math"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -50,9 +52,11 @@ func GetReporter(thread *starlark.Thread) Reporter {
 }
 
 var (
-	once      sync.Once
-	assert    starlark.StringDict
-	assertErr error
+	once   sync.Once
+	assert starlark.StringDict
+	//go:embed assert.star
+	assertFileSrc string
+	assertErr     error
 )
 
 // LoadAssertModule loads the assert module.
@@ -60,16 +64,15 @@ var (
 func LoadAssertModule() (starlark.StringDict, error) {
 	once.Do(func() {
 		predeclared := starlark.StringDict{
-			"error":   starlark.NewBuiltin("error", error_),
-			"catch":   starlark.NewBuiltin("catch", catch),
-			"matches": starlark.NewBuiltin("matches", matches),
-			"module":  starlark.NewBuiltin("module", starlarkstruct.MakeModule),
-			"_freeze": starlark.NewBuiltin("freeze", freeze),
+			"error":    starlark.NewBuiltin("error", error_),
+			"catch":    starlark.NewBuiltin("catch", catch),
+			"matches":  starlark.NewBuiltin("matches", matches),
+			"module":   starlark.NewBuiltin("module", starlarkstruct.MakeModule),
+			"_freeze":  starlark.NewBuiltin("freeze", freeze),
+			"_floateq": starlark.NewBuiltin("floateq", floateq),
 		}
-		// TODO(adonovan): embed the file using embed.FS when we can rely on go1.16,
-		// and make the apparent filename reference that file.
 		thread := new(starlark.Thread)
-		assert, assertErr = starlark.ExecFile(thread, "builtins/assert.star", assertStar, predeclared)
+		assert, assertErr = starlark.ExecFile(thread, "assert.star", assertFileSrc, predeclared)
 	})
 	return assert, assertErr
 }
@@ -130,6 +133,29 @@ func freeze(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, k
 	return args[0], nil
 }
 
+// floateq(x, y) reports whether two floats are within 1 ULP of each other.
+func floateq(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
+	var xf, yf starlark.Float
+	if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 2, &xf, &yf); err != nil {
+		return nil, err
+	}
+
+	res := false
+	switch {
+	case xf == yf:
+		res = true
+	case math.IsNaN(float64(xf)):
+		res = math.IsNaN(float64(yf))
+	case math.IsNaN(float64(yf)):
+		// false (non-NaN = Nan)
+	default:
+		x := math.Float64bits(float64(xf))
+		y := math.Float64bits(float64(yf))
+		res = x == y+1 || y == x+1
+	}
+	return starlark.Bool(res), nil
+}
+
 // DataFile returns the effective filename of the specified
 // test data resource.  The function abstracts differences between
 // 'go build', under which a test runs in its package directory,
diff --git a/syntax/testdata/scan.star b/syntax/testdata/scan.star
index eec85c9..14fa6da 100644
--- a/syntax/testdata/scan.star
+++ b/syntax/testdata/scan.star
@@ -113,7 +113,7 @@ def _emit_generate_params_action(cmds, ctx, fn):
     ]
     cmds_all += cmds
     cmds_all_str = "\n".join(cmds_all) + "\n"
-    f = ctx.new_file(ctx.configuration.bin_dir, fn)
+    f = ctx.actions.declare_file(fn)
     ctx.file_action(
         output = f,
         content = cmds_all_str,
@@ -254,7 +254,10 @@ def _emit_go_cover_action(ctx, sources):
             continue
 
         cover_var = "GoCover_%d" % count
-        out = ctx.new_file(src, src.basename[:-3] + "_" + cover_var + ".cover.go")
+        out = ctx.actions.declare_file(
+            src.basename[:-3] + "_" + cover_var + ".cover.go",
+            sibling = src,
+        )
         outputs += [out]
         ctx.action(
             inputs = [src] + ctx.files.toolchain,
@@ -306,13 +309,16 @@ def go_library_impl(ctx):
 
     extra_objects = [cgo_object.cgo_obj] if cgo_object else []
     for src in asm_srcs:
-        obj = ctx.new_file(src, "%s.dir/%s.o" % (ctx.label.name, src.basename[:-2]))
+        obj = ctx.actions.declare_file(
+            "%s.dir/%s.o" % (ctx.label.name, src.basename[:-2]),
+            sibling = src,
+        )
         _emit_go_asm_action(ctx, src, asm_hdrs, obj)
         extra_objects += [obj]
 
     lib_name = _go_importpath(ctx) + ".a"
-    out_lib = ctx.new_file(lib_name)
-    out_object = ctx.new_file(ctx.label.name + ".o")
+    out_lib = ctx.actions.declare_file(lib_name)
+    out_object = ctx.actions.declare_file(ctx.label.name + ".o")
     search_path = out_lib.path[:-len(lib_name)]
     gc_goopts = _gc_goopts(ctx)
     transitive_go_libraries = depset([out_lib])
@@ -531,9 +537,9 @@ def go_test_impl(ctx):
     test into a binary."""
 
     lib_result = go_library_impl(ctx)
-    main_go = ctx.new_file(ctx.label.name + "_main_test.go")
-    main_object = ctx.new_file(ctx.label.name + "_main_test.o")
-    main_lib = ctx.new_file(ctx.label.name + "_main_test.a")
+    main_go = ctx.actions.declare_file(ctx.label.name + "_main_test.go")
+    main_object = ctx.actions.declare_file(ctx.label.name + "_main_test.o")
+    main_lib = ctx.actions.declare_file(ctx.label.name + "_main_test.a")
     go_import = _go_importpath(ctx)
 
     cmds = [
@@ -754,7 +760,7 @@ def _cgo_filter_srcs_impl(ctx):
     for src in srcs:
         stem, _, ext = src.path.rpartition(".")
         dst_basename = "%s.filtered.%s" % (stem, ext)
-        dst = ctx.new_file(src, dst_basename)
+        dst = ctx.actions.declare_file(dst_basename, sibling = src)
         cmds += [
             "if '%s' -cgo -quiet '%s'; then" %
             (ctx.executable._filter_tags.path, src.path),
@@ -824,7 +830,7 @@ def _cgo_codegen_impl(ctx):
     p = _pkg_dir(ctx.label.workspace_root, ctx.label.package) + "/"
     if p == "./":
         p = ""  # workaround when cgo_library in repository root
-    out_dir = (ctx.configuration.genfiles_dir.path + "/" +
+    out_dir = (ctx.configuration.bin_dir.path + "/" +
                p + ctx.attr.outdir)
     cc = ctx.fragments.cpp.compiler_executable
     cmds = [
@@ -905,7 +911,6 @@ _cgo_codegen_rule = rule(
         ),
     },
     fragments = ["cpp"],
-    output_to_genfiles = True,
 )
 
 def _cgo_codegen(
diff --git a/syntax/walk_test.go b/syntax/walk_test.go
index 00d9784..ec62473 100644
--- a/syntax/walk_test.go
+++ b/syntax/walk_test.go
@@ -96,7 +96,7 @@ for o in [p for q, r in s if t]:
 	})
 	fmt.Println(strings.Join(idents, " "))
 
-	// The identifer 'a' appears in both LoadStmt.From[0] and LoadStmt.To[0].
+	// The identifier 'a' appears in both LoadStmt.From[0] and LoadStmt.To[0].
 
 	// Output:
 	// a a b c d e f g h i j k l m n o p q r s t u v w x y z