Skip to content

Commit

Permalink
fix: defer function not working when calling a reciever method
Browse files Browse the repository at this point in the history
The defer statement was not capturing that a receiver was specfied
in the call. The fix captures the reciver stack created for the defer
statement and makes the same stack available when the defered statement
are run at function exit.

Also, cleaned up formatting of numerous native and internal object types,
so you can print an *os.File or time.Duration value and get a meaningful
result.
  • Loading branch information
tucats committed Dec 14, 2024
1 parent 58e765f commit 2794b55
Show file tree
Hide file tree
Showing 18 changed files with 204 additions and 64 deletions.
12 changes: 10 additions & 2 deletions bytecode/callframe.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ const (

func (f *CallFrame) String() string {
name := f.Module
if name == "" {
name = f.name
}

if name != "" && f.Package != "" {
name = f.Package + "." + name
}

if name == "" {
name = defs.Anon
}
Expand All @@ -71,7 +79,7 @@ func (c *Context) callframePushWithTable(table *symbols.SymbolTable, bc *ByteCod
blockDepth: c.blockDepth,
singleStep: c.singleStep,
tokenizer: c.tokenizer,
thisStack: c.thisStack,
thisStack: c.receiverStack,
deferStack: c.deferStack,
pc: c.programCounter,
fp: c.framePointer,
Expand Down Expand Up @@ -137,7 +145,7 @@ func (c *Context) callFramePop() error {
c.symbols = callFrame.symbols
c.singleStep = callFrame.singleStep
c.tokenizer = callFrame.tokenizer
c.thisStack = callFrame.thisStack
c.receiverStack = callFrame.thisStack
c.bc = callFrame.bytecode
c.programCounter = callFrame.pc
c.framePointer = callFrame.fp
Expand Down
8 changes: 6 additions & 2 deletions bytecode/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type deferStatement struct {
// Function target
target interface{}

// Receiver stack in effect for this defer statement
receiverStack []this

// Arguments
args []interface{}
}
Expand All @@ -66,7 +69,7 @@ type Context struct {
tryStack []tryInfo
rangeStack []*rangeDefinition
timerStack []time.Time
thisStack []this
receiverStack []this
packageStack []packageDef
deferStack []deferStatement
output *strings.Builder
Expand All @@ -82,6 +85,7 @@ type Context struct {
lastLine int
blockDepth int
argCountDelta int
deferThisSize int
threadID int32
fullSymbolScope bool
running bool
Expand Down Expand Up @@ -146,7 +150,7 @@ func NewContext(s *symbols.SymbolTable, b *ByteCode) *Context {
line: 0,
symbols: s,
fullSymbolScope: true,
thisStack: nil,
receiverStack: nil,
deferStack: make([]deferStatement, 0),
throwUncheckedErrors: settings.GetBool(defs.ThrowUncheckedErrorsSetting),
fullStackTrace: settings.GetBool(defs.FullStackTraceSetting),
Expand Down
41 changes: 31 additions & 10 deletions bytecode/defer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import (
"github.com/tucats/ego/errors"
)

func deferStartByteCode(c *Context, i interface{}) error {
c.deferThisSize = len(c.receiverStack)

return nil
}

// deferByteCode is the bytecode for the defer statement. This captures
// the arguments to the function as well as the function target object,
// and stores them in the runtime context. This is then executed when
Expand All @@ -32,11 +38,24 @@ func deferByteCode(c *Context, i interface{}) error {
return err
}

var receivers []this

// If at the start of the defer we captured the size of the receiver stack
// and the receiver stack size is now larger, it means the defer exprssion
// included receiver values. We need to capture these values and store them
// in the defer stack object.
if c.deferThisSize > 0 && (c.deferThisSize < len(c.receiverStack)) {
// Capture the slide of the this stack since we started the defer.
receivers = c.receiverStack[len(c.receiverStack)-c.deferThisSize:]
c.receiverStack = c.receiverStack[:c.deferThisSize]
}

// Push the function and arguments onto the defer stack
c.deferStack = append(c.deferStack, deferStatement{
target: f,
args: args,
name: name,
target: f,
receiverStack: receivers,
args: args,
name: name,
})

return nil
Expand All @@ -57,23 +76,25 @@ func runDefersByteCode(c *Context, i interface{}) error {
func (c *Context) invokeDeferredStatements() error {
// Run the deferred statements in reverse order.
for i := len(c.deferStack) - 1; i >= 0; i-- {
ds := c.deferStack[i]
deferTask := c.deferStack[i]

// Create a new bytecode area to execute the defer operations.
cb := New("defer " + ds.name)
cb := New("defer " + deferTask.name)

// Push the target function onto the stack
cb.Emit(Push, ds.target)
cb.Emit(Push, deferTask.target)

// Push the arguments in reverse order from the stack
for j := len(ds.args) - 1; j >= 0; j-- {
cb.Emit(Push, ds.args[j])
for j := len(deferTask.args) - 1; j >= 0; j-- {
cb.Emit(Push, deferTask.args[j])
}

cb.Emit(Call, len(ds.args))
cb.Emit(Call, len(deferTask.args))

// Create a context for executing the deferred statement
// Create a context for executing the deferred statement. The context
// is given the active receiver stack associated with the defer statement.
cx := NewContext(c.symbols, cb)
cx.receiverStack = deferTask.receiverStack

// Execute the deferred statement
if err := cx.Run(); err != nil && err != errors.ErrStop {
Expand Down
5 changes: 3 additions & 2 deletions bytecode/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ func atLineByteCode(c *Context, i interface{}) error {
text := c.tokenizer.GetLine(c.line)
if len(strings.TrimSpace(text)) > 0 {
location := fmt.Sprintf("line %d", c.line)

ui.Log(ui.TraceLogger, "(%d) >>> %-19s %s", c.threadID, location, strings.TrimSpace(text))
}
}
Expand All @@ -165,11 +166,11 @@ func atLineByteCode(c *Context, i interface{}) error {
// See if the top of the "this" stack is a package, and if so return
// it's symbol table. The stack is not modified.
func (c *Context) getPackageSymbols() *symbols.SymbolTable {
if len(c.thisStack) == 0 {
if len(c.receiverStack) == 0 {
return nil
}

this := c.thisStack[len(c.thisStack)-1]
this := c.receiverStack[len(c.receiverStack)-1]

if pkg, ok := this.value.(*data.Package); ok {
if s, ok := pkg.Get(data.SymbolsMDKey); ok {
Expand Down
2 changes: 1 addition & 1 deletion bytecode/instruction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func Test_instruction_String(t *testing.T) {
name: "token operand",
Operation: Push,
Operand: tokenizer.BoolToken,
want: "Push kind struct Type(bool)",
want: "Push Type(bool)",
},
}
for _, tt := range tests {
Expand Down
3 changes: 3 additions & 0 deletions bytecode/opcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
Copy
CreateAndStore
Defer
DeferStart
DeRef
Div
Drop
Expand Down Expand Up @@ -183,6 +184,7 @@ var opcodeNames = map[Opcode]string{
Copy: "Copy",
CreateAndStore: "CreateAndStore",
Defer: "Defer",
DeferStart: "DeferStart",
DeRef: "DeRef",
Div: "Div",
Drop: "Drop",
Expand Down Expand Up @@ -308,6 +310,7 @@ func initializeDispatch() {
dispatchTable[Copy] = copyByteCode
dispatchTable[CreateAndStore] = createAndStoreByteCode
dispatchTable[Defer] = deferByteCode
dispatchTable[DeferStart] = deferStartByteCode
dispatchTable[DeRef] = deRefByteCode
dispatchTable[Div] = divideByteCode
dispatchTable[Drop] = dropByteCode
Expand Down
2 changes: 1 addition & 1 deletion bytecode/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func isStackMarker(i interface{}, values ...string) bool {
// Produce a string reprsentation of a stack marker.
func (sm StackMarker) String() string {
b := strings.Builder{}
b.WriteString("M<")
b.WriteString("Marker<")
b.WriteString(sm.label)

for _, data := range sm.values {
Expand Down
6 changes: 3 additions & 3 deletions bytecode/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,17 @@ func TestStackMarker_String(t *testing.T) {
{
name: "simple marker",
marker: NewStackMarker("test"),
want: "M<test>",
want: "Marker<test>",
},
{
name: "marker with one item",
marker: NewStackMarker("test", 33),
want: "M<test, 33>",
want: "Marker<test, 33>",
},
{
name: "marker with multiple items",
marker: NewStackMarker("test", 33, "foo", true),
want: "M<test, 33, foo, true>",
want: "Marker<test, 33, foo, true>",
},
}
for _, tt := range tests {
Expand Down
2 changes: 1 addition & 1 deletion bytecode/symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func popScopeByteCode(c *Context, i interface{}) error {
return errors.New(err)
}

c.thisStack = nil
c.receiverStack = nil
c.blockDepth--

count--
Expand Down
44 changes: 28 additions & 16 deletions bytecode/this.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,31 @@ func setThisByteCode(c *Context, i interface{}) error {
// loadThisByteCode implements the LoadThis opcode. This combines the
// functionality of the Load followed by the SetThis opcodes.
func loadThisByteCode(c *Context, i interface{}) error {
name := data.String(i)
if len(name) == 0 {
return c.error(errors.ErrInvalidIdentifier)
}
var (
found bool
this interface{}
)

// If the operand is a name, look up the value in the
// symbol table. If it's not a name, then it's a value for
// a receiver and we use it as-is.
if name, ok := i.(string); ok {
if len(name) == 0 {
return c.error(errors.ErrInvalidIdentifier)
}

v, found := c.get(name)
if !found {
return c.error(errors.ErrUnknownIdentifier).Context(name)
this, found = c.get(name)
if !found {
return c.error(errors.ErrUnknownIdentifier).Context(name)
}
} else {
this = i
}

_ = c.push(v)
name = data.GenerateName()
c.setAlways(name, v)
// Assign the value to a geenated name and put it on the reciver stack.
_ = c.push(this)
name := data.GenerateName()
c.setAlways(name, this)

if v, ok := c.get(name); ok {
c.pushThis(name, v)
Expand All @@ -73,21 +85,21 @@ func getThisByteCode(c *Context, i interface{}) error {

// pushThis adds a receiver value to the "this" stack.
func (c *Context) pushThis(name string, v interface{}) {
if c.thisStack == nil {
c.thisStack = []this{}
if c.receiverStack == nil {
c.receiverStack = []this{}
}

c.thisStack = append(c.thisStack, this{name, v})
c.receiverStack = append(c.receiverStack, this{name, v})
}

// popThis removes a receiver value from this "this" stack.
func (c *Context) popThis() (interface{}, bool) {
if len(c.thisStack) == 0 {
if len(c.receiverStack) == 0 {
return nil, false
}

this := c.thisStack[len(c.thisStack)-1]
c.thisStack = c.thisStack[:len(c.thisStack)-1]
this := c.receiverStack[len(c.receiverStack)-1]
c.receiverStack = c.receiverStack[:len(c.receiverStack)-1]

return this.value, true
}
7 changes: 6 additions & 1 deletion commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,12 @@ func runREPL(interactive bool, extensions bool, text string, debug bool, lineNum

// Compile the token stream we have accumulated, using the entrypoint name provided by
// the user (or defaulting to "main").
b, err = comp.Compile("main '"+mainName+"'", t)
label := "console"
if mainName != "" {
label = "main '" + mainName + "'"
}

b, err = comp.Compile(label, t)
if err != nil {
exitValue = 1
msg := fmt.Sprintf("%s: %s\n", i18n.L("Error"), err.Error())
Expand Down
7 changes: 5 additions & 2 deletions compiler/defer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ func (c *Compiler) compileDefer() error {
return c.error(errors.ErrDeferOutsideFunction)
}

if c.t.AnyNext(tokenizer.SemicolonToken, tokenizer.EndOfTokens) {
if c.t.EndofStatement() {
return c.error(errors.ErrMissingFunction)
}

// Mark the start of the defer statement, which also captures
// state that the defer statement will use.
c.b.Emit(bc.DeferStart)

// Is it a function constant?
if c.t.IsNext(tokenizer.FuncToken) {
// Compile a function literal onto the stack.
Expand Down Expand Up @@ -61,4 +65,3 @@ func (c *Compiler) compileDefer() error {

return nil
}

2 changes: 1 addition & 1 deletion data/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ func UnwrapConstant(i interface{}) interface{} {
// String generates a human-readable string describing the value
// in the immutable wrapper.
func (w Immutable) String() string {
return fmt.Sprintf("%s <read only>", Format(w.Value))
return fmt.Sprintf("^%s", Format(w.Value))
}
2 changes: 1 addition & 1 deletion data/constructors.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func PackageType(name string) *Type {
// the required functions in the Interface specification.
func NewInterfaceType(name string) *Type {
if name == "" {
name = "interface{}"
name = InterfaceTypeName
}

t := &Type{
Expand Down
Loading

0 comments on commit 2794b55

Please sign in to comment.