Skip to content

Commit d538b68

Browse files
authored
[JS/TS] Fix Array.zeroCreate producing null for user-defined struct (value type) elements instead of a default-initialized instance (#4423)
1 parent 396c4b5 commit d538b68

4 files changed

Lines changed: 85 additions & 48 deletions

File tree

src/Fable.Cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
* [JS/TS] Fix `Array.zeroCreate` producing `null` for user-defined struct (value type) elements instead of a default-initialized instance (by @MangelMaxime)
1213
* [All] Fix interpolated string holes missing format specifiers in State.fs and Python/Replacements.fs (code scanning alerts 1144, 1145, 1512)
1314
* [Rust] Replace unsafe `.IsSome && .Value` option pattern with `Option.exists` in Fable2Rust.fs (code scanning alert 1125)
1415
* [JS/TS] Fix `Unchecked.defaultof<char>` being emitted as `null` instead of `'\0'` (by @MangelMaxime)

src/Fable.Compiler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
* [JS/TS] Fix `Array.zeroCreate` producing `null` for user-defined struct (value type) elements instead of a default-initialized instance (by @MangelMaxime)
1213
* [All] Fix interpolated string holes missing format specifiers in State.fs and Python/Replacements.fs (code scanning alerts 1144, 1145, 1512)
1314
* [Rust] Replace unsafe `.IsSome && .Value` option pattern with `Option.exists` in Fable2Rust.fs (code scanning alert 1125)
1415
* [JS/TS] Fix `Unchecked.defaultof<char>` being emitted as `null` instead of `'\0'` (by @MangelMaxime)

src/Fable.Transforms/Replacements.fs

Lines changed: 72 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,54 @@ let makeHashSet (com: ICompiler) ctx r t sourceSeq =
673673
makeEqualityComparer com ctx key |> makeHashSetWithComparer com r t sourceSeq
674674
| _ -> Helper.GlobalCall("Set", t, [ sourceSeq ], isConstructor = true, ?loc = r)
675675

676-
let rec getZero (com: ICompiler) (ctx: Context) (t: Type) =
676+
let tryEntityIdent (com: Compiler) entFullName =
677+
match entFullName with
678+
| BuiltinDefinition BclDateOnly
679+
| BuiltinDefinition BclDateTime
680+
| BuiltinDefinition BclDateTimeOffset -> makeIdentExpr "Date" |> Some
681+
| BuiltinDefinition BclTimer -> makeImportLib com Any "default" "Timer" |> Some
682+
| BuiltinDefinition(FSharpReference _) -> makeImportLib com Any "FSharpRef" "Types" |> Some
683+
| BuiltinDefinition(FSharpResult _) -> makeImportLib com Any "FSharpResult$2" "Result" |> Some
684+
| BuiltinDefinition(FSharpChoice genArgs) ->
685+
let membName = $"FSharpChoice${List.length genArgs}"
686+
makeImportLib com Any membName "Choice" |> Some
687+
// | BuiltinDefinition BclGuid -> jsTypeof "string" expr
688+
// | BuiltinDefinition BclTimeSpan -> jsTypeof "number" expr
689+
// | BuiltinDefinition BclHashSet _ -> fail "MutableSet" // TODO:
690+
// | BuiltinDefinition BclDictionary _ -> fail "MutableMap" // TODO:
691+
// | BuiltinDefinition BclKeyValuePair _ -> fail "KeyValuePair" // TODO:
692+
// | BuiltinDefinition FSharpSet _ -> fail "Set" // TODO:
693+
// | BuiltinDefinition FSharpMap _ -> fail "Map" // TODO:
694+
| Types.matchFail -> makeImportLib com Any "MatchFailureException" "Types" |> Some
695+
| Types.exception_ -> makeImportLib com Any "Exception" "Util" |> Some
696+
| "System.OperationCanceledException" -> makeImportLib com Any "OperationCanceledException" "AsyncBuilder" |> Some
697+
| "System.Collections.Generic.KeyNotFoundException" ->
698+
makeImportLib com Any "KeyNotFoundException" "System.Collections.Generic"
699+
|> Some
700+
| BuiltinSystemException entName -> makeImportLib com Any entName "System" |> Some
701+
// | Naming.EndsWith "Exception" _ -> makeImportLib com Any "Exception" "Util" |> Some
702+
| Types.attribute -> makeImportLib com Any "Attribute" "Types" |> Some
703+
| "System.Uri" -> makeImportLib com Any "Uri" "Uri" |> Some
704+
| "Microsoft.FSharp.Control.FSharpAsyncReplyChannel`1" ->
705+
makeImportLib com Any "AsyncReplyChannel" "AsyncBuilder" |> Some
706+
| "Microsoft.FSharp.Control.FSharpEvent`1" -> makeImportLib com Any "Event" "Event" |> Some
707+
| "Microsoft.FSharp.Control.FSharpEvent`2" -> makeImportLib com Any "Event$2" "Event" |> Some
708+
| "Microsoft.FSharp.Core.CompilerServices.ListCollector`1" ->
709+
makeImportLib com Any "ListCollector$1" "FSharp.Core.CompilerServices" |> Some
710+
| _ -> None
711+
712+
let tryConstructor com (ent: Entity) =
713+
if FSharp2Fable.Util.isReplacementCandidate ent.Ref then
714+
tryEntityIdent com ent.FullName
715+
else
716+
FSharp2Fable.Util.tryEntityIdentMaybeGlobalOrImported com ent
717+
718+
let constructor com ent =
719+
match tryConstructor com ent with
720+
| Some e -> e
721+
| None -> $"Cannot find %s{ent.FullName} constructor" |> addErrorAndReturnNull com [] None
722+
723+
let rec private getZero (com: ICompiler) (ctx: Context) (t: Type) =
677724
match t with
678725
| Boolean -> makeBoolConst false
679726
| Char -> makeCharConst '\000'
@@ -686,6 +733,30 @@ let rec getZero (com: ICompiler) (ctx: Context) (t: Type) =
686733
| Builtin(FSharpSet genArg) as t -> makeSet com ctx None t "Empty" [] genArg
687734
| Builtin(BclKeyValuePair(k, v)) -> makeTuple None true [ getZero com ctx k; getZero com ctx v ]
688735
| ListSingleton(CustomOp com ctx None t "get_Zero" [] e) -> e
736+
| DeclaredType(entRef, _) ->
737+
let ent = com.GetEntity(entRef)
738+
// For user-defined value types (structs), call the constructor with zero values
739+
// for each instance field so Array.zeroCreate produces properly initialized instances
740+
// instead of null. Only instance fields are constructor parameters; static fields
741+
// (from `static let` bindings) are not, and iterating them could cause deep recursion
742+
// through static field type graphs.
743+
if ent.IsValueType then
744+
tryConstructor com ent
745+
|> Option.map (fun e ->
746+
let args =
747+
ent.FSharpFields
748+
|> List.choose (fun f ->
749+
if f.IsStatic then
750+
None
751+
else
752+
getZero com ctx f.FieldType |> Some
753+
)
754+
755+
Helper.ConstructorCall(e, t, args)
756+
)
757+
|> Option.defaultValue (Value(Null Any, None)) // null
758+
else
759+
Value(Null Any, None) // null
689760
| _ -> Value(Null Any, None) // null
690761

691762
let getOne (com: ICompiler) (ctx: Context) (t: Type) =
@@ -911,53 +982,6 @@ let injectArg (com: ICompiler) (ctx: Context) r moduleName methName (genArgs: Ty
911982
| None -> args
912983
| Some injectInfo -> injectArgInner args injectInfo
913984

914-
let tryEntityIdent (com: Compiler) entFullName =
915-
match entFullName with
916-
| BuiltinDefinition BclDateOnly
917-
| BuiltinDefinition BclDateTime
918-
| BuiltinDefinition BclDateTimeOffset -> makeIdentExpr "Date" |> Some
919-
| BuiltinDefinition BclTimer -> makeImportLib com Any "default" "Timer" |> Some
920-
| BuiltinDefinition(FSharpReference _) -> makeImportLib com Any "FSharpRef" "Types" |> Some
921-
| BuiltinDefinition(FSharpResult _) -> makeImportLib com Any "FSharpResult$2" "Result" |> Some
922-
| BuiltinDefinition(FSharpChoice genArgs) ->
923-
let membName = $"FSharpChoice${List.length genArgs}"
924-
makeImportLib com Any membName "Choice" |> Some
925-
// | BuiltinDefinition BclGuid -> jsTypeof "string" expr
926-
// | BuiltinDefinition BclTimeSpan -> jsTypeof "number" expr
927-
// | BuiltinDefinition BclHashSet _ -> fail "MutableSet" // TODO:
928-
// | BuiltinDefinition BclDictionary _ -> fail "MutableMap" // TODO:
929-
// | BuiltinDefinition BclKeyValuePair _ -> fail "KeyValuePair" // TODO:
930-
// | BuiltinDefinition FSharpSet _ -> fail "Set" // TODO:
931-
// | BuiltinDefinition FSharpMap _ -> fail "Map" // TODO:
932-
| Types.matchFail -> makeImportLib com Any "MatchFailureException" "Types" |> Some
933-
| Types.exception_ -> makeImportLib com Any "Exception" "Util" |> Some
934-
| "System.OperationCanceledException" -> makeImportLib com Any "OperationCanceledException" "AsyncBuilder" |> Some
935-
| "System.Collections.Generic.KeyNotFoundException" ->
936-
makeImportLib com Any "KeyNotFoundException" "System.Collections.Generic"
937-
|> Some
938-
| BuiltinSystemException entName -> makeImportLib com Any entName "System" |> Some
939-
// | Naming.EndsWith "Exception" _ -> makeImportLib com Any "Exception" "Util" |> Some
940-
| Types.attribute -> makeImportLib com Any "Attribute" "Types" |> Some
941-
| "System.Uri" -> makeImportLib com Any "Uri" "Uri" |> Some
942-
| "Microsoft.FSharp.Control.FSharpAsyncReplyChannel`1" ->
943-
makeImportLib com Any "AsyncReplyChannel" "AsyncBuilder" |> Some
944-
| "Microsoft.FSharp.Control.FSharpEvent`1" -> makeImportLib com Any "Event" "Event" |> Some
945-
| "Microsoft.FSharp.Control.FSharpEvent`2" -> makeImportLib com Any "Event$2" "Event" |> Some
946-
| "Microsoft.FSharp.Core.CompilerServices.ListCollector`1" ->
947-
makeImportLib com Any "ListCollector$1" "FSharp.Core.CompilerServices" |> Some
948-
| _ -> None
949-
950-
let tryConstructor com (ent: Entity) =
951-
if FSharp2Fable.Util.isReplacementCandidate ent.Ref then
952-
tryEntityIdent com ent.FullName
953-
else
954-
FSharp2Fable.Util.tryEntityIdentMaybeGlobalOrImported com ent
955-
956-
let constructor com ent =
957-
match tryConstructor com ent with
958-
| Some e -> e
959-
| None -> $"Cannot find %s{ent.FullName} constructor" |> addErrorAndReturnNull com [] None
960-
961985
let tryOp com r t op args =
962986
Helper.LibCall(com, "Option", "tryOp", t, op :: args, ?loc = r)
963987

tests/Js/Main/ArrayTests.fs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ type Things =
4545

4646
type Animal = Duck of int | Dog of int
4747

48+
[<Struct>]
49+
type MyStruct(value: int, flag: bool) =
50+
member _.Value = value
51+
member _.Flag = flag
52+
4853
let tests =
4954
testList "Arrays" [
5055
testCase "Pattern matching with arrays works" <| fun () ->
@@ -240,6 +245,12 @@ let tests =
240245
equal 0. a.[1].Key
241246
equal false a.[2].Value
242247

248+
testCase "Array.zeroCreate works with struct" <| fun () ->
249+
let arr = Array.zeroCreate<MyStruct> 3
250+
equal 0 arr.[0].Value
251+
equal false arr.[0].Flag
252+
equal 3 arr.Length
253+
243254
testCase "Array.create works" <| fun () ->
244255
let xs = Array.create 2 5
245256
equal 2 xs.Length

0 commit comments

Comments
 (0)