mirror of
https://github.com/nim-lang/Nim.git
synced 2026-05-25 06:18:16 +00:00
`setLenUninit(string)` was broken on the legacy refc backend when
growing within existing spare capacity.
`setLengthStrUninit` in `lib/system/sysstr.nim` only updated len when it
had to reallocate or when shrinking.
If oldLen < newLen <= capacity, it returned early without finalizing:
```nim
var s = newStringOfCap(10)
s.add("abc")
s.setLenUninit(6)
doAssert s.len == 6 # used to fail, len stayed 3
```
This escaped `tests/stdlib/tstring.nim` because the testing routine
`checkSetLenUninit` mostly resizes strings created at **exact**
length/capacity, so growth usually took the reallocating branch.
The new regression test covers the missing edge case.
So sorry for catching this only on the day of the stable release! In my
defense, the original PR hung in limbo for quite a while and it didn't
spend enough time in devel after the merge.
262 lines
8.1 KiB
Nim
262 lines
8.1 KiB
Nim
discard """
|
|
matrix: "--backend:c --mm:refc; --backend:c --mm:orc; --backend:c --mm:orc -d:nimsso; --backend:cpp --mm:refc; --backend:cpp --mm:orc; --backend:js --mm:refc; --backend:js --mm:orc"
|
|
"""
|
|
|
|
from std/sequtils import toSeq, map
|
|
from std/sugar import `=>`
|
|
import std/assertions
|
|
|
|
const hasNativeSso = defined(nimsso) and
|
|
(defined(gcArc) or defined(gcAtomicArc) or defined(gcOrc) or defined(gcYrc))
|
|
|
|
proc tester[T](x: T) =
|
|
let test = toSeq(0..4).map(i => newSeq[int]())
|
|
doAssert $test == "@[@[], @[], @[], @[], @[]]"
|
|
|
|
when not hasNativeSso:
|
|
func reverse*(a: string): string =
|
|
result = a
|
|
for i in 0 ..< a.len div 2:
|
|
let j = result.len - i - 1
|
|
swap(result[i], result[j])
|
|
|
|
proc main() =
|
|
block: # ..
|
|
const
|
|
characters = "abcdefghijklmnopqrstuvwxyz"
|
|
numbers = "1234567890"
|
|
|
|
# test "slice of length == len(characters)":
|
|
# replace characters completely by numbers
|
|
var s: string
|
|
s = characters
|
|
s[0..^1] = numbers
|
|
doAssert s == numbers
|
|
|
|
# test "slice of length > len(numbers)":
|
|
# replace characters by slice of same length
|
|
s = characters
|
|
s[1..16] = numbers
|
|
doAssert s == "a1234567890rstuvwxyz"
|
|
|
|
# test "slice of length == len(numbers)":
|
|
# replace characters by slice of same length
|
|
s = characters
|
|
s[1..10] = numbers
|
|
doAssert s == "a1234567890lmnopqrstuvwxyz"
|
|
|
|
# test "slice of length < len(numbers)":
|
|
# replace slice of length. and insert remaining chars
|
|
s = characters
|
|
s[1..4] = numbers
|
|
doAssert s == "a1234567890fghijklmnopqrstuvwxyz"
|
|
|
|
# test "slice of length == 1":
|
|
# replace first character. and insert remaining 9 chars
|
|
s = characters
|
|
s[1..1] = numbers
|
|
doAssert s == "a1234567890cdefghijklmnopqrstuvwxyz"
|
|
|
|
# test "slice of length == 0":
|
|
# insert chars at slice start index
|
|
s = characters
|
|
s[2..1] = numbers
|
|
doAssert s == "ab1234567890cdefghijklmnopqrstuvwxyz"
|
|
|
|
# test "slice of negative length":
|
|
# same as slice of zero length
|
|
s = characters
|
|
s[2..0] = numbers
|
|
doAssert s == "ab1234567890cdefghijklmnopqrstuvwxyz"
|
|
|
|
when nimvm:
|
|
discard
|
|
else:
|
|
# bug #6223
|
|
doAssertRaises(IndexDefect):
|
|
discard s[0..999]
|
|
|
|
block: # ==, cmp
|
|
let world = "hello\0world"
|
|
let earth = "hello\0earth"
|
|
let short = "hello\0"
|
|
let hello = "hello"
|
|
let goodbye = "goodbye"
|
|
|
|
doAssert world == world
|
|
doAssert world != earth
|
|
doAssert world != short
|
|
doAssert world != hello
|
|
doAssert world != goodbye
|
|
|
|
doAssert cmp(world, world) == 0
|
|
doAssert cmp(world, earth) > 0
|
|
doAssert cmp(world, short) > 0
|
|
doAssert cmp(world, hello) > 0
|
|
doAssert cmp(world, goodbye) > 0
|
|
|
|
block: # bug #7816
|
|
tester(1)
|
|
|
|
when not hasNativeSso:
|
|
block: # bug #14497, reverse
|
|
doAssert reverse("hello") == "olleh"
|
|
|
|
block: # len, high
|
|
var a = "ab\0cd"
|
|
doAssert a.len == 5
|
|
doAssert a.high == a.len - 1
|
|
|
|
when not (hasNativeSso and defined(cpp)):
|
|
let b = a.cstring
|
|
block: # bug #16405
|
|
when defined(js):
|
|
when nimvm: doAssert b.len == 2
|
|
else: doAssert b.len == 5
|
|
else: doAssert b.len == 2
|
|
doAssert b.high == b.len - 1
|
|
|
|
doAssert "".len == 0
|
|
doAssert "".high == -1
|
|
when not (hasNativeSso and defined(cpp)):
|
|
doAssert "".cstring.len == 0
|
|
doAssert "".cstring.high == -1
|
|
|
|
block: # bug #16674
|
|
var c: cstring = nil
|
|
doAssert c.len == 0
|
|
doAssert c.high == -1
|
|
|
|
block: # setLen, setLenUninit
|
|
when hasNativeSso:
|
|
const
|
|
alwaysAvail = sizeof(uint) - 1
|
|
payloadSize = sizeof(uint) + sizeof(pointer) - 2
|
|
longStringDataOffset = 3 * sizeof(int)
|
|
|
|
template rawSlenOf(s: string): int =
|
|
int(cast[ptr byte](unsafeAddr s)[])
|
|
|
|
template inlineDataOf(s: string): ptr UncheckedArray[char] =
|
|
cast[ptr UncheckedArray[char]](cast[uint](unsafeAddr s) + 1'u)
|
|
|
|
template longDataOf(s: string): ptr UncheckedArray[char] =
|
|
let ssPtr = cast[ptr tuple[bytes: uint, more: pointer]](unsafeAddr s)
|
|
cast[ptr UncheckedArray[char]](
|
|
cast[uint](ssPtr.more) + uint(longStringDataOffset))
|
|
|
|
proc checkStrInternals(s: string; expectedLen: int) =
|
|
doAssert s.len == expectedLen, "expected " & $expectedLen & ", got " & $s.len
|
|
when nimvm:
|
|
discard
|
|
else:
|
|
when hasNativeSso and not defined(js) and not defined(nimscript):
|
|
# SSO
|
|
let rawSlen = rawSlenOf(s)
|
|
if rawSlen > payloadSize:
|
|
doAssert rawSlen == 255
|
|
let data = longDataOf(s)
|
|
doAssert data[expectedLen] == '\0'
|
|
else:
|
|
doAssert rawSlen == expectedLen
|
|
let data = inlineDataOf(s)
|
|
doAssert data[expectedLen] == '\0'
|
|
if expectedLen < alwaysAvail:
|
|
for i in expectedLen + 1 ..< alwaysAvail:
|
|
doAssert data[i] == '\0'
|
|
elif defined(UncheckedArray): # skip JS
|
|
# string V2
|
|
let cs = s.cstring
|
|
let arr = cast[ptr UncheckedArray[char]](unsafeAddr cs[0])
|
|
doAssert arr[expectedLen] == '\0'
|
|
|
|
proc makeStr(n: int): string =
|
|
result = newStringOfCap(n)
|
|
for i in 0..<n:
|
|
result.add char(ord('a') + i mod 26)
|
|
|
|
proc checkSetLenUninit(oldLen, newLen: int; cmpAfter = -1) =
|
|
## Verifies `setLenUninit`:
|
|
## - preserves the existing prefix
|
|
## - updates the string length
|
|
## - keeps internal null termination valid for both shrink and growth
|
|
##
|
|
## `cmpAfter` is used for layouts where trailing zeroed padding affects
|
|
## string comparison semantics after the resize.
|
|
var s = makeStr(oldLen)
|
|
let prefixLen = min(oldLen, newLen)
|
|
let prefix = makeStr(prefixLen)
|
|
s.setLenUninit(newLen)
|
|
s.checkStrInternals(newLen)
|
|
doAssert s[0..<prefixLen] == prefix
|
|
if newLen <= oldLen:
|
|
doAssert s == prefix
|
|
if cmpAfter >= 0:
|
|
doAssert s < makeStr(cmpAfter)
|
|
|
|
const numbers = "1234567890"
|
|
block setLen:
|
|
# Trim to zero and grow past the old end. Must keep the prefix and zero the tail.
|
|
var s = numbers
|
|
s.setLen(0)
|
|
s.checkStrInternals(0)
|
|
doAssert s == ""
|
|
|
|
s = numbers
|
|
s.setLen(numbers.len + 1)
|
|
s.checkStrInternals(numbers.len + 1)
|
|
doAssert s[0..numbers.high] == numbers
|
|
doAssert s[numbers.len] == '\0'
|
|
|
|
block setLenUninit:
|
|
# Shared baseline for both SSO and V2: noop, shrink, grow.
|
|
checkSetLenUninit(10, 10)
|
|
checkSetLenUninit(10, 5)
|
|
checkSetLenUninit(10, 11)
|
|
|
|
block growingWithinBiggerCapacity:
|
|
# Strings can reserve spare capacity even for short strings.
|
|
# Growing within that capacity must still update len and the trailing zero.
|
|
var s = newStringOfCap(10)
|
|
s.add("abc")
|
|
s.setLenUninit(6)
|
|
s.checkStrInternals(6)
|
|
doAssert s[0..2] == "abc"
|
|
|
|
when hasNativeSso:
|
|
const
|
|
shortLen = alwaysAvail
|
|
medLen = payloadSize
|
|
longLen = payloadSize + 8
|
|
|
|
# Staying short and verify short-compare padding after shrink.
|
|
checkSetLenUninit(shortLen, shortLen - 1, shortLen)
|
|
checkSetLenUninit(shortLen - 2, shortLen - 1)
|
|
checkSetLenUninit(shortLen, 0)
|
|
|
|
# Cross the short/medium boundary in both directions.
|
|
checkSetLenUninit(medLen, medLen - 1)
|
|
checkSetLenUninit(medLen, alwaysAvail - 1, alwaysAvail)
|
|
checkSetLenUninit(alwaysAvail, medLen)
|
|
|
|
# Cross the inline/long boundary in both directions and cover long growth.
|
|
checkSetLenUninit(longLen, longLen - 2)
|
|
checkSetLenUninit(longLen, medLen - 1)
|
|
checkSetLenUninit(longLen, alwaysAvail - 1, alwaysAvail)
|
|
checkSetLenUninit(medLen, longLen)
|
|
checkSetLenUninit(longLen, longLen + 10)
|
|
checkSetLenUninit(longLen, 0)
|
|
|
|
when not defined(js) and not defined(nimscript):
|
|
# shared long strings must not mutate the original when grown
|
|
let src = makeStr(longLen)
|
|
var orig = src
|
|
var copy = orig
|
|
copy.setLenUninit(longLen + 4)
|
|
copy.checkStrInternals(longLen + 4)
|
|
doAssert orig == src
|
|
doAssert copy[0..<longLen] == src
|
|
|
|
static: main()
|
|
main()
|