diff options
| -rw-r--r-- | cli/pure-run.c | 17 | ||||
| -rw-r--r-- | core/module.c | 167 | ||||
| -rw-r--r-- | core/module.h | 22 | ||||
| -rw-r--r-- | core/test-module.c | 117 | ||||
| -rw-r--r-- | docs/design/008-module-format.md | 11 |
5 files changed, 313 insertions, 21 deletions
diff --git a/cli/pure-run.c b/cli/pure-run.c index e62bc98..97bd6d5 100644 --- a/cli/pure-run.c +++ b/cli/pure-run.c @@ -13,7 +13,8 @@ * every N commands (and at the end), so N events share one commit. * down NAME | up NAME | frame [N] | type NAME HEX|- | message NAME HEX|- | tick NAME | time MS | replace PATH | save | show * budget N (the work an event may do) | memory N (what the states may hold) | work (the last event's work) - * (type gives a text input its bytes, message a message input its payload, laid out as the module says.) + * (type gives a text input its bytes, message a message input its payload, laid out as the module says; + * message NAME @PATH gives it the bytes of the file at PATH, of any size.) * Text states and text views print as JSON strings; cursors as byte positions; * lists as arrays of objects; indices as positions; a table view as an array * with an object of its columns per row. A failed view or cell is null. */ @@ -173,10 +174,22 @@ int main(int argc, char **argv) { for (size_t i = 0; i < length && length * 2 == strlen(data); ++i) text[i] = (unsigned char)(hex(data[2 * i]) << 4 | hex(data[2 * i + 1])); PureSpan given = {text, length}; - if (input < 0) failure = "unknown input"; + unsigned char *read = NULL; /* message NAME @PATH: the file's bytes */ + if (data[0] == '@') { + FILE *file = fopen(data + 1, "rb"); + long size = file && !fseek(file, 0, SEEK_END) ? ftell(file) : -1; + if (size >= 0 && !fseek(file, 0, SEEK_SET) && (read = malloc(size ? (size_t)size : 1)) && + fread(read, 1, (size_t)size, file) == (size_t)size) + given = (PureSpan){read, (size_t)size}; + else failure = "cannot read the file"; + if (file) fclose(file); + } + if (failure) { + } else if (input < 0) failure = "unknown input"; else if ((command[0] == 't' ? pure_app_text(&app, (uint32_t)input, given) : pure_app_message(&app, (uint32_t)input, NULL, -1, given)) != PURE_OK) failure = "event failed"; + free(read); } else if (!strcmp(command, "tick")) { int input = pure_module_input(app.module, argument); if (input < 0) failure = "unknown input"; diff --git a/core/module.c b/core/module.c index d95769b..76a6821 100644 --- a/core/module.c +++ b/core/module.c @@ -18,7 +18,7 @@ enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = * state named. */ enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7, T_KEY = 8, T_LOCAL = 9, T_BOOL = 10, T_STATE = 11, T_I64 = 12 }; -enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_ARENA, FROM_ARGUMENTS }; +enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_ARENA, FROM_ARGUMENTS, FROM_HELD }; /* Where an opcode may appear. */ /* IN_ARGUMENTS: fields read where nothing is done with them (a view's arguments, a message's fields in a fold). */ /* IN_LOCALS: a handler's locals, read in its walks and the folds in them. */ @@ -31,11 +31,13 @@ enum { READ = IN_VIEW | IN_HANDLER }; * collection, a constant, a unit, the column's row, a branch target, a stack * depth, a fold, a relation, a keyed list, an output, the row of the column * a fold runs in, a number, bool or i64 state (A_VALUE), or the value's type - * (A_TYPE: 0 a number, 1 a bool, 2 an i64), how I64_BITS computes (A_BITS) or - * what a position is (A_POSITION: 0 a number, 1 an i64). */ + * (A_TYPE: 0 a number, 1 a bool, 2 an i64), how I64_BITS computes (A_BITS), + * what a position is (A_POSITION: 0 a number, 1 an i64), what CONCAT joins + * (A_JOINED: 0 texts, 1 bytes), how NUM_TEXT writes (A_FORMAT) or the value + * held by name (A_HELD). */ enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_I64 + 1, A_FIELD, A_COLLECTION, A_CONSTANT, A_UNIT, A_ROW, A_TARGET, A_DEPTH, A_FOLD, A_RELATION, A_KEYED, A_OUTPUT, A_OUTER, A_EACH, A_LOCAL, A_VALUE, A_TYPE, - A_LOOP, A_BITS, A_POSITION }; + A_LOOP, A_BITS, A_POSITION, A_JOINED, A_FORMAT, A_HELD }; /* A fold's block is verified as a column that leaves one bool (count, first) * or a value and whether to include it (a bool): a number (FOLD_TWO) or an * i64 (FOLD_I64). A table's filter is verified as a column that leaves a @@ -73,15 +75,15 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi [PURE_OP_FIELD] = {0, {0}, T_FIELD, IN_MESSAGE | IN_ARGUMENTS, A_FIELD, O_ANY}, [PURE_OP_DOC] = {0, {0}, T_BYTES, IN_KEYED, A_COLLECTION, O_ANY}, [PURE_OP_DOC_SIZE] = {0, {0}, T_NUM, IN_KEYED, A_COLLECTION, O_ANY}, - [PURE_OP_LEN] = {1, {T_BYTES}, T_NUM, IN_HANDLER, A_NONE, O_ANY}, + [PURE_OP_LEN] = {1, {T_BYTES}, T_NUM, READ, A_NONE, O_ANY}, [PURE_OP_REPLACE] = {3, {T_NUM, T_NUM, T_BYTES}, 0, IN_KEYED, A_COLLECTION, O_ANY}, [PURE_OP_REPLY] = {1, {T_BYTES}, 0, IN_MESSAGE, A_NONE, O_ANY}, [PURE_OP_REQUIRE] = {1, {T_BOOL}, 0, IN_HANDLER, A_NONE, O_ANY}, [PURE_OP_TEXT_CONST] = {0, {0}, T_TEXT, READ, A_CONSTANT, O_ANY}, [PURE_OP_TEXT_LOAD] = {0, {0}, T_TEXT, READ, STATE(TEXT), O_ANY}, [PURE_OP_TEXT_INPUT] = {0, {0}, T_TEXT, IN_TEXT, A_NONE, O_ANY}, - [PURE_OP_NUM_TEXT] = {1, {T_NUM}, T_TEXT, READ, A_TYPE, O_ANY}, - [PURE_OP_CONCAT] = {2, {T_TEXT, T_TEXT}, T_TEXT, READ, A_NONE, O_ANY}, + [PURE_OP_NUM_TEXT] = {1, {T_NUM}, T_TEXT, READ, A_FORMAT, O_ANY}, + [PURE_OP_CONCAT] = {2, {T_TEXT, T_TEXT}, T_TEXT, READ, A_JOINED, O_ANY}, [PURE_OP_CHARS] = {1, {T_TEXT}, T_NUM, READ, A_NONE, O_ANY}, [PURE_OP_LINES] = {1, {T_TEXT}, T_NUM, READ, A_NONE, O_ANY}, [PURE_OP_TEXT_BYTES] = {1, {T_TEXT}, T_NUM, READ, A_NONE, O_ANY}, @@ -138,6 +140,10 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi [PURE_OP_BYTE_AT] = {2, {T_BYTES, T_NUM}, T_I64, READ, A_POSITION, O_ANY}, [PURE_OP_BYTES_OF] = {0, {0}, T_BYTES, READ, STATE(LIST), O_ANY}, [PURE_OP_INDEX_AT] = {1, {T_NUM}, T_INDEX, READ, STATE(LIST), O_ARG}, + [PURE_OP_BYTES_SLICE] = {3, {T_BYTES, T_NUM, T_NUM}, T_BYTES, READ, A_POSITION, O_ANY}, + [PURE_OP_BYTE] = {1, {T_I64}, T_BYTES, READ, A_NONE, O_ANY}, + [PURE_OP_BYTES_LOAD] = {0, {0}, T_BYTES, IN_LOCALS, A_HELD, O_ANY}, + [PURE_OP_BYTES_STORE] = {1, {T_BYTES}, 0, IN_HANDLER | IN_COMPUTE, A_HELD, O_ANY}, }; #undef NUMS #undef COMPARE @@ -388,7 +394,13 @@ PURE_COLD static bool names_arg(const PureModule *m, unsigned kind, uint32_t arg case A_LOOP: return (arg & 0xFF) < PURE_MODULE_LOCALS && !(arg & 0xFE00); /* a slot, and whether i64s */ case A_TYPE: return arg <= 2; case A_BITS: return arg <= PURE_BITS_RIGHT; - case A_POSITION: return arg <= 1; + case A_POSITION: case A_JOINED: return arg <= 1; + case A_HELD: return (arg & ~(uint32_t)PURE_HELD_APPEND) < PURE_MODULE_HELD; + case A_FORMAT: { /* a type; a width, zeros, a form fitting it and digits for FIXED alone */ + uint32_t type = arg & 3, form = arg >> PURE_FORMAT_FORM & 7, digits = arg >> PURE_FORMAT_DIGITS & 31; + return type <= 2 && !(arg & 0xFC) && !(arg >> 25) && form <= PURE_FORMAT_FIXED && (type != 1 || !form) && + (form != PURE_FORMAT_FIXED || type == 0) && (form == PURE_FORMAT_FIXED || !digits); + } case A_RELATION: return arg <= PURE_NOT_EQUAL; case A_KEYED: return state_of(m, arg, PURE_STATE_LIST) && m->state_key[arg]; case A_OUTPUT: return arg < m->inputs && m->input[arg].kind == PURE_KIND_OUTPUT; @@ -569,8 +581,14 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i need[0] = local_type(arg); } else if (need[0] == T_STATE) { need[0] = state_type(m, arg); - } else if (op->op == PURE_OP_NUM_TEXT && arg) { - need[0] = arg == 1 ? T_BOOL : T_I64; + } else if (op->op == PURE_OP_NUM_TEXT && arg & 3) { + need[0] = (arg & 3) == 1 ? T_BOOL : T_I64; + } else if (op->op == PURE_OP_CONCAT && arg) { + need[0] = need[1] = T_BYTES; + } else if (op->op == PURE_OP_BYTES_SLICE && arg) { + need[1] = need[2] = T_I64; /* the positions i64s */ + } else if (op->op == PURE_OP_BYTES_STORE && (arg & 0xFF) >= PURE_HELD_TEXT) { + need[0] = T_TEXT; } else if (op->op == PURE_OP_BYTE_AT && arg) { need[1] = T_I64; /* the position an i64 */ } else if ((op->op == PURE_OP_EQ || op->op == PURE_OP_NE) && height >= 2 && types[height - 1] == T_BOOL && @@ -611,6 +629,8 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i if (out == T_STATE) out = state_type(m, arg); if (truth) out = T_BOOL; if (integer) out = T_I64; + if ((op->op == PURE_OP_CONCAT && arg) || (op->op == PURE_OP_BYTES_LOAD && (arg & 0xFF) < PURE_HELD_TEXT)) out = T_BYTES; + else if (op->op == PURE_OP_BYTES_LOAD) out = T_TEXT; if (out) { if (height == PURE_MODULE_STACK) REFUSE("stack overflow"); owners[height] = (unsigned char)(out != T_CURSOR && out != T_INDEX ? 0 : of == O_OPERAND ? owners[height] : owner); @@ -1096,13 +1116,13 @@ PURE_COLD static unsigned arity(uint8_t op) { switch (op) { case PURE_OP_CONST: case PURE_OP_LOAD: case PURE_OP_INDEX_LOAD: case PURE_OP_ROW: case PURE_OP_OUTER_ROW: case PURE_OP_LIST_COUNT: case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: case PURE_OP_FIELD: case PURE_OP_LOCAL_LOAD: - case PURE_OP_BYTES_OF: + case PURE_OP_BYTES_OF: case PURE_OP_BYTES_LOAD: return 0; case PURE_OP_ITEM_GET: case PURE_OP_LIST_FIND: case PURE_OP_LIST_HAS: case PURE_OP_NEG: case PURE_OP_NOT: case PURE_OP_FLOOR: case PURE_OP_INDEX_NUM: case PURE_OP_I64_NEG: case PURE_OP_I64_FROM: case PURE_OP_NUM_FROM: - case PURE_OP_INDEX_AT: + case PURE_OP_INDEX_AT: case PURE_OP_BYTE: return 1; - case PURE_OP_CLAMP: return 3; + case PURE_OP_CLAMP: case PURE_OP_BYTES_SLICE: return 3; default: return 2; } } @@ -2007,6 +2027,7 @@ static PureResult visit_value(const PureApp *app, const PureArena *arena, PureBy case FROM_COPY: direct = app->copied + v.offset; break; case FROM_CONSTANT: direct = app->module->constant_data + v.offset; break; case FROM_ARENA: direct = arena->bytes + v.offset; break; + case FROM_HELD: direct = app->held[v.owner].data + v.offset; break; case FROM_TEXT: return pure_draft_read_version(app->texts[v.owner], v.version, (size_t)v.offset, (size_t)v.length, visit, context); @@ -2040,8 +2061,8 @@ static PureResult contiguous(const PureApp *app, PureArena *arena, PureBytes v, *out = (PureSpan){v.length ? base + v.offset : NULL, (size_t)v.length}; return PURE_OK; } - if (v.source == FROM_ARENA) { - *out = (PureSpan){arena->bytes + v.offset, (size_t)v.length}; + if (v.source == FROM_ARENA || v.source == FROM_HELD) { + *out = (PureSpan){(v.source == FROM_ARENA ? arena->bytes : app->held[v.owner].data) + v.offset, (size_t)v.length}; return PURE_OK; } size_t start = arena->used; @@ -2125,6 +2146,71 @@ static bool copy_out(void *context, PureSpan span) { return true; } +/* BYTES_STORE: the value held by name slot becomes v, or v after it (append); what it holds counts in the room. + * 1 where that passes the room (MEMORY), 2 out of memory, else 0. */ +static int hold(PureApp *app, PureArena *arena, unsigned slot, PureBytes v, bool append) { + __typeof__(app->held[0]) *h = &app->held[slot]; + size_t mark = arena->used, before = h->size, at = append ? h->size : 0, size = at + (size_t)v.length; + if (v.source == FROM_HELD && v.owner == slot && v.length) { /* a part of itself: by way of the arena */ + if (append_value(app, arena, v) != PURE_OK) return 2; + v = (PureBytes){FROM_ARENA, 0, {0, 0}, mark, v.length}; + } + if (!grows(app, (Held){0, before}, (Held){0, size})) return 1; + if (size > h->capacity) { + size_t capacity = h->capacity ? h->capacity : 64; + while (capacity < size) capacity *= 2; + unsigned char *data = realloc(h->data, capacity); + if (!data) return 2; + h->data = data, h->capacity = capacity; + } + Copy copy = {h->data + at}; + if (visit_value(app, arena, v, copy_out, ©) != PURE_OK) return 2; + h->size = size; + arena->used = mark; + return 0; +} + +/* NUM_TEXT's text of v in a format (the argument past its type, PureFormat): false where an integer's form meets a + * number that is not a whole one within 2^53. */ +static bool formatted(Slot v, uint32_t arg, char *out, size_t *length) { + unsigned type = arg & 3, width = arg >> PURE_FORMAT_WIDTH & 0xFF, form = arg >> PURE_FORMAT_FORM & 7, + digits = arg >> PURE_FORMAT_DIGITS & 31; + char body[400]; + size_t n = 0; + if (form == PURE_FORMAT_FIXED) { + n = (size_t)snprintf(body, sizeof(body), "%.*f", digits ? (int)digits - 1 : 6, v.num == 0 ? 0.0 : v.num); + } else if (form) { + int64_t i = v.i64; + if (type != 2) { + if (!(fabs(v.num) <= 0x1p53 && v.num == floor(v.num))) return false; + i = (int64_t)v.num; + } + uint64_t magnitude = i < 0 ? 0 - (uint64_t)i : (uint64_t)i; + unsigned base = form == PURE_FORMAT_DECIMAL ? 10 : form <= PURE_FORMAT_HEX_UPPER ? 16 : form == PURE_FORMAT_BINARY ? 2 : 8; + const char *set = form == PURE_FORMAT_HEX_UPPER ? "0123456789ABCDEF" : "0123456789abcdef"; + char reversed[64]; + unsigned count = 0; + do reversed[count++] = set[magnitude % base], magnitude /= base; while (magnitude); + if (i < 0) body[n++] = '-'; + while (count) body[n++] = reversed[--count]; + } else + n = type == 2 ? (size_t)snprintf(body, sizeof(body), "%" PRId64, v.i64) + : type ? (size_t)snprintf(body, sizeof(body), "%s", v.num ? "true" : "false") : pure_format_number(v.num, body); + size_t sign = body[0] == '-', pad = width > n ? width - n : 0, k = 0; + if (arg & PURE_FORMAT_ZEROS) { + if (sign) out[k++] = '-'; + while (pad--) out[k++] = '0'; + memcpy(out + k, body + sign, n - sign); + k += n - sign; + } else { + while (pad--) out[k++] = ' '; + memcpy(out + k, body, n); + k += n; + } + *length = k; + return true; +} + /* An output's message from its field values, laid out as a message's * payload (size bytes), numbered and kept with the event's changes. */ static PureResult send(PureApp *app, uint32_t output, const Slot *fields, const PureArena *arena, size_t size) { @@ -3097,7 +3183,17 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp NEXT; } /* A reply outlives the arena mark of later statements: keep it by value. */ - case PURE_OP_REPLY: app->reply = stack[--sp].bytes; app->has_reply = true; NEXT; + case PURE_OP_REPLY: { /* by value; a value held by name may change after it, so its bytes go to the arena */ + PureBytes reply = stack[--sp].bytes; + if (reply.source == FROM_HELD) { + size_t start = arena->used; + if ((status = append_value(app, arena, reply)) != PURE_OK) STOPPED(status); + reply = (PureBytes){FROM_ARENA, 0, {0, 0}, start, reply.length}; + } + app->reply = reply; + app->has_reply = true; + NEXT; + } case PURE_OP_REQUIRE: if (stack[--sp].num == 0) { kind = PURE_FAULT_LIMIT; FAILED; } NEXT; case PURE_OP_TEXT_CONST: stack[sp++].bytes = (PureBytes){FROM_CONSTANT, 0, {0, 0}, m->constant_offset[arg], m->constant_length[arg]}; @@ -3106,6 +3202,13 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp case PURE_OP_TEXT_INPUT: stack[sp++].bytes = (PureBytes){FROM_PAYLOAD, 0, {0, 0}, 0, app->payload.size}; NEXT; case PURE_OP_NUM_TEXT: { char digits[32]; + if (arg > 2) { /* in a format */ + char text[700]; + size_t length; + if (!formatted(*top, arg, text, &length)) { kind = PURE_FAULT_LIMIT; FAILED; } + if ((status = keep_bytes(arena, text, length, &top->bytes)) != PURE_OK) STOPPED(status); + NEXT; + } size_t length = arg == 2 ? (size_t)snprintf(digits, sizeof(digits), "%" PRId64, top->i64) : arg ? (size_t)snprintf(digits, sizeof(digits), "%s", top->num ? "true" : "false") : pure_format_number(top->num, digits); @@ -3495,6 +3598,35 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp if (top->num < 0 || top->num >= (double)pure_list_count(app->lists[arg])) { kind = PURE_FAULT_NOT_FOUND; FAILED; } top->bytes.owner = (uint16_t)arg; NEXT; + case PURE_OP_BYTES_SLICE: { /* from a up to before c, both whole and within */ + Slot high = stack[--sp], low = stack[--sp]; + PureBytes *v = &stack[sp - 1].bytes; + if (arg ? low.i64 < 0 || high.i64 < low.i64 || (uint64_t)high.i64 > v->length + : !(low.num >= 0 && high.num >= low.num && high.num <= (double)v->length) || low.num != floor(low.num) || + high.num != floor(high.num)) { + kind = PURE_FAULT_LIMIT; + FAILED; + } + uint64_t from = arg ? (uint64_t)low.i64 : (uint64_t)low.num, to = arg ? (uint64_t)high.i64 : (uint64_t)high.num; + v->offset += from; + v->length = to - from; + NEXT; + } + case PURE_OP_BYTE: { + if (top->i64 < 0 || top->i64 > 255) { kind = PURE_FAULT_LIMIT; FAILED; } + unsigned char byte = (unsigned char)top->i64; + if ((status = keep_bytes(arena, &byte, 1, &top->bytes)) != PURE_OK) STOPPED(status); + NEXT; + } + case PURE_OP_BYTES_LOAD: + stack[sp++].bytes = (PureBytes){FROM_HELD, (uint16_t)(arg & 0xFF), {0, 0}, 0, app->held[arg & 0xFF].size}; + NEXT; + case PURE_OP_BYTES_STORE: { + int held = hold(app, arena, arg & 0xFF, stack[--sp].bytes, arg & PURE_HELD_APPEND); + if (held == 1) { kind = PURE_FAULT_MEMORY; FAILED; } + if (held) STOPPED(PURE_ALLOCATION); + NEXT; + } default: break; } double b = stack[--sp].num, a = stack[sp - 1].num, value; @@ -3916,6 +4048,7 @@ void pure_app_stop(PureApp *app) { pure_snapshot_release(app->snapshot); } pure_retired_free(&app->retired); + for (unsigned i = 0; i < PURE_MODULE_HELD; ++i) free(app->held[i].data); pure_draft_close(app->stored_values); free(app->carried); pure_draft_close(app->stored_links); @@ -4051,6 +4184,7 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) { * holds for it the default room, or the budget when that is larger. */ app->grown = app->spent = 0; app->room = app->memory > PURE_GROWTH_DEFAULT ? app->memory : PURE_GROWTH_DEFAULT; + for (unsigned i = 0; i < PURE_MODULE_HELD; ++i) app->held[i].size = 0; Mark mark; mark_event(app, &mark); memcpy(pending, app->values, sizeof(app->values)); @@ -4398,6 +4532,7 @@ static PureResult evaluate(PureApp *app, uint32_t view, size_t row, Slot *out) { if (!own_lists(app, m->view_lists[view])) return PURE_ALLOCATION; app->grown = app->spent = 0; app->room = app->memory > PURE_GROWTH_DEFAULT ? app->memory : PURE_GROWTH_DEFAULT; + for (unsigned i = 0; i < PURE_MODULE_HELD; ++i) app->held[i].size = 0; } PureFault fault = app->fault; uint32_t fault_pc = app->fault_pc; diff --git a/core/module.h b/core/module.h index b230fc3..8477b86 100644 --- a/core/module.h +++ b/core/module.h @@ -44,6 +44,7 @@ typedef enum { PURE_OP_I64_COMPARE, PURE_OP_I64_MIN, PURE_OP_I64_MAX, PURE_OP_I64_FROM, PURE_OP_NUM_FROM, PURE_OP_LOOP, PURE_OP_BREAK, PURE_OP_I64_BITS, PURE_OP_BYTE_AT, PURE_OP_BYTES_OF, PURE_OP_INDEX_AT, + PURE_OP_BYTES_SLICE, PURE_OP_BYTE, PURE_OP_BYTES_LOAD, PURE_OP_BYTES_STORE, PURE_OP_COUNT } PureOpcode; /* A bool is its own type (docs/design/016-bool-and-i64.md), held as a @@ -73,6 +74,25 @@ typedef enum { * a position (a number), failing as LIMIT if it is not a whole number and * as NOT_FOUND where there is no item. */ typedef enum { PURE_BITS_AND, PURE_BITS_OR, PURE_BITS_XOR, PURE_BITS_LEFT, PURE_BITS_RIGHT } PureBits; +/* 027's stage 2a. CONCAT with argument 1 joins two bytes values (0: two + * texts). BYTES_SLICE takes a bytes value and two positions a <= c (numbers, + * or i64s with argument 1) and gives its bytes from a up to before c, failing + * as LIMIT unless both are whole and within it. BYTE makes one byte of an + * i64 0-255 (else LIMIT). BYTES_LOAD and BYTES_STORE read and set one of an + * event's PURE_MODULE_HELD values held by name (a let), empty when the event + * or a view's body begins: the argument's bits 0-4 name it, a text below 16 + * bytes and from 16 on a text (so a text one holds UTF-8 alone); STORE with + * bit 8 appends the value to what it holds. What they hold counts in the + * event's room (MEMORY past it). + * NUM_TEXT's argument: bits 0-1 the value's type (as CONST's), 8-15 a width + * the text is padded to on the left, bit 16 padding with zeros after a sign + * (else spaces), 17-19 the form (PureFormat), 20-24 one more than the digits + * after the point of FIXED. A form of an integer takes an i64 or a whole + * number (else LIMIT), in its base with a sign; FIXED takes a number. */ +enum { PURE_MODULE_HELD = 32, PURE_HELD_TEXT = 16, PURE_HELD_APPEND = 1 << 8 }; +typedef enum { PURE_FORMAT_PLAIN, PURE_FORMAT_DECIMAL, PURE_FORMAT_HEX, PURE_FORMAT_HEX_UPPER, PURE_FORMAT_BINARY, + PURE_FORMAT_OCTAL, PURE_FORMAT_FIXED } PureFormat; +enum { PURE_FORMAT_WIDTH = 8, PURE_FORMAT_ZEROS = 1 << 16, PURE_FORMAT_FORM = 17, PURE_FORMAT_DIGITS = 20 }; /* A local: one of an event's PURE_MODULE_LOCALS slots, 0 when the event * begins and never saved. LOCAL_LOAD and LOCAL_STORE name the slot (bits * 0-7), what it holds (bits 8-15: 0 a number, 1 an index, 2 a bool) and an index's @@ -499,6 +519,8 @@ typedef struct { struct PureSnapshot *snapshot; /* the newest snapshot made, or NULL */ PureRetired retired; /* list items let go since it was made */ bool reading; /* a reader: its states are a snapshot's */ + /* The values an event or a view's body holds by name (BYTES_STORE): emptied as it begins. */ + struct { unsigned char *data; size_t size, capacity; } held[PURE_MODULE_HELD]; } PureApp; PureResult pure_app_start(PureApp *, const PureModule *, PureData data); diff --git a/core/test-module.c b/core/test-module.c index dca1b67..02d5015 100644 --- a/core/test-module.c +++ b/core/test-module.c @@ -6131,6 +6131,122 @@ static void bits_and_bytes(void) { printf("{\"bits_and_bytes\":{\"bits\":%zu,\"reads\":%zu,\"refused\":%zu}}\n", LEN(cases), LEN(reads), LEN(refused)); } +/* ---- Formats, slices and values held by name (docs/design/027, stage 2a) -- + * NUM_TEXT writes a number, a bool or an i64 in a width, with zeros or + * spaces, in a base or with fixed digits; an integer's form fails as limit + * on a number that is not whole. BYTES_SLICE takes whole positions inside + * the value; BYTE makes one byte. BYTES_LOAD and BYTES_STORE keep bytes + * and texts by name for the event (a text's slots hold text alone): set, + * appended, set to a part of itself, counted in the room, empty at the next + * event; a reply of one is kept as it was. */ +enum { NTX = PURE_OP_NUM_TEXT, SLC = PURE_OP_BYTES_SLICE, BYT = PURE_OP_BYTE, HLD = PURE_OP_BYTES_LOAD, HST = PURE_OP_BYTES_STORE }; +#define FMT(type, width, zeros, form, digits) ((uint32_t)(type) | (uint32_t)(width) << PURE_FORMAT_WIDTH | \ + ((zeros) ? (uint32_t)PURE_FORMAT_ZEROS : 0) | (uint32_t)(form) << PURE_FORMAT_FORM | (uint32_t)(digits) << PURE_FORMAT_DIGITS) + +static void formats_and_held(void) { + static const struct { int i64; double value; uint32_t format; PureFault fault; const char *text; } shown[] = { + {1, 255, FMT(2, 0, 0, PURE_FORMAT_HEX, 0), PURE_FAULT_NONE, "ff"}, + {1, 255, FMT(2, 8, 1, PURE_FORMAT_HEX, 0), PURE_FAULT_NONE, "000000ff"}, + {1, -255, FMT(2, 6, 1, PURE_FORMAT_HEX_UPPER, 0), PURE_FAULT_NONE, "-000FF"}, + {1, -5, FMT(2, 4, 0, PURE_FORMAT_DECIMAL, 0), PURE_FAULT_NONE, " -5"}, + {1, 5, FMT(2, 0, 0, PURE_FORMAT_BINARY, 0), PURE_FAULT_NONE, "101"}, + {1, 8, FMT(2, 0, 0, PURE_FORMAT_OCTAL, 0), PURE_FAULT_NONE, "10"}, + {1, 0, FMT(2, 3, 1, PURE_FORMAT_PLAIN, 0), PURE_FAULT_NONE, "000"}, + {0, 2.5, FMT(0, 0, 0, PURE_FORMAT_FIXED, 3), PURE_FAULT_NONE, "2.50"}, + {0, 1.005, FMT(0, 0, 0, PURE_FORMAT_FIXED, 3), PURE_FAULT_NONE, "1.00"}, /* the binary value is below 1.005 */ + {0, -0.0, FMT(0, 0, 0, PURE_FORMAT_FIXED, 2), PURE_FAULT_NONE, "0.0"}, + {0, -1.25, FMT(0, 7, 1, PURE_FORMAT_FIXED, 2), PURE_FAULT_NONE, "-0001.2"}, + {0, 3, FMT(0, 0, 0, PURE_FORMAT_FIXED, 0), PURE_FAULT_NONE, "3.000000"}, + {0, 0.1, FMT(0, 6, 0, PURE_FORMAT_PLAIN, 0), PURE_FAULT_NONE, " 0.1"}, + {0, 42, FMT(0, 0, 0, PURE_FORMAT_HEX, 0), PURE_FAULT_NONE, "2a"}, + {0, 2.5, FMT(0, 0, 0, PURE_FORMAT_HEX, 0), PURE_FAULT_LIMIT, ""}, + {0, 1, FMT(1, 6, 0, PURE_FORMAT_PLAIN, 0), PURE_FAULT_NONE, " true"}, + }; + for (size_t k = 0; k < LEN(shown); ++k) { + O value = shown[k].i64 ? IC((int64_t)shown[k].value) : (O){C, shown[k].format & 3, shown[k].value}; + O code[] = {value, {NTX, shown[k].format, 0}, {PURE_OP_TEXT_SET, 4, 0}}; + Byted ran = run_bytes(code, LEN(code)); + if (ran.fault != shown[k].fault || strcmp(ran.said, shown[k].text)) { + fprintf(stderr, "format %zu: fault %d, '%s'\n", k, (int)ran.fault, ran.said); + abort(); + } + } + /* Slices of a constant's bytes ("Az\xc3\xa9") and of a text state's, by numbers or i64s; one byte. */ + static const struct { uint32_t text_state, as_i64; double from, to; PureFault fault; const char *text; } slices[] = { + {0, 0, 0, 2, PURE_FAULT_NONE, "Az"}, {0, 1, 2, 4, PURE_FAULT_NONE, "\xc3\xa9"}, {1, 0, 1, 1, PURE_FAULT_NONE, ""}, + {1, 1, 0, 4, PURE_FAULT_NONE, "Az\xc3\xa9"}, {0, 0, 2, 1, PURE_FAULT_LIMIT, ""}, {0, 0, 0, 5, PURE_FAULT_LIMIT, ""}, + {0, 0, 0.5, 2, PURE_FAULT_LIMIT, ""}, {0, 1, -1, 2, PURE_FAULT_LIMIT, ""}, {0, 0, 2, 3, PURE_FAULT_ENCODING, ""}, + }; + for (size_t k = 0; k < LEN(slices); ++k) { + O from = slices[k].as_i64 ? IC((int64_t)slices[k].from) : (O){C, 0, slices[k].from}; + O to = slices[k].as_i64 ? IC((int64_t)slices[k].to) : (O){C, 0, slices[k].to}; + O code[] = {slices[k].text_state ? (O){TL, 1, 0} : (O){TK, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, from, to, + {SLC, slices[k].as_i64, 0}, {PURE_OP_TEXT_FROM, 0, 0}, {PURE_OP_TEXT_SET, 4, 0}}; + Byted ran = run_bytes(code, LEN(code)); + CHECK(ran.fault == slices[k].fault && !strcmp(ran.said, slices[k].text)); + } + O bytes_joined[] = {IC('O'), {BYT, 0, 0}, IC('k'), {BYT, 0, 0}, {CAT, 1, 0}, {PURE_OP_TEXT_FROM, 0, 0}, {PURE_OP_TEXT_SET, 4, 0}}; + CHECK(!strcmp(run_bytes(bytes_joined, LEN(bytes_joined)).said, "Ok")); + O past[] = {IC(256), {BYT, 0, 0}, {PURE_OP_TEXT_FROM, 0, 0}, {PURE_OP_TEXT_SET, 4, 0}}; + CHECK(run_bytes(past, LEN(past)).fault == PURE_FAULT_LIMIT); + /* Held: bytes slot 3 set to "Az", appended "Az\xc3\xa9", set to a part of itself, then shown; a text slot. */ + O held[] = {{TK, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {C, 0, 0}, {C, 0, 2}, {SLC, 0, 0}, {HST, 3, 0}, + {TK, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {HST, 3 | PURE_HELD_APPEND, 0}, + {HLD, 3, 0}, {HLD, 3, 0}, {CAT, 1, 0}, {HST, 3, 0}, /* "AzAz\xc3\xa9" twice */ + {HLD, 3, 0}, {C, 0, 1}, {C, 0, 6}, {SLC, 0, 0}, {HST, 3 | PURE_HELD_APPEND, 0}, /* itself, appended */ + {HLD, 3, 0}, {C, 0, 2}, {C, 0, 8}, {SLC, 0, 0}, {HST, 3, 0}, /* a part of itself */ + {HLD, 3, 0}, {PURE_OP_TEXT_FROM, 0, 0}, {HST, 20, 0}, {HLD, 20, 0}, {PURE_OP_TEXT_SET, 4, 0}}; + Byted ran = run_bytes(held, LEN(held)); + CHECK(ran.fault == PURE_FAULT_NONE && !strcmp(ran.said, "Az\xc3\xa9" "Az")); + /* Held values start empty each event; doubling past the room fails as memory and leaves nothing. */ + static const S nums[] = {{"n", 0}}; + static const T others[] = {{"note", 1, 0}, {"bytes", 3, 0}, {"i", PURE_STATE_I64, 0}, {"said", 1, 1}}; + static const RF byte_fields[] = {{"v", 3, 0}}; + static const R records[] = {{"Byte", byte_fields, 1}}; + static const char *const constants[] = {"Az\xc3\xa9", ""}; + static const I inputs[] = {KEY("go", "KeyG"), KEY("grow", "KeyH"), {"ask", NULL, PURE_KIND_MESSAGE, false, NULL, 0}}; + static const O code[] = { + /* 0 go: held5 = held5 + bytes(note); said = text(held5) */ + {HLD, 5, 0}, {TK, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {CAT, 1, 0}, {HST, 5, 0}, {HLD, 5, 0}, {PURE_OP_TEXT_FROM, 0, 0}, + {PURE_OP_TEXT_SET, 4, 0}, + /* 8 grow: held6 = bytes(note); for 40 rounds held6 = held6 + held6; said = "x" */ + {TK, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {HST, 6, 0}, {C, 0, 0}, {C, 0, 40}, {PURE_OP_LOOP, 0 | 2u << 16, 0}, + {HLD, 6, 0}, {HST, 6 | PURE_HELD_APPEND, 0}, {TK, 0, 0}, {PURE_OP_TEXT_SET, 4, 0}, + /* 18 ask: held7 = bytes(note); reply held7; held7 = "" */ + {TK, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {HST, 7, 0}, {HLD, 7, 0}, {PURE_OP_REPLY, 0, 0}, {TK, 1, 0}, + {PURE_OP_BYTES_FROM, 0, 0}, {HST, 7, 0}, + }; + static const H handlers[] = {{1, 0, 8}, {3, 8, 10}, {5, 18, 8}}; + Spec spec = {nums, 1, inputs, LEN(inputs), NULL, 0, handlers, LEN(handlers), code, LEN(code), NULL, 0, others, LEN(others), + constants, LEN(constants), NULL, 0, records, 1, NULL, 0}; + if (load(spec, &module_b) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } + PureApp app; + CHECK(pure_app_start(&app, &module_b, (PureData){0}) == PURE_OK); + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK); + CHECK(!strcmp(state_text(&app, 4), "Az\xc3\xa9")); /* held5 began empty again */ + CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_BUDGET && app.fault == PURE_FAULT_MEMORY); + CHECK(!strcmp(state_text(&app, 4), "Az\xc3\xa9")); /* nothing changed */ + CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){NULL, 0}) == PURE_OK); + Text replied = {{0}, 0}; + CHECK(pure_app_reply(&app, text_span, &replied) == PURE_OK && !strcmp(replied.text, "Az\xc3\xa9")); + pure_app_stop(&app); + /* Refused: bytes into a text's slot, a slot past the last, a format that does not fit its type. */ + const O refused[][3] = { + {{TK, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {HST, PURE_HELD_TEXT, 0}}, {{TK, 0, 0}, {HST, 3, 0}, {C, 0, 0}}, + {{TK, 0, 0}, {HST, PURE_MODULE_HELD, 0}, {C, 0, 0}}, {{C, 1, 1}, {NTX, FMT(1, 0, 0, PURE_FORMAT_HEX, 0), 0}, {PURE_OP_TEXT_SET, 4, 0}}, + {IC(1), {NTX, FMT(2, 0, 0, PURE_FORMAT_FIXED, 2), 0}, {PURE_OP_TEXT_SET, 4, 0}}, + {IC(1), {NTX, FMT(2, 0, 0, PURE_FORMAT_HEX, 2), 0}, {PURE_OP_TEXT_SET, 4, 0}}, + {{TK, 0, 0}, {TK, 0, 0}, {CAT, 1, 0}}, + }; + for (size_t k = 0; k < LEN(refused); ++k) { + H one = {1, 0, 3}; + Spec bad = {nums, 1, inputs, 1, NULL, 0, &one, 1, refused[k], 3, NULL, 0, others, LEN(others), constants, + LEN(constants), NULL, 0, records, 1, NULL, 0}; + if (load(bad, &module_b) == PURE_OK) { fprintf(stderr, "formats and held: refused %zu loaded\n", k); abort(); } + } + printf("{\"formats_and_held\":{\"formats\":%zu,\"slices\":%zu,\"refused\":%zu}}\n", LEN(shown), LEN(slices), LEN(refused)); +} + /* 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 }; @@ -6505,6 +6621,7 @@ int main(void) { bools(); integers(); bits_and_bytes(); + formats_and_held(); memory_budget(); work_budget(); fold_mutations(); diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index fa6af3a..04b75ee 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -22,7 +22,8 @@ v6은 **bool**을 수와 다른 스택 종류로 더한다([016](016-bool-and-i6 v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계): 새 명령, 새 타입 번호, 0이던 자리의 값(CONST와 NUM_TEXT의 인자 2, 집계의 PURE_FOLD_INTEGER)이라 판은 그대로다. 그 뒤 [027](027-computation-core.md)의 1단계로 **비트 연산, 자리의 바이트, i64 리스트의 바이트, 자리로 찾은 항목**이 더해졌다(새 명령 -넷, 90–93). 판은 그대로다. +넷, 90–93). 2a로 **바이트 자르기, 한 바이트, 이름으로 담은 바이트와 text**(94–97), CONCAT의 바이트, NUM_TEXT의 형식이 더해졌다. 판은 +그대로다. ## 바이트 배치 @@ -78,8 +79,8 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계): | 30 | TEXT_CONST | → t | 상수 `인자` | | 31 | TEXT_LOAD | → t | text 상태 `인자`의 현재 값(복사 없는 버전) | | 32 | TEXT_INPUT | → t | text 입력의 내용. text 입력 핸들러 전용 | -| 33 | NUM_TEXT | n → t | 가장 짧은 왕복 10진수. 1e15 미만 정수는 소수점·지수 없이, −0은 `0`. `인자`가 1이면 bool → t: `true`·`false` | -| 34 | CONCAT | t t → t | | +| 33 | NUM_TEXT | n → t | 가장 짧은 왕복 10진수. 1e15 미만 정수는 소수점·지수 없이, −0은 `0`. `인자`의 비트 0–1이 1이면 bool → t: `true`·`false`, 2면 i64. 그 위는 형식([027](027-computation-core.md)): 비트 8–15 폭(왼쪽을 채움), 16 부호 뒤를 0으로 채움, 17–19 꼴(PureFormat: 10진, 16진, 대문자 16진, 2진, 8진, 고정 소수), 20–24 고정 소수의 자릿수 + 1. 정수의 꼴에 정수가 아닌 수는 `limit` | +| 34 | CONCAT | t t → t 또는 b b → b | `인자` 1이면 바이트끼리 | | 35–37 | CHARS LINES TEXT_BYTES | t → n | 글자(Unicode scalar) 수, 줄 수(줄바꿈 + 1), 바이트 수 | | 38 | CUR_LOAD | → c | 커서 상태 `인자` | | 39–40 | CUR_START CUR_END | → c | text 상태 `인자`의 처음·끝 | @@ -131,6 +132,10 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계): | 91 | BYTE_AT | b n 또는 b i64 → i64 | 바이트 값의 그 자리 바이트(0–255). `인자` 0이면 자리가 수, 1이면 i64. 정수가 아니거나 값 밖이면 `limit` | | 92 | BYTES_OF | → b | 리스트 상태 `인자`(레코드가 i64 필드 하나)의 항목들을 한 바이트씩. 0–255 밖의 값은 `limit` | | 93 | INDEX_AT | n → i | 리스트 상태 `인자`의 그 자리 항목의 인덱스. 정수가 아니면 `limit`, 그 자리에 항목이 없으면 `not_found` | +| 94 | BYTES_SLICE | b n n 또는 b i64 i64 → b | a부터 c 앞까지의 바이트. `인자` 1이면 자리가 i64. 둘 다 정수이고 0 ≤ a ≤ c ≤ 길이가 아니면 `limit` | +| 95 | BYTE | i64 → b | 한 바이트. 0–255 밖이면 `limit` | +| 96 | BYTES_LOAD | → b 또는 t | 이름으로 담은 값 `인자`(0–15 바이트, 16–31 text). 이벤트와 보기의 본문이 시작할 때 비어 있다 | +| 97 | BYTES_STORE | b 또는 t → | 이름으로 담은 값 `인자`의 비트 0–4를 그 값으로(비트 8이면 뒤에 덧붙여). 담은 바이트는 이벤트의 몫에 세고, 넘으면 `memory`. text 칸에는 text만 | bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이트의 정수다. CONST의 인자 2는 i64 상수(값의 8바이트), NUM_TEXT의 인자 2는 i64의 10진 글자다. LOAD·STORE·지역 칸·항목·FIELD·SEND는 i64를 수처럼 옮긴다. FOLD의 종류에 64(PURE_FOLD_INTEGER)를 |
