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
7 changes: 7 additions & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

* [JS/TS] Fix `Unchecked.defaultof<'T>` for struct types with fields returning `undefined` instead of zero-initialized values (by @MangelMaxime)
* [Python] Fix `Unchecked.defaultof<'T>` for struct types with fields returning incorrect values instead of zero-initialized instances (by @MangelMaxime)
* [Python] Fix `Unchecked.defaultof<char>` returning `""` (empty string) instead of `"\u0000"` (null character) (by @MangelMaxime)
* [Python] Improve `Unchecked.defaultof<_>` for declared entities (by @MangelMaxime)

## 5.0.0-rc.5 - 2026-03-20

### Added
Expand Down
7 changes: 7 additions & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

* [JS/TS] Fix `Unchecked.defaultof<'T>` for struct types with fields returning `undefined` instead of zero-initialized values (by @MangelMaxime)
* [Python] Fix `Unchecked.defaultof<'T>` for struct types with fields returning incorrect values instead of zero-initialized instances (by @MangelMaxime)
* [Python] Fix `Unchecked.defaultof<char>` returning `""` (empty string) instead of `"\u0000"` (null character) (by @MangelMaxime)
* [Python] Improve `Unchecked.defaultof<_>` for declared entities (by @MangelMaxime)

## 5.0.0-rc.11 - 2026-03-20

### Added
Expand Down
12 changes: 9 additions & 3 deletions src/Fable.Transforms/Python/Fable2Python.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -805,9 +805,15 @@ module Util =
| Float32 -> makeFloat com ctx None t "float32" 0.0 |> fst
| Float64 -> makeFloat com ctx None t "float64" 0.0 |> fst
| Decimal -> makeFloat com ctx None t "float64" 0.0 |> fst
| Fable.String
| Fable.Char -> Expression.stringConstant ""
| Fable.DeclaredType(ent, _) -> Expression.none
| Fable.Char -> Expression.stringConstant "\u0000"
| Fable.String -> Expression.stringConstant ""
| Fable.DeclaredType(entRef, _) ->
match com.TryGetEntity(entRef) with
| Some ent when ent.IsValueType ->
match tryPyConstructor com ctx ent with
| Some(ctorExpr, _stmts) -> Expression.call (ctorExpr, [])
| None -> Expression.none
| _ -> Expression.none
| Fable.GenericParam _ -> libValue com ctx "util" "UNIT"
| _ -> Expression.none

Expand Down
130 changes: 77 additions & 53 deletions src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -727,20 +727,93 @@ let makeHashSet (com: ICompiler) ctx r t sourceSeq =
makeEqualityComparer com ctx key |> makeHashSetWithComparer com r t sourceSeq
| _ -> Helper.GlobalCall("set", t, [ sourceSeq ], isConstructor = true, ?loc = r)

let rec getZero (com: ICompiler) ctx (t: Type) =
let tryEntityIdent (com: Compiler) entFullName =
match entFullName with
| BuiltinDefinition BclDateOnly
| BuiltinDefinition BclDateTime
| BuiltinDefinition BclDateTimeOffset -> makeIdentExpr "Date" |> Some
| BuiltinDefinition BclTimer -> makeImportLib com Any "default" "Timer" |> Some
| BuiltinDefinition(FSharpReference _) -> makeImportLib com Any "FSharpRef" "Types" |> Some
| BuiltinDefinition(FSharpResult _) ->
// Import the underscore-prefixed base class (has cases() method), not the type alias
makeImportLib com Any "_FSharpResult_2" "Result" |> Some
| BuiltinDefinition(FSharpChoice genArgs) ->
// Import the underscore-prefixed base class (has cases() method), not the type alias
let membName = $"_FSharpChoice_%d{List.length genArgs}"
makeImportLib com Any membName "Choice" |> Some
// | BuiltinDefinition BclGuid -> jsTypeof "string" expr
// | BuiltinDefinition BclTimeSpan -> jsTypeof "number" expr
// | BuiltinDefinition BclHashSet _ -> fail "MutableSet" // TODO:
// | BuiltinDefinition BclDictionary _ -> fail "MutableMap" // TODO:
// | BuiltinDefinition BclKeyValuePair _ -> fail "KeyValuePair" // TODO:
// | BuiltinDefinition FSharpSet _ -> fail "Set" // TODO:
// | BuiltinDefinition FSharpMap _ -> fail "Map" // TODO:
| Types.matchFail -> makeImportLib com Any "MatchFailureException" "Types" |> Some
| Types.exception_ -> makeIdentExpr "Exception" |> Some
// | "System.Collections.Generic.KeyNotFoundException" ->
// makeImportLib com Any "KeyNotFoundException" "System.Collections.Generic" |> Some
| BuiltinSystemException entName -> makeImportLib com Any entName "System" |> Some
// | Naming.EndsWith "Exception" _ -> makeIdentExpr "Exception" |> Some
| _ -> None

let tryConstructor com (ent: Entity) =
if FSharp2Fable.Util.isReplacementCandidate ent.Ref then
tryEntityIdent com (ent.FullName |> Naming.toPythonNaming)
else
match FSharp2Fable.Util.tryEntityIdentMaybeGlobalOrImported com ent with
| Some(IdentExpr ident) when ent.IsFSharpUnion ->
// For F# union types, the base class is prefixed with underscore (_UnionName)
// This is needed for both reflection (base class has cases() method) and
// type annotations (self inside base class methods)
Some(IdentExpr { ident with Name = "_" + ident.Name })
| other -> other

let constructor com ent =
match tryConstructor com ent with
| Some e -> e
| None -> $"Cannot find %s{ent.FullName} constructor" |> addErrorAndReturnNull com [] None

/// For user-defined value types (structs), returns a constructor call initialized with
/// a zero value for each instance field. Only instance fields are constructor parameters;
/// static fields (from `static let` bindings) are excluded to avoid deep recursion through
/// static field type graphs. Returns None if no constructor is available or the type is
/// not a value type.
let rec private tryZeroValueTypeConstructor (com: ICompiler) (ctx: Context) (t: Type) (ent: Entity) =
if ent.IsValueType then
tryConstructor com ent
|> Option.map (fun e ->
let args =
ent.FSharpFields
|> List.choose (fun f ->
if f.IsStatic then
None
else
getZero com ctx f.FieldType |> Some
)

Helper.ConstructorCall(e, t, args)
)
else
None

and private getZero (com: ICompiler) ctx (t: Type) =
match t with
| Boolean -> makeBoolConst false
| Number(BigInt, _) as t -> Helper.LibCall(com, "big_int", "fromInt32", t, [ makeIntConst 0 ])
| Number(Decimal, _) as t -> makeIntConst 0 |> makeDecimalFromExpr com None t
| Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue None
| Char
| Char -> makeCharConst '\u0000'
| String -> makeStrConst "" // TODO: Use null for string?
| Builtin BclTimeSpan -> Helper.LibCall(com, "time_span", "create", t, [ makeIntConst 0 ])
| Builtin BclDateTime as t -> Helper.LibCall(com, "date", "minValue", t, [])
| Builtin BclDateTimeOffset as t -> Helper.LibCall(com, "DateOffset", "minValue", t, [])
| Builtin(FSharpSet genArg) as t -> makeSet com ctx None t "Empty" [] genArg
| Builtin(BclKeyValuePair(k, v)) -> makeTuple None true [ getZero com ctx k; getZero com ctx v ]
| ListSingleton(CustomOp com ctx None t "get_Zero" [] e) -> e
| DeclaredType(entRef, _) ->
com.GetEntity(entRef)
|> tryZeroValueTypeConstructor com ctx t
|> Option.defaultValue (Value(Null Any, None)) // null
| _ -> Value(Null Any, None) // null

let getOne (com: ICompiler) ctx (t: Type) =
Expand Down Expand Up @@ -810,52 +883,6 @@ let injectArg (com: ICompiler) (ctx: Context) r moduleName methName (genArgs: Ty
| None -> args
| Some injectInfo -> injectArgInner args injectInfo

let tryEntityIdent (com: Compiler) entFullName =
match entFullName with
| BuiltinDefinition BclDateOnly
| BuiltinDefinition BclDateTime
| BuiltinDefinition BclDateTimeOffset -> makeIdentExpr "Date" |> Some
| BuiltinDefinition BclTimer -> makeImportLib com Any "default" "Timer" |> Some
| BuiltinDefinition(FSharpReference _) -> makeImportLib com Any "FSharpRef" "Types" |> Some
| BuiltinDefinition(FSharpResult _) ->
// Import the underscore-prefixed base class (has cases() method), not the type alias
makeImportLib com Any "_FSharpResult_2" "Result" |> Some
| BuiltinDefinition(FSharpChoice genArgs) ->
// Import the underscore-prefixed base class (has cases() method), not the type alias
let membName = $"_FSharpChoice_%d{List.length genArgs}"
makeImportLib com Any membName "Choice" |> Some
// | BuiltinDefinition BclGuid -> jsTypeof "string" expr
// | BuiltinDefinition BclTimeSpan -> jsTypeof "number" expr
// | BuiltinDefinition BclHashSet _ -> fail "MutableSet" // TODO:
// | BuiltinDefinition BclDictionary _ -> fail "MutableMap" // TODO:
// | BuiltinDefinition BclKeyValuePair _ -> fail "KeyValuePair" // TODO:
// | BuiltinDefinition FSharpSet _ -> fail "Set" // TODO:
// | BuiltinDefinition FSharpMap _ -> fail "Map" // TODO:
| Types.matchFail -> makeImportLib com Any "MatchFailureException" "Types" |> Some
| Types.exception_ -> makeIdentExpr "Exception" |> Some
// | "System.Collections.Generic.KeyNotFoundException" ->
// makeImportLib com Any "KeyNotFoundException" "System.Collections.Generic" |> Some
| BuiltinSystemException entName -> makeImportLib com Any entName "System" |> Some
// | Naming.EndsWith "Exception" _ -> makeIdentExpr "Exception" |> Some
| _ -> None

let tryConstructor com (ent: Entity) =
if FSharp2Fable.Util.isReplacementCandidate ent.Ref then
tryEntityIdent com (ent.FullName |> Naming.toPythonNaming)
else
match FSharp2Fable.Util.tryEntityIdentMaybeGlobalOrImported com ent with
| Some(IdentExpr ident) when ent.IsFSharpUnion ->
// For F# union types, the base class is prefixed with underscore (_UnionName)
// This is needed for both reflection (base class has cases() method) and
// type annotations (self inside base class methods)
Some(IdentExpr { ident with Name = "_" + ident.Name })
| other -> other

let constructor com ent =
match tryConstructor com ent with
| Some e -> e
| None -> $"Cannot find %s{ent.FullName} constructor" |> addErrorAndReturnNull com [] None

let tryOp com r t op args =
Helper.LibCall(com, "option", "tryOp", t, op :: args, ?loc = r)

Expand All @@ -871,6 +898,7 @@ let rec defaultof com ctx r t =
| Nullable _ -> Value(Null t, r)
| Tuple(args, true) -> NewTuple(args |> List.map (defaultof com ctx r), true) |> makeValue None
| Boolean
| Char
| Number _
| Builtin BclTimeSpan
| Builtin BclDateTime
Expand All @@ -879,11 +907,7 @@ let rec defaultof com ctx r t =
| DeclaredType(ent, _) ->
let ent = com.GetEntity(ent)
// TODO: For BCL types we cannot access the constructor, raise error or warning?
if ent.IsValueType then
tryConstructor com ent
else
None
|> Option.map (fun e -> Helper.ConstructorCall(e, t, []))
tryZeroValueTypeConstructor com ctx t ent
|> Option.defaultWith (fun () -> Null t |> makeValue None)
// TODO: Fail (or raise warning) if this is an unresolved generic parameter?
| _ -> Null t |> makeValue None
Expand Down
57 changes: 28 additions & 29 deletions src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,30 @@ let constructor com ent =
| Some e -> e
| None -> $"Cannot find %s{ent.FullName} constructor" |> addErrorAndReturnNull com [] None

let rec private getZero (com: ICompiler) (ctx: Context) (t: Type) =
/// For user-defined value types (structs), returns a constructor call initialized with
/// a zero value for each instance field. Only instance fields are constructor parameters;
/// static fields (from `static let` bindings) are excluded to avoid deep recursion through
/// static field type graphs. Returns None if no constructor is available or the type is
/// not a value type.
let rec private tryZeroValueTypeConstructor (com: ICompiler) (ctx: Context) (t: Type) (ent: Entity) =
if ent.IsValueType then
tryConstructor com ent
|> Option.map (fun e ->
let args =
ent.FSharpFields
|> List.choose (fun f ->
if f.IsStatic then
None
else
getZero com ctx f.FieldType |> Some
)

Helper.ConstructorCall(e, t, args)
)
else
None

and private getZero (com: ICompiler) (ctx: Context) (t: Type) =
match t with
| Boolean -> makeBoolConst false
| Char -> makeCharConst '\000'
Expand All @@ -734,29 +757,9 @@ let rec private getZero (com: ICompiler) (ctx: Context) (t: Type) =
| Builtin(BclKeyValuePair(k, v)) -> makeTuple None true [ getZero com ctx k; getZero com ctx v ]
| ListSingleton(CustomOp com ctx None t "get_Zero" [] e) -> e
| DeclaredType(entRef, _) ->
let ent = com.GetEntity(entRef)
// For user-defined value types (structs), call the constructor with zero values
// for each instance field so Array.zeroCreate produces properly initialized instances
// instead of null. Only instance fields are constructor parameters; static fields
// (from `static let` bindings) are not, and iterating them could cause deep recursion
// through static field type graphs.
if ent.IsValueType then
tryConstructor com ent
|> Option.map (fun e ->
let args =
ent.FSharpFields
|> List.choose (fun f ->
if f.IsStatic then
None
else
getZero com ctx f.FieldType |> Some
)

Helper.ConstructorCall(e, t, args)
)
|> Option.defaultValue (Value(Null Any, None)) // null
else
Value(Null Any, None) // null
com.GetEntity(entRef)
|> tryZeroValueTypeConstructor com ctx t
|> Option.defaultValue (Value(Null Any, None)) // null
| _ -> Value(Null Any, None) // null

let getOne (com: ICompiler) (ctx: Context) (t: Type) =
Expand Down Expand Up @@ -1008,11 +1011,7 @@ let rec defaultof (com: ICompiler) (ctx: Context) r t =
| DeclaredType(entRef, _) ->
let ent = com.GetEntity(entRef)
// TODO: For BCL types we cannot access the constructor, raise error or warning?
if ent.IsValueType then
tryConstructor com ent
else
None
|> Option.map (fun e -> Helper.ConstructorCall(e, t, []))
tryZeroValueTypeConstructor com ctx t ent
|> Option.defaultWith (fun () ->
// Null t |> makeValue None
Helper.LibCall(com, "Util", "defaultOf", t, [], ?loc = r)
Expand Down
21 changes: 21 additions & 0 deletions tests/Js/Main/TypeTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,21 @@ type ClassWithDefaultValueStaticFields() =
static member Decimal = ClassWithDefaultValueStaticFields.decimal
static member StructTuple2 = ClassWithDefaultValueStaticFields.structTuple2

[<Struct>]
type NestedStruct =

val A : float
val B : bool

new (a, b) = { A = a; B = b }

[<Struct>]
type TopLevelStruct =

val A : float
val B : NestedStruct

new (a, b) = { A = a; B = b }

type Default1 = int

Expand Down Expand Up @@ -1600,4 +1615,10 @@ let tests =
ClassWithDefaultValueStaticFields.Decimal |> equal 0M

ClassWithDefaultValueStaticFields.IncrCount() |> equal 1

testCase "Unchecked.defaultof works for fields on structs" <| fun () ->
let top = TopLevelStruct()
top.A |> equal 0
top.B.A |> equal 0
top.B.B |> equal false
]
1 change: 1 addition & 0 deletions tests/Python/TestMisc.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,7 @@
Unchecked.defaultof<int64> |> equal 0L
Unchecked.defaultof<bigint> |> equal 0I
Unchecked.defaultof<decimal> |> equal 0M
Unchecked.defaultof<char> |> equal '\u0000'
Unchecked.defaultof<DateTime> |> equal DateTime.MinValue
Unchecked.defaultof<TimeSpan> |> equal (TimeSpan.FromMilliseconds 0.)
Unchecked.defaultof<bool> |> equal false
Expand Down Expand Up @@ -1137,7 +1138,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 1141 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 1141 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 1141 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 1141 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 1141 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

Check warning on line 1141 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
| _ -> x <- "?"
equal "3" x

Expand Down
19 changes: 19 additions & 0 deletions tests/Python/TestType.fs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,18 @@
[<DefaultValue>]
val mutable ObjValue: System.Collections.Generic.Dictionary<string, string>

[<Struct>]
type NestedStruct =
val A: float
val B: bool
new(a, b) = { A = a; B = b }

[<Struct>]
type TopLevelStruct =
val A: float
val B: NestedStruct
new(a, b) = { A = a; B = b }

type Default1 = int

type Distinct1 =
Expand Down Expand Up @@ -570,7 +582,7 @@

type IndexedProps(v: int) =
let mutable v = v
member _.Item with get (v2: int) = v + v2 and set v2 (s: string) = v <- v2 + int s

Check warning on line 585 in tests/Python/TestType.fs

View workflow job for this annotation

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

An indexed property's getter and setter must have the same type. Property 'Item' has getter of type 'int' but setter of type 'string'.

Check warning on line 585 in tests/Python/TestType.fs

View workflow job for this annotation

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

An indexed property's getter and setter must have the same type. Property 'Item' has getter of type 'int' but setter of type 'string'.

Check warning on line 585 in tests/Python/TestType.fs

View workflow job for this annotation

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

An indexed property's getter and setter must have the same type. Property 'Item' has getter of type 'int' but setter of type 'string'.

Check warning on line 585 in tests/Python/TestType.fs

View workflow job for this annotation

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

An indexed property's getter and setter must have the same type. Property 'Item' has getter of type 'int' but setter of type 'string'.

Check warning on line 585 in tests/Python/TestType.fs

View workflow job for this annotation

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

An indexed property's getter and setter must have the same type. Property 'Item' has getter of type 'int' but setter of type 'string'.

Check warning on line 585 in tests/Python/TestType.fs

View workflow job for this annotation

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

An indexed property's getter and setter must have the same type. Property 'Item' has getter of type 'int' but setter of type 'string'.
member _.Item with get (v2: float) = float v + v2 / 2.

[<Interface>]
Expand Down Expand Up @@ -1757,3 +1769,10 @@

let result3 = getTwoValues<TestTypeB, TestTypeC>()
result3 |> equal ("B", "C")

[<Fact>]
let ``test Unchecked.defaultof works for fields on structs`` () =
let top = TopLevelStruct()
top.A |> equal 0
top.B.A |> equal 0
top.B.B |> equal false
1 change: 1 addition & 0 deletions tests/Rust/tests/src/MiscTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@
Unchecked.defaultof<int64> |> equal 0L
Unchecked.defaultof<bigint> |> equal 0I
Unchecked.defaultof<decimal> |> equal 0M
Unchecked.defaultof<char> |> equal '\u0000'
Unchecked.defaultof<DateTime> |> equal DateTime.MinValue
Unchecked.defaultof<TimeSpan> |> equal (TimeSpan.FromMilliseconds 0.)
Unchecked.defaultof<bool> |> equal false
Expand Down Expand Up @@ -1234,7 +1235,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 1238 in tests/Rust/tests/src/MiscTests.fs

View workflow job for this annotation

GitHub Actions / analyzers (src/Fable.Transforms/Rust/AST/Rust.AST.fsproj)

This rule will never be matched

Check warning on line 1238 in tests/Rust/tests/src/MiscTests.fs

View workflow job for this annotation

GitHub Actions / build-rust (default)

This rule will never be matched

Check warning on line 1238 in tests/Rust/tests/src/MiscTests.fs

View workflow job for this annotation

GitHub Actions / build-rust (no_std)

This rule will never be matched

Check warning on line 1238 in tests/Rust/tests/src/MiscTests.fs

View workflow job for this annotation

GitHub Actions / build-rust (threaded)

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

Expand Down
Loading
Loading