Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/Fable.Transforms/Python/Fable2Python.Reflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,17 @@ let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map<string, Expressi

Expression.call (callee, generics), stmts @ stmts'

let private getReflectionBaseType (com: IPythonCompiler) (ent: Fable.Entity) =
ent.BaseType
|> Option.bind (fun baseType ->
let baseEnt = com.GetEntity(baseType.Entity)

// decision: native exception bridges add Python ancestry without changing .NET reflection ancestry.
match (com :> Compiler), baseEnt.Attributes with
| FSharp2Fable.Util.ImportAtt(_, "fable_library.exception_bases") -> baseEnt.BaseType
| _ -> Some baseType
)

let transformReflectionInfo com ctx r (ent: Fable.Entity) generics =
if ent.IsFSharpRecord then
transformRecordReflectionInfo com ctx r ent generics
Expand All @@ -310,7 +321,7 @@ let transformReflectionInfo com ctx r (ent: Fable.Entity) generics =
yield Expression.name (name.Name |> Naming.toPythonNaming), stmts
| Some(cons, stmts) -> yield cons, stmts
| None -> ()
match ent.BaseType with
match getReflectionBaseType com ent with
| Some d ->
let genMap =
Seq.zip ent.GenericParameters generics
Expand Down
4 changes: 2 additions & 2 deletions src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2847,7 +2847,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr
// | "System.Collections.Generic.KeyNotFoundException"
| BuiltinSystemException _ -> bclType com ctx r t i thisArg args
| _ ->
let e = makeImportLib com Any "ExceptionBase" "Types"
let e = makeImportLib com Any "ExceptionBase" "exception_bases"
Helper.ConstructorCall(e, t, args, ?loc = r) |> Some
| "get_Message", Some e -> Helper.GlobalCall("str", t, [ thisArg.Value ], ?loc = r) |> Some
| "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some
Expand Down Expand Up @@ -4329,7 +4329,7 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr

let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy<Type list>) genArgs args =
match ent.FullName with
| Types.exception_ -> Some(makeImportLib com Any "ExceptionBase" "Types", args)
| Types.exception_ -> Some(makeImportLib com Any "ExceptionBase" "exception_bases", args)
| Types.attribute -> Some(makeImportLib com Any "Attribute" "Types", args)
| Types.dictionary ->
let args =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<Compile Include="Range.fs" />
<Compile Include="Set.fs" />
<Compile Include="Map.fs" />
<Compile Include="../../fable-library-ts/System.fs" Link="System.fs" />
<Compile Include="System.fs" />
<Compile Include="../../fable-library-ts/System.Collections.Generic.fs" Link="System.Collections.Generic.fs" />
<Compile Include="System.Text.fs" />
</ItemGroup>
Expand Down
32 changes: 32 additions & 0 deletions src/fable-library-py/fable_library/Native.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,38 @@ module Native
open Fable.Core
open Fable.Core.PyInterop

[<Import("ValueErrorBase", "fable_library.exception_bases")>]
type ValueErrorBase(message: string, innerException: exn | null) =
inherit System.Exception(message, innerException)

[<Import("IndexErrorBase", "fable_library.exception_bases")>]
type IndexErrorBase(message: string) =
inherit System.Exception(message)

[<Import("RuntimeErrorBase", "fable_library.exception_bases")>]
type RuntimeErrorBase(message: string) =
inherit System.Exception(message)

[<Import("ZeroDivisionErrorBase", "fable_library.exception_bases")>]
type ZeroDivisionErrorBase(message: string) =
inherit System.Exception(message)

[<Import("OverflowErrorBase", "fable_library.exception_bases")>]
type OverflowErrorBase(message: string) =
inherit System.Exception(message)

[<Import("NotImplementedErrorBase", "fable_library.exception_bases")>]
type NotImplementedErrorBase(message: string) =
inherit System.Exception(message)

[<Import("MemoryErrorBase", "fable_library.exception_bases")>]
type MemoryErrorBase(message: string) =
inherit System.Exception(message)

[<Import("TimeoutErrorBase", "fable_library.exception_bases")>]
type TimeoutErrorBase(message: string) =
inherit System.Exception(message)

[<Import("FSharpCons", ".array_")>]
[<AllowNullLiteral>]
type Cons<'T>(arrayType: string) =
Expand Down
96 changes: 96 additions & 0 deletions src/fable-library-py/fable_library/System.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
namespace System

// decision: keep Python exception ancestry local so other targets retain their existing bases.

type SystemException(message: string) =
inherit Exception(message)
new() = SystemException(SR.Arg_SystemException)

type ApplicationException(message: string) =
inherit Exception(message)
new() = ApplicationException(SR.Arg_ApplicationException)

type ArithmeticException(message: string) =
inherit Exception(message)
new() = ArithmeticException(SR.Arg_ArithmeticException)

type DivideByZeroException(message: string) =
inherit Native.ZeroDivisionErrorBase(message)
new() = DivideByZeroException(SR.Arg_DivideByZero)

type FormatException(message: string) =
inherit Native.ValueErrorBase(message, Unchecked.defaultof<exn>)
new() = FormatException(SR.Arg_FormatException)

type IndexOutOfRangeException(message: string) =
inherit Native.IndexErrorBase(message)
new() = IndexOutOfRangeException(SR.Arg_IndexOutOfRangeException)

type InvalidOperationException(message: string) =
inherit Native.RuntimeErrorBase(message)
new() = InvalidOperationException(SR.Arg_InvalidOperationException)

type NotFiniteNumberException(message: string) =
inherit Exception(message)
new() = NotFiniteNumberException(SR.Arg_NotFiniteNumberException)

type NotImplementedException(message: string) =
inherit Native.NotImplementedErrorBase(message)
new() = NotImplementedException(SR.Arg_NotImplementedException)

type NotSupportedException(message: string) =
inherit Exception(message)
new() = NotSupportedException(SR.Arg_NotSupportedException)

type NullReferenceException(message: string) =
inherit Exception(message)
new() = NullReferenceException(SR.Arg_NullReferenceException)

type OutOfMemoryException(message: string) =
inherit Native.MemoryErrorBase(message)
new() = OutOfMemoryException(SR.Arg_OutOfMemoryException)

type OverflowException(message: string) =
inherit Native.OverflowErrorBase(message)
new() = OverflowException(SR.Arg_OverflowException)

type RankException(message: string) =
inherit Exception(message)
new() = RankException(SR.Arg_RankException)

type StackOverflowException(message: string) =
inherit Exception(message)
new() = StackOverflowException(SR.Arg_StackOverflowException)

type TimeoutException(message: string) =
inherit Native.TimeoutErrorBase(message)
new() = TimeoutException(SR.Arg_TimeoutException)

type ArgumentException(message: string, paramName: string, innerException: exn | null) =
inherit
Native.ValueErrorBase(
(if System.String.IsNullOrEmpty(paramName) then
message
else
message + SR.Arg_ParamName_Name + paramName + "')"),
innerException
)

// Use Unchecked.defaultof rather than a `null` literal for the absent inner
// exception: Fable erases nullable reference annotations, so a `null` literal
// would be emitted against a non-nullable parameter type and fail type checking.
new() = ArgumentException(SR.Arg_ArgumentException, "", Unchecked.defaultof<exn>)
new(message) = ArgumentException(message, "", Unchecked.defaultof<exn>)
new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof<exn>)
new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException)
member _.ParamName = paramName

type ArgumentNullException(paramName: string, message: string) =
inherit ArgumentException(message, paramName, Unchecked.defaultof<exn>)
new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic)
new() = ArgumentNullException("")

type ArgumentOutOfRangeException(paramName: string, message: string) =
inherit ArgumentException(message, paramName, Unchecked.defaultof<exn>)
new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException)
new() = ArgumentOutOfRangeException("")
52 changes: 52 additions & 0 deletions src/fable-library-py/fable_library/exception_bases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Python exception bases for generated .NET exception classes.

decision: retain ExceptionBase ancestry and payloads while adding one specialized Python exception base.
"""


class ExceptionBase(Exception):
"""Base class for .NET ``System.Exception`` and its subclasses.

Subclasses the built-in ``Exception`` so ``raise``/``except``/``isinstance``
keep working as before. Only the message is forwarded to the built-in
initializer, so ``str(exc)`` still returns the message even when an inner
exception is supplied (the built-in would otherwise stringify the whole
argument tuple). The inner exception is kept on a dedicated attribute so it
can be read back through ``System.Exception.InnerException``.
"""

def __init__(self, message: str | None = None, inner_exception: Exception | None = None) -> None:
super().__init__(message if message is not None else "")
self.inner_exception: Exception | None = inner_exception


class ValueErrorBase(ExceptionBase, ValueError):
"""A value error with .NET message and inner-exception support."""


class IndexErrorBase(ExceptionBase, IndexError):
"""An index error with .NET message and inner-exception support."""


class RuntimeErrorBase(ExceptionBase, RuntimeError):
"""A runtime error with .NET message and inner-exception support."""


class ZeroDivisionErrorBase(ExceptionBase, ZeroDivisionError):
"""A division error with .NET message and inner-exception support."""


class OverflowErrorBase(ExceptionBase, OverflowError):
"""An overflow error with .NET message and inner-exception support."""


class NotImplementedErrorBase(ExceptionBase, NotImplementedError):
"""An unimplemented operation with .NET message and inner-exception support."""


class MemoryErrorBase(ExceptionBase, MemoryError):
"""A memory error with .NET message and inner-exception support."""


class TimeoutErrorBase(ExceptionBase, TimeoutError):
"""A timeout error with .NET message and inner-exception support."""
17 changes: 1 addition & 16 deletions src/fable-library-py/fable_library/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
uint32,
uint64,
)
from .exception_bases import ExceptionBase as ExceptionBase


# Unit type for F# unit-typed parameters. Using None mirrors F#'s
Expand All @@ -44,22 +45,6 @@ class Attribute:
...


class ExceptionBase(Exception):
"""Base class for .NET ``System.Exception`` and its subclasses.

Subclasses the built-in ``Exception`` so ``raise``/``except``/``isinstance``
keep working as before. Only the message is forwarded to the built-in
initializer, so ``str(exc)`` still returns the message even when an inner
exception is supplied (the built-in would otherwise stringify the whole
argument tuple). The inner exception is kept on a dedicated attribute so it
can be read back through ``System.Exception.InnerException``.
"""

def __init__(self, message: str | None = None, inner_exception: Exception | None = None) -> None:
super().__init__(message if message is not None else "")
self.inner_exception: Exception | None = inner_exception


# We don't use type aliases here because we need to do isinstance checks.
#
# Generated code represents Int32 as a plain `int` and Float64 as a plain `float`, so
Expand Down
88 changes: 88 additions & 0 deletions tests/Python/TestMisc.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,94 @@
let ex = System.ArgumentException("no inner")
isNull (box ex.InnerException) |> equal true

[<Fact>]
let ``test TimeoutException preserves message and empty InnerException`` () =
let ex = TimeoutException("timeout message")
ex.Message |> equal "timeout message"
isNull (box ex.InnerException) |> equal true
let caught =
try
raise ex
false
with
| :? TimeoutException as caught -> obj.ReferenceEquals(ex, caught)
caught |> equal true

[<Fact>]
let ``test exceptions sharing a Python base retain distinct dotnet types`` () =
let caught =
try
raise (FormatException("bad format"))
"not caught"
with
| :? ArgumentException -> "argument"
| :? FormatException as ex -> ex.Message
caught |> equal "bad format"

#if FABLE_COMPILER
module private PythonExceptionBases =
open Fable.Core.PyInterop

let valueError: obj = import "ValueError" "builtins"
let indexError: obj = import "IndexError" "builtins"
let runtimeError: obj = import "RuntimeError" "builtins"
let zeroDivisionError: obj = import "ZeroDivisionError" "builtins"
let overflowError: obj = import "OverflowError" "builtins"
let notImplementedError: obj = import "NotImplementedError" "builtins"
let memoryError: obj = import "MemoryError" "builtins"
let timeoutError: obj = import "TimeoutError" "builtins"
let exceptionBase: obj = import "ExceptionBase" "fable_library.exception_bases"
let legacyExceptionBase: obj = import "ExceptionBase" "fable_library.types"
let timeoutReflection: Func<System.Type> = import "TimeoutException_reflection" "fable_library.system"
let argumentNullReflection: Func<System.Type> = import "ArgumentNullException_reflection" "fable_library.system"

[<Global("TimeoutError")>]
type TimeoutError() = inherit Exception()

[<Fact>]
let ``test Python ExceptionBase compatibility import preserves class identity`` () =
obj.ReferenceEquals(PythonExceptionBases.exceptionBase, PythonExceptionBases.legacyExceptionBase) |> equal true

[<Fact>]
let ``test dotnet exceptions also match their Python exception bases`` () =
let mappings: (exn * obj) list = [
ArgumentException("message"), PythonExceptionBases.valueError
ArgumentNullException("parameter", "message"), PythonExceptionBases.valueError
ArgumentOutOfRangeException("parameter", "message"), PythonExceptionBases.valueError
FormatException("message"), PythonExceptionBases.valueError
IndexOutOfRangeException("message"), PythonExceptionBases.indexError
InvalidOperationException("message"), PythonExceptionBases.runtimeError
DivideByZeroException("message"), PythonExceptionBases.zeroDivisionError
OverflowException("message"), PythonExceptionBases.overflowError
NotImplementedException("message"), PythonExceptionBases.notImplementedError
OutOfMemoryException("message"), PythonExceptionBases.memoryError
TimeoutException("message"), PythonExceptionBases.timeoutError
]
for ex, pythonBase in mappings do
Fable.Core.PyInterop.pyInstanceof ex pythonBase |> equal true
Fable.Core.PyInterop.pyInstanceof ex PythonExceptionBases.exceptionBase |> equal true
ex.Message.Contains("message") |> equal true

[<Fact>]
let ``test Python TimeoutError handler catches dotnet TimeoutException`` () =
let ex = TimeoutException("timeout message")
let caught =
try
raise ex
false
with
| :? PythonExceptionBases.TimeoutError as caught -> obj.ReferenceEquals(ex, caught)
caught |> equal true

[<Fact>]
let ``test Python exception reflection preserves dotnet ancestry`` () =
let timeoutType = PythonExceptionBases.timeoutReflection.Invoke()
timeoutType.IsSubclassOf(typeof<Exception>) |> equal true
let argumentNullType = PythonExceptionBases.argumentNullReflection.Invoke()
argumentNullType.IsSubclassOf(typeof<ArgumentException>) |> equal true
argumentNullType.IsSubclassOf(typeof<Exception>) |> equal true
#endif

[<Fact>]
let ``test use doesn't return on finally clause`` () = // See #211
let foo() =
Expand Down Expand Up @@ -1200,7 +1288,7 @@
| 2 -> x <- "2"
| 3 | 4 -> x <- "3" // Multiple cases are allowed
// | 5 | 6 as j -> x <- string j // This prevents the optimization
| 4 -> x <- "4" // Unreachable cases are removed

Check warning on line 1291 in tests/Python/TestMisc.fs

View workflow job for this annotation

GitHub Actions / build-python (ubuntu-latest, 3.13)

This rule will never be matched

Check warning on line 1291 in tests/Python/TestMisc.fs

View workflow job for this annotation

GitHub Actions / build-python (ubuntu-latest, 3.12)

This rule will never be matched

Check warning on line 1291 in tests/Python/TestMisc.fs

View workflow job for this annotation

GitHub Actions / build-python (ubuntu-latest, 3.14)

This rule will never be matched

Check warning on line 1291 in tests/Python/TestMisc.fs

View workflow job for this annotation

GitHub Actions / build-python (windows-latest, 3.12)

This rule will never be matched

Check warning on line 1291 in tests/Python/TestMisc.fs

View workflow job for this annotation

GitHub Actions / build-python (windows-latest, 3.13)

This rule will never be matched

Check warning on line 1291 in tests/Python/TestMisc.fs

View workflow job for this annotation

GitHub Actions / build-python (windows-latest, 3.14)

This rule will never be matched
| _ -> x <- "?"
equal "3" x

Expand Down
Loading