Better stacktrace for manifestation, +: and object assertions.

Fixes #282
This commit is contained in:
Stanisław Barzowski 2020-12-13 18:02:53 +01:00
parent 7d3bda3911
commit 6140a2f75a
29 changed files with 220 additions and 42 deletions

View File

@ -62,7 +62,12 @@ func desugarFields(nodeBase ast.NodeBase, fields *ast.ObjectFields, objLevel int
if msg == nil { if msg == nil {
msg = buildLiteralString("Object assertion failed.") msg = buildLiteralString("Object assertion failed.")
} }
onFailure := &ast.Error{Expr: msg} onFailure := &ast.Error{
NodeBase: ast.NodeBase{
LocRange: field.LocRange,
},
Expr: msg,
}
asserts = append(asserts, &ast.Conditional{ asserts = append(asserts, &ast.Conditional{
NodeBase: ast.NodeBase{ NodeBase: ast.NodeBase{
LocRange: field.LocRange, LocRange: field.LocRange,

View File

@ -35,9 +35,9 @@ type environment struct {
// Bindings introduced in this frame. The way previous bindings are treated // Bindings introduced in this frame. The way previous bindings are treated
// depends on the type of a frame. // depends on the type of a frame.
// If isCall == true then previous bindings are ignored (it's a clean // If cleanEnv == true then previous bindings are ignored (it's a clean
// environment with just the variables we have here). // environment with just the variables we have here).
// If isCall == false then if this frame doesn't contain a binding // If cleanEnv == false then if this frame doesn't contain a binding
// previous bindings will be used. // previous bindings will be used.
upValues bindingFrame upValues bindingFrame
} }
@ -52,7 +52,7 @@ func makeEnvironment(upValues bindingFrame, sb selfBinding) environment {
func (i *interpreter) getCurrentStackTrace() []traceFrame { func (i *interpreter) getCurrentStackTrace() []traceFrame {
var result []traceFrame var result []traceFrame
for _, f := range i.stack.stack { for _, f := range i.stack.stack {
if f.isCall { if f.cleanEnv {
result = append(result, traceElementToTraceFrame(f.trace)) result = append(result, traceElementToTraceFrame(f.trace))
} }
} }
@ -65,8 +65,7 @@ func (i *interpreter) getCurrentStackTrace() []traceFrame {
type callFrame struct { type callFrame struct {
// True if it switches to a clean environment (function call or array element) // True if it switches to a clean environment (function call or array element)
// False otherwise, e.g. for local // False otherwise, e.g. for local
// This makes callFrame a misnomer as it is technically not always a call... cleanEnv bool
isCall bool
// Tracing information about the place where it was called from. // Tracing information about the place where it was called from.
trace traceElement trace traceElement
@ -87,8 +86,8 @@ func dumpCallFrame(c *callFrame) string {
} else { } else {
loc = *c.trace.loc loc = *c.trace.loc
} }
return fmt.Sprintf("<callFrame isCall = %t location = %v trimmable = %t>", return fmt.Sprintf("<callFrame cleanEnv = %t location = %v trimmable = %t>",
c.isCall, c.cleanEnv,
loc, loc,
c.trimmable, c.trimmable,
) )
@ -121,7 +120,7 @@ func (s *callStack) top() *callFrame {
// of the frame we want to pop. // of the frame we want to pop.
func (s *callStack) popIfExists(whichFrame int) { func (s *callStack) popIfExists(whichFrame int) {
if len(s.stack) == whichFrame { if len(s.stack) == whichFrame {
if s.top().isCall { if s.top().cleanEnv {
s.calls-- s.calls--
} }
s.setCurrentTrace(s.stack[len(s.stack)-1].trace) s.setCurrentTrace(s.stack[len(s.stack)-1].trace)
@ -132,7 +131,7 @@ func (s *callStack) popIfExists(whichFrame int) {
/** If there is a trimmable frame followed by some locals, pop them all. */ /** If there is a trimmable frame followed by some locals, pop them all. */
func (s *callStack) tailCallTrimStack() { func (s *callStack) tailCallTrimStack() {
for i := len(s.stack) - 1; i >= 0; i-- { for i := len(s.stack) - 1; i >= 0; i-- {
if s.stack[i].isCall { if s.stack[i].cleanEnv {
if !s.stack[i].trimmable { if !s.stack[i].trimmable {
return return
} }
@ -167,7 +166,7 @@ func (s *callStack) newCall(env environment, trimmable bool) {
panic("Saving empty traceElement on stack") panic("Saving empty traceElement on stack")
} }
s.stack = append(s.stack, &callFrame{ s.stack = append(s.stack, &callFrame{
isCall: true, cleanEnv: true,
trace: s.currentTrace, trace: s.currentTrace,
env: env, env: env,
trimmable: trimmable, trimmable: trimmable,
@ -187,7 +186,7 @@ func (s *callStack) newLocal(vars bindingFrame) {
// getSelfBinding resolves the self construct // getSelfBinding resolves the self construct
func (s *callStack) getSelfBinding() selfBinding { func (s *callStack) getSelfBinding() selfBinding {
for i := len(s.stack) - 1; i >= 0; i-- { for i := len(s.stack) - 1; i >= 0; i-- {
if s.stack[i].isCall { if s.stack[i].cleanEnv {
return s.stack[i].env.selfBinding return s.stack[i].env.selfBinding
} }
} }
@ -201,7 +200,7 @@ func (s *callStack) lookUpVar(id ast.Identifier) *cachedThunk {
if present { if present {
return bind return bind
} }
if s.stack[i].isCall { if s.stack[i].cleanEnv {
// Nothing beyond the captured environment of the thunk / closure. // Nothing beyond the captured environment of the thunk / closure.
break break
} }
@ -654,6 +653,15 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) {
if i.stack.currentTrace == (traceElement{}) { if i.stack.currentTrace == (traceElement{}) {
panic("manifesting JSON with empty traceElement") panic("manifesting JSON with empty traceElement")
} }
// Fresh frame for better stack traces
err := i.newCall(environment{}, false)
if err != nil {
return nil, err
}
stackSize := len(i.stack.stack)
defer i.stack.popIfExists(stackSize)
switch v := v.(type) { switch v := v.(type) {
case *valueBoolean: case *valueBoolean:
@ -673,16 +681,23 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) {
case *valueArray: case *valueArray:
result := make([]interface{}, 0, len(v.elements)) result := make([]interface{}, 0, len(v.elements))
for _, th := range v.elements { for index, th := range v.elements {
msg := ast.MakeLocationRangeMessage(fmt.Sprintf("Array element %d", index))
i.stack.setCurrentTrace(traceElement{
loc: &msg,
})
elVal, err := i.evaluatePV(th) elVal, err := i.evaluatePV(th)
if err != nil { if err != nil {
i.stack.clearCurrentTrace()
return nil, err return nil, err
} }
elem, err := i.manifestJSON(elVal) elem, err := i.manifestJSON(elVal)
if err != nil { if err != nil {
i.stack.clearCurrentTrace()
return nil, err return nil, err
} }
result = append(result, elem) result = append(result, elem)
i.stack.clearCurrentTrace()
} }
return result, nil return result, nil
@ -690,24 +705,37 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) {
fieldNames := objectFields(v, withoutHidden) fieldNames := objectFields(v, withoutHidden)
sort.Strings(fieldNames) sort.Strings(fieldNames)
msg := ast.MakeLocationRangeMessage("Checking object assertions")
i.stack.setCurrentTrace(traceElement{
loc: &msg,
})
err := checkAssertions(i, v) err := checkAssertions(i, v)
if err != nil { if err != nil {
i.stack.clearCurrentTrace()
return nil, err return nil, err
} }
i.stack.clearCurrentTrace()
result := make(map[string]interface{}) result := make(map[string]interface{})
for _, fieldName := range fieldNames { for _, fieldName := range fieldNames {
msg := ast.MakeLocationRangeMessage(fmt.Sprintf("Field %#v", fieldName))
i.stack.setCurrentTrace(traceElement{
loc: &msg,
})
fieldVal, err := v.index(i, fieldName) fieldVal, err := v.index(i, fieldName)
if err != nil { if err != nil {
i.stack.clearCurrentTrace()
return nil, err return nil, err
} }
field, err := i.manifestJSON(fieldVal) field, err := i.manifestJSON(fieldVal)
if err != nil { if err != nil {
i.stack.clearCurrentTrace()
return nil, err return nil, err
} }
result[fieldName] = field result[fieldName] = field
i.stack.clearCurrentTrace()
} }
return result, nil return result, nil
@ -944,10 +972,13 @@ func (i *interpreter) EvalInCleanEnv(env *environment, ast ast.Node, trimmable b
stackSize := len(i.stack.stack) stackSize := len(i.stack.stack)
val, err := i.evaluate(ast, tailCall) val, err := i.evaluate(ast, tailCall)
if err != nil {
return nil, err
}
i.stack.popIfExists(stackSize) i.stack.popIfExists(stackSize)
return val, err return val, nil
} }
func (i *interpreter) evaluatePV(ph potentialValue) (value, error) { func (i *interpreter) evaluatePV(ph potentialValue) (value, error) {
@ -1211,7 +1242,14 @@ func makeInitialEnv(filename string, baseStd *valueObject) environment {
) )
} }
func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, traceElement, error) { func manifestationTrace() traceElement {
manifestationLoc := ast.MakeLocationRangeMessage("During manifestation")
return traceElement{
loc: &manifestationLoc,
}
}
func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, error) {
evalLoc := ast.MakeLocationRangeMessage("During evaluation") evalLoc := ast.MakeLocationRangeMessage("During evaluation")
evalTrace := traceElement{ evalTrace := traceElement{
loc: &evalLoc, loc: &evalLoc,
@ -1221,7 +1259,7 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, traceEleme
result, err := i.EvalInCleanEnv(&env, node, false) result, err := i.EvalInCleanEnv(&env, node, false)
i.stack.clearCurrentTrace() i.stack.clearCurrentTrace()
if err != nil { if err != nil {
return nil, traceElement{}, err return nil, err
} }
// If it's not a function, ignore TLA // If it's not a function, ignore TLA
if f, ok := result.(*valueFunction); ok { if f, ok := result.(*valueFunction); ok {
@ -1238,14 +1276,10 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, traceEleme
result, err = f.call(i, args) result, err = f.call(i, args)
i.stack.clearCurrentTrace() i.stack.clearCurrentTrace()
if err != nil { if err != nil {
return nil, traceElement{}, err return nil, err
} }
} }
manifestationLoc := ast.MakeLocationRangeMessage("During manifestation") return result, nil
manifestationTrace := traceElement{
loc: &manifestationLoc,
}
return result, manifestationTrace, nil
} }
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead // TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
@ -1257,13 +1291,13 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
return "", err return "", err
} }
result, manifestationTrace, err := evaluateAux(i, node, tla) result, err := evaluateAux(i, node, tla)
if err != nil { if err != nil {
return "", err return "", err
} }
var buf bytes.Buffer var buf bytes.Buffer
i.stack.setCurrentTrace(manifestationTrace) i.stack.setCurrentTrace(manifestationTrace())
if stringOutputMode { if stringOutputMode {
err = i.manifestString(&buf, result) err = i.manifestString(&buf, result)
} else { } else {
@ -1286,12 +1320,12 @@ func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[st
return nil, err return nil, err
} }
result, manifestationTrace, err := evaluateAux(i, node, tla) result, err := evaluateAux(i, node, tla)
if err != nil { if err != nil {
return nil, err return nil, err
} }
i.stack.setCurrentTrace(manifestationTrace) i.stack.setCurrentTrace(manifestationTrace())
manifested, err := i.manifestAndSerializeMulti(result, stringOutputMode) manifested, err := i.manifestAndSerializeMulti(result, stringOutputMode)
i.stack.clearCurrentTrace() i.stack.clearCurrentTrace()
return manifested, err return manifested, err
@ -1306,12 +1340,12 @@ func evaluateStream(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[s
return nil, err return nil, err
} }
result, manifestationTrace, err := evaluateAux(i, node, tla) result, err := evaluateAux(i, node, tla)
if err != nil { if err != nil {
return nil, err return nil, err
} }
i.stack.setCurrentTrace(manifestationTrace) i.stack.setCurrentTrace(manifestationTrace())
manifested, err := i.manifestAndSerializeYAMLStream(result) manifested, err := i.manifestAndSerializeYAMLStream(result)
i.stack.clearCurrentTrace() i.stack.clearCurrentTrace()
return manifested, err return manifested, err

View File

@ -124,7 +124,7 @@ func prepareStdlib(g *typeGraph) {
"setInter": g.newFuncType(anyArrayType, []ast.Parameter{required("a"), required("b"), optional("keyF")}), "setInter": g.newFuncType(anyArrayType, []ast.Parameter{required("a"), required("b"), optional("keyF")}),
"setUnion": g.newFuncType(anyArrayType, []ast.Parameter{required("a"), required("b"), optional("keyF")}), "setUnion": g.newFuncType(anyArrayType, []ast.Parameter{required("a"), required("b"), optional("keyF")}),
"setDiff": g.newFuncType(anyArrayType, []ast.Parameter{required("a"), required("b"), optional("keyF")}), "setDiff": g.newFuncType(anyArrayType, []ast.Parameter{required("a"), required("b"), optional("keyF")}),
"setMember": g.newFuncType(anyArrayType, []ast.Parameter{required("x"), required("arr"), optional("keyF")}), "setMember": g.newFuncType(boolType, []ast.Parameter{required("x"), required("arr"), optional("keyF")}),
// Encoding // Encoding

View File

@ -0,0 +1 @@
!std.setMember([1, 2, 3], 1)

View File

View File

@ -9,6 +9,9 @@ RUNTIME ERROR: xxx
{ local foo(bar) = error bar, baz: foo("xxx") } { local foo(bar) = error bar, baz: foo("xxx") }
-------------------------------------------------
Field "baz"
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,4 +1,7 @@
RUNTIME ERROR: couldn't manifest function as JSON RUNTIME ERROR: couldn't manifest function as JSON
-------------------------------------------------
Field "f"
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -4,6 +4,9 @@ RUNTIME ERROR: Attempt to use super when there is no super class.
{ x: super.x } { x: super.x }
-------------------------------------------------
Field "x"
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -4,6 +4,9 @@ RUNTIME ERROR: xxx
{ ["x"]: error "xxx" for x in [1] } { ["x"]: error "xxx" for x in [1] }
-------------------------------------------------
Field "x"
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed. RUNTIME ERROR: Object assertion failed.
------------------------------------------------- -------------------------------------------------
testdata/object_invariant10:1:16-28
{ assert true, assert false }
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,6 +1,8 @@
RUNTIME ERROR: Object assertion failed. RUNTIME ERROR: Object assertion failed.
------------------------------------------------- -------------------------------------------------
testdata/object_invariant11:1:3-15
{ assert false }.x
------------------------------------------------- -------------------------------------------------
testdata/object_invariant11:1:1-19 $ testdata/object_invariant11:1:1-19 $

View File

@ -4,6 +4,9 @@ RUNTIME ERROR: x
{ assert error "x" } { assert error "x" }
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: xxx RUNTIME ERROR: xxx
------------------------------------------------- -------------------------------------------------
testdata/object_invariant14:1:3-22
{ assert false: "xxx" }
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed. RUNTIME ERROR: Object assertion failed.
------------------------------------------------- -------------------------------------------------
testdata/object_invariant2:1:3-15
{ assert false }
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -4,6 +4,9 @@ RUNTIME ERROR: Attempt to use super when there is no super class.
{ x: 5, assert super.x == 5 } { x: 5, assert super.x == 5 }
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed. RUNTIME ERROR: Object assertion failed.
------------------------------------------------- -------------------------------------------------
testdata/object_invariant8:1:9-27
{ x: 5, assert self.x == 4 }
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed. RUNTIME ERROR: Object assertion failed.
------------------------------------------------- -------------------------------------------------
testdata/object_invariant9:1:16-28
{ assert true, assert false }
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed. RUNTIME ERROR: Object assertion failed.
------------------------------------------------- -------------------------------------------------
testdata/object_invariant_plus:1:2-14
{assert false} + {assert true}
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed. RUNTIME ERROR: Object assertion failed.
------------------------------------------------- -------------------------------------------------
testdata/object_invariant_plus2:1:18-30
{assert true} + {assert false}
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: yyy RUNTIME ERROR: yyy
------------------------------------------------- -------------------------------------------------
testdata/object_invariant_plus6:1:27-46
{ assert false: "xxx" } { assert false: "yyy" }
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

13
testdata/stacktrace_assert.golden vendored Normal file
View File

@ -0,0 +1,13 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/stacktrace_assert:1:3-15
{ assert false }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

1
testdata/stacktrace_assert.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
{ assert false }

View File

13
testdata/stacktrace_plussuper.golden vendored Normal file
View File

@ -0,0 +1,13 @@
RUNTIME ERROR: Unexpected type null
-------------------------------------------------
testdata/stacktrace_plussuper:2:7-9 +:
a+: {},
-------------------------------------------------
Field "a"
-------------------------------------------------
During manifestation

5
testdata/stacktrace_plussuper.jsonnet vendored Normal file
View File

@ -0,0 +1,5 @@
local a(input) = input + {
a+: {},
};
a({a: null})

View File

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed. RUNTIME ERROR: Object assertion failed.
------------------------------------------------- -------------------------------------------------
testdata/supersugar8:1:3-16
{ assert self.x } { x +: false }
-------------------------------------------------
Checking object assertions
------------------------------------------------- -------------------------------------------------
During manifestation During manifestation

View File

@ -36,6 +36,10 @@ func (rv *readyValue) evaluate(i *interpreter, sb selfBinding, origBinding bindi
return rv.content, nil return rv.content, nil
} }
func (rv *readyValue) loc() *ast.LocationRange {
return &ast.LocationRange{}
}
// potentialValues // potentialValues
// ------------------------------------- // -------------------------------------
@ -89,7 +93,12 @@ type codeUnboundField struct {
func (f *codeUnboundField) evaluate(i *interpreter, sb selfBinding, origBindings bindingFrame, fieldName string) (value, error) { func (f *codeUnboundField) evaluate(i *interpreter, sb selfBinding, origBindings bindingFrame, fieldName string) (value, error) {
env := makeEnvironment(origBindings, sb) env := makeEnvironment(origBindings, sb)
return i.EvalInCleanEnv(&env, f.body, false) val, err := i.EvalInCleanEnv(&env, f.body, false)
return val, err
}
func (f *codeUnboundField) loc() *ast.LocationRange {
return f.body.Loc()
} }
// Provide additional bindings for a field. It shadows bindings from the object. // Provide additional bindings for a field. It shadows bindings from the object.
@ -110,24 +119,54 @@ func (f *bindingsUnboundField) evaluate(i *interpreter, sb selfBinding, origBind
return f.inner.evaluate(i, sb, upValues, fieldName) return f.inner.evaluate(i, sb, upValues, fieldName)
} }
func (f *bindingsUnboundField) loc() *ast.LocationRange {
return f.inner.loc()
}
// plusSuperUnboundField represents a `field+: ...` that hasn't been bound to an object. // plusSuperUnboundField represents a `field+: ...` that hasn't been bound to an object.
type plusSuperUnboundField struct { type plusSuperUnboundField struct {
inner unboundField inner unboundField
} }
func (f *plusSuperUnboundField) evaluate(i *interpreter, sb selfBinding, origBinding bindingFrame, fieldName string) (value, error) { func (f *plusSuperUnboundField) evaluate(i *interpreter, sb selfBinding, origBinding bindingFrame, fieldName string) (value, error) {
err := i.newCall(environment{}, false)
if err != nil {
return nil, err
}
stackSize := len(i.stack.stack)
defer i.stack.popIfExists(stackSize)
context := "+:"
i.stack.setCurrentTrace(traceElement{
loc: f.loc(),
context: &context,
})
defer i.stack.clearCurrentTrace()
right, err := f.inner.evaluate(i, sb, origBinding, fieldName) right, err := f.inner.evaluate(i, sb, origBinding, fieldName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !objectHasField(sb.super(), fieldName, withHidden) { if !objectHasField(sb.super(), fieldName, withHidden) {
return right, nil return right, nil
} }
left, err := objectIndex(i, sb.super(), fieldName) left, err := objectIndex(i, sb.super(), fieldName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return builtinPlus(i, left, right)
value, err := builtinPlus(i, left, right)
if err != nil {
return nil, err
}
return value, nil
}
func (f *plusSuperUnboundField) loc() *ast.LocationRange {
return f.inner.loc()
} }
// evalCallables // evalCallables

View File

@ -572,6 +572,7 @@ func checkAssertionsHelper(i *interpreter, obj *valueObject, curr uncachedObject
default: default:
panic(fmt.Sprintf("Unknown object type %#v", curr)) panic(fmt.Sprintf("Unknown object type %#v", curr))
} }
} }
func checkAssertions(i *interpreter, obj *valueObject) error { func checkAssertions(i *interpreter, obj *valueObject) error {
@ -611,6 +612,7 @@ type simpleObjectField struct {
// unboundField is a field that doesn't know yet in which object it is. // unboundField is a field that doesn't know yet in which object it is.
type unboundField interface { type unboundField interface {
evaluate(i *interpreter, sb selfBinding, origBinding bindingFrame, fieldName string) (value, error) evaluate(i *interpreter, sb selfBinding, origBinding bindingFrame, fieldName string) (value, error)
loc() *ast.LocationRange
} }
// extendedObject represents an object created through inheritance (left + right). // extendedObject represents an object created through inheritance (left + right).