diff --git a/compiler/src/main/pascal/uDebugOPDF.pas b/compiler/src/main/pascal/uDebugOPDF.pas index 9899e98..8244b37 100644 --- a/compiler/src/main/pascal/uDebugOPDF.pas +++ b/compiler/src/main/pascal/uDebugOPDF.pas @@ -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 "" # 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 diff --git a/compiler/src/test/pascal/cp.test.assembler.pas b/compiler/src/test/pascal/cp.test.assembler.pas index 7fa616f..a3dd02c 100644 --- a/compiler/src/test/pascal/cp.test.assembler.pas +++ b/compiler/src/test/pascal/cp.test.assembler.pas @@ -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 ""` + 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(''); + 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 diff --git a/compiler/src/test/pascal/cp.test.opdf.pas b/compiler/src/test/pascal/cp.test.opdf.pas index 7361f4a..af5bc48 100644 --- a/compiler/src/test/pascal/cp.test.opdf.pas +++ b/compiler/src/test/pascal/cp.test.opdf.pas @@ -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;