From a2a430f6c2d973232a4e995133b372ba877558cf Mon Sep 17 00:00:00 2001 From: eunhhu Date: Fri, 25 Sep 2026 12:09:41 +0100 Subject: Add text states and cursors to Pure modules Module format v3 adds text states, cursors, UTF-8 constants, text inputs and text views. A text state is a runtime-owned draft without a store, so a large text edits as an extent tree. Cursors are byte positions the runtime keeps on character boundaries: insertion pushes cursors at or after the point, removal gathers cursors inside the range, and a failed event returns every text draft and cursor to where it was. The verifier types a cursor by the text it belongs to and rejects editing one text with another's cursor. Operations cover constants, concatenation, shortest round-trip number text, character, line and byte counts, character and column-keeping line moves, insert, remove, line views, whole-text replacement and UTF-8 checked conversion from bytes. Replies of edited documents or texts are copied before they are finished. pure-run gains text input and text output. test-module covers typing Korean and emoji, moves that stop at the ends, line moves, views, rollback, invalid UTF-8 and replacement, text-specific verifier rules, and 20,000 damaged text modules whose cursors always stay on character boundaries under ASan/UBSan. --- prototypes/connections/module-text.c | 174 +++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 prototypes/connections/module-text.c (limited to 'prototypes/connections/module-text.c') diff --git a/prototypes/connections/module-text.c b/prototypes/connections/module-text.c new file mode 100644 index 0000000..5c9efe1 --- /dev/null +++ b/prototypes/connections/module-text.c @@ -0,0 +1,174 @@ +#include "module-text.h" +#include +#include +#include +#include + +enum { CHUNK = 4096 }; + +/* state: pending continuation bytes (bits 0-1) and the allowed range of the + * next one (bits 2-4): 0 any, 1 A0-BF after E0, 2 80-9F after ED, 3 90-BF + * after F0, 4 80-8F after F4. */ +bool pure_utf8_step(unsigned *state, PureSpan span) { + unsigned remaining = *state & 3, range = *state >> 2; + for (size_t i = 0; i < span.size; ++i) { + unsigned char b = span.data[i]; + if (remaining) { + unsigned low = range == 1 ? 0xA0 : range == 3 ? 0x90 : 0x80; + unsigned high = range == 2 ? 0x9F : range == 4 ? 0x8F : 0xBF; + if (b < low || b > high) return false; + --remaining; + range = 0; + } else if (b < 0x80) { + continue; + } else if (b >= 0xC2 && b <= 0xDF) { + remaining = 1; + } else if (b >= 0xE0 && b <= 0xEF) { + remaining = 2; + range = b == 0xE0 ? 1 : b == 0xED ? 2 : 0; + } else if (b >= 0xF0 && b <= 0xF4) { + remaining = 3; + range = b == 0xF0 ? 3 : b == 0xF4 ? 4 : 0; + } else return false; + } + *state = remaining | range << 2; + return true; +} + +bool pure_utf8_complete(unsigned state) { return (state & 3) == 0; } + +bool pure_utf8_valid(PureSpan span) { + unsigned state = 0; + return pure_utf8_step(&state, span) && pure_utf8_complete(state); +} + +size_t pure_utf8_scalars(PureSpan span) { + size_t count = 0; + for (size_t i = 0; i < span.size; ++i) count += (span.data[i] & 0xC0) != 0x80; + return count; +} + +size_t pure_utf8_newlines(PureSpan span) { + size_t count = 0; + for (size_t i = 0; i < span.size; ++i) count += span.data[i] == '\n'; + return count; +} + +size_t pure_format_number(double value, char out[32]) { + if (value == 0) { memcpy(out, "0", 2); return 1; } + if (value == floor(value) && fabs(value) < 1e15) return (size_t)snprintf(out, 32, "%.0f", value); + for (int precision = 1; precision <= 17; ++precision) { + snprintf(out, 32, "%.*g", precision, value); + if (strtod(out, NULL) == value) break; + } + char *exponent = strchr(out, 'e'); + if (exponent) { + char *digits = exponent + 2; /* after the sign */ + while (digits[0] == '0' && digits[1]) memmove(digits, digits + 1, strlen(digits)); + } + return strlen(out); +} + +typedef struct { unsigned char *out; } Copy; + +static bool copy(void *context, PureSpan span) { + Copy *c = context; + memcpy(c->out, span.data, span.size); + c->out += span.size; + return true; +} + +static void read_bytes(const PureDraft *draft, PureVersion version, size_t offset, size_t length, + unsigned char *out) { + Copy c = {out}; + if (length) pure_draft_read_version(draft, version, offset, length, copy, &c); +} + +static size_t forward(const PureDraft *draft, PureVersion version, size_t position) { + if (position >= version.bytes) return version.bytes; + unsigned char lead; + read_bytes(draft, version, position, 1, &lead); + size_t length = lead < 0x80 ? 1 : lead < 0xE0 ? 2 : lead < 0xF0 ? 3 : 4; + return position + length <= version.bytes ? position + length : version.bytes; +} + +static size_t backward(const PureDraft *draft, PureVersion version, size_t position) { + if (!position) return 0; + size_t take = position < 4 ? position : 4; + unsigned char bytes[4]; + read_bytes(draft, version, position - take, take, bytes); + size_t at = take - 1; + while (at && (bytes[at] & 0xC0) == 0x80) --at; + return position - take + at; +} + +/* Saturating: stops at the start or end, so work is bounded by the text size. */ +size_t pure_text_step(const PureDraft *draft, PureVersion version, size_t position, double count) { + for (; count > 0 && position < version.bytes; --count) position = forward(draft, version, position); + for (; count < 0 && position > 0; ++count) position = backward(draft, version, position); + return position; +} + +size_t pure_text_line_start(const PureDraft *draft, PureVersion version, size_t position) { + unsigned char chunk[CHUNK]; + while (position) { + size_t take = position < CHUNK ? position : CHUNK; + read_bytes(draft, version, position - take, take, chunk); + for (size_t i = take; i-- > 0;) + if (chunk[i] == '\n') return position - take + i + 1; + position -= take; + } + return 0; +} + +size_t pure_text_line_end(const PureDraft *draft, PureVersion version, size_t position) { + unsigned char chunk[CHUNK]; + while (position < version.bytes) { + size_t take = version.bytes - position < CHUNK ? version.bytes - position : CHUNK; + read_bytes(draft, version, position, take, chunk); + for (size_t i = 0; i < take; ++i) + if (chunk[i] == '\n') return position + i; + position += take; + } + return version.bytes; +} + +static size_t advance_within(const PureDraft *draft, PureVersion version, size_t position, + size_t columns, size_t limit) { + for (; columns && position < limit; --columns) position = forward(draft, version, position); + return position < limit ? position : limit; +} + +/* Moves keep the column; with no further line the cursor goes to the end or + * the start of the text. */ +size_t pure_text_line_step(const PureDraft *draft, PureVersion version, size_t position, double count) { + size_t start = pure_text_line_start(draft, version, position), column = 0; + for (size_t at = start; at < position; at = forward(draft, version, at)) ++column; + for (; count > 0; --count) { + size_t end = pure_text_line_end(draft, version, position); + if (end == version.bytes) return version.bytes; + size_t next = end + 1; + position = advance_within(draft, version, next, column, pure_text_line_end(draft, version, next)); + } + for (; count < 0; ++count) { + size_t here = pure_text_line_start(draft, version, position); + if (!here) return 0; + size_t previous = pure_text_line_start(draft, version, here - 1); + position = advance_within(draft, version, previous, column, here - 1); + } + return position; +} + +typedef struct { size_t count; unsigned unit; } Count; + +static bool count_span(void *context, PureSpan span) { + Count *c = context; + c->count += c->unit ? pure_utf8_newlines(span) : pure_utf8_scalars(span); + return true; +} + +size_t pure_text_count(const PureDraft *draft, PureVersion version, size_t position, unsigned unit) { + Count c = {0, unit}; + if (position) pure_draft_read_version(draft, version, 0, position, count_span, &c); + return c.count + (unit ? 1 : 0); +} -- cgit v1.2.3