commit d08e794c51bffbb4075e83df6dcb0d1bb3f2fc64
parent b8de5c0b45f3724531f7f6a797f82cb719da4e87
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 2 Jun 2026 04:47:01 -0700
cg: bitfield as a PLACE subkind, drop the MemAccess bitfield rider (Track 3b)
Diffstat:
6 files changed, 135 insertions(+), 57 deletions(-)
diff --git a/include/cfree/cg.h b/include/cfree/cg.h
@@ -257,13 +257,6 @@ typedef struct CfreeCgMemAccess {
uint32_t align; /* 0 = natural for type */
uint32_t address_space; /* normally inherited from pointer type */
uint32_t flags; /* CfreeCgMemAccessFlag */
- /* Bit-field metadata. When bit_width != 0 the memop performs a bit-field
- * access at bit range [bit_offset, bit_offset + bit_width) within a
- * storage_size-byte container located at the EA. */
- uint16_t bit_offset;
- uint16_t bit_width;
- uint32_t storage_size;
- int bit_signed;
} CfreeCgMemAccess;
/* ============================================================
@@ -625,9 +618,33 @@ CFREE_API void cfree_cg_deref(CfreeCg*, int64_t offset);
/* PLACE(record) -> PLACE(field): project a record place to one of its fields by
* index; the byte offset and field type come from the record's layout (CG owns
* layout). Stack: [place] -> [place]. Errors if TOS is not a record PLACE — for
- * `p->f` the frontend does `deref; field`, not `field` on a pointer. */
+ * `p->f` the frontend does `deref; field`, not `field` on a pointer.
+ *
+ * Bit-field PLACE subkind: when field_index names a bit-field, `field` projects
+ * to a *bit-field place* — a PLACE that addresses the field's storage unit and
+ * additionally carries the bit-field descriptor (bit offset/width, storage-unit
+ * size, signedness) from the record layout. A bit-field place is a place like
+ * any other: a plain `load` reads the field (extract + sign/zero-extend) and a
+ * plain `store` writes it (read-modify-write insert). There is no separate
+ * bit-field memop and no bit-field rider on CfreeCgMemAccess; the place is the
+ * one and only carrier of bit-field geometry. `addr` and aggregate access on a
+ * bit-field place are not legal (a bit-field has no addressable byte location);
+ * the frontend rejects `&` / `sizeof` on a bit-field before reaching CG. */
CFREE_API void cfree_cg_field(CfreeCg*, uint32_t field_index);
+/* PLACE -> PLACE(bit-field): tag the TOS place as a bit-field place carrying the
+ * given bit geometry. This is the manual-addressing counterpart to the implicit
+ * bit-field projection `field` performs: a frontend that builds the storage-unit
+ * place itself (deref/elem onto the storage unit) calls this to attach the
+ * descriptor, after which a plain load/store extracts/inserts the field. The
+ * place must address the field's storage unit. bit_storage_size is the storage-
+ * unit size in bytes; bit_signed selects signed extraction on load. Stack:
+ * [place] -> [place]. There is no separate bit-field memop — the place is the
+ * one carrier of bit-field geometry. */
+CFREE_API void cfree_cg_field_bits(CfreeCg*, uint16_t bit_offset,
+ uint16_t bit_width, uint32_t bit_storage_size,
+ int bit_signed);
+
/* (VALUE(ptr to T), index VALUE) -> PLACE(T): the element at `*(base +
* index*sizeof(T) + offset)`. The index is scaled by sizeof(T) (the pointer's
* pointee) and the constant `offset` byte displacement is folded in, so the
@@ -639,9 +656,11 @@ CFREE_API void cfree_cg_elem(CfreeCg*, int64_t offset);
/* Load a VALUE from a PLACE / store a VALUE into a PLACE. The PLACE carries all
* addressing (built via push_local / deref / field / elem); the memop takes no
- * effective-address rider. Bit-field metadata rides in `access`. Both error if
- * the addressed operand is not a PLACE — a pointer VALUE must be `deref`'d
- * first.
+ * effective-address rider. When the PLACE is a bit-field place (produced by
+ * `field` on a bit-field), the load/store extract/insert the field per the
+ * descriptor the place carries — `access` then describes only the access type,
+ * alignment, and volatility, never bit geometry. Both error if the addressed
+ * operand is not a PLACE — a pointer VALUE must be `deref`'d first.
*
* Stack effects:
* load: [place] -> [value]
diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c
@@ -437,19 +437,16 @@ void pcg_push_float(Parser* p, double v, const Type* ty) {
pcg_push_type(p, ty);
}
-/* Fill `access` (CfreeCgMemAccess) for a memop against an lvalue carrying the
- * given aux. Bit-field metadata rides in the access; addressing is no longer a
- * rider — it is built explicitly with deref/elem/field before the load/store. */
-static CfreeCgMemAccess pcg_access_for(Parser* p, const Type* access_ty,
- const PcgLvAux* lv) {
- CfreeCgMemAccess access = pcg_mem(p, access_ty);
+/* Tag the TOS place as a bit-field place when `lv` carries bit-field geometry.
+ * The frontend builds the storage-unit place itself (materialize + deref), so it
+ * attaches the descriptor here; a plain load/store then extracts/inserts the
+ * field. Must be called on the place each time a fresh one is built (each deref
+ * yields a plain place). No-op for non-bit-field lvalues. */
+static void pcg_apply_bitfield(Parser* p, const PcgLvAux* lv) {
if (lv && lv->bit_width) {
- access.bit_offset = lv->bit_offset;
- access.bit_width = lv->bit_width;
- access.storage_size = lv->storage_size;
- access.bit_signed = lv->bit_signed;
+ cfree_cg_field_bits(p->cg, lv->bit_offset, lv->bit_width, lv->storage_size,
+ lv->bit_signed);
}
- return access;
}
/* A trivially-addressable lvalue: a bare local slot with no pending field
@@ -493,15 +490,19 @@ void pcg_load(Parser* p) {
int was_lvalue = pcg_top_is_lvalue(p);
if (pcg_emit_enabled(p)) {
PcgLvAux* lv = pcg_top_lv_aux(p);
- CfreeCgMemAccess access = pcg_access_for(p, ty, lv);
+ CfreeCgMemAccess access = pcg_mem(p, ty);
+ /* Snapshot bit-field geometry before materialize clears the aux. */
+ PcgLvAux bf = lv ? *lv : (PcgLvAux){0};
/* Build the PLACE the strict load requires. A trivial local already has its
* PLACE on the CG stack (push_local) and loads directly; anything else is
* reduced to a single pointer (materialize, or a pointer-rvalue base) and
- * then deref'd to a place. */
+ * then deref'd to a place. A bit-field place is then tagged with its bit
+ * geometry so the load extracts the field. */
if (!was_lvalue || !pcg_lv_is_trivial_local(lv)) {
if (was_lvalue) pcg_materialize_lv_to_ptr(p, type_ptr(p->pool, ty));
cfree_cg_deref(p->cg, 0);
}
+ pcg_apply_bitfield(p, &bf);
cfree_cg_load(p->cg, access);
}
if (was_lvalue && p->cg_type_sp) {
@@ -634,11 +635,13 @@ void pcg_store(Parser* p) {
int emit = pcg_emit_enabled(p);
/* The aux to consume lives on the lvalue slot at parser depth 1. */
PcgLvAux* lv = pcg_lv_aux_at(p, 1);
+ /* Snapshot bit-field geometry before materialize clears the aux. */
+ PcgLvAux bf = lv ? *lv : (PcgLvAux){0};
CfreeCgMemAccess access;
if (rv_ty && type_is_ptr(rv_ty) && (!lv_ty || !type_is_ptr(lv_ty))) {
mem_ty = rv_ty;
}
- access = pcg_access_for(p, mem_ty ? mem_ty : rv_ty, lv);
+ access = pcg_mem(p, mem_ty ? mem_ty : rv_ty);
if (emit) {
int wide = rv_ty && (rv_ty->kind == TY_INT128 ||
rv_ty->kind == TY_UINT128 || rv_ty->kind == TY_LDOUBLE);
@@ -669,10 +672,12 @@ void pcg_store(Parser* p) {
if (!trivial) {
/* Materialize operates on the parser-TOS lvalue; drop the rv type slot
* so the lvalue is on top and matches the CG [base..]. The pointer is
- * deref'd to the PLACE the strict store requires. */
+ * deref'd to the PLACE the strict store requires, then tagged with the
+ * bit-field geometry so the store inserts the field. */
pcg_drop_type(p);
pcg_materialize_lv_to_ptr(p, type_ptr(p->pool, lv_ty)); /* [dst_ptr] */
cfree_cg_deref(p->cg, 0); /* [dst_place] */
+ pcg_apply_bitfield(p, &bf);
pcg_push_type(p, rv_ty);
}
cfree_cg_push_local(p->cg, tmp);
@@ -927,7 +932,9 @@ void pcg_inc_dec(Parser* p, BinOp op, int post) {
{
CfreeCgIntBinOp cg_op = pcg_int_binop(op);
PcgLvAux* lv = pcg_top_lv_aux(p);
- CfreeCgMemAccess access = pcg_access_for(p, ty, lv);
+ /* Snapshot bit-field geometry before materialize clears the aux. */
+ PcgLvAux bf = lv ? *lv : (PcgLvAux){0};
+ CfreeCgMemAccess access = pcg_mem(p, ty);
const Type* step_ty = ty;
u32 step = 1;
if (ty && ty->kind == TY_PTR) {
@@ -953,6 +960,7 @@ void pcg_inc_dec(Parser* p, BinOp op, int post) {
tmp = pcg_local(p, &fsd);
cfree_cg_dup(p->cg); /* [ptr, ptr] */
cfree_cg_deref(p->cg, 0); /* [ptr, place] */
+ pcg_apply_bitfield(p, &bf);
cfree_cg_load(p->cg, access); /* [ptr, old] */
if (post) {
/* Stash old, compute new, store, then re-load old as result. */
@@ -963,6 +971,7 @@ void pcg_inc_dec(Parser* p, BinOp op, int post) {
pcg_emit_inc_step(p, ty, op, cg_op, step_ty, step); /* [ptr, new] */
cfree_cg_swap(p->cg); /* [new, ptr] */
cfree_cg_deref(p->cg, 0); /* [new, place] */
+ pcg_apply_bitfield(p, &bf);
cfree_cg_swap(p->cg); /* [place, new] */
cfree_cg_store(p->cg, access); /* [] */
cfree_cg_push_local(p->cg, tmp);
@@ -976,6 +985,7 @@ void pcg_inc_dec(Parser* p, BinOp op, int post) {
cfree_cg_store(p->cg, r_access); /* [ptr, new] */
cfree_cg_swap(p->cg); /* [new, ptr] */
cfree_cg_deref(p->cg, 0); /* [new, place] */
+ pcg_apply_bitfield(p, &bf);
cfree_cg_swap(p->cg); /* [place, new] */
cfree_cg_store(p->cg, access); /* [] */
cfree_cg_push_local(p->cg, tmp);
diff --git a/src/cg/control.c b/src/cg/control.c
@@ -966,13 +966,17 @@ void cfree_cg_field(CfreeCg* g, uint32_t field_index) {
api_release(g, &base);
return;
}
- /* Push the record-base address as a place of the field type. The bit-field
- * geometry (storage size/offset, bit offset/width, signedness) is carried
- * by the CfreeCgMemAccess the frontend supplies on the following load/store,
- * which rebuilds the descriptor via bf_from_access; the place itself only
- * needs to address the enclosing storage. */
+ /* Project to a bit-field PLACE: the place addresses the enclosing storage
+ * unit and carries the bit-field geometry from the record layout. A plain
+ * load/store on this place performs the extract/insert; there is no separate
+ * bit-field memop and no bit-field rider on CfreeCgMemAccess. */
base_addr = api_lvalue_addr(g, &base, rec_ptr_ty);
sv = api_make_lv(base_addr, field_ty);
+ sv.bitfield.bit_offset = layout->fields[field_index].bit_offset;
+ sv.bitfield.bit_width = layout->fields[field_index].bit_width;
+ sv.bitfield.bit_storage_size = layout->fields[field_index].storage_size;
+ sv.bitfield.bit_signed =
+ rec_info->record.fields[field_index].bit_signed ? 1u : 0u;
api_release(g, &base);
api_push(g, sv);
return;
@@ -1010,6 +1014,22 @@ void cfree_cg_field(CfreeCg* g, uint32_t field_index) {
}
}
+void cfree_cg_field_bits(CfreeCg* g, uint16_t bit_offset, uint16_t bit_width,
+ uint32_t bit_storage_size, int bit_signed) {
+ ApiSValue* top;
+ if (!g || g->sp == 0) return;
+ top = &g->stack[g->sp - 1u];
+ if (!api_is_lvalue_sv(top)) {
+ compiler_panic(g->c, g->cur_loc,
+ "CfreeCg: field_bits requires a place destination");
+ return;
+ }
+ top->bitfield.bit_offset = bit_offset;
+ top->bitfield.bit_width = bit_width;
+ top->bitfield.bit_storage_size = bit_storage_size;
+ top->bitfield.bit_signed = bit_signed ? 1u : 0u;
+}
+
/* ============================================================
* Calls / return
* ============================================================ */
diff --git a/src/cg/internal.h b/src/cg/internal.h
@@ -59,6 +59,19 @@ typedef struct ApiDelayedArith {
u8 pad;
} ApiDelayedArith;
+/* Bit-field geometry carried by a bit-field PLACE. `cfree_cg_field` fills this
+ * from the record layout when it projects to a bit-field; a plain load/store on
+ * the carrying place then performs the extract/insert. The storage MemAccess is
+ * rebuilt from the place operand + field type at load/store time, so only the
+ * bit geometry needs to ride on the place. width == 0 means "not a bit-field". */
+typedef struct ApiBitField {
+ u16 bit_offset; /* target-endian bit offset within the storage unit */
+ u16 bit_width; /* 0 => the place is not a bit-field place */
+ u32 bit_storage_size; /* storage-unit size in bytes */
+ u8 bit_signed; /* signed extraction on load */
+ u8 pad[3];
+} ApiBitField;
+
typedef struct ApiSValue {
Operand op;
union {
@@ -71,6 +84,7 @@ typedef struct ApiSValue {
u8 pinned;
u8 lvalue;
CfreeCgLocal source_local;
+ ApiBitField bitfield; /* bit_width != 0 marks a bit-field PLACE subkind */
} ApiSValue;
#define API_CG_STACK_INITIAL 16u
@@ -261,6 +275,8 @@ void cfree_cg_push_label_addr(CfreeCg* g, CfreeCgLabel label,
void cfree_cg_computed_goto(CfreeCg* g, const CfreeCgLabel* valid_targets,
uint32_t ntargets);
+void cfree_cg_field_bits(CfreeCg* g, uint16_t bit_offset, uint16_t bit_width,
+ uint32_t bit_storage_size, int bit_signed);
void cfree_cg_unreachable(CfreeCg* g);
CfreeCgScope api_scope_handle(u32 idx, u32 generation);
ApiCgScope* api_scope_from_handle(CfreeCg* g, CfreeCgScope scope,
@@ -384,6 +400,9 @@ int api_operand_can_address(const Operand* o);
int api_sv_op_is(const ApiSValue* sv, OpKind kind);
int api_sv_op_is_local_or_imm(const ApiSValue* sv);
int api_is_lvalue_sv(const ApiSValue* sv);
+int api_sv_is_bitfield(const ApiSValue* sv);
+BitFieldAccess api_bitfield_access(CfreeCg* g, const ApiSValue* sv,
+ const Operand* storage, CfreeCgTypeId field_ty);
void api_stack_grow(CfreeCg* g, u32 want);
void api_push(CfreeCg* g, ApiSValue v);
ApiSValue api_pop(CfreeCg* g);
diff --git a/src/cg/memory.c b/src/cg/memory.c
@@ -142,22 +142,6 @@ void cfree_cg_push_symbol_addr(CfreeCg* g, CfreeCgSym sym, int64_t addend) {
}
-/* Build a BitFieldAccess descriptor from the CfreeCgMemAccess metadata. */
-static BitFieldAccess bf_from_access(CfreeCg* g, CfreeCgMemAccess access,
- CfreeCgTypeId field_ty,
- const Operand* addr) {
- BitFieldAccess bf;
- memset(&bf, 0, sizeof bf);
- bf.field_type = field_ty;
- bf.storage = api_mem_for_lvalue(g, addr, field_ty);
- if (access.storage_size) bf.storage.size = access.storage_size;
- bf.storage_offset = 0;
- bf.bit_offset = access.bit_offset;
- bf.bit_width = access.bit_width;
- bf.signed_ = access.bit_signed ? 1 : 0;
- return bf;
-}
-
static int api_sv_local_storage_is_aggregate(CfreeCg* g, const ApiSValue* sv) {
ApiSourceLocal* rec;
if (!sv || sv->op.kind != OPK_LOCAL ||
@@ -203,13 +187,12 @@ void cfree_cg_load(CfreeCg* g, CfreeCgMemAccess access) {
T = g->target;
if (access.flags & CFREE_CG_MEM_VOLATILE) api_local_const_memory_boundary(g);
- is_bitfield = (access.bit_width != 0);
-
base = api_pop(g);
if (!api_is_lvalue_sv(&base)) {
compiler_panic(g->c, g->cur_loc,
"CfreeCg: load requires a place; deref the pointer first");
}
+ is_bitfield = api_sv_is_bitfield(&base);
/* Aggregate place: an aggregate-typed access returns the place itself; a
* scalar access reads a scalar sub-object and falls through. */
@@ -273,7 +256,7 @@ void cfree_cg_load(CfreeCg* g, CfreeCgMemAccess access) {
}
if (is_bitfield) {
- BitFieldAccess bf = bf_from_access(g, access, access_ty, &mem_op);
+ BitFieldAccess bf = api_bitfield_access(g, &base, &mem_op, access_ty);
CGLocal rr = api_alloc_temp_local(g, access_ty);
dst = api_op_local(rr, access_ty);
T->bitfield_load(T, dst, mem_op, bf);
@@ -359,8 +342,6 @@ void cfree_cg_store(CfreeCg* g, CfreeCgMemAccess access) {
T = g->target;
if (access.flags & CFREE_CG_MEM_VOLATILE) api_local_const_memory_boundary(g);
- is_bitfield = (access.bit_width != 0);
-
/* Stack: [base, value] - pop value, then base. */
rv = api_pop(g);
base = api_pop(g);
@@ -371,6 +352,7 @@ void cfree_cg_store(CfreeCg* g, CfreeCgMemAccess access) {
"CfreeCg: store requires a place destination; deref first");
return;
}
+ is_bitfield = api_sv_is_bitfield(&base);
ty = api_mem_access_type(g, access, api_sv_type(&base), "store");
access_ty = ty;
@@ -501,7 +483,7 @@ void cfree_cg_store(CfreeCg* g, CfreeCgMemAccess access) {
}
if (is_bitfield) {
- BitFieldAccess bf = bf_from_access(g, access, access_ty, &mem_op);
+ BitFieldAccess bf = api_bitfield_access(g, &base, &mem_op, access_ty);
T->bitfield_store(T, mem_op, src, bf);
} else {
T->store(T, mem_op, src, api_mem_from_access(g, &mem_op, access));
diff --git a/src/cg/value.c b/src/cg/value.c
@@ -132,14 +132,42 @@ int api_sv_op_is_local_or_imm(const ApiSValue* sv) {
* never carry lvalue=1 — see fold.c, which clears it on materialization), and
* the operand kind decides addressability. The former heuristic OR'd in
* `bitfield_lvalue` and a (source_local && OPK_LOCAL) term, both subsumed here:
- * a bitfield lvalue is built on the OPK_LOCAL address returned by
- * api_lvalue_addr (see cfree_cg_field), and source_local only co-occurs with
- * OPK_LOCAL — so api_operand_can_address already covers both. */
+ * a bit-field place is an ordinary addressable place (it addresses the storage
+ * unit) that additionally carries a bit descriptor — see api_sv_is_bitfield and
+ * cfree_cg_field — and source_local only co-occurs with OPK_LOCAL, so
+ * api_operand_can_address already covers both. */
int api_is_lvalue_sv(const ApiSValue* sv) {
return sv->lvalue && sv->kind == SV_OPERAND &&
api_operand_can_address(&sv->op);
}
+/* A bit-field PLACE is a PLACE that additionally carries bit geometry (set by
+ * cfree_cg_field when it projects to a bit-field). The plain load/store detect
+ * this and perform the extract/insert; there is no separate bit-field memop. */
+int api_sv_is_bitfield(const ApiSValue* sv) {
+ return sv->bitfield.bit_width != 0;
+}
+
+/* Rebuild the full BitFieldAccess the extract/insert helper consumes from the
+ * geometry the place carries plus the storage operand it addresses. The storage
+ * MemAccess is derived from the operand + field type, the way the old
+ * bf_from_access did, so the place need only carry the bit geometry. */
+BitFieldAccess api_bitfield_access(CfreeCg* g, const ApiSValue* sv,
+ const Operand* storage,
+ CfreeCgTypeId field_ty) {
+ BitFieldAccess bf;
+ memset(&bf, 0, sizeof bf);
+ bf.field_type = field_ty;
+ bf.storage = api_mem_for_lvalue(g, storage, field_ty);
+ if (sv->bitfield.bit_storage_size)
+ bf.storage.size = sv->bitfield.bit_storage_size;
+ bf.storage_offset = 0;
+ bf.bit_offset = sv->bitfield.bit_offset;
+ bf.bit_width = sv->bitfield.bit_width;
+ bf.signed_ = sv->bitfield.bit_signed ? 1 : 0;
+ return bf;
+}
+
void api_stack_grow(CfreeCg* g, u32 want) {
Heap* h = g->c->ctx->heap;
u32 cap = g->cap;