Ferro 파서를 Ferro 로, 그리고 그것이 드러낸 네 가지

렉서 다음은 파서다. 노드는 한 배열에 살고 자식은 그 안의 인덱스다 -- 노드는
^Node 를 들 수 없고(여럿이며 한 번씩 소유하지 않는다) &Node 도 들 수 없다(R4).
인덱스는 둘 다 아니다. 소스도 필드가 아니라 매 단계에 같이 다닌다.

  unit demo / fn answer @2 / let n = (+ 1 (* 2 3)) / return n / balanced

전위 표기로 다시 찍는 것이 시험의 요점이다. 1 + 2 * 3 이 어떻게 묶였는지는
그렇게만 보인다.

쓰면서 나온 컴파일러 버그 넷:

1. 다른 유닛의 타입을 필드로 쓰면 그 필드 타입이 영영 UNKNOWN 이었다. 필드
   해석이 유닛마다 선언 직후에 돌아서, 아직 선언되지 않은 유닛의 타입을 찾다
   실패하고 그 답을 굳혔다. 이제 모든 유닛이 선언을 마친 뒤에 한 번 푼다.

2. 그리고 그 해석은 타입을 선언한 유닛에서 해야 한다. 필드 타입은 그 유닛의
   import 로 쓰였는데 아무 유닛에서나 풀고 있었다. 타입 계층에 enter/leave
   콜백을 두고 체커가 그 자리로 데려간다.

3. cycle_state 를 재귀 검사와 크기 계산이 같이 썼다. 첫 번째가 보는 중인 구조체
   가 두 번째에게는 다 끝난 것으로 보여서, 필드가 하나뿐인 것처럼 1 바이트로
   자리를 잡았다 -- Parser 가 그래서 자기 토큰을 밟았다. layout_state 로 나눴다.

4. 다른 유닛의 상수(ast.NONE)를 lowering 이 필드 접근으로 봤다. 체커가 이미
   링크 이름을 붙여두었으니 그것이 있으면 전역이다.

그리고 R1 을 실제로 지키게 했다: 소유자를 놓으면 그것이 가진 것도 놓는다.
전에는 자기 drop 이 있거나 자기가 owned 일 때만이어서, drop 을 가진 타입을
필드로 담은 구조체는 그것을 놓을 방법이 없었다(drop 은 손으로 못 부른다).
이제 release_at 이 drop 을 부르고 필드로 내려간다. 그 덕에 List/Arena/Map 의
drop 이 전부 필요 없어져서 지웠다 -- 버퍼가 owned 이니 R1 이 알아서 한다.

221/221, 29/29.
This commit is contained in:
2026-08-17 12:49:53 +09:00
parent 8c4e80e246
commit e84f892147
15 changed files with 582 additions and 58 deletions
+35
View File
@@ -284,6 +284,9 @@ void enter_unit(FeCheck *c, unsigned index)
static int enter_decl_hook(void *owner, const char *unit);
static void leave_decl_hook(void *owner, int back);
void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags,
unsigned pointer_bits, int no_checks)
{
@@ -301,6 +304,8 @@ void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags,
c->types.unit_name = "unit";
c->types.instantiate = instantiate_type_node;
c->types.instantiate_owner = c;
c->types.enter_decl = enter_decl_hook;
c->types.leave_decl = leave_decl_hook;
c->instances = (FeInstance *)fe_arena_alloc(&c->arena,
(unsigned long)FE_GENERIC_INSTANCE_MAX * sizeof(FeInstance));
c->instance_count = 0;
@@ -354,6 +359,36 @@ FeType *unit_type(FeCheck *c, FeUnit *u, const char *name)
return 0;
}
/* A field type is written in the unit that declared the type, so it has to be
resolved with that unit's imports in scope -- not with whichever unit
happens to be current when the walk reaches it. Returns the index to go back
to, or -1 when there is nowhere to go. */
int enter_declaring_unit(FeCheck *c, const char *unit_name)
{
unsigned i;
unsigned here;
if (!unit_name || !c->build || !c->unit) return -1;
here = unit_index(c,c->unit);
for (i=0;i<c->build->count;++i)
if (strcmp(c->build->units[i].name,unit_name)==0) {
if (i==here) return -1;
enter_unit(c,i);
return (int)here;
}
return -1;
}
/* The type layer calls these; it knows nothing about units beyond a name. */
static int enter_decl_hook(void *owner, const char *unit)
{
return enter_declaring_unit((FeCheck *)owner, unit);
}
static void leave_decl_hook(void *owner, int back)
{
enter_unit((FeCheck *)owner, (unsigned)back);
}
/* The AST declaration of a type another unit declares, for its visibility and
for its methods. */
FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name)
+1
View File
@@ -104,6 +104,7 @@ FeUnit *binding_unit(FeCheckerState *s, FeNode *base);
int decl_is_public(const FeNode *decl);
FeSym *unit_member(FeCheck *c, FeUnit *u, const char *name);
FeType *unit_type(FeCheck *c, FeUnit *u, const char *name);
int enter_declaring_unit(FeCheck *c, const char *unit_name);
FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name);
FeType *node_type_in(FeCheck *c, const char *unit, FeNode *node);
FeNode *find_method(FeCheck *c, FeType *owner, const char *name);
+3 -1
View File
@@ -79,7 +79,6 @@ void declare_unit(FeCheck *c)
fe_type_declare_enum(&c->types,n);
for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next)
if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n);
check_type_cycles(c);
}
/* The unit's top-level names, in a scope of their own so that another unit
@@ -166,6 +165,9 @@ int fe_check_program(FeCheck *c)
s.fn_node=0;
fe_own_liveness_init(&s.liveness,&c->arena);
for (u=0;u<c->build->count;++u) { enter_unit(c,u); declare_unit(c); }
/* Only now: a field may name a type in a unit that had not declared it
yet, and resolving it early would freeze the wrong answer in place. */
for (u=0;u<c->build->count;++u) { enter_unit(c,u); check_type_cycles(c); }
fe_type_layout_all(&c->types);
for (u=0;u<c->build->count;++u) {
enter_unit(c,u);
+11 -4
View File
@@ -140,18 +140,25 @@ void check_type_cycle(FeCheck *c, FeType *t)
if (t->kind == FE_TYPE_ARRAY) {
check_type_cycle(c,t->elem);
} else if (t->kind == FE_TYPE_STRUCT) {
for (i=0;i<t->field_count;i++) {
int back=enter_declaring_unit(c,t->unit);
for (i=0;i<t->field_count;i++)
if (!t->fields[i].type && t->fields[i].ast_node)
t->fields[i].type=fe_type_from_ast(&c->types,t->fields[i].ast_node->a);
check_type_cycle(c,t->fields[i].type);
}
if (back>=0) enter_unit(c,(unsigned)back);
for (i=0;i<t->field_count;i++) check_type_cycle(c,t->fields[i].type);
} else if (t->kind == FE_TYPE_ENUM) {
int back=enter_declaring_unit(c,t->unit);
for (i=0;i<t->variant_count;i++) {
unsigned j;
for (j=0;j<t->variants[i].field_count;j++) {
for (j=0;j<t->variants[i].field_count;j++)
if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node)
t->variants[i].fields[j].type=fe_type_from_ast(&c->types,
t->variants[i].fields[j].ast_node->a);
}
if (back>=0) enter_unit(c,(unsigned)back);
for (i=0;i<t->variant_count;i++) {
unsigned j;
for (j=0;j<t->variants[i].field_count;j++) {
next=t->variants[i].fields[j].type;
check_type_cycle(c,next);
}
+10 -1
View File
@@ -113,9 +113,18 @@ unsigned as_address(Lower *L, Slot s, FeNode *n)
/* Does letting go of this type have to do something? */
int needs_release(const FeType *t)
{
unsigned i;
if (!t) return 0;
if (t->kind == FE_TYPE_OWNED) return 1;
return t->has_drop != 0;
if (t->has_drop) return 1;
/* SPEC 5 R1: letting go of an owner lets go of what it owns. A struct that
holds an owner has something to do even when it says nothing itself --
which is what lets one type hold another that has a `drop`, since
calling `drop` by hand is not allowed. */
if (t->kind == FE_TYPE_STRUCT)
for (i = 0; i < t->field_count; ++i)
if (needs_release(t->fields[i].type)) return 1;
return 0;
}
int lower_reserve(Lower *L, void **items, unsigned *capacity, unsigned needed,
+38 -20
View File
@@ -171,6 +171,12 @@ Slot lower_expr_core(Lower *L, FeNode *n)
if (base && (base->kind == FE_TYPE_REF ||
base->kind == FE_TYPE_OWNED)) base = base->elem;
field = fe_type_field(base, n->b && n->b->text ? n->b->text : "");
/* `binding.name` is not a field of anything: it is a constant or a
global in another unit, and the checker already turned it into a
link name. */
if (!field && n->cname)
return slot_place(fe_ir_at_global(n->cname, 0), it,
ir_size(t));
if (!field) { fail(L, "an unresolved field", n); return slot_void(); }
b = lower_expr(L, n->a);
if (n->a->sem_type && (n->a->sem_type->kind == FE_TYPE_REF ||
@@ -305,6 +311,37 @@ const char *drop_name(Lower *L, const FeType *t)
return method->cname;
}
/* Let go of one value sitting at `at`. A type that says how to let go of
itself is asked first; then whatever it holds is let go of in turn, so a
struct that owns a struct that owns a buffer settles all three without
anyone writing a `drop` (SPEC 5 R1). */
void release_at(Lower *L, const FeType *t, FeIrPlace at)
{
unsigned args[1];
unsigned i;
if (!t) return;
if (t->has_drop) {
const char *how = drop_name(L, t);
args[0] = fe_ir_addr(L->m, L->b, at);
if (how) fe_ir_call(L->m, L->b, FE_IR_VOID, how, args, 1);
}
if (t->kind == FE_TYPE_OWNED) {
FeIrPlace p = at;
if (t->elem && t->elem->kind == FE_TYPE_SLICE)
p.offset += SLICE_PTR_OFFSET;
args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, p);
fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1);
return;
}
if (t->kind == FE_TYPE_STRUCT)
for (i = 0; i < t->field_count; ++i) {
FeIrPlace p = at;
if (!needs_release(t->fields[i].type)) continue;
p.offset += (long)t->fields[i].offset;
release_at(L, t->fields[i].type, p);
}
}
/* Settle what a scope owes, most recent first. A `return` in the middle of a
function still owes everything, so every exit path calls this. */
void run_deferred(Lower *L, unsigned from)
@@ -321,29 +358,10 @@ void run_deferred(Lower *L, unsigned from)
fe_ir_at_local(L->owed[i - 1].flag, 0));
FeIrBlock *doit = new_block(L);
FeIrBlock *skip = new_block(L);
unsigned args[1];
FeType *t = L->owed[i - 1].type;
fe_ir_br(L->b, live, doit->id, skip->id);
L->b = doit;
if (t && t->kind == FE_TYPE_OWNED && t->elem &&
t->elem->kind == FE_TYPE_SLICE) {
FeIrPlace at = fe_ir_at_local(L->owed[i - 1].local,
SLICE_PTR_OFFSET);
args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, at);
} else {
args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR,
fe_ir_at_local(L->owed[i - 1].local, 0));
}
if (t && t->has_drop) {
/* A type that says how to let go of itself is asked to; the
name is the one its instance was given. */
const char *how = drop_name(L, t);
args[0] = fe_ir_addr(L->m, L->b,
fe_ir_at_local(L->owed[i - 1].local, 0));
if (how) fe_ir_call(L->m, L->b, FE_IR_VOID, how, args, 1);
} else {
fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1);
}
release_at(L, t, fe_ir_at_local(L->owed[i - 1].local, 0));
fe_ir_jmp(L->b, skip->id);
L->b = skip;
}
+1
View File
@@ -145,6 +145,7 @@ Slot lower_call(Lower *L, FeNode *n);
Slot lower_expr(Lower *L, FeNode *n);
Slot lower_expr_core(Lower *L, FeNode *n);
const char *drop_name(Lower *L, const FeType *t);
void release_at(Lower *L, const FeType *t, FeIrPlace at);
void run_deferred(Lower *L, unsigned from);
Slot wrap_context(Lower *L, Slot v, FeNode *n);
unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n);
+52 -18
View File
@@ -47,6 +47,7 @@ static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind)
t->next = ctx->types;
t->emit_state = 0;
t->cycle_state = 0;
t->layout_state = 0;
ctx->types = t;
return t;
}
@@ -61,6 +62,8 @@ void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits)
ctx->param_count = 0;
ctx->instantiate = 0;
ctx->instantiate_owner = 0;
ctx->enter_decl = 0;
ctx->leave_decl = 0;
}
/* Does this type answer to `name` for someone checking `unit`? A type with no
@@ -452,6 +455,26 @@ unsigned fe_type_align(const FeType *t)
return t && t->align ? t->align : 1U;
}
/* Resolve this type's fields where they were written. Without the callback
installed -- or for a type nobody declared -- everything stays where it is,
which is what the non-checking users of this layer want. */
static int enter_decl_unit(FeTypeCtx *ctx, const char *unit, const char **was)
{
*was = ctx->unit_name;
if (!ctx->enter_decl || !unit) return -1;
return ctx->enter_decl(ctx->instantiate_owner, unit);
}
/* Put back both halves: the unit the checker was in, and the name this layer
was interning under -- an instantiation moves the second without the
first, so restoring one is not restoring the other. */
static void leave_decl_unit(FeTypeCtx *ctx, int back, const char *was)
{
if (back >= 0 && ctx->leave_decl)
ctx->leave_decl(ctx->instantiate_owner, back);
ctx->unit_name = was;
}
static void layout_type(FeTypeCtx *ctx, FeType *t)
{
unsigned i;
@@ -460,14 +483,14 @@ static void layout_type(FeTypeCtx *ctx, FeType *t)
unsigned long max_size;
unsigned max_align;
if (!t || t->size) return;
if (t->cycle_state == 1) {
if (t->layout_state == 1) {
t->size = 1;
t->align = 1;
return;
}
t->cycle_state = 1;
t->layout_state = 1;
if (t->kind == FE_TYPE_VOID || t->kind == FE_TYPE_UNKNOWN ||
t->kind == FE_TYPE_ERROR) { t->size = 0; t->align = 1; t->cycle_state = 2; return; }
t->kind == FE_TYPE_ERROR) { t->size = 0; t->align = 1; t->layout_state = 2; return; }
if (t->kind == FE_TYPE_ERROR_UNION) {
if (t->error_value && t->error_value->kind != FE_TYPE_VOID) {
layout_type(ctx,t->error_value);
@@ -478,7 +501,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t)
t->size=2;
t->align=2U;
}
t->cycle_state = 2; return;
t->layout_state = 2; return;
}
if (t->kind == FE_TYPE_OPTIONAL) {
layout_type(ctx,t->elem);
@@ -490,44 +513,48 @@ static void layout_type(FeTypeCtx *ctx, FeType *t)
t->size=round_up(1UL,t->align)+fe_type_size(t->elem);
t->size=round_up(t->size,t->align);
}
t->cycle_state=2; return;
t->layout_state=2; return;
}
if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) {
t->size = 1; t->align = 1; t->cycle_state = 2; return;
t->size = 1; t->align = 1; t->layout_state = 2; return;
}
if (t->kind == FE_TYPE_INT) {
t->size = (t->bits + 7U) / 8U;
t->align = t->size;
if (t->size > 4UL) t->size = 4UL;
t->cycle_state = 2; return;
t->layout_state = 2; return;
}
if (t->kind == FE_TYPE_REF || t->kind == FE_TYPE_RAW) {
t->size = FE_PTR_SIZE;
t->align = FE_PTR_ALIGN;
t->cycle_state = 2; return;
t->layout_state = 2; return;
}
if (t->kind == FE_TYPE_OWNED) {
t->size = t->elem && t->elem->kind==FE_TYPE_SLICE ?
2UL * FE_PTR_SIZE : FE_PTR_SIZE;
t->align = FE_PTR_ALIGN;
t->cycle_state = 2; return;
t->layout_state = 2; return;
}
if (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR) {
t->size = 2UL * FE_PTR_SIZE;
t->align = FE_PTR_ALIGN;
t->cycle_state = 2; return;
t->layout_state = 2; return;
}
if (t->kind == FE_TYPE_ARRAY) {
layout_type(ctx, t->elem);
t->align = t->packed ? 1U : fe_type_align(t->elem);
t->size = t->length * fe_type_size(t->elem);
t->cycle_state = 2; return;
t->layout_state = 2; return;
}
if (t->kind == FE_TYPE_STRUCT) {
off = 0; max_align = 1;
for (i = 0; i < t->field_count; ++i) {
const char *was;
int back = enter_decl_unit(ctx, t->unit, &was);
for (i = 0; i < t->field_count; ++i)
if (!t->fields[i].type && t->fields[i].ast_node)
t->fields[i].type = fe_type_from_ast(ctx, t->fields[i].ast_node->a);
leave_decl_unit(ctx, back, was);
off = 0; max_align = 1;
for (i = 0; i < t->field_count; ++i) {
layout_type(ctx, t->fields[i].type);
align = t->packed ? 1U : fe_type_align(t->fields[i].type);
if (align > max_align) max_align = align;
@@ -537,18 +564,25 @@ static void layout_type(FeTypeCtx *ctx, FeType *t)
}
t->align = max_align;
t->size = round_up(off, max_align);
t->cycle_state = 2;
t->layout_state = 2;
return;
}
if (t->kind == FE_TYPE_ENUM) {
const char *was;
int back = enter_decl_unit(ctx, t->unit, &was);
for (i = 0; i < t->variant_count; ++i) {
unsigned j;
for (j = 0; j < t->variants[i].field_count; ++j)
if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node)
t->variants[i].fields[j].type = fe_type_from_ast(
ctx, t->variants[i].fields[j].ast_node->a);
}
leave_decl_unit(ctx, back, was);
max_size = 0; max_align = 1;
for (i = 0; i < t->variant_count; ++i) {
unsigned j;
off = 0;
for (j = 0; j < t->variants[i].field_count; ++j) {
if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node)
t->variants[i].fields[j].type = fe_type_from_ast(
ctx, t->variants[i].fields[j].ast_node->a);
layout_type(ctx, t->variants[i].fields[j].type);
if (fe_type_align(t->variants[i].fields[j].type) > max_align)
max_align = fe_type_align(t->variants[i].fields[j].type);
@@ -564,7 +598,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t)
off = round_up(t->bits / 8U, max_align);
t->size = round_up(off + max_size, max_align);
t->align = max_align;
t->cycle_state = 2;
t->layout_state = 2;
}
}
+11
View File
@@ -98,6 +98,11 @@ struct FeType {
FeType *next;
int emit_state;
int cycle_state;
/* Separate from `cycle_state`: the checker's by-value recursion walk and
this layer's size computation run inside one another, and sharing one
marker made a struct in the middle of the first look complete to the
second -- one byte wide, with every field on top of the next. */
int layout_state;
};
typedef struct FeTypeCtx {
@@ -114,6 +119,12 @@ typedef struct FeTypeCtx {
so it installs this and the type layer calls back into it. */
FeType *(*instantiate)(void *owner, const FeNode *node);
void *instantiate_owner;
/* A field type is written in the unit that declared it, so resolving one
has to happen with that unit's imports in scope. The checker owns that
knowledge, so it installs this pair and the type layer calls back.
`enter` answers with what to hand `leave`, or -1 for "stayed put". */
int (*enter_decl)(void *owner, const char *unit);
void (*leave_decl)(void *owner, int back);
} FeTypeCtx;
void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits);