aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-19 01:53:40 +0100
committereunhhu <[email protected]>2026-10-19 01:53:40 +0100
commitdefb2cf06a9b5db9429ca368d8c2af127a5c9c3d (patch)
treeb13c94eb334ce2a5055854791900f8702ba9fc3b
parent809a83a5764ec0f0c84b96c43a20cfb906eee031 (diff)
downloadpure-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.py28
-rw-r--r--benchmarks/tpcc/run.py27
-rw-r--r--benchmarks/tpcc/sqlite-tpcc.c24
-rw-r--r--benchmarks/tpcc/tpcc-load.c4
-rw-r--r--benchmarks/tpcc/tpcc.aup30
-rw-r--r--core/module.c15
-rw-r--r--core/module.h6
-rw-r--r--core/test-module.c14
-rw-r--r--docs/design/008-module-format.md6
-rw-r--r--docs/design/027-computation-core.md12
-rw-r--r--docs/research/092-tpcc.md2
-rw-r--r--research/experiments/040-against-c/2026-10-19-counts-s45.jsonl12
-rw-r--r--research/experiments/041-batches/2026-10-19-counts.jsonl10
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}