/development/std/string.nct
string.nct
//! Common owning string type.
//!
//! `str` is the compiler built-in unsized UTF-8 data type. `&str` is the
//! non-owning UTF-8 string slice type and the type of string literals. String is
//! the owning string type. Its storage details are intentionally not exposed
//! outside `std/string`.
//!
//! The public functions are ordinary standard-library APIs. Empty strings,
//! copying from `&str`, capacity reservation, growth, UTF-8 view construction,
//! byte-slice view construction, and release are implemented in Nocter code
//! through restricted runtime pointer primitives.
use std/error.Error
use std/iter.ViewIter
use std/mem.{RawBuffer, TryAllocator, alloc, allocation_abort_raw, current_allocator}
use std/mem.{empty_page_buffer, try_alloc, try_grow_owned}
use std/ptr.copy_str_to_ptr
use std/ptr.str_from_raw_parts
use std/ptr.store_u8_to_ptr
use std/vec.Vec
pub(nocter) primitive bytes_from_str(value: &str): &[u8]
pub struct String {
storage: RawBuffer
len: usize
}
construct String {
/// Copies a static string view into owned storage in the current allocation context.
pub default literal ""(text: &str): Self {
return String.copy(text)
}
pub func empty(): Self {
return String {
storage: empty_page_buffer(1),
len: 0,
}
}
pub func with_capacity(requested_capacity: usize): Self {
var allocator = current_allocator()
return String {
storage: alloc(&+allocator, requested_capacity, 1),
len: 0,
}
}
pub func try_with_capacity(
allocator: &+TryAllocator,
requested_capacity: usize,
): Self! from allocator {
return String {
storage: try_alloc(allocator, requested_capacity, 1)?,
len: 0,
}
}
pub func from_str(value: &str): Self {
let value_len: usize = value.len()
var result = String.with_capacity(value_len)
copy_str_to_ptr(result.storage.ptr, 0, value)
result.len = value_len
return move result
}
pub func copy(value: &str): Self {
return String.from_str(value)
}
pub func try_from_str(
allocator: &+TryAllocator,
value: &str,
): Self! from allocator {
let value_len: usize = value.len()
var result = String.try_with_capacity(allocator, value_len)?
copy_str_to_ptr(result.storage.ptr, 0, value)
result.len = value_len
return move result
}
pub func try_copy(allocator: &+TryAllocator, value: &str): Self! from allocator {
return String.try_from_str(allocator, value)?
}
pub func from_utf8(candidate: &[u8]): Self! {
if !is_valid_utf8(candidate) { return invalid_utf8() }
var result = String.with_capacity(candidate.len())
var offset: usize = 0
while offset < candidate.len() {
store_u8_to_ptr(result.storage.ptr, offset, candidate[offset])
offset = offset + 1
}
result.len = candidate.len()
return move result
}
pub func try_from_utf8(
allocator: &+TryAllocator,
candidate: &[u8],
): Self! from allocator {
if !is_valid_utf8(candidate) { return invalid_utf8() }
var result = String.try_with_capacity(allocator, candidate.len())?
var offset: usize = 0
while offset < candidate.len() {
store_u8_to_ptr(result.storage.ptr, offset, candidate[offset])
offset = offset + 1
}
result.len = candidate.len()
return move result
}
}
coerce String {
/// Exposes the initialized UTF-8 prefix without transferring ownership.
pub &self as &str from self {
return view(self)
}
}
pub func empty(): String {
return String.empty()
}
pub func with_capacity(requested_capacity: usize): String {
return String.with_capacity(requested_capacity)
}
pub func try_with_capacity(
allocator: &+TryAllocator,
requested_capacity: usize,
): String! from allocator {
return String.try_with_capacity(allocator, requested_capacity)?
}
pub func from_str(value: &str): String {
return String.from_str(value)
}
pub func try_from_str(
allocator: &+TryAllocator,
value: &str,
): String! from allocator {
return String.try_from_str(allocator, value)?
}
pub func from_utf8(candidate: &[u8]): String! {
return String.from_utf8(candidate)?
}
pub func try_from_utf8(
allocator: &+TryAllocator,
candidate: &[u8],
): String! from allocator {
return String.try_from_utf8(allocator, candidate)?
}
pub func view(text: &String): &str from text {
return str_from_raw_parts(text.storage.ptr, text.len)
}
pub func len(text: &String): usize {
let text_len: usize = text.len
return text_len
}
pub func capacity(text: &String): usize {
let text_capacity: usize = text.storage.len
return text_capacity
}
pub func is_empty(text: &String): bool {
let text_len: usize = text.len
return text_len == 0
}
pub func try_reserve(text: &+String, additional: usize): void! {
let text_len: usize = text.len
if additional > 18446744073709551615 - text_len {
return capacity_overflow()
}
let required_capacity: usize = text_len + additional
if required_capacity <= text.storage.len {
return
}
try_grow_owned(&+text.storage, required_capacity)?
return
}
pub func reserve(text: &+String, additional: usize): void {
try_reserve(text, additional) catch allocation_error {
return allocation_abort_raw()
}
return
}
pub func clear(text: &+String): void {
text.len = 0
return
}
pub func bytes(value: &str): &[u8] from value {
return bytes_from_str(value)
}
/// Returns whether `candidate` is a well-formed UTF-8 byte sequence.
pub func is_valid_utf8(candidate: &[u8]): bool {
var offset: usize = 0
while offset < candidate.len() {
let leading: u8 = candidate[offset]
if leading < 128 {
offset = offset + 1
} else if leading >= 194 && leading <= 223 {
if offset + 1 >= candidate.len() || !is_continuation(candidate[offset + 1]) {
return false
}
offset = offset + 2
} else if leading >= 224 && leading <= 239 {
if offset + 2 >= candidate.len() {
return false
}
let second: u8 = candidate[offset + 1]
if !is_continuation(second) || !is_continuation(candidate[offset + 2]) {
return false
}
if leading == 224 && second < 160 { return false }
if leading == 237 && second >= 160 { return false }
offset = offset + 3
} else if leading >= 240 && leading <= 244 {
if offset + 3 >= candidate.len() {
return false
}
let second: u8 = candidate[offset + 1]
if !is_continuation(second) || !is_continuation(candidate[offset + 2]) || !is_continuation(candidate[offset + 3]) {
return false
}
if leading == 240 && second < 144 { return false }
if leading == 244 && second >= 144 { return false }
offset = offset + 4
} else {
return false
}
}
return true
}
func is_continuation(byte: u8): bool {
return byte >= 128 && byte <= 191
}
/// Returns the first byte offset of `needle` at or after `start`.
pub func find_from(value: &str, needle: &str, start: usize): usize? {
let haystack: &[u8] = bytes_from_str(value)
let sought: &[u8] = bytes_from_str(needle)
if start > haystack.len() { return none }
if sought.len() == 0 { return start }
var candidate: usize = start
while candidate + sought.len() <= haystack.len() {
var matched: usize = 0
var equal: bool = true
while matched < sought.len() {
let actual: u8 = haystack[candidate + matched]
let expected: u8 = sought[matched]
if actual != expected {
equal = false
}
matched = matched + 1
}
if equal { return candidate }
candidate = candidate + 1
}
return none
}
pub func find(value: &str, needle: &str): usize? {
return find_from(value, needle, 0)?
}
pub func contains(value: &str, needle: &str): bool {
let offset: usize = find(value, needle) otherwise { return false }
return offset <= value.len()
}
pub func starts_with(value: &str, prefix: &str): bool {
let offset: usize = find_from(value, prefix, 0) otherwise { return false }
return offset == 0
}
pub func ends_with(value: &str, suffix: &str): bool {
if suffix.len() > value.len() { return false }
let offset: usize = find_from(value, suffix, value.len() - suffix.len()) otherwise { return false }
return offset == value.len() - suffix.len()
}
/// Splits UTF-8 text into independently owned strings.
///
/// An empty separator is rejected because character-boundary splitting belongs
/// to a future scalar-value iterator rather than byte-oriented search.
pub func split(value: &str, separator: &str): Vec<String>! {
if separator.len() == 0 {
return empty_separator()
}
var result: Vec<String> = Vec.empty()
var part_start: usize = 0
while part_start <= value.len() {
let separator_offset: usize = find_from(value, separator, part_start) otherwise {
result.push(copy_range(value, part_start, value.len()))
break
}
result.push(copy_range(value, part_start, separator_offset))
part_start = separator_offset + separator.len()
}
return move result
}
func copy_range(value: &str, start: usize, end: usize): String {
let source: &[u8] = bytes_from_str(value)
var result = String.with_capacity(end - start)
var offset: usize = start
while offset < end {
store_u8_to_ptr(result.storage.ptr, result.len, source[offset])
result.len = result.len + 1
offset = offset + 1
}
return move result
}
pub func invalid_utf8(): error {
return Error.new("std.string.invalid_utf8", "invalid UTF-8")
}
pub func empty_separator(): error {
return Error.new("std.string.empty_separator", "string separator must not be empty")
}
pub func bytes_iter(text: &String): ViewIter<u8> from text {
return ViewIter.from_view(bytes_from_str(view(text)))
}
pub func try_push_str(text: &+String, value: &str): void! {
let value_len: usize = value.len()
let old_len: usize = text.len
try_reserve(text, value_len)?
copy_str_to_ptr(text.storage.ptr, old_len, value)
text.len = old_len + value_len
return
}
pub func push_str(text: &+String, value: &str): void {
try_push_str(text, value) catch allocation_error {
return allocation_abort_raw()
}
return
}
pub func capacity_overflow(): error {
return Error.new("std.string.capacity_overflow", "string capacity overflow")
}
impl String {
pub method &self.view(): &str from self {
return view(self)
}
pub method &self.len(): usize {
return len(self)
}
pub method &self.capacity(): usize {
return capacity(self)
}
pub method &self.is_empty(): bool {
return is_empty(self)
}
pub method &+self.reserve(additional: usize): void {
reserve(self, additional)
return
}
pub method &+self.try_reserve(additional: usize): void! {
try_reserve(self, additional)?
return
}
pub method &+self.clear(): void {
clear(self)
return
}
pub method &self.bytes(): &[u8] from self {
return bytes_from_str(view(self))
}
pub method &self.find(needle: &str): usize? {
return find(view(self), needle)?
}
pub method &self.contains(needle: &str): bool {
return contains(view(self), needle)
}
pub method &self.starts_with(prefix: &str): bool {
return starts_with(view(self), prefix)
}
pub method &self.ends_with(suffix: &str): bool {
return ends_with(view(self), suffix)
}
pub method &self.split(separator: &str): Vec<String>! {
return split(view(self), separator)?
}
/// Iterates over the exact UTF-8 encoding bytes without allocation.
pub method &self.bytes_iter(): ViewIter<u8> from self {
return bytes_iter(self)
}
pub method &+self.push_str(value: &str): void {
push_str(self, value)
return
}
pub method &+self.try_push_str(value: &str): void! {
try_push_str(self, value)?
return
}
}