boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

commit c788ee58d6d3019c89d929467fa7ebd8d1fad4e8
parent 21fd5f91cb8798e4a79b49ef028e8a307bf84e87
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 22 Jul 2026 17:20:10 -0700

Add Kit0 bootstrap compiler path

Diffstat:
MM1pp/M1pp.P1 | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MM1pp/M1pp.c | 33+++++++++++++++++++++++++++++++++
MMakefile | 19++++++++++++++++++-
MP1/P1-aarch64.M1pp | 7+++++++
MP1/P1-amd64.M1pp | 28++++++++++++++++++++++++++++
MP1/P1-riscv32.M1pp | 6++++++
MP1/P1-riscv64.M1pp | 6++++++
MP1/P1.M1pp | 8++++++++
Aboot/kit0.sh | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcc/cc.scm | 780+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mdocs/CC.md | 61+++++++++++++++++++++++++++++++++++++++++++++----------------
Mdocs/HEX2pp.md | 8++++++++
Adocs/KIT0.md | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/M1PP.md | 14++++++++++++++
Mdocs/P1.md | 14++++++++------
Adocs/TODO.md | 30++++++++++++++++++++++++++++++
Mhex2pp/hex2pp.P1 | 26+++++++++++++-------------
Mhex2pp/hex2pp.c | 2+-
Mtcc/libc/aarch64/sys_stubs.S | 13++++++++++---
Atests/M1pp/032-reset-output.M1pp | 10++++++++++
Atests/M1pp/032-reset-output.expected | 3+++
Atests/cc-util/015-lib-static-namespace.expected | 5+++++
Atests/cc-util/015-lib-static-namespace.expected-exit | 1+
Atests/cc-util/015-lib-static-namespace.scm | 18++++++++++++++++++
Mtests/cc/320-vararg-cap.c | 11++++++-----
Mtests/cc/320-vararg-cap.expected-exit | 2+-
Atests/cc/346-c11-decl-align.c | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/cc/346-c11-decl-align.expected-exit | 1+
Atests/cc/347-aggregate-indirect-arg.c | 33+++++++++++++++++++++++++++++++++
Atests/cc/347-aggregate-indirect-arg.expected-exit | 1+
Atests/cc/348-unsigned-div-rem.c | 14++++++++++++++
Atests/cc/348-unsigned-div-rem.expected-exit | 1+
Atests/cc/349-aggregate-indirect-lvalue-arg.c | 30++++++++++++++++++++++++++++++
Atests/cc/349-aggregate-indirect-lvalue-arg.expected-exit | 1+
Atests/cc/350-large-aggregate-flow.c | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/cc/350-large-aggregate-flow.expected-exit | 1+
Atests/cc/351-address-of-member-local.c | 28++++++++++++++++++++++++++++
Atests/cc/351-address-of-member-local.expected-exit | 1+
Atests/cc/352-aggregate-compound-call.c | 30++++++++++++++++++++++++++++++
Atests/cc/352-aggregate-compound-call.expected-exit | 1+
Atests/cc/353-static-local-scope-collision.c | 25+++++++++++++++++++++++++
Atests/cc/353-static-local-scope-collision.expected-exit | 1+
42 files changed, 1584 insertions(+), 185 deletions(-)

diff --git a/M1pp/M1pp.P1 b/M1pp/M1pp.P1 @@ -13,7 +13,7 @@ ## stream and walks it token-by-token, dispatching to ## define_macro at line-start %macro, emit_newline / ## emit_token for pass-through, expand_builtin_call for -## !@%$, %select, %str, %local, and expand_call +## !@%$, %select, %str, %local, %reset-output, and expand_call ## for user macros. Macro expansions and %select push ## fresh streams onto streams[]; popping rewinds the ## expansion pool. @@ -1019,7 +1019,7 @@ DEFINE OFF_macro_body_is_local_label 80632A1F00000000 ## B — second-byte dispatch: within the c0=='%' branch, a single ## c1-byte switch picks at most one directive/builtin to actually ## compare against (e.g. c1='s' selects %struct/%select/%str only). -## A user macro %FOO with c1 outside {m,s,e,f,b,l} skips every +## A user macro %FOO with c1 outside {m,s,e,f,b,l,r} skips every ## tok_eq_const and goes straight to find_macro. :proc_loop # s = current_stream(); if (s == 0) done @@ -1132,6 +1132,9 @@ DEFINE OFF_macro_body_is_local_label 80632A1F00000000 li_a0 %108 %0 la_br &proc_c1_l beq_a3,a0 + li_a0 %114 %0 + la_br &proc_c1_r + beq_a3,a0 # No directive/builtin candidate — try user macro. la_br &proc_check_macro b @@ -1298,6 +1301,27 @@ DEFINE OFF_macro_body_is_local_label 80632A1F00000000 la_br &proc_do_builtin b +:proc_c1_r + # %reset-output BASE (no paren). Discard everything emitted by the + # macro/runtime prelude and seed a disjoint macro-hygiene id range. + ld_a0,sp,8 + la_a1 &const_reset_output + li_a2 %13 %0 + la_br &tok_eq_const + call + la_br &proc_check_macro + beqz_a0 + ld_a0,sp,0 + ld_a1,sp,8 + la_br &proc_save_pos_and_ls + call + ld_a0,sp,0 + ld_a0,a0,8 + la_br &reset_output + call + la_br &proc_restore_and_loop + b + :proc_handle_newline ld_a0,sp,0 ld_t0,sp,8 @@ -1516,6 +1540,68 @@ DEFINE OFF_macro_body_is_local_label 80632A1F00000000 :pop_frame_done eret +## reset_output(a0 = stream_end): consume a line-start `%reset-output BASE`, +## clear the buffered output, and install BASE as next_expansion_id. The next +## macro expansion receives BASE+1. This lets partitioned builds replay a +## shared macro prelude without duplicating its runtime output or hygiene ids. +:reset_output + enter_16 + st_a0,sp,0 + + # Directive is valid only at line start. + la_t0 &proc_line_start + ld_t1,t0,0 + la_br &err_bad_directive + beqz_t1 + + # Advance to BASE and require a WORD token within the stream. + la_t0 &proc_pos + ld_t1,t0,0 + addi_t1,t1,32 + st_t1,t0,0 + ld_a0,sp,0 + la_br &err_bad_directive + beq_t1,a0 + ld_t2,t1,0 + la_br &err_bad_directive + bnez_t2 + + # parse_int_token(BASE), accepting 0..INT_MAX-1. + mov_a0,t1 + la_br &parse_int_token + call + la_br &err_bad_directive + bltz_a0 + li_t0 %2147483646 %0 + la_br &err_bad_directive + blt_t0,a0 + la_t0 &next_expansion_id + st_a0,t0,0 + + # Require and consume the newline immediately following BASE. + la_t0 &proc_pos + ld_t1,t0,0 + addi_t1,t1,32 + st_t1,t0,0 + ld_a0,sp,0 + la_br &err_bad_directive + beq_t1,a0 + ld_t2,t1,0 + li_t0 TOK_NEWLINE + la_br &err_bad_directive + bne_t2,t0 + addi_t1,t1,32 + la_t0 &proc_pos + st_t1,t0,0 + + # Drop the replayed prelude's output and reset token spacing. + li_t0 %0 %0 + la_t1 &output_used + st_t0,t1,0 + la_t1 &output_need_space + st_t0,t1,0 + eret + ## --- %macro storage: parse header + body into macros[] / macro_body_tokens -- ## Called at proc_pos == line-start `%macro`. Leaves proc_pos past the %endm ## line with proc_line_start = 1. Uses BSS scratch (def_m_ptr, def_param_ptr, @@ -6557,6 +6643,7 @@ DEFINE OFF_macro_body_is_local_label 80632A1F00000000 :const_endframe "%endframe" :const_local "%local" :const_bytes "%bytes" +:const_reset_output "%reset-output" ## Suffix appended to the frame name when looking up <frame>_FRAME.<field>. :const_frame_suffix "_FRAME." diff --git a/M1pp/M1pp.c b/M1pp/M1pp.c @@ -10,6 +10,8 @@ * * %struct NAME { f1 f2 ... } fixed-layout 8-byte-field aggregate * %enum NAME { l1 l2 ... } incrementing integer constants + * %reset-output BASE discard output so far and seed the + * macro-hygiene expansion id * * %NAME(x, y) function-like macro call * ## token pasting inside macro bodies @@ -1913,6 +1915,30 @@ static int pop_frame(struct Stream *s) return 1; } +static int reset_output(struct Stream *s) +{ + long long base; + + if (!s->line_start) { + return fail("bad reset-output directive"); + } + s->pos++; + if (s->pos >= s->end || s->pos->kind != TOK_WORD || + !parse_int_token(s->pos, &base) || base < 0 || base > 2147483646LL) { + return fail("bad reset-output directive"); + } + s->pos++; + if (s->pos >= s->end || s->pos->kind != TOK_NEWLINE) { + return fail("bad reset-output directive"); + } + s->pos++; + s->line_start = 1; + output_used = 0; + output_need_space = 0; + next_expansion_id = (int)base; + return 1; +} + static int process_tokens(void) { if (!push_stream_span((struct TokenSpan){source_tokens, source_tokens + source_count}, -1)) { @@ -2016,6 +2042,13 @@ static int process_tokens(void) handled = 1; } break; + case 'r': + if (len == 13 && + memcmp(p + 2, "eset-output", 11) == 0) { + if (!reset_output(s)) return 0; + handled = 1; + } + break; } if (handled) { diff --git a/Makefile b/Makefile @@ -68,7 +68,7 @@ OUT_DIR := build/$(ARCH)/$(DRIVER) .SUFFIXES: -.PHONY: all help clean cloc src package release +.PHONY: all help clean cloc src package release kit0 # ── Top-level targets ──────────────────────────────────────────────────── @@ -98,10 +98,27 @@ help: @echo ' assemble final static toolchain' @echo ' make build/<arch>/<driver>/bootN/<file> any single artifact' @echo ' make test SUITE=<suite> test suite (NAMES=<filter> optional)' + @echo ' make kit0 KIT0_PREP=<kit-prep-dir> cc.scm + P1 build of Kit0 for ARCH' @echo ' make image per-arch tests container image' @echo ' make cloc line counts for bootstrap sources' @echo ' make clean rm -rf build/' +KIT0_ARCHES := aarch64 amd64 riscv64 +KIT0_PROFILE_aarch64 := aa64 +KIT0_PROFILE_amd64 := x64 +KIT0_PROFILE_riscv64 := rv64 +KIT0_PROFILE ?= $(KIT0_PROFILE_$(ARCH)) +KIT_DIR ?= ../../kit +KIT0_PREP ?= $(KIT_DIR)/build/bootstrap-$(KIT0_PROFILE)/kit0-prep + +ifeq ($(filter $(ARCH),$(KIT0_ARCHES)),) +kit0: + @echo "kit0 supports ARCH in {$(KIT0_ARCHES)} (got '$(ARCH)')" >&2; exit 2 +else +kit0: build/$(ARCH)/$(DRIVER)/boot2/.stamp build/$(ARCH)/src/.stamp + KIT0_PREP='$(KIT0_PREP)' DRIVER='$(DRIVER)' boot/kit0.sh $(ARCH) +endif + clean: rm -rf build/ diff --git a/P1/P1-aarch64.M1pp b/P1/P1-aarch64.M1pp @@ -412,6 +412,9 @@ %macro aa64_rrr_DIV(rd, ra, rb) %aa64_rrr(0x9AC00C00, rd, ra, rb) %endm +%macro aa64_rrr_UDIV(rd, ra, rb) +%aa64_rrr(0x9AC00800, rd, ra, rb) +%endm %macro aa64_rrr_MUL(rd, ra, rb) %((| 0x9B000000 (<< %aa64_reg(rb) 16) (<< 31 10) (<< %aa64_reg(ra) 5) %aa64_reg(rd))) %endm @@ -419,6 +422,10 @@ %((| 0x9AC00C00 (<< %aa64_reg(rb) 16) (<< %aa64_reg(ra) 5) %aa64_reg(scratch))) %((| 0x9B008000 (<< %aa64_reg(rb) 16) (<< %aa64_reg(ra) 10) (<< %aa64_reg(scratch) 5) %aa64_reg(rd))) %endm +%macro aa64_rrr_UREM(rd, ra, rb) +%((| 0x9AC00800 (<< %aa64_reg(rb) 16) (<< %aa64_reg(ra) 5) %aa64_reg(scratch))) +%((| 0x9B008000 (<< %aa64_reg(rb) 16) (<< %aa64_reg(ra) 10) (<< %aa64_reg(scratch) 5) %aa64_reg(rd))) +%endm %macro aa64_rrr_op(op, rd, ra, rb) %aa64_rrr_##op(rd, ra, rb) diff --git a/P1/P1-amd64.M1pp b/P1/P1-amd64.M1pp @@ -327,6 +327,13 @@ %amd_modrm_ext_r(7, src) %endm +# div src -- 48 F7 /6. +%macro amd_div_r(src) +%amd_rex_wb(src) +!(0xF7) +%amd_modrm_ext_r(6, src) +%endm + # cqo -- 48 99 (sign-extend rax into rdx:rax). %macro amd_cqo() !(0x48) @@ -529,6 +536,27 @@ %amd_mov_rr(rd, rax) %endm +%macro amd_rrr_UDIV(rd, ra, rb) +%amd_mov_rr(rbp, rdx) +%amd_mov_rr(scratch, rb) +%amd_mov_rr(rax, ra) +%amd_alu_rr(0x31, rdx, rdx) +%amd_div_r(scratch) +%amd_mov_rr(rdx, rbp) +%amd_mov_rr(rd, rax) +%endm + +%macro amd_rrr_UREM(rd, ra, rb) +%amd_mov_rr(rbp, rdx) +%amd_mov_rr(scratch, rb) +%amd_mov_rr(rax, ra) +%amd_alu_rr(0x31, rdx, rdx) +%amd_div_r(scratch) +%amd_mov_rr(rax, rdx) +%amd_mov_rr(rdx, rbp) +%amd_mov_rr(rd, rax) +%endm + # SHL / SHR / SAR with reg count. x86 reads the count from CL only, so # staging goes through rcx — which IS P1 a3. Save rcx to rbp for the # duration. diff --git a/P1/P1-riscv32.M1pp b/P1/P1-riscv32.M1pp @@ -385,9 +385,15 @@ %macro rv_rrr_DIV(rd, ra, rb) %rv_r_type(0x02004033, rd, ra, rb) %endm +%macro rv_rrr_UDIV(rd, ra, rb) +%rv_r_type(0x02005033, rd, ra, rb) +%endm %macro rv_rrr_REM(rd, ra, rb) %rv_r_type(0x02006033, rd, ra, rb) %endm +%macro rv_rrr_UREM(rd, ra, rb) +%rv_r_type(0x02007033, rd, ra, rb) +%endm %macro rv_rrr_op(op, rd, ra, rb) %rv_rrr_##op(rd, ra, rb) diff --git a/P1/P1-riscv64.M1pp b/P1/P1-riscv64.M1pp @@ -403,9 +403,15 @@ %macro rv_rrr_DIV(rd, ra, rb) %rv_r_type(0x02004033, rd, ra, rb) %endm +%macro rv_rrr_UDIV(rd, ra, rb) +%rv_r_type(0x02005033, rd, ra, rb) +%endm %macro rv_rrr_REM(rd, ra, rb) %rv_r_type(0x02006033, rd, ra, rb) %endm +%macro rv_rrr_UREM(rd, ra, rb) +%rv_r_type(0x02007033, rd, ra, rb) +%endm %macro rv_rrr_op(op, rd, ra, rb) %rv_rrr_##op(rd, ra, rb) diff --git a/P1/P1.M1pp b/P1/P1.M1pp @@ -66,10 +66,18 @@ target %p1_rrr(DIV, rd, ra, rb) %endm +%macro udiv(rd, ra, rb) +%p1_rrr(UDIV, rd, ra, rb) +%endm + %macro rem(rd, ra, rb) %p1_rrr(REM, rd, ra, rb) %endm +%macro urem(rd, ra, rb) +%p1_rrr(UREM, rd, ra, rb) +%endm + # ---- Immediate arithmetic ------------------------------------------------- %macro addi(rd, ra, imm) diff --git a/boot/kit0.sh b/boot/kit0.sh @@ -0,0 +1,69 @@ +#!/bin/sh +## Compile a prepared Kit0 translation-unit graph through cc.scm, then use the +## ordinary per-architecture P1/M1pp/hex2pp boot path to produce one executable. +set -eu + +. boot/lib-arch.sh +bootlib_init kit0 "${1:-aarch64}" + +case "$ARCH" in + aarch64) kit_arch_flag=KIT_ARCH_AA64_ENABLED ;; + amd64) kit_arch_flag=KIT_ARCH_X64_ENABLED ;; + riscv64) kit_arch_flag=KIT_ARCH_RV64_ENABLED ;; + *) + echo "kit0: supported architectures are aarch64, amd64, and riscv64" >&2 + exit 2 + ;; +esac + +: "${KIT0_PREP:?set KIT0_PREP to the matching Kit bootstrap profile kit0-prep directory}" +test -f "$KIT0_PREP/manifest.tsv" +test -f "$KIT0_PREP/m1pp-partitions.tsv" +test -f "$KIT0_PREP/kit0-run.scm" +test -f "$KIT0_PREP/resolved-config.h" +test -d "$KIT0_PREP/tu" +grep -Eq "^#define[[:space:]]+${kit_arch_flag}[[:space:]]+1$" \ + "$KIT0_PREP/resolved-config.h" || { + echo "kit0: $KIT0_PREP does not enable $kit_arch_flag for ARCH=$ARCH" >&2 + exit 2 +} + +driver_init empty +require_src + +BOOT1=build/$ARCH/$DRIVER/boot1 +BOOT2=build/$ARCH/$DRIVER/boot2 +SRC=build/$ARCH/src +OUT=build/$ARCH/$DRIVER/kit0 +STAGE=build/$ARCH/.stage/$DRIVER/kit0 + +require_prev "$BOOT1" M1pp hex2pp +require_prev "$BOOT2" catm scheme1 + +. boot/lib-runscm.sh +runscm_init "$STAGE" "$OUT" +runscm_scheme1 "$BOOT2/scheme1" +runscm_prelude "$SRC/src/scheme1/prelude.scm" +runscm_runscm "$KIT0_PREP/kit0-run.scm" + +runscm_input catm "$BOOT2/catm" +runscm_input M1pp "$BOOT1/M1pp" +runscm_input hex2pp "$BOOT1/hex2pp" +runscm_input_from_src scheme1/prelude.scm +runscm_input_from_src cc/cc.scm +runscm_input_from_src cc/main.scm +runscm_input_from_src "P1/P1-$ARCH.M1pp" backend.M1pp +runscm_input_from_src P1/P1.M1pp frontend.M1pp +runscm_input_from_src P1/P1pp.P1pp libp1pp.P1pp +runscm_input_from_src P1/entry-libc.P1pp +runscm_input_from_src P1/elf-end.P1pp +runscm_input_from_src stage0-posix/ELF.hex2 +runscm_input_from_src libc/libc.flat.c +runscm_input_tree kit0 "$KIT0_PREP/tu" + +runscm_export kit0 libc.P1pp +# cc.scm processes the deliberately separate Kit translation units in one +# deterministic seed run. On an emulated target this can take several hours. +runscm_run "${KIT0_TIMEOUT:-28800}" + +echo "[$BOOT_TAG] OK -> $OUT/kit0 ($(wc -c <"$OUT/kit0") bytes)" diff --git a/cc/cc.scm b/cc/cc.scm @@ -550,8 +550,8 @@ ;; across functions (reset, not re-allocated). ;; -------------------------------------------------------------------- -;; Tuning constants — total fixed pre-allocation ≈ 12.27 MiB on a -;; managed heap. Bump these when a workload overflows; the buf-overflow +;; Tuning constants — total fixed pre-allocation is about 12.58 MiB on +;; 64-bit targets and 12.33 MiB on RV32. Bump these when a workload overflows; the buf-overflow ;; die() reports off/len/cap so misses are easy to diagnose. ;; ;; Each cap is a power of two. scheme1's bv_capacity_for rounds the @@ -560,7 +560,8 @@ (define %BUF-CAP-TEXT 8388608) ; 8 MiB: .text + entry stub (define %BUF-CAP-DATA 2097152) ; 2 MiB: .data (strings, globals) (define %BUF-CAP-BSS 2097152) ; 2 MiB: .bss -(define %BUF-CAP-FN 262144) ; 256 KiB: per-fn body asm +(define %BUF-CAP-FN (if (= (target-word-bytes) 4) 262144 524288)) + ; 256/512 KiB: per-fn body asm (define %BUF-CAP-PROLOGUE 16384) ; 16 KiB: per-fn prologue (define %BUF-CAP-DEFAULT 65536) ; 64 KiB: make-buf fallback @@ -902,13 +903,17 @@ ;; fn-ctx — current-function context inside the parser. ;; -------------------------------------------------------------------- (define-record-type fn-ctx - (%fn-ctx name return-type params variadic? labels) + (%fn-ctx name return-type params variadic? labels static-counter) fn-ctx? (name fn-ctx-name) (return-type fn-ctx-return-type) (params fn-ctx-params) (variadic? fn-ctx-variadic?) - (labels fn-ctx-labels fn-ctx-labels-set!)) + (labels fn-ctx-labels fn-ctx-labels-set!) + ;; Monotonic declaration identity for block-scope static objects. The C + ;; identifier alone is not unique: separate lexical scopes in one function + ;; may each declare `static ... order[]` and must emit distinct labels. + (static-counter fn-ctx-static-counter fn-ctx-static-counter-set!)) ;; -------------------------------------------------------------------- ;; world — cross-decl persistent parser/cg state. The same world record @@ -1042,7 +1047,7 @@ ("extern" . extern) ("typedef" . typedef) ;; qualifiers (parsed and discarded by parse) ("const" . const) ("volatile" . volatile) ("restrict" . restrict) - ("inline" . inline) + ("inline" . inline) ("_Noreturn" . _Noreturn) ;; GNU attribute spec — parsed and discarded; see skip-gnu-attribute! ("__attribute__" . __attribute__) ;; type specifiers @@ -1064,7 +1069,8 @@ ;; reserved-and-rejected (KW so diagnostics are crisp) ("_Generic" . _Generic) ("_Atomic" . _Atomic) ("_Thread_local" . _Thread_local) - ("_Alignof" . _Alignof) ("_Alignas" . _Alignas) + ("_Alignof" . _Alignof) ("__alignof" . _Alignof) + ("__alignof__" . _Alignof) ("_Alignas" . _Alignas) ("_Static_assert" . _Static_assert) ("_Complex" . _Complex) ("_Imaginary" . _Imaginary))) @@ -3095,11 +3101,11 @@ (if (< (cg-max-outgoing cg) n) (cg-max-outgoing-set! cg n) 0)) (define (%cg-slot-expr cg logical-off) - (let ((nm (%cg-fn-get cg '%fn-name))) + (let ((nm (%cg-fn-get cg '%fn-label))) (bv-cat (list "(+ %" nm "__SO " (%n logical-off) ")")))) -(define (%cg-mangle-global name-bv) - (bytevector-append "cc__" name-bv)) +(define (%cg-mangle-global cg name-bv) + (bytevector-append (cg-str-prefix cg) "cc__" name-bv)) ;; Label for a sym at the M1 layer. ;; @@ -3115,9 +3121,9 @@ ;; Block-scope statics already mangle their sym-name to ;; `<fnname>__<n>` at parse time (see line ~5125); the cc__ prefix ;; here just nests another layer of namespacing on top of that. -(define (%cg-sym-label sm) +(define (%cg-sym-label cg sm) (cond - ((eq? (sym-storage sm) 'static) (%cg-mangle-global (sym-name sm))) + ((eq? (sym-storage sm) 'static) (%cg-mangle-global cg (sym-name sm))) (else (sym-name sm)))) (define (%cg-reg->bv r) (bytevector-append (symbol->string r))) @@ -3463,15 +3469,16 @@ (define (cg-fn-begin cg name params return-type) (cg-fn-begin/v cg name params return-type #f)) -;; Variadic-aware variant. variadic? = #t reserves 16 contiguous target-word -;; slots covering incoming arg indices 0..15, populating each from the +;; Variadic-aware variant. variadic? = #t reserves a fixed contiguous +;; target-word window, populating each slot from the ;; appropriate source: a-register for idx 0..3, LDARG slot (idx-4) for -;; idx 4..15. va_start computes the address of the slot at index = +;; later indices. va_start computes the address of the slot at index = ;; named-arg count, so va_arg walks linearly through the rest. -;; Indices 4..15 may be garbage when the caller passed fewer args; user +;; Indices beyond the actual arguments may be garbage; user ;; code stops walking based on a count or sentinel before those slots -;; are read. Limit of 15 variadic args (after named) is enough for -;; tcc.c's logging shapes; bump VARARG_WINDOW if you need more. +;; are read. Kit's widest diagnostic calls require 20 total arguments. +(define %CG-VARARG-WINDOW 32) + (define (cg-fn-begin/v cg name params return-type variadic?) (buf-reset! (cg-fn-buf cg)) (buf-reset! (cg-prologue-buf cg)) @@ -3487,6 +3494,9 @@ (cg-max-outgoing-set! cg 0) (cg-fn-meta-set! cg '()) (%cg-fn-set! cg '%fn-name name) + (let ((sm (%hash-ref (car (world-scope (cg-world cg))) name))) + (%cg-fn-set! cg '%fn-label + (cond (sm (%cg-sym-label cg sm)) (else name)))) (%cg-fn-set! cg '%fn-ret-type return-type) (%cg-fn-set! cg '%indirect-slots '()) (%cg-fn-set! cg '%fn-variadic? variadic?) @@ -3524,13 +3534,18 @@ (bv-cat (list "%st(a0, sp, " (%cg-slot-expr cg ss) ")\n"))))) (else (%cg-fn-set! cg '%fn-sret-slot #f)))) - ;; Variadic save area is capped at 16 incoming-arg slots; reject + ;; Reject definitions whose named parameters leave no variadic slot. ;; variadic definitions whose named-arg count would already fill or ;; exceed it (no room left for variadic reads). - (cond - ((and variadic? (> (length params) 16)) - (die #f "cg-fn-begin: variadic function exceeds 16-arg save-area cap" - name (length params)))) + (let ((named-slots + (let count ((xs params) (n 0)) + (cond ((null? xs) n) + (else (count (cdr xs) + (+ n (%cg-param-reg-count (cdar xs))))))))) + (cond + ((and variadic? (>= named-slots %CG-VARARG-WINDOW)) + (die #f "cg-fn-begin: variadic function fills save-area" + name named-slots %CG-VARARG-WINDOW)))) ;; With sret, explicit arg i lives at ABI position (i+1): args 0..2 ;; in a1..a3, args 3+ in slot (i-3). (let* ((sret-shift (if (%cg-fn-get cg '%fn-sret?) 1 0)) @@ -3553,7 +3568,7 @@ (variadic? (let pad ((i idx) (vfirst #f) (fs first-slot)) (cond - ((>= i 16) + ((>= i %CG-VARARG-WINDOW) (%cg-fn-set! cg '%fn-vararg-first-slot (or vfirst fs)) (reverse out)) (else @@ -3565,16 +3580,17 @@ (let* ((p (car ps)) (nm (car p)) (ty (cdr p)) - ;; Two-word aggregates ride two consecutive arg positions; - ;; wider aggregates - ;; aggregates would normally pass by reference — not - ;; supported here yet. + ;; Aggregates wider than two words ride one ABI slot containing + ;; a pointer to a caller-owned copy (AAPCS64 / SysV class + ;; MEMORY). Smaller aggregates ride one or two words inline. (n (%cg-param-reg-count ty)) - (sz (cond ((or (%cg-param-aggregate? ty) + (sz (cond ((%cg-param-indirect? ty) %CC-WORD-BYTES) + ((or (%cg-param-aggregate? ty) (%ctype-wide-int? ty)) (align-up (ctype-size ty) %CC-WORD-BYTES)) (else %CC-WORD-BYTES))) - (al (cond ((or (%cg-param-aggregate? ty) + (al (cond ((%cg-param-indirect? ty) %CC-WORD-BYTES) + ((or (%cg-param-aggregate? ty) (%ctype-wide-int? ty)) (max %CC-WORD-BYTES (ctype-align ty))) (else %CC-WORD-BYTES))) @@ -3586,6 +3602,7 @@ (spill (+ idx sret-shift i) (+ off (* i %CC-WORD-BYTES))) (chunk (+ i 1))))) + (cond ((%cg-param-indirect? ty) (%cg-mark-indirect! cg off))) (walk (cdr ps) (+ idx n) (cons (cons nm psym) out) (or first-slot off)))))))) @@ -3597,8 +3614,7 @@ ((%cg-param-aggregate? ty) (let ((sz (ctype-size ty))) (cond - ((> sz %CC-PAIR-BYTES) - (die #f "cg: aggregate arg/param wider than two words not supported" sz)) + ((> sz %CC-PAIR-BYTES) 1) ((> sz %CC-WORD-BYTES) 2) (else 1)))) (else 1))) @@ -3607,6 +3623,10 @@ (let ((k (ctype-kind ty))) (or (eq? k 'struct) (eq? k 'union)))) +(define (%cg-param-indirect? ty) + (and (%cg-param-aggregate? ty) + (> (ctype-size ty) %CC-PAIR-BYTES))) + (define (cg-fn-end cg) ;; Drain prologue-buf and fn-buf directly into cg-text via buf-drain! ;; (memcpy, no allocation). Header/footer pieces go through buf-push! @@ -3623,9 +3643,7 @@ ;; emits the same cc__-mangled label that callers reference. ;; The sym was bound by parse-fn-body before the body parse, ;; so it's in the top scope frame at this point. - (fn-sym (%hash-ref (car (world-scope (cg-world cg))) name)) - (mangled (cond (fn-sym (%cg-sym-label fn-sym)) - (else name))) + (mangled (%cg-fn-get cg '%fn-label)) (tb (cg-text cg))) ;; Now that the body is fully emitted, leave fn dispatch so any ;; trailing emits in this function (including the ret-block below) @@ -3633,7 +3651,7 @@ (cg-in-fn?-set! cg #f) ;; staging-size macro (buf-push! tb "%macro ") - (buf-push! tb name) + (buf-push! tb mangled) (buf-push! tb "__SO()\n") (buf-push! tb (%n staging-bytes)) (buf-push! tb "\n%endm\n") @@ -3718,7 +3736,11 @@ ;; bytes past the new offset become garbage that the next buf-push! ;; will overwrite (buf-push! always copies into [offset, offset+len)). ;; frame-hi and max-outgoing are also restored so cg-alloc-slot calls -;; inside the rewound region don't leak frame bytes. +;; inside the rewound region don't leak frame bytes. The label counter and +;; indirect-slot set are part of the same transaction: parsing `sizeof *p` +;; mints an indirect temporary, and leaving that tag behind after frame-hi is +;; rewound would make the next ordinary local at the reused offset behave as a +;; pointer-to-storage slot. ;; -------------------------------------------------------------------- (define (cg-snapshot cg) (cond @@ -3727,13 +3749,17 @@ (list (cg-vstack cg) (buf-offset (cg-fn-buf cg)) (cg-frame-hi cg) - (cg-max-outgoing cg))) + (cg-max-outgoing cg) + (cg-label-ctr cg) + (or (%cg-fn-get cg '%indirect-slots) '()))) (define (cg-rewind cg tag) (cg-vstack-set! cg (car tag)) (buf-offset-set! (cg-fn-buf cg) (cadr tag)) (cg-frame-hi-set! cg (caddr tag)) - (cg-max-outgoing-set! cg (cadddr tag))) + (cg-max-outgoing-set! cg (cadddr tag)) + (cg-label-ctr-set! cg (car (cddddr tag))) + (%cg-fn-set! cg '%indirect-slots (cadr (cddddr tag)))) ;; Duplicate the top vstack entry. For lvals this is safe — the slot ;; (or label, or indirect-marked frame) backing the lval keeps existing @@ -3759,13 +3785,13 @@ (define (cg-push-sym cg sm) (pmatch sm (($ sym? (kind fn) (type ,ty)) - (cg-push cg (%opnd 'global ty (%cg-sym-label sm) #f))) + (cg-push cg (%opnd 'global ty (%cg-sym-label cg sm) #f))) (($ sym? (kind enum-const) (type ,ty) (slot ,v)) (cg-push cg (%opnd 'imm ty v #f))) (($ sym? (kind var) (storage extern) (type ,ty)) - (cg-push cg (%opnd 'global ty (%cg-sym-label sm) #t))) + (cg-push cg (%opnd 'global ty (%cg-sym-label cg sm) #t))) (($ sym? (kind var) (storage static) (type ,ty)) - (cg-push cg (%opnd 'global ty (%cg-sym-label sm) #t))) + (cg-push cg (%opnd 'global ty (%cg-sym-label cg sm) #t))) (($ sym? (kind var) (type ,ty) (slot ,off)) (cg-push cg (%opnd 'frame ty off #t))) (($ sym? (kind param) (type ,ty) (slot ,off)) @@ -4327,8 +4353,10 @@ (if (%ctype-unsigned? ta) (%cg-emit-rrr cg "shr" 't0 'a0 'a1) (%cg-emit-rrr cg "sar" 't0 'a0 'a1))) - ((eq? op 'div) (%cg-emit-rrr cg "div" 't0 'a0 'a1)) - ((eq? op 'rem) (%cg-emit-rrr cg "rem" 't0 'a0 'a1)) + ((eq? op 'div) + (%cg-emit-rrr cg (if unsigned? "udiv" "div") 't0 'a0 'a1)) + ((eq? op 'rem) + (%cg-emit-rrr cg (if unsigned? "urem" "rem") 't0 'a0 'a1)) ((eq? op 'eq) (%cg-emit-cmp cg "eq" 'a0 'a1 't0)) ((eq? op 'ne) (%cg-emit-cmp cg "ne" 'a0 'a1 't0)) ((eq? op 'lt) (%cg-emit-cmp cg (if unsigned? "ltu" "lt") 'a0 'a1 't0)) @@ -4452,7 +4480,7 @@ (or (eq? rk 'struct) (eq? rk 'union)) (> (ctype-size rty) %CC-PAIR-BYTES))) ;; If the callee is variadic, the callee's save area caps total - ;; incoming args at 16. Reject silent miscompiles up front. + ;; incoming ABI words. Reject silent miscompiles up front. (callee-fty (cond ((eq? (ctype-kind fty) 'fn) fty) ((and (eq? (ctype-kind fty) 'ptr) @@ -4464,10 +4492,17 @@ (and (pair? ext) (pair? (cdr ext)) (pair? (cddr ext)) (car (cddr ext)))))) + (arg-slots + (let count ((xs args) (n 0)) + (cond ((null? xs) n) + (else (count (cdr xs) + (+ n (%cg-param-reg-count + (opnd-type (car xs))))))))) (_cap-check (cond - ((and callee-variadic? (> arity 16)) - (die #f "cg-call: variadic call exceeds 16-arg save-area cap" - arity)) + ((and callee-variadic? + (> arg-slots %CG-VARARG-WINDOW)) + (die #f "cg-call: variadic call exceeds save-area" + arg-slots %CG-VARARG-WINDOW)) (else 0))) (sret-shift (if sret? 1 0)) (recv-slot (cond @@ -4476,14 +4511,53 @@ (align-up (ctype-size rty) %CC-WORD-BYTES) (max %CC-WORD-BYTES (ctype-align rty)))) (else #f)))) - (let stage ((xs args) (idx 0)) - (cond - ((null? xs) 0) - (else - (let* ((arg (car xs)) - (aty (opnd-type arg)) - (n (%cg-param-reg-count aty))) - (cond + ;; Copy every indirect aggregate before loading any ABI argument register: + ;; %memcpy_call uses a0-a2, so interleaving a later copy with register + ;; staging would clobber earlier arguments. The parallel list records the + ;; private frame slot for each large aggregate and #f for direct args. + (let ((indirect-copies + (let materialize ((xs args) (out '())) + (cond + ((null? xs) (reverse out)) + (else + (let* ((arg (car xs)) + (aty (opnd-type arg))) + (cond + ((%cg-param-indirect? aty) + (let* ((sz (ctype-size aty)) + (al (max %CC-WORD-BYTES (ctype-align aty))) + (slot (cg-alloc-slot + cg (align-up sz %CC-WORD-BYTES) al))) + (%cg-emit-addr-of cg arg 't0) + (%cg-emit-lea-slot cg "t2" (%cg-slot-expr cg slot)) + (%cg-emit-byte-copy cg 't2 't0 't1 sz) + (materialize (cdr xs) (cons slot out)))) + (else + (materialize (cdr xs) (cons #f out)))))))))) + (let stage ((xs args) (copies indirect-copies) (idx 0)) + (cond + ((null? xs) 0) + (else + (let* ((arg (car xs)) + (aty (opnd-type arg)) + (n (%cg-param-reg-count aty))) + (cond + ;; Large aggregate: pass the address of the private copy through + ;; one pointer ABI slot. The callee marks its pointer slot + ;; indirect, so ordinary struct lvalue operations address it. + ((%cg-param-indirect? aty) + (let ((slot (car copies)) + (abi (+ idx sret-shift))) + (cond + ((< abi 4) + (%cg-emit-lea-slot cg + (%cg-reg->bv (%reg-by-idx abi)) + (%cg-slot-expr cg slot))) + (else + (%cg-emit-lea-slot cg "t0" (%cg-slot-expr cg slot)) + (%cg-emit-st cg 't0 'sp + (* %CC-WORD-BYTES (- abi 4))))) + (stage (cdr xs) (cdr copies) (+ idx 1)))) ;; RV32 i64/u64 values consume two consecutive ABI words. ((%ctype-wide-int? aty) (%cg-load-wide-opnd-into cg arg 't0 't1) @@ -4504,11 +4578,14 @@ (%cg-emit-st cg src 'sp (* %CC-WORD-BYTES (- tabi 4))))) (chunk (+ i 1)))))) - (stage (cdr xs) (+ idx 2))) - ;; Multi-word aggregate: load its target-word chunks into successive arg - ;; regs / stack slots. Stage the struct's address in t0 - ;; once and chunk-load one target word at a time. - ((and (%cg-param-aggregate? aty) (> n 1)) + (stage (cdr xs) (cdr copies) (+ idx 2))) + ;; Direct aggregate: load its target-word chunks into successive + ;; arg regs / stack slots. This path also handles a one-word + ;; aggregate: unlike the generic scalar load below, addr-of knows + ;; when an aggregate lvalue is represented by an indirect frame + ;; slot (for example `p->loc`) and follows that slot before the + ;; chunk load. + ((%cg-param-aggregate? aty) (%cg-emit-addr-of cg arg 't0) (let chunk ((i 0)) (cond @@ -4529,18 +4606,18 @@ (%cg-emit-st cg 't1 'sp (* %CC-WORD-BYTES (- tabi 4)))))) (chunk (+ i 1))))) - (stage (cdr xs) (+ idx n))) + (stage (cdr xs) (cdr copies) (+ idx n))) (else (let ((abi (+ idx sret-shift))) (cond ((< abi 4) (%cg-load-opnd-into cg arg (%reg-by-idx abi)) - (stage (cdr xs) (+ idx 1))) + (stage (cdr xs) (cdr copies) (+ idx 1))) (else (%cg-load-opnd-into cg arg 't0) (%cg-emit-st cg 't0 'sp (* %CC-WORD-BYTES (- abi 4))) - (stage (cdr xs) (+ idx 1))))))))))) + (stage (cdr xs) (cdr copies) (+ idx 1)))))))))))) ;; Stack-arg footprint accounts for the extra ABI slot any ;; >8B-aggregate arg consumed beyond its single-position cousin. (let* ((nabi (let count ((xs args) (n sret-shift)) @@ -4768,10 +4845,10 @@ (%cg-emit-many cg (list "%continue\n"))) ;; -------------------------------------------------------------------- -;; Variadic receive (§G.2). Layout: cg-fn-begin/v reserves a 16-slot -;; (one target word each) save area at known frame offsets, populating each +;; Variadic receive (§G.2). Layout: cg-fn-begin/v reserves a fixed +;; target-word save area at known frame offsets, populating each ;; slot from the appropriate ABI source — a-register for indices 0..3, -;; LDARG for indices 4..15. va_start sets ap to the address of the +;; LDARG for later indices. va_start sets ap to the address of the ;; first slot past the named-arg count; va_arg reads *ap, advances ap ;; by one target word, and pushes the value as the requested type. ;; @@ -4780,8 +4857,8 @@ ;; pushes nothing. cg-va-arg pops ap-lval, loads ap, dereferences for ;; the value, advances ap, stores back, pushes the loaded value. ;; -;; Cap: total incoming args (named + variadic) must fit in the 16-slot -;; save area. Variadic call sites exceeding this die in cg-call; +;; Cap: total incoming ABI words (named + variadic) must fit in the +;; %CG-VARARG-WINDOW-slot save area. Variadic call sites exceeding this die in cg-call; ;; variadic definitions whose named-arg count exceeds it die in ;; cg-fn-begin/v. ;; -------------------------------------------------------------------- @@ -4843,8 +4920,8 @@ ;; inside a nested `.scope` would be invisible to gotos outside it. ;; -------------------------------------------------------------------- (define (%cg-user-label cg name-bv) - (let ((fn (%cg-fn-get cg '%fn-name))) - (bv-cat (list "cc__" fn "__user_" name-bv)))) + (let ((fn (%cg-fn-get cg '%fn-label))) + (bv-cat (list fn "__user_" name-bv)))) (define (cg-emit-label cg name-bv) (%cg-emit-many cg (list ":" (%cg-user-label cg name-bv) "\n"))) @@ -4959,7 +5036,7 @@ (else (die #f "cg-emit-global: bad init piece" piece)))) (define (cg-emit-global cg sym init) - (let* ((lbl (%cg-sym-label sym)) + (let* ((lbl (%cg-sym-label cg sym)) (sz (ctype-size (sym-type sym))) (size (if (< sz 0) %CC-WORD-BYTES sz)) (al (max 1 (ctype-align (sym-type sym))))) @@ -5211,6 +5288,8 @@ (else (let ((v (%hash-ref (car f) n))) (if v v (loop (cdr f)))))))) +(define (scope-lookup-current ps n) + (%hash-ref (car (ps-scope ps)) n)) (define (tag-bind! ps n c) (%hash-set! (car (ps-tags ps)) n c)) (define (tag-lookup ps n) @@ -5265,6 +5344,31 @@ (skip-gnu-attribute! ps) (eat-gnu-attributes! ps)) (else #t))) +;; Parse the universally layout-neutral C11 alignment requests: zero (the +;; standard's no-effect spelling) and one/char alignment (which cannot weaken +;; any object's natural alignment). cc.scm does not carry a pending alignment +;; specifier into its declarator layout, so accepting larger values could +;; silently under-align an object; reject them instead. +(define (skip-c11-alignas! ps) + (expect-kw ps '_Alignas) + (expect-punct ps 'lparen) + (cond + ((%const-tok-is-decl? ps) + (let*-values (((_sto bty) (parse-decl-spec ps)) + ((_n ty) (parse-declarator ps bty))) + (let ((al (ctype-align ty))) + (cond ((<= al 0) + (die (tok-loc (peek ps)) "_Alignas of incomplete type")) + ((> al 1) + (die (tok-loc (peek ps)) + "_Alignas over-alignment unsupported" al)))))) + (else + (let ((n (parse-const-int ps))) + (cond ((or (< n 0) (> n 1)) + (die (tok-loc (peek ps)) + "_Alignas over-alignment unsupported" n)))))) + (expect-punct ps 'rparen)) + (define (parse-decl-spec ps) (let loop ((sto #f) (sn #f) (lg 0) (b #f) (saw #f)) (let ((t (peek ps))) @@ -5277,8 +5381,11 @@ ((at-kw? ps 'extern) (advance ps) (loop 'extern sn lg b #t)) ((at-kw? ps 'typedef) (advance ps) (loop 'typedef sn lg b #t)) ((or (at-kw? ps 'const) (at-kw? ps 'volatile) - (at-kw? ps 'restrict) (at-kw? ps 'inline)) + (at-kw? ps 'restrict) (at-kw? ps 'inline) + (at-kw? ps '_Noreturn)) (advance ps) (loop sto sn lg b #t)) + ((at-kw? ps '_Alignas) + (skip-c11-alignas! ps) (loop sto sn lg b #t)) ((at-kw? ps 'signed) (advance ps) (loop sto 'signed lg b #t)) ((at-kw? ps 'unsigned) (advance ps) (loop sto 'unsigned lg b #t)) ((at-kw? ps 'short) (advance ps) (loop sto sn -1 b #t)) @@ -5297,8 +5404,7 @@ ((or (at-kw? ps '_Complex) (at-kw? ps '_Imaginary)) (advance ps) (loop sto sn lg b #t)) ((or (at-kw? ps '_Atomic) (at-kw? ps '_Thread_local) - (at-kw? ps '_Alignas) (at-kw? ps '_Generic) - (at-kw? ps '_Alignof) (at-kw? ps '_Static_assert)) + (at-kw? ps '_Generic)) (die (tok-loc t) "rejected" (tok-value t))) ((at-kw? ps 'struct) (loop sto sn lg (parse-aggregate-spec ps 'struct) #t)) @@ -5395,6 +5501,8 @@ (let loop ((acc '()) (off 0)) (cond ((at-punct? ps 'rbrace) (reverse acc)) + ((at-kw? ps '_Static_assert) + (parse-static-assert! ps) (loop acc off)) (else (let-values (((_sto bty) (parse-decl-spec ps))) (let dl ((acc2 acc) (o2 off)) @@ -5836,6 +5944,17 @@ (else ;; `sizeof EXPR` (no parens). Same no-eval rule. (cons (%const-sizeof-expr ps #f) %t-word-u)))) + (($ tok? (kind KW) (value _Alignof)) + (advance ps) + (expect-punct ps 'lparen) + (cond + ((%const-tok-is-decl? ps) + (let*-values (((_sto bty) (parse-decl-spec ps)) + ((_n ty) (parse-declarator ps bty))) + (expect-punct ps 'rparen) + (cons (max (ctype-align ty) 1) %t-word-u))) + (else + (cons (%const-alignof-expr ps) %t-word-u)))) (else (parse-const-primary ps))))) ;; Does TOK begin a type-name? Type specifiers, qualifiers, @@ -5852,12 +5971,48 @@ (eq? v '_Complex) (eq? v '_Imaginary) (eq? v 'struct) (eq? v 'union) (eq? v 'enum) (eq? v 'const) (eq? v 'volatile) (eq? v 'restrict) - (eq? v 'inline))) + (eq? v 'inline) (eq? v '_Noreturn))) (($ tok? (kind IDENT) (value ,n)) (typedef? ps n)) (else #f))) (define (%const-tok-is-decl? ps) (%tok-decl-start? ps (peek ps))) +(define (%const-offsetof-field ps ty offset) + (let ((nt (advance ps))) + (cond ((not (eq? (tok-kind nt) 'IDENT)) + (die (tok-loc nt) "__builtin_offsetof expects a member"))) + (let ((f (%cg-find-field (%init-struct-fields ty) (tok-value nt)))) + (cond ((not f) + (die (tok-loc nt) "__builtin_offsetof: no such member" + (tok-value nt)))) + (cons (+ offset (car (cddr f))) (car (cdr f)))))) + +(define (%const-builtin-offsetof ps) + ;; `__builtin_offsetof(T, member[const].nested)` is an integer constant. + ;; Reuse the aggregate field metadata and array element sizes already used by + ;; normal designators; no pointer expression or code generation is needed. + (expect-punct ps 'lparen) + (let*-values (((_sto bty) (parse-decl-spec ps)) + ((_n ty) (parse-declarator ps bty))) + (expect-punct ps 'comma) + (let lp ((p (%const-offsetof-field ps ty 0))) + (pmatch (peek ps) + (($ tok? (kind PUNCT) (value dot)) + (advance ps) (lp (%const-offsetof-field ps (cdr p) (car p)))) + (($ tok? (kind PUNCT) (value lbrack)) + (advance ps) + (let* ((index (parse-const-int ps)) + (_ (expect-punct ps 'rbrack)) + (aty (cdr p))) + (cond ((not (eq? (ctype-kind aty) 'arr)) + (die (tok-loc (peek ps)) + "__builtin_offsetof: subscript on non-array"))) + (let ((elem (car (ctype-ext aty)))) + (lp (cons (+ (car p) (* index (ctype-size elem))) elem))))) + (else + (expect-punct ps 'rparen) + (cons (car p) %t-word-u)))))) + (define (parse-const-primary ps) (let ((t (peek ps))) (pmatch t @@ -5873,10 +6028,15 @@ (let ((v (parse-const-expr ps))) (expect-punct ps 'rparen) v)) (($ tok? (kind IDENT) (value ,n)) - (let ((sm (scope-lookup ps n))) - (cond ((and sm (eq? (sym-kind sm) 'enum-const)) - (advance ps) (cons (sym-slot sm) %t-i32)) - (else (die (tok-loc t) "const-expr: not a constant" n))))) + (cond + ((bv= n "__builtin_offsetof") + (advance ps) (%const-builtin-offsetof ps)) + (else + (let ((sm (scope-lookup ps n))) + (cond ((and sm (eq? (sym-kind sm) 'enum-const)) + (advance ps) (cons (sym-slot sm) %t-i32)) + (else + (die (tok-loc t) "const-expr: not a constant" n))))))) (else (die (tok-loc t) "const-expr: bad operand" (tok-value t)))))) @@ -5971,18 +6131,140 @@ ;; not evaluated, so any emission or vstack push from the parse is ;; discarded. Returns the operand's byte size as a non-negative int. ;; If `paren?`, consumes the closing `)` after parsing. +(define (%const-unevaluated-primary-type ps) + ;; File-scope sizeof/_Alignof has no active function buffer to snapshot. + ;; Cover the ordinary unevaluated-designator grammar directly so static + ;; initializers such as `sizeof table / sizeof table[0]` remain constant + ;; expressions without inventing a synthetic function. + (let ((t (peek ps))) + (pmatch t + (($ tok? (kind IDENT) (value ,n)) + (let ((sm (scope-lookup ps n))) + (cond ((not sm) (die (tok-loc t) "unevaluated: undecl" n))) + (advance ps) + (sym-type sm))) + (($ tok? (kind INT) (value ,v)) + (advance ps) (%c-int-type v)) + (($ tok? (kind CHAR)) (advance ps) %t-i32) + (($ tok? (kind STR) (value ,v)) + (advance ps) (%mk-arr %t-i8 (+ (bytevector-length v) 1))) + (($ tok? (kind PUNCT) (value lparen)) + (cond + ((%const-paren-is-cast? ps) + ;; In an unevaluated operand the cast expression still has to be + ;; consumed, but only its result type matters. This covers the + ;; standard `sizeof(((T *)0)->field)` spelling without asking for an + ;; active function/codegen buffer at file scope. + (advance ps) + (let*-values (((_sto bty) (parse-decl-spec ps)) + ((_n ty) (parse-declarator ps bty))) + (expect-punct ps 'rparen) + (%const-unevaluated-unary-type ps) + ty)) + (else + (advance ps) + (let ((ty (%const-unevaluated-unary-type ps))) + (expect-punct ps 'rparen) + ty)))) + (else (die (tok-loc t) "unevaluated: unsupported operand" + (tok-value t)))))) + +(define (%const-unevaluated-postfix-type ps) + (let lp ((ty (%const-unevaluated-primary-type ps))) + (pmatch (peek ps) + (($ tok? (kind PUNCT) (value lbrack)) + (advance ps) + ;; The index is itself unevaluated by sizeof, but parsing it as a + ;; constant keeps token consumption deterministic for the bootstrap + ;; subset (array tables conventionally use [0]). + (parse-const-int ps) + (expect-punct ps 'rbrack) + (let ((k (ctype-kind ty))) + (cond ((eq? k 'arr) (lp (car (ctype-ext ty)))) + ((eq? k 'ptr) (lp (ctype-ext ty))) + (else (die #f "unevaluated: subscript on non-array" k))))) + (($ tok? (kind PUNCT) (value dot)) + (advance ps) + (let ((nt (peek ps))) + (cond ((not (eq? (tok-kind nt) 'IDENT)) + (die (tok-loc nt) "unevaluated: field"))) + (advance ps) + (let ((f (%cg-find-field (car (cddr (ctype-ext ty))) + (tok-value nt)))) + (cond ((not f) (die (tok-loc nt) "unevaluated: no field" + (tok-value nt)))) + (lp (cadr f))))) + (($ tok? (kind PUNCT) (value arrow)) + (advance ps) + (cond ((not (eq? (ctype-kind ty) 'ptr)) + (die #f "unevaluated: arrow on non-pointer"))) + (let* ((sty (ctype-ext ty)) (nt (peek ps))) + (cond ((not (eq? (tok-kind nt) 'IDENT)) + (die (tok-loc nt) "unevaluated: field"))) + (advance ps) + (let ((f (%cg-find-field (car (cddr (ctype-ext sty))) + (tok-value nt)))) + (cond ((not f) (die (tok-loc nt) "unevaluated: no field" + (tok-value nt)))) + (lp (cadr f))))) + (else ty)))) + +(define (%const-unevaluated-unary-type ps) + (pmatch (peek ps) + (($ tok? (kind PUNCT) (value amp)) + (advance ps) (%mk-ptr (%const-unevaluated-unary-type ps))) + (($ tok? (kind PUNCT) (value star)) + (advance ps) + (let ((ty (%const-unevaluated-unary-type ps))) + (cond ((not (eq? (ctype-kind ty) 'ptr)) + (die #f "unevaluated: dereference of non-pointer"))) + (ctype-ext ty))) + (($ tok? (kind PUNCT) (value plus)) + (advance ps) + (cdr (%const-promote (cons 0 (%const-unevaluated-unary-type ps))))) + (($ tok? (kind PUNCT) (value minus)) + (advance ps) + (cdr (%const-promote (cons 0 (%const-unevaluated-unary-type ps))))) + (($ tok? (kind PUNCT) (value tilde)) + (advance ps) + (cdr (%const-promote (cons 0 (%const-unevaluated-unary-type ps))))) + (($ tok? (kind PUNCT) (value bang)) + (advance ps) (%const-unevaluated-unary-type ps) %t-i32) + (else (%const-unevaluated-postfix-type ps)))) + (define (%const-sizeof-expr ps paren?) (cond ((not (ps-cg ps)) (die #f "#if: sizeof of expression not valid in preprocessor context")) - (else + ((cg-in-fn? (ps-cg ps)) (let ((tag (cg-snapshot (ps-cg ps)))) (cond (paren? (parse-expr ps) (expect-punct ps 'rparen)) (else (parse-unary ps))) (let* ((tp (cg-top (ps-cg ps))) (sz (max (ctype-size (opnd-type tp)) 0))) (cg-rewind (ps-cg ps) tag) - sz))))) + sz))) + (else + (let ((ty (%const-unevaluated-unary-type ps))) + (cond (paren? (expect-punct ps 'rparen))) + (max (ctype-size ty) 0))))) + +(define (%const-alignof-expr ps) + (cond + ((not (ps-cg ps)) + (die #f "#if: _Alignof of expression not valid in preprocessor context")) + ((not (cg-in-fn? (ps-cg ps))) + (let ((ty (%const-unevaluated-unary-type ps))) + (expect-punct ps 'rparen) + (max (ctype-align ty) 1))) + (else + (let ((tag (cg-snapshot (ps-cg ps)))) + (parse-expr ps) + (expect-punct ps 'rparen) + (let* ((tp (cg-top (ps-cg ps))) + (al (max (ctype-align (opnd-type tp)) 1))) + (cg-rewind (ps-cg ps) tag) + al))))) ;; Convenience: returns the integer value alone (callers that don't ;; need the type half of parse-const-expr's (value . ctype) result). @@ -6111,11 +6393,28 @@ (let ((loc (tok-loc (peek ps)))) (debug-log "decl" "line" (loc-line loc) "heap" (heap-usage))))) - (parse-decl-or-fn ps) + (cond ((at-kw? ps '_Static_assert) (parse-static-assert! ps)) + (else (parse-decl-or-fn ps))) ;; Function-local metadata is not part of the persistent world. (cg-fn-meta-set! (ps-cg ps) '()) (loop))))) +(define (parse-static-assert! ps) + (let ((loc (tok-loc (peek ps)))) + (expect-kw ps '_Static_assert) + (expect-punct ps 'lparen) + (let ((v (parse-const-int ps))) + (expect-punct ps 'comma) + (cond ((not (eq? (tok-kind (peek ps)) 'STR)) + (die (tok-loc (peek ps)) "_Static_assert needs a string"))) + (let strings () + (cond ((eq? (tok-kind (peek ps)) 'STR) + (advance ps) (strings)))) + (expect-punct ps 'rparen) + (expect-punct ps 'semi) + (cond ((%c-value-zero? v) (die loc "static assertion failed")) + (else #t))))) + (define (parse-decl-or-fn ps) (let-values (((sto b) (parse-decl-spec ps))) (cond @@ -6228,15 +6527,25 @@ ((ctype-is-fn? ty) (scope-bind! ps n (%sym n 'fn (or sto 'extern) ty #f #f))) - ;; §I: block-scope `static` routes to a global with a name mangled - ;; on the enclosing function so two functions can each have their - ;; own `static int n;` without colliding. The sym's NAME holds the - ;; mangled form (cg-push-sym / cg-emit-global both prefix "cc__" - ;; onto sym-name to derive the emitted label); scope-bind!s key - ;; remains the original identifier for source-level lookup. + ;; §I: block-scope `static` routes to a global with a declaration-id + ;; name under the enclosing function. Function + identifier is not + ;; enough: separate nested scopes may legally reuse the same identifier. + ;; A same-scope redeclaration reuses its existing mangled name; a new + ;; declaration consumes the function's monotonic static counter. The + ;; scope-bind! key remains the original identifier for source lookup. ((and (eq? sto 'static) (ps-fn-ctx ps)) - (let* ((fname (fn-ctx-name (ps-fn-ctx ps))) - (mangled (bytevector-append fname "__" n))) + (let* ((fc (ps-fn-ctx ps)) + (old (scope-lookup-current ps n)) + (mangled + (cond + ((and old (eq? (sym-kind old) 'var) + (eq? (sym-storage old) 'static)) + (sym-name old)) + (else + (let ((id (fn-ctx-static-counter fc))) + (fn-ctx-static-counter-set! fc (+ id 1)) + (bytevector-append (fn-ctx-name fc) "__static_" + (number->string id 10) "__" n)))))) (cond ((at-punct? ps 'assign) (advance ps) @@ -6383,6 +6692,16 @@ ;; - (T){...} (file-scope compound literal) -> (label-ref . cc__cl_N) (let ((t (peek ps))) (cond + ;; Redundant expression grouping around a static initializer. Macro + ;; APIs commonly spell literals as `((T){...})` and strings as + ;; `("...")`; peel one non-cast pair and let the regular initializer + ;; paths consume the enclosed value. + ((and (eq? (tok-kind t) 'PUNCT) (eq? (tok-value t) 'lparen) + (eq? (tok-kind (peek2 ps)) 'STR)) + (advance ps) + (let ((p (%const-init-piece ps ty))) + (expect-punct ps 'rparen) + p)) ;; Address initializer: &ident -> label-ref ((and (eq? (tok-kind t) 'PUNCT) (eq? (tok-value t) 'amp)) (advance ps) @@ -6397,7 +6716,7 @@ (and (eq? (sym-kind sm) 'var) (or (eq? (sym-storage sm) 'static) (eq? (sym-storage sm) 'extern)))) - (cons 'label-ref (%cg-sym-label sm))) + (cons 'label-ref (%cg-sym-label (ps-cg ps) sm))) (else (die (tok-loc it) "init: &x must reference a global" (tok-value it)))))) @@ -6466,7 +6785,7 @@ (eq? (sym-storage sm) 'extern))))))) (advance ps) (let ((sm (scope-lookup ps (tok-value t)))) - (cons 'label-ref (%cg-sym-label sm)))) + (cons 'label-ref (%cg-sym-label (ps-cg ps) sm)))) ;; Plain string literal as char* initializer. ((eq? (tok-kind t) 'STR) (advance ps) @@ -6477,6 +6796,52 @@ (let ((v (parse-const-int ps))) (%int->le-bv v (max (ctype-size ty) 1))))))) +;; Parse a file-scope aggregate compound literal, accepting redundant outer +;; grouping: `(T){...}` and `((T){...})`. The caller already knows an +;; aggregate is required, so a leading parenthesis is unambiguous here. +(define (%global-init-compound ps expected-ty) + (expect-punct ps 'lparen) + (cond + ((at-punct? ps 'lparen) + (let ((pieces (%global-init-compound ps expected-ty))) + (expect-punct ps 'rparen) + pieces)) + (else + (let*-values (((_sto bty) (parse-decl-spec ps)) + ((_n literal-ty) (parse-declarator ps bty))) + (expect-punct ps 'rparen) + (cond ((not (or (ctype-compat? literal-ty expected-ty) + (%ctype-same-aggregate-tag? literal-ty expected-ty))) + (die (tok-loc (peek ps)) + "init: incompatible aggregate compound literal"))) + (expect-punct ps 'lbrace) + (let ((k (ctype-kind literal-ty))) + (cond + ((eq? k 'arr) + (let-values (((pieces _count) + (%parse-init-array-list ps literal-ty))) + pieces)) + ((or (eq? k 'struct) (eq? k 'union)) + (%parse-init-struct-list ps literal-ty)) + (else (die #f "init: compound literal is not aggregate" k)))))))) + +(define (%ctype-same-aggregate-tag? a b) + ;; Qualifier propagation can clone a tagged aggregate ctype, so pointer + ;; identity alone is too strict for `(T){...}` assigned to a const-qualified + ;; T field. A matching non-anonymous tag is the seed compiler's nominal + ;; identity for structs/unions. + (let ((ka (ctype-kind a)) (kb (ctype-kind b))) + (and (eq? ka kb) + (or (eq? ka 'struct) (eq? ka 'union)) + (let ((ta (car (ctype-ext a))) (tb (car (ctype-ext b)))) + (and ta tb (bytes=? ta tb)))))) + +(define (%init-grouped-compound-start? ps) + (or (%const-paren-is-cast? ps) + (pmatch (peek2 ps) + (($ tok? (kind PUNCT) (value lparen)) #t) + (else #f)))) + (define (%init-array-elem-type ty) (cond ((eq? (ctype-kind ty) 'arr) (car (ctype-ext ty))) (else (die #f "init: not an array" ty)))) @@ -6507,6 +6872,30 @@ ((bytes=? (car (car fields)) fname) (cdr fields)) (else (%init-drop-thru-field (cdr fields) fname)))) +;; Consume a C99 field designator, including a chain such as `.v.stack`. +;; Return the top-level field name (for positional-cursor advancement), the +;; final selected type, and its aggregate-relative byte offset. +(define (%init-field-designator ps fields) + (expect-punct ps 'dot) + (let walk ((cur-fields fields) (top-name #f) (offset 0)) + (let ((nt (advance ps))) + (cond ((not (eq? (tok-kind nt) 'IDENT)) + (die (tok-loc nt) "init: .field expects ident"))) + (let ((f (%cg-find-field cur-fields (tok-value nt)))) + (cond ((not f) (die (tok-loc nt) "init: no such field" + (tok-value nt)))) + (let ((name (car f)) + (fty (car (cdr f))) + (foff (car (cddr f)))) + (cond + ((at-punct? ps 'dot) + (advance ps) + (walk (%init-struct-fields fty) + (or top-name name) (+ offset foff))) + (else + (expect-punct ps 'assign) + (list (or top-name name) fty (+ offset foff))))))))) + ;; #t when TY is an array of i8/u8 — a char[] a string literal may ;; initialize directly (C11 §6.7.9 ¶14). (define (%char-arr-type? t) @@ -6541,6 +6930,17 @@ (define (%global-init-elem ps t elide?) (let ((k (ctype-kind t))) (cond + ((and elide? + (or (eq? k 'arr) (eq? k 'struct) (eq? k 'union)) + (at-punct? ps 'lparen) + (%init-grouped-compound-start? ps)) + (let ((pieces (%global-init-compound ps t))) + (cond + (elide? pieces) + (else + (cond ((at-punct? ps 'comma) (advance ps))) + (expect-punct ps 'rbrace) + pieces)))) ;; char[] initialized by a string literal (`{"..."}` or, under ;; brace elision, a bare `"..."`). Without this, the `arr` arm ;; below would treat the string as an element-list and encode it @@ -6577,6 +6977,67 @@ ;; %global-init-elem but emits per-element store ops via cg-assign for ;; scalar leaves, and recurses into the local-list walkers for ;; aggregates. Returns 0; the side effect is the emitted code. +(define (%local-init-expr-tokens ps) + ;; Capture one assignment-expression without consuming the comma or `}` + ;; which terminates it in the surrounding initializer list. Braces must + ;; be tracked separately from parentheses: a compound literal such as + ;; `(struct S){ .x = 1, .y = 2 }` is one expression even though it contains + ;; both commas and a closing brace. + (let loop ((acc '()) (paren 0) (brack 0) (brace 0)) + (let ((t (peek ps))) + (cond + ((eq? (tok-kind t) 'EOF) + (die (tok-loc t) "EOF in local initializer expression")) + ((and (= paren 0) (= brack 0) (= brace 0) + (eq? (tok-kind t) 'PUNCT) + (or (eq? (tok-value t) 'comma) + (eq? (tok-value t) 'rbrace))) + (reverse acc)) + (else + (let ((nt (advance ps))) + (cond + ((not (eq? (tok-kind nt) 'PUNCT)) + (loop (cons nt acc) paren brack brace)) + ((eq? (tok-value nt) 'lparen) + (loop (cons nt acc) (+ paren 1) brack brace)) + ((eq? (tok-value nt) 'rparen) + (loop (cons nt acc) (- paren 1) brack brace)) + ((eq? (tok-value nt) 'lbrack) + (loop (cons nt acc) paren (+ brack 1) brace)) + ((eq? (tok-value nt) 'rbrack) + (loop (cons nt acc) paren (- brack 1) brace)) + ((eq? (tok-value nt) 'lbrace) + (loop (cons nt acc) paren brack (+ brace 1))) + ((eq? (tok-value nt) 'rbrace) + (loop (cons nt acc) paren brack (- brace 1))) + (else + (loop (cons nt acc) paren brack brace))))))))) + +(define (%local-init-aggregate-expr? ps expected) + ;; A brace-less aggregate initializer is ambiguous until its first + ;; assignment-expression has been typed: it can be a whole-aggregate copy + ;; (`.member = value`) or brace elision (`.member = 1, 2`). Parse that + ;; expression once under a codegen snapshot, then put its tokens back. The + ;; real parse below either copies the aggregate or descends into its first + ;; scalar member. This is the same no-side-effect mechanism used by sizeof. + (let* ((it (ps-iter ps)) + (toks (%local-init-expr-tokens ps))) + (for-each (lambda (t) (iter-unget! it t)) (reverse toks)) + (cond + ((null? toks) #f) + (else + (let ((tag (cg-snapshot (ps-cg ps)))) + (parse-saved-expr ps toks) + (let* ((top (cg-top (ps-cg ps))) + (actual (opnd-type top)) + (aggregate? + (and (or (eq? (ctype-kind actual) 'struct) + (eq? (ctype-kind actual) 'union)) + (or (ctype-compat? actual expected) + (%ctype-same-aggregate-tag? actual expected))))) + (cg-rewind (ps-cg ps) tag) + aggregate?)))))) + (define (%local-init-elem ps sm eoff t elide?) (let ((k (ctype-kind t))) (cond @@ -6611,6 +7072,10 @@ (else (%parse-init-local-array-list ps sm eoff t)))) ((or (eq? k 'struct) (eq? k 'union)) (cond + ((and elide? (%local-init-aggregate-expr? ps t)) + (%push-frame-elem-lval ps eoff t) + (parse-expr-bp ps 4) + (cg-copy-struct (ps-cg ps))) (elide? (%parse-init-local-struct-list/mode ps sm eoff t #f)) (else (%parse-init-local-struct-list ps sm eoff t)))) (else @@ -6702,24 +7167,35 @@ (let* ((elem (%init-array-elem-type ty)) (esize (ctype-size elem)) (decl (%init-array-decl-len ty))) - (let lp ((acc '()) (count 0)) + (let lp ((entries '()) (cursor 0) (count 0)) (cond ((cond (brace? (at-punct? ps 'rbrace)) - (else (or (>= count (cond ((< decl 0) 0) (else decl))) + (else (or (>= cursor (cond ((< decl 0) 0) (else decl))) (at-punct? ps 'rbrace) (at-punct? ps 'dot) (at-punct? ps 'lbrack)))) (cond (brace? (advance ps))) - ;; Pad to declared length if longer than count. - (let* ((final (cond ((< decl 0) count) (else decl))) - (pad (- final count))) - (values - (cond - ((> pad 0) (reverse (cons (%pad-piece (* pad esize)) acc))) - (else (reverse acc))) - count))) + (let ((final (cond ((< decl 0) count) (else decl)))) + (values (%merge-init-entries (reverse entries) (* final esize)) + count))) (else - (let ((piece + (let* ((designated? (and brace? (at-punct? ps 'lbrack))) + (index + (cond + (designated? + (advance ps) + (let ((n (parse-const-int ps))) + (expect-punct ps 'rbrack) + (expect-punct ps 'assign) + n)) + (else cursor))) + (_range + (cond + ((or (< index 0) (and (>= decl 0) (>= index decl))) + (die (tok-loc (peek ps)) + "init: array designator out of range" index)) + (else 0))) + (piece (let ((p (cond ((at-punct? ps 'lbrace) @@ -6737,12 +7213,14 @@ ;; no-brace: consume comma only if more items ;; remain in our quota. (cond - ((and (< (+ count 1) + ((and (< (+ index 1) (cond ((< decl 0) 0) (else decl))) (at-punct? ps 'comma)) (advance ps))))) - p))) - (lp (%init-prepend-reversed piece acc) (+ count 1)))))))) + p)) + (next (+ index 1))) + (lp (cons (cons (* index esize) piece) entries) + next (max count next)))))))) (define (%piece-bytesize p) ;; Output width of one piece (cf. %cg-init-piece->bv): a bv emits @@ -6833,19 +7311,9 @@ (else (let* ((designated? (at-punct? ps 'dot)) (target - (cond - (designated? - (advance ps) - (let ((nt (advance ps))) - (cond - ((not (eq? (tok-kind nt) 'IDENT)) - (die (tok-loc nt) "init: .field expects ident"))) - (let ((f (%cg-find-field fields (tok-value nt)))) - (cond - ((not f) (die (tok-loc nt) "init: no such field" - (tok-value nt)))) - (expect-punct ps 'assign) - f))) + (cond + (designated? + (%init-field-designator ps fields)) ((null? rest) (die (tok-loc (peek ps)) "init: too many fields")) (else (car rest)))) @@ -7063,16 +7531,9 @@ (else (let* ((designated? (at-punct? ps 'dot)) (target - (cond - (designated? - (advance ps) - (let ((nt (advance ps))) - (let ((f (%cg-find-field fields (tok-value nt)))) - (cond - ((not f) (die (tok-loc nt) "init: no such field" - (tok-value nt)))) - (expect-punct ps 'assign) - f))) + (cond + (designated? + (%init-field-designator ps fields)) ((null? rest) (die (tok-loc (peek ps)) "init: too many fields")) (else (car rest)))) @@ -7114,7 +7575,7 @@ (par (cadr e)) (var (car (cddr e)))) (let ((psyms (cg-fn-begin/v (ps-cg ps) name par ret var))) (ps-fn-ctx-set! ps - (%fn-ctx name ret (map cdr psyms) var '())) + (%fn-ctx name ret (map cdr psyms) var '() 0)) (scope-enter! ps) (for-each (lambda (p) (scope-bind! ps (car p) (cdr p))) psyms) @@ -7141,6 +7602,7 @@ (advance ps) (expect-punct ps 'semi) (do-continue ps)) (($ tok? (kind KW) (value case)) (parse-case-stmt ps)) (($ tok? (kind KW) (value default)) (parse-default-stmt ps)) + (($ tok? (kind KW) (value _Static_assert)) (parse-static-assert! ps)) (($ tok? (kind IDENT)) (guard (and (eq? (tok-kind (peek2 ps)) 'PUNCT) (eq? (tok-value (peek2 ps)) 'colon))) @@ -7607,6 +8069,18 @@ (expect-punct ps 'rparen) (cg-push-imm (ps-cg ps) %t-word-u (max (ctype-size ty) 0)))) + ;; A string literal has type char[N] until ordinary expression + ;; conversion. parse-primary deliberately lowers strings as + ;; pointers for runtime expressions, so preserve the array bound + ;; here before entering that path. Kit's KIT_SLICE_LIT relies on + ;; exactly this `sizeof("...") - 1` shape during parser setup. + ((and (eq? (tok-kind (peek ps)) 'STR) + (eq? (tok-kind (peek2 ps)) 'PUNCT) + (eq? (tok-value (peek2 ps)) 'rparen)) + (let ((n (+ (bytevector-length (tok-value (peek ps))) 1))) + (advance ps) + (expect-punct ps 'rparen) + (cg-push-imm (ps-cg ps) %t-word-u n))) (else ;; sizeof(EXPR): C semantics — operand is NOT evaluated. ;; Snapshot cg state, parse the expr to learn its type, @@ -7621,12 +8095,36 @@ (cg-push-imm (ps-cg ps) %t-word-u sz)))))) (else ;; sizeof EXPR (no parens) — same no-eval rule. + (cond + ((eq? (tok-kind (peek ps)) 'STR) + (let ((n (+ (bytevector-length (tok-value (peek ps))) 1))) + (advance ps) + (cg-push-imm (ps-cg ps) %t-word-u n))) + (else + (let ((tag (cg-snapshot (ps-cg ps)))) + (parse-unary ps) + (let* ((tp (cg-top (ps-cg ps))) + (sz (max (ctype-size (opnd-type tp)) 0))) + (cg-rewind (ps-cg ps) tag) + (cg-push-imm (ps-cg ps) %t-word-u sz)))))))) + (($ tok? (kind KW) (value _Alignof)) + (advance ps) + (expect-punct ps 'lparen) + (cond + ((token-is-decl? ps) + (let*-values (((_sto bty) (parse-decl-spec ps)) + ((_n ty) (parse-declarator ps bty))) + (expect-punct ps 'rparen) + (cg-push-imm (ps-cg ps) %t-word-u + (max (ctype-align ty) 1)))) + (else (let ((tag (cg-snapshot (ps-cg ps)))) - (parse-unary ps) + (parse-expr ps) + (expect-punct ps 'rparen) (let* ((tp (cg-top (ps-cg ps))) - (sz (max (ctype-size (opnd-type tp)) 0))) + (al (max (ctype-align (opnd-type tp)) 1))) (cg-rewind (ps-cg ps) tag) - (cg-push-imm (ps-cg ps) %t-word-u sz)))))) + (cg-push-imm (ps-cg ps) %t-word-u al)))))) (else (parse-postfix ps)))) (define (token-is-decl? ps) (%tok-decl-start? ps (peek ps))) @@ -7880,6 +8378,13 @@ (cg-va-end (ps-cg ps)) (cg-push-imm (ps-cg ps) %t-i32 0)) +(define (parse-builtin-offsetof ps) + ;; Reuse the constant aggregate-designator parser in ordinary expression + ;; context; offsetof emits no code beyond materializing its integer value. + (advance ps) ; IDENT + (let ((v (%const-builtin-offsetof ps))) + (cg-push-imm (ps-cg ps) (cdr v) (car v)))) + (define (parse-primary ps) (let ((t (peek ps))) (pmatch t @@ -7899,6 +8404,7 @@ ((bv= n "__builtin_va_arg") (parse-builtin-va-arg ps)) ((bv= n "__builtin_va_end") (parse-builtin-va-end ps)) ((bv= n "__builtin_expect") (parse-builtin-expect ps)) + ((bv= n "__builtin_offsetof") (parse-builtin-offsetof ps)) (else (let ((sm (scope-lookup ps n))) (advance ps) @@ -7977,9 +8483,9 @@ ;; --lib=PFX selects library-mode codegen: cc.scm skips the p1_main ;; entry stub and trailing :ELF_end (the catm chain supplies them -;; from P1/entry-*.P1pp + P1/elf-end.P1pp once), and namespaces -;; anonymous string labels as PFX+"cc__str_N" so two cc.scm outputs -;; in the same link don't collide on cc__str_0..N. Returns +;; from P1/entry-*.P1pp + P1/elf-end.P1pp once), and uses PFX as the +;; translation-unit namespace for internal-linkage symbols, strings, +;; compound literals, frame macros, and generated labels. Returns ;; (values prefix-bv rest-args). PREFIX = "" means exec mode (flag ;; absent). PREFIX = "" with the flag present is rejected — silently ;; falling back to exec mode would mask a typo'd Makefile rule. diff --git a/docs/CC.md b/docs/CC.md @@ -262,11 +262,27 @@ char *(*tab[5])(int) // array of 5 pointers to function (int) returning char* - `static` at file scope — gives internal linkage; prevents the symbol from being emitted as a P1pp `:public_label`. Honored. - `static` at block scope — single shared instance, zero-initialized - by default. Honored. + by default. Honored. Distinct declarations in nested lexical scopes receive + distinct emitted identities even when they reuse the same C identifier. - `auto` — accepted, no effect (the default for block scope). - `register` — accepted, no effect. - `typedef` — handled specially (see Types). +### C11 declaration forms used by Kit0 + +- `_Noreturn` is accepted as a declaration specifier and has no P1 codegen + effect. +- `_Static_assert(integer-constant-expression, "message");` is accepted at + file, block, and aggregate-member scope. A false expression is a compile + error. +- `_Alignof(type)` and `_Alignof expression` use the existing target type + metadata. The GNU spellings `__alignof` and `__alignof__` are aliases. +- `__builtin_offsetof(type, member.designator)` is an integer constant and + supports nested member and constant array-subscript steps. +- `_Alignas(type)` and `_Alignas(constant)` accept only zero or one/`char` + alignment. Those requests cannot change any object's natural layout; + over-aligned storage is rejected because the seed layout cannot represent it. + ### Function definitions ``` @@ -286,17 +302,25 @@ K&R-style (`int f(a, b) int a, b; { … }`) is **not** supported. - Scalars: `T x = expr;` — `expr` must be a constant for static-storage variables; arbitrary for auto-storage. - Arrays: `T a[N] = { e0, e1, ... };` and `T a[] = { ... };` (size - inferred). String-literal initializer for `char[]` allowed. + inferred). String-literal initializer for `char[]` and constant array + designators such as `{ [2] = value }` are supported. - Structs: `S s = { e0, e1, ... };` (positional). Designated - initializers (`{ .field = ... }`) **supported** at struct top level - only — required by tcc.c. + initializers (`{ .field = ... }`) support nested field chains such as + `.outer.inner = value`. - Nested initializers brace-flatten the obvious way. +- For local nested aggregate members, a compatible aggregate expression is a + whole-member assignment; other initializer tokens continue through brace + elision. This includes designated aggregate compound literals. +- Aggregate compound literals are accepted in aggregate + initializers, including redundant grouping from macros such as + `#define S(x) ((Slice){ .text = x, .len = sizeof(x)-1 })`. ### Inline / attributes - `inline` — already removed by `-D inline=` in the bootstrap. Our preprocessor would also strip the keyword if it appeared. No effect on codegen either way. +- `_Noreturn` — parsed and discarded; P1 has no return-analysis metadata. - `__attribute__((...))` — parsed and discarded everywhere it appears in declarations. @@ -321,7 +345,7 @@ Cut: - statement expressions `({ ... })` (GCC ext) — tcc.c doesn't use them - `__label__` (GCC) — N/A -- compound literals `(T){ ... }` — tcc.c doesn't use them +- compound literals outside the aggregate-initializer subset - `_Generic` selection — tcc.c doesn't use it - inline asm `__asm__(...)` — N/A; tcc.c gates this on conditions that aren't active at the tcc-mes stage @@ -348,17 +372,20 @@ All standard C operators with standard precedence and associativity: Notes: - `sizeof T` and `sizeof e` both supported. `sizeof e` does **not** - evaluate `e` (standard). + evaluate `e` (standard). Its speculative codegen snapshot restores value + stack, emission offset, frame allocation and indirect-slot metadata, + outgoing-call size, and label state. An immediate string-literal operand + retains its `char[N]` type for this operation, so `sizeof("abc") == 4`. - Integer promotion (rank ≤ `int` → `int`) and usual arithmetic conversions performed automatically. Pointer arithmetic scales by pointee size. - Implicit conversions for assignment, return, and function arguments (incl. promotion of variadic args to `int` / `unsigned int` / pointer / `long` / `unsigned long`). -- String literals have type `char *` (not `const char[N]`) for our - purposes — we strip const, and tcc.c writes through string literals - in a few places. -- `_Alignof` — **not** supported. tcc.c uses no alignment intrinsics. +- Outside `sizeof`, string literals use the bootstrap-level `char *` model — + we strip const, and tcc.c writes through string literals in a few places. +- `_Alignof` and its GNU aliases work for types and unevaluated designator + expressions, including file-scope array/member/subscript expressions. ### Variadic argument access @@ -381,10 +408,12 @@ The compiler ABI has four target-word argument registers (`a0`–`a3`) and word-indexed stack arguments after them. A scalar or pointer consumes one slot; an RV32 `long long` consumes two consecutive slots and is returned in `a0:a1`. Aggregates of at most two target words use the same direct-register -convention; wider aggregate returns pass a hidden result pointer in `a0`. -Variadic functions reserve a contiguous 16-target-word save area, and +convention; wider aggregate arguments are copied by the caller and passed by +an invisible pointer, preserving C by-value semantics. Wider aggregate returns +pass a hidden result pointer in `a0`. +Variadic functions reserve a contiguous 32-target-word save area, and `va_arg(ap, long long)` advances by two RV32 words. Calls exceeding that -documented 16-word cap are rejected rather than silently miscompiled. +documented 32-word cap are rejected rather than silently miscompiled. ## Standard library expectations @@ -424,12 +453,12 @@ Kept explicit so additions are deliberate. | Bitfields | rejected | HAVE_BITFIELD off | | `setjmp` / `longjmp` | not lib | HAVE_SETJMP off | | VLAs | rejected | tcc.c doesn't use; complicates frame layout | -| Compound literals `(T){...}` | rejected | tcc.c doesn't use | +| Compound literals `(T){...}` | limited | aggregate-initializer subset for Kit0 | | Statement expressions `({...})` (GCC) | rejected | tcc.c doesn't use | | `_Generic` | rejected | not used | | `_Atomic`, `_Thread_local` | rejected | not used | -| `_Alignof`, `_Alignas` | rejected | not used | -| `_Static_assert` | rejected | not used | +| `_Alignof`, `_Alignas` | supported/limited | `_Alignas` accepts only layout-neutral 0/1 | +| `_Static_assert`, `_Noreturn` | supported | assertion checked; noreturn metadata ignored | | Wide / UTF strings (`L"…"`, `u8"…"`) | rejected | not used | | Anonymous struct/union members | rejected | not used | | Multi-character constants (`'AB'`) | rejected | not used | diff --git a/docs/HEX2pp.md b/docs/HEX2pp.md @@ -200,3 +200,11 @@ lookup for a non-dotted name ignores scope. Both labels in `LABEL-OTHER` have known addresses by the start of pass 2, so the subtraction is a single operation at emit time. No third pass is required. + +## Limits + +The seed implementation accepts up to 128 MiB of input and emits up to +128 MiB. Its fixed arenas for input, output, token text, labels, fixups, and +scope history end at about 424 MiB past `ELF_end`, within the 512 MiB writable +mapping supplied by the seed ELF headers. The C reference enforces the same +input and output limits. diff --git a/docs/KIT0.md b/docs/KIT0.md @@ -0,0 +1,128 @@ +# Kit0 integration + +boot2 can compile a prepared Kit bootstrap profile as separate translation +units and link the resulting P1pp through its ordinary per-architecture +P1/M1pp/hex2pp path. The supported boot2 architecture names are `aarch64`, +`amd64`, and `riscv64`; RV32 has no corresponding Kit0 profile. Kit currently +supplies the `aa64` preparation; the `x64` and `rv64` mappings are ready for +the corresponding profiles when they are added. + +Prepare and check from the Kit worktree: + +```sh +cd ../../kit +gmake bootstrap-aa64-prepare +KIT0_SWEEP_JOBS=2 gmake bootstrap-aa64-check +``` + +Or run only the boot2 link from this worktree: + +```sh +gmake ARCH=aarch64 DRIVER=podman \ + KIT0_PREP="$(cd ../../kit && pwd)/build/bootstrap-aa64/kit0-prep" kit0 +podman run --rm --pull=never --platform linux/arm64 \ + -v "$(pwd):/boot2" -w /boot2 boot2-busybox-test:aarch64 \ + /boot2/build/aarch64/podman/kit0/kit0 --version +``` + +When `KIT0_PREP` is omitted, the Makefile maps boot2 architecture names to the +conventional Kit profile suffixes: `aarch64` to `aa64`, `amd64` to `x64`, and +`riscv64` to `rv64`. `KIT_DIR` defaults to `../../kit`, `KIT0_PROFILE` can +override just the profile suffix, and an explicit `KIT0_PREP` can point +anywhere. The selected preparation must enable the matching +`KIT_ARCH_*_ENABLED` setting in `resolved-config.h`; this prevents accidentally +framing, assembling, or running one target's prepared sources as another +target. + +`boot/kit0.sh` validates the prepared `manifest.tsv`, `resolved-config.h`, and +`m1pp-partitions.tsv`, then imports the preprocessed `tu/*.i` files and +`kit0-run.scm`. The partition manifest is the inspectable record of the order +encoded in that recipe. The recipe compiles mes-libc and each Kit TU +separately. It expands the M1pp runtime +once, then independently expands entry, libc, every Kit TU, and the ELF +terminator by replaying the common macro prelude and using `%reset-output` +with disjoint hygiene-label ranges. Each non-runtime partition begins with +`.align 16`. The ordered hex2pp partitions are concatenated, prefixed with +the ELF framing, and assembled by the existing hex2pp stage. It does not copy +generated Kit inputs into this worktree. + +## General cc.scm additions + +Kit0 exposed a few ordinary C gaps. They are implemented without Kit source +or filename special cases: + +- `_Noreturn`, checked `_Static_assert`, `_Alignof` (plus GNU aliases), and + layout-neutral `_Alignas`; motivated by Kit's public/config/core headers. +- File-scope unevaluated `sizeof`/alignment designators, array/nested-field + designators, and aggregate compound literals in global initializers; + motivated by Kit's source tables and `KIT_SLICE_LIT`-style initializers. +- `__builtin_offsetof` over member/constant-subscript designators in both + integer-constant and ordinary expression contexts, motivated by Kit's + aggregate-layout assertions and container recovery in `link_script.c`. +- By-value calls for aggregates wider than two target words, implemented as a + caller-owned copy passed through an invisible pointer; motivated by Kit's + context/option structures. Direct aggregate arguments are addressed and + copied correctly even when the source is an indirect member lvalue. +- Unsigned one-word division and remainder, carried as distinct `UDIV` and + `UREM` P1 operations on every backend rather than inheriting signed `DIV` + and `REM` behavior. +- Local nested aggregate initialization distinguishes a compatible + whole-aggregate assignment expression from brace elision, including + designated compound literals. +- Unevaluated-expression snapshots restore frame allocation, indirect-slot, + label, outgoing-call, emission-buffer, and value-stack state, so `sizeof` + cannot alter a later local's storage. +- An immediate string literal retains its array bound under `sizeof`, making + local `KIT_SLICE_LIT` lengths agree with their file-scope equivalents. +- Block-scope static objects use a per-function declaration identity in their + emitted label. Distinct lexical `order` tables no longer collapse merely + because they share a source spelling. +- Complete `--lib` translation-unit namespacing for static objects/functions, + strings, frame slots, user labels, and generated labels. This makes P1pp + fragments independently compilable and safely concatenable. +- A 512 KiB reusable per-function emission buffer on 64-bit targets, sized + for Kit's largest linker-layout routine while preserving cc.scm's + fixed-allocation model and RV32's existing 256 KiB heap budget. +- A 32-word variadic receive window (with ABI-word-aware bounds checks), + covering Kit's 20-argument linker diagnostics without changing the existing + four-register/stack-argument convention. + +The intentionally bootstrap-level behavior is documented in `docs/CC.md`: +`_Noreturn` carries no metadata, and `_Alignas` accepts only the universally +layout-neutral zero or one/`char` requests because P1's seed object layout +cannot represent over-alignment. Kit's prepared implementation TUs contain no +live `_Alignas` declarations; the focused fixture exercises this no-op +contract. + +## Focused regression tests + +Run the compatibility cases across all four boot2 architectures: + +```sh +tests/run.sh --suite cc \ + 320-vararg-cap 346-c11-decl-align 347-aggregate-indirect-arg \ + 348-unsigned-div-rem 349-aggregate-indirect-lvalue-arg \ + 350-large-aggregate-flow 351-address-of-member-local \ + 352-aggregate-compound-call 353-static-local-scope-collision +tests/run.sh --suite cc-util 015-lib-static-namespace +tests/run.sh --suite m1pp 032-reset-output +``` + +The first fixture exercises a 20-argument call above the former variadic +window. The declaration fixture covers every added declaration/alignment form +plus both `offsetof` contexts, nested designators, and grouped aggregate +compound initialization. The aggregate fixture checks that a callee may +mutate a large by-value argument without changing its source object. The +additional aggregate fixtures cover indirect member lvalues and nested +whole-aggregate flow. The arithmetic fixture checks unsigned quotient and +remainder boundary cases. The address fixture protects the unevaluated +snapshot invariant. The compound-call fixture checks both the two-word +argument shape and local string-literal `sizeof`; the static-local fixture +checks same-spelling declarations in distinct scopes. The utility fixture +checks every non-function-local label family under `--lib`; the M1pp fixture +checks output reset and hygiene-base selection. + +The Kit-side check then supplies the integration coverage: every selected TU +must translate, the entire P1 set must link with mes-libc, the resulting Kit0 +must run for the selected architecture, and that seed-built compiler must emit +and archive target objects and statically link a representative executable. diff --git a/docs/M1PP.md b/docs/M1PP.md @@ -24,6 +24,8 @@ The implementation lives in `M1pp/M1pp.c`. It is one pass, allocation-free zero-arg macros plus `SIZE`/`COUNT` - Named stack-frame access via `%frame` / `%endframe` + `%local(field)`, composing with `%struct`-generated `<frame>_FRAME.<field>` macros +- Partitioned expansion via `%reset-output BASE`, retaining definitions while + discarding a replayed prelude and assigning a disjoint hygiene-id range - Compile-time target-word integer expression language (Lisp S-expressions: arithmetic, bitwise, shift, comparison, `strlen`) - Little-endian hex emission: `!` (1B), `@` (2B), `%` (4B), `$` (8B) — @@ -120,6 +122,18 @@ synthesized by `%struct`). Frames do not nest: a second `%frame` before end-of-input. `NAME` is a single `WORD` token and may come from macro-argument substitution. +### `%reset-output` + + %reset-output BASE + +At line start, discards all output emitted so far, resets token spacing, and +sets the macro-hygiene expansion counter to the nonnegative integer `BASE`. +Macro definitions remain installed, so a large program can expand each +translation unit independently by replaying a shared macro prelude, issuing a +different reset base, and concatenating the resulting output partitions. The +first macro call after the directive uses `BASE + 1`; callers must reserve +non-overlapping ranges. `BASE` may not exceed 2147483646. + ## Macro calls %NAME(arg, arg, ...) diff --git a/docs/P1.md b/docs/P1.md @@ -249,7 +249,7 @@ Leaf functions that need no frame-local storage may omit the frame entirely. |----------|------------| | Materialization | `LI rd, imm`, `LA rd, %label`, `LA_BR %label` | | Moves | `MOV rd, rs`, `MOV rd, sp` | -| Arithmetic | `ADD`, `SUB`, `AND`, `OR`, `XOR`, `SHL`, `SHR`, `SAR`, `MUL`, `DIV`, `REM` | +| Arithmetic | `ADD`, `SUB`, `AND`, `OR`, `XOR`, `SHL`, `SHR`, `SAR`, `MUL`, `DIV`, `REM`, `UDIV`, `UREM` | | Immediate arithmetic | `ADDI`, `ANDI`, `ORI`, `SHLI`, `SHRI`, `SARI` | | Memory | `LD`, `ST`, `LB`, `SB` | | ABI access | `LDARG` | @@ -403,7 +403,7 @@ P1 defines the following arithmetic and bitwise operations on one-word values: - register-register: `ADD`, `SUB`, `AND`, `OR`, `XOR`, `SHL`, `SHR`, `SAR`, - `MUL`, `DIV`, `REM` + `MUL`, `DIV`, `REM`, `UDIV`, `UREM` - immediate: `ADDI`, `ANDI`, `ORI`, `SHLI`, `SHRI`, `SARI` For `ADD`, `SUB`, `MUL`, `AND`, `OR`, and `XOR`, computation is modulo the @@ -419,11 +419,13 @@ Immediate-form shifts use inline immediates in the range `0..31` in `P1-32` and `0..63` in `P1-64`. `DIV` is signed division on one-word two's-complement values and truncates -toward zero. `REM` is the corresponding signed remainder. +toward zero. `REM` is the corresponding signed remainder. `UDIV` and `UREM` +interpret both operands as unsigned one-word values and produce the unsigned +quotient and remainder. -Division by zero is outside the portable contract. The overflow case -`MIN_INT / -1` is also outside the portable contract, as is the corresponding -remainder case. +Division by zero is outside the portable contract for all four operations. +The overflow case `MIN_INT / -1` is outside the signed `DIV` contract, as is +the corresponding signed `REM` case; it does not apply to `UDIV` or `UREM`. ### Moves diff --git a/docs/TODO.md b/docs/TODO.md @@ -0,0 +1,30 @@ +# Bootstrap TODO + +## TCC parity and `cc.scm` cleanup + +- [ ] Restore strict `tcc-cc` parity: every C construct accepted by `cc.scm` + must also be accepted by the bundled TCC. +- [ ] Remove raw `_Static_assert`, `_Noreturn`, `_Alignas`, and C11 `_Alignof` + support from `cc.scm`; have the Kit bootstrap configuration erase or map + those spellings before either compiler sees them. +- [ ] Replace Kit's `__builtin_offsetof` dependency with a portable `offsetof` + macro, then remove the builtin parser support. +- [ ] Rewrite Kit's three nested-designator initializers and four file-scope + slice compound initializers, then remove their special handling and the + now-unused file-scope unevaluated-expression walker from `cc.scm`. +- [ ] Retain only the ordered, non-overlapping array-designator behavior Kit + needs; keep the aggregate ABI, arithmetic, `sizeof`, namespacing, static-local, + function-buffer, and variadic correctness changes. +- [ ] Rewrite or remove `346-c11-decl-align` and run the complete `cc` and + `tcc-cc` suites after the language subset is reduced. + +## Kit bootstrap profiles + +- [ ] Add a test that uses tcc to compile the kit0 TUs so that we reject + surviving unsupported C11/builtin tokens in Kit's generated translation + units. +- [ ] Add a test that uses kit0 to compile TCC (so that each can compile the + other) +- [ ] Add Kit `x64` and `rv64` bootstrap profiles matching boot2's `amd64` and + `riscv64` mappings, then run compile, link, smoke, and fixed-point checks for + all three 64-bit targets. diff --git a/hex2pp/hex2pp.P1 b/hex2pp/hex2pp.P1 @@ -33,7 +33,7 @@ ## --- Caps ------------------------------------------------------------------ ## Mirrors hex2pp.c constants. Stored as 8-byte little-endian. -DEFINE H2_INPUT_CAP 0000000100000000 +DEFINE H2_INPUT_CAP 0000000800000000 DEFINE H2_OUTPUT_CAP 0000000800000000 DEFINE H2_TEXT_CAP 0000800000000000 DEFINE H2_LABEL_CAP 0000100000000000 @@ -66,7 +66,7 @@ DEFINE ZERO4 '00000000' ## pat_buf = 4096 B ## ev_bytes = 8 B ## df_byte = 8 B -## input_buf = 16 MiB +## input_buf = 128 MiB ## output_buf = 128 MiB ## text_buf = 8 MiB ## labels = 32 MiB (1<<20 * 32 B) @@ -82,18 +82,18 @@ DEFINE H2_OFF_other_buf 4021000000000000 DEFINE H2_OFF_pat_buf 4031000000000000 DEFINE H2_OFF_ev_bytes 4041000000000000 DEFINE H2_OFF_df_byte 5041000000000000 -## input_buf: 0x00004200 + 16 MiB -## output_buf: 0x01004200 + 128 MiB -## text_buf: 0x09004200 + 8 MiB -## labels: 0x09804200 + 32 MiB -## fixups: 0x0B804200 + 96 MiB -## scope_history: 0x11804200 + 32 MiB (end at ~313 MiB; ELF p_memsz is 512 MiB). +## input_buf: 0x00004200 + 128 MiB +## output_buf: 0x08004200 + 128 MiB +## text_buf: 0x10004200 + 8 MiB +## labels: 0x10804200 + 32 MiB +## fixups: 0x12804200 + 96 MiB +## scope_history: 0x18804200 + 32 MiB (end at ~424 MiB; ELF p_memsz is 512 MiB). DEFINE H2_OFF_input_buf 0042000000000000 -DEFINE H2_OFF_output_buf 0042000100000000 -DEFINE H2_OFF_text_buf 0042000900000000 -DEFINE H2_OFF_labels 0042800900000000 -DEFINE H2_OFF_fixups 0042800B00000000 -DEFINE H2_OFF_scope_history 0042801100000000 +DEFINE H2_OFF_output_buf 0042000800000000 +DEFINE H2_OFF_text_buf 0042001000000000 +DEFINE H2_OFF_labels 0042801000000000 +DEFINE H2_OFF_fixups 0042801200000000 +DEFINE H2_OFF_scope_history 0042801800000000 ## --- p1_main: argv parse -> load input -> two passes -> write -> exit ------ diff --git a/hex2pp/hex2pp.c b/hex2pp/hex2pp.c @@ -28,7 +28,7 @@ #include <string.h> #include <sys/stat.h> -#define MAX_INPUT_BYTES (16 * 1024 * 1024) +#define MAX_INPUT_BYTES (128 * 1024 * 1024) #define MAX_OUTPUT_BYTES (128 * 1024 * 1024) #define MAX_LABELS (1 << 20) #define MAX_FIXUPS (1 << 20) diff --git a/tcc/libc/aarch64/sys_stubs.S b/tcc/libc/aarch64/sys_stubs.S @@ -13,8 +13,14 @@ * don't surface to callers. */ - .globl sys_read, sys_write, sys_close, sys_open - .globl sys_lseek, sys_brk, sys_unlink, sys_exit + .globl sys_read + .globl sys_write + .globl sys_close + .globl sys_open + .globl sys_lseek + .globl sys_brk + .globl sys_unlink + .globl sys_exit sys_read: mov x8, #63 @@ -64,4 +70,5 @@ sys_exit: mov x8, #93 svc #0 /* unreachable */ - b . +.Lsys_exit_hang: + b .Lsys_exit_hang diff --git a/tests/M1pp/032-reset-output.M1pp b/tests/M1pp/032-reset-output.M1pp @@ -0,0 +1,10 @@ +# A partition can replay macro definitions and runtime text, then discard the +# runtime text while retaining the macros. BASE gives its hygiene labels a +# disjoint range from every other partition. +discard me +%macro AGAIN() + :@loop + jump &@loop +%endm +%reset-output 100 +%AGAIN() tail diff --git a/tests/M1pp/032-reset-output.expected b/tests/M1pp/032-reset-output.expected @@ -0,0 +1,3 @@ +:loop__101 +jump &loop__101 +tail diff --git a/tests/cc-util/015-lib-static-namespace.expected b/tests/cc-util/015-lib-static-namespace.expected @@ -0,0 +1,5 @@ +tu7__cc__state +tu7__cc__helper +(+ %tu7__cc__helper__SO 24) +tu7__cc__helper__user_done +tu7__cc__cl_0 diff --git a/tests/cc-util/015-lib-static-namespace.expected-exit b/tests/cc-util/015-lib-static-namespace.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-util/015-lib-static-namespace.scm b/tests/cc-util/015-lib-static-namespace.scm @@ -0,0 +1,18 @@ +;; --lib's prefix is a translation-unit namespace, not merely a string-literal +;; namespace. Exercise every non-function-local label family directly. +(let* ((cg (cg-init/v #t "tu7__")) + (sty (%ctype 'fn -1 -1 (list %t-i32 '() #f))) + (fn (%sym "helper" 'fn 'static sty #f #t)) + (var (%sym "state" 'var 'static %t-i32 #f #t))) + (%hash-set! (car (world-scope (cg-world cg))) "helper" fn) + (write-bv-fd 1 (%cg-sym-label cg var)) + (write-bv-fd 1 "\n") + (cg-fn-begin cg "helper" '() %t-i32) + (write-bv-fd 1 (%cg-fn-get cg '%fn-label)) + (write-bv-fd 1 "\n") + (write-bv-fd 1 (%cg-slot-expr cg 24)) + (write-bv-fd 1 "\n") + (write-bv-fd 1 (%cg-user-label cg "done")) + (write-bv-fd 1 "\n") + (write-bv-fd 1 (%cg-fresh-cl-label cg)) + (write-bv-fd 1 "\n")) diff --git a/tests/cc/320-vararg-cap.c b/tests/cc/320-vararg-cap.c @@ -1,10 +1,10 @@ -/* Variadic save area is capped at 16 incoming-arg slots (named + +/* Variadic save area is capped at 32 incoming-argument words (named + * variadic). cg should reject calls that exceed this cap with a clear * compile-time error rather than silently miscompile by writing past * the save area in the callee's prologue. * - * This test exercises the boundary: 1 named + 15 variadic = 16 args - * total, which is the maximum supported. Sum 1..15 = 120. */ + * This test exercises the Kit-motivated range above the old 16-word cap: + * 1 named + 19 variadic = 20 args total. Sum 1..19 = 190. */ #ifndef CCSCM #include <stdarg.h> @@ -31,6 +31,7 @@ int sum(int n, ...) { } int main(void) { - /* 15 variadic ints; n itself is named arg 0; total 16 args. */ - return sum(15, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); + /* 19 variadic ints; n itself is named arg 0; total 20 args. */ + return sum(19, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19); } diff --git a/tests/cc/320-vararg-cap.expected-exit b/tests/cc/320-vararg-cap.expected-exit @@ -1 +1 @@ -120 +190 diff --git a/tests/cc/346-c11-decl-align.c b/tests/cc/346-c11-decl-align.c @@ -0,0 +1,83 @@ +/* Small C11 declaration forms used by Kit's public/core headers. _Alignas + * is intentionally natural here: cc.scm's seed ABI accepts the spelling but + * does not promise over-aligned storage. */ + +typedef struct Pair { + char c; + long value; + _Static_assert(sizeof(long) >= sizeof(char), "member assertion"); +} Pair; + +struct Nested { + struct { + int value; + } inner; +}; + +typedef struct Slice { + const char *text; + unsigned long len; +} Slice; + +typedef struct NameSlice { + union { + const char *s; + const unsigned char *data; + }; + unsigned long len; +} NameSlice; + +typedef struct InsnDesc { + NameSlice name; + int code; +} InsnDesc; + +#define SLICE(lit) ((Slice){ .text = (lit), .len = sizeof(lit) - 1 }) +#define NAME_SLICE(lit) \ + ((NameSlice){ .s = (lit), .len = sizeof(lit) - 1 }) + +_Static_assert(_Alignof(long) == sizeof(long), "natural scalar alignment"); +_Static_assert(_Alignof(Pair) >= _Alignof(long), "aggregate alignment"); + +_Alignas(char) static long aligned_value = 9; +static const unsigned seed_table[] = { 1, 2, 3 }; +static const unsigned designated_table[4] = { [2] = 13, [0] = 5 }; +static const struct Nested nested = { .inner.value = 11 }; +static const Slice slices[] = { SLICE("x"), SLICE("yz") }; +static const InsnDesc insns[] = { + {{{("movn")}, sizeof("movn") - 1}, 3}, +}; +static const InsnDesc compound_insns[] = { + {NAME_SLICE("movz"), 4}, +}; + +_Static_assert(sizeof seed_table / sizeof seed_table[0] == 3, + "file-scope unevaluated array designator"); +_Static_assert(_Alignof(seed_table[0]) == _Alignof(unsigned), + "file-scope alignof expression"); +_Static_assert(__alignof__(unsigned) == _Alignof(unsigned), + "GNU alignof alias"); +_Static_assert(__builtin_offsetof(Pair, value) == _Alignof(long), + "builtin offsetof uses aggregate layout"); +_Static_assert(sizeof(((Pair *)0)->value) == sizeof(long), + "file-scope sizeof handles a cast designator"); + +_Noreturn static void seed_noreturn_spelling(int *out) +{ + *out = 7; +} + +int main(void) +{ + int result = 0; + _Static_assert(_Alignof(Pair) >= 1, "block assertion"); + seed_noreturn_spelling(&result); + return result + (aligned_value != 9) + (nested.inner.value != 11) + + (slices[0].len != 1) + (slices[1].text[0] != 'y') + + (designated_table[0] != 5) + (designated_table[1] != 0) + + (designated_table[2] != 13) + (designated_table[3] != 0) + + (insns[0].name.s[0] != 'm') + (insns[0].code != 3) + + (compound_insns[0].name.s[3] != 'z') + + (compound_insns[0].code != 4) + + (__builtin_offsetof(Pair, value) != _Alignof(long)); +} diff --git a/tests/cc/346-c11-decl-align.expected-exit b/tests/cc/346-c11-decl-align.expected-exit @@ -0,0 +1 @@ +7 diff --git a/tests/cc/347-aggregate-indirect-arg.c b/tests/cc/347-aggregate-indirect-arg.c @@ -0,0 +1,33 @@ +/* Aggregates wider than two target words are passed through a pointer to a + * caller-owned copy. The callee may mutate its by-value parameter without + * changing the source object. */ +struct BigArg { + long a; + long b; + long c; +}; + +static long consume(struct BigArg value, long bias) +{ + long sum = value.a + value.b + value.c + bias; + value.a = 99; + return sum; +} + +static long consume_after(long bias, struct BigArg value) +{ + value.b = 20; + return bias + value.a + value.b + value.c; +} + +int main(void) +{ + struct BigArg value = { 1, 2, 3 }; + long got = consume(value, 4); + if (got != 10) return 1; + if (value.a != 1) return 2; + got = consume_after(4, value); + if (got != 28) return 3; + if (value.b != 2) return 4; + return 0; +} diff --git a/tests/cc/347-aggregate-indirect-arg.expected-exit b/tests/cc/347-aggregate-indirect-arg.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc/348-unsigned-div-rem.c b/tests/cc/348-unsigned-div-rem.c @@ -0,0 +1,14 @@ +int main(void) { + unsigned long all = ~0UL; + unsigned int word = ~0U; + + if (sizeof all == 8) { + if (all / 8UL != 2305843009213693951UL) return 1; + } else if (all / 8UL != 536870911UL) { + return 1; + } + if (all % 10UL != 5UL) return 2; + if (word / 3U != 1431655765U) return 3; + if (word % 7U != 3U) return 4; + return 0; +} diff --git a/tests/cc/348-unsigned-div-rem.expected-exit b/tests/cc/348-unsigned-div-rem.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc/349-aggregate-indirect-lvalue-arg.c b/tests/cc/349-aggregate-indirect-lvalue-arg.c @@ -0,0 +1,30 @@ +/* A small aggregate passed from an indirect lvalue must contribute its + * contents to the ABI argument slot, not the address of the lvalue. This is + * the shape produced by `p->loc` throughout Kit's preprocessor. */ +struct LocRef { + unsigned file_id; + unsigned off; +}; + +struct Token { + unsigned kind; + struct LocRef loc; +}; + +static int check(struct LocRef loc) +{ + if (loc.file_id != 17) return 1; + if (loc.off != 29) return 2; + return 0; +} + +static int check_token(struct Token *token) +{ + return check(token->loc); +} + +int main(void) +{ + struct Token token = { 3, { 17, 29 } }; + return check_token(&token); +} diff --git a/tests/cc/349-aggregate-indirect-lvalue-arg.expected-exit b/tests/cc/349-aggregate-indirect-lvalue-arg.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc/350-large-aggregate-flow.c b/tests/cc/350-large-aggregate-flow.c @@ -0,0 +1,125 @@ +/* Exercise the large-aggregate path Kit's ApiSValue stack uses: construct a + * 64-byte value, pass it by value into a stack push, return it indirectly from + * a pop, and initialize another local from that return value. */ +struct Value { + unsigned long a; + unsigned long b; + unsigned long c; + unsigned long d; + unsigned long e; + unsigned long f; + unsigned long g; + unsigned long h; +}; + +struct Stack { + struct Value values[2]; + unsigned n; +}; + +struct Operand { + unsigned long x; + unsigned long y; + unsigned long z; +}; + +struct Wrapped { + struct Operand operand; + unsigned long d; + unsigned long e; + unsigned long f; + unsigned long g; + unsigned long h; +}; + +static struct Operand make_operand(unsigned long base) +{ + struct Operand operand; + operand.x = base; + operand.y = base + 1; + operand.z = base + 2; + return operand; +} + +static struct Wrapped wrap_operand(struct Operand operand, + unsigned long tail) +{ + return (struct Wrapped){ + .operand = operand, + .d = tail, + .e = tail + 1, + .f = tail + 2, + .g = tail + 3, + .h = tail + 4, + }; +} + +static struct Value make_value(unsigned long base) +{ + struct Value value; + value.a = base + 0; + value.b = base + 1; + value.c = base + 2; + value.d = base + 3; + value.e = base + 4; + value.f = base + 5; + value.g = base + 6; + value.h = base + 7; + return value; +} + +static struct Value make_value_from_operand(struct Operand operand, + unsigned long tail) +{ + struct Value value; + value.a = operand.x; + value.b = operand.y; + value.c = operand.z; + value.d = tail; + value.e = tail + 1; + value.f = tail + 2; + value.g = tail + 3; + value.h = tail + 4; + return value; +} + +static void push(struct Stack *stack, struct Value value) +{ + stack->values[stack->n++] = value; +} + +static struct Value pop(struct Stack *stack) +{ + return stack->values[--stack->n]; +} + +int main(void) +{ + struct Stack stack = {0}; + struct Value value; + struct Wrapped wrapped = wrap_operand(make_operand(80), 83); + if (wrapped.operand.x != 80) return 11; + if (wrapped.operand.y != 81) return 12; + if (wrapped.operand.z != 82) return 13; + if (wrapped.d != 83 || wrapped.h != 87) return 14; + push(&stack, make_value(32)); + push(&stack, make_value_from_operand(make_operand(64), 67)); + { + struct Value tmp = stack.values[0]; + stack.values[0] = stack.values[1]; + stack.values[1] = tmp; + } + value = pop(&stack); + if (value.a != 32) return 1; + if (value.b != 33) return 2; + if (value.c != 34) return 3; + if (value.d != 35) return 4; + if (value.e != 36) return 5; + if (value.f != 37) return 6; + if (value.g != 38) return 7; + if (value.h != 39) return 8; + value = pop(&stack); + if (value.a != 64) return 9; + if (value.h != 71) return 10; + return 0; +} diff --git a/tests/cc/350-large-aggregate-flow.expected-exit b/tests/cc/350-large-aggregate-flow.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc/351-address-of-member-local.c b/tests/cc/351-address-of-member-local.c @@ -0,0 +1,28 @@ +typedef unsigned long ulong; + +struct Inner { + ulong first; + ulong second; +}; + +struct Outer { + ulong prefix; + struct Inner inner; + ulong suffix; +}; + +static struct Outer storage; + +int main(void) { + struct Outer* outer = &storage; + ulong outer_size = sizeof *outer; + struct Inner* inner = &outer->inner; + + if (outer_size != sizeof(struct Outer)) return 4; + inner->first = 0x11223344ul; + inner->second = 0x55667788ul; + if (storage.inner.first != 0x11223344ul) return 1; + if (storage.inner.second != 0x55667788ul) return 2; + if (inner != &storage.inner) return 3; + return 0; +} diff --git a/tests/cc/351-address-of-member-local.expected-exit b/tests/cc/351-address-of-member-local.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc/352-aggregate-compound-call.c b/tests/cc/352-aggregate-compound-call.c @@ -0,0 +1,30 @@ +/* A two-word compound literal passed directly by value must populate both + * ABI argument words. Kit uses this shape for KIT_SLICE_LIT in parser setup. */ +struct Slice { + const char *text; + unsigned long len; +}; + +static unsigned long inspect(struct Slice slice) +{ + if (!slice.text) return 1; + if (slice.text[0] != 'a') return 2; + if (slice.text[1] != 'b') return 3; + if (slice.text[2] != 'c') return 4; + if (slice.len != 3) return 5; + return 0; +} + +int main(void) +{ + struct Slice slice = (struct Slice){ + .text = "abc", + .len = sizeof("abc") - 1, + }; + if (slice.len != 3) return (int)(10 + slice.len); + if (inspect(slice) != 0) return 7; + return (int)inspect((struct Slice){ + .text = "abc", + .len = sizeof("abc") - 1, + }); +} diff --git a/tests/cc/352-aggregate-compound-call.expected-exit b/tests/cc/352-aggregate-compound-call.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc/353-static-local-scope-collision.c b/tests/cc/353-static-local-scope-collision.c @@ -0,0 +1,25 @@ +/* Distinct lexical scopes in one function may reuse a block-scope static + * identifier. Each declaration has its own object and emitted label. */ +static int pick(int which) +{ + if (which == 0) { + static const int order[] = {12}; + return order[0]; + } + if (which == 1) { + static const int order[] = {7, 9, 11}; + return order[0]; + } + { + static const int order[] = {3, 5}; + return order[1]; + } +} + +int main(void) +{ + if (pick(0) != 12) return 1; + if (pick(1) != 7) return 2; + if (pick(2) != 5) return 3; + return 0; +} diff --git a/tests/cc/353-static-local-scope-collision.expected-exit b/tests/cc/353-static-local-scope-collision.expected-exit @@ -0,0 +1 @@ +0