diff options
| author | eunhhu <[email protected]> | 2026-10-19 01:53:40 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-19 01:53:40 +0100 |
| commit | defb2cf06a9b5db9429ca368d8c2af127a5c9c3d (patch) | |
| tree | b13c94eb334ce2a5055854791900f8702ba9fc3b | |
| parent | 809a83a5764ec0f0c84b96c43a20cfb906eee031 (diff) | |
| download | pure-defb2cf06a9b5db9429ca368d8c2af127a5c9c3d.tar.gz pure-defb2cf06a9b5db9429ca368d8c2af127a5c9c3d.zip | |
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
| -rw-r--r-- | benchmarks/tpcc/data.py | 28 | ||||
| -rw-r--r-- | benchmarks/tpcc/run.py | 27 | ||||
| -rw-r--r-- | benchmarks/tpcc/sqlite-tpcc.c | 24 | ||||
| -rw-r--r-- | benchmarks/tpcc/tpcc-load.c | 4 | ||||
| -rw-r--r-- | benchmarks/tpcc/tpcc.aup | 30 | ||||
| -rw-r--r-- | core/module.c | 15 | ||||
| -rw-r--r-- | core/module.h | 6 | ||||
| -rw-r--r-- | core/test-module.c | 14 | ||||
| -rw-r--r-- | docs/design/008-module-format.md | 6 | ||||
| -rw-r--r-- | docs/design/027-computation-core.md | 12 | ||||
| -rw-r--r-- | docs/research/092-tpcc.md | 2 | ||||
| -rw-r--r-- | research/experiments/040-against-c/2026-10-19-counts-s45.jsonl | 12 | ||||
| -rw-r--r-- | research/experiments/041-batches/2026-10-19-counts.jsonl | 10 |
13 files changed, 124 insertions, 66 deletions
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} |
