aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-19 04:52:42 +0100
committereunhhu <[email protected]>2026-10-19 04:52:42 +0100
commitc80ac23fd74fe07e7e02514486fd0ecfbc8da779 (patch)
tree34200c62c847390022bb0d9f3a4c8e1d6193d58b
parent6596533b0d5fe10f3cab028d7fa69a40e5416ecd (diff)
downloadpure-c80ac23fd74fe07e7e02514486fd0ecfbc8da779.tar.gz
pure-c80ac23fd74fe07e7e02514486fd0ecfbc8da779.zip
140: small block fns inlined in aup, made and measured, not merged (the patch kept): a block fn whose only return is its last line, with no loop or called where the stack is empty, written out where it is called, its parameters slots of its own (one set a fn and kinds of arguments in a handler); the three specs' checks passed, but inflate 191.2 instructions a byte as before and HTTP 61.4 to 62.1 (+1.1%, 277.7 to 274.9 ns): the hot fns (inflate's decode, HTTP's crlf) return early inside a loop, a fn with a loop cannot be written out where values are on the stack (Pure opens no loop above values), and storing a short body's parameters costs what CALL and PICK did; inflate's 23% was the expression fn bit's (a loop, a let and conversions gone with the call); what would make it pay is Pure's (loops above values, or a cheaper frame), one spec behind it; the decision point's list of what was made after it
-rw-r--r--docs/research/140-spec-suite.md16
-rw-r--r--research/experiments/044-specs/2026-10-19-inline-block.patch276
2 files changed, 292 insertions, 0 deletions
diff --git a/docs/research/140-spec-suite.md b/docs/research/140-spec-suite.md
index 28883a0..187e0cf 100644
--- a/docs/research/140-spec-suite.md
+++ b/docs/research/140-spec-suite.md
@@ -676,6 +676,22 @@
- 앞의 둘은 이제 핵에 들일 자격이 있다. 바이트 하나인 상태(저장, 교체, 보기, HTTP의 GET이 그것을 어떻게 다루는가)와 바이트의 비교
(TEXT_COMPARE의 인자로 할지, 새 명령으로 할지)는 설계 문서에서 정한다. 아직 시작하지 않았다.
- aup의 셋은 컴파일러의 일이다. Pure의 함수 서명은 이미 리스트를 받는다.
+
+**결정점 뒤에 만든 것(2026-10-19).**
+- 바이트의 `==`: TEXT_COMPARE의 인자 비트 `PURE_COMPARE_BYTES`(008). aup는 바이트끼리 견주고, 바이트를 만나는 문자열은 바이트다.
+- 바이트 하나인 상태: [028](../design/028-bytes-state.md). 상태 타입 6, text와 한 draft, `values`의 표시로 손상과 옮김을 가른다.
+- fn의 맨 리스트(aup): let의 리스트와 메시지의 행이 값이 되어 fn에 넘어가고 돌아온다.
+- 블록 안의 let 리스트와 `xs = [...]`(aup): let을 지날 때마다 빈 리스트로 시작한다.
+- **작은 블록 fn의 인라인(aup): 만들어 쟀고 넣지 않았다.** 반환이 끝에만 있고 고리가 없는(또는 스택이 빈 자리에서 부르는) 블록
+ fn을 부른 쪽에 풀어 넣었다. 매개변수는 제 지역 칸이고, 칸은 핸들러마다 fn과 인자 종류의 한 벌이다. 세 명세의 검사는 모두
+ 지났다. 하지만 inflate는 바이트당 명령 191.2 → 191.2(시간도 같다), HTTP는 61.4 → 62.1(+1.1%, 시간 277.7 → 274.9 ns)이었다.
+ - 효과가 없는 까닭은 셋이다. 첫째, 뜨거운 fn(inflate의 `decode`, HTTP의 `crlf`)은 고리 안에서 일찍 돌아오므로 조건에 들지
+ 않는다. 둘째, 고리를 가진 fn은 식 안(스택에 값이 있는 자리)에서는 풀 수 없다. Pure가 값 위에 고리를 열지 않기 때문이다
+ ("loop above values", 2c의 구멍을 막은 규칙). 셋째, 짧은 몸에서는 매개변수를 칸에 담는 값이 CALL과 PICK의 값만큼 든다.
+ - inflate의 23%는 부름을 없앤 것이 아니었다. `take(d, at, 1)`을 식 fn `bit`로 바꾸며 고리, let, i64 변환이 함께 사라진 값이었다.
+ - 그래서 인라인이 값을 하려면 Pure 쪽이 바뀌어야 한다. 값 위의 고리(블록의 스택이 그 자리의 높이에서 시작한다: 검증과 실행기),
+ 또는 더 싼 프레임이다. 명세 하나(inflate)가 닿은 핵의 질문이라 적어 두고 시작하지 않는다.
+ - 패치: `research/experiments/044-specs/2026-10-19-inline-block.patch`(aup의 `compiler/compile.c`, 142줄을 더한다).
- 시간은 명령당 3.8–6.0 ns가 든다. 바이트 일을 C에 가깝게 하려면 명령의 수(바이트 찾기 같은 일을 명령 하나로 할지)와 명령당
비용(기계어 계층이 맡지 못하는 명령들) 둘 다를 봐야 한다. 이것도 따로 정한다.
diff --git a/research/experiments/044-specs/2026-10-19-inline-block.patch b/research/experiments/044-specs/2026-10-19-inline-block.patch
new file mode 100644
index 0000000..83dfe37
--- /dev/null
+++ b/research/experiments/044-specs/2026-10-19-inline-block.patch
@@ -0,0 +1,276 @@
+diff --git a/compiler/compile.c b/compiler/compile.c
+index 0fcaa1b..47d525e 100644
+--- a/compiler/compile.c
++++ b/compiler/compile.c
+@@ -182,6 +182,8 @@ typedef struct {
+ bool typed; /* -> its results' types */
+ bool active; /* being inlined: a call back into it is recursion */
+ bool called; /* run as a function (a block, or a cycle of calls through it) */
++ bool inlined; /* a block inlined where it is called (its only return last, no let's list, no cycle: inline_block) */
++ bool loops; /* its block holds a for or a while: inlined only where the stack is empty (Pure opens no loop above values) */
+ } Fn;
+
+ typedef struct { uint8_t op; uint32_t arg; union { double value; int64_t integer; }; } Op;
+@@ -274,6 +276,13 @@ typedef struct {
+ * fields; variant: a variant's case; alias: a match's name for a case's value (read, never assigned). */
+ struct { char name[NAME]; unsigned slot; Kind kind; int list; bool counter, variant, alias; } locals[LOCALS];
+ unsigned local_count, local_slots, held_bytes, held_texts; /* held: the bytes' and texts' lets' slots */
++ /* An inlined block's names begin at these floors (its parameters and lets alone); its slots, one set for each fn and
++ * kinds of arguments in a handler (or a function), used again at each call: where they start. */
++ unsigned local_floor, loop_floor, alias_floor;
++ struct { int fn; Kind kinds[PARAMS]; unsigned slots, bytes, texts; } inlines[64];
++ unsigned inline_count, inline_floor;
++ /* The block being inlined: its last return leaves its value at its call's height and says its kind here. */
++ struct { Kind *kind; int height; Type want; bool handler; } giving;
+ unsigned whiles; /* while loops so far: each counts by a name of its own */
+ unsigned settled; /* records' values calls gave, held by names of their own (027's 2c) */
+ const struct Expr *given; /* the left side of the && or || whose right side faultless looks at */
+@@ -1521,7 +1530,7 @@ static int input_index(const Compiler *c, const char *name) {
+ }
+
+ static int state_index(const Compiler *c, const char *name) {
+- for (unsigned i = c->local_count; i-- > 0;) /* a let's list, by the let's name */
++ for (unsigned i = c->local_count; i-- > c->local_floor;) /* a let's list, by the let's name */
+ if (c->locals[i].list >= 0 && !strcmp(c->locals[i].name, name)) return c->locals[i].list;
+ for (unsigned i = 0; i < c->states; ++i) if (!c->state_event[i] && !strcmp(c->state_names[i], name)) return (int)i;
+ return -1;
+@@ -2689,21 +2698,21 @@ static void app_name(Compiler *c, const Expr *e, const char *name) {
+ /* The local named name in scope, or -1. */
+ /* A let of a value (or a for's count) in scope, by name, or -1; a let's list is a state (state_index). */
+ static int local_named(const Compiler *c, const char *name) {
+- for (unsigned i = c->local_count; i-- > 0;)
++ for (unsigned i = c->local_count; i-- > c->local_floor;)
+ if (c->locals[i].list < 0 && !strcmp(c->locals[i].name, name)) return (int)i;
+ return -1;
+ }
+
+ /* Any let in scope by name (a value's or a list's), or -1. */
+ static int let_named(const Compiler *c, const char *name) {
+- for (unsigned i = c->local_count; i-- > 0;)
++ for (unsigned i = c->local_count; i-- > c->local_floor;)
+ if (!strcmp(c->locals[i].name, name)) return (int)i;
+ return -1;
+ }
+
+ /* The for (walk or count) in whose block a name is its item or count, innermost first, or -1. */
+ static int loop_named(const Compiler *c, const char *name) {
+- for (unsigned i = c->loop_count; i-- > 0;)
++ for (unsigned i = c->loop_count; i-- > c->loop_floor;)
+ if (!strcmp(c->loops[i].name, name)) return (int)i;
+ return -1;
+ }
+@@ -2780,7 +2789,7 @@ static int loop_named(const Compiler *c, const char *name);
+ static int parameter(const Compiler *c, const char *name);
+
+ static int alias_named(const Compiler *c, const char *name) {
+- for (unsigned i = c->alias_count; i-- > 0;) if (!strcmp(c->aliases[i].name, name)) return (int)i;
++ for (unsigned i = c->alias_count; i-- > c->alias_floor;) if (!strcmp(c->aliases[i].name, name)) return (int)i;
+ return -1;
+ }
+
+@@ -3139,6 +3148,32 @@ static void named_by(const Compiler *c, int module, const Stmt *s, uint64_t *out
+ }
+
+ /* Which fns run as functions: those with a block, and those a cycle of calls goes through. */
++/* Whether statements hold a for or a while, at any depth. */
++static bool holds_loop(const Stmt *s) {
++ for (; s; s = s->next)
++ if (s->kind == S_FOR || s->kind == S_COUNT || holds_loop(s->then) || holds_loop(s->otherwise)) return true;
++ return false;
++}
++
++/* Whether statements return only at their end (top: the block's own last statement, a return of one value at most)
++ * and hold no let's list (each call would want its own). */
++static bool returns_last(const Stmt *s, bool top) {
++ for (; s; s = s->next) {
++ if ((s->kind == S_RETURN && (!top || s->next)) || (s->kind == S_LET && s->listed)) return false;
++ if (!returns_last(s->then, false) || !returns_last(s->otherwise, false)) return false;
++ }
++ return true;
++}
++
++/* The type a typed fn's one result names when it is a number, an i64, a bool, bytes or text, or 0. */
++static Type result_type(const Fn *f) {
++ static const char *const names[] = {"number", "i64", "bool", "bytes", "text"};
++ static const Type types[] = {NUM, I64, BOOL, BYTES, TEXT};
++ for (unsigned k = 0; f->typed && f->result_count == 1 && !f->results[0].list && k < 5; ++k)
++ if (!strcmp(f->results[0].name, names[k])) return types[k];
++ return 0;
++}
++
+ static void called_fns(Compiler *c) {
+ uint64_t reach[FNS] = {0};
+ for (unsigned i = 0; i < c->fn_count; ++i) {
+@@ -3148,7 +3183,15 @@ static void called_fns(Compiler *c) {
+ for (unsigned k = 0; k < c->fn_count; ++k)
+ for (unsigned i = 0; i < c->fn_count; ++i)
+ if (reach[i] >> k & 1) reach[i] |= reach[k];
+- for (unsigned i = 0; i < c->fn_count; ++i) c->fns[i].called = c->fns[i].block || (reach[i] >> i & 1);
++ for (unsigned i = 0; i < c->fn_count; ++i) {
++ Fn *f = &c->fns[i];
++ f->called = f->block || (reach[i] >> i & 1);
++ const Stmt *last = f->block;
++ while (last && last->next) last = last->next;
++ f->inlined = f->block && !(reach[i] >> i & 1) && returns_last(f->block, true) && (!f->typed || result_type(f)) &&
++ (!last || last->kind != S_RETURN || !last->value || !last->value->next);
++ f->loops = holds_loop(f->block);
++ }
+ }
+
+ /* What making a function's code puts aside of what was being made. */
+@@ -3159,6 +3202,7 @@ typedef struct {
+ Scope *fold_scope, *scope;
+ const Expr *given;
+ unsigned local_count, local_slots, held_bytes, held_texts, alias_count, loop_count, nested, joins, code_count, code_capacity;
++ unsigned local_floor, loop_floor, alias_floor, inline_count, inline_floor;
+ Type want;
+ Kind returned;
+ Op *code;
+@@ -3167,8 +3211,8 @@ typedef struct {
+ static void put_aside(Compiler *c, Aside *a, void *locals, void *aliases, void *loops) {
+ *a = (Aside){c->message, c->arguments, c->row, c->fold_list, c->module, c->current, c->folding, c->filtering, c->handling,
+ c->has_returned, {0}, c->fold_scope, c->scope, c->given, c->local_count, c->local_slots, c->held_bytes,
+- c->held_texts, c->alias_count, c->loop_count, c->nested, c->joins, c->code_count, c->code_capacity, c->want,
+- c->returned, c->code};
++ c->held_texts, c->alias_count, c->loop_count, c->nested, c->joins, c->code_count, c->code_capacity,
++ c->local_floor, c->loop_floor, c->alias_floor, c->inline_count, c->inline_floor, c->want, c->returned, c->code};
+ memcpy(a->fold_name, c->fold_name, NAME);
+ memcpy(locals, c->locals, sizeof(c->locals));
+ memcpy(aliases, c->aliases, sizeof(c->aliases));
+@@ -3183,6 +3227,8 @@ static void take_back(Compiler *c, const Aside *a, const void *locals, const voi
+ c->alias_count = a->alias_count, c->loop_count = a->loop_count, c->nested = a->nested, c->joins = a->joins;
+ c->code_count = a->code_count, c->code_capacity = a->code_capacity, c->want = a->want, c->returned = a->returned;
+ c->code = a->code;
++ c->local_floor = a->local_floor, c->loop_floor = a->loop_floor, c->alias_floor = a->alias_floor;
++ c->inline_count = a->inline_count, c->inline_floor = a->inline_floor;
+ memcpy(c->fold_name, a->fold_name, NAME);
+ memcpy(c->locals, locals, sizeof(c->locals));
+ memcpy(c->aliases, aliases, sizeof(c->aliases));
+@@ -3207,6 +3253,7 @@ static void make_instance(Compiler *c, int index) {
+ c->message = c->arguments = c->row = c->fold_list = -1;
+ c->folding = c->filtering = false, c->fold_scope = NULL, c->given = NULL, c->want = 0, c->joins = 0;
+ c->local_count = c->local_slots = c->held_bytes = c->held_texts = c->alias_count = c->loop_count = c->nested = 0;
++ c->local_floor = c->loop_floor = c->alias_floor = 0, c->inline_floor = c->inline_count; /* its slots its own */
+ for (unsigned i = 0; i < in->param_count; ++i) name_value(c, f->params[i], in->params[i], f->line, f->column);
+ for (unsigned i = in->param_count; i-- > 0;) pop_named(c, local_named(c, f->params[i]), f->line, f->column);
+ if (f->block) {
+@@ -3257,11 +3304,86 @@ static int instance_of(Compiler *c, int fn, const Kind *kinds, unsigned count, c
+
+ /* A call of a fn run as a function: its arguments onto the stack (an item's as its record's value), the CALL, and
+ * what it gives (a value, several: TUPLE, or none). Arguments already given (count of them, kinds) are on the stack. */
++static void statements(Compiler *c, const Stmt *s);
++static void name_value(Compiler *c, const char *name, Kind kind, int line, int column);
++static void pop_named(Compiler *c, int local, int line, int column);
++
++/* A block fn inlined where it is called (Fn.inlined): its arguments stored into its parameters (each a let of its own),
++ * its statements and its last return made here, its names its own (the floors). Its slots are one set for each fn and
++ * kinds of arguments in a handler, used again at each call: Pure keeps a slot one kind, and a call's arguments are all
++ * made before its body stores them, so f(x, f(y)) is done with the inner body before the outer one begins. */
++static Kind inline_block(Compiler *c, const Expr *e, int fn, bool handler, int height) {
++ Fn *f = &c->fns[fn];
++ Kind kinds[PARAMS];
++ for (unsigned i = 0; i < f->param_count; ++i) {
++ kinds[i] = as_value(c, e->args[i], value(c, e->args[i], handler, height + (int)i));
++ Type t = kinds[i].type;
++ if (t != NUM && t != BOOL && t != I64 && t != BYTES && t != TEXT && t != LIST) /* a record's: called */
++ return call_fn(c, e, fn, handler, height, kinds, i + 1);
++ }
++ unsigned entry = c->inline_floor;
++ while (entry < c->inline_count &&
++ (c->inlines[entry].fn != fn || memcmp(c->inlines[entry].kinds, kinds, f->param_count * sizeof(Kind))))
++ ++entry;
++ if (entry == c->inline_count) {
++ if (entry == sizeof(c->inlines) / sizeof(c->inlines[0])) return call_fn(c, e, fn, handler, height, kinds, f->param_count);
++ c->inlines[entry].fn = fn;
++ memcpy(c->inlines[entry].kinds, kinds, f->param_count * sizeof(Kind));
++ c->inlines[entry].slots = c->local_slots, c->inlines[entry].bytes = c->held_bytes, c->inlines[entry].texts = c->held_texts;
++ ++c->inline_count;
++ }
++ unsigned slots = c->local_slots, bytes = c->held_bytes, texts = c->held_texts, locals = c->local_count;
++ unsigned floors[3] = {c->local_floor, c->loop_floor, c->alias_floor};
++ int message = c->message, arguments = c->arguments, row = c->row, module = c->module;
++ Scope *scope = c->scope;
++ c->local_slots = c->inlines[entry].slots, c->held_bytes = c->inlines[entry].bytes, c->held_texts = c->inlines[entry].texts;
++ c->local_floor = c->local_count, c->loop_floor = c->loop_count, c->alias_floor = c->alias_count;
++ c->message = c->arguments = c->row = -1, c->scope = NULL, c->module = f->module;
++ ++c->nested; /* its lets' lists begin empty at each call (c2) */
++ for (unsigned i = f->param_count; i-- > 0;) { /* the last argument is on top */
++ if (kinds[i].type == LIST) {
++ name_value(c, f->params[i], kinds[i], e->line, e->column);
++ pop_named(c, local_named(c, f->params[i]), e->line, e->column);
++ continue;
++ }
++ if (c->local_count == LOCALS || c->local_slots == LOCALS)
++ fail_at(c, e->line, e->column, "at most %d lets are allowed in a handler", LOCALS);
++ unsigned local = c->local_count++;
++ memset(&c->locals[local], 0, sizeof(c->locals[local]));
++ memcpy(c->locals[local].name, f->params[i], NAME);
++ c->locals[local].kind = kinds[i];
++ c->locals[local].list = -1;
++ if (kinds[i].type == BYTES || kinds[i].type == TEXT) {
++ unsigned *count = kinds[i].type == BYTES ? &c->held_bytes : &c->held_texts;
++ if (*count == HELD)
++ fail_at(c, e->line, e->column, "at most %d lets of %s are allowed in a handler", HELD, type_name(kinds[i].type));
++ c->locals[local].slot = (kinds[i].type == TEXT ? HELD_TEXT : 0) + (*count)++;
++ emit(c, e->line, e->column, BYTES_STORE, c->locals[local].slot, 0);
++ } else {
++ c->locals[local].slot = c->local_slots++;
++ emit(c, e->line, e->column, LOCAL_STORE, local_arg(c, (int)local), 0);
++ }
++ }
++ Kind result = {NOTHING, -1};
++ __typeof__(c->giving) giving = c->giving;
++ c->giving.kind = &result, c->giving.height = height, c->giving.want = result_type(f), c->giving.handler = handler;
++ statements(c, f->block); /* its return (its last line) leaves the value, its lets still there */
++ c->giving = giving;
++ --c->nested;
++ c->local_count = locals;
++ c->local_floor = floors[0], c->loop_floor = floors[1], c->alias_floor = floors[2];
++ c->message = message, c->arguments = arguments, c->row = row, c->scope = scope, c->module = module;
++ c->local_slots = c->local_slots > slots ? c->local_slots : slots;
++ c->held_bytes = c->held_bytes > bytes ? c->held_bytes : bytes, c->held_texts = c->held_texts > texts ? c->held_texts : texts;
++ return result;
++}
++
+ static Kind call_fn(Compiler *c, const Expr *e, int fn, bool handler, int height, Kind *kinds, unsigned given) {
+ const Fn *f = &c->fns[fn];
+ unsigned count = e->kind == E_FN ? e->count : 0;
+ if (count != f->param_count)
+ fail_at(c, e->line, e->column, "%s expects %u argument%s", f->name, f->param_count, f->param_count == 1 ? "" : "s");
++ if (f->inlined && !given && !c->folding && !c->filtering && (!height || !f->loops)) return inline_block(c, e, fn, handler, height);
+ if (c->folding) fail_at(c, e->line, e->column, "count, sum, max and min call no fn with a block, nor one that calls itself");
+ if (!handler && c->current < 0)
+ fail_at(c, e->line, e->column, "a view calls no fn with a block, nor one that calls itself: handlers do ('%s')", f->name);
+@@ -5033,6 +5155,16 @@ static void statements(Compiler *c, const Stmt *s) {
+ break;
+ }
+ case S_RETURN:
++ if (c->giving.kind) { /* an inlined block's last line: its value where its call is (inline_block) */
++ Kind *into = c->giving.kind;
++ Type want = c->giving.want;
++ const Expr *v = s->value;
++ c->giving.kind = NULL; /* (a block inlined in its value gives its own) */
++ if (v) *into = as_value(c, v, want == NUM || want == I64 ? wanted(c, v, c->giving.handler, c->giving.height, want)
++ : value(c, v, c->giving.handler, c->giving.height));
++ if (v && want && into->type != want) expect_kind(c, v, *into, want);
++ break;
++ }
+ if (c->current >= 0) { /* what a fn's call gives (027's 2c) */
+ Instance *in = &c->instances[c->current];
+ const char *name = c->fns[in->fn].name;
+@@ -5611,6 +5743,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
+ Kind kind;
+ if (c->view_body[i]) { /* statements as a handler's, changing only its lets and lists, and the value returned */
+ c->local_count = c->local_slots = c->loop_count = c->nested = c->held_bytes = c->held_texts = 0;
++ c->inline_count = c->inline_floor = 0;
+ c->handling = c->has_returned = false;
+ statements(c, c->view_body[i]);
+ kind = c->returned;
+@@ -5633,6 +5766,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
+ int input = handler_input[i];
+ c->message = input >= 0 && c->inputs[input].kind != KIND_KEY && c->inputs[input].kind != KIND_RELEASE ? input : -1;
+ c->local_count = c->local_slots = c->loop_count = c->nested = c->held_bytes = c->held_texts = 0;
++ c->inline_count = c->inline_floor = 0;
+ c->handler_code[i][0] = c->code_count;
+ if (c->inputs[input].kind == KIND_POINT) { /* a pointing: its list's item at the row's position, the index's */
+ const Input *in = &c->inputs[input];