nova-std 0.3.0

The Nova language's standard library sources (internal to the nova toolchain)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
//! Methods on `String`: length, slicing, searching, splitting, joining,
//! trimming and case.
//!
//! Every index and length here counts characters, the Unicode scalar values a
//! `Char` holds, never bytes; `bytes_from_string` gives a string's UTF-8
//! bytes. Most of these methods walk the whole string, so `len` and `char_at`
//! take time in proportion to its length: to visit every character, call
//! `chars` once and index the array it returns. Nothing here knows about
//! grapheme clusters, so a combining accent counts as a character of its own.
//!
//! Trimming, splitting, joining and searching strings, where every index
//! and length counts characters:
//!
//! ```
//! let line = "  name = Nova  "
//! let parts = line.trim().split(" = ")
//! assert_eq(parts.len(), 2)
//! assert_eq(parts[0], "name")
//! assert_eq(parts[1].to_upper(), "NOVA")
//! assert_eq(", ".join(["a", "b", "c"]), "a, b, c")
//!
//! // Indexes and lengths count characters, not bytes.
//! let word = "héllo"
//! assert_eq(word.len(), 5)
//! assert_eq(bytes_from_string(word).len(), 6)
//! assert_eq(word.slice(1, 3), "él")
//! assert_eq(word.index_of("llo").unwrap(), 2)
//! assert(word.starts_with("hé"), "starts_with compares characters")
//! ```

// Nova standard library — strings.
//
// Compiled as an implicit module and glob-imported into every user module, so
// these names need no `import` (see docs/adr/0004-stdlib-compile-model.md).
//
// EVERY index and length here is in CODEPOINTS (Unicode scalar values), never
// bytes. `NovaStr` stores UTF-8 and its own `len` field is a byte count, but
// no part of this module's surface exposes that.
//
// PERFORMANCE CONTRACT, worth reading before using this module in a loop:
// most operations that inspect the string decode the whole thing into a
// `[Char]` first, so `char_at` is O(n), and calling it for each index is
// QUADRATIC. Call `chars()` once and index the resulting array instead.
// `index_of`, `contains` and `join` do not decode: runtime builtins work on
// their bytes. This
// is the same class of rule as the `hash & (cap - 1)` note beside
// `pub trait Hash` in std/core — an invariant a caller has to know.
//
// This is the first inherent `impl String` in the language. Keep it as ONE
// block: whether two inherent impls on the same primitive in different std
// modules is rejected (`E0074` is specified for *trait* impls) or silently
// accepted with one shadowing the other is untested. An inherent method also
// wins by priority over a same-named trait method, so no method here may be
// named `fmt`, `dbg`, `eq`, `ne`, `cmp`, `clone`, `default` or `hash` — those
// are std/core's trait methods on `String` and would be silently shadowed.

// The codepoints `start..end` of `cs` as a string. `end` is exclusive.
// Callers have already validated the range. Neither half of the `n <= 0`
// guard is strictly needed for correctness: at `n == 0` the general path
// below already produces "" on its own (`[' '; 0]` is a legal repeat
// literal, the `0..0` loop is vacuous, and `str_from_chars` of an empty
// array is already ""), so that half is only a fast path skipping the
// allocation. The `n < 0` half is defensive rather than load-bearing today —
// `slice`, `split` and the `trim` family (`trim`, `trim_start`, `trim_end`)
// are this helper's only callers, and all of them already guarantee
// `start <= end` themselves — kept here in case a future caller of this
// module-private helper does not validate as carefully.
fn chars_to_string(cs: [Char], start: Int, end: Int) -> String {
    let n = end - start
    if n <= 0 { return "" }
    // `[' '; n]` needs a filler; every slot is overwritten immediately below,
    // so the space never survives. A repeat literal is the only way to
    // allocate a runtime-length array.
    let mut out = [' '; n]
    for i in 0..n { out[i] = cs[start + i] }
    str_from_chars(out)
}

// Whether `n` occurs in `h` starting exactly at `at`. A needle that would run
// past the end of `h` does not match; an empty needle matches at any `at`
// within `h`, including `h.len()`. Private: `starts_with`, `ends_with` and
// `split` are its only callers.
fn chars_match_at(h: [Char], n: [Char], at: Int) -> Bool {
    // Every call site already guarantees `at >= 0` — `starts_with` passes the
    // literal `0`; `ends_with` passes `h.len() - n.len()` only after checking
    // `n.len() <= h.len()`; `split` only ever steps `i`/`j` upward starting
    // from `0` — so this guard is currently unreachable. It
    // is defensive rather than load-bearing today, kept in case a future
    // caller of this module-private helper does not validate as carefully,
    // matching `chars_to_string`'s analogous `n < 0` guard above.
    if at < 0 { return false }
    if at + n.len() > h.len() { return false }
    let mut k = 0
    while k < n.len() {
        if h[at + k] != n[k] { return false }
        k = k + 1
    }
    true
}

// Whether `c` is whitespace, for the `trim` family.
//
// DELIBERATE APPROXIMATION: this is an explicit list, not Unicode's full
// White_Space property. `Char` cannot currently be asked whether it is
// whitespace, and an exact answer would need a sixth permanent runtime ABI
// symbol, which is not worth it for this increment. It lives in exactly one
// place so there is one line to correct when that changes.
fn char_is_whitespace(c: Char) -> Bool {
    if c == ' ' { return true }
    if c == '\t' { return true }
    if c == '\n' { return true }
    if c == '\r' { return true }
    // The non-ASCII spaces are compared by SCALAR VALUE, not as character
    // literals: `'\u{00A0}'` DOES NOT LEX — the `\u{…}` escape works in a
    // string literal but not in a char literal (verified; it fails with
    // `L0001: unexpected character '`). `char_to_int` is already a std-only
    // builtin, so an std module can compare codepoints directly.
    let v = char_to_int(c)
    if v == 160 { return true } // U+00A0 NO-BREAK SPACE
    if v == 8194 { return true } // U+2002 EN SPACE
    if v == 8195 { return true } // U+2003 EM SPACE
    if v == 12288 { return true } // U+3000 IDEOGRAPHIC SPACE
    false
}

// First index at or after 0 that is not whitespace; `cs.len()` if all of it is.
fn trim_start_index(cs: [Char]) -> Int {
    let mut a = 0
    while a < cs.len() { if char_is_whitespace(cs[a]) { a = a + 1 } else { return a } }
    cs.len()
}

// Exclusive end index of the last non-whitespace codepoint at or after
// `floor`; `floor` if everything from there on is whitespace.
fn trim_end_index(cs: [Char], floor: Int) -> Int {
    let mut b = cs.len()
    while b > floor { if char_is_whitespace(cs[b - 1]) { b = b - 1 } else { return b } }
    floor
}

impl String {
    // Not `self.len() == 0`: `len()` decodes the whole string, while `==` on
    // `String` compares byte lengths first in the runtime.
    /// Returns `true` if the string is empty, `""`.
    pub fn is_empty(self) -> Bool { self == "" }

    // Codepoints, not bytes. O(n): the runtime walks the UTF-8.
    /// Returns the number of characters in the string, not its bytes.
    ///
    /// It walks the whole string to count them.
    pub fn len(self) -> Int { str_len_chars(self) }

    // The string's codepoints. Prefer this over repeated `char_at` — see the
    // performance contract in the module header.
    /// Returns the string's characters, in order, as an array.
    pub fn chars(self) -> [Char] { str_chars(self) }

    // The codepoint at `i`, or `None` when `i` is outside `0..len()`. A
    // negative index is `None` rather than a panic, matching `Vec::get`.
    /// Returns the character at index `i`, or `None` if `i` is negative or not
    /// less than `len()`.
    ///
    /// It decodes the whole string to find the character, so a loop calling
    /// it for every index takes time in proportion to the square of the
    /// length. To visit every character, call `chars` once and index its
    /// array.
    pub fn char_at(self, i: Int) -> Option<Char> {
        if i < 0 { return None }
        let cs = str_chars(self)
        if i >= cs.len() { None } else { Some(cs[i]) }
    }

    // Codepoints `start..end`, with `end` EXCLUSIVE (half-open, like every
    // other index range in the language). `start == end` yields "".
    // Panics rather than returning an Option, matching `Vec::set` — an index
    // that must be valid is a caller bug, whereas `char_at`'s query is not.
    /// Returns the characters from index `start` up to, but not including,
    /// `end`.
    ///
    /// `start == end` gives `""`. Panics if `start` is negative, if `end` is
    /// greater than `len()`, or if `start` is greater than `end`.
    pub fn slice(self, start: Int, end: Int) -> String {
        let cs = str_chars(self)
        if start < 0 { panic("String::slice start is negative") }
        if end > cs.len() { panic("String::slice end is past the end of the string") }
        if start > end { panic("String::slice start is after end") }
        chars_to_string(cs, start, end)
    }

    // Reverses CODEPOINTS, so a combining accent detaches from the character
    // it modifies. That is inherent to codepoint-level operations and is why
    // this module is not grapheme-aware (see the design doc, decision D1).
    //
    // The `n == 0` guard is not required for correctness — the general path
    // below already produces "" for empty input on its own (`[' '; 0]` is a
    // legal repeat literal, the `0..n` loop is then vacuous, and
    // `str_from_chars` of an empty array is already "") — it exists purely
    // as a fast path that skips the allocation for the empty string.
    /// Returns the string with its characters in reverse order.
    ///
    /// It reverses characters, not grapheme clusters, so a combining accent
    /// comes apart from the character it modifies.
    pub fn reverse(self) -> String {
        let cs = str_chars(self)
        let n = cs.len()
        if n == 0 { return "" }
        let mut out = [' '; n]
        for i in 0..n { out[i] = cs[n - 1 - i] }
        str_from_chars(out)
    }

    // Whether `self` begins with `prefix`, codepoint-for-codepoint. An empty
    // `prefix` always matches: `chars_match_at` accepts `at == 0` trivially
    // when `n` is empty.
    /// Returns `true` if the string begins with `prefix`. An empty `prefix`
    /// always matches.
    pub fn starts_with(self, prefix: String) -> Bool {
        chars_match_at(str_chars(self), str_chars(prefix), 0)
    }

    // Whether `self` ends with `suffix`, codepoint-for-codepoint. Guards
    // `n.len() > h.len()` before subtracting so `h.len() - n.len()` never
    // goes negative; an empty `suffix` still matches, since the subtraction
    // then lands on `h.len()` itself, which `chars_match_at` accepts.
    /// Returns `true` if the string ends with `suffix`. An empty `suffix`
    /// always matches.
    pub fn ends_with(self, suffix: String) -> Bool {
        let h = str_chars(self)
        let n = str_chars(suffix)
        if n.len() > h.len() { return false }
        chars_match_at(h, n, h.len() - n.len())
    }

    // Codepoint index of the FIRST occurrence, or `None`. An empty needle
    // occurs at every position, so it reports `Some(0)`. A runtime builtin
    // searches the bytes; it used to build a `[Char]` for each string first.
    /// Returns the character index at which `needle` first occurs, or `None`
    /// if it does not occur.
    ///
    /// An empty `needle` occurs at index 0.
    pub fn index_of(self, needle: String) -> Option<Int> {
        let at = str_index_of(self, needle)
        if at < 0 { return None }
        Some(at)
    }

    // Whether `needle` occurs anywhere in `self`. An empty `needle` always
    // matches, since it occurs at `0`. Calls the builtin directly, so a
    // search allocates no `Option`.
    /// Returns `true` if `needle` occurs in the string. An empty `needle`
    /// always does.
    pub fn contains(self, needle: String) -> Bool { str_index_of(self, needle) >= 0 }

    // Split on every non-overlapping occurrence of `sep`, left to right.
    //
    // No separator found yields a ONE-element array holding the whole string,
    // never an empty array. Adjacent, leading and trailing separators each
    // produce an empty piece — nothing is collapsed or trimmed. An EMPTY
    // separator splits into single codepoints (the JavaScript behaviour;
    // Rust adds boundary empties and Python raises, so there is no consensus
    // to inherit), and `"".split("")` is `[]`.
    /// Splits the string at each occurrence of `sep`, and returns the pieces
    /// in order.
    ///
    /// Occurrences are found from left to right and do not overlap. Nothing
    /// is collapsed or trimmed: two adjacent separators, or one at either end,
    /// give an empty piece. A string without `sep` gives one piece, the whole
    /// string, so `"".split(",")` is `[""]`. An empty `sep` splits the string
    /// into its characters, and `"".split("")` is `[]`.
    ///
    /// Nothing is collapsed or trimmed:
    ///
    /// ```
    /// // Nothing is collapsed or trimmed: adjacent separators give an empty piece.
    /// let pieces = "a,,b,".split(",")
    /// assert_eq(pieces.len(), 4)
    /// assert_eq(pieces[1], "")
    /// assert_eq(pieces[3], "")
    /// assert_eq(",".join(pieces), "a,,b,") // `join` undoes `split`
    ///
    /// assert_eq("".split(",").len(), 1) // one piece: the empty string
    /// assert_eq("abc".split("").len(), 3) // an empty separator gives the characters
    /// assert_eq("".split("").len(), 0)
    /// ```
    pub fn split(self, sep: String) -> [String] {
        let h = str_chars(self)
        let s = str_chars(sep)
        if s.len() == 0 {
            if h.len() == 0 { return [] }
            let mut single = [""; h.len()]
            for i in 0..h.len() { single[i] = chars_to_string(h, i, i + 1) }
            return single
        }
        // Pass 1: count the pieces. One more piece than separators found.
        let mut pieces = 1
        let mut i = 0
        while i + s.len() <= h.len() {
            if chars_match_at(h, s, i) {
                pieces = pieces + 1
                i = i + s.len()
            } else {
                i = i + 1
            }
        }
        // Pass 2: fill, walking the same way so the two passes agree.
        let mut out = [""; pieces]
        let mut w = 0
        let mut start = 0
        let mut j = 0
        while j + s.len() <= h.len() {
            if chars_match_at(h, s, j) {
                out[w] = chars_to_string(h, start, j)
                w = w + 1
                j = j + s.len()
                start = j
            } else {
                j = j + 1
            }
        }
        out[w] = chars_to_string(h, start, h.len())
        out
    }

    // Join `parts` with `self` between them: `",".join(parts)`. The separator
    // is the receiver rather than this being a free `join(parts, sep)`,
    // because a top-level `pub fn` is glob-imported into every module and
    // would take the name `join` from all user code.
    //
    // A runtime builtin copies the parts' bytes straight into one result. It
    // used to walk every part character by character into a `[Char]`.
    /// Returns the strings of `parts` joined into one, with this string
    /// between each pair: `",".join(parts)`.
    ///
    /// An empty `parts` gives `""`.
    pub fn join(self, parts: [String]) -> String { str_join(self, parts) }

    // Trims whitespace (see `char_is_whitespace`) from both ends. An
    // all-whitespace string, including "", yields "".
    /// Returns the string without the whitespace at its start and end.
    ///
    /// A string of whitespace alone gives `""`. Whitespace here is, for now,
    /// a space, a tab, `\n`, `\r`, and the no-break, en, em and ideographic
    /// spaces (U+00A0, U+2002, U+2003 and U+3000), not the rest of Unicode's
    /// whitespace: a form feed, say, stays.
    pub fn trim(self) -> String {
        let cs = str_chars(self)
        let a = trim_start_index(cs)
        chars_to_string(cs, a, trim_end_index(cs, a))
    }

    // Trims whitespace from the start only.
    /// Returns the string without the whitespace at its start, as `trim`
    /// defines whitespace.
    pub fn trim_start(self) -> String {
        let cs = str_chars(self)
        chars_to_string(cs, trim_start_index(cs), cs.len())
    }

    // Trims whitespace from the end only.
    /// Returns the string without the whitespace at its end, as `trim`
    /// defines whitespace.
    pub fn trim_end(self) -> String {
        let cs = str_chars(self)
        chars_to_string(cs, 0, trim_end_index(cs, 0))
    }

    // `n` copies of `self`. Builds one array rather than concatenating, both
    // because `String` has no `+` (E0013) and because repeated interpolation
    // would be quadratic.
    /// Returns `n` copies of the string, joined end to end.
    ///
    /// `n == 0` gives `""`. Panics if `n` is negative.
    pub fn repeat(self, n: Int) -> String {
        if n < 0 { panic("String::repeat count must not be negative") }
        let cs = str_chars(self)
        let total = cs.len() * n
        if total == 0 { return "" }
        let mut out = [' '; total]
        let mut w = 0
        for r in 0..n {
            for i in 0..cs.len() {
                out[w] = cs[i]
                w = w + 1
            }
        }
        str_from_chars(out)
    }

    // Full Unicode case mapping, so the result may be LONGER than the input
    // in codepoints: `"ß".to_upper()` is `"SS"`.
    /// Returns the string in upper case, by Unicode's full case mapping.
    ///
    /// The result can have more characters than the string: `"ß".to_upper()`
    /// is `"SS"`.
    pub fn to_upper(self) -> String { str_to_upper(self) }

    // Full Unicode case mapping, for the same reason as `to_upper`: the
    // result may differ in codepoint count from the input, just in the other
    // direction — `"İ".to_lower()` is `"i"` followed by a combining dot
    // above (U+0307), 2 codepoints from 1.
    /// Returns the string in lower case, by Unicode's full case mapping.
    ///
    /// The result can have more characters than the string: `"İ".to_lower()`
    /// is `i` followed by a combining dot above, U+0307.
    pub fn to_lower(self) -> String { str_to_lower(self) }
}