Skip to content

Commit

Permalink
Implement to-be-closed variables
Browse files Browse the repository at this point in the history
  • Loading branch information
zombiezen committed Dec 15, 2024
1 parent e024d8b commit dce8c9b
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 85 deletions.
26 changes: 26 additions & 0 deletions internal/mylua/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2024 The zb Authors
// SPDX-License-Identifier: MIT

package mylua

func (l *State) localVariableName(frame *callFrame, i int) string {
if start, end := frame.extraArgumentsRange(); start <= i && i < end {
return "(vararg)"
}
registerStart := frame.registerStart()
if i < registerStart {
return ""
}
f, isLua := l.stack[frame.functionIndex].(luaFunction)
if !isLua {
return "(Go temporary)"
}
if i >= int(f.proto.MaxStackSize) {
return ""
}
name := f.proto.LocalName(uint8(i), frame.pc)
if name == "" {
name = "(temporary)"
}
return name
}
37 changes: 37 additions & 0 deletions internal/mylua/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2024 The zb Authors
// SPDX-License-Identifier: MIT

package mylua

import "errors"

// errorToValue converts a Go error to a Lua [value].
// If there is an [errorObject] in the error chain,
// then errorToValue returns its value.
// errorToValue(nil) returns nil.
func errorToValue(err error) value {
if err == nil {
return nil
}
if obj := (errorObject{}); errors.As(err, &obj) {
return obj.value
}
// TODO(maybe): Use a userdata instead (so errors can be round-tripped)?
return stringValue{s: err.Error()}
}

// errorObject wraps a [value] as an [error].
type errorObject struct {
value value
}

func (obj errorObject) Error() string {
if obj.value == nil {
return "<lua nil>"
}
s, ok := toString(obj.value)
if !ok {
return "<" + obj.value.valueType().String() + ">"
}
return s.s
}
180 changes: 180 additions & 0 deletions internal/mylua/functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright 2024 The zb Authors
// SPDX-License-Identifier: MIT

package mylua

import (
"fmt"
"slices"

"zb.256lights.llc/pkg/internal/luacode"
)

type function interface {
value
functionID() uint64
upvaluesSlice() []*upvalue
}

var (
_ function = goFunction{}
_ function = luaFunction{}
)

type goFunction struct {
id uint64
cb Function
upvalues []*upvalue
}

func (f goFunction) valueType() Type { return TypeFunction }
func (f goFunction) functionID() uint64 { return f.id }
func (f goFunction) upvaluesSlice() []*upvalue { return f.upvalues }

type luaFunction struct {
id uint64
proto *luacode.Prototype
upvalues []*upvalue
}

func (f luaFunction) valueType() Type { return TypeFunction }
func (f luaFunction) functionID() uint64 { return f.id }
func (f luaFunction) upvaluesSlice() []*upvalue { return f.upvalues }

// markTBC marks the given index in l.stack as “to be closed”.
// When the stack element is popped (or explicitly closed),
// then its “__close” metamethod will be invoked.
// If the value at l.stack[i] is false or nil,
// then markTBC does not mark the index and returns nil.
// Otherwise, markTBC returns an error if the value at l.stack[i]
// does not have a “__close” metamethod.
func (l *State) markTBC(i int) error {
v := l.stack[i]
if !toBoolean(v) {
return nil
}
if l.metamethod(v, luacode.TagMethodClose) == nil {
variableName := l.localVariableName(l.frame(), i)
if variableName == "" {
variableName = "?"
}
return fmt.Errorf("variable '%s' got a non-closable value", variableName)
}
l.tbc.Add(uint(i))
return nil
}

// closeTBCSlots runs the “__close” metamethods of any to-be-closed variables
// previously marked by [*State.markTBC]
// from the top of the stack to the given bottom index in last-in first-out order.
// If preserveTop is false, then closeTBCSlots moves the stack's top
// before calling each “__close” metamethod to save on stack space
// and finally moves the stack's top to bottom before returning.
// closeTBCSlots returns the last error raised during execution of the metamethods,
// or the original error object if no errors were raised.
func (l *State) closeTBCSlots(bottom int, preserveTop bool, err error) error {
for tbc := range l.tbc.Reversed() {
if tbc < uint(bottom) {
break
}
l.tbc.Delete(tbc)

v := l.stack[tbc]
if !preserveTop {
newTop := tbc + 1
clear(l.stack[newTop:])
l.stack = l.stack[:newTop]
}
newError := l.call(0, l.metamethod(v, luacode.TagMethodClose), v, errorToValue(err))
if newError != nil {
err = newError
}
}
if !preserveTop {
l.setTop(bottom)
}
return err
}

// An upvalue is a variable defined in the lexical scope outside a function.
// An upvalue is "open" if it refers to the stack
// or "closed" if it has escaped the stack.
type upvalue struct {
stackIndex int
storage value
}

// closedUpvalue returns an [upvalue] with the given value
// that is stored off the stack.
func closedUpvalue(v value) *upvalue {
return &upvalue{
storage: v,
stackIndex: -1,
}
}

// isOpen reports whether the upvalue is stored on the stack.
func (uv *upvalue) isOpen() bool {
return uv.stackIndex >= 0
}

// stackUpvalue returns an [*upvalue] for the given stack index.
// Until the upvalue is closed,
// multiple calls to stackUpvalue for the same index
// will return the same [*upvalue].
func (l *State) stackUpvalue(i int) *upvalue {
uvIndex := slices.IndexFunc(l.pendingVariables, func(uv *upvalue) bool {
return uv.stackIndex == i
})
if uvIndex != -1 {
return l.pendingVariables[uvIndex]
}
uv := &upvalue{stackIndex: i}
l.pendingVariables = append(l.pendingVariables, uv)
return uv
}

// resolveUpvalue converts an [*upvalue] to a pointer to a [value],
// representing the upvalue's variable.
// If the upvalue is open, then the returned pointer is valid
// until the stack grows.
func (l *State) resolveUpvalue(uv *upvalue) *value {
if uv.isOpen() {
return &l.stack[uv.stackIndex]
}
return &uv.storage
}

// checkUpvalues ensures that the given set of upvalues
// are either closed or referring to variables in the calling function.
func (l *State) checkUpvalues(upvalues []*upvalue) error {
frame := l.frame()
for i, uv := range upvalues {
if uv.stackIndex >= frame.framePointer() {
return fmt.Errorf("internal error: function upvalue [%d] inside current frame", i)
}
}
return nil
}

// closeUpvalues moves the values of any upvalues
// that refer to stack values at indices less than top
// off to the stack, thus “closing” them.
// This is distinct from calling the “__close” metamethods,
// but often happens at the same time.
func (l *State) closeUpvalues(top int) {
n := 0
for _, uv := range l.pendingVariables {
if uv.isOpen() && uv.stackIndex >= top {
// Close the upvalue.
uv.storage = l.stack[uv.stackIndex]
uv.stackIndex = -1
} else {
// Keep the upvalue in the list.
l.pendingVariables[n] = uv
n++
}
}
clear(l.pendingVariables[n:])
l.pendingVariables = l.pendingVariables[:n]
}
20 changes: 12 additions & 8 deletions internal/mylua/lua.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ func isPseudo(i int) bool {
// Such information cannot be gathered after the return of a [State] method,
// since by then the stack will have been unwound.
type State struct {
stack []value
registry table
callStack []callFrame
typeMetatables [9]*table
stack []value
registry table
callStack []callFrame
typeMetatables [9]*table
pendingVariables []*upvalue
tbc sets.Bit
}

func (l *State) init() {
Expand Down Expand Up @@ -224,6 +226,8 @@ func (l *State) SetTop(idx int) {
l.setTop(newTop)
}

// setTop sets the top of the stack to i.
// It does not close any upvalues or close any to-be-closed variables.
func (l *State) setTop(i int) {
if i < len(l.stack) {
clear(l.stack[i:])
Expand Down Expand Up @@ -749,9 +753,9 @@ func (l *State) PushClosure(n int, f Function) {
panic("too many upvalues")
}
upvalueStart := len(l.stack) - n
upvalues := make([]upvalue, 0, n)
upvalues := make([]*upvalue, 0, n)
for _, v := range l.stack[upvalueStart:] {
upvalues = append(upvalues, standaloneUpvalue(v))
upvalues = append(upvalues, closedUpvalue(v))
}
l.setTop(upvalueStart)
l.push(goFunction{
Expand Down Expand Up @@ -1376,8 +1380,8 @@ func (l *State) Load(r io.Reader, chunkName luacode.Source, mode string) (err er
l.push(luaFunction{
id: nextID(),
proto: p,
upvalues: []upvalue{
standaloneUpvalue(l.registry.get(integerValue(RegistryIndexGlobals))),
upvalues: []*upvalue{
closedUpvalue(l.registry.get(integerValue(RegistryIndexGlobals))),
},
})
return nil
Expand Down
54 changes: 0 additions & 54 deletions internal/mylua/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,60 +504,6 @@ func (v stringValue) toInteger() (integerValue, bool) {
return integerValue(i), true
}

type function interface {
value
functionID() uint64
upvaluesSlice() []upvalue
}

var (
_ function = goFunction{}
_ function = luaFunction{}
)

type goFunction struct {
id uint64
cb Function
upvalues []upvalue
}

func (f goFunction) valueType() Type { return TypeFunction }
func (f goFunction) functionID() uint64 { return f.id }
func (f goFunction) upvaluesSlice() []upvalue { return f.upvalues }

type luaFunction struct {
id uint64
proto *luacode.Prototype
upvalues []upvalue
}

func (f luaFunction) valueType() Type { return TypeFunction }
func (f luaFunction) functionID() uint64 { return f.id }
func (f luaFunction) upvaluesSlice() []upvalue { return f.upvalues }

type upvalue struct {
p *value
stackIndex int
}

func stackUpvalue(i int) upvalue {
return upvalue{stackIndex: i}
}

func standaloneUpvalue(v value) upvalue {
return upvalue{
p: &v,
stackIndex: -1,
}
}

func (l *State) resolveUpvalue(uv upvalue) *value {
if uv.p == nil {
return &l.stack[uv.stackIndex]
}
return uv.p
}

var globalIDs struct {
mu sync.Mutex
n uint64
Expand Down
Loading

0 comments on commit dce8c9b

Please sign in to comment.