From c66cb857f2294620e9d67ea9624496c8f1c7eda1 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Mon, 25 Jan 2021 10:28:06 -0800 Subject: [PATCH] Indicate float conversion due to overflows (#31) As flag to tape that indicates that an integer was converted to float due to int64/uint64 limits. Fixes #25 --- README.md | 22 ++++++++++ parse_json_amd64.go | 12 +++--- parse_json_amd64_test.go | 43 ++++++++++++-------- parse_number_amd64.go | 32 ++++++++++----- parse_number_test.go | 2 +- parsed_json.go | 72 +++++++++++++++++++++++++++++---- parsed_serialize.go | 50 +++++++++++++++++------ stage1_find_marks_amd64.go | 6 +-- stage1_find_marks_amd64_test.go | 6 +-- stage2_build_tape_amd64.go | 38 ++++++++--------- 10 files changed, 204 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index c6db80f..4f13090 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,28 @@ method to get an iterator. There are methods that allow you to retrieve all elements as a single type, []int64, []uint64, float64 and strings. +## Number parsing + +Numbers in JSON are untyped and are returned by the following rules in order: + +* If there is any float point notation, like exponents, or a dot notation, it is always returned as float. +* If number is a pure integer and it fits within an int64 it is returned as such. +* If number is a pure positive integer and fits within a uint64 it is returned as such. +* If the number is valid number it is returned as float64. + +If the number was converted from integer notation to a float due to not fitting inside int64/uint64 +the `FloatOverflowedInteger` flag is set, which can be retrieved using `(Iter).FloatFlags()` method. + +JSON numbers follow JavaScript’s double-precision floating-point format. + +* Represented in base 10 with no superfluous leading zeros (e.g. 67, 1, 100). +* Include digits between 0 and 9. +* Can be a negative number (e.g. -10). +* Can be a fraction (e.g. .5). +* Can also have an exponent of 10, prefixed by e or E with a plus or minus sign to indicate positive or negative exponentiation. +* Octal and hexadecimal formats are not supported. +* Can not have a value of NaN (Not A Number) or Infinity. + ## Parsing NDSJON stream Newline delimited json is sent as packets with each line being a root element. diff --git a/parse_json_amd64.go b/parse_json_amd64.go index d17165a..7559613 100644 --- a/parse_json_amd64.go +++ b/parse_json_amd64.go @@ -42,10 +42,10 @@ func (pj *internalParsedJson) initialize(size int) { pj.Strings = make([]byte, 0, stringsSize) } pj.Strings = pj.Strings[:0] - if cap(pj.containing_scope_offset) < maxdepth { - pj.containing_scope_offset = make([]uint64, 0, maxdepth) + if cap(pj.containingScopeOffset) < maxdepth { + pj.containingScopeOffset = make([]uint64, 0, maxdepth) } - pj.containing_scope_offset = pj.containing_scope_offset[:0] + pj.containingScopeOffset = pj.containingScopeOffset[:0] } func (pj *internalParsedJson) parseMessage(msg []byte) error { @@ -75,8 +75,8 @@ func (pj *internalParsedJson) parseMessageInternal(msg []byte, ndjson bool) (err // Make the capacity of the channel smaller than the number of slots. // This way the sender will automatically block until the consumer // has finished the slot it is working on. - pj.index_chan = make(chan indexChan, indexSlots-2) - pj.buffers_offset = ^uint64(0) + pj.indexChans = make(chan indexChan, indexSlots-2) + pj.buffersOffset = ^uint64(0) var errStage1 error go func() { @@ -89,7 +89,7 @@ func (pj *internalParsedJson) parseMessageInternal(msg []byte, ndjson bool) (err if !unifiedMachine(pj.Message, pj) { err = errors.New("Bad parsing while executing stage 2") // drain the channel until empty - for range pj.index_chan { + for range pj.indexChans { } } wg.Done() diff --git a/parse_json_amd64_test.go b/parse_json_amd64_test.go index 66e2670..197ed9d 100644 --- a/parse_json_amd64_test.go +++ b/parse_json_amd64_test.go @@ -96,7 +96,7 @@ func BenchmarkNdjsonStage1(b *testing.B) { for i := 0; i < b.N; i++ { // Create new channel (large enough so we won't block) - pj.index_chan = make(chan indexChan, 128*10240) + pj.indexChans = make(chan indexChan, 128*10240) findStructuralIndices([]byte(ndjson), &pj) } } @@ -210,24 +210,30 @@ func TestParseNumber(t *testing.T) { expectedD float64 expectedI int64 expectedU uint64 + flags FloatFlags }{ - {"1", TagInteger, 0.0, 1, 0}, - {"-1", TagInteger, 0.0, -1, 0}, - {"10000000000000000000", TagUint, 0.0, 0, 10000000000000000000}, - {"10000000000000000001", TagUint, 0.0, 0, 10000000000000000001}, - {"-10000000000000000000", TagFloat, -10000000000000000000, 0, 0}, - {"1.0", TagFloat, 1.0, 0, 0}, - {"1234567890", TagInteger, 0.0, 1234567890, 0}, - {"9876.543210", TagFloat, 9876.543210, 0, 0}, - {"0.123456789e-12", TagFloat, 1.23456789e-13, 0, 0}, - {"1.234567890E+34", TagFloat, 1.234567890e+34, 0, 0}, - {"23456789012E66", TagFloat, 23456789012e66, 0, 0}, - {"-9876.543210", TagFloat, -9876.543210, 0, 0}, - {"-65.619720000000029", TagFloat, -65.61972000000003, 0, 0}, + {input: "1", wantTag: TagInteger, expectedI: 1}, + {input: "-1", wantTag: TagInteger, expectedI: -1}, + {input: "10000000000000000000", wantTag: TagUint, expectedU: 10000000000000000000}, + {input: "10000000000000000001", wantTag: TagUint, expectedU: 10000000000000000001}, + // math.MinInt64 - 1 + {input: "-9223372036854775809", wantTag: TagFloat, expectedD: -9.223372036854776e+18, flags: FloatOverflowedInteger.Flags()}, + {input: "-10000000000000000000", wantTag: TagFloat, expectedD: -10000000000000000000, flags: FloatOverflowedInteger.Flags()}, + {input: "100000000000000000000", wantTag: TagFloat, expectedD: 100000000000000000000, flags: FloatOverflowedInteger.Flags()}, + // math.MaxUint64 +1 + {input: "18446744073709551616", wantTag: TagFloat, expectedD: 1.8446744073709552e+19, flags: FloatOverflowedInteger.Flags()}, + {input: "1.0", wantTag: TagFloat, expectedD: 1.0}, + {input: "1234567890", wantTag: TagInteger, expectedI: 1234567890}, + {input: "9876.543210", wantTag: TagFloat, expectedD: 9876.543210}, + {input: "0.123456789e-12", wantTag: TagFloat, expectedD: 1.23456789e-13}, + {input: "1.234567890E+34", wantTag: TagFloat, expectedD: 1.234567890e+34}, + {input: "23456789012E66", wantTag: TagFloat, expectedD: 23456789012e66}, + {input: "-9876.543210", wantTag: TagFloat, expectedD: -9876.543210}, + {input: "-65.619720000000029", wantTag: TagFloat, expectedD: -65.61972000000003}, } for _, tc := range testCases { - tag, val := parseNumber([]byte(fmt.Sprintf(`%s:`, tc.input))) + tag, val, flags := parseNumber([]byte(fmt.Sprintf(`%s:`, tc.input))) if tag != tc.wantTag { t.Errorf("TestParseNumber: got: %v want: %v", tag, tc.wantTag) } @@ -246,6 +252,9 @@ func TestParseNumber(t *testing.T) { t.Errorf("TestParseNumber: got: %d want: %d", val, tc.expectedU) } } + if flags != uint64(tc.flags) { + t.Errorf("TestParseNumber flags; got: %d want: %d", flags, tc.flags) + } } } @@ -295,7 +304,7 @@ func TestParseInt64(t *testing.T) { test := &parseInt64Tests[i] t.Run(test.in, func(t *testing.T) { - tag, val := parseNumber([]byte(fmt.Sprintf(`%s:`, test.in))) + tag, val, _ := parseNumber([]byte(fmt.Sprintf(`%s:`, test.in))) if tag != test.tag { // Ignore intentionally bad syntactical errors t.Errorf("TestParseInt64: got: %v want: %v", tag, test.tag) @@ -478,7 +487,7 @@ func TestParseFloat64(t *testing.T) { for i := 0; i < len(atoftests); i++ { test := &atoftests[i] t.Run(test.in, func(t *testing.T) { - tag, val := parseNumber([]byte(fmt.Sprintf(`%s:`, test.in))) + tag, val, _ := parseNumber([]byte(fmt.Sprintf(`%s:`, test.in))) switch tag { case TagEnd: if test.err == nil { diff --git a/parse_number_amd64.go b/parse_number_amd64.go index 561d107..791a9d3 100644 --- a/parse_number_amd64.go +++ b/parse_number_amd64.go @@ -21,6 +21,7 @@ package simdjson import ( + "errors" "math" "strconv" ) @@ -63,14 +64,14 @@ var isNumberRune = [256]uint8{ // parseNumber will parse the number starting in the buffer. // Any non-number characters at the end will be ignored. // Returns TagEnd if no valid value found be found. -func parseNumber(buf []byte) (tag Tag, val uint64) { +func parseNumber(buf []byte) (tag Tag, val, flags uint64) { pos := 0 found := uint8(0) for i, v := range buf { t := isNumberRune[v] if t == 0 { //fmt.Println("aborting on", string(v), "in", string(buf[:i])) - return TagEnd, 0 + return TagEnd, 0, 0 } if t == isEOVFlag { break @@ -78,14 +79,14 @@ func parseNumber(buf []byte) (tag Tag, val uint64) { if t&isMustHaveDigitNext > 0 { // A period and minus must be followed by a digit if len(buf) < i+2 || isNumberRune[buf[i+1]]&isDigitFlag == 0 { - return TagEnd, 0 + return TagEnd, 0, 0 } } found |= t pos = i + 1 } if pos == 0 { - return TagEnd, 0 + return TagEnd, 0, 0 } const maxIntLen = 20 @@ -94,33 +95,42 @@ func parseNumber(buf []byte) (tag Tag, val uint64) { if found&isMinusFlag == 0 { if pos > 1 && buf[0] == '0' { // Integers cannot have a leading zero. - return TagEnd, 0 + return TagEnd, 0, 0 } } else { if pos > 2 && buf[1] == '0' { // Integers cannot have a leading zero after minus. - return TagEnd, 0 + return TagEnd, 0, 0 } } i64, err := strconv.ParseInt(string(buf[:pos]), 10, 64) if err == nil { - return TagInteger, uint64(i64) + return TagInteger, uint64(i64), 0 } + if errors.Is(err, strconv.ErrRange) { + flags |= uint64(FloatOverflowedInteger) + } + if found&isMinusFlag == 0 { u64, err := strconv.ParseUint(string(buf[:pos]), 10, 64) if err == nil { - return TagUint, u64 + return TagUint, u64, 0 + } + if errors.Is(err, strconv.ErrRange) { + flags |= uint64(FloatOverflowedInteger) } } + } else if found&isFloatOnlyFlag == 0 { + flags |= uint64(FloatOverflowedInteger) } if pos > 1 && buf[0] == '0' && isNumberRune[buf[1]]&isFloatOnlyFlag == 0 { // Float can only have have a leading 0 when followed by a period. - return TagEnd, 0 + return TagEnd, 0, 0 } f64, err := strconv.ParseFloat(string(buf[:pos]), 64) if err == nil { - return TagFloat, math.Float64bits(f64) + return TagFloat, math.Float64bits(f64), flags } - return TagEnd, 0 + return TagEnd, 0, 0 } diff --git a/parse_number_test.go b/parse_number_test.go index 36bfc93..d343470 100644 --- a/parse_number_test.go +++ b/parse_number_test.go @@ -31,7 +31,7 @@ func TestNumberIsValid(t *testing.T) { // From: https://stackoverflow.com/a/13340826 var jsonNumberRegexp = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`) isValidNumber := func(s string) bool { - tag, _ := parseNumber([]byte(s)) + tag, _, _ := parseNumber([]byte(s)) return tag != TagEnd } validTests := []string{ diff --git a/parsed_json.go b/parsed_json.go index 2a01ffd..6ac05e9 100644 --- a/parsed_json.go +++ b/parsed_json.go @@ -42,6 +42,32 @@ const STRINGBUFMASK = 0x7fffffffffffff const maxdepth = 128 +// FloatFlags are flags recorded when converting floats. +type FloatFlags uint64 + +// FloatFlag is a flag recorded when parsing floats. +type FloatFlag uint64 + +const ( + // FloatOverflowedInteger is set when number in JSON was in integer notation, + // but under/overflowed both int64 and uint64 and therefore was parsed as float. + FloatOverflowedInteger FloatFlag = 1 << iota +) + +// Contains returns whether f contains the specified flag. +func (f FloatFlags) Contains(flag FloatFlag) bool { + return FloatFlag(f)&flag == flag +} + +// Flags converts the flag to FloatFlags and optionally merges more flags. +func (f FloatFlag) Flags(more ...FloatFlag) FloatFlags { + // We operate on a copy, so we can modify f. + for _, v := range more { + f |= v + } + return FloatFlags(f) +} + type ParsedJson struct { Message []byte Tape []uint64 @@ -63,13 +89,13 @@ type indexChan struct { type internalParsedJson struct { ParsedJson - containing_scope_offset []uint64 - isvalid bool - index_chan chan indexChan - indexesChan indexChan - buffers [indexSlots][indexSize]uint32 - buffers_offset uint64 - ndjson uint64 + containingScopeOffset []uint64 + isvalid bool + indexChans chan indexChan + indexesChan indexChan + buffers [indexSlots][indexSize]uint32 + buffersOffset uint64 + ndjson uint64 } // Iter returns a new Iter. @@ -479,6 +505,34 @@ func (i *Iter) Float() (float64, error) { } } +// FloatFlags returns the float value of the next element. +// This will include flags from parsing. +// Integers are automatically converted to float. +func (i *Iter) FloatFlags() (float64, FloatFlags, error) { + switch i.t { + case TagFloat: + if i.off >= len(i.tape.Tape) { + return 0, 0, errors.New("corrupt input: expected float, but no more values on tape") + } + v := math.Float64frombits(i.tape.Tape[i.off]) + return v, 0, nil + case TagInteger: + if i.off >= len(i.tape.Tape) { + return 0, 0, errors.New("corrupt input: expected integer, but no more values on tape") + } + v := int64(i.tape.Tape[i.off]) + return float64(v), 0, nil + case TagUint: + if i.off >= len(i.tape.Tape) { + return 0, 0, errors.New("corrupt input: expected integer, but no more values on tape") + } + v := i.tape.Tape[i.off] + return float64(v), FloatFlags(i.cur), nil + default: + return 0, 0, fmt.Errorf("unable to convert type %v to float", i.t) + } +} + // Int returns the integer value of the next element. // Integers and floats within range are automatically converted. func (i *Iter) Int() (int64, error) { @@ -771,6 +825,10 @@ func (pj *ParsedJson) writeTapeTagVal(tag Tag, val uint64) { pj.Tape = append(pj.Tape, uint64(tag)<<56, val) } +func (pj *ParsedJson) writeTapeTagValFlags(tag Tag, val, flags uint64) { + pj.Tape = append(pj.Tape, uint64(tag)<<56|flags, val) +} + func (pj *ParsedJson) write_tape_s64(val int64) { pj.writeTapeTagVal(TagInteger, uint64(val)) } diff --git a/parsed_serialize.go b/parsed_serialize.go index 5742161..e479b5e 100644 --- a/parsed_serialize.go +++ b/parsed_serialize.go @@ -22,19 +22,21 @@ import ( "encoding/binary" "errors" "fmt" - "github.com/klauspost/compress/s2" - "github.com/klauspost/compress/zstd" "io" "math" "runtime" "sync" "unsafe" + + "github.com/klauspost/compress/s2" + "github.com/klauspost/compress/zstd" ) const ( - stringBits = 14 - stringSize = 1 << stringBits - stringmask = stringSize - 1 + stringBits = 14 + stringSize = 1 << stringBits + stringmask = stringSize - 1 + serializedVersion = 2 ) // Serializer allows to serialize parsed json and read it back. @@ -189,6 +191,10 @@ func serializeNDStream(dst io.Writer, in <-chan Stream, reuse chan<- *ParsedJson return writeErr } +const ( + tagFloatWithFlag = Tag('e') +) + // Serialize the data in pj and return the data. // An optional destination can be provided. func (s *Serializer) Serialize(dst []byte, pj ParsedJson) []byte { @@ -222,6 +228,7 @@ func (s *Serializer) Serialize(dst []byte, pj ParsedJson) []byte { // - TagObjectEnd, TagArrayEnd: No value stored, derived from start. // - TagInteger, TagUint, TagFloat: 64 bits // - TagString: offset, length stored. + // - tagFloatWithFlag (v2): Contains float parsing flag. // // If there are any values left as tag or value, it is considered invalid. @@ -278,8 +285,6 @@ func (s *Serializer) Serialize(dst []byte, pj ParsedJson) []byte { entry := pj.Tape[off] ntype := Tag(entry >> 56) payload := entry & JSONVALUEMASK - s.tagsBuf[tagsOff] = uint8(ntype) - tagsOff++ switch ntype { case TagString: @@ -303,9 +308,18 @@ func (s *Serializer) Serialize(dst []byte, pj ParsedJson) []byte { s.valuesBuf = append(s.valuesBuf, tmp[:]...) off++ case TagFloat: - binary.LittleEndian.PutUint64(tmp[:], pj.Tape[off+1]) - s.valuesBuf = append(s.valuesBuf, tmp[:]...) - off++ + if payload == 0 { + binary.LittleEndian.PutUint64(tmp[:], pj.Tape[off+1]) + s.valuesBuf = append(s.valuesBuf, tmp[:]...) + off++ + } else { + ntype = tagFloatWithFlag + binary.LittleEndian.PutUint64(tmp[:], entry) + s.valuesBuf = append(s.valuesBuf, tmp[:]...) + binary.LittleEndian.PutUint64(tmp[:], pj.Tape[off+1]) + s.valuesBuf = append(s.valuesBuf, tmp[:]...) + off++ + } case TagNull, TagBoolTrue, TagBoolFalse: // No value. case TagObjectStart, TagArrayStart, TagRoot: @@ -319,6 +333,8 @@ func (s *Serializer) Serialize(dst []byte, pj ParsedJson) []byte { wg.Wait() panic(fmt.Errorf("unknown tag: %d", int(ntype))) } + s.tagsBuf[tagsOff] = uint8(ntype) + tagsOff++ off++ } if tagsOff > 0 { @@ -359,7 +375,7 @@ func (s *Serializer) Serialize(dst []byte, pj ParsedJson) []byte { wg.Wait() // Version - dst = append(dst, 1) + dst = append(dst, serializedVersion) // Size of varints... varInts := binary.PutUvarint(tmp[:], uint64(0)) + @@ -449,7 +465,8 @@ func (s *Serializer) Deserialize(src []byte, dst *ParsedJson) (*ParsedJson, erro if v, err := br.ReadByte(); err != nil { return dst, err - } else if v != 1 { + } else if v > serializedVersion { + // v2 reads v1. return dst, errors.New("unknown version") } @@ -587,6 +604,15 @@ func (s *Serializer) Deserialize(src []byte, dst *ParsedJson) (*ParsedJson, erro dst.Tape[off+1] = binary.LittleEndian.Uint64(values[:8]) values = values[8:] off += 2 + case tagFloatWithFlag: + // Tape contains full value + if len(values) < 16 { + return dst, fmt.Errorf("reading %v: no values left", tag) + } + dst.Tape[off] = binary.LittleEndian.Uint64(values[:8]) + dst.Tape[off+1] = binary.LittleEndian.Uint64(values[8:16]) + values = values[16:] + off += 2 case TagNull, TagBoolTrue, TagBoolFalse, TagEnd: dst.Tape[off] = tagDst off++ diff --git a/stage1_find_marks_amd64.go b/stage1_find_marks_amd64.go index af38326..db414d5 100644 --- a/stage1_find_marks_amd64.go +++ b/stage1_find_marks_amd64.go @@ -74,7 +74,7 @@ func findStructuralIndices(buf []byte, pj *internalParsedJson) bool { for len(buf) > 0 { index := indexChan{} - offset := atomic.AddUint64(&pj.buffers_offset, 1) + offset := atomic.AddUint64(&pj.buffersOffset, 1) index.indexes = &pj.buffers[offset%indexSlots] // In case last index during previous round was stripped back, put it back @@ -125,13 +125,13 @@ func findStructuralIndices(buf []byte, pj *internalParsedJson) bool { index.length -= 1 } - pj.index_chan <- index + pj.indexChans <- index indexTotal += index.length buf = buf[processed:] position -= processed } - close(pj.index_chan) + close(pj.indexChans) // a valid JSON file cannot have zero structural indexes - we should have found something return error_mask == 0 && indexTotal > 0 diff --git a/stage1_find_marks_amd64_test.go b/stage1_find_marks_amd64_test.go index b597f16..a85a64e 100644 --- a/stage1_find_marks_amd64_test.go +++ b/stage1_find_marks_amd64_test.go @@ -138,13 +138,13 @@ func TestFindStructuralIndices(t *testing.T) { } pj := internalParsedJson{} - pj.index_chan = make(chan indexChan, 16) + pj.indexChans = make(chan indexChan, 16) // No need to spawn go-routine since the channel is large enough findStructuralIndices([]byte(demo_json), &pj) ipos, pos := 0, ^uint64(0) - for ic := range pj.index_chan { + for ic := range pj.indexChans { for j := 0; j < ic.length; j++ { pos += uint64((*ic.indexes)[j]) result := fmt.Sprintf("%s%s", strings.Repeat(" ", int(pos)), demo_json[pos:]) @@ -168,7 +168,7 @@ func BenchmarkStage1(b *testing.B) { for i := 0; i < b.N; i++ { // Create new channel (large enough so we won't block) - pj.index_chan = make(chan indexChan, 128) + pj.indexChans = make(chan indexChan, 128) findStructuralIndices([]byte(msg), &pj) } } diff --git a/stage2_build_tape_amd64.go b/stage2_build_tape_amd64.go index 714fb07..911a3bd 100644 --- a/stage2_build_tape_amd64.go +++ b/stage2_build_tape_amd64.go @@ -35,7 +35,7 @@ const retAddressArrayConst = 3 func updateChar(pj *internalParsedJson, idx_in uint64) (done bool, idx uint64) { if pj.indexesChan.index >= pj.indexesChan.length { var ok bool - pj.indexesChan, ok = <-pj.index_chan // Get next element from channel + pj.indexesChan, ok = <-pj.indexChans // Get next element from channel if !ok { done = true // return done if channel closed return @@ -50,7 +50,7 @@ func updateChar(pj *internalParsedJson, idx_in uint64) (done bool, idx uint64) { func updateCharDebug(pj *internalParsedJson, idx_in uint64) (done bool, idx uint64) { if pj.indexesChan.index >= pj.indexesChan.length { var ok bool - pj.indexesChan, ok = <-pj.index_chan // Get next element from channel + pj.indexesChan, ok = <-pj.indexChans // Get next element from channel if !ok { done = true // return done if channel closed return @@ -115,11 +115,11 @@ func parseString(pj *ParsedJson, idx uint64, maxStringSize uint64) bool { } func addNumber(buf []byte, pj *ParsedJson) bool { - tag, val := parseNumber(buf) + tag, val, flags := parseNumber(buf) if tag == TagEnd { return false } - pj.writeTapeTagVal(tag, val) + pj.writeTapeTagValFlags(tag, val, flags) return true } @@ -174,7 +174,7 @@ func unifiedMachine(buf []byte, pj *internalParsedJson) bool { offset := uint64(0) // used to contain last element of containing_scope_offset ////////////////////////////// START STATE ///////////////////////////// - pj.containing_scope_offset = append(pj.containing_scope_offset, (pj.get_current_loc()<>retAddressShift, pj.get_current_loc()+addOneForRoot) pj.write_tape(offset>>retAddressShift, 'r') // r is root // And open a new root - pj.containing_scope_offset = append(pj.containing_scope_offset, (pj.get_current_loc()<>retAddressShift, buf[idx]) pj.annotate_previousloc(offset>>retAddressShift, pj.get_current_loc()) @@ -394,13 +394,13 @@ mainArraySwitch: case '{': // we have not yet encountered ] so we need to come back for it - pj.containing_scope_offset = append(pj.containing_scope_offset, (pj.get_current_loc()<