diff options
| -rw-r--r-- | core/module-run.h | 1 | ||||
| -rw-r--r-- | core/module.c | 51 | ||||
| -rw-r--r-- | core/module.h | 8 | ||||
| -rw-r--r-- | core/test-module.c | 59 | ||||
| -rw-r--r-- | docs/design/008-module-format.md | 6 | ||||
| -rw-r--r-- | docs/design/027-computation-core.md | 61 | ||||
| -rw-r--r-- | research/experiments/040-against-c/2026-10-18-counts-s30.jsonl | 4 | ||||
| -rw-r--r-- | research/experiments/041-batches/2026-10-18-counts.jsonl | 10 | ||||
| -rw-r--r-- | serve/message-json.c | 66 | ||||
| -rw-r--r-- | serve/message-json.h | 6 | ||||
| -rw-r--r-- | tests/check.py | 128 |
11 files changed, 359 insertions, 41 deletions
diff --git a/core/module-run.h b/core/module-run.h index 4ae94a4..2ad87c6 100644 --- a/core/module-run.h +++ b/core/module-run.h @@ -102,6 +102,7 @@ typedef struct { PureValue *locals; /* the event's locals (a handler's, and what runs in it), or NULL */ bool arguments; /* FIELD reads the arguments given for a view, not the message */ bool modified, exists; + bool nested; /* its rows made some of the event's lists in records (027's 4a): the event begins with them */ uint32_t (*spans)[2]; /* a message's variable fields: start and size in the payload */ } Event; diff --git a/core/module.c b/core/module.c index 6804a4c..03cc434 100644 --- a/core/module.c +++ b/core/module.c @@ -1822,16 +1822,14 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u] == PURE_RECORD_BOOL))) || (out->state_type[i] == PURE_STATE_INDEX && !state_of(out, out->state_ref[i], PURE_STATE_LIST))) REFUSE("state reference", i); - /* A list in a record is an event's alone (027's stage 2b): no stored, transient or message's list holds one, and no - * list is keyed by one; a register is no message's rows. */ + /* No list is keyed by a list in its records; a register is no message's rows. */ for (uint32_t i = 0; i < out->states; ++i) { if (out->state_type[i] != PURE_STATE_LIST) continue; if (out->state_key[i] && out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u] == PURE_RECORD_LIST) REFUSE("list in a record as a key", i); } for (uint32_t f = 0; f < out->fields; ++f) - if (out->field[f].type == PURE_FIELD_ROWS && - (out->record_nested[out->state_ref[out->field[f].rows]] || out->state_flags[out->field[f].rows] & PURE_LIST_BOUND >> 8)) + if (out->field[f].type == PURE_FIELD_ROWS && out->state_flags[out->field[f].rows] & PURE_LIST_BOUND >> 8) REFUSE("rows field", f); /* A stored position into what is not stored would come back meaning nothing. */ for (uint32_t i = 0; i < out->states; ++i) @@ -2518,28 +2516,31 @@ static unsigned char *value_put(const PureApp *app, uint32_t record, uint64_t li return out; } -/* A list value's stored form (bytes) read into a new one of the event's lists of record's items, its lists in fields - * too: its number (0: none), or 0 with *made false where the bytes are not that form or memory ran out (*bad: they are - * not). */ -static uint64_t value_take(PureApp *app, uint32_t record, const unsigned char *bytes, size_t size, bool *made, bool *bad) { +/* A list value's stored form (bytes) read into a new one of the event's lists of record's items (or appended to into, a + * message's rows), its lists in fields too: its number (0: none, or into), or 0 with *made false where the bytes are not + * that form or memory ran out (*bad: they are not). */ +static uint64_t value_take(PureApp *app, uint32_t record, const unsigned char *bytes, size_t size, PureList *into, bool *made, + bool *bad) { const PureModule *m = app->module; const PureRecord *r = &m->record[record]; *made = true; if (!size) return 0; - uint64_t list = nest(app, record, NULL); - if (!list) return *made = false, 0; + uint64_t list = into ? 0 : nest(app, record, NULL); + if (!into && !list) return *made = false, 0; for (size_t at = 0; at < size;) { PureCell cells[PURE_RECORD_FIELDS]; for (unsigned f = 0; f < r->count; ++f) { uint32_t field = r->first + f; uint8_t type = m->record_field_type[field]; if (type == PURE_RECORD_TEXT || type == PURE_RECORD_LIST) { - uint32_t length = size - at >= 4 ? read32(bytes + at) : UINT32_MAX; + if (size - at < 4) return *bad = true, *made = false, 0; + uint32_t length = read32(bytes + at); if (length > size - at - 4 || (type == PURE_RECORD_TEXT && !pure_utf8_valid((PureSpan){bytes + at + 4, length}))) return *bad = true, *made = false, 0; cells[f] = (PureCell){.text = (unsigned char *)bytes + at + 4, .size = length}; if (type == PURE_RECORD_LIST) { - cells[f] = (PureCell){.i64 = (int64_t)value_take(app, nested_record(m, field), bytes + at + 4, length, made, bad)}; + cells[f] = (PureCell){.i64 = (int64_t)value_take(app, nested_record(m, field), bytes + at + 4, length, NULL, made, + bad)}; if (!*made) return 0; } at += 4 + length; @@ -2550,7 +2551,7 @@ static uint64_t value_take(PureApp *app, uint32_t record, const unsigned char *b if (!pure_cell_fits(pure_record_kind(m, field), cells[f])) return *bad = true, *made = false, 0; } } - PureList *items = app->nested.list[list - 1]; /* (the lists in its fields may have moved the table) */ + PureList *items = into ? into : app->nested.list[list - 1]; /* (the lists in its fields may have moved the table) */ if (pure_list_insert(items, pure_list_count(items), cells, NULL) != PURE_OK) return *made = false, 0; if (m->record_nested[record]) share_cells(app, m, record, cells); } @@ -2604,7 +2605,8 @@ static __attribute__((cold, noinline)) PureResult kept_value(PureApp *app, uint3 int64_t *out) { const PureModule *m = app->module; bool made, bad = false; - uint64_t value = value_take(app, nested_record(m, m->record[m->state_ref[list]].first + field), cell.text, cell.size, &made, &bad); + uint32_t record = nested_record(m, m->record[m->state_ref[list]].first + field); + uint64_t value = value_take(app, record, cell.text, cell.size, NULL, &made, &bad); if (!made) return bad ? PURE_STORE_ERROR : PURE_ALLOCATION; if (!grows(app, (Held){0, 0}, (Held){0, cell.size})) return PURE_BUDGET; *out = (int64_t)value; @@ -5118,7 +5120,7 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) { app->grown = app->spent = 0; app->room = app->memory > PURE_GROWTH_DEFAULT ? app->memory : PURE_GROWTH_DEFAULT; for (unsigned i = 0; m->held && i < PURE_MODULE_HELD; ++i) app->held[i].size = 0, app->held[i].by_ref = false; - if (app->nested.count) unbind(app), drop_nested(app); /* a view's body bound some on the app itself */ + if (app->nested.count && !ev->nested) unbind(app), drop_nested(app); /* a view's body bound some on the app itself */ Mark mark; mark_event(app, &mark); memcpy(pending, app->values, sizeof(app->values)); @@ -5310,19 +5312,32 @@ static PureResult message(PureApp *app, uint32_t input, const char *key, int64_t if (opened != PURE_OK) { app->document = NULL; return opened; } if (base >= 0 && base != app->revision) { pure_app_finish(app); return PURE_CONFLICT; } } - /* Its rows fill their lists (emptied when the event ends); rows that are not whole fail it. */ + /* Its rows fill their lists (emptied when the event ends); rows that are not whole fail it. Rows whose records hold + * lists (027's 4a) read those into the event's lists as a kept list's cell is read, in the event's lists begun anew. */ + bool nested = false; for (uint32_t f = 0; f < shape->field_count; ++f) { const PureField *field = &m->field[shape->field_start + f]; if (field->type != PURE_FIELD_ROWS) continue; - PureResult filled = pure_list_read(app->lists[field->rows], payload.data + spans[f][0], spans[f][1]); + uint32_t record = m->state_ref[field->rows]; + const unsigned char *rows = payload.data + spans[f][0]; + PureResult filled; + if (m->record_nested[record]) { + if (!nested && app->nested.count) unbind(app), drop_nested(app); + nested = true; + bool made, bad = false; + value_take(app, record, rows, spans[f][1], app->lists[field->rows], &made, &bad); + filled = made ? PURE_OK : bad ? PURE_STORE_ERROR : PURE_ALLOCATION; + } else + filled = pure_list_read(app->lists[field->rows], rows, spans[f][1]); if (filled == PURE_OK) continue; clear_rows(app); + if (nested) drop_nested(app); pure_app_finish(app); return filled == PURE_ALLOCATION ? filled : fault(app, PURE_FAULT_LIMIT, UINT32_MAX); } app->payload = payload; Event ev = {.app = app, .shape = shape, .key = key, .base = base, .arena = &app->arena, .exists = app->revision > 0, - .spans = spans}; + .nested = nested, .spans = spans}; PureResult result = dispatch(app, 1 + 2 * input, &ev); if (result != PURE_OK) pure_app_finish(app); return result; diff --git a/core/module.h b/core/module.h index 15109b6..349c4d2 100644 --- a/core/module.h +++ b/core/module.h @@ -98,9 +98,11 @@ enum { PURE_MODULE_HELD = 32, PURE_HELD_TEXT = 16, PURE_HELD_APPEND = 1 << 8 }; * an empty one), an i64 in its cell, so moves and reads (ITEM_GET) are an * integer's; such lists go as the event or a view's body ends. A list is a * value: storing one in a second cell shares it, and a list shared or bound - * is copied before it changes (so no other value sees the change). Since - * stage 3 a stored or a transient list's records may hold lists too (a - * message's rows not, nor as a key): there the cell holds the value in its + * is copied before it changes (so no other value sees the change). A + * message's rows may hold lists (027's 4a): each comes in as one of the + * event's lists, read from its stored form (below) before the handler runs. + * Since stage 3 a stored or a transient list's records may hold lists too + * (never as a key): there the cell holds the value in its * items' stored form (PURE_CELL_LIST), read into one of the event's lists * as ITEM_GET reads it and written from one as it is set (LIST_APPEND, * LIST_INSERT, ITEM_SET; APPEND_IN reads, appends and writes back), so diff --git a/core/test-module.c b/core/test-module.c index eb6752d..9d925dd 100644 --- a/core/test-module.c +++ b/core/test-module.c @@ -6628,6 +6628,64 @@ static void nested_replace(void) { printf("{\"nested_replace\":{\"lists\":2,\"items\":3}}\n"); } +/* 027's 4a: a message's rows whose records hold lists. Each node's kids come in as one of the event's lists, read + * through a register; the rows and their lists go as the event ends, and rows that are not whole fail it with nothing + * changed. */ +static size_t put_node(unsigned char *out, int64_t name, const int64_t *kids, uint32_t count) { + memcpy(out, &name, 8); + put32(out + 8, 8 * count); + for (uint32_t k = 0; k < count; ++k) memcpy(out + 12 + 8 * k, &kids[k], 8); + return 12 + 8 * (size_t)count; +} + +static void nested_rows(void) { + enum { LC_ = PURE_OP_LIST_COUNT, BIND_ = PURE_OP_LIST_BIND, NODES = 1, KIDS = 2, TOTAL = 3 }; + static const S nums[] = {{"n", 0}}; + static const RF leaf_fields[] = {{"v", 3, 0}}; + static const RF node_fields[] = {{"name", 3, 0}, {"kids", 4, 0}}; + static const R records[] = {{"Leaf", leaf_fields, 1}, {"Node", node_fields, 2}}; + static const T others[] = {{"order.nodes", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}, + {"let.kids", 3 | (uint32_t)(PURE_LIST_ROWS | PURE_LIST_BOUND) << 8, 0}, {"total", PURE_STATE_I64, 0}}; + static const F fields[] = {{"nodes", PURE_FIELD_ROWS | NODES << 8}}; + static const I inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, fields, 1}}; + static const O code[] = { + /* 0: for x in order.nodes { total += sum(k.v for k in x.kids) } */ + {EACH, WALKING(NODES, 10), 0}, {ROW, NODES, 0}, {IG, ITEM(NODES, 1), 0}, {BIND_, KIDS, 0}, + {L, TOTAL, 0}, {FOLD, FOLDING(PURE_FOLD_SUM | PURE_FOLD_INTEGER, KIDS, 3), 0}, {ROW, KIDS, 0}, {IG, ITEM(KIDS, 0), 0}, + {C, 1, 1}, {IADD, 0, 0}, {ST, TOTAL, 0}, + /* 11: n = n * 10 + order.nodes.count */ + {L, 0, 0}, {C, 0, 10}, {MUL, 0, 0}, {LC_, NODES, 0}, {ADD, 0, 0}, {ST, 0, 0}, + }; + H handler = {1, 0, LEN(code)}; + Spec spec = {nums, 1, inputs, 1, NULL, 0, &handler, 1, code, LEN(code), NULL, 0, others, LEN(others), NULL, 0, NULL, 0, + records, 2, NULL, 0}; + if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "nested rows: %s\n", error); abort(); } + PureApp app; + CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK); + static const int64_t first[] = {7, 9}, second[] = {5}; + unsigned char payload[64]; + size_t size = put_node(payload, 1, first, 2); + size += put_node(payload + size, 2, second, 1); + size += put_node(payload + size, 3, NULL, 0); + for (int round = 1; round <= 3; ++round) { /* each event begins with its lists and theirs empty */ + CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, size}) == PURE_OK); + CHECK(app.values[TOTAL].i64 == 21 * round && app.values[0].num == (round == 1 ? 3 : round == 2 ? 33 : 333)); + CHECK(pure_list_count(app.lists[NODES]) == 0 && app.nested.count == 2); /* the third node's kids are none */ + } + /* Not whole: a node's kids past the end, a kid cut short, a node cut short. */ + const size_t cuts[] = {size - 1, size - 12 - 4, 4}; + unsigned char broken[64]; + memcpy(broken, payload, size); + put32(broken + 8, 1000); + CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){broken, size}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); + for (size_t k = 0; k < LEN(cuts); ++k) + CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, cuts[k]}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); + CHECK(app.values[TOTAL].i64 == 63 && app.values[0].num == 333 && pure_list_count(app.lists[NODES]) == 0 && !app.nested.count); + CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, size}) == PURE_OK && app.values[TOTAL].i64 == 84); + pure_app_stop(&app); + printf("{\"nested_rows\":{\"rounds\":3,\"refused\":%zu}}\n", LEN(cuts) + 1); +} + /* A timer's tick sees the time the host set; floor and a floored modulo * take it apart. now is for handlers only. */ enum { NOW = PURE_OP_NOW, FLOOR = PURE_OP_FLOOR, MOD = PURE_OP_MOD }; @@ -7007,6 +7065,7 @@ int main(void) { functions_and_characters(); nested_crashes(); nested_replace(); + nested_rows(); memory_budget(); work_budget(); fold_mutations(); diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index 223bc4f..4ab39f5 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -32,7 +32,7 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계): - 레코드 필드의 타입 4(LIST)는 항목들의 레코드를 기본값 자리(i64)에 둔다. - 칸의 값은 이벤트가 만든 리스트의 번호(0은 빈 리스트)다. 정수처럼 옮겨지고 ITEM_GET으로 읽힌다. 스택의 타입은 리스트이고, 그 레코드를 가진다. -- 3단계부터 저장 상태와 transient의 리스트도 그런 레코드를 받는다. 메시지의 행은 받지 않고, 리스트 필드는 키가 되지 않는다. +- 3단계부터 저장 상태와 transient의 리스트도 그런 레코드를 받는다. 4a부터 메시지의 행도 받는다. 리스트 필드는 키가 되지 않는다. - 이벤트를 넘어 사는 리스트(저장, transient)의 칸은 리스트 값을 그 항목들의 저장 형태로 담는다(바이트처럼 담고, 이미지 머리는 종류 5로 적는다). 그런 칸을 다루는 명령은 로드가 실행기만의 내부 명령으로 바꿔 쓴다(다른 명령의 걸음은 그대로 둔다). 항목마다 필드의 차례로, 수·bool·i64는 8바이트 little-endian, text와 리스트는 u32le 길이와 바이트(리스트는 다시 이 형태)다. @@ -286,7 +286,9 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이� **메시지의 행.** 행 필드는 가변 필드이고, 바이트는 그 행 목록의 저장 형식(항목마다 필드 순서대로 수는 binary64, text는 u32 길이와 바이트)이다. 메시지가 도착하면 핸들러가 돌기 전에 행 목록을 그 행들로 채우고, 유한하지 않은 수, UTF-8이 아닌 -text, 끝이 맞지 않는 바이트는 메시지를 `limit`으로 실패시킨다. 행 목록은 모든 이벤트가 끝날 때(성공이든 실패든) 비워지고, +text, 끝이 맞지 않는 바이트는 메시지를 `limit`으로 실패시킨다. 행의 레코드가 리스트를 담으면(027의 4a) 그 필드는 3단계의 칸과 같은 +저장 형식(u32 길이와, 그 레코드의 항목들을 다시 이 형식으로)이고, 도착할 때 이벤트의 리스트 하나로 읽힌다. JSON 경계는 객체 +안의 배열을 그 레코드의 객체들로 읽고, 리스트 64겹까지 받는다. 행 목록은 모든 이벤트가 끝날 때(성공이든 실패든) 비워지고, 저장되지 않으며, 그 변경은 되돌리기 기록에 쌓이지 않는다(비울 것이므로). 그래서 핸들러는 메시지의 행을 보통의 리스트로 읽고(걷기, 집계, `count`), 한 이벤트 안의 임시 표로 쓸 수도 있다. 한 메시지의 모든 행은 한 트랜잭션이다. 행 하나가 실패하면 (없는 키, `require`) 어느 행도 적용되지 않는다. diff --git a/docs/design/027-computation-core.md b/docs/design/027-computation-core.md index 09392f3..3c07590 100644 --- a/docs/design/027-computation-core.md +++ b/docs/design/027-computation-core.md @@ -210,6 +210,67 @@ - 명령과 상태 종류의 수, 코어의 줄 수가 줄었다(세어 적는다). - TPC-C와 은행 질의가 그대로다. +**계획(2026-10-18).** 여섯 걸음으로 나눈다. +- 걸음마다 지킬 것: Pure, aup, Vega의 검사가 초록이다. 기존 일의 명령 수(041의 다섯 일, 은행 질의)가 막대(1%) 안이다. 걸음마다 + 커밋한다. +- 시작의 수(3단계 뒤, 9f17dd6): + - 명령 106개 + - 상태 종류 7개: 수, text, 커서, 리스트, 인덱스, bool, i64 + - 입력 종류 6개: 키, 메시지, text, 타이머, 출력, 인자. 여기에 프레임 이벤트와 가리킴(`pure_app_point`)이 따로 있다. + - 보기 종류 5개: 수, text, 표, bool, i64 + - 메시지 필드의 타입 7개, 레코드 필드의 타입 5개 + - 코어(`core/`의 시험이 아닌 파일) 22,398줄, serve 5,086줄 +- **4a. 메시지의 리스트가 중첩을 받는다.** + - 메시지의 행은 이미 이벤트의 리스트로 들어온다(let의 리스트와 같은 것). 그 레코드가 리스트를 담게 한다. + - 페이로드는 3단계의 저장 형태 그대로다. JSON 경계는 객체 안의 배열을 그 형태로 짓는다. + - 3단계가 남긴 것이다. JSON 경계에서 바로 트리로 들어온다. + - **만듦(2026-10-18).** + - Pure: 메시지가 도착할 때 리스트를 담은 행을 3단계의 해독기로 읽는다. 행마다의 리스트는 이벤트의 리스트가 된다. + - serve: JSON 경계가 객체 안의 배열을 그 레코드의 객체들로 짓는다(리스트 64겹까지). + - aup: 메시지의 행이 리스트를 담는 것을 받는다. 메시지의 행 안의 리스트에 덧붙이는 것은 거절한다(행은 읽기만 한다). + - 찾아 막은 구멍: 3단계의 해독기가 4바이트가 남지 않은 길이를 넘겨 읽었다(`size - at - 4`가 넘쳐 돌았다). 저장한 것만 읽던 + 3단계에서는 드러나지 않았고, 밖에서 오는 메시지가 드러냈다. + - 시험: + - 단위 시험 `nested_rows`: 세 번의 이벤트, 온전하지 않은 행 넷이 아무것도 바꾸지 않는다. + - Pure의 검사 `server_trees`: 무작위 숲 300개를 JSON으로 보내 같은 차례로 도는 파이썬 모델과 견준다. 거절 셋(리스트 + 필드가 배열이 아님, 없는 필드, 65겹)과 재시작도 본다. + - 명령 수(b30 대 b29): 다섯 일이 묶음 BFS +0.12%(늘 오가는 범위) 말고는 ±0.001% 안이다. 은행 질의(s29 대 s30, 각 두 + 번)는 −0.00%에서 +0.23%다. +- **4b. 키별 문서를 없앤다(026의 의심 4).** + - 레코드 필드가 바이트를 받는다. 편집기는 `list of Doc by key`와 앱이 가진 리비전으로 쓴다. + - Pure에서 컬렉션, DOC·DOC_SIZE·REPLACE, 키 있는 메시지와 기준 리비전, PureData의 문서 콜백, 문서별 묶음 저장이 빠진다. serve의 + 문서 모드(키마다 앱, ETag와 If-Match)도 빠진다. + - 옛 편집기(editor.pure)와의 대조는 같은 뜻의 파이썬 모델과의 대조로 바꾼다. + - 두 커밋으로 나눈다. + - 4b-1: 레코드 필드의 바이트(Pure, aup). 새 편집기를 키 있는 리스트로 쓰고 모델과 견준다. 옛 문서의 기계는 아직 둔다. + - 4b-2: 키별 문서를 지운다. 문서에 기대던 검사는 같은 것을 보는 다른 앱으로 옮긴다. + - 파이프라이닝과 묶음 저장(kill -9 포함)은 새 편집기의 입력과 보기로 본다. + - 빌려 읽기와 그 예산은 text 상태의 보기로 본다. text 상태도 저장된 조각을 빌려준다. + - HTTP 벤치마크의 문서 묶음은 그 계약(axum, drogon과 같은 `/objects`)이 사라지므로 걷는다. 앱 묶음은 남는다. + - 잃는 것: 키마다 따로 도는 앱(문서만 가진 모듈이 스레드마다 앱 하나)과 큰 문서의 조각 편집. 키 있는 리스트를 키로 나눠 + 돌리는 것과 큰 바이트 값의 조각 표현은 뜻을 바꾸지 않고 나중에 고를 수 있는 구현이다. +- **4c. 커서와 인덱스를 앵커 하나로(의심 3).** + - 상태 종류 "앵커"가 text나 리스트 하나를 가리킨다. + - 뜻은 그대로다: 편집을 따라가고, 글자의 경계에 서고, 지운 항목에서는 이웃으로 간다. + - 같은 일을 하는 커서와 인덱스의 명령은 하나로 합친다. +- **4d. 입력은 메시지 하나(의심 1).** + - 키, text, 타이머, 프레임, 가리킴은 표면과 호스트가 만드는 메시지다. + - 모듈의 입력은 메시지(필드들)와 표면이 읽는 묶음(키 이름, text, 주기, 프레임)이다. 묶음을 해석하는 것은 표면이고, Pure는 메시지만 + 받는다. + - `dt`는 프레임 메시지의 필드가 된다. 키의 누름과 뗌은 메시지의 bool 필드다. +- **4e. 보기는 아무 값(의심 2).** + - 보기의 타입은 레코드 필드의 타입이다. 표는 레코드의 리스트를 내는 보기이고, 열은 그 레코드의 필드다. + - 게으른 칸 계산과 한 번 훑는 집계는 그 구현으로 남는다. 항목이 리스트를 담을 수 있고, 보기의 본문이 지은 리스트도 낼 수 있다. +- **4f. 고정 한도를 뺀다(의심 7).** + - 상태, 입력, 레코드, 필드, 중첩의 수를 모듈에 맞춰 잡는다. 비트 하나씩 쓰던 집합은 길이를 가진다. + - aup 소스의 32 KB도 뺀다. + - 앞의 걸음들이 표를 바꾸므로 마지막에 한다. +- **차례의 까닭.** + - 4a는 작고, 3단계의 끝을 맺는다. + - 4b는 가장 많이 지운다. 키 있는 메시지와 기준 리비전이 사라지면 4d가 단순해진다. + - 4c는 가리킴(4d)이 무엇을 옮기는지 정한다. + - 4e는 표면(Vega)이 읽는 쪽을 바꾸므로, 입력 쪽(4d)과 따로 잰다. + ### 5단계: 수의 모델 - 정수 리터럴의 기본, `/`와 `//`, 돈의 십진수, 묶음 실행기의 2^53 검사를 정한다. diff --git a/research/experiments/040-against-c/2026-10-18-counts-s30.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s30.jsonl new file mode 100644 index 0000000..8d5d46e --- /dev/null +++ b/research/experiments/040-against-c/2026-10-18-counts-s30.jsonl @@ -0,0 +1,4 @@ +{"serve": "pure-serve", "per_branch": {"instructions": 27230405, "cycles": 12221300}, "in_credit": {"instructions": 3545992, "cycles": 1675191}, "large": {"instructions": 1423198, "cycles": 622136}, "ranks": {"instructions": 24536813, "cycles": 14895547}, "none": {"instructions": 617123, "cycles": 193648}, "field": {"instructions": 9028796, "cycles": 3338189}, "found": {"instructions": 14179417, "cycles": 10655083}} +{"serve": "pure-serve", "per_branch": {"instructions": 26908242, "cycles": 11686470}, "in_credit": {"instructions": 3473397, "cycles": 1676065}, "large": {"instructions": 1423286, "cycles": 682081}, "ranks": {"instructions": 24358211, "cycles": 14718864}, "none": {"instructions": 617123, "cycles": 196214}, "field": {"instructions": 9028796, "cycles": 3314507}, "found": {"instructions": 14182593, "cycles": 10595866}} +{"serve": "pure-serve", "per_branch": {"instructions": 26952627, "cycles": 11776386}, "in_credit": {"instructions": 3483449, "cycles": 1583305}, "large": {"instructions": 1423246, "cycles": 674235}, "ranks": {"instructions": 24382297, "cycles": 15052012}, "none": {"instructions": 617123, "cycles": 190875}, "field": {"instructions": 9028796, "cycles": 3351305}, "found": {"instructions": 14180762, "cycles": 10924627}} +{"serve": "pure-serve", "per_branch": {"instructions": 27346357, "cycles": 12085410}, "in_credit": {"instructions": 3572085, "cycles": 1636879}, "large": {"instructions": 1423174, "cycles": 613836}, "ranks": {"instructions": 24601158, "cycles": 14958524}, "none": {"instructions": 617115, "cycles": 192553}, "field": {"instructions": 9028891, "cycles": 3345141}, "found": {"instructions": 14180354, "cycles": 10664594}} diff --git a/research/experiments/041-batches/2026-10-18-counts.jsonl b/research/experiments/041-batches/2026-10-18-counts.jsonl index 6db0072..04c6b37 100644 --- a/research/experiments/041-batches/2026-10-18-counts.jsonl +++ b/research/experiments/041-batches/2026-10-18-counts.jsonl @@ -48,3 +48,13 @@ {"version":"b29","way":"rounds","work":"join","instructions":7670274118,"cycles":3526186126} {"version":"b29","way":"batches","work":"bfs","instructions":3364524544,"cycles":1644560620} {"version":"b29","way":"rounds","work":"bfs","instructions":4723942606,"cycles":2096987224} +{"version":"b30","way":"batches","work":"matmul","instructions":713395456,"cycles":228717396} +{"version":"b30","way":"rounds","work":"matmul","instructions":21265247853,"cycles":7438769118} +{"version":"b30","way":"batches","work":"knapsack","instructions":348634134,"cycles":111296108} +{"version":"b30","way":"rounds","work":"knapsack","instructions":21881636299,"cycles":8100998092} +{"version":"b30","way":"batches","work":"roots","instructions":157303671,"cycles":109698296} +{"version":"b30","way":"rounds","work":"roots","instructions":2719817880,"cycles":1109694376} +{"version":"b30","way":"batches","work":"join","instructions":1783720564,"cycles":1011535653} +{"version":"b30","way":"rounds","work":"join","instructions":7670273852,"cycles":3502744585} +{"version":"b30","way":"batches","work":"bfs","instructions":3368525625,"cycles":1640099773} +{"version":"b30","way":"rounds","work":"bfs","instructions":4723944330,"cycles":2101713847} diff --git a/serve/message-json.c b/serve/message-json.c index 0905b34..f0f965c 100644 --- a/serve/message-json.c +++ b/serve/message-json.c @@ -293,15 +293,15 @@ static bool rows_room(PureJsonMessage *b, size_t more) { return true; } -/* A rows field's array (at its opening bracket): each an object of its - * record's fields by name, a number or a string, a field left out taking its - * default; encoded one after another as the list's stored form (a number's - * binary64 bits, a text's u32le length and bytes). */ -static const char *rows_of(PureJsonMessage *b, unsigned char *json, size_t size, size_t *at, uint32_t f, unsigned depth) { +/* An array (at its opening bracket) of objects of a record's fields by name: a number, a string, or an array of the + * record a list field holds (027's 4a), a field left out taking its default; encoded one after another into rows as + * the list's stored form (a number's binary64 bits, a text's u32le length and bytes, a list's u32le length and its + * items so). An item's lists are encoded first and the item after them, moved down over them at its end. */ +static const char *items_of(PureJsonMessage *b, unsigned char *json, size_t size, size_t *at, uint32_t record_index, + unsigned depth) { const PureModule *m = b->module; - const PureField *field = &m->field[b->shape->field_start + f]; - const PureRecord *record = &m->record[m->state_ref[field->rows]]; - size_t start = b->rows_size; + const PureRecord *record = &m->record[record_index]; + if (depth >= 64) return "limit"; /* 64 lists deep at most */ if (*at == size || json[(*at)++] != '[') return "json_type"; *at = pure_json_space(json, *at, size); for (bool first = true; *at < size && json[*at] != ']'; first = false) { @@ -312,7 +312,9 @@ static const char *rows_of(PureJsonMessage *b, unsigned char *json, size_t size, if (*at == size || json[(*at)++] != '{') return "json_type"; PureValue numbers[PURE_RECORD_FIELDS]; /* a number's, a bool's or an i64's */ PureSpan texts[PURE_RECORD_FIELDS]; + size_t lists[PURE_RECORD_FIELDS][2]; /* a list's stored form: where in rows, and its bytes */ bool given[PURE_RECORD_FIELDS] = {false}; + size_t item = b->rows_size; *at = pure_json_space(json, *at, size); for (bool member = true; *at < size && json[*at] != '}'; member = false) { if (!member) { @@ -340,6 +342,16 @@ static const char *rows_of(PureJsonMessage *b, unsigned char *json, size_t size, given[k] = true; uint8_t type = m->record_field_type[record->first + k]; bool text = type == PURE_RECORD_TEXT, truth; + if (type == PURE_RECORD_LIST) { + size_t from = b->rows_size; + const char *why = items_of(b, json, size, at, (uint32_t)m->record_field_default[record->first + k].i64, + depth + 1); + if (why) return why; + lists[k][0] = from; + lists[k][1] = b->rows_size - from; + *at = pure_json_space(json, *at, size); + continue; + } if (type == PURE_RECORD_BOOL) { /* true or false, nothing else */ size_t word = json_bool(json + *at, size - *at, &truth); if (!word) return "json_type"; @@ -370,16 +382,24 @@ static const char *rows_of(PureJsonMessage *b, unsigned char *json, size_t size, } if (*at == size) return "json"; ++*at; + size_t lists_end = b->rows_size; for (unsigned k = 0; k < record->count; ++k) { uint32_t at_field = record->first + k; - if (m->record_field_type[at_field] == PURE_RECORD_TEXT) { - uint32_t constant = (uint32_t)m->record_field_default[at_field].num; - PureSpan text = given[k] ? texts[k] - : (PureSpan){m->constant_data + m->constant_offset[constant], m->constant_length[constant]}; - if (!rows_room(b, 4 + text.size)) return "memory"; - write_u32(b->rows + b->rows_size, (uint32_t)text.size); - if (text.size) memcpy(b->rows + b->rows_size + 4, text.data, text.size); - b->rows_size += 4 + text.size; + uint8_t type = m->record_field_type[at_field]; + if (type == PURE_RECORD_TEXT || type == PURE_RECORD_LIST) { + PureSpan text = {0}; + size_t length = type == PURE_RECORD_LIST ? (given[k] ? lists[k][1] : 0) : 0; + if (type == PURE_RECORD_TEXT) { + uint32_t constant = (uint32_t)m->record_field_default[at_field].num; + text = given[k] ? texts[k] + : (PureSpan){m->constant_data + m->constant_offset[constant], m->constant_length[constant]}; + length = text.size; + } + if (length > UINT32_MAX || !rows_room(b, 4 + length)) return "memory"; + write_u32(b->rows + b->rows_size, (uint32_t)length); + if (type == PURE_RECORD_LIST && length) memcpy(b->rows + b->rows_size + 4, b->rows + lists[k][0], length); + else if (length) memcpy(b->rows + b->rows_size + 4, text.data, length); + b->rows_size += 4 + length; } else { uint64_t bits = (uint64_t)(given[k] ? numbers[k] : m->record_field_default[at_field]).i64; /* its bits */ if (!rows_room(b, 8)) return "memory"; @@ -387,9 +407,23 @@ static const char *rows_of(PureJsonMessage *b, unsigned char *json, size_t size, b->rows_size += 8; } } + if (lists_end > item) { /* the item over its lists' forms */ + memmove(b->rows + item, b->rows + lists_end, b->rows_size - lists_end); + b->rows_size -= lists_end - item; + } } if (*at == size) return "json"; ++*at; + return NULL; +} + +/* A rows field's array, encoded so (items_of) and placed as the table says. */ +static const char *rows_of(PureJsonMessage *b, unsigned char *json, size_t size, size_t *at, uint32_t f, unsigned depth) { + const PureModule *m = b->module; + const PureField *field = &m->field[b->shape->field_start + f]; + size_t start = b->rows_size; + const char *why = items_of(b, json, size, at, m->state_ref[field->rows], depth); + if (why) return why; if (b->rows_base + b->rows_size > UINT32_MAX >> 2) return "limit"; write_u32(b->table + 8 * f, (uint32_t)(b->rows_base + start)); write_u32(b->table + 8 * f + 4, (uint32_t)(b->rows_size - start)); diff --git a/serve/message-json.h b/serve/message-json.h index ffb1651..45fae92 100644 --- a/serve/message-json.h +++ b/serve/message-json.h @@ -16,14 +16,16 @@ typedef struct { bool seen[PURE_MODULE_FIELDS]; /* A rows field's array of objects is encoded in the stored form of its * list into rows (grown with malloc: the caller frees it), which the - * caller places from rows_base on in the payload, as the table says. */ + * caller places from rows_base on in the payload, as the table says. A + * list field of an object is an array of its record's objects so, 64 + * lists deep at most. */ unsigned char *rows; size_t rows_size, rows_capacity, rows_base; } PureJsonMessage; /* The object at json[*at] (its opening brace), *at moved past it: NULL, or * what is wrong ("json", "json_field": unknown or given twice, "json_type", - * "limit": rows past what a payload holds, "memory"). */ + * "limit": rows past what a payload holds or 64 lists deep, "memory"). */ const char *pure_json_message(PureJsonMessage *, unsigned char *json, size_t size, size_t *at); size_t pure_json_space(const unsigned char *json, size_t at, size_t size); /* A fixed field's value from its text (a query value or a JSON value) into diff --git a/tests/check.py b/tests/check.py index ed2a5c9..52aa17d 100644 --- a/tests/check.py +++ b/tests/check.py @@ -3637,6 +3637,133 @@ def server_rows(aup, pure, work): 'orders_applied': applied, 'orders_refused': refused, 'logged': len(log)} +TREES = ''' +record Node { + name: text + n = 0 + kids: list of Node +} +state nodes = 0 +state total = 0 +state deepest = 0 +state name_bytes = 0 +input tree = message { + label: text + roots: list of Node +} + +fn walk(x, depth) { + nodes = nodes + 1 + total = total + x.n + name_bytes = name_bytes + x.name.bytes + if depth > deepest { deepest = depth } + for k in x.kids { walk(k, depth + 1) } +} + +on tree { + require tree.label != "refuse" + for r in tree.roots { walk(r, 1) } +} + +view count = nodes +view sum = total +view depth = deepest +view named = name_bytes +''' + + +def server_trees(aup, pure, work): + """A message's rows whose records hold lists (027's 4a): a JSON forest + read straight into the event's lists, nested to any depth up to 64, and + walked by a function that calls itself. Random forests (names with + escapes and characters past ASCII, fields left out taking their + defaults) against a model walking them in the same order; a require + refusing one leaves everything as it was; malformed trees (a list field + not an array, a field the record lacks, past 64 deep) are refused before + the app sees them; a restart keeps the states and no rows.""" + import http.client + import random + source, module = work / 'trees.aup', work / 'trees.pmod' + source.write_text(TREES) + compile_module(aup, source, module) + store = work / 'trees-store' + + def call(port, method, path, body=None): + connection = http.client.HTTPConnection('127.0.0.1', port, timeout=60) + try: + connection.request(method, path, body, {'Content-Type': 'application/json'} if body is not None else {}) + answer = connection.getresponse() + return answer.status, answer.getheader('X-Pure-Error'), answer.read() + finally: + connection.close() + + def views(port): + return tuple(float(call(port, 'GET', f'/views/{v}')[2]) for v in ('count', 'sum', 'depth', 'named')) + + rng = random.Random(44) + names = ['', 'a', 'node', '빨리', 'a "quoted" \\ name', 'tab\there', '😀', 'line\nbreak', 'é'] + + def forest(depth): + roots = [] + for _ in range(rng.randrange(4 if depth < 6 else 1)): + node = {'name': rng.choice(names)} + if rng.random() < 0.8: + node['n'] = rng.randrange(-1000, 1000) / 8 + kids = forest(depth + 1) + if kids or rng.random() < 0.5: + node['kids'] = kids + roots.append(node) + return roots + + model = [0, 0.0, 0, 0] + + def walk(node, depth, into): + into[0] += 1 + into[1] += node.get('n', 0) + into[2] = max(into[2], depth) + into[3] += len(node['name'].encode()) + for k in node.get('kids', []): + walk(k, depth + 1, into) + + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store]) + sent = refused = 0 + try: + for i in range(300): + roots = forest(1) + label = 'refuse' if rng.random() < 0.1 else 'tree' + after = list(model) + for r in roots: + walk(r, 1, after) + status, error, _ = call(host.port, 'POST', '/inputs/tree', json.dumps({'label': label, 'roots': roots}).encode()) + if label == 'refuse': + assert (status, error) == (422, 'limit'), (status, error) # require fails as limit + refused += 1 + else: + assert (status, error) == (200, 'none'), (status, error) + model = after + sent += 1 + if i % 25 == 0: + assert views(host.port) == tuple(model), (views(host.port), model) + deep = {'name': 'z'} + for _ in range(63): + deep = {'name': 'z', 'kids': [deep]} + for body, want in [({'label': 'x', 'roots': [{'name': 'a', 'kids': 3}]}, (400, 'json_type')), + ({'label': 'x', 'roots': [{'name': 'a', 'kid': []}]}, (400, 'json_field')), + ({'label': 'x', 'roots': [{'name': 'a', 'kids': [deep]}]}, (400, 'limit'))]: + assert call(host.port, 'POST', '/inputs/tree', json.dumps(body).encode())[:2] == want, body + assert call(host.port, 'POST', '/inputs/tree', json.dumps({'label': 'x', 'roots': [deep]}).encode())[:2] == (200, 'none') + walk(deep, 1, model) + assert views(host.port) == tuple(model) and model[2] == 64 + finally: + host.stop() + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store]) + try: + assert views(host.port) == tuple(model) + finally: + host.stop() + assert sent > 200 and refused > 10 + return {'forests': sent, 'refused': refused, 'nodes': model[0], 'deepest': model[2]} + ARGUED = ''' record Account { aid = 0 @@ -6332,6 +6459,7 @@ def main(): report['server_surface'] = server_surface(aup, pure, work) report['server_queries'] = server_queries(aup, pure, work) report['server_rows'] = server_rows(aup, pure, work) + report['server_trees'] = server_trees(aup, pure, work) report['server_arguments'] = server_arguments(aup, pure, work) report['server_answers'] = server_answers(aup, pure, work) report['server_answers_memory'] = server_answers_memory(aup, pure, work) |
