lower: 태그드 유니온을 해체한다 -- match 페이로드와 if let

AST 도 IR 도 본질이 태그드 유니온이다. 태그 비교만 되고 안을 꺼내지 못하면
검사 없이 필드를 읽어야 하고, 그러면 안전성 이야기가 통째로 무너진다.

무언가를 담는 변이는 메모리다: 태그가 앞, 페이로드가 뒤. 태그를 읽는 것은
어느 쪽이든 같은 질문이고 자리만 다르다. 페이로드는 가리키지 않고 복사한다 --
매치한 것의 소유권을 arm 이 가져가는 것이 보통이고, 그게 허용되는지는 검사기가
이미 판단했다.

레코드 변이의 필드 오프셋을 아무도 기록하지 않고 있었다. 레이아웃이 크기는
재면서 자리는 버렸다.

Enum.Variant{...} 와 Enum.Variant(x) 와 페이로드 없는 Enum.Variant 를 모두
만든다. 마지막 것은 페이로드를 가진 enum 안에서는 태그만 든 값이다.

  some 7 / none / point 3 4 / empty

exec.py 25/25.
This commit is contained in:
2026-08-17 07:15:57 +09:00
parent 676fef88fb
commit 0fd3f187f4
6 changed files with 197 additions and 7 deletions
+44 -5
View File
@@ -108,13 +108,23 @@ Slot lower_expr_core(Lower *L, FeNode *n)
fail(L, "this unary operator", n); fail(L, "this unary operator", n);
return slot_void(); return slot_void();
case FE_N_MEMBER: case FE_N_MEMBER:
/* A payload-free variant used as a value is just its tag. */ /* A variant used as a value carries nothing but its tag. When no
if (t && t->kind == FE_TYPE_ENUM && !enum_has_payload(t) && variant of the enum carries anything the whole value is that tag;
n->b && n->b->text) { otherwise it is a tag sitting in front of an unused payload. */
if (t && t->kind == FE_TYPE_ENUM && n->b && n->b->text) {
FeVariantType *v = fe_type_variant(t, n->b->text); FeVariantType *v = fe_type_variant(t, n->b->text);
if (v) if (v && !enum_has_payload(t))
return slot_value(fe_ir_const(L->m, L->b, ir_type(t), return slot_value(fe_ir_const(L->m, L->b, ir_type(t),
(long)v->tag), ir_type(t)); (long)v->tag), ir_type(t));
if (v && !v->field_count) {
unsigned local = scratch(L, t, "variant");
unsigned tag = fe_ir_const(L->m, L->b, tag_type_of(t),
(long)v->tag);
fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), tag,
tag_type_of(t));
return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM,
ir_size(t));
}
} }
/* `error.Name` is a member of the open default set: a code, and /* `error.Name` is a member of the open default set: a code, and
nothing to look up. */ nothing to look up. */
@@ -211,8 +221,37 @@ Slot lower_expr_core(Lower *L, FeNode *n)
return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t));
} }
case FE_N_STRUCT_INIT: { case FE_N_STRUCT_INIT: {
unsigned local = scratch(L, t, "struct"); unsigned local;
FeNode *f; FeNode *f;
/* `Enum.Variant{ .. }` builds a variant, not a struct: the tag first,
then the named fields inside the payload area. */
if (t && t->kind == FE_TYPE_ENUM && n->a && n->a->kind == FE_N_MEMBER) {
const FeVariantType *v = fe_type_variant(t,
n->a->b && n->a->b->text ? n->a->b->text : "");
long base = (long)fe_type_payload_offset(t);
unsigned tag;
if (!v) { fail(L, "an unknown variant", n); return slot_void(); }
local = scratch(L, t, "variant");
tag = fe_ir_const(L->m, L->b, tag_type_of(t), (long)v->tag);
fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), tag,
tag_type_of(t));
for (f = n->children; f; f = f->next) {
unsigned i;
if (f->kind != FE_N_FIELD) continue;
for (i = 0; i < v->field_count; ++i)
if (f->text && v->fields[i].name &&
!strcmp(v->fields[i].name, f->text)) break;
if (i == v->field_count) {
fail(L, "an unknown variant field", f);
return slot_void();
}
store_into(L, fe_ir_at_local(local,
base + (long)v->fields[i].offset),
lower_expr(L, f->a), f, ir_size(v->fields[i].type));
}
return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t));
}
local = scratch(L, t, "struct");
for (f = n->children; f; f = f->next) { for (f = n->children; f; f = f->next) {
FeFieldType *field; FeFieldType *field;
Slot v; Slot v;
+5
View File
@@ -87,6 +87,11 @@ typedef struct Slot {
#define SLICE_LEN_OFFSET 4L #define SLICE_LEN_OFFSET 4L
/* Every definition in lowering, so the split files can see each other. */ /* Every definition in lowering, so the split files can see each other. */
FeIrType tag_type_of(const FeType *t);
void lower_if_let(Lower *L, FeNode *n);
unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n);
void bind_payload(Lower *L, Slot subject, const FeType *t,
const FeVariantType *v, FeNode *arm);
void fail(Lower *L, const char *why, FeNode *n); void fail(Lower *L, const char *why, FeNode *n);
FeIrType ir_type_of(const FeType *t); FeIrType ir_type_of(const FeType *t);
int enum_has_payload(const FeType *t); int enum_has_payload(const FeType *t);
+21
View File
@@ -160,6 +160,27 @@ Slot lower_call(Lower *L, FeNode *n)
if (lower_mem(L, n, &built)) return built; if (lower_mem(L, n, &built)) return built;
if (lower_print(L, n, &built)) return built; if (lower_print(L, n, &built)) return built;
} }
/* `Enum.Variant(payload)` is a constructor, not a call. */
if (ret && ret->kind == FE_TYPE_ENUM && n->a && n->a->kind == FE_N_MEMBER &&
n->a->b && n->a->b->text) {
const FeVariantType *v = fe_type_variant(ret, n->a->b->text);
if (v) {
unsigned local = scratch(L, ret, "variant");
unsigned tag = fe_ir_const(L->m, L->b, tag_type_of(ret),
(long)v->tag);
fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), tag,
tag_type_of(ret));
if (v->field_count && n->children)
store_into(L,
fe_ir_at_local(local,
(long)fe_type_payload_offset(ret) +
(long)v->fields[0].offset),
lower_expr(L, n->children), n->children,
ir_size(v->fields[0].type));
return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM,
ir_size(ret));
}
}
if (!callee) { fail(L, "a call with no target", n); return slot_void(); } if (!callee) { fail(L, "a call with no target", n); return slot_void(); }
/* An aggregate result is written through a hidden first argument. */ /* An aggregate result is written through a hidden first argument. */
if (rt == FE_IR_MEM) { if (rt == FE_IR_MEM) {
+87 -2
View File
@@ -274,6 +274,81 @@ void lower_for(Lower *L, FeNode *n)
arm's pattern in turn. The checker already proved the arms cover everything, arm's pattern in turn. The checker already proved the arms cover everything,
so falling off the end cannot happen in a program that compiled -- but the so falling off the end cannot happen in a program that compiled -- but the
generated code has to go somewhere, and going to the join is right. */ generated code has to go somewhere, and going to the join is right. */
/* The width of a tag: an enum's own, or the byte an optional puts in front. */
FeIrType tag_type_of(const FeType *t)
{
if (!t) return FE_IR_I8;
if (t->kind == FE_TYPE_ERROR_UNION) return FE_IR_I16;
if (t->kind == FE_TYPE_ENUM) return t->bits > 8U ? FE_IR_I16 : FE_IR_I8;
return FE_IR_I8;
}
/* Give an arm's names somewhere to live and put the variant's payload there.
The payload is copied rather than pointed at: an arm that takes ownership of
what it matched is the normal case, and the checker has already decided
whether that was allowed. */
void bind_payload(Lower *L, Slot subject, const FeType *t,
const FeVariantType *v, FeNode *arm)
{
FeNode *name;
unsigned i;
long base;
if (!v || !v->field_count || !arm->children || !subject.is_place) return;
base = (long)fe_type_payload_offset(t);
name = arm->children;
for (i = 0; i < v->field_count && name; ++i, name = name->next) {
FeType *ft = v->fields[i].type;
unsigned local = declare_var(L, name->cname, ft, name->text);
FeIrPlace from = subject.place;
from.offset += base + (long)v->fields[i].offset;
store_into(L, fe_ir_at_local(local, 0),
slot_place(from, ir_type(ft), ir_size(ft)), name,
ir_size(ft));
}
}
/* `if let Some(x) = opt { .. } else { .. }` -- and its None twin.
The optional is read once into a place, the tag decides the branch, and the
binding gets what was inside. A binding whose type is a reference gets the
address instead of a copy: the checker chose that when the payload was not
something you may quietly duplicate. */
void lower_if_let(Lower *L, FeNode *n)
{
FeType *opt = n->a ? n->a->sem_type : 0;
Slot value = lower_expr(L, n->a);
FeNode *binding = n->children;
int is_some = n->aux_text && !strcmp(n->aux_text, "Some");
unsigned tag;
FeIrBlock *present;
FeIrBlock *absent;
FeIrBlock *join;
if (!value.is_place) { fail(L, "if let over a temporary", n); return; }
tag = wrapper_tag(L, value, opt, n);
present = new_block(L);
absent = new_block(L);
join = new_block(L);
fe_ir_br(L->b, tag, present->id, absent->id);
/* Which side runs the body depends on which pattern was written. */
L->b = is_some ? present : absent;
if (is_some && binding) {
FeType *bt = binding->sem_type;
Slot payload = wrapper_payload(L, value, opt);
unsigned local = declare_var(L, binding->cname, bt, binding->text);
if (bt && (bt->kind == FE_TYPE_REF || bt->kind == FE_TYPE_RAW))
fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0),
as_address(L, payload, n), FE_IR_PTR);
else
store_into(L, fe_ir_at_local(local, 0), payload, n, ir_size(bt));
}
lower_stmt(L, n->b);
fe_ir_jmp(L->b, join->id);
L->b = is_some ? absent : present;
if (n->c) lower_stmt(L, n->c);
fe_ir_jmp(L->b, join->id);
L->b = join;
}
void lower_match(Lower *L, FeNode *n) void lower_match(Lower *L, FeNode *n)
{ {
FeType *t = n->a ? n->a->sem_type : 0; FeType *t = n->a ? n->a->sem_type : 0;
@@ -282,8 +357,16 @@ void lower_match(Lower *L, FeNode *n)
unsigned value; unsigned value;
FeIrBlock *join; FeIrBlock *join;
FeNode *arm; FeNode *arm;
if (it == FE_IR_MEM) { fail(L, "a match over a payload", n); return; } /* A variant that carries something is memory: the tag comes first and the
value = as_value(L, subject, n->a); payload after it. Reading the tag is then the same question either way,
just from a different place. */
if (it == FE_IR_MEM) {
if (!subject.is_place) { fail(L, "a match over a temporary", n); return; }
it = tag_type_of(t);
value = fe_ir_load(L->m, L->b, it, subject.place);
} else {
value = as_value(L, subject, n->a);
}
join = new_block(L); join = new_block(L);
for (arm = n->children; arm; arm = arm->next) { for (arm = n->children; arm; arm = arm->next) {
FeIrBlock *body; FeIrBlock *body;
@@ -307,6 +390,7 @@ void lower_match(Lower *L, FeNode *n)
next = new_block(L); next = new_block(L);
fe_ir_br(L->b, same, body->id, next->id); fe_ir_br(L->b, same, body->id, next->id);
L->b = body; L->b = body;
bind_payload(L, subject, t, v, arm);
lower_stmt(L, arm->a); lower_stmt(L, arm->a);
fe_ir_jmp(L->b, join->id); fe_ir_jmp(L->b, join->id);
L->b = next; L->b = next;
@@ -362,6 +446,7 @@ void lower_stmt(Lower *L, FeNode *n)
lower_return(L, n); lower_return(L, n);
return; return;
case FE_N_IF: case FE_N_IF:
if (n->text && !strcmp(n->text, "if let")) { lower_if_let(L, n); return; }
lower_if(L, n); lower_if(L, n);
return; return;
case FE_N_WHILE: case FE_N_WHILE:
+5
View File
@@ -443,6 +443,7 @@ unsigned long fe_type_payload_offset(const FeType *t)
if (fe_m7_optional_uses_niche(t->elem)) return 0; if (fe_m7_optional_uses_niche(t->elem)) return 0;
return round_up(1UL, fe_type_align(t->elem)); return round_up(1UL, fe_type_align(t->elem));
} }
if (t->kind == FE_TYPE_ENUM) return round_up(t->bits / 8U, t->align);
return 0; return 0;
} }
@@ -551,6 +552,10 @@ static void layout_type(FeTypeCtx *ctx, FeType *t)
layout_type(ctx, t->variants[i].fields[j].type); layout_type(ctx, t->variants[i].fields[j].type);
if (fe_type_align(t->variants[i].fields[j].type) > max_align) if (fe_type_align(t->variants[i].fields[j].type) > max_align)
max_align = fe_type_align(t->variants[i].fields[j].type); max_align = fe_type_align(t->variants[i].fields[j].type);
/* Where this field sits inside the payload area, which the
code generator needs and nobody was recording. */
off = round_up(off, fe_type_align(t->variants[i].fields[j].type));
t->variants[i].fields[j].offset = off;
off += fe_type_size(t->variants[i].fields[j].type); off += fe_type_size(t->variants[i].fields[j].type);
} }
if (off > max_size) max_size = off; if (off > max_size) max_size = off;
+35
View File
@@ -0,0 +1,35 @@
// EXIT:0
// OUTPUT:some 7
// OUTPUT:none
// OUTPUT:point 3 4
// OUTPUT:empty
unit patterns;
enum Shape {
Empty,
Point(i32),
Pair { x: i32, y: i32 },
}
fn pick(flag: bool) -> ?i32 {
if flag { return 7; }
return null;
}
fn describe(s: Shape) -> void {
match s {
Empty => { @print("empty\n"); }
Point(v) => { @print("point {}\n", v); }
Pair { x, y } => { @print("point {} {}\n", x, y); }
}
}
fn main() -> i32 {
if let Some(v) = pick(true) { @print("some {}\n", v); }
else { @print("unexpected\n"); }
if let None = pick(false) { @print("none\n"); }
else { @print("unexpected\n"); }
describe(Shape.Pair{ x: 3, y: 4 });
describe(Shape.Empty);
return 0;
}