diff --git a/debugger.go b/debugger.go new file mode 100644 index 0000000..cb89ecb --- /dev/null +++ b/debugger.go @@ -0,0 +1,401 @@ +package jsonnet + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/google/go-jsonnet/ast" + "github.com/google/go-jsonnet/toolutils" +) + +type Debugger struct { + // VM evaluating the input + vm *VM + + // Interpreter built by the evaluation. Required to look up variables and stack traces + interpreter *interpreter + + // breakpoints are stored as the result of the .String function of + // *ast.LocationRange to speed up lookup + breakpoints map[string]bool + + // The events channel is used to communicate events happening in the VM with the debugger + events chan DebugEvent + // The cont channel is used to pass continuation events from the frontend to the VM + cont chan continuationEvent + + // lastEvaluation stores the result of the last evaluated node + lastEvaluation value + + // breakOnNode allows the debugger to request continuation until after a + // certain node has been evaluated (step-out) + breakOnNode ast.Node + + // singleStep is used to break on every instruction if set to true + singleStep bool + + // skip skips all hooks when performing sub-evaluation (to lookup vars) + skip bool + + // current keeps track of the node currently being evaluated + current ast.Node +} + +// ContinuationEvents are sent by the debugger frontend. Specifying `until` +// results in continuation until the evaluated node matches the argument +type continuationEvent struct { + until *ast.Node +} + +type DebugStopReason int + +const ( + StopReasonStep DebugStopReason = iota + StopReasonBreakpoint + StopReasonException +) + +// A DebugEvent is emitted by the hooks to signal certain events happening in the VM. Examples are: +// - Hitting a breakpoint +// - Catching an exception +// - Program termination +type DebugEvent interface { + anEvent() +} + +type DebugEventExit struct { + Output string + Error error +} + +func (d *DebugEventExit) anEvent() {} + +type DebugEventStop struct { + Reason DebugStopReason + Breakpoint string + Current ast.Node + LastEvaluation *string + Error error + + // efmt is used to format the error (if any). Built by the vm so we need to + // keep a reference in the event + efmt ErrorFormatter +} + +func (d *DebugEventStop) anEvent() {} +func (d *DebugEventStop) ErrorFmt() string { + return d.efmt.Format(d.Error) +} + +func MakeDebugger() *Debugger { + d := &Debugger{ + events: make(chan DebugEvent, 2048), + cont: make(chan continuationEvent), + } + vm := MakeVM() + vm.EvalHook = EvalHook{ + pre: d.preHook, + post: d.postHook, + } + d.vm = vm + d.breakpoints = make(map[string]bool) + return d +} + +func traverse(root ast.Node, f func(node *ast.Node) error) error { + if err := f(&root); err != nil { + return fmt.Errorf("pre error: %w", err) + } + + children := toolutils.Children(root) + for _, c := range children { + if err := traverse(c, f); err != nil { + return err + } + } + return nil +} + +func (d *Debugger) Continue() { + d.cont <- continuationEvent{} +} +func (d *Debugger) ContinueUntilAfter(n ast.Node) { + d.cont <- continuationEvent{ + until: &n, + } +} + +func (d *Debugger) Step() { + d.singleStep = true + d.Continue() +} + +func (d *Debugger) Terminate() { + d.events <- &DebugEventExit{ + Error: fmt.Errorf("terminated"), + } +} + +func (d *Debugger) postHook(i *interpreter, n ast.Node, v value, err error) { + d.lastEvaluation = v + if d.skip { + return + } + if err != nil { + d.events <- &DebugEventStop{ + Current: n, + Reason: StopReasonException, + Error: err, + efmt: d.vm.ErrorFormatter, + } + d.waitForContinuation() + } + if d.breakOnNode == n { + d.breakOnNode = nil + d.singleStep = true + } +} + +func (d *Debugger) waitForContinuation() { + c := <-d.cont + if c.until != nil { + d.breakOnNode = *c.until + } +} + +func (d *Debugger) preHook(i *interpreter, n ast.Node) { + d.interpreter = i + d.current = n + if d.skip { + return + } + + switch n.(type) { + case *ast.LiteralNull, *ast.LiteralNumber, *ast.LiteralString, *ast.LiteralBoolean: + return + } + l := n.Loc() + if l.File == nil { + return + } + vs := valueToString(d.lastEvaluation) + if d.singleStep { + d.singleStep = false + d.events <- &DebugEventStop{ + Reason: StopReasonStep, + Current: n, + LastEvaluation: &vs, + } + d.waitForContinuation() + return + } + loc := n.Loc() + if loc == nil || loc.File == nil { + // virtual file such as + return + } + if _, ok := d.breakpoints[loc.String()]; ok { + d.events <- &DebugEventStop{ + Reason: StopReasonBreakpoint, + Breakpoint: loc.Begin.String(), + Current: n, + LastEvaluation: &vs, + } + d.waitForContinuation() + } + return +} + +func valueToString(v value) string { + switch i := v.(type) { + case *valueFlatString: + return "\"" + i.getGoString() + "\"" + case *valueObject: + if i == nil { + return "{}" + } + var sb strings.Builder + sb.WriteString("{") + firstLine := true + for k, v := range i.cache { + if k.depth != 0 { + continue + } + if !firstLine { + sb.WriteString(", ") + firstLine = true + } + sb.WriteString(k.field) + sb.WriteString(": ") + sb.WriteString(valueToString(v)) + } + sb.WriteString("}") + return sb.String() + case *valueArray: + var sb strings.Builder + sb.WriteString("[") + for i, e := range i.elements { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(valueToString(e.content)) + } + sb.WriteString("]") + return sb.String() + case *valueNumber: + return fmt.Sprintf("%f", i.value) + case *valueBoolean: + return fmt.Sprintf("%t", i.value) + case *valueFunction: + var sb strings.Builder + sb.WriteString("function(") + for i, p := range i.parameters() { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(string(p.name)) + } + sb.WriteString(")") + return sb.String() + } + return fmt.Sprintf("%T%+v", v, v) +} + +func (d *Debugger) ActiveBreakpoints() []string { + bps := []string{} + for k := range d.breakpoints { + bps = append(bps, k) + } + return bps +} + +func (d *Debugger) BreakpointLocations(file string) ([]*ast.LocationRange, error) { + abs, err := filepath.Abs(file) + if err != nil { + return nil, err + } + raw, err := os.ReadFile(abs) + if err != nil { + return nil, fmt.Errorf("reading file: %w", err) + } + a, err := SnippetToAST(file, string(raw)) + if err != nil { + return nil, fmt.Errorf("invalid source file: %w", err) + } + bps := []*ast.LocationRange{} + traverse(a, func(n *ast.Node) error { + if n != nil { + l := (*n).Loc() + if l.File != nil { + bps = append(bps, l) + } + } + return nil + }) + return bps, nil +} + +func (d *Debugger) SetBreakpoint(file string, line int, column int) (string, error) { + valid, err := d.BreakpointLocations(file) + if err != nil { + return "", fmt.Errorf("getting valid breakpoint locations: %w", err) + } + target := "" + for _, b := range valid { + if b.Begin.Line == line { + if column < 0 { + target = b.String() + break + } else if b.Begin.Column == column { + target = b.String() + break + } + } + } + if target == "" { + return "", fmt.Errorf("breakpoint location invalid") + } + d.breakpoints[target] = true + return target, nil +} +func (d *Debugger) ClearBreakpoints(file string) { + abs, _ := filepath.Abs(file) + for k := range d.breakpoints { + parts := strings.Split(k, ":") + full, err := filepath.Abs(parts[0]) + if err == nil && full == abs { + delete(d.breakpoints, k) + } + } +} + +func (d *Debugger) LookupValue(val string) (string, error) { + switch val { + case "self": + return valueToString(d.interpreter.stack.getSelfBinding().self), nil + case "super": + return valueToString(d.interpreter.stack.getSelfBinding().super().self), nil + default: + v := d.interpreter.stack.lookUpVar(ast.Identifier(val)) + if v != nil { + if v.content == nil { + d.skip = true + e, err := func() (rv value, err error) { // closure to use defer->recover + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + rv, err = d.interpreter.rawevaluate(v.body, 0) + return + }() + d.skip = false + if err != nil { + return "", err + } + v.content = e + } + return valueToString(v.content), nil + } + } + return "", fmt.Errorf("invalid identifier %s", val) +} + +func (d *Debugger) ListVars() []ast.Identifier { + if d.interpreter != nil { + return d.interpreter.stack.listVars() + } + return make([]ast.Identifier, 0) +} + +func (d *Debugger) Launch(filename, snippet string, jpaths []string) { + jpaths = append(jpaths, filepath.Dir(filename)) + d.vm.Importer(&FileImporter{ + JPaths: jpaths, + }) + go func() { + out, err := d.vm.EvaluateAnonymousSnippet(filename, snippet) + d.events <- &DebugEventExit{ + Output: out, + Error: err, + } + }() +} + +func (d *Debugger) Events() chan DebugEvent { + return d.events +} + +func (d *Debugger) StackTrace() []TraceFrame { + if d.interpreter == nil || d.current == nil { + return nil + } + trace := d.interpreter.getCurrentStackTrace() + for i, t := range trace { + trace[i].Name = t.Loc.FileName // use pseudo file name as name + } + trace[len(trace)-1].Loc = *d.current.Loc() + return trace +} diff --git a/error_formatter.go b/error_formatter.go index 03a803a..7fcfb95 100644 --- a/error_formatter.go +++ b/error_formatter.go @@ -110,7 +110,7 @@ func (ef *termErrorFormatter) showCode(buf *bytes.Buffer, loc ast.LocationRange) fmt.Fprintf(buf, "\n") } -func (ef *termErrorFormatter) frame(frame *traceFrame, buf *bytes.Buffer) { +func (ef *termErrorFormatter) frame(frame *TraceFrame, buf *bytes.Buffer) { // TODO(sbarzowski) tabs are probably a bad idea fmt.Fprintf(buf, "\t%v\t%v\n", frame.Loc.String(), frame.Name) if ef.pretty { @@ -118,7 +118,7 @@ func (ef *termErrorFormatter) frame(frame *traceFrame, buf *bytes.Buffer) { } } -func (ef *termErrorFormatter) buildStackTrace(frames []traceFrame) string { +func (ef *termErrorFormatter) buildStackTrace(frames []TraceFrame) string { // https://github.com/google/jsonnet/blob/master/core/libjsonnet.cpp#L594 maxAbove := ef.maxStackTraceSize / 2 maxBelow := ef.maxStackTraceSize - maxAbove diff --git a/interpreter.go b/interpreter.go index 390d459..b055f12 100644 --- a/interpreter.go +++ b/interpreter.go @@ -49,8 +49,8 @@ func makeEnvironment(upValues bindingFrame, sb selfBinding) environment { } } -func (i *interpreter) getCurrentStackTrace() []traceFrame { - var result []traceFrame +func (i *interpreter) getCurrentStackTrace() []TraceFrame { + var result []TraceFrame for _, f := range i.stack.stack { if f.cleanEnv { result = append(result, traceElementToTraceFrame(f.trace)) @@ -208,6 +208,20 @@ func (s *callStack) lookUpVar(id ast.Identifier) *cachedThunk { return nil } +func (s *callStack) listVars() []ast.Identifier { + vars := []ast.Identifier{} + for i := len(s.stack) - 1; i >= 0; i-- { + for k := range s.stack[i].env.upValues { + vars = append(vars, k) + } + if s.stack[i].cleanEnv { + // Nothing beyond the captured environment of the thunk / closure. + break + } + } + return vars +} + func (s *callStack) lookUpVarOrPanic(id ast.Identifier) *cachedThunk { th := s.lookUpVar(id) if th == nil { @@ -239,6 +253,11 @@ func makeCallStack(limit int) callStack { } } +type EvalHook struct { + pre func(i *interpreter, n ast.Node) + post func(i *interpreter, n ast.Node, v value, err error) +} + // Keeps current execution context and evaluates things type interpreter struct { // Output stream for trace() for @@ -260,6 +279,8 @@ type interpreter struct { // 1) Keeping environment (object we're in, variables) // 2) Diagnostic information in case of failure stack callStack + + evalHook EvalHook } // Map union, b takes precedence when keys collide. @@ -287,6 +308,13 @@ func (i *interpreter) newCall(env environment, trimmable bool) error { } func (i *interpreter) evaluate(a ast.Node, tc tailCallStatus) (value, error) { + i.evalHook.pre(i, a) + v, err := i.rawevaluate(a, tc) + i.evalHook.post(i, a, v, err) + return v, err +} + +func (i *interpreter) rawevaluate(a ast.Node, tc tailCallStatus) (value, error) { trace := traceElement{ loc: a.Loc(), context: a.Context(), @@ -1237,12 +1265,13 @@ func buildObject(hide ast.ObjectFieldHide, fields map[string]value) *valueObject return makeValueSimpleObject(bindingFrame{}, fieldMap, nil, nil) } -func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxStack int, ic *importCache, traceOut io.Writer) (*interpreter, error) { +func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook) (*interpreter, error) { i := interpreter{ stack: makeCallStack(maxStack), importCache: ic, traceOut: traceOut, nativeFuncs: nativeFuncs, + evalHook: evalHook, } stdObj, err := buildStdObject(&i) @@ -1315,9 +1344,9 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, error) { // TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction, - maxStack int, ic *importCache, traceOut io.Writer, stringOutputMode bool) (string, error) { + maxStack int, ic *importCache, traceOut io.Writer, stringOutputMode bool, evalHook EvalHook) (string, error) { - i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut) + i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut, evalHook) if err != nil { return "", err } @@ -1344,9 +1373,9 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string] // TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction, - maxStack int, ic *importCache, traceOut io.Writer, stringOutputMode bool) (map[string]string, error) { + maxStack int, ic *importCache, traceOut io.Writer, stringOutputMode bool, evalHook EvalHook) (map[string]string, error) { - i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut) + i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut, evalHook) if err != nil { return nil, err } @@ -1364,9 +1393,9 @@ func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[st // TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead func evaluateStream(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction, - maxStack int, ic *importCache, traceOut io.Writer) ([]string, error) { + maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook) ([]string, error) { - i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut) + i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut, evalHook) if err != nil { return nil, err } diff --git a/runtime_error.go b/runtime_error.go index 40a705b..2a52d3a 100644 --- a/runtime_error.go +++ b/runtime_error.go @@ -21,10 +21,10 @@ import "github.com/google/go-jsonnet/ast" // RuntimeError is an error discovered during evaluation of the program type RuntimeError struct { Msg string - StackTrace []traceFrame + StackTrace []TraceFrame } -func makeRuntimeError(msg string, stackTrace []traceFrame) RuntimeError { +func makeRuntimeError(msg string, stackTrace []TraceFrame) RuntimeError { return RuntimeError{ Msg: msg, StackTrace: stackTrace, @@ -37,15 +37,15 @@ func (err RuntimeError) Error() string { // The stack -// traceFrame is tracing information about a single frame of the call stack. +// TraceFrame is tracing information about a single frame of the call stack. // TODO(sbarzowski) the difference from traceElement. Do we even need this? -type traceFrame struct { +type TraceFrame struct { Name string Loc ast.LocationRange } -func traceElementToTraceFrame(trace traceElement) traceFrame { - tf := traceFrame{Loc: *trace.loc} +func traceElementToTraceFrame(trace traceElement) TraceFrame { + tf := TraceFrame{Loc: *trace.loc} if trace.context != nil { // TODO(sbarzowski) maybe it should never be nil tf.Name = *trace.context diff --git a/vm.go b/vm.go index 44b966c..f619632 100644 --- a/vm.go +++ b/vm.go @@ -46,6 +46,7 @@ type VM struct { //nolint:govet StringOutput bool importCache *importCache traceOut io.Writer + EvalHook EvalHook } // extKind indicates the kind of external variable that is being initialized for the VM @@ -81,6 +82,10 @@ func MakeVM() *VM { importer: &FileImporter{}, importCache: makeImportCache(defaultImporter), traceOut: os.Stderr, + EvalHook: EvalHook{ + pre: func(i *interpreter, a ast.Node) {}, + post: func(i *interpreter, a ast.Node, v value, err error) {}, + }, } } @@ -182,7 +187,7 @@ func (vm *VM) Evaluate(node ast.Node) (val string, err error) { err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) } }() - return evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput) + return evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) } // EvaluateStream evaluates a Jsonnet program given by an Abstract Syntax Tree @@ -193,7 +198,7 @@ func (vm *VM) EvaluateStream(node ast.Node) (output []string, err error) { err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) } }() - return evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut) + return evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook) } // EvaluateMulti evaluates a Jsonnet program given by an Abstract Syntax Tree @@ -205,7 +210,7 @@ func (vm *VM) EvaluateMulti(node ast.Node) (output map[string]string, err error) err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) } }() - return evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput) + return evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) } func (vm *VM) evaluateSnippet(diagnosticFileName ast.DiagnosticFileName, filename string, snippet string, kind evalKind) (output interface{}, err error) { @@ -220,11 +225,11 @@ func (vm *VM) evaluateSnippet(diagnosticFileName ast.DiagnosticFileName, filenam } switch kind { case evalKindRegular: - output, err = evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput) + output, err = evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) case evalKindMulti: - output, err = evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput) + output, err = evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) case evalKindStream: - output, err = evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut) + output, err = evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook) } if err != nil { return "", err @@ -250,20 +255,20 @@ func getAbsPath(path string) (string, error) { return cleanedAbsPath, nil } -func (vm *VM) findDependencies(filePath string, node *ast.Node, dependencies map[string]struct{}, stackTrace *[]traceFrame) (err error) { +func (vm *VM) findDependencies(filePath string, node *ast.Node, dependencies map[string]struct{}, stackTrace *[]TraceFrame) (err error) { var cleanedAbsPath string switch i := (*node).(type) { case *ast.Import: node, foundAt, err := vm.ImportAST(filePath, i.File.Value) if err != nil { - *stackTrace = append([]traceFrame{{Loc: *i.Loc()}}, *stackTrace...) + *stackTrace = append([]TraceFrame{{Loc: *i.Loc()}}, *stackTrace...) return err } cleanedAbsPath = foundAt if _, isFileImporter := vm.importer.(*FileImporter); isFileImporter { cleanedAbsPath, err = getAbsPath(foundAt) if err != nil { - *stackTrace = append([]traceFrame{{Loc: *i.Loc()}}, *stackTrace...) + *stackTrace = append([]TraceFrame{{Loc: *i.Loc()}}, *stackTrace...) return err } } @@ -274,20 +279,20 @@ func (vm *VM) findDependencies(filePath string, node *ast.Node, dependencies map dependencies[cleanedAbsPath] = struct{}{} err = vm.findDependencies(foundAt, &node, dependencies, stackTrace) if err != nil { - *stackTrace = append([]traceFrame{{Loc: *i.Loc()}}, *stackTrace...) + *stackTrace = append([]TraceFrame{{Loc: *i.Loc()}}, *stackTrace...) return err } case *ast.ImportStr: foundAt, err := vm.ResolveImport(filePath, i.File.Value) if err != nil { - *stackTrace = append([]traceFrame{{Loc: *i.Loc()}}, *stackTrace...) + *stackTrace = append([]TraceFrame{{Loc: *i.Loc()}}, *stackTrace...) return err } cleanedAbsPath = foundAt if _, isFileImporter := vm.importer.(*FileImporter); isFileImporter { cleanedAbsPath, err = getAbsPath(foundAt) if err != nil { - *stackTrace = append([]traceFrame{{Loc: *i.Loc()}}, *stackTrace...) + *stackTrace = append([]TraceFrame{{Loc: *i.Loc()}}, *stackTrace...) return err } } @@ -295,14 +300,14 @@ func (vm *VM) findDependencies(filePath string, node *ast.Node, dependencies map case *ast.ImportBin: foundAt, err := vm.ResolveImport(filePath, i.File.Value) if err != nil { - *stackTrace = append([]traceFrame{{Loc: *i.Loc()}}, *stackTrace...) + *stackTrace = append([]TraceFrame{{Loc: *i.Loc()}}, *stackTrace...) return err } cleanedAbsPath = foundAt if _, isFileImporter := vm.importer.(*FileImporter); isFileImporter { cleanedAbsPath, err = getAbsPath(foundAt) if err != nil { - *stackTrace = append([]traceFrame{{Loc: *i.Loc()}}, *stackTrace...) + *stackTrace = append([]TraceFrame{{Loc: *i.Loc()}}, *stackTrace...) return err } } @@ -455,7 +460,7 @@ func (vm *VM) EvaluateFileMulti(filename string) (files map[string]string, forma // The `importedPaths` are parsed as if they were imported from a Jsonnet file located at `importedFrom`. func (vm *VM) FindDependencies(importedFrom string, importedPaths []string) ([]string, error) { var nodes []*ast.Node - var stackTrace []traceFrame + var stackTrace []TraceFrame filePaths := make([]string, len(importedPaths)) depsToExclude := make([]string, len(importedPaths)) deps := make(map[string]struct{})