diff options
| author | eunhhu <[email protected]> | 2026-10-04 13:30:31 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-04 13:30:31 +0100 |
| commit | fafed7edaf36ba18c200d395bd229d1f071cd409 (patch) | |
| tree | 9181a75677c7744c80c03f4609c1b47b50c511d6 | |
| parent | 57804c158610996433dc57b31b6d05938a292ec7 (diff) | |
| download | aup-fafed7edaf36ba18c200d395bd229d1f071cd409.tar.gz aup-fafed7edaf36ba18c200d395bd229d1f071cd409.zip | |
Declare transient state: held while the app runs, never stored (Pure 018)
| -rw-r--r-- | compiler/compile.c | 23 | ||||
| -rw-r--r-- | docs/design/001-language-v0.md | 4 | ||||
| -rw-r--r-- | docs/design/002-dsl-direction.md | 38 | ||||
| -rw-r--r-- | tests/check.py | 61 |
4 files changed, 119 insertions, 7 deletions
diff --git a/compiler/compile.c b/compiler/compile.c index e0d694e..2d66286 100644 --- a/compiler/compile.c +++ b/compiler/compile.c @@ -28,6 +28,7 @@ enum { KIND_KEY = 1, KIND_MESSAGE = 2, KIND_TEXT = 3, KIND_TIMER = 4, KIND_OUTPU FIELD_ROWS = 5, FIELD_BOOL = 6, FIELD_I64 = 7 }; /* A list of a message's rows: filled as the message arrives, emptied when its event ends (Pure's flag). */ enum { LIST_ROWS = 256 }; +enum { STATE_TRANSIENT = 256 }; enum { STATE_NUM = 0, STATE_TEXT = 1, STATE_CURSOR = 2, STATE_LIST = 3, STATE_INDEX = 4, STATE_BOOL = 5, STATE_I64 = 6, UNIT_CHAR = 0, UNIT_LINE = 1 }; /* Pure's view types, record field types (0 a number, 1 a text, 2 a bool, 3 an i64) and what a local holds. */ @@ -151,6 +152,7 @@ typedef struct { char state_names[STATES][NAME], state_target[STATES][NAME]; 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) */ int state_rows[STATES]; /* a list of a message's rows: its input + 1, or 0 */ int state_library[STATES]; /* a list's record's library (LIBRARY.NAME), or 0: the app's */ /* A keyed list: the field named after by, and once resolved, that field + 1. */ @@ -221,7 +223,8 @@ typedef struct { static const char *const reserved[] = {"state", "input", "view", "on", "if", "else", "key", "frame", "dt", "min", "max", "clamp", "message", "by", "bytes", "u32", "reply", "require", "record", "each", "fn", "use", "now", "every", - "floor", "output", "send", "for", "let", "true", "false"}; + "floor", "output", "send", "for", "let", "true", "false", + "transient"}; /* An error in the module being parsed or inlined; inside a fn it names the call. */ static _Noreturn void fail_at(Compiler *c, int line, int column, const char *format, ...) { @@ -1408,10 +1411,11 @@ static Token held_record(Compiler *c, char name[NAME], int *library) { return second; } -static void state_declaration(Compiler *c) { +static void state_declaration(Compiler *c, bool transient) { Token name = expect_name(c, "state name"); declare(c, &name); if (is(c, ":") && lookahead(c)->kind == T_NAME && !strcmp(lookahead(c)->text, "bytes")) { + if (transient) fail_at(c, name.line, name.column, "a keyed collection is stored; only a state can be transient"); take(c); take(c); expect_word(c, "by"); @@ -1423,6 +1427,7 @@ static void state_declaration(Compiler *c) { if (c->states == STATES) fail_at(c, name.line, name.column, "at most 32 states are allowed"); unsigned index = c->states; memcpy(c->state_names[index], name.text, NAME); + c->state_transient[index] = transient; c->state_line[index] = name.line; c->state_column[index] = name.column; if (is(c, ":")) { @@ -1658,13 +1663,18 @@ static void view_arguments(Compiler *c, unsigned view, const Token *name) { } static void declaration(Compiler *c) { - Token kind = expect_name(c, "state, record, input, output, view, fn, use, or on"); + Token kind = expect_name(c, "state, transient, record, input, output, view, fn, use, or on"); if (!strcmp(kind.text, "fn")) { fn_declaration(c); } else if (!strcmp(kind.text, "use")) { use_declaration(c); } else if (!strcmp(kind.text, "state")) { - state_declaration(c); + state_declaration(c, false); + } else if (!strcmp(kind.text, "transient")) { + /* transient state x = 0: the app's while it runs, never stored */ + if (!word(c, "state")) fail_at(c, c->token.line, c->token.column, "expected 'state' after 'transient'"); + take(c); + state_declaration(c, true); } else if (!strcmp(kind.text, "record")) { record_declaration(c); } else if (!strcmp(kind.text, "input")) { @@ -3026,6 +3036,9 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { 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; + if (!c->state_transient[i] && c->state_transient[target]) + fail_at(c, c->state_line[i], c->state_column[i], "'%s' is stored but points into transient '%s'; make it " + "transient too", c->state_names[i], c->state_target[i]); } else if (c->state_types[i] == STATE_LIST) { int record = module_record(c, c->state_library[i], c->state_target[i]); for (unsigned l = 1; record < 0 && l < c->module_count; ++l) /* a library's, written without its name */ @@ -3170,7 +3183,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { p += 40; for (unsigned i = 0; i < c->states; ++i) { p = put32(p, state_name[i]); - p = put32(p, (uint32_t)c->state_types[i]); + p = put32(p, (uint32_t)c->state_types[i] | (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/001-language-v0.md b/docs/design/001-language-v0.md index 7694188..1e11f6b 100644 --- a/docs/design/001-language-v0.md +++ b/docs/design/001-language-v0.md @@ -1,6 +1,6 @@ # aup v0 — 한 언어, 한 모듈 형식 -2026-09-25. 상태: v0a(scalar), v0b(바이트·영속 문서·text·커서), v0c(레코드·리스트·인덱스·조건식·표 보기·fn·use·라이브러리의 레코드·`bool`·`i64`) 구현. +2026-09-25. 상태: v0a(scalar), v0b(바이트·영속 문서·text·커서), v0c(레코드·리스트·인덱스·조건식·표 보기·fn·use·라이브러리의 레코드·`bool`·`i64`·`transient state`) 구현. text와 리스트 문법은 [002](002-dsl-direction.md). 착수 기준: @@ -49,7 +49,7 @@ on frame { # 1/60초 진행. 핸들러 안에서 dt를 쓸 - 선언과 문장은 줄바꿈이나 `;`로 끝난다. 이름은 ASCII `[A-Za-z_][A-Za-z_0-9]*`, 31바이트 이하다. - 예약어: `state input view on if else key frame dt min max clamp message by bytes u32 reply require record each fn use now - every floor output send for let true false`. + every floor output send for let true false transient`. - 키: `Space Enter ArrowLeft ArrowRight ArrowUp ArrowDown KeyA..KeyZ Digit0..Digit9`. 입력 이름과 키는 각각 한 번만 쓴다. 상태·입력·보기의 이름공간은 서로 겹칠 수 없다. - 식: 숫자, `true`·`false`, 상태 이름, `dt`(핸들러 안에서만), 괄호, 단항 `- !`, 이항 `* /`, `+ -`, diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md index f59019f..a226bc8 100644 --- a/docs/design/002-dsl-direction.md +++ b/docs/design/002-dsl-direction.md @@ -303,6 +303,44 @@ view golds = count(u in users where store.rank(u.points) == "gold") - 값을 이름이 아니라 선언으로 맞추는 까닭: 쓰이지 않아 모듈에 없는 다른 라이브러리의 같은 이름 레코드가, 이름만 같아서 리스트에 들어가는 일을 막는다. +### 상태의 수명: transient (구현) + +Pure [018](/srv/git/pure.git)의 1단계다. 지금까지 모든 `state`는 저장됐다. 접속 수, 캐시, 계산의 중간값처럼 다시 시작한 뒤에는 +뜻이 없는 상태도 커밋마다 저장소에 썼다. 이제 값의 수명은 선언이 말한다. + +```text +state visits = 0 # 저장한다: 다시 시작해도 남는다 +transient state online = 0 # 앱이 도는 동안만: 저장하지 않고, 다시 시작하면 초기값 +transient state recent: list of Hit +transient state status: text = "idle" +transient state picked: index of tasks # 저장되는 리스트의 저장되지 않는 선택 +on tap.down { + let n = online + 1 # 이벤트의 값: 이벤트가 끝나면 없다 + online = n +} +``` + +| | 실패한 이벤트 | 저장소 | 다시 시작하면 | 모듈을 바꾸면 | +|---|---|---|---|---| +| `state` | 되돌린다 | 쓴다 | 저장된 값 | 이름으로 잇는다 | +| `transient state` | 되돌린다 | 쓰지 않는다 | 초기값 | 이름으로 잇는다 | +| `let` | 버린다 | 없다 | — | — | + +- **`transient state`** 는 모든 상태 타입에 쓴다(수, bool, i64, text, 커서, 리스트, 인덱스). 보기, 이벤트, 되돌리기, 메모리 예산, + 스냅숏의 읽기에서는 다른 상태와 같다. 다른 것은 저장소뿐이다: 그것만 바꾼 이벤트는 저장소에 아무것도 쓰지 않는다. +- 저장되는 커서나 인덱스는 transient text나 리스트를 가리키지 못한다(다시 시작하면 가리키던 것이 비어 있다). transient 커서나 + 인덱스는 저장되는 것을 가리켜도 된다. +- 모듈의 상태 타입 워드에 비트 하나다(Pure 008). transient가 없는 앱의 모듈은 바이트 그대로 지금과 같다. +- 모듈을 바꿀 때(`aup watch`, 호스트의 SIGHUP) 저장되던 상태를 transient로, 또는 그 반대로 바꿀 수 있다. 저장소에 남은 옛 값은 + 버린 상태의 것처럼 다룬다. +- `transient`는 예약어다. + +검증(`tests/check.py`의 `transients`와 진단 5건, Pure의 `test-module`과 `server_transient`): 같은 앱의 transient 판과 저장 판의 +모듈이 상태 타입 워드 세 바이트만 다르다. 같은 저장 이벤트(`tap`) 사이에 transient만 바꾸는 이벤트와 실패하는 이벤트를 섞은 실행의 +저장소가, 섞지 않은 실행의 저장소와 파일과 바이트까지 같다. 다시 열면 transient는 초기값이고 저장되는 것은 그대로다. 달리는 동안 +모듈을 바꿔 저장되던 것을 transient로, transient를 저장되는 것으로 바꾸면 다시 열었을 때 바뀐 대로다. `pure-serve`에서 transient만 +바꾸는 요청 100개가 저장소의 어떤 파일도 바꾸지 않고, `kill -9` 뒤에 다시 띄우면 transient는 초기값이다. + ### 시간 ```text diff --git a/tests/check.py b/tests/check.py index 79da1ab..aa18217 100644 --- a/tests/check.py +++ b/tests/check.py @@ -77,6 +77,13 @@ DIAGNOSTICS = [ ('input k = key Space\nview v = k\n', '2:10', "'k' is a key input, not a state"), ('state a = 0\nview v = min(a)\n', '2:10', 'min expects 2 arguments'), ('state if = 0\n', '1:7', "'if' is reserved"), + ('state transient = 0\n', '1:7', "'transient' is reserved"), + ('transient x = 0\n', '1:11', "expected 'state' after 'transient'"), + ('transient state docs: bytes by key\n', '1:17', 'a keyed collection is stored; only a state can be transient'), + ('transient state note: text\nstate at: cursor in note\n', '2:7', + "'at' is stored but points into transient 'note'; make it transient too"), + ('record R {\n n = 0\n}\ntransient state rs: list of R\nstate at: index of rs\n', '5:7', + "'at' is stored but points into transient 'rs'; make it transient too"), ('state a = 0 $\n', '1:13', "unexpected character '$'"), ('state a = 1e400\n', '1:11', 'number must be finite'), ('view v = 1 +\n', '1:13', 'expected a number, state, or expression'), @@ -1033,6 +1040,59 @@ def library_records(aup, pure_run, pure, work): 'diagnostics': len(bad)} +def transients(aup, pure_run, work): + """A transient state (Pure 018) is the app's while it runs and never + stored: a run that changes only transient states between the same stored + events leaves the store byte for byte as one that does not, a start + begins them anew, a failed event undoes them, and replacing the module + moves a state between stored and transient by name. Its module differs + from the same app's without transient only in those states' type words.""" + import filecmp + app = ('record Hit {\n n = 0\n}\nstate count = 0\n@state online = 0\n@state recent: list of Hit\n' + 'state hits: list of Hit\n@state picked: index of hits\ninput tap = key Space\ninput peek = key KeyP\n' + 'input bad = key KeyB\n' + 'on tap.down {\n count = count + 1\n online = online + 1\n recent.append(Hit { n: online })\n' + ' hits.append(Hit { n: count })\n picked = hits.last\n}\n' + 'on peek.down {\n online = online + 10\n recent.append(Hit { n: online })\n}\n' + 'on bad.down {\n online = online + 100\n require online < 0\n}\n' + 'view seen = recent.count\nview at = picked.n\n') + (work / 'transient.aup').write_text(app.replace('@', 'transient ')) + (work / 'stored.aup').write_text(app.replace('@', '')) + transient = compile_module(aup, work / 'transient.aup', work / 'transient.pmod') + stored = compile_module(aup, work / 'stored.aup', work / 'stored.pmod') + differ = [i for i in range(len(stored)) if stored[i] != transient[i]] + assert len(stored) == len(transient) and len(differ) == 3 and all(transient[i] == 1 for i in differ), differ + + def states(store, keys): + out = run(pure_run, work / 'transient.pmod', '--store', store, + stdin=''.join(f'down {k}\nup {k}\n' for k in keys) + 'show\n', check=False).stdout.splitlines() + return json.loads(out[-1]) + + plain, mixed = work / 'transient-plain', work / 'transient-mixed' + first = states(plain, ['tap', 'tap', 'tap']) + second = states(mixed, ['peek', 'tap', 'peek', 'bad', 'tap', 'peek', 'tap', 'peek']) + assert first['states']['count'] == second['states']['count'] == 3, (first, second) + assert second['states']['online'] == 43 and second['views']['seen'] == 7, second + compared = filecmp.dircmp(plain, mixed) + assert not compared.left_only and not compared.right_only and not compared.diff_files and not compared.funny_files, \ + (compared.left_only, compared.right_only, compared.diff_files) + for name in compared.common_files: + assert (plain / name).read_bytes() == (mixed / name).read_bytes(), name + again = states(mixed, []) + assert again['states']['count'] == 3 and again['states']['online'] == 0 and again['views']['seen'] == 0, again + assert again['states']['picked'] == 0 and again['views']['at'] == 1, again + # online stored from now on, count transient: replaced while running, then started again. + (work / 'swapped.aup').write_text(app.replace('@', '').replace('state count', 'transient state count') + .replace('state picked', 'transient state picked')) + compile_module(aup, work / 'swapped.aup', work / 'swapped.pmod') + swapped = work / 'transient-swapped' + run(pure_run, work / 'transient.pmod', '--store', swapped, + stdin=f'down tap\nup tap\ndown peek\nreplace {work / "swapped.pmod"}\ndown tap\nup tap\n') + back = json.loads(run(pure_run, work / 'swapped.pmod', '--store', swapped, stdin='show\n').stdout.splitlines()[-1]) + assert back['states']['online'] == 12 and back['states']['count'] == 0 and back['views']['seen'] == 3, back + return {'store_files_compared': len(compared.common_files), 'type_words_differ': len(differ)} + + def stored_runs(aup, pure_run, work, seeds, steps=240): """Declared state is stored state: the list app and the text editor run on a store, restarting (a new pure-run process) at random points, and keep @@ -1732,6 +1792,7 @@ def main(): report['stored_snapshots'], report['stored_restarts'] = stored_runs(aup, pure_run, work, range(args.text_seeds)) report['library_programs'], report['library_diagnostics'] = libraries(aup, pure_run, work) report['library_records'] = library_records(aup, pure_run, args.pure.resolve(), work) + report['transients'] = transients(aup, pure_run, work) report['editor_parity_operations'], report['editor_parity_outcomes'] = \ editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds)) print(json.dumps(report)) |
