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
This commit is contained in:
Graeme Geldenhuys 2026-04-20 23:40:53 +01:00
parent 54bfe6e93a
commit e555f1dea9
3 changed files with 84 additions and 25 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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 <stdint.h>
#include <stdlib.h>
#include <string.h>
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;
}