aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-19 04:42:05 +0100
committereunhhu <[email protected]>2026-10-19 04:42:05 +0100
commit096a70a7cd5a5785b1b52aa8512422b30f014c09 (patch)
tree5d0632f6dccfdd5d83da709b2e8212ab9d1a0451
parent5f223bd45d9279982ec6e4ca334c089c07782639 (diff)
downloadaup-096a70a7cd5a5785b1b52aa8512422b30f014c09.tar.gz
aup-096a70a7cd5a5785b1b52aa8512422b30f014c09.zip
Bytes alone as a state (Pure 028): state x: bytes (= "..." its UTF-8), held as a text state is and written to the module as type 6, its name read as a bytes value and x = ... setting it whole (a string meets bytes), cursors and marks in it, its insert and remove, its start and end refused with a bytes state's diagnostic, a text never set from it; test bytes_state (300 steps of random bytes with 0x00 and 0xff added and cut, started again from the store 15 times, against Python; the same name as a text from the store and by a replace: UTF-8 bytes kept, others anew, and back to bytes what the text held), three diagnostics; the 60 sources the same modules; 002
-rw-r--r--compiler/compile.c27
-rw-r--r--docs/design/002-dsl-direction.md22
-rw-r--r--tests/check.py69
3 files changed, 111 insertions, 7 deletions
diff --git a/compiler/compile.c b/compiler/compile.c
index 942e6a0..0fcaa1b 100644
--- a/compiler/compile.c
+++ b/compiler/compile.c
@@ -222,6 +222,7 @@ typedef struct {
int state_types[STATES], state_line[STATES], state_column[STATES];
bool state_stays[STATES]; /* a mark: a cursor that stays before text inserted at it */
bool state_transient[STATES]; /* held while the app runs, never stored (Pure 018) */
+ bool state_bytes[STATES]; /* a text state of bytes: read and set whole, UTF-8 asked of none (Pure 028) */
int state_rows[STATES]; /* a list of a message's rows: its input + 1, or 0 */
bool state_event[STATES]; /* a let's list (let.NAME): its handlers' alone, found only by its let */
/* Lists the code makes (Pure 027's 2b): a register (bound: Pure's LIST_BIND gives it a list in a record to read) or
@@ -1572,7 +1573,7 @@ static const char *declared_kind(const Compiler *c, const char *name) {
if (used_library(c, c->module, name) >= 0) return "a library";
if (c->module) return library_record(c, c->module, name) >= 0 ? "a record" : NULL; /* fns, records and uses */
int state = state_index(c, name);
- if (state >= 0) return states[c->state_types[state]];
+ if (state >= 0) return c->state_types[state] == STATE_TEXT && c->state_bytes[state] ? "a bytes state" : states[c->state_types[state]];
if (record_index(c, name) >= 0) return "a record";
int input = input_index(c, name);
if (input >= 0) return c->inputs[input].kind == KIND_KEY ? "a key input" :
@@ -1877,8 +1878,15 @@ static void state_declaration(Compiler *c, bool transient) {
c->state_column[index] = name.column;
if (is(c, ":")) {
take(c);
- Token type = expect_name(c, "text, i64, cursor, mark, list or index");
- if (!strcmp(type.text, "i64")) { /* state n: i64 = 0: exact, whole */
+ Token type = expect_name(c, "text, bytes, i64, cursor, mark, list or index");
+ if (!strcmp(type.text, "bytes")) { /* state data: bytes: held as a text is, read and set whole (Pure 028) */
+ c->state_types[index] = STATE_TEXT;
+ c->state_bytes[index] = true;
+ if (is(c, "=")) {
+ take(c);
+ c->state_text[index] = plain_string(c, "a bytes state");
+ }
+ } else if (!strcmp(type.text, "i64")) { /* state n: i64 = 0: exact, whole */
c->state_types[index] = STATE_I64;
if (is(c, "=")) {
take(c);
@@ -2309,6 +2317,8 @@ static uint32_t constant(Compiler *c, const Expr *e) {
static int text_state(Compiler *c, const char *name, int line, int column) {
int index = state_index(c, name);
+ if (index >= 0 && c->state_types[index] == STATE_TEXT && c->state_bytes[index])
+ fail_at(c, line, column, "'%s' is a bytes state: it is read and set whole, no cursor in it", name);
if (index < 0 || c->state_types[index] != STATE_TEXT) {
const char *kind = declared_kind(c, name);
if (kind) fail_at(c, line, column, "'%s' is %s, not a text state", name, kind);
@@ -3445,6 +3455,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
static const uint8_t loads[] = {LOAD, TEXT_LOAD, CUR_LOAD, 0, INDEX_LOAD, LOAD, LOAD};
static const Type types[] = {NUM, TEXT, CURSOR, 0, INDEX, BOOL, I64};
emit(c, e->line, e->column, loads[type], (uint32_t)index, 0);
+ if (type == STATE_TEXT && c->state_bytes[index]) return (Kind){BYTES, -1}; /* a bytes state's value (Pure 028) */
return (Kind){types[type], type == STATE_CURSOR || type == STATE_INDEX ? cursor_owner(c, index) : -1};
}
int input = input_index(c, e->name);
@@ -4744,7 +4755,8 @@ static void statements(Compiler *c, const Stmt *s) {
index_of(c, s->value, c->handling, 0, cursor_owner(c, index));
emit(c, s->line, s->column, INDEX_STORE, (uint32_t)index, 0);
} else if (c->state_types[index] == STATE_TEXT) {
- expect_kind(c, s->value, value(c, s->value, c->handling, 0), TEXT);
+ Type type = c->state_bytes[index] ? BYTES : TEXT;
+ expect_kind(c, s->value, meeting(c, s->value, c->handling, 0, type), type);
emit(c, s->line, s->column, TEXT_SET, (uint32_t)index, 0);
} else {
int owner = cursor_owner(c, index);
@@ -5089,7 +5101,7 @@ static bool text_path(Compiler *c, const char *path) {
int param = parameter(c, path), local = c->scope ? -1 : local_named(c, path), state = c->module ? -1 : state_index(c, path);
if (param >= 0) return c->scope->kinds[param].type == TEXT;
if (local >= 0) return c->locals[local].kind.type == TEXT;
- return state >= 0 && c->state_types[state] == STATE_TEXT;
+ return state >= 0 && c->state_types[state] == STATE_TEXT && !c->state_bytes[state];
}
/* for ch in t: t held by a name of its own, then a count whose block begins if at >= end { break }, takes the character
@@ -5436,7 +5448,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
} else if (c->state_types[i] == STATE_CURSOR || c->state_types[i] == STATE_INDEX) {
bool cursor = c->state_types[i] == STATE_CURSOR;
int target = state_index(c, c->state_target[i]);
- if (target < 0 || c->state_types[target] != (cursor ? STATE_TEXT : STATE_LIST))
+ if (target < 0 || c->state_types[target] != (cursor ? STATE_TEXT : STATE_LIST) || (cursor && c->state_bytes[target]))
fail_at(c, c->state_line[i], c->state_column[i], cursor ? "a cursor moves in a text state; '%s' is not one" :
"an index points into a list state; '%s' is not one", c->state_target[i]);
state_ref[i] = (uint32_t)target;
@@ -5693,7 +5705,8 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
/* Pure's types in a module: a cursor and an index are its anchor (027's 4c), a bool and an i64 then 4 and 5. */
static const uint32_t module_types[] = {[STATE_NUM] = 0, [STATE_TEXT] = 1, [STATE_CURSOR] = 2, [STATE_LIST] = 3,
[STATE_INDEX] = 2, [STATE_BOOL] = 4, [STATE_I64] = 5};
- p = put32(p, module_types[c->state_types[i]] | (c->state_transient[i] ? STATE_TRANSIENT : 0));
+ uint32_t type = c->state_types[i] == STATE_TEXT && c->state_bytes[i] ? 6 : module_types[c->state_types[i]]; /* 6 bytes */
+ p = put32(p, type | (c->state_transient[i] ? STATE_TRANSIENT : 0));
if (c->state_types[i] == STATE_NUM || c->state_types[i] == STATE_BOOL) p = put_double(p, c->initial[i]);
else if (c->state_types[i] == STATE_I64) p = put64(p, (uint64_t)c->state_integer[i]);
else { /* flags: a mark (1: the cursor stays), or a keyed list's key field + 1 */
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md
index d13c595..4f7e855 100644
--- a/docs/design/002-dsl-direction.md
+++ b/docs/design/002-dsl-direction.md
@@ -956,3 +956,25 @@ ys = [4, 5] # let의 리스트를 비우고 �
- 검증: `inner_lists`가 for의 블록, 키 있는 리스트, fn의 for, 보기의 본문에 둔 let 리스트와 `xs = [...]`, `xs = []`를 무작위 100
사건으로 파이썬과 견준다. 진단 하나가 바뀌었다(블록 밖에서 그 리스트를 읽으면 `unknown state`). 그 전에 컴파일되던 소스 60개의
모듈은 바이트까지 같다.
+
+### 바이트 하나인 상태 (Pure 028)
+
+```text
+state pending: bytes # 빈 바이트로 시작한다. state x: bytes = "..."는 그 문자열의 UTF-8로
+input packet = message { data: bytes }
+on packet {
+ pending = pending + packet.data # 읽기는 바이트 값, 쓰기는 통째로
+ require pending.bytes < 65536
+}
+view left = pending # 바이트 보기
+```
+
+- **뜻.** text 상태처럼 담고 저장하지만 UTF-8을 묻지 않는다. 이름은 바이트 값으로 읽힌다(`.bytes`, `[i]`, `[a .. b]`, 비교,
+ 잇기). `x = …`가 바이트를 쓴다. 문자열은 바이트를 만나므로 `x = "ok"`도 된다.
+- **커서가 없다.** `cursor in x`, `mark in x`, `x.insert(…)`, `x.start`는 진단이 나온다. text에 바이트를 넣지도 않는다(`text(b)`로
+ 옮긴다).
+- **이름이 같은 text와 바이트.** 교체와 저장소의 시작에서 정확할 때만 옮긴다: text는 언제나 바이트가 되고, 바이트는 UTF-8일 때만
+ text가 된다(아니면 초기값). Pure의 `values` 기록이 바이트 상태를 적어 손상과 옮김을 가른다.
+- 검증: `bytes_state`가 0x00과 0xff가 든 무작위 바이트를 붙이고 자르며 300걸음을 저장소로 15번 다시 시작하며 파이썬과 견준다. 같은
+ 이름의 text로 저장소에서 시작하고 실행 중에 교체해, UTF-8인 바이트는 남고 아닌 것은 새로 시작하며, 바이트로 돌아오면 text가 가졌던
+ 것을 찾는다. 진단 셋(커서, insert, text에 바이트).
diff --git a/tests/check.py b/tests/check.py
index b999262..fc08708 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -83,6 +83,11 @@ DIAGNOSTICS = [
('record R {\n v = 0\n}\ninput m = message { d: bytes }\nstate t: text\non m {\n let xs: list of R\n'
' t = text(bytes(xs))\n}\n', '8:18', 'bytes(...) makes bytes of a list of i64, not of R'),
('state x = 0\nview v = [1, 2]\n', '2:10', "a list's elements begin a let's list"),
+ # Bytes alone as a state (Pure 028): read and set whole, no cursor in it, no text set from it.
+ ('state data: bytes\nstate at: cursor in data\n', '2:7', "a cursor moves in a text state; 'data' is not one"),
+ ('state data: bytes\ninput k = key KeyK\non k.down {\n data.insert(data.start, "x")\n}\n', '4:3',
+ "'data' is a bytes state: it is read and set whole, no cursor in it"),
+ ('state data: bytes\nstate t: text\ninput k = key KeyK\non k.down {\n t = data\n}\n', '5:7', 'expected text, not bytes'),
# Lets' lists in inner blocks and xs = [...] (after 140's inflate): a stored list is not given elements so.
('record R {\n v = 0\n}\nstate rs: list of R\non frame {\n rs = []\n}\n', '6:3',
"a list changes by append, insert, remove, rs[index].field = value and (a let's) rs = [...]"),
@@ -666,6 +671,22 @@ view inner {
}
'''
+BYTES_STATE = '''state data: bytes
+state n = 0
+input add = message { d: bytes }
+input cut = message { k: u32 }
+on add {
+ data = data + add.d
+ n = data.bytes
+}
+on cut {
+ require cut.k <= data.bytes
+ data = data[0 .. cut.k]
+ n = data.bytes
+}
+view whole = data
+'''
+
ACCEPTED = [
# Views of any value (Pure 027's 4e): a list as it is, filtered, a map with a list and a bytes column, a list a body
# built, bytes.
@@ -1583,6 +1604,53 @@ def nested_values(aup, pure_run, work):
+def bytes_state(aup, pure_run, work, steps=300):
+ """Bytes alone as a state (Pure 028): random bytes (0x00 and 0xff among them) added and cut, the app started again
+ from its store every so often, each step's state and view against Python's; then the same name as a text: a start
+ from the store and a replace while running keep bytes that are UTF-8 and start anew from bytes that are not, and
+ going back to bytes finds what the text held."""
+ import random
+ import struct
+ path = work / 'bytes_state.pmod'
+ (work / 'bytes_state.aup').write_text(BYTES_STATE)
+ compile_module(aup, work / 'bytes_state.aup', path)
+ text_path = work / 'bytes_as_text.pmod'
+ (work / 'bytes_as_text.aup').write_text('state data: text\nstate n = 0\nview whole = data\n')
+ compile_module(aup, work / 'bytes_as_text.aup', text_path)
+ rng = random.Random(28)
+ store, model, starts = work / 'bytes-store', b'', 0
+ for segment in range(steps // 20):
+ commands, wanted = '', []
+ for _ in range(20):
+ if rng.random() < 0.7:
+ d = bytes(rng.choice(b'a\x00\xff\xc3\xa9') for _ in range(rng.randrange(0, 5)))
+ commands += f"message add {d.hex() or '-'}\n"
+ model += d
+ wanted.append((model, 'none'))
+ else:
+ k = rng.randrange(0, len(model) + 3)
+ commands += f"message cut {struct.pack('<I', k).hex()}\n"
+ if k <= len(model): model = model[:k]
+ wanted.append((model, 'none' if k <= len(model) or k == len(model) else 'limit'))
+ lines = run(pure_run, path, '--store', store, stdin=commands, check=False).stdout.splitlines()
+ starts += 1
+ assert len(lines) == len(wanted), (segment, len(lines))
+ for (data, _), line in zip(wanted, lines):
+ state = json.loads(line)
+ assert state['states']['data'] == data.hex() == state['views']['whole'] and state['states']['n'] == len(data), (data, state)
+ # The same name as a text, from the store: UTF-8 bytes kept, others anew (the values record says it was bytes).
+ for given in (b'ok \xc3\xa9', b'no \xff'):
+ store = work / f'bytes-store-{given[0]:02x}'
+ run(pure_run, path, '--store', store, stdin=f"message add {given.hex()}\n")
+ shown = json.loads(run(pure_run, text_path, '--store', store, stdin='show\n').stdout.splitlines()[-1])
+ assert shown['states']['data'] == (given.decode() if given[0] == 0x6f else ''), shown
+ back = json.loads(run(pure_run, path, '--store', store, stdin='show\n').stdout.splitlines()[-1])
+ assert back['states']['data'] == (given.hex() if given[0] == 0x6f else ''), back # what the text held, stored
+ replaced = json.loads(run(pure_run, path, stdin=f"message add {given.hex()}\nreplace {text_path}\nshow\n").stdout.splitlines()[-1])
+ assert replaced['states']['data'] == (given.decode() if given[0] == 0x6f else ''), replaced
+ return {'steps': steps, 'starts': starts}
+
+
def inner_lists(aup, pure_run, work, events=100):
"""Lets' lists in inner blocks, and xs = [...] (after 140's inflate): a let's list in a for's block, a keyed one, one
in a fn's for and one in a view's body begin empty each time their let is run; xs = [...] empties a let's list and
@@ -2801,6 +2869,7 @@ def main():
report['bare_lists'] = bare_lists(aup, pure_run, work)
report['bytes_compared'] = bytes_compared(aup, pure_run, work)
report['inner_lists'] = inner_lists(aup, pure_run, work)
+ report['bytes_state'] = bytes_state(aup, pure_run, work)
report['stored_nested'] = stored_nested(aup, pure_run, work, range(6))
report['documents'] = documents_model(aup, pure_run, work, range(4))
print(json.dumps(report))