fix(opdf): escape string-constant values in the recConstant .ascii line

The OPDF emitter wrote a string constant's value verbatim into the debug
section's `.ascii "<val>"  # Value` line. Any double-quote, newline or
backslash in the value broke the line: an unescaped `"` closed the literal
early, leaving `..."  # Value` as a garbage mnemonic, and a newline split the
directive across two lines. Under --debug-opdf the OPDF section is appended to
the same assembly text the assembler consumes, so this aborted the compile —
the internal assembler with "unhandled mnemonic: [\"  # Value]" and the
external assembler with "junk at end of line". It was content-specific, so it
only surfaced on larger programs (e.g. building a debuggable TestRunner) whose
constants happened to contain such characters.

Add EscapeAsciiStr (C-style escapes: \" \\ \n \r \t + octal for other
non-printables) and apply it to the recConstant value. The OPDF ValueLen field
stays the RAW byte count, which `.ascii` with these escapes reproduces exactly,
so the emitted byte length is unchanged.

Tests: TOPDFTests.TestOPDF_StringConst_SpecialChars_Escaped (IR: the .ascii
line is escaped) and TInternalAsmE2ETests.TestDebugOPDF_StringConstSpecialChars_
AssemblesAndRuns (compile+run under --debug-opdf --assembler internal). Both the
internal- and external-assembler failure modes are resolved by the one escape.

Verified: all four fixpoints green; full suite passes under QBE-built and
native-built test runner (4026 tests).
This commit is contained in:
Graeme Geldenhuys 2026-07-01 12:04:34 +01:00
parent 135588502d
commit 9bbcfd8b77
3 changed files with 136 additions and 1 deletions

View file

@ -197,6 +197,44 @@ begin
FDone := False;
end;
{ Escape a raw string value for a GNU-as / internal-assembler `.ascii "..."`
literal. A string CONSTANT's value is arbitrary user text it may contain a
double-quote, backslash, `#`, or control characters, any of which would break
the surrounding `.ascii "<val>" # Value` line (an unescaped `"` closes the
literal early, leaving `..." # Value` as a garbage mnemonic). We emit the
C-style escapes both assemblers understand. The OPDF ValueLen field is the
RAW byte count (Length before escaping), which `.ascii` with these escapes
reproduces exactly, so escaping does not change the emitted byte length. }
function EscapeAsciiStr(const S: string): string;
var
I, B: Integer;
begin
Result := '';
for I := 0 to Length(S) - 1 do
begin
B := OrdAt(S, I) and $FF;
case B of
Ord('\'): Result := Result + '\\';
Ord('"'): Result := Result + '\"';
10: Result := Result + '\n';
13: Result := Result + '\r';
9: Result := Result + '\t';
else
{ Printable ASCII passes through; anything else (including '#', which is a
comment start OUTSIDE a quoted literal but harmless inside one, and any
non-printable byte) is emitted as a 3-digit octal escape so the line
stays a single well-formed token. }
if (B >= 32) and (B < 127) then
Result := Result + Chr(B)
else
Result := Result + '\' +
Chr(Ord('0') + ((B shr 6) and 7)) +
Chr(Ord('0') + ((B shr 3) and 7)) +
Chr(Ord('0') + (B and 7));
end;
end;
end;
function TOPDFEmitter.ActiveSymTable: TSymbolTable;
begin
if FUnit <> nil then
@ -1144,7 +1182,7 @@ begin
L(' .word ' + IntToStr(Length(C.StrVal)) + ' # ValueLen');
EmitNameLen(C.Name);
if Length(C.StrVal) > 0 then
L(' .ascii "' + C.StrVal + '" # Value');
L(' .ascii "' + EscapeAsciiStr(C.StrVal) + '" # Value');
EmitNameData(C.Name);
end
else

View file

@ -121,6 +121,13 @@ type
assembled verbatim by the internal assembler and called from Pascal. }
procedure TestInlineAsm_ReturnsValue;
procedure TestInlineAsm_AddsTwoArgs;
{ --debug-opdf appends the OPDF section to the SAME assembly text the
internal assembler consumes. A string constant whose value contains a
double-quote or newline used to break the recConstant `.ascii "<val>"`
line (the raw value closed the literal early), aborting the assemble.
Escaping the value fixes it; this compiles+runs such a program under
--debug-opdf --assembler internal. }
procedure TestDebugOPDF_StringConstSpecialChars_AssemblesAndRuns;
end;
implementation
@ -1326,6 +1333,57 @@ begin
AssertEquals('42' + LineEnding, Out_);
end;
procedure TInternalAsmE2ETests.TestDebugOPDF_StringConstSpecialChars_AssemblesAndRuns;
var
SrcFile, OutFile, CompOut, RunOut: string;
Rc, EC: Integer;
Src: string;
begin
if not Self.CompilerAvailable() then
begin
Ignore('<toolchain-missing>');
Exit;
end;
{ A double-quote, a newline and a backslash in string-constant VALUES each
would break the recConstant `.ascii` line if emitted unescaped. }
Src :=
'program test_opdf_qc;' + LineEnding +
'const' + LineEnding +
' Q = ' + '''' + '"' + '''' + ';' + LineEnding +
' NL = ' + '''' + 'a' + '''' + '#10' + '''' + 'b' + '''' + ';' + LineEnding +
' BS = ' + '''' + 'a\b' + '''' + ';' + LineEnding +
'begin' + LineEnding +
' WriteLn(Q);' + LineEnding +
' WriteLn(NL);' + LineEnding +
' WriteLn(BS)' + LineEnding +
'end.';
FCounter := FCounter + 1;
SrcFile := FScratch + 'test_opdf_' + IntToStr(FCounter) + '.pas';
OutFile := FScratch + 'test_opdf_' + IntToStr(FCounter);
WriteFile(SrcFile, Src);
Rc := Self.RunProc(FCompiler, [
'--source', SrcFile,
'--unit-path', FRTLPath,
'--unit-path', FStdlibPath,
'--output', OutFile,
'--backend', 'native',
'--assembler', 'internal',
'--debug-opdf'
], CompOut);
{ The bug manifested as an "Internal assembler error: ... unhandled mnemonic"
from the mangled .ascii line a non-zero compile with that text. }
if Rc <> 0 then
Fail('compile under --debug-opdf failed (rc=' + IntToStr(Rc) + '): ' + CompOut);
EC := Self.RunProcNoArgs(OutFile, RunOut);
AssertEquals(0, EC);
AssertEquals('"' + LineEnding + 'a' + LineEnding + 'b' + LineEnding +
'a\b' + LineEnding, RunOut);
end;
{ ---- Registration ---- }
initialization

View file

@ -30,6 +30,10 @@ type
procedure TestOPDF_Primitive_Double_SubKindFloat;
procedure TestOPDF_Primitive_Single_SubKindFloat;
procedure TestOPDF_AnsiStr_Record;
{ A string constant whose value contains a double-quote, newline or
backslash must be C-escaped in the recConstant `.ascii` line, else the
literal closes early and the assembler sees a garbage mnemonic. }
procedure TestOPDF_StringConst_SpecialChars_Escaped;
procedure TestOPDF_GlobalVar_QuadLabel;
procedure TestOPDF_GlobalVar_RecType;
procedure TestOPDF_Enum_RecType;
@ -224,6 +228,41 @@ begin
AssertTrue('Utf8String name emitted', Contains(IR, '.ascii "Utf8String"'));
end;
procedure TOPDFTests.TestOPDF_StringConst_SpecialChars_Escaped;
var
IR: string;
begin
{ A string const whose value is a single double-quote must be emitted as
`.ascii "\""`, NOT `.ascii """` (which closes the literal after the first
quote and leaves `" # Value` as a stray token). }
IR := GenOPDF(
'''
program P;
const Q = '"';
begin end.
''');
AssertTrue('quote value is escaped', Contains(IR, '.ascii "\"" # Value'));
AssertFalse('no unescaped triple-quote', Contains(IR, '.ascii """ # Value'));
{ A newline in the value must become \n, not a literal line break. }
IR := GenOPDF(
'''
program P;
const NL = 'a'#10'b';
begin end.
''');
AssertTrue('newline escaped as \n', Contains(IR, '.ascii "a\nb" # Value'));
{ A backslash must be doubled. }
IR := GenOPDF(
'''
program P;
const BS = 'a\b';
begin end.
''');
AssertTrue('backslash doubled', Contains(IR, '.ascii "a\\b" # Value'));
end;
procedure TOPDFTests.TestOPDF_GlobalVar_QuadLabel;
var
IR: string;