Repository navigation
Make string primitives count Unicode code points (UTF-8) - #82
Merged
Merged
Conversation
Shen strings are now sequences of Unicode code points held as UTF-8: - n->string encodes any code point 0..U+10FFFF except surrogates and raises an error otherwise (was string.char: 0..255, with 128..255 returned as a lone invalid byte). - string->n, pos, tlstr, hdstr, explode, shen.string->bytes, shen.str->bytes and the native hash count characters. ASCII text stays on the byte fast path; the UTF-8 path runs only when a byte >= 0x80 is present. The inline POS/TLSTR/STRN/NSTR helpers check the bytes involved and fall back to the primitive otherwise. - read-file decodes source as UTF-8 into code points before parsing, and shen.my-read-byte (behind read/lineread) reads one UTF-8 character, so non-ASCII literals read as their characters instead of mojibake. read-byte, write-byte and read-file-as-bytelist stay byte primitives. - pr writes strings through shen.write-string (char-stoutput? is true), since write-chars cannot carry a code point through write-byte. - Invalid UTF-8 is lenient and lossless: each ill-formed byte is one character whose code is the byte value. Documented in prims.lua and the README. - Bump the kernel cache format to SHENKC5. fasl and stdlib image keys fold the kernel key, so caches built over the byte primitives are invalidated. Adds test/utf8_strings_spec.lua and a Bifrost suite (bifrost/bifrost.suite.json) for non-ASCII strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHL2X2i5C1mWW2f5ypuSaw
string.find with a pattern is not compiled by LuaJIT 2.1 and aborts any trace that reaches it, which made an ASCII pos loop about 1000x slower. The ASCII checks are now plain string.byte loops, and pos remembers the first non-ASCII byte of the last string it scanned, so a loop indexing one string scans it once. compiler.lua lowers (string->n (pos S I)) and (string->n (hdstr S)) to a new STRNPOS helper that reads the character code in place instead of allocating a one-character string. The guard STRN needs for non-ASCII input otherwise keeps that string alive in a snapshot, which stops LuaJIT from sinking the allocation. Also updates the yggdrasil-build.lua comments: shen.write-string and shen.read-unit-string are prims.lua entries now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHL2X2i5C1mWW2f5ypuSaw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #80.
What changes
The string primitives used to treat a Shen string as raw Lua bytes, so they broke on any character outside ASCII. Now a string is a sequence of Unicode code points, stored as UTF-8:
(n->string N)accepts whole numbers from 0 to 0x10FFFF, except surrogates, and errors on anything else.string->n,pos,tlstr,hdstr,explodeandstring-lengthcount characters, not bytes.hashhashes code points, so it agrees with=."Zürich"reads as 6 characters and prints back unchanged.string->nreturns its byte value.(cn (hdstr S) (tlstr S))always gives backS. This is documented in the new README section "Strings and Unicode".read-byte,write-byteandread-file-as-byteliststill work on raw bytes.ASCII-only strings stay on a plain byte fast path.
compiler.luanow compiles(string->n (pos S I))and(string->n (hdstr S))toSTRNPOS, which reads the code without allocating a one-character string. The kernel cache format moves fromSHENKC4toSHENKC5, which invalidates the old kernel, fasl and stdlib-image caches.Files:
prims.lua,compiler.lua,boot.lua,README.md,bin/yggdrasil-build.lua(comments only), the newtest/utf8_strings_spec.lua(122 checks), and a newbifrost/suite withunicode-codepointsandunicode-literalsprograms.Tests
run-kernel-tests.luatest/utf8_strings_spec.luamake testThe 59 Lua 5.4 failures also happen on
main(fc97577): they are in specs that need LuaJIT.Bifrost run on
--impls shen-lua,shen-go: shen-lua passes 7/7. shen-go fails 4 cases because of its own behaviour: it writes characters above 255 as raw bytes, uses a non-kernel string hash, and reads source as Latin-1.Downstream check: Yggdrasil's (
a40d8c5)luabuild of a real rule set used to fail withbad argument #1 to 'cf.rune'. It now passesparity(58 checks).Performance
Medians of interleaved runs with
bench/ab/run.py,mainagainst this branch:An ASCII loop that calls
pos4 million times takes 0.010 s against 0.007 s onmain, about 0.75 ns more per call.How other ports compare
On strings built with
n->string, shen-lua, shen-go and Tarver's S42 SBCL build all agree. Tarver's build and shen-go read source files as Latin-1, so a literal"ü"becomes two characters there. This PR deliberately reads UTF-8 instead.Open questions for review
string->nreject strings longer than one character, as Tarver's build does? It still returns the first character's code.n->stringon a surrogate return U+FFFD, as Tarver's build does? It errors here.🤖 Generated with Claude Code
https://claude.ai/code/session_01DHL2X2i5C1mWW2f5ypuSaw
Generated by Claude Code