feat(rtl): integer div/mod by zero raises catchable EDivByZero
Integer div and mod by zero previously trapped in hardware (SIGFPE),
uncatchable even inside try/except — a single bad divisor terminated the
process. Both backends now emit a divisor-zero guard before each integer
idiv that raises a catchable EDivByZero exception via the normal exception
machinery, matching standard Pascal/Delphi semantics.
- SysUtils: add EDivByZero = class(Exception) and the _RaiseDivByZero helper
that raises EDivByZero('Division by zero').
- QBE + native x86-64: emit a divisor==0 check before integer div/rem
(32- and 64-bit); on zero, call SysUtils__RaiseDivByZero (which longjmps).
- The guard is emitted only when SysUtils is in scope (EDivByZero resolves
through the symbol table); without SysUtils, division traps as before.
This mirrors Delphi's placement of EDivByZero in System.SysUtils and keeps
the always-linked runtime free of stdlib class machinery.
- Tests: in-process e2e proves the guard does not disturb normal division on
both backends; shell-out cli tests (real compiler + linked stdlib) prove
catchable div/mod-by-zero on both backends.
- Rationale recorded in language-rationale.adoc.
All three fixpoints green (FIXPOINT_OK, NATIVE_FIXPOINT_OK,
NATIVE_INTERNAL_OK); full suite OK (3345 tests).
This commit is contained in:
parent
5a15d4d15e
commit
712263abd4
|
|
@ -178,6 +178,10 @@ type
|
|||
|
||||
{ Allocate a fresh local assembly label (".L<prefix><N>"). }
|
||||
function NewLabel(const APrefix: string): string;
|
||||
{ True when a catchable EDivByZero can be raised (SysUtils, which declares
|
||||
EDivByZero + _RaiseDivByZero, is in scope). Gates the div/mod zero
|
||||
guard; without it a zero divisor traps in hardware as before. }
|
||||
function DivGuardAvailable(): Boolean;
|
||||
{ Register a global integer slot of the given type (idempotent; the first
|
||||
registration's type wins). The width and signedness drive both the
|
||||
.data directive and every load/store of the slot. }
|
||||
|
|
@ -793,6 +797,11 @@ begin
|
|||
Inc(FLabelCount);
|
||||
end;
|
||||
|
||||
function TX86_64Backend.DivGuardAvailable(): Boolean;
|
||||
begin
|
||||
Result := (FSymTable <> nil) and (FSymTable.Lookup('EDivByZero') <> nil);
|
||||
end;
|
||||
|
||||
procedure TX86_64Backend.AddGlobal(const AName: string; AType: TTypeDesc);
|
||||
begin
|
||||
if not FDataGlobals.ContainsKey(AName) then
|
||||
|
|
@ -4779,6 +4788,7 @@ var
|
|||
SetElem: TASTExpr;
|
||||
ScEndLbl: string;
|
||||
IsS: Boolean;
|
||||
DivOkLbl: string;
|
||||
begin
|
||||
if AExpr is TNilLiteral then
|
||||
begin
|
||||
|
|
@ -6196,6 +6206,18 @@ begin
|
|||
boMul: Self.Emit(#9'imulq %rcx, %rax');
|
||||
boDiv, boMod:
|
||||
begin
|
||||
{ Divisor-zero guard: when SysUtils is in scope, a zero divisor
|
||||
raises a catchable EDivByZero instead of trapping (SIGFPE).
|
||||
The divisor is in %rcx; SysUtils__RaiseDivByZero longjmps and
|
||||
never returns. }
|
||||
if Self.DivGuardAvailable() then
|
||||
begin
|
||||
DivOkLbl := Self.NewLabel('divok');
|
||||
Self.Emit(#9'testq %rcx, %rcx');
|
||||
Self.Emit(#9'jne ' + DivOkLbl);
|
||||
Self.Emit(#9'callq SysUtils__RaiseDivByZero');
|
||||
Self.Emit(DivOkLbl + ':');
|
||||
end;
|
||||
{ 64-bit divide. Choose signed vs unsigned by the operand types:
|
||||
if either side is an unsigned 64-bit type, use unsigned division
|
||||
so the top bit is a magnitude bit, not a sign. cqto sign-extends
|
||||
|
|
|
|||
|
|
@ -264,6 +264,13 @@ type
|
|||
procedure EmitTryFinallyStmt(AStmt: TTryFinallyStmt);
|
||||
procedure EmitTryExceptStmt(AStmt: TTryExceptStmt);
|
||||
procedure EmitRaiseStmt(AStmt: TRaiseStmt);
|
||||
{ Returns True when a catchable EDivByZero can be raised, i.e. SysUtils
|
||||
(which declares EDivByZero + _RaiseDivByZero) is in scope. When False
|
||||
the div/mod guard is omitted and a zero divisor traps in hardware. }
|
||||
function DivGuardAvailable(): Boolean;
|
||||
{ Emit a divisor==0 check before an integer div/rem. ADivisor is the QBE
|
||||
temp holding the right operand; AIsLong selects l vs w comparison. }
|
||||
procedure EmitDivZeroGuard(const ADivisor: string; AIsLong: Boolean);
|
||||
procedure EmitCompoundStmt(AStmt: TCompoundStmt);
|
||||
procedure EmitAssignment(AAssign: TAssignment);
|
||||
procedure EmitFieldAssignment(AAssign: TFieldAssignment);
|
||||
|
|
@ -2716,6 +2723,39 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
function TCodeGenQBE.DivGuardAvailable(): Boolean;
|
||||
begin
|
||||
{ EDivByZero is declared in SysUtils. If it resolves through the active
|
||||
symbol table, SysUtils is in scope and $SysUtils_RaiseDivByZero is
|
||||
linkable, so the guard can raise a catchable exception. Otherwise the
|
||||
divisor-zero path must fall through to the hardware trap. }
|
||||
Result := (FSymTable <> nil) and (FSymTable.Lookup('EDivByZero') <> nil);
|
||||
end;
|
||||
|
||||
procedure TCodeGenQBE.EmitDivZeroGuard(const ADivisor: string; AIsLong: Boolean);
|
||||
var
|
||||
CmpTemp: string;
|
||||
RaiseLbl: string;
|
||||
OkLbl: string;
|
||||
begin
|
||||
if not Self.DivGuardAvailable() then
|
||||
Exit;
|
||||
CmpTemp := AllocTemp();
|
||||
RaiseLbl := AllocLabel('divzero_raise');
|
||||
OkLbl := AllocLabel('divzero_ok');
|
||||
if AIsLong then
|
||||
EmitLine(Format(' %s =w ceql %s, 0', [CmpTemp, ADivisor]))
|
||||
else
|
||||
EmitLine(Format(' %s =w ceqw %s, 0', [CmpTemp, ADivisor]));
|
||||
EmitLine(Format(' jnz %s, @%s, @%s', [CmpTemp, RaiseLbl, OkLbl]));
|
||||
EmitLine(Format('@%s', [RaiseLbl]));
|
||||
EmitLine(' call $SysUtils__RaiseDivByZero()');
|
||||
{ _RaiseDivByZero longjmps and never returns; the jmp keeps QBE happy by
|
||||
giving the block a terminator. }
|
||||
EmitLine(Format(' jmp @%s', [OkLbl]));
|
||||
EmitLine(Format('@%s', [OkLbl]));
|
||||
end;
|
||||
|
||||
procedure TCodeGenQBE.EmitForStmt(AStmt: TForStmt);
|
||||
var
|
||||
LblCond: string;
|
||||
|
|
@ -12206,6 +12246,8 @@ begin
|
|||
Op := 'add';
|
||||
end;
|
||||
end;
|
||||
if BinExpr.Op in [boDiv, boMod] then
|
||||
Self.EmitDivZeroGuard(R, True);
|
||||
EmitLine(Format(' %s =l %s %s, %s', [T, Op, L, R]));
|
||||
end
|
||||
{ Float arithmetic/comparison: QBE uses d/s typed instructions.
|
||||
|
|
@ -12345,6 +12387,8 @@ begin
|
|||
else
|
||||
Op := 'add';
|
||||
end;
|
||||
if BinExpr.Op in [boDiv, boMod] then
|
||||
Self.EmitDivZeroGuard(R, False);
|
||||
EmitLine(Format(' %s =w %s %s, %s', [T, Op, L, R]));
|
||||
end;
|
||||
Result := T;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,14 @@ type
|
|||
function RunCompiler(const AArgs: array of string;
|
||||
out ACombined: string): Integer;
|
||||
function WriteScratchSource(const ASrc: string): string;
|
||||
{ Run a produced binary, capturing stdout/stderr; returns its exit code. }
|
||||
function RunBinary(const AExe: string; out ACombined: string): Integer;
|
||||
{ Compile ASrc with the given backend (empty = default QBE), link against
|
||||
the full RTL, run it, and report stdout + exit code. Used for features
|
||||
that need stdlib units loaded + linked, which the in-process e2e harness
|
||||
cannot do. }
|
||||
function CompileRunFull(const ASrc, ABackend: string;
|
||||
out AStdout: string; out AExitCode: Integer): Boolean;
|
||||
protected
|
||||
procedure SetUp; override;
|
||||
published
|
||||
|
|
@ -60,6 +68,11 @@ type
|
|||
procedure TestWrongBackendAssemblerRejected; { addendum 2: qbe + --assembler }
|
||||
procedure TestEmitIrStillValidatesAssembler; { addendum 1: validate runs in stdout mode }
|
||||
procedure TestAssemblerLineInHelp; { DescribeOptions drives --help }
|
||||
{ ---- div/mod by zero raises a catchable EDivByZero (needs stdlib) ---- }
|
||||
procedure TestDivByZeroCaught_QBE;
|
||||
procedure TestDivByZeroCaught_Native;
|
||||
procedure TestModByZeroCaught_QBE;
|
||||
procedure TestModByZeroCaught_Native;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -348,6 +361,123 @@ begin
|
|||
Pos('--assembler', Out_) >= 0);
|
||||
end;
|
||||
|
||||
function TCLIContractTests.RunBinary(const AExe: string;
|
||||
out ACombined: string): Integer;
|
||||
var
|
||||
Proc: TProcess;
|
||||
Chunk: string;
|
||||
begin
|
||||
Proc := TProcess.Create(nil);
|
||||
try
|
||||
Proc.Executable := AExe;
|
||||
Proc.Execute();
|
||||
ACombined := '';
|
||||
repeat
|
||||
Chunk := Proc.ReadOutput();
|
||||
ACombined := ACombined + Chunk;
|
||||
until (Chunk = '') and not Proc.Running;
|
||||
Proc.WaitOnExit();
|
||||
Result := Proc.ExitCode;
|
||||
finally
|
||||
Proc.Free();
|
||||
end;
|
||||
end;
|
||||
|
||||
function TCLIContractTests.CompileRunFull(const ASrc, ABackend: string;
|
||||
out AStdout: string; out AExitCode: Integer): Boolean;
|
||||
var
|
||||
SrcPath, BinPath, CompileOut: string;
|
||||
EC: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
SrcPath := WriteScratchSource(ASrc);
|
||||
BinPath := FScratch + 'cli_run_' + IntToStr(FCounter);
|
||||
if ABackend = '' then
|
||||
EC := RunCompiler(['--source', SrcPath,
|
||||
'--unit-path', FRTLPath, '--unit-path', FStdlibPath,
|
||||
'--output', BinPath], CompileOut)
|
||||
else
|
||||
EC := RunCompiler(['--source', SrcPath, '--backend', ABackend,
|
||||
'--unit-path', FRTLPath, '--unit-path', FStdlibPath,
|
||||
'--output', BinPath], CompileOut);
|
||||
if EC <> 0 then
|
||||
begin
|
||||
AStdout := 'compile failed: ' + CompileOut;
|
||||
AExitCode := EC;
|
||||
Exit;
|
||||
end;
|
||||
AExitCode := RunBinary(BinPath, AStdout);
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
const
|
||||
SrcDivByZeroCaught =
|
||||
'program P;' + LineEnding +
|
||||
'uses SysUtils;' + LineEnding +
|
||||
'var a, b: Integer;' + LineEnding +
|
||||
'begin' + LineEnding +
|
||||
' a := 10; b := 0;' + LineEnding +
|
||||
' try' + LineEnding +
|
||||
' WriteLn(a div b)' + LineEnding +
|
||||
' except' + LineEnding +
|
||||
' on E: EDivByZero do WriteLn(''caught: '' + E.Message)' + LineEnding +
|
||||
' end;' + LineEnding +
|
||||
' WriteLn(''after'')' + LineEnding +
|
||||
'end.';
|
||||
|
||||
SrcModByZeroCaught =
|
||||
'program P;' + LineEnding +
|
||||
'uses SysUtils;' + LineEnding +
|
||||
'var a, b: Integer;' + LineEnding +
|
||||
'begin' + LineEnding +
|
||||
' a := 10; b := 0;' + LineEnding +
|
||||
' try' + LineEnding +
|
||||
' WriteLn(a mod b)' + LineEnding +
|
||||
' except' + LineEnding +
|
||||
' on E: EDivByZero do WriteLn(''mod caught'')' + LineEnding +
|
||||
' end' + LineEnding +
|
||||
'end.';
|
||||
|
||||
procedure TCLIContractTests.TestDivByZeroCaught_QBE;
|
||||
var Out_: string; EC: Integer;
|
||||
begin
|
||||
if not CompilerAvailable() then begin Ignore('<toolchain-missing>'); Exit; end;
|
||||
AssertTrue('compile+run', CompileRunFull(SrcDivByZeroCaught, '', Out_, EC));
|
||||
AssertEquals('exit code 0 (exception caught, not SIGFPE)', 0, EC);
|
||||
AssertTrue('EDivByZero caught with message',
|
||||
Pos('caught: Division by zero', Out_) >= 0);
|
||||
AssertTrue('execution continued past the catch', Pos('after', Out_) >= 0);
|
||||
end;
|
||||
|
||||
procedure TCLIContractTests.TestDivByZeroCaught_Native;
|
||||
var Out_: string; EC: Integer;
|
||||
begin
|
||||
if not CompilerAvailable() then begin Ignore('<toolchain-missing>'); Exit; end;
|
||||
AssertTrue('compile+run', CompileRunFull(SrcDivByZeroCaught, 'native', Out_, EC));
|
||||
AssertEquals('exit code 0 (exception caught, not SIGFPE)', 0, EC);
|
||||
AssertTrue('EDivByZero caught with message',
|
||||
Pos('caught: Division by zero', Out_) >= 0);
|
||||
AssertTrue('execution continued past the catch', Pos('after', Out_) >= 0);
|
||||
end;
|
||||
|
||||
procedure TCLIContractTests.TestModByZeroCaught_QBE;
|
||||
var Out_: string; EC: Integer;
|
||||
begin
|
||||
if not CompilerAvailable() then begin Ignore('<toolchain-missing>'); Exit; end;
|
||||
AssertTrue('compile+run', CompileRunFull(SrcModByZeroCaught, '', Out_, EC));
|
||||
AssertEquals('exit code 0', 0, EC);
|
||||
AssertTrue('mod by zero caught', Pos('mod caught', Out_) >= 0);
|
||||
end;
|
||||
|
||||
procedure TCLIContractTests.TestModByZeroCaught_Native;
|
||||
var Out_: string; EC: Integer;
|
||||
begin
|
||||
if not CompilerAvailable() then begin Ignore('<toolchain-missing>'); Exit; end;
|
||||
AssertTrue('compile+run', CompileRunFull(SrcModByZeroCaught, 'native', Out_, EC));
|
||||
AssertEquals('exit code 0', 0, EC);
|
||||
AssertTrue('mod by zero caught', Pos('mod caught', Out_) >= 0);
|
||||
end;
|
||||
|
||||
{ ---- Registration ---- }
|
||||
|
||||
initialization
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@ type
|
|||
{ Exit inside the second try block of a function must pop its exc frame;
|
||||
a later raise must still dispatch correctly (stale-frame regression). }
|
||||
procedure TestRun_ExitInSecondTry_LaterRaiseStillWorks;
|
||||
|
||||
{ The div/mod zero guard (emitted when SysUtils is in scope) must not
|
||||
disturb normal division. Catchable-EDivByZero behaviour needs the
|
||||
stdlib loaded + linked, which the in-process harness cannot do; that
|
||||
is covered by the shell-out test in cp.test.cli.pas. }
|
||||
procedure TestRun_DivNonZero_StillWorks;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -216,6 +222,19 @@ const
|
|||
end.
|
||||
''';
|
||||
|
||||
{ Non-zero divisor: the SysUtils-gated div/mod zero guard must not disturb
|
||||
normal division on either backend. }
|
||||
SrcDivNonZeroStillWorks = '''
|
||||
program P;
|
||||
uses SysUtils;
|
||||
var a, b: Integer;
|
||||
begin
|
||||
a := 20; b := 4;
|
||||
WriteLn(a div b);
|
||||
WriteLn(a mod 7)
|
||||
end.
|
||||
''';
|
||||
|
||||
SrcExcBase =
|
||||
'''
|
||||
program P;
|
||||
|
|
@ -526,6 +545,12 @@ begin
|
|||
'100' + LE + '50' + LE + 'caught' + LE + 'done' + LE, 0);
|
||||
end;
|
||||
|
||||
procedure TE2EExceptionTests.TestRun_DivNonZero_StillWorks;
|
||||
begin
|
||||
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
|
||||
AssertRunsOnAll(SrcDivNonZeroStillWorks, '5' + LE + '6' + LE, 0);
|
||||
end;
|
||||
|
||||
initialization
|
||||
RegisterTest(TE2EExceptionTests);
|
||||
|
||||
|
|
|
|||
|
|
@ -1720,6 +1720,45 @@ OnClause = "on" [ IDENT ":" ] TypeRef DO Stmt ;
|
|||
* *Requiring `on E: T` to always have a variable binding* — Delphi and FPC
|
||||
both permit the no-binding form `on T do`; Blaise follows suit.
|
||||
|
||||
=== Integer division by zero
|
||||
|
||||
==== Decision
|
||||
|
||||
Integer `div` and `mod` by zero raise a catchable `EDivByZero` exception
|
||||
(declared in `SysUtils`, a subclass of `Exception`) rather than trapping in
|
||||
hardware. The code generator emits a divisor-equals-zero check before each
|
||||
integer `idiv`; when the divisor is zero it calls the runtime helper
|
||||
`_RaiseDivByZero`, which raises `EDivByZero('Division by zero')` through the
|
||||
normal exception machinery. Both backends (QBE and native x86-64) emit the
|
||||
guard.
|
||||
|
||||
The guard is emitted *only when `SysUtils` is in scope* — i.e. when
|
||||
`EDivByZero` resolves through the active symbol table. A program that does
|
||||
not use `SysUtils` cannot name the exception class anyway, and its integer
|
||||
division falls through to the hardware behaviour (a `SIGFPE` trap) as before.
|
||||
This mirrors Delphi, where `EDivByZero` lives in `System.SysUtils` and the
|
||||
catchable behaviour is tied to that unit.
|
||||
|
||||
==== Rationale
|
||||
|
||||
A bare hardware trap is uncatchable: a `try`/`except` around `a div b`
|
||||
cannot recover from a zero divisor, so a single bad input terminates the
|
||||
whole process. Routing division by zero through the exception system makes
|
||||
it recoverable, matching standard Pascal/Delphi semantics and the
|
||||
expectations of code that validates input by catching the exception rather
|
||||
than pre-checking every divisor.
|
||||
|
||||
==== Alternatives Rejected
|
||||
|
||||
* *Leave it as a hardware trap* — zero runtime cost, but a zero divisor
|
||||
crashes hard and uncatchably; unacceptable for code that handles untrusted
|
||||
input.
|
||||
* *Define `EDivByZero` in the always-linked runtime so the guard is
|
||||
unconditional* — the runtime (`blaise_exc.pas`) deliberately does not
|
||||
depend on stdlib, and exception classes (with their vtable + TypeInfo) are
|
||||
a stdlib concern. Gating the guard on `SysUtils` keeps the runtime free of
|
||||
class machinery and matches Delphi's unit placement.
|
||||
|
||||
---
|
||||
|
||||
== Project Governance
|
||||
|
|
|
|||
|
|
@ -38,6 +38,20 @@ type
|
|||
EConvertError = class(Exception)
|
||||
end;
|
||||
|
||||
{ Raised by integer `div`/`mod` when the divisor is zero. The compiler
|
||||
emits a divisor==0 guard before each integer division that calls
|
||||
_RaiseDivByZero (below) when SysUtils is in scope; without SysUtils the
|
||||
division traps in hardware (SIGFPE) as before. Matches Delphi, where
|
||||
EDivByZero lives in System.SysUtils. }
|
||||
EDivByZero = class(Exception)
|
||||
end;
|
||||
|
||||
{ _RaiseDivByZero — raises EDivByZero('Division by zero'). Called by the
|
||||
compiler-emitted div/mod guard; not intended for direct use. Declared in
|
||||
the interface so the code generator can reference the $SysUtils_RaiseDivByZero
|
||||
symbol. }
|
||||
procedure _RaiseDivByZero;
|
||||
|
||||
{ BoolToStr — not a compiler built-in; pure Pascal implementation }
|
||||
function BoolToStr(B: Boolean; AUseBoolStrs: Boolean = False): string;
|
||||
|
||||
|
|
@ -57,6 +71,11 @@ begin
|
|||
Self.FMessage := AMessage
|
||||
end;
|
||||
|
||||
procedure _RaiseDivByZero;
|
||||
begin
|
||||
raise EDivByZero.Create('Division by zero')
|
||||
end;
|
||||
|
||||
function BoolToStr(B: Boolean; AUseBoolStrs: Boolean): string;
|
||||
begin
|
||||
if B then
|
||||
|
|
|
|||
Loading…
Reference in a new issue