aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--compiler/compile.c23
-rw-r--r--docs/design/001-language-v0.md4
-rw-r--r--docs/design/002-dsl-direction.md38
-rw-r--r--tests/check.py61
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))