diff options
| author | eunhhu <[email protected]> | 2026-10-19 04:42:05 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-19 04:42:05 +0100 |
| commit | 096a70a7cd5a5785b1b52aa8512422b30f014c09 (patch) | |
| tree | 5d0632f6dccfdd5d83da709b2e8212ab9d1a0451 | |
| parent | 5f223bd45d9279982ec6e4ca334c089c07782639 (diff) | |
| download | aup-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.c | 27 | ||||
| -rw-r--r-- | docs/design/002-dsl-direction.md | 22 | ||||
| -rw-r--r-- | tests/check.py | 69 |
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)) |
