From e555f1dea9ad1434a1b695ab7e983b36e7ecc0e5 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Mon, 20 Apr 2026 23:40:53 +0100 Subject: [PATCH] Implement ARC RTL and wire string header layout end-to-end - blaise_arc.c: implement _StringAddRef, _StringRelease, _StringConcat using a 12-byte header (refcnt/length/capacity) at the string pointer; refcnt=-1 marks immortal static strings; _StringConcat returns refcnt=0 so the compiler-inserted _StringAddRef at assignment brings it to 1 - EmitDataSection: string literals now include the 12-byte immortal header so _StringAddRef/_StringRelease are safe when called on literal values - EmitWrite: add 12 to string pointer before calling printf so char data is passed correctly (header is now part of every string allocation) - Update TestWriteLn_StringLit_CallsPrintf to check for the +12 offset instruction instead of the no-longer-direct label argument --- compiler/src/main/pascal/uCodeGenQBE.pas | 20 ++++- compiler/src/test/pascal/cp.test.codegen.pas | 5 +- rtl/src/main/c/blaise_arc.c | 84 +++++++++++++++----- 3 files changed, 84 insertions(+), 25 deletions(-) diff --git a/compiler/src/main/pascal/uCodeGenQBE.pas b/compiler/src/main/pascal/uCodeGenQBE.pas index 815e05c..ad4cfda 100644 --- a/compiler/src/main/pascal/uCodeGenQBE.pas +++ b/compiler/src/main/pascal/uCodeGenQBE.pas @@ -109,14 +109,20 @@ end; procedure TCodeGenQBE.EmitDataSection; var - I: Integer; + I: Integer; + StrLen: Integer; begin if FStrLits.Count = 0 then Exit; EmitLine('# String literals'); + { Each literal has a 12-byte ARC header: refcnt=-1 (immortal), length, capacity. + The string pointer IS the header pointer; char data begins at ptr+12. } for I := 0 to FStrLits.Count - 1 do - EmitLine(Format('data $__s%d = { b "%s", b 0 }', - [I, QbeEscapeString(FStrLits[I])])); + begin + StrLen := Length(FStrLits[I]); + EmitLine(Format('data $__s%d = { w -1, w %d, w %d, b "%s", b 0 }', + [I, StrLen, StrLen, QbeEscapeString(FStrLits[I])])); + end; EmitLine('data $__fmt_s_nl = { b "%s\n", b 0 }'); EmitLine('data $__fmt_s = { b "%s", b 0 }'); EmitLine('data $__fmt_d_nl = { b "%d\n", b 0 }'); @@ -873,6 +879,7 @@ procedure TCodeGenQBE.EmitWrite(ACall: TProcCall; ANewline: Boolean); var ArgExpr: TASTExpr; ArgTemp: string; + CharPtr: string; FmtLabel: string; IsString: Boolean; begin @@ -897,7 +904,12 @@ begin FmtLabel := IfThen(ANewline, '$__fmt_d_nl', '$__fmt_d'); if IsString then - EmitLine(Format(' call $printf(l %s, ..., l %s)', [FmtLabel, ArgTemp])) + begin + { String pointer is the header pointer; char data starts at ptr+12. } + CharPtr := AllocTemp; + EmitLine(Format(' %s =l add %s, 12', [CharPtr, ArgTemp])); + EmitLine(Format(' call $printf(l %s, ..., l %s)', [FmtLabel, CharPtr])); + end else EmitLine(Format(' call $printf(l %s, ..., w %s)', [FmtLabel, ArgTemp])); end; diff --git a/compiler/src/test/pascal/cp.test.codegen.pas b/compiler/src/test/pascal/cp.test.codegen.pas index 95946f1..99add6e 100644 --- a/compiler/src/test/pascal/cp.test.codegen.pas +++ b/compiler/src/test/pascal/cp.test.codegen.pas @@ -135,8 +135,9 @@ begin IR := GenerateIR('program P; begin WriteLn(''Hi'') end.'); AssertTrue('Calls printf with s_nl format', IRContains(IR, '$__fmt_s_nl')); - AssertTrue('Passes string pointer', - IRContains(IR, '..., l $__s0')); + { String header is 12 bytes; char data is accessed via add $__s0, 12 } + AssertTrue('Offsets past string header', + IRContains(IR, 'add $__s0, 12')); end; procedure TCodeGenTests.TestWriteLn_IntExpr_CallsPrintfInt; diff --git a/rtl/src/main/c/blaise_arc.c b/rtl/src/main/c/blaise_arc.c index 19f9ec9..4e58189 100644 --- a/rtl/src/main/c/blaise_arc.c +++ b/rtl/src/main/c/blaise_arc.c @@ -1,34 +1,80 @@ /* - * Blaise RTL — ARC string management stubs (Phase 2) + * Blaise RTL — ARC string management (Phase 2) * - * Phase 2: no-op implementations. nil-safe (null check guards). - * - * Phase 3 will implement real reference counting using the locked - * string header layout: + * String pointer convention: + * A Blaise string value is a pointer to the 12-byte header below. + * The character data starts immediately after the header. + * nil (0) represents an empty / unassigned string. * * +--[4 bytes]--+--[4 bytes]--+--[4 bytes]--+--[N bytes]--+--[1 byte]--+ * | RefCount | Length | Capacity | UTF-8 data | NUL | * +-------------+-------------+-------------+-------------+------------+ + * ^--- string pointer (header ptr) ^--- chars at ptr+12 * - * A RefCount of -1 marks a statically-allocated string (string literals - * in the data section). _StringAddRef and _StringRelease are no-ops for - * static strings and nil pointers. + * RefCount = -1 marks a statically-allocated string (string literals in the + * data section). _StringAddRef and _StringRelease are no-ops for static + * strings and nil pointers. + * + * _StringConcat allocates a new header with RefCount = 0 (unowned). The + * compiler inserts a _StringAddRef at every assignment, which brings the + * count to 1. A corresponding _StringRelease at scope exit frees it. */ #include #include +#include -void _StringAddRef(void *ptr) { - (void)ptr; - /* Phase 3: if (ptr && *(int32_t*)ptr != -1) ++*(int32_t*)ptr; */ +#define IMMORTAL_REFCNT (-1) + +typedef struct { + int32_t refcnt; + int32_t length; + int32_t capacity; + /* char data[]; follows immediately */ +} BlaiseStrHdr; + +static inline BlaiseStrHdr* hdr(void* ptr) { + return (BlaiseStrHdr*)ptr; } -void _StringRelease(void *ptr) { - (void)ptr; - /* Phase 3: - if (!ptr) return; - int32_t *rc = (int32_t*)ptr; - if (*rc == -1) return; // static string - if (--(*rc) == 0) free(ptr); - */ +void _StringAddRef(void* ptr) { + if (!ptr) return; + BlaiseStrHdr* h = hdr(ptr); + if (h->refcnt == IMMORTAL_REFCNT) return; + h->refcnt++; +} + +void _StringRelease(void* ptr) { + if (!ptr) return; + BlaiseStrHdr* h = hdr(ptr); + if (h->refcnt == IMMORTAL_REFCNT) return; + if (--h->refcnt == 0) free(ptr); +} + +/* + * Concatenate two Blaise strings. Either or both may be nil. + * Returns a new header with RefCount = 0 (caller takes ownership via AddRef). + * Returns nil if both inputs are nil. + */ +void* _StringConcat(void* s1, void* s2) { + const char* c1 = s1 ? (const char*)s1 + sizeof(BlaiseStrHdr) : ""; + const char* c2 = s2 ? (const char*)s2 + sizeof(BlaiseStrHdr) : ""; + int32_t len1 = s1 ? hdr(s1)->length : 0; + int32_t len2 = s2 ? hdr(s2)->length : 0; + int32_t total = len1 + len2; + BlaiseStrHdr* result; + + result = (BlaiseStrHdr*)malloc(sizeof(BlaiseStrHdr) + total + 1); + if (!result) return NULL; + + result->refcnt = 0; /* unowned; caller's _StringAddRef brings it to 1 */ + result->length = total; + result->capacity = total; + + char* dest = (char*)result + sizeof(BlaiseStrHdr); + if (len1 > 0) memcpy(dest, c1, len1); + if (len2 > 0) memcpy(dest + len1, c2, len2); + dest[total] = '\0'; + + return result; }