kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 4e4c6a08c1906ea9af6dae7157b9d2b27b72e9c6
parent 49ddbdeb2d484ac81bf4da4703f4b33f6f24de4a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 29 May 2026 15:29:25 -0700

C front end: implement labels-as-values (computed goto)

Add GNU labels-as-values to the C front end, reusing the existing
CG/IR/backend support that the Toy front end already drove:

  - `&&label` in expressions and static initializers (parse_expr.c,
    parse_init.c) -> cfree_cg_push_label_addr / cfree_cg_data_label_addr,
    yielding a void* value.
  - `goto *expr;` computed goto (parse_stmt.c) -> cfree_cg_computed_goto.
    Valid targets are every label whose address was taken so far; taking a
    label's address after the first computed goto is rejected so each goto
    sees the complete target set.
  - Function-local `static void *tab[] = { &&a, &&b }` dispatch tables:
    label-address relocations route through the FUNCTION_LOCAL data path.

Block-scope `static T x[] = {...}` is now completed before decl_declare so
the registered CG symbol type carries the real element count, which the C
target uses to size function-local label tables.

With the front end now supporting labels-as-values, the interpreter's
direct-threaded dispatch compiles under __cfree__ too, so the self-host
build threads instead of falling back to the portable switch
(src/interp/engine.c, include/cfree/config.h). Verified by compiling
engine.c with cfree on the threaded path.

Tests (test/parse): threaded bytecode interpreter, basic `&&`+`goto *`,
runtime and static label tables, named/computed goto mix, label-address
comparison; plus negative cases (file-scope &&, no targets, non-pointer
operand, address-after-goto, undefined label). All green across the
interpreter, native -O0/-O1, and the emit-C backend.

Diffstat:
Minclude/cfree/config.h | 8++++----
Mlang/c/parse/cg_adapter.c | 22++++++++++++++++++----
Mlang/c/parse/cg_public_compat.h | 2++
Mlang/c/parse/parse.c | 21++++++++++++---------
Mlang/c/parse/parse_expr.c | 14++++++++++++++
Mlang/c/parse/parse_init.c | 57++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mlang/c/parse/parse_priv.h | 18++++++++++++++++--
Mlang/c/parse/parse_stmt.c | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/interp/engine.c | 11+++++------
Mtest/parse/CORPUS.md | 16++++++++++++++++
Atest/parse/cases/gnu_label_addr_basic.c | 9+++++++++
Atest/parse/cases/gnu_label_addr_basic.expected | 2++
Atest/parse/cases/gnu_label_addr_compare.c | 12++++++++++++
Atest/parse/cases/gnu_label_addr_compare.expected | 2++
Atest/parse/cases/gnu_label_addr_mixed_goto.c | 15+++++++++++++++
Atest/parse/cases/gnu_label_addr_mixed_goto.expected | 2++
Atest/parse/cases/gnu_label_addr_runtime_table.c | 11+++++++++++
Atest/parse/cases/gnu_label_addr_runtime_table.expected | 2++
Atest/parse/cases/gnu_labels_as_values_threaded.c | 36++++++++++++++++++++++++++++++++++++
Atest/parse/cases/gnu_labels_as_values_threaded.expected | 2++
Atest/parse/cases_err/gnu_computed_goto_no_targets.c | 6++++++
Atest/parse/cases_err/gnu_computed_goto_no_targets.errpat | 2++
Atest/parse/cases_err/gnu_computed_goto_non_pointer.c | 6++++++
Atest/parse/cases_err/gnu_computed_goto_non_pointer.errpat | 2++
Atest/parse/cases_err/gnu_label_addr_after_computed_goto.c | 10++++++++++
Atest/parse/cases_err/gnu_label_addr_after_computed_goto.errpat | 2++
Atest/parse/cases_err/gnu_label_addr_file_scope.c | 3+++
Atest/parse/cases_err/gnu_label_addr_file_scope.errpat | 2++
Atest/parse/cases_err/gnu_label_addr_undefined.c | 6++++++
Atest/parse/cases_err/gnu_label_addr_undefined.errpat | 2++
30 files changed, 326 insertions(+), 29 deletions(-)

diff --git a/include/cfree/config.h b/include/cfree/config.h @@ -78,10 +78,10 @@ #define CFREE_INTERP_ENABLED 1 /* Interpreter dispatch: direct-threaded (computed goto) by default. The engine - * additionally requires the host compiler to support labels-as-values, so it - * transparently falls back to a portable switch when built by cfree itself - * (no labels-as-values) or a non-GNU compiler — this flag only expresses the - * preference. Guarded so a build can force the switch with + * additionally requires the compiler to support labels-as-values — GCC, clang, + * and cfree itself qualify, so the self-host build threads too; any other + * compiler transparently falls back to a portable switch. This flag only + * expresses the preference. Guarded so a build can force the switch with * -DCFREE_INTERP_THREADED=0. */ #ifndef CFREE_INTERP_THREADED #define CFREE_INTERP_THREADED 1 diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c @@ -540,6 +540,20 @@ void pcg_addr(Parser* p) { pcg_materialize_lv_to_ptr(p, type_ptr(p->pool, ty)); } +/* Pushes the address of a label as a `void*` rvalue (GNU `&&label`). */ +void pcg_push_label_addr(Parser* p, CGLabel label) { + const Type* vp = type_ptr(p->pool, type_void(p->pool)); + if (pcg_emit_enabled(p)) + cfree_cg_push_label_addr(p->cg, label, pcg_tid(p, vp)); + pcg_push_type(p, vp); +} + +/* Pops the target pointer and emits a computed `goto *expr;`. */ +void pcg_computed_goto(Parser* p, const CGLabel* targets, u32 ntargets) { + if (pcg_emit_enabled(p)) cfree_cg_computed_goto(p->cg, targets, ntargets); + pcg_drop_type(p); +} + /* Store [lv, rv] -> [rv]. The expression-value of an assignment is the * assigned rvalue, so the store sequence must leave a copy of rv on TOS. */ void pcg_store(Parser* p) { @@ -917,15 +931,15 @@ void pcg_inc_dec(Parser* p, BinOp op, int post) { cfree_cg_store(p->cg, r_access, zero_ea); /* ..., lv-base[, idx], old */ pcg_emit_inc_step(p, ty, op, cg_op, step_ty, - step); /* ..., lv-base[, idx], new */ - cfree_cg_store(p->cg, access, ea); /* [] */ + step); /* ..., lv-base[, idx], new */ + cfree_cg_store(p->cg, access, ea); /* [] */ cfree_cg_push_local(p->cg, tmp); cfree_cg_load(p->cg, r_access, zero_ea); /* [old] */ } else { /* Compute new, stash new, store, then re-load new as result. */ pcg_emit_inc_step(p, ty, op, cg_op, step_ty, - step); /* ..., lv-base[, idx], new */ - cfree_cg_dup(p->cg); /* ..., lv-base[, idx], new, new */ + step); /* ..., lv-base[, idx], new */ + cfree_cg_dup(p->cg); /* ..., lv-base[, idx], new, new */ cfree_cg_push_local(p->cg, tmp); cfree_cg_swap(p->cg); cfree_cg_store(p->cg, r_access, diff --git a/lang/c/parse/cg_public_compat.h b/lang/c/parse/cg_public_compat.h @@ -246,6 +246,8 @@ void pcg_push_local_typed(Parser*, FrameSlot, const Type*); void pcg_push_global(Parser*, ObjSymId, const Type*); void pcg_load(Parser*); void pcg_addr(Parser*); +void pcg_push_label_addr(Parser*, CGLabel); +void pcg_computed_goto(Parser*, const CGLabel*, u32); void pcg_store(Parser*); void pcg_deref(Parser*, const Type*); diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c @@ -757,6 +757,15 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { Sym lname = mint_static_local_sym(p, name); int has_init; u32 align_eff; + has_init = accept_punct(p, '='); + /* Complete `T x[] = {...}` before declaring the symbol so the registered + * (CG) type carries the real element count. Backends such as the C target + * derive function-local label-table sizes from the symbol's type, so the + * declared type must already be the completed array. */ + if (has_init && var_ty && var_ty->kind == TY_ARRAY && + var_ty->arr.incomplete) { + var_ty = complete_incomplete_array(p, var_ty); + } memset(&decl_in, 0, sizeof decl_in); decl_in.name = lname; decl_in.type = var_ty; @@ -772,15 +781,6 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { e = scope_define(p, name, SEK_GLOBAL, var_ty); e->v.sym = sym; sym_set_decl(e, did, DS_STATIC, DL_NONE, decl_in.flags, DSTATE_DEFINED); - has_init = accept_punct(p, '='); - if (has_init && var_ty && var_ty->kind == TY_ARRAY && - var_ty->arr.incomplete) { - const Type* completed = complete_incomplete_array(p, var_ty); - if (completed != var_ty) { - var_ty = completed; - e->type = var_ty; - } - } align_eff = (specs->align > decl_in.align) ? specs->align : decl_in.align; define_static_object(p, sym, decl_in.section_id, var_ty, specs->quals, has_init, loc, align_eff); @@ -1165,8 +1165,10 @@ static void parse_function_body(Parser* p, ObjSymId fsym, const Type* fn_ty, scope_push(p); /* parameter scope */ GotoLabel* saved_goto_labels = p->goto_labels; SwitchCtx* saved_switch = p->cur_switch; + u8 saved_computed_goto = p->computed_goto_emitted; p->goto_labels = NULL; p->cur_switch = NULL; + p->computed_goto_emitted = 0; cg_set_loc(p->cg, fname_loc); cg_func_begin(p->cg, &fd); @@ -1209,6 +1211,7 @@ static void parse_function_body(Parser* p, ObjSymId fsym, const Type* fn_ty, } p->goto_labels = saved_goto_labels; p->cur_switch = saved_switch; + p->computed_goto_emitted = saved_computed_goto; cg_func_end(p->cg); scope_pop(p); } diff --git a/lang/c/parse/parse_expr.c b/lang/c/parse/parse_expr.c @@ -2445,6 +2445,20 @@ void parse_unary(Parser* p) { cg_unop(p->cg, UO_BNOT); return; } + if (is_punct(&t, P_AND)) { + /* GNU labels-as-values: `&&label` yields the label's address as void*. */ + Sym name; + SrcLoc loc; + advance(p); /* '&&' */ + if (p->cur.kind != TOK_IDENT || ident_kw(p, p->cur.v.ident) != KW_NONE) { + perr(p, "expected label name after '&&'"); + } + name = p->cur.v.ident; + loc = p->cur.loc; + advance(p); + pcg_push_label_addr(p, take_label_addr(p, name, loc)); + return; + } if (is_punct(&t, '&')) { advance(p); parse_unary(p); diff --git a/lang/c/parse/parse_init.c b/lang/c/parse/parse_init.c @@ -978,13 +978,24 @@ void srl_push(Parser* p, u32 offset, u32 size, ObjSymId target, i64 addend) { p->static_relocs[p->static_relocs_len].size = size; p->static_relocs[p->static_relocs_len].target = target; p->static_relocs[p->static_relocs_len].addend = addend; + p->static_relocs[p->static_relocs_len].label = 0; + p->static_relocs[p->static_relocs_len].is_label = 0; ++p->static_relocs_len; } +/* Append one pending label-address relocation (&&label) to the list. */ +void srl_push_label(Parser* p, u32 offset, u32 size, CGLabel label, + i64 addend) { + srl_push(p, offset, size, 0, addend); + p->static_relocs[p->static_relocs_len - 1u].label = label; + p->static_relocs[p->static_relocs_len - 1u].is_label = 1; +} + typedef enum CStaticConstKind { C_STATIC_CONST_INT, C_STATIC_CONST_NULL_PTR, C_STATIC_CONST_ADDR, + C_STATIC_CONST_LABEL_ADDR, /* &&label in a static pointer initializer */ } CStaticConstKind; typedef struct CStaticConst { @@ -992,6 +1003,7 @@ typedef struct CStaticConst { CConstInt int_value; ObjSymId target; i64 addend; + CGLabel label; /* valid when kind == C_STATIC_CONST_LABEL_ADDR */ } CStaticConst; typedef struct StaticRelocSave { @@ -1254,6 +1266,24 @@ static CStaticConst parse_static_const(Parser* p, const Type* ty, SrcLoc loc) { memset(&r, 0, sizeof r); r.kind = C_STATIC_CONST_INT; if (ty && ty->kind == TY_PTR) { + if (is_punct(&p->cur, P_AND)) { + /* GNU labels-as-values: `&&label` in a static pointer initializer, + * e.g. a direct-threaded dispatch table `static void *tab[] = {...}`. */ + Sym lname; + SrcLoc lloc; + advance(p); /* '&&' */ + if (p->cur.kind != TOK_IDENT || + ident_kw_init(p, p->cur.v.ident) != KW_NONE) { + perr(p, "expected label name after '&&' in static initializer"); + } + lname = p->cur.v.ident; + lloc = tok_loc_init(&p->cur); + advance(p); + r.kind = C_STATIC_CONST_LABEL_ADDR; + r.label = take_label_addr(p, lname, lloc); + r.addend = 0; + return r; + } if (is_punct(&p->cur, '(')) { Tok n = peek1(p); /* Grouping parens around the pointer initializer, e.g. `("str")`, @@ -1567,6 +1597,8 @@ void parse_static_init_at(Parser* p, u8* buf, u32 buflen, u32 offset, cv = parse_static_const(p, ty, cloc); if (cv.kind == C_STATIC_CONST_ADDR) { srl_push(p, offset, sz, cv.target, cv.addend); + } else if (cv.kind == C_STATIC_CONST_LABEL_ADDR) { + srl_push_label(p, offset, sz, cv.label, cv.addend); } else if (cv.kind == C_STATIC_CONST_NULL_PTR) { encode_int_le(buf + offset, sz, 0); } else { @@ -1607,9 +1639,15 @@ static void emit_static_data(Parser* p, const u8* buf, u32 size) { cfree_cg_data_zero(p->cg, best_off - pos); } } - cfree_cg_data_addr(p->cg, p->static_relocs[best].target, - p->static_relocs[best].addend, - p->static_relocs[best].size, 0); + if (p->static_relocs[best].is_label) { + cfree_cg_data_label_addr(p->cg, p->static_relocs[best].label, + p->static_relocs[best].addend, + p->static_relocs[best].size, 0); + } else { + cfree_cg_data_addr(p->cg, p->static_relocs[best].target, + p->static_relocs[best].addend, + p->static_relocs[best].size, 0); + } pos = best_off + p->static_relocs[best].size; ++emitted_relocs; } @@ -1633,6 +1671,7 @@ void define_static_object(Parser* p, ObjSymId sym, ObjSecId section_id, if (align_override > align) align = align_override; u8* buf = NULL; int has_nonzero = 0; + int has_label_reloc = 0; if (has_init) { buf = (u8*)arena_array(p->pool->arena, u8, size ? size : 1u); @@ -1646,11 +1685,23 @@ void define_static_object(Parser* p, ObjSymId sym, ObjSecId section_id, } } if (p->static_relocs_len) has_nonzero = 1; + for (u32 i = 0; i < p->static_relocs_len; ++i) { + if (p->static_relocs[i].is_label) { + has_label_reloc = 1; + break; + } + } } memset(&attrs, 0, sizeof attrs); attrs.section = section_id; attrs.align = align ? align : 1u; + /* Label-address relocations (&&label) are tied to the enclosing function's + * label namespace, so the table must be emitted as function-local static + * data while that function is still open. */ + if (has_label_reloc) { + attrs.flags |= CFREE_CG_DATADEF_FUNCTION_LOCAL; + } if ((quals & Q_CONST) != 0 && has_nonzero) { attrs.flags |= CFREE_CG_DATADEF_READONLY; } diff --git a/lang/c/parse/parse_priv.h b/lang/c/parse/parse_priv.h @@ -108,8 +108,11 @@ struct ParamVLABoundExpr { struct StaticReloc { u32 offset; u32 size; - ObjSymId target; + ObjSymId target; /* symbol target when is_label == 0 */ i64 addend; + CGLabel label; /* label target when is_label != 0 */ + u8 is_label; /* 1 -> label-address reloc (&&label), 0 -> symbol reloc */ + u8 pad[3]; }; struct SymEntry { @@ -184,7 +187,8 @@ struct GotoLabel { Sym name; CGLabel label; u8 placed; - u8 pad[3]; + u8 addr_taken; /* set when &&label is used (labels-as-values) */ + u8 pad[2]; SrcLoc first_use; u32 min_forward_vla_mark; u32 label_vla_mark; @@ -297,6 +301,11 @@ typedef struct Parser { SwitchCtx* cur_switch; GotoLabel* goto_labels; + /* Set once a computed `goto *expr;` has been emitted in the current + * function. After this point taking a new label's address with `&&label` + * is rejected, because each computed goto's target set is finalized when + * it is emitted and a later address-taken label would not be in it. */ + u8 computed_goto_emitted; u8 vla_pending; FrameSlot vla_pending_count_slot; @@ -537,6 +546,7 @@ void define_static_object(Parser* p, ObjSymId sym, ObjSecId section_id, const Type* var_ty, u16 quals, int has_init, SrcLoc loc, u32 align_override); void srl_push(Parser* p, u32 offset, u32 size, ObjSymId target, i64 addend); +void srl_push_label(Parser* p, u32 offset, u32 size, CGLabel label, i64 addend); void encode_int_le(u8* dst, u32 size, i64 v); void push_subobject_lv(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, const Type* elem_ty); @@ -549,6 +559,10 @@ int is_char_kind(const Type* ty); void parse_stmt(Parser* p); void parse_compound_stmt(Parser* p); void parse_static_assert(Parser* p); +GotoLabel* label_get_or_create(Parser* p, Sym name, SrcLoc loc); +/* Records that `name`'s address is taken (GNU labels-as-values) and returns + * its CG label. Used by both `&&label` expressions and static initializers. */ +CGLabel take_label_addr(Parser* p, Sym name, SrcLoc loc); /* parse.c (residual — TU driver) */ void parse_param_list(Parser* p, ParamInfo** infos_out, u16* nparams_out, diff --git a/lang/c/parse/parse_stmt.c b/lang/c/parse/parse_stmt.c @@ -229,7 +229,7 @@ static void parse_do_stmt(Parser* p) { cg_label_place(p->cg, L_end); } -static GotoLabel* label_get_or_create(Parser* p, Sym name, SrcLoc loc) { +GotoLabel* label_get_or_create(Parser* p, Sym name, SrcLoc loc) { GotoLabel* gl; for (gl = p->goto_labels; gl; gl = gl->next) { if (gl->name == name) return gl; @@ -248,10 +248,60 @@ static GotoLabel* label_get_or_create(Parser* p, Sym name, SrcLoc loc) { return gl; } +CGLabel take_label_addr(Parser* p, Sym name, SrcLoc loc) { + GotoLabel* gl; + if (!p->cur_func_name) { + perr(p, "label address ('&&label') is only valid inside a function"); + } + gl = label_get_or_create(p, name, loc); + if (p->computed_goto_emitted && !gl->addr_taken) { + perr(p, + "label address taken after a computed 'goto *'; take all label " + "addresses before the first computed goto in a function"); + } + gl->addr_taken = 1; + return gl->label; +} + +/* Computed goto: `goto *expr;` (GNU labels-as-values). The branch may target + * any label whose address has been taken in this function with `&&label`. */ +static void parse_computed_goto(Parser* p) { + GotoLabel* gl; + CGLabel* targets; + u32 ntargets = 0; + u32 i = 0; + advance(p); /* '*' */ + parse_expr(p); + to_rvalue(p); + if (!type_is_ptr(cg_top_type(p->cg))) { + perr(p, "computed goto requires a pointer operand"); + } + expect_punct(p, ';', "';' after computed goto"); + for (gl = p->goto_labels; gl; gl = gl->next) { + if (gl->addr_taken) ++ntargets; + } + if (ntargets == 0) { + perr(p, + "computed 'goto *' requires at least one label whose address is " + "taken with '&&label'"); + } + targets = arena_array(p->pool->arena, CGLabel, ntargets); + if (!targets) perr(p, "out of memory for computed goto targets"); + for (gl = p->goto_labels; gl; gl = gl->next) { + if (gl->addr_taken) targets[i++] = gl->label; + } + p->computed_goto_emitted = 1; + pcg_computed_goto(p, targets, ntargets); +} + static void parse_goto_stmt(Parser* p) { Sym name; SrcLoc loc; GotoLabel* gl; + if (is_punct(&p->cur, '*')) { + parse_computed_goto(p); + return; + } if (p->cur.kind != TOK_IDENT || ident_kw_stmt(p, p->cur.v.ident) != KW_NONE) { perr(p, "expected label name after 'goto'"); } diff --git a/src/interp/engine.c b/src/interp/engine.c @@ -688,11 +688,10 @@ static u64 ext_call(InterpStack* st, InterpFrame* fr, u64* regs, void* host_fp, * direct-threaded: each InterpInsn caches the &&handler of its opcode and every * handler tail-dispatches straight to the next via `goto *`, giving the branch * predictor a distinct indirect branch per opcode site. This is the default - * (CFREE_INTERP_THREADED in <cfree/config.h>). cfree's own C front end does not - * implement labels-as-values, so the self-host build (__cfree__) and any non-GNU - * compiler transparently fall back to a portable `switch`; the two share one set - * of handler bodies through OP()/NEXT()/GO(). Force the choice with - * -DCFREE_INTERP_THREADED=0|1. */ + * (CFREE_INTERP_THREADED in <cfree/config.h>). GCC, clang, and cfree itself + * (__cfree__) all implement labels-as-values; any other compiler transparently + * falls back to a portable `switch`, sharing one set of handler bodies through + * OP()/NEXT()/GO(). Force the choice with -DCFREE_INTERP_THREADED=0|1. */ #if !defined(CFREE_INTERP_THREADED) /* Belt-and-braces: config.h normally defines this. Default on so a missed * include degrades to threaded-where-supported, never a silent switch. */ @@ -700,7 +699,7 @@ static u64 ext_call(InterpStack* st, InterpFrame* fr, u64* regs, void* host_fp, #endif /* Effective dispatch: requested AND the compiler can compile labels-as-values. */ #if CFREE_INTERP_THREADED && \ - (defined(__GNUC__) || defined(__clang__)) && !defined(__cfree__) + (defined(__GNUC__) || defined(__clang__) || defined(__cfree__)) # define INTERP_DISPATCH_THREADED 1 #else # define INTERP_DISPATCH_THREADED 0 diff --git a/test/parse/CORPUS.md b/test/parse/CORPUS.md @@ -469,6 +469,22 @@ cover compound typedef targets. | `6_8_27_label_on_null_stmt` | ★ | `end: ;` — label applied to a null statement | 42 | | `6_8_28_return_narrow_convert` | ★ | `unsigned char narrow(int x){return x;}` — 298 & 0xff = 42; narrowing on return | 42 | +## GNU labels as values (computed goto) + +| name | tier | description | exit | +|------|------|-------------|------| +| `gnu_labels_as_values_threaded` | ★ | direct-threaded bytecode interpreter: function-local `static void *const tab[] = {&&op,...}` + `goto *tab[*pc++]`; `(3+4)*5` | 35 | +| `gnu_label_addr_basic` | ★ | `void *p = &&done; goto *p;` — take a label address and branch to it | 7 | +| `gnu_label_addr_runtime_table` | ★ | automatic `void *tab[] = {&&even, &&odd}; goto *tab[n&1];` (runtime array of label addresses) | 20 | +| `gnu_label_addr_mixed_goto` | ★ | mixes named `goto` with computed `goto *` to address-taken labels | 11 | +| `gnu_label_addr_compare` | ★ | label addresses are comparable: `if (next == &&op_halt)` where `op_halt` is also a computed-goto target | 7 | + +Negative cases (`cases_err/`): `gnu_label_addr_file_scope` (`&&` outside a function), +`gnu_computed_goto_no_targets` (`goto *p` with no address-taken label), +`gnu_computed_goto_non_pointer` (`goto *` on a non-pointer), `gnu_label_addr_after_computed_goto` +(taking a label address after the first computed goto), `gnu_label_addr_undefined` (`&&` of an +undefined label). + ## §6.8.6.4 Aggregate return values Aggregate-by-value return. Mirrors §6.5.2.2: ≤16B uses `ABI_ARG_DIRECT` diff --git a/test/parse/cases/gnu_label_addr_basic.c b/test/parse/cases/gnu_label_addr_basic.c @@ -0,0 +1,9 @@ +/* GNU labels-as-values: take a label address into a pointer and branch to it + * with a computed goto. */ +int test_main(void) { + void *p = &&done; + goto *p; + return 1; +done: + return 7; +} diff --git a/test/parse/cases/gnu_label_addr_basic.expected b/test/parse/cases/gnu_label_addr_basic.expected @@ -0,0 +1 @@ +7 +\ No newline at end of file diff --git a/test/parse/cases/gnu_label_addr_compare.c b/test/parse/cases/gnu_label_addr_compare.c @@ -0,0 +1,12 @@ +/* Label addresses are comparable values. Here the compared label `op_halt` + * is also a computed-goto target, the idiomatic threaded-interpreter shape. */ +int test_main(void) { + static void *const tab[] = {&&op_a, &&op_halt}; + void *next = tab[1]; + if (next == &&op_halt) return 7; + goto *tab[0]; +op_a: + return 1; +op_halt: + return 9; +} diff --git a/test/parse/cases/gnu_label_addr_compare.expected b/test/parse/cases/gnu_label_addr_compare.expected @@ -0,0 +1 @@ +7 +\ No newline at end of file diff --git a/test/parse/cases/gnu_label_addr_mixed_goto.c b/test/parse/cases/gnu_label_addr_mixed_goto.c @@ -0,0 +1,15 @@ +/* Mix named goto (to `done`) with computed gotos (to address-taken `a`/`b`). + * tab[1]->b: x=10, tab[0]->a: x=11, then named goto done -> 11. */ +int test_main(void) { + static void *const tab[] = {&&a, &&b}; + int x = 0; + goto *tab[1]; +a: + x += 1; + goto done; +b: + x += 10; + goto *tab[0]; +done: + return x; +} diff --git a/test/parse/cases/gnu_label_addr_mixed_goto.expected b/test/parse/cases/gnu_label_addr_mixed_goto.expected @@ -0,0 +1 @@ +11 +\ No newline at end of file diff --git a/test/parse/cases/gnu_label_addr_runtime_table.c b/test/parse/cases/gnu_label_addr_runtime_table.c @@ -0,0 +1,11 @@ +/* GNU labels-as-values: a non-static (automatic) array initialized with label + * addresses, selected at runtime. 5 & 1 == 1 -> `odd` -> 20. */ +int test_main(void) { + void *tab[] = {&&even, &&odd}; + int n = 5; + goto *tab[n & 1]; +even: + return 10; +odd: + return 20; +} diff --git a/test/parse/cases/gnu_label_addr_runtime_table.expected b/test/parse/cases/gnu_label_addr_runtime_table.expected @@ -0,0 +1 @@ +20 +\ No newline at end of file diff --git a/test/parse/cases/gnu_labels_as_values_threaded.c b/test/parse/cases/gnu_labels_as_values_threaded.c @@ -0,0 +1,36 @@ +/* GNU labels-as-values: a direct-threaded (computed-goto) bytecode + * interpreter driven by a function-local static dispatch table built from + * label addresses. Runs `push 3; push 4; add; push 5; mul; halt`, i.e. + * (3 + 4) * 5 = 35. */ +enum { OP_PUSH, OP_ADD, OP_MUL, OP_HALT }; + +int test_main(void) { + static const unsigned char prog[] = { + OP_PUSH, 3, OP_PUSH, 4, OP_ADD, OP_PUSH, 5, OP_MUL, OP_HALT, + }; + static void *const tab[] = { + &&do_push, + &&do_add, + &&do_mul, + &&do_halt, + }; + int stack[16]; + int sp = 0; + const unsigned char *pc = prog; + + goto *tab[*pc++]; + +do_push: + stack[sp++] = *pc++; + goto *tab[*pc++]; +do_add: + stack[sp - 2] = stack[sp - 2] + stack[sp - 1]; + sp--; + goto *tab[*pc++]; +do_mul: + stack[sp - 2] = stack[sp - 2] * stack[sp - 1]; + sp--; + goto *tab[*pc++]; +do_halt: + return stack[sp - 1]; +} diff --git a/test/parse/cases/gnu_labels_as_values_threaded.expected b/test/parse/cases/gnu_labels_as_values_threaded.expected @@ -0,0 +1 @@ +35 +\ No newline at end of file diff --git a/test/parse/cases_err/gnu_computed_goto_no_targets.c b/test/parse/cases_err/gnu_computed_goto_no_targets.c @@ -0,0 +1,6 @@ +/* A computed goto needs at least one address-taken label as a target. */ +int test_main(void) { + void *p = 0; + goto *p; + return 0; +} diff --git a/test/parse/cases_err/gnu_computed_goto_no_targets.errpat b/test/parse/cases_err/gnu_computed_goto_no_targets.errpat @@ -0,0 +1 @@ +at least one label +\ No newline at end of file diff --git a/test/parse/cases_err/gnu_computed_goto_non_pointer.c b/test/parse/cases_err/gnu_computed_goto_non_pointer.c @@ -0,0 +1,6 @@ +/* The computed-goto operand must be a pointer. */ +int test_main(void) { + int x = 0; + goto *x; + return 0; +} diff --git a/test/parse/cases_err/gnu_computed_goto_non_pointer.errpat b/test/parse/cases_err/gnu_computed_goto_non_pointer.errpat @@ -0,0 +1 @@ +pointer operand +\ No newline at end of file diff --git a/test/parse/cases_err/gnu_label_addr_after_computed_goto.c b/test/parse/cases_err/gnu_label_addr_after_computed_goto.c @@ -0,0 +1,10 @@ +/* A label's address may not be taken after a computed goto, since each + * computed goto's target set is finalized when it is emitted. */ +int test_main(void) { + void *p = &&a; + goto *p; +a: + p = &&b; +b: + return 0; +} diff --git a/test/parse/cases_err/gnu_label_addr_after_computed_goto.errpat b/test/parse/cases_err/gnu_label_addr_after_computed_goto.errpat @@ -0,0 +1 @@ +after a computed +\ No newline at end of file diff --git a/test/parse/cases_err/gnu_label_addr_file_scope.c b/test/parse/cases_err/gnu_label_addr_file_scope.c @@ -0,0 +1,3 @@ +/* Taking a label address is only meaningful inside a function. */ +void *g = &&nowhere; +int test_main(void) { return 0; } diff --git a/test/parse/cases_err/gnu_label_addr_file_scope.errpat b/test/parse/cases_err/gnu_label_addr_file_scope.errpat @@ -0,0 +1 @@ +only valid inside a function +\ No newline at end of file diff --git a/test/parse/cases_err/gnu_label_addr_undefined.c b/test/parse/cases_err/gnu_label_addr_undefined.c @@ -0,0 +1,6 @@ +/* Address taken of a label that is never defined. */ +int test_main(void) { + void *p = &&nope; + goto *p; + return 0; +} diff --git a/test/parse/cases_err/gnu_label_addr_undefined.errpat b/test/parse/cases_err/gnu_label_addr_undefined.errpat @@ -0,0 +1 @@ +undefined label +\ No newline at end of file