From defb2cf06a9b5db9429ca368d8c2af127a5c9c3d Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 19 Oct 2026 01:53:40 +0100 Subject: 027's 5c: money as i64 minor units shown by .Nd: NUM_TEXT's decimal form takes digits (bits 20-24 as FIXED's), an integer's last N digits written after a point (1234 at two is 12.34, -5 -0.05, 0 0.00; width and zeros after the sign), verification allowing digits on FIXED and DECIMAL alone; unit test: ten rows of formats_and_held (-2^63 at two, 25 digits, a number's whole 1234, its 12.5 a limit); TPC-C's money i64 cents and its rates ten-thousandths (tables, messages, load, the transactions), SQLite's INTEGER columns and int64 bindings alike, the comparison now exact (no 1e-9 tolerance): after 20,000 transactions every row of every table the same (grouped SQLite too) and 3.3.2 holding on both; counts: b45 against b44 BFS without batches +0.09%, the rest within 0.001%; the bank's queries (s44 and s45 in turns, six each) in_credit -0.59%, per_branch -0.34%, ranks -0.20%, the rest within 0.01%, the seven answers the same bytes; 008 (NUM_TEXT's digits, a message row's i64 cells), 027 and 092 --- benchmarks/tpcc/data.py | 28 ++++++++++---------- benchmarks/tpcc/run.py | 27 +++++++++---------- benchmarks/tpcc/sqlite-tpcc.c | 24 ++++++++--------- benchmarks/tpcc/tpcc-load.c | 4 +-- benchmarks/tpcc/tpcc.aup | 30 ++++++++++++---------- core/module.c | 15 +++++++---- core/module.h | 6 +++-- core/test-module.c | 14 +++++++++- docs/design/008-module-format.md | 6 ++--- docs/design/027-computation-core.md | 12 +++++++++ docs/research/092-tpcc.md | 2 ++ .../040-against-c/2026-10-19-counts-s45.jsonl | 12 +++++++++ .../041-batches/2026-10-19-counts.jsonl | 10 ++++++++ 13 files changed, 124 insertions(+), 66 deletions(-) create mode 100644 research/experiments/040-against-c/2026-10-19-counts-s45.jsonl diff --git a/benchmarks/tpcc/data.py b/benchmarks/tpcc/data.py index d8a4ccb..82ebb07 100644 --- a/benchmarks/tpcc/data.py +++ b/benchmarks/tpcc/data.py @@ -1,13 +1,15 @@ """TPC-C's initial database (clause 4.3.3.1) for W warehouses, as rows of the records in tpcc.aup (each composite key packed into one number, as there), and the same rows for SQLite's tables with their own keys. Texts the -transactions only carry along are left out (see tpcc.aup).""" +transactions only carry along are left out (see tpcc.aup). Money is in +cents and a rate in ten-thousandths, integers (i64s in tpcc.aup, INTEGER in +SQLite).""" import random import struct ITEMS, CUSTOMERS, ORDERS, DELIVERED = 100000, 3000, 3000, 2100 -# Each table's fields in record order (tpcc.aup), all numbers. +# Each table's fields in record order (tpcc.aup), numbers but for the i64s in INTEGERS. FIELDS = { 'warehouses': ('id', 'tax', 'ytd'), 'districts': ('id', 'tax', 'ytd', 'next_order', 'next_delivery'), @@ -18,6 +20,7 @@ FIELDS = { 'new_orders': ('id',), 'lines': ('id', 'item', 'supply', 'delivery', 'quantity', 'amount'), } +INTEGERS = {'tax', 'ytd', 'discount', 'balance', 'ytd_payment', 'price', 'amount'} def district_key(w, d): @@ -36,13 +39,12 @@ def generate(warehouses, seed=1, now=1.7e12): """The tables as lists of tuples in FIELDS order.""" rng = random.Random(seed) tables = {name: [] for name in FIELDS} - tables['items'] = [(i, round(rng.uniform(1, 100), 2)) for i in range(1, ITEMS + 1)] + tables['items'] = [(i, rng.randint(100, 10000)) for i in range(1, ITEMS + 1)] for w in range(1, warehouses + 1): - tables['warehouses'].append((w, round(rng.uniform(0, 0.2), 4), 300000.0)) + tables['warehouses'].append((w, rng.randint(0, 2000), 30000000)) tables['stock'].extend((w * 100000 + i, rng.randint(10, 100), 0, 0, 0) for i in range(1, ITEMS + 1)) for d in range(1, 11): - tables['districts'].append((district_key(w, d), round(rng.uniform(0, 0.2), 4), 30000.0, ORDERS + 1, - DELIVERED + 1)) + tables['districts'].append((district_key(w, d), rng.randint(0, 2000), 3000000, ORDERS + 1, DELIVERED + 1)) owners = list(range(1, CUSTOMERS + 1)) rng.shuffle(owners) last = {} @@ -55,14 +57,14 @@ def generate(warehouses, seed=1, now=1.7e12): tables['new_orders'].append((key,)) for n in range(1, count + 1): tables['lines'].append((key * 16 + n, rng.randint(1, ITEMS), w, now if delivered else 0, 5, - 0 if delivered else round(rng.uniform(0.01, 9999.99), 2))) + 0 if delivered else rng.randint(1, 999999))) for c in range(1, CUSTOMERS + 1): - tables['customers'].append((customer_key(w, d, c), round(rng.uniform(0, 0.5), 4), - 0 if rng.random() < 0.1 else 1, -10.0, 10.0, 1, 0, last[c])) + tables['customers'].append((customer_key(w, d, c), rng.randint(0, 5000), + 0 if rng.random() < 0.1 else 1, -1000, 1000, 1, 0, last[c])) return tables -def packed(rows): - """Rows in a list's stored form (each number's binary64, little-endian).""" - width = len(rows[0]) - return struct.pack(f'<{width * len(rows)}d', *(v for row in rows for v in row)) +def packed(table, rows): + """A table's rows in a list's stored form (each number's binary64, each i64's two's complement, little-endian).""" + row = '<' + ''.join('q' if field in INTEGERS else 'd' for field in FIELDS[table]) + return b''.join(struct.pack(row, *r) for r in rows) diff --git a/benchmarks/tpcc/run.py b/benchmarks/tpcc/run.py index 377d555..0c68552 100644 --- a/benchmarks/tpcc/run.py +++ b/benchmarks/tpcc/run.py @@ -50,13 +50,13 @@ sys.path.insert(0, str(HERE)) import data # noqa: E402 SCHEMA = ''' -CREATE TABLE warehouse (w_id INTEGER PRIMARY KEY, w_tax REAL, w_ytd REAL); -CREATE TABLE district (d_w_id INTEGER, d_id INTEGER, d_tax REAL, d_ytd REAL, d_next_o_id INTEGER, d_next_delivery INTEGER, +CREATE TABLE warehouse (w_id INTEGER PRIMARY KEY, w_tax INTEGER, w_ytd INTEGER); +CREATE TABLE district (d_w_id INTEGER, d_id INTEGER, d_tax INTEGER, d_ytd INTEGER, d_next_o_id INTEGER, d_next_delivery INTEGER, PRIMARY KEY (d_w_id, d_id)) WITHOUT ROWID; -CREATE TABLE customer (c_w_id INTEGER, c_d_id INTEGER, c_id INTEGER, c_discount REAL, c_credit INTEGER, c_balance REAL, - c_ytd_payment REAL, c_payment_cnt INTEGER, c_delivery_cnt INTEGER, c_last_order INTEGER, +CREATE TABLE customer (c_w_id INTEGER, c_d_id INTEGER, c_id INTEGER, c_discount INTEGER, c_credit INTEGER, c_balance INTEGER, + c_ytd_payment INTEGER, c_payment_cnt INTEGER, c_delivery_cnt INTEGER, c_last_order INTEGER, PRIMARY KEY (c_w_id, c_d_id, c_id)) WITHOUT ROWID; -CREATE TABLE item (i_id INTEGER PRIMARY KEY, i_price REAL); +CREATE TABLE item (i_id INTEGER PRIMARY KEY, i_price INTEGER); CREATE TABLE stock (s_w_id INTEGER, s_i_id INTEGER, s_quantity INTEGER, s_ytd INTEGER, s_order_cnt INTEGER, s_remote_cnt INTEGER, PRIMARY KEY (s_w_id, s_i_id)) WITHOUT ROWID; CREATE TABLE orders (o_w_id INTEGER, o_d_id INTEGER, o_id INTEGER, o_c_id INTEGER, o_entry_d REAL, o_carrier_id INTEGER, @@ -64,10 +64,10 @@ CREATE TABLE orders (o_w_id INTEGER, o_d_id INTEGER, o_id INTEGER, o_c_id INTEGE CREATE TABLE new_order (no_w_id INTEGER, no_d_id INTEGER, no_o_id INTEGER, PRIMARY KEY (no_w_id, no_d_id, no_o_id)) WITHOUT ROWID; CREATE TABLE order_line (ol_w_id INTEGER, ol_d_id INTEGER, ol_o_id INTEGER, ol_number INTEGER, ol_i_id INTEGER, - ol_supply_w_id INTEGER, ol_delivery_d REAL, ol_quantity INTEGER, ol_amount REAL, + ol_supply_w_id INTEGER, ol_delivery_d REAL, ol_quantity INTEGER, ol_amount INTEGER, PRIMARY KEY (ol_w_id, ol_d_id, ol_o_id, ol_number)) WITHOUT ROWID; CREATE TABLE history (h_c_w_id INTEGER, h_c_d_id INTEGER, h_c_id INTEGER, h_w_id INTEGER, h_d_id INTEGER, h_date REAL, - h_amount REAL); + h_amount INTEGER); ''' ORDERS = 10000000 # an order's key is its district's times this, plus its number @@ -142,7 +142,7 @@ class Pure: continue chunk = max(1, (1400000 // 8) // len(data.FIELDS[name]) - 1) for at in range(0, len(rows), chunk): - status, body = self.call('POST', f'/inputs/load_{name}', data.packed(rows[at:at + chunk]), + status, body = self.call('POST', f'/inputs/load_{name}', data.packed(name, rows[at:at + chunk]), 'application/octet-stream') assert status == 200, (name, status, body[:200]) @@ -254,19 +254,16 @@ def asked(pure, name, page=200000): return rows -def same(a, b): - return a == b or abs(a - b) <= 1e-9 * max(1.0, abs(a), abs(b)) - - def compare(table, pure_rows, sqlite_rows_): - """Rows that differ between the two (a table's rows in key order; history's sorted).""" + """Rows that differ between the two (a table's rows in key order; history's sorted): every value compared is an + integer (money in cents, Pure 027's stage 5), so equal exactly.""" if table in ('lines',): # a line's delivery time differs; whether it has one must not pure_rows = [(*r[:-1], r[-1] > 0) for r in pure_rows] sqlite_rows_ = [(*r[:-1], r[-1] > 0) for r in sqlite_rows_] pure_rows, sqlite_rows_ = sorted(pure_rows), sorted(sqlite_rows_) if len(pure_rows) != len(sqlite_rows_): return [('count', len(pure_rows), len(sqlite_rows_))] - return [(p, s) for p, s in zip(pure_rows, sqlite_rows_) if not all(same(x, y) for x, y in zip(p, s))] + return [(p, s) for p, s in zip(pure_rows, sqlite_rows_) if p != s] def device_written(directory): @@ -294,7 +291,7 @@ def consistency(pure, db, sqlite_only=False): sides = [] if sqlite_only else [('pure', asked(pure, 'district_check'))] for system, districts in sides + [('sqlite', db.execute(DISTRICTS[1]).fetchall())]: for key, next_order, newest, newest_new, oldest_new, new_orders, ordered, lines, ytd, district_ytd in districts: - if not same(ytd, district_ytd): + if ytd != district_ytd: failures.append((system, '3.3.2.1', ytd, district_ytd)) if not next_order - 1 == newest == newest_new: failures.append((system, '3.3.2.2', key, next_order, newest, newest_new)) diff --git a/benchmarks/tpcc/sqlite-tpcc.c b/benchmarks/tpcc/sqlite-tpcc.c index f497ae5..fb969fc 100644 --- a/benchmarks/tpcc/sqlite-tpcc.c +++ b/benchmarks/tpcc/sqlite-tpcc.c @@ -16,7 +16,7 @@ * commit, as pure-serve shows views from the last save (docs/research/106). * Departures as in tpcc.aup: customers by number, no texts carried along, * a customer's last order kept in c_last_order, Delivery a district a - * transaction. With TRANSACTIONS each terminal stops after that many; the + * transaction; money in cents and rates in ten-thousandths, as INTEGERs. With TRANSACTIONS each terminal stops after that many; the * inputs are drawn in tpcc-load's order. SQLITE_PRAGMAS, when set, is run on * each connection after its own (mmap_size, cache_size). Prints one JSON line * as tpcc-load does, with the CPU seconds of the run, the peak resident @@ -180,13 +180,13 @@ static bool read_only(Terminal *t, int kind, int w, int d, uint64_t *r) { sqlite3_bind_int(s, 1, w), sqlite3_bind_int(s, 2, d), sqlite3_bind_int(s, 3, nurand(r, 1023, 1, 3000, 259)); if (!step(s, &row) || !row) return false; sqlite3_int64 order = sqlite3_column_int64(s, 1); - (void)sqlite3_column_double(s, 0); + (void)sqlite3_column_int64(s, 0); sqlite3_reset(s); s = t->lines_of; sqlite3_reset(s); sqlite3_bind_int(s, 1, w), sqlite3_bind_int(s, 2, d), sqlite3_bind_int64(s, 3, order); int result; - while ((result = sqlite3_step(s)) == SQLITE_ROW) (void)sqlite3_column_double(s, 3); + while ((result = sqlite3_step(s)) == SQLITE_ROW) (void)sqlite3_column_int64(s, 3); sqlite3_reset(s); return result == SQLITE_DONE; } @@ -239,7 +239,7 @@ static bool body(Terminal *t, int kind, int w, int d, uint64_t *r, bool *undo) { *undo = unused; return unused; } - double price = sqlite3_column_double(s, 0); + sqlite3_int64 price = sqlite3_column_int64(s, 0); /* cents */ sqlite3_reset(s); s = t->stock_of; sqlite3_reset(s); @@ -259,7 +259,7 @@ static bool body(Terminal *t, int kind, int w, int d, uint64_t *r, bool *undo) { sqlite3_reset(s); sqlite3_bind_int(s, 1, w), sqlite3_bind_int(s, 2, d), sqlite3_bind_int64(s, 3, order), sqlite3_bind_int(s, 4, n); sqlite3_bind_int(s, 5, item), sqlite3_bind_int(s, 6, supply), sqlite3_bind_int(s, 7, quantity); - sqlite3_bind_double(s, 8, quantity * price); + sqlite3_bind_int64(s, 8, quantity * price); ok = ok && RUN(s); } s = t->order_add; @@ -281,23 +281,23 @@ static bool body(Terminal *t, int kind, int w, int d, uint64_t *r, bool *undo) { do cw = uniform(r, 1, warehouses); while (cw == w); cd = uniform(r, 1, 10); } - double amount = uniform(r, 100, 500000) / 100.0; + sqlite3_int64 amount = uniform(r, 100, 500000); /* cents */ sqlite3_stmt *s = t->warehouse_pay; sqlite3_reset(s); - sqlite3_bind_int(s, 1, w), sqlite3_bind_double(s, 2, amount); + sqlite3_bind_int(s, 1, w), sqlite3_bind_int64(s, 2, amount); ok = RUN(s); s = t->district_pay; sqlite3_reset(s); - sqlite3_bind_int(s, 1, w), sqlite3_bind_int(s, 2, d), sqlite3_bind_double(s, 3, amount); + sqlite3_bind_int(s, 1, w), sqlite3_bind_int(s, 2, d), sqlite3_bind_int64(s, 3, amount); ok = ok && RUN(s); s = t->customer_pay; sqlite3_reset(s); - sqlite3_bind_int(s, 1, cw), sqlite3_bind_int(s, 2, cd), sqlite3_bind_int(s, 3, c), sqlite3_bind_double(s, 4, amount); + sqlite3_bind_int(s, 1, cw), sqlite3_bind_int(s, 2, cd), sqlite3_bind_int(s, 3, c), sqlite3_bind_int64(s, 4, amount); ok = ok && RUN(s) && sqlite3_changes(t->db) == 1; s = t->history_add; sqlite3_reset(s); sqlite3_bind_int(s, 1, cw), sqlite3_bind_int(s, 2, cd), sqlite3_bind_int(s, 3, c), sqlite3_bind_int(s, 4, w); - sqlite3_bind_int(s, 5, d), sqlite3_bind_double(s, 6, now_ms()), sqlite3_bind_double(s, 7, amount); + sqlite3_bind_int(s, 5, d), sqlite3_bind_double(s, 6, now_ms()), sqlite3_bind_int64(s, 7, amount); ok = ok && RUN(s); } else { /* DELIVERY, one district */ int carrier = uniform(r, 1, 10); @@ -332,11 +332,11 @@ static bool body(Terminal *t, int kind, int w, int d, uint64_t *r, bool *undo) { sqlite3_reset(s); sqlite3_bind_int(s, 1, w), sqlite3_bind_int(s, 2, d), sqlite3_bind_int64(s, 3, order); ok = ok && step(s, &row) && row; - double total = ok ? sqlite3_column_double(s, 0) : 0; + sqlite3_int64 total = ok ? sqlite3_column_int64(s, 0) : 0; sqlite3_reset(s); s = t->customer_deliver; sqlite3_reset(s); - sqlite3_bind_int(s, 1, w), sqlite3_bind_int(s, 2, d), sqlite3_bind_int(s, 3, c), sqlite3_bind_double(s, 4, total); + sqlite3_bind_int(s, 1, w), sqlite3_bind_int(s, 2, d), sqlite3_bind_int(s, 3, c), sqlite3_bind_int64(s, 4, total); ok = ok && RUN(s); } } diff --git a/benchmarks/tpcc/tpcc-load.c b/benchmarks/tpcc/tpcc-load.c index 176ec1e..d3cee99 100644 --- a/benchmarks/tpcc/tpcc-load.c +++ b/benchmarks/tpcc/tpcc-load.c @@ -118,8 +118,8 @@ static void prepare(Connection *c) { do cw = uniform(r, 1, warehouses); while (cw == w); cd = uniform(r, 1, 10); } - double amount = uniform(r, 100, 500000) / 100.0; - length = snprintf(body, sizeof(body), "{\"w\":%d,\"d\":%d,\"cw\":%d,\"cd\":%d,\"c\":%d,\"amount\":%.2f}", w, d, cw, + int amount = uniform(r, 100, 500000); /* cents */ + length = snprintf(body, sizeof(body), "{\"w\":%d,\"d\":%d,\"cw\":%d,\"cd\":%d,\"c\":%d,\"amount\":%d}", w, d, cw, cd, customer, amount); path = "/inputs/payment"; } else if (c->kind == DELIVERY) { diff --git a/benchmarks/tpcc/tpcc.aup b/benchmarks/tpcc/tpcc.aup index b3e75aa..a67dc2e 100644 --- a/benchmarks/tpcc/tpcc.aup +++ b/benchmarks/tpcc/tpcc.aup @@ -3,6 +3,10 @@ # w * 10 + d, a customer district * 3000 + c, an order district * 10^7 + o, # an order line order * 16 + number, a stock item w * 100000 + i. # +# Money is an i64 of cents and a rate (tax, discount) an i64 of +# ten-thousandths (the specification's decimals, exactly: Pure 027's +# stage 5), as SQLite's INTEGER columns hold them. +# # Where this departs from the specification: a customer is always chosen by # number (never by last name), texts the transactions only carry along # (names, addresses, data) are left out, a customer keeps its last order, @@ -11,29 +15,29 @@ record Warehouse { id = 0 - tax = 0 - ytd = 0 + tax: i64 + ytd: i64 } record District { id = 0 - tax = 0 - ytd = 0 + tax: i64 + ytd: i64 next_order = 3001 next_delivery = 2101 } record Customer { id = 0 - discount = 0 + discount: i64 credit = 1 - balance = -10 - ytd_payment = 10 + balance: i64 = -1000 + ytd_payment: i64 = 1000 payments = 1 deliveries = 0 last_order = 0 } record Item { id = 0 - price = 0 + price: i64 } record Stock { id = 0 @@ -59,13 +63,13 @@ record Line { supply = 0 delivery = 0 quantity = 0 - amount = 0 + amount: i64 } record History { customer = 0 district = 0 date = 0 - amount = 0 + amount: i64 } record Ordered { item = 0 @@ -183,7 +187,7 @@ on new_order { } count = count + 1 lines.add(Line { id: order * 16 + count, item: l.item, supply: l.supply, quantity: l.quantity, - amount: l.quantity * price }) + amount: i64(l.quantity) * price }) } orders.add(Order { id: order, customer: customer.id, entry: now, lines: count, local: local }) new_orders.add(NewOrder { id: order }) @@ -198,7 +202,7 @@ input payment = message { cw: u32 cd: u32 c: u32 - amount: number + amount: i64 } on payment { let warehouse = warehouses[payment.w] @@ -227,7 +231,7 @@ on delivery { new_orders.remove(new_orders[order]) let o = orders[order] o.carrier = delivery.carrier - let total = 0 + let total = i64(0) for l in lines[order * 16 .. order * 16 + 16] { l.delivery = now total = total + l.amount diff --git a/core/module.c b/core/module.c index ca5b0e2..3657843 100644 --- a/core/module.c +++ b/core/module.c @@ -453,10 +453,10 @@ PURE_COLD static bool names_arg(const PureModule *m, unsigned kind, uint32_t arg case A_POSITION: case A_JOINED: return arg <= 1; case A_HELD: return (arg & ~(uint32_t)PURE_HELD_APPEND) < PURE_MODULE_HELD; case A_FUNCTION: return arg < m->functions; - case A_FORMAT: { /* a type; a width, zeros, a form fitting it and digits for FIXED alone */ + case A_FORMAT: { /* a type; a width, zeros, a form fitting it and digits for FIXED and DECIMAL 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); + (form != PURE_FORMAT_FIXED || type == 0) && (form == PURE_FORMAT_FIXED || form == PURE_FORMAT_DECIMAL || !digits); } case A_RELATION: return arg <= PURE_NOT_EQUAL; case A_KEYED: return state_of(m, arg, PURE_STATE_LIST) && m->state_key[arg]; @@ -2942,7 +2942,8 @@ static __attribute__((cold, noinline)) int kept_step(PureApp *app, PureArena *ar } /* 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. */ + * number that is not a whole one within 2^53. DECIMAL with digits writes the integer in minor units: its last + * digits after a point (money's cents: 1234 at two is 12.34, -5 is -0.05). */ 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; @@ -2960,10 +2961,14 @@ static bool formatted(Slot v, uint32_t arg, char *out, size_t *length) { 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; + unsigned count = 0, places = form == PURE_FORMAT_DECIMAL && digits ? digits - 1 : 0; do reversed[count++] = set[magnitude % base], magnitude /= base; while (magnitude); + while (count <= places) reversed[count++] = '0'; /* a digit before the point */ if (i < 0) body[n++] = '-'; - while (count) body[n++] = reversed[--count]; + for (; count; --count) { + if (count == places) body[n++] = '.'; + body[n++] = reversed[count - 1]; + } } 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); diff --git a/core/module.h b/core/module.h index ce8cebf..75e27ba 100644 --- a/core/module.h +++ b/core/module.h @@ -101,8 +101,10 @@ typedef enum { PURE_BITS_AND, PURE_BITS_OR, PURE_BITS_XOR, PURE_BITS_LEFT, PURE_ * 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. */ + * after the point of FIXED or of DECIMAL (there the integer in minor units: + * 1234 at two digits is 12.34, as money's cents). 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 }; /* 027's stage 2b: lists in records. A record field of type LIST holds a list * of the record its default names. In an event's list (and the lists in its diff --git a/core/test-module.c b/core/test-module.c index 36722d2..f89e198 100644 --- a/core/test-module.c +++ b/core/test-module.c @@ -6166,7 +6166,8 @@ static void bits_and_bytes(void) { /* ---- 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 + * spaces, in a base or with fixed digits, or an integer in minor units + * (money's: 1234 at two digits is 12.34); 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, @@ -6194,6 +6195,17 @@ static void formats_and_held(void) { {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"}, + /* Money's form (027's stage 5): an integer in minor units, its last digits after a point. */ + {1, 1234, FMT(2, 0, 0, PURE_FORMAT_DECIMAL, 3), PURE_FAULT_NONE, "12.34"}, + {1, -5, FMT(2, 0, 0, PURE_FORMAT_DECIMAL, 3), PURE_FAULT_NONE, "-0.05"}, + {1, 0, FMT(2, 0, 0, PURE_FORMAT_DECIMAL, 3), PURE_FAULT_NONE, "0.00"}, + {1, -5, FMT(2, 7, 1, PURE_FORMAT_DECIMAL, 3), PURE_FAULT_NONE, "-000.05"}, + {1, 100, FMT(2, 6, 0, PURE_FORMAT_DECIMAL, 2), PURE_FAULT_NONE, " 10.0"}, + {1, 7, FMT(2, 0, 0, PURE_FORMAT_DECIMAL, 1), PURE_FAULT_NONE, "7"}, + {1, 5, FMT(2, 0, 0, PURE_FORMAT_DECIMAL, 26), PURE_FAULT_NONE, "0.0000000000000000000000005"}, + {1, -0x1p63, FMT(2, 0, 0, PURE_FORMAT_DECIMAL, 3), PURE_FAULT_NONE, "-92233720368547758.08"}, + {0, 1234, FMT(0, 0, 0, PURE_FORMAT_DECIMAL, 3), PURE_FAULT_NONE, "12.34"}, + {0, 12.5, FMT(0, 0, 0, PURE_FORMAT_DECIMAL, 3), PURE_FAULT_LIMIT, ""}, }; 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}; diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index 0ef9bf8..c817c48 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -121,7 +121,7 @@ v7은 027의 4단계(패턴 모으기)의 판이다. 4b로 **키별 문서**를 | 25 | REQUIRE | bool → | 거짓이면 이벤트가 `limit`으로 실패 | | 26 | TEXT_CONST | → t | 상수 `인자` | | 27 | TEXT_LOAD | → t | text 상태 `인자`의 현재 값(복사 없는 버전) | -| 28 | 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` | +| 28 | 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 고정 소수나 10진의 소수점 뒤 자릿수 + 1(10진은 정수를 최소 단위로 보고 끝의 그 자릿수를 소수점 뒤에 쓴다: 2자리에서 1234는 `12.34`, −5는 `-0.05`, 돈의 센트. 027의 5단계). 정수의 꼴에 정수가 아닌 수는 `limit` | | 29 | CONCAT | t t → t 또는 b b → b | `인자` 1이면 바이트끼리 | | 30–32 | CHARS LINES TEXT_BYTES | t → n | 글자(Unicode scalar) 수, 줄 수(줄바꿈 + 1), 바이트 수 | | 33 | ANCHOR_LOAD | → a | 앵커 상태 `인자`(text 안이면 c, 리스트 안이면 i) | @@ -293,8 +293,8 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이 반복을 멈춘다(걷기도 같다). 안쪽 반복의 BREAK는 안쪽만 끝낸다. 바깥 걷기의 항목은 aup가 안쪽 반복 앞에서 인덱스 지역 칸에 담아 읽는다. 바깥 걷기의 리스트는 안쪽에서 넣거나 지울 수 없으니 그 칸은 그 항목을 가리킨 채다. -**메시지의 행.** 행 필드는 가변 필드이고, 바이트는 그 행 목록의 저장 형식(항목마다 필드 순서대로 수는 binary64, text는 -u32 길이와 바이트)이다. 메시지가 도착하면 핸들러가 돌기 전에 행 목록을 그 행들로 채우고, 유한하지 않은 수, UTF-8이 아닌 +**메시지의 행.** 행 필드는 가변 필드이고, 바이트는 그 행 목록의 저장 형식(항목마다 필드 순서대로 수는 binary64, i64는 2의 보수, 모두 +리틀 엔디언 8바이트, text는 u32 길이와 바이트)이다. 메시지가 도착하면 핸들러가 돌기 전에 행 목록을 그 행들로 채우고, 유한하지 않은 수, UTF-8이 아닌 text, 끝이 맞지 않는 바이트는 메시지를 `limit`으로 실패시킨다. 행의 레코드가 리스트를 담으면(027의 4a) 그 필드는 3단계의 칸과 같은 저장 형식(u32 길이와, 그 레코드의 항목들을 다시 이 형식으로)이고, 도착할 때 이벤트의 리스트 하나로 읽힌다. JSON 경계는 객체 안의 배열을 그 레코드의 객체들로 읽고, 리스트 64겹까지 받는다. 행 목록은 모든 이벤트가 끝날 때(성공이든 실패든) 비워지고, diff --git a/docs/design/027-computation-core.md b/docs/design/027-computation-core.md index 0813958..6d24433 100644 --- a/docs/design/027-computation-core.md +++ b/docs/design/027-computation-core.md @@ -548,6 +548,18 @@ - 명령 수(b44 대 b43): ±0.04% 안이다(실행기만의 BFS −0.03%). 은행 질의(s43과 s44를 한 번에 번갈아 여섯 번의 중앙값)는 per_branch −1.32%(여섯 번 모두 s43의 가장 낮은 것 아래: Fold가 커져 number 합의 고리가 달리 놓였다), in_credit −0.72%, ranks −0.26%, 나머지 ±0.01% 안이다. 일곱 질의의 답은 두 빌드에서 바이트까지 같다. + - **5c 만듦(2026-10-19).** + - Pure: NUM_TEXT의 10진 꼴이 자릿수를 받는다. 정수를 최소 단위로 보고 끝의 그 자릿수를 소수점 뒤에 쓴다(2자리에서 1234는 + `12.34`, −5는 `-0.05`, 0은 `0.00`). 폭과 0 채우기는 다른 꼴처럼 부호 뒤다. 검증은 자릿수를 고정 소수와 10진에만 허락한다. + - aup: `{cents:.2d}`. 파이썬에 없는 꼴이라, 시험은 그 Decimal(v / 10^N)을 `.Nf`로 쓴 것과 견준다(무작위 형식의 `d` 열에 다섯 중 + 둘 꼴로 자릿수). i64의 `.2f`는 진단이 `.2d`를 가리킨다. + - TPC-C: 돈은 i64의 센트다(창고·구역의 ytd, 고객의 잔고와 ytd 지불, 상품 가격, 주문 줄과 기록의 금액, Payment의 금액). 세율과 + 할인율은 i64의 만분율이다. SQLite도 INTEGER 열과 int64 바인딩이다. 초기 데이터는 정수로 뽑고(가격 100–10,000센트 등), 적재는 i64 + 칸을 2의 보수로 싼다. 견주기는 이제 오차 없는 같음이다(1e-9의 상대 오차를 지웠다). 2만 트랜잭션 뒤 모든 표의 모든 행이 + 같고(묶은 SQLite도), 3.3.2의 일관성 조건이 양쪽에서 선다. 막대가 더 굳어졌다: 같음이 정확하다. + - 008: NUM_TEXT의 10진 자릿수, 메시지 행의 i64 칸(2의 보수, 리틀 엔디언 8바이트). aup의 002: `.Nd`. + - 명령 수(b45 대 b44): 실행기만의 BFS +0.09%, 나머지 ±0.001% 안이다. 은행 질의(s44와 s45를 한 번에 번갈아 여섯 번의 중앙값)는 + in_credit −0.59%, per_branch −0.34%, ranks −0.20%, 나머지 ±0.01% 안이다. 일곱 질의의 답은 두 빌드에서 바이트까지 같다. **순서의 까닭.** - 1과 2가 묶음의 명세 대부분을 연다. 가장 값이 크다. diff --git a/docs/research/092-tpcc.md b/docs/research/092-tpcc.md index 4cac74e..0e73de1 100644 --- a/docs/research/092-tpcc.md +++ b/docs/research/092-tpcc.md @@ -23,6 +23,8 @@ Order-Status, Delivery, Stock-Level)을 aup 앱 하나로 쓰고, 같은 트랜 넣는다. 없는 상품(주문의 1%)이 나오면 이벤트가 실패하고 그때까지의 변경이 모두 되돌려진다. Delivery는 구역의 가장 오래된 미배송 주문의 줄들을 조각(`lines[주문*16 .. 주문*16+16]`)으로 훑어 금액을 더한다. Stock-Level은 구역의 마지막 20 주문의 줄을 조각으로 훑어 재고가 기준보다 적은 상품을 중복 없이 센다(`distinct`). +(2026-10-19부터 돈은 i64의 센트, 세율과 할인율은 i64의 만분율이고 SQLite도 INTEGER다. 두 쪽의 행은 오차 없이 같다: +[027](../design/027-computation-core.md)의 5단계. 아래의 코드와 결과는 그 전의 binary64 금액이다.) ```aup on new_order { diff --git a/research/experiments/040-against-c/2026-10-19-counts-s45.jsonl b/research/experiments/040-against-c/2026-10-19-counts-s45.jsonl new file mode 100644 index 0000000..edc7c8a --- /dev/null +++ b/research/experiments/040-against-c/2026-10-19-counts-s45.jsonl @@ -0,0 +1,12 @@ +{"serve": "pure-serve", "per_branch": {"instructions": 26979684, "cycles": 12023884}, "in_credit": {"instructions": 3536659, "cycles": 1590814}, "large": {"instructions": 1412854, "cycles": 590997}, "ranks": {"instructions": 24513604, "cycles": 14801992}, "none": {"instructions": 617346, "cycles": 189667}, "field": {"instructions": 8917574, "cycles": 3117068}, "found": {"instructions": 14070023, "cycles": 10629861}} +{"serve": "pure-serve", "per_branch": {"instructions": 26992579, "cycles": 11966388}, "in_credit": {"instructions": 3539415, "cycles": 1651939}, "large": {"instructions": 1412846, "cycles": 594121}, "ranks": {"instructions": 24519666, "cycles": 14793114}, "none": {"instructions": 617346, "cycles": 196182}, "field": {"instructions": 8917574, "cycles": 3090170}, "found": {"instructions": 14069623, "cycles": 10444633}} +{"serve": "pure-serve", "per_branch": {"instructions": 27060595, "cycles": 12023286}, "in_credit": {"instructions": 3554732, "cycles": 1479578}, "large": {"instructions": 1412854, "cycles": 590796}, "ranks": {"instructions": 24557464, "cycles": 15012291}, "none": {"instructions": 617346, "cycles": 193396}, "field": {"instructions": 8917677, "cycles": 3094436}, "found": {"instructions": 14068799, "cycles": 10460494}} +{"serve": "pure-serve", "per_branch": {"instructions": 26924120, "cycles": 11931237}, "in_credit": {"instructions": 3524095, "cycles": 1708160}, "large": {"instructions": 1412814, "cycles": 703225}, "ranks": {"instructions": 24482221, "cycles": 14816677}, "none": {"instructions": 617346, "cycles": 192466}, "field": {"instructions": 8917574, "cycles": 3198568}, "found": {"instructions": 14069015, "cycles": 11709649}} +{"serve": "pure-serve", "per_branch": {"instructions": 26966893, "cycles": 12140466}, "in_credit": {"instructions": 3533730, "cycles": 1625190}, "large": {"instructions": 1412854, "cycles": 591174}, "ranks": {"instructions": 24505195, "cycles": 14963964}, "none": {"instructions": 617346, "cycles": 202237}, "field": {"instructions": 8917574, "cycles": 3093659}, "found": {"instructions": 14070175, "cycles": 10543849}} +{"serve": "pure-serve", "per_branch": {"instructions": 26826358, "cycles": 11925544}, "in_credit": {"instructions": 3502195, "cycles": 1582489}, "large": {"instructions": 1412894, "cycles": 588093}, "ranks": {"instructions": 24428492, "cycles": 14933031}, "none": {"instructions": 617346, "cycles": 189394}, "field": {"instructions": 8917574, "cycles": 3093669}, "found": {"instructions": 14070335, "cycles": 10396792}} +{"serve": "pure-serve", "per_branch": {"instructions": 26960422, "cycles": 12218167}, "in_credit": {"instructions": 3532175, "cycles": 1606369}, "large": {"instructions": 1412854, "cycles": 600368}, "ranks": {"instructions": 24502148, "cycles": 14978404}, "none": {"instructions": 617346, "cycles": 190949}, "field": {"instructions": 8917575, "cycles": 3104911}, "found": {"instructions": 14070262, "cycles": 10282605}} +{"serve": "pure-serve", "per_branch": {"instructions": 26872531, "cycles": 12098562}, "in_credit": {"instructions": 3512578, "cycles": 1652287}, "large": {"instructions": 1412894, "cycles": 591682}, "ranks": {"instructions": 24454638, "cycles": 15243744}, "none": {"instructions": 617346, "cycles": 190955}, "field": {"instructions": 8917574, "cycles": 3097775}, "found": {"instructions": 14070255, "cycles": 10861947}} +{"serve": "pure-serve", "per_branch": {"instructions": 27013066, "cycles": 12479111}, "in_credit": {"instructions": 3544010, "cycles": 1636042}, "large": {"instructions": 1412822, "cycles": 644341}, "ranks": {"instructions": 24531053, "cycles": 15460303}, "none": {"instructions": 617346, "cycles": 193549}, "field": {"instructions": 8917574, "cycles": 3246776}, "found": {"instructions": 14068823, "cycles": 10689353}} +{"serve": "pure-serve", "per_branch": {"instructions": 26890220, "cycles": 11905835}, "in_credit": {"instructions": 3516422, "cycles": 1660765}, "large": {"instructions": 1412814, "cycles": 606666}, "ranks": {"instructions": 24463698, "cycles": 14748420}, "none": {"instructions": 617346, "cycles": 191176}, "field": {"instructions": 8917574, "cycles": 3105283}, "found": {"instructions": 14070302, "cycles": 10260437}} +{"serve": "pure-serve", "per_branch": {"instructions": 26774006, "cycles": 12323592}, "in_credit": {"instructions": 3490433, "cycles": 1675259}, "large": {"instructions": 1412878, "cycles": 590025}, "ranks": {"instructions": 24399648, "cycles": 14860136}, "none": {"instructions": 617346, "cycles": 191301}, "field": {"instructions": 8917575, "cycles": 3111265}, "found": {"instructions": 14070607, "cycles": 10484430}} +{"serve": "pure-serve", "per_branch": {"instructions": 26865589, "cycles": 11960663}, "in_credit": {"instructions": 3510953, "cycles": 1578217}, "large": {"instructions": 1412870, "cycles": 600951}, "ranks": {"instructions": 24450447, "cycles": 14936245}, "none": {"instructions": 617346, "cycles": 189389}, "field": {"instructions": 8917574, "cycles": 3090773}, "found": {"instructions": 14070494, "cycles": 10550630}} diff --git a/research/experiments/041-batches/2026-10-19-counts.jsonl b/research/experiments/041-batches/2026-10-19-counts.jsonl index 958d9d2..fbb9666 100644 --- a/research/experiments/041-batches/2026-10-19-counts.jsonl +++ b/research/experiments/041-batches/2026-10-19-counts.jsonl @@ -38,3 +38,13 @@ {"version":"b44","way":"rounds","work":"matmul","instructions":21186170497,"cycles":6763916506} {"version":"b44","way":"batches","work":"bfs","instructions":3341455171,"cycles":1657087395} {"version":"b44","way":"rounds","work":"bfs","instructions":4703547672,"cycles":2087752517} +{"version":"b45","way":"batches","work":"roots","instructions":157245335,"cycles":109451862} +{"version":"b45","way":"rounds","work":"roots","instructions":2715504052,"cycles":1002258422} +{"version":"b45","way":"batches","work":"knapsack","instructions":348031907,"cycles":105539805} +{"version":"b45","way":"rounds","work":"knapsack","instructions":22162412220,"cycles":7775287896} +{"version":"b45","way":"batches","work":"join","instructions":1783720583,"cycles":1039636935} +{"version":"b45","way":"rounds","work":"join","instructions":7723077757,"cycles":3351023538} +{"version":"b45","way":"batches","work":"matmul","instructions":702231936,"cycles":226733671} +{"version":"b45","way":"rounds","work":"matmul","instructions":21186169957,"cycles":6774482517} +{"version":"b45","way":"batches","work":"bfs","instructions":3341456134,"cycles":1665227825} +{"version":"b45","way":"rounds","work":"bfs","instructions":4707546895,"cycles":2096871761} -- cgit v1.2.3