Skip to content

Commit f0210a7

Browse files
authored
[JS/TS] Fix static val mutable fields declared with [<DefaultValue>] not being zero-initialized (#4418)
1 parent 364c5bc commit f0210a7

10 files changed

Lines changed: 214 additions & 6 deletions

File tree

src/Fable.AST/Fable.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type Field =
7171
abstract IsMutable: bool
7272
abstract IsStatic: bool
7373
abstract LiteralValue: obj option
74+
abstract HasDefaultValueAttribute: bool
7475

7576
type UnionCase =
7677
abstract Name: string

src/Fable.Cli/CHANGELOG.md

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

1010
### Fixed
1111

12+
* [JS/TS] Fix `Unchecked.defaultof<char>` being emitted as `null` instead of `'\0'` (by @MangelMaxime)
13+
* [JS/TS] Fix `static val mutable` fields declared with `[<DefaultValue>]` not being zero-initialized (fix #2739) (by @MangelMaxime)
1214
* [JS/TS/Python] Fix record/struct types augmented with `static let` or `static member val` generating extra constructor parameters for each static field, causing constructor arguments to be assigned to wrong slots (by @MangelMaxime)
1315
* [TS] Annotate `System.Collections.Generic.IList<T>` as `MutableArray<T>` (by @MangelMaxime)
1416
* [JS/TS] Fix `ResizeArray` index getter/setter not throwing `IndexOutOfRangeException` when index is out of bounds (fix #3812) (by @MangelMaxime)

src/Fable.Compiler/CHANGELOG.md

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

1010
### Fixed
1111

12+
* [JS/TS] Fix `Unchecked.defaultof<char>` being emitted as `null` instead of `'\0'` (by @MangelMaxime)
13+
* [JS/TS] Fix `static val mutable` fields declared with `[<DefaultValue>]` not being zero-initialized (fix #2739) (by @MangelMaxime)
1214
* [JS/TS/Python] Fix record/struct types augmented with `static let` or `static member val` generating extra constructor parameters for each static field, causing constructor arguments to be assigned to wrong slots (by @MangelMaxime)
1315
* [TS] Annotate `System.Collections.Generic.IList<T>` as `MutableArray<T>` (by @MangelMaxime)
1416
* [JS/TS] Fix `ResizeArray` index getter/setter not throwing `IndexOutOfRangeException` when index is out of bounds (fix #3812) (by @MangelMaxime)

src/Fable.Transforms/FSharp2Fable.Util.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ type FsField(fi: FSharpField) =
7676
member _.IsStatic = fi.IsStatic
7777
member _.IsMutable = fi.IsMutable
7878

79+
member _.HasDefaultValueAttribute =
80+
fi.FieldAttributes |> Helpers.hasAttrib Atts.defaultValue
81+
7982
static member FSharpFieldName(fi: FSharpField) =
8083
let rec countConflictingCases acc (ent: FSharpEntity) (name: string) =
8184
match TypeHelpers.tryGetBaseEntity ent with

src/Fable.Transforms/Fable2Babel.fs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3412,6 +3412,30 @@ but thanks to the optimisation done below we get
34123412
let sanitizeName fieldName =
34133413
fieldName |> Naming.sanitizeJsIdentForbiddenChars |> Naming.checkJsKeywords
34143414

3415+
[<RequireQualifiedAccess>]
3416+
module DefaultValue =
3417+
3418+
open Replacements.Util
3419+
open Fable.Transforms
3420+
3421+
/// Mirrors `Fable.Transforms.JS.Replacements.defaultof` for the types we can handle
3422+
/// without the full FSharp2Fable compiler context (which that function requires).
3423+
let rec forType (com: IBabelCompiler) ctx (t: Fable.Type) : Expression =
3424+
match t with
3425+
| Fable.Nullable _ -> Expression.nullLiteral ()
3426+
// Struct tuples are value types — initialize each element to its zero
3427+
| Fable.Tuple(args, true) -> Expression.arrayExpression (args |> List.map (forType com ctx) |> List.toArray)
3428+
| Fable.Boolean -> Expression.booleanLiteral false
3429+
| Fable.Char -> Expression.stringLiteral "\u0000"
3430+
| Fable.Number(kind, uom) ->
3431+
com.TransformAsExpr(ctx, Fable.NumberConstant(Fable.NumberValue.GetZero kind, uom) |> makeValue None)
3432+
| Builtin(BclTimeSpan | BclTimeOnly) -> Expression.numericLiteral 0
3433+
| Builtin BclGuid -> Expression.stringLiteral "00000000-0000-0000-0000-000000000000"
3434+
| Builtin BclDateTime -> libCall com ctx None "Date" "minValue" [] []
3435+
| Builtin BclDateTimeOffset -> libCall com ctx None "DateOffset" "minValue" [] []
3436+
| Builtin BclDateOnly -> libCall com ctx None "DateOnly" "minValue" [] []
3437+
| _ -> libCall com ctx None "Util" "defaultOf" [] []
3438+
34153439
let getEntityFieldsAsIdents (ent: Fable.Entity) =
34163440
ent.FSharpFields
34173441
|> List.choose (fun field ->
@@ -3533,6 +3557,45 @@ but thanks to the optimisation done below we get
35333557
None
35343558
|> declareClassWithParams com ctx ent entName doc consArgs [||] consBody superClass classMembers
35353559

3560+
/// Emits an IIFE that zero-initializes all static [<DefaultValue>] fields of a class.
3561+
///
3562+
/// ```
3563+
/// [<DefaultValue>]
3564+
/// static val mutable MyField: int
3565+
/// ```
3566+
///
3567+
/// This mirrors the pattern used for `static let mutable` bindings, which FCS compiles
3568+
/// to a `.cctor` that Fable emits as an IIFE.
3569+
let declareDefaultValueStaticFieldInits
3570+
(com: IBabelCompiler)
3571+
ctx
3572+
(ent: Fable.Entity)
3573+
(entName: string)
3574+
: ModuleDeclaration list
3575+
=
3576+
let defaultValueFields =
3577+
ent.FSharpFields
3578+
|> List.filter (fun f -> f.IsStatic && f.HasDefaultValueAttribute)
3579+
3580+
if defaultValueFields.IsEmpty then
3581+
[]
3582+
else
3583+
let classIdent = Expression.identifier entName
3584+
3585+
let assignments =
3586+
defaultValueFields
3587+
|> List.map (fun field ->
3588+
let left = get None classIdent field.Name
3589+
let right = DefaultValue.forType com ctx field.FieldType
3590+
assign None left right |> ExpressionStatement
3591+
)
3592+
|> List.toArray
3593+
3594+
let iife =
3595+
Expression.callExpression (Expression.functionExpression ([||], BlockStatement(assignments)), [||])
3596+
3597+
[ iife |> ExpressionStatement |> PrivateModuleDeclaration ]
3598+
35363599
let declareTypeReflection (com: IBabelCompiler) ctx (ent: Fable.Entity) entName : ModuleDeclaration =
35373600
let ta =
35383601
if com.IsTypeScript then
@@ -3571,12 +3634,14 @@ but thanks to the optimisation done below we get
35713634
let typeDeclaration =
35723635
declareClass com ctx ent entName doc consArgs consBody baseExpr classMembers
35733636
3637+
let fieldInits = declareDefaultValueStaticFieldInits com ctx ent entName
3638+
35743639
if com.Options.NoReflection then
3575-
[ typeDeclaration ]
3640+
[ typeDeclaration; yield! fieldInits ]
35763641
else
35773642
let reflectionDeclaration = declareTypeReflection com ctx ent entName
35783643
3579-
[ typeDeclaration; reflectionDeclaration ]
3644+
[ typeDeclaration; reflectionDeclaration; yield! fieldInits ]
35803645
35813646
let hasAttribute fullName (atts: Fable.Attribute seq) =
35823647
atts |> Seq.exists (fun att -> att.Entity.FullName = fullName)

src/Fable.Transforms/Replacements.fs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,10 +676,10 @@ let makeHashSet (com: ICompiler) ctx r t sourceSeq =
676676
let rec getZero (com: ICompiler) (ctx: Context) (t: Type) =
677677
match t with
678678
| Boolean -> makeBoolConst false
679-
| Char
679+
| Char -> makeCharConst '\000'
680680
| String -> makeStrConst "" // TODO: Use null for string?
681681
| Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue None
682-
| Builtin(BclTimeSpan | BclTimeOnly) -> makeIntConst 0 // TODO: Type cast
682+
| Builtin(BclTimeSpan | BclTimeOnly) -> TypeCast(makeIntConst 0, t)
683683
| Builtin BclDateTime as t -> Helper.LibCall(com, "Date", "minValue", t, [])
684684
| Builtin BclDateTimeOffset as t -> Helper.LibCall(com, "DateOffset", "minValue", t, [])
685685
| Builtin BclDateOnly as t -> Helper.LibCall(com, "DateOnly", "minValue", t, [])
@@ -993,6 +993,7 @@ let rec defaultof (com: ICompiler) (ctx: Context) r t =
993993
// Null t |> makeValue None
994994
Helper.LibCall(com, "Util", "defaultOf", t, [], ?loc = r)
995995
)
996+
| Char -> makeCharConst '\000'
996997
// TODO: Fail (or raise warning) if this is an unresolved generic parameter?
997998
| _ ->
998999
// Null t |> makeValue None

src/Fable.Transforms/Transforms.Util.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ module Atts =
4343
[<Literal>]
4444
let mangle = "Fable.Core.MangleAttribute" // typeof<Fable.Core.MangleAttribute>.FullName
4545

46+
[<Literal>]
47+
let defaultValue = "Microsoft.FSharp.Core.DefaultValueAttribute" // typeof<Microsoft.FSharp.Core.DefaultValueAttribute>.FullName
48+
4649
[<Literal>]
4750
let attachMembers = "Fable.Core.AttachMembersAttribute"
4851

@@ -1042,6 +1045,7 @@ module AST =
10421045

10431046
let makeBoolConst (x: bool) = BoolConstant x |> makeValue None
10441047
let makeStrConst (x: string) = StringConstant x |> makeValue None
1048+
let makeCharConst (x: char) = CharConstant x |> makeValue None
10451049

10461050
let makeIntConst (x: int) =
10471051
NumberConstant(NumberValue.Int32 x, NumberInfo.Empty) |> makeValue None

src/fable-library-ts/Reflection.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,8 +536,7 @@ export function createInstance(t: TypeInfo, consArgs?: any[]): any {
536536
case decimal_type.fullname:
537537
return new Decimal(0);
538538
case char_type.fullname:
539-
// Even though char is a value type, it's erased to string, and Unchecked.defaultof<char> is null
540-
return null;
539+
return "\0";
541540
default:
542541
throw new Exception(`Cannot access constructor of ${t.fullname}`);
543542
}

tests/Js/Main/MiscTests.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,7 @@ let tests =
11251125
Unchecked.defaultof<TimeSpan> |> equal (TimeSpan.FromMilliseconds 0.)
11261126
Unchecked.defaultof<bool> |> equal false
11271127
Unchecked.defaultof<string> |> equal null
1128+
Unchecked.defaultof<char> |> equal '\u0000'
11281129
Unchecked.defaultof<Guid> |> equal Guid.Empty
11291130
#if !FABLE_COMPILER_TYPESCRIPT
11301131
let x = ValueType()

tests/Js/Main/TypeTests.fs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,107 @@ type Test_TestTypeWithParameterizedUnitMeasure = {
329329
// [<DefaultValue>] val mutable StringValue: string
330330
// [<DefaultValue>] val mutable ObjValue: System.Collections.Generic.Dictionary<string, string>
331331

332+
[<AllowNullLiteral>]
333+
type MyUser =
334+
class end
335+
336+
// See #2739: static [<DefaultValue>] fields must be zero-initialized in JS/TS
337+
type ClassWithDefaultValueStaticFields() =
338+
339+
[<DefaultValue>]
340+
static val mutable private int: int
341+
[<DefaultValue>]
342+
static val mutable private bool: bool
343+
[<DefaultValue>]
344+
static val mutable private string: string
345+
[<DefaultValue>]
346+
static val mutable private guid: Guid
347+
[<DefaultValue>]
348+
static val mutable private char: char
349+
350+
[<DefaultValue>]
351+
static val mutable private dateTime: DateTime
352+
353+
[<DefaultValue>]
354+
static val mutable private timeOnly : TimeOnly
355+
[<DefaultValue>]
356+
static val mutable private dateOnly : DateOnly
357+
[<DefaultValue>]
358+
static val mutable private dateTimeOffset : DateTimeOffset
359+
[<DefaultValue>]
360+
static val mutable private int8 : int8
361+
[<DefaultValue>]
362+
static val mutable private uInt8 : uint8
363+
[<DefaultValue>]
364+
static val mutable private int16 : Int16
365+
[<DefaultValue>]
366+
static val mutable private uInt16 : UInt16
367+
[<DefaultValue>]
368+
static val mutable private int32 : Int32
369+
[<DefaultValue>]
370+
static val mutable private uInt32 : UInt32
371+
[<DefaultValue>]
372+
static val mutable private int64 : Int64
373+
[<DefaultValue>]
374+
static val mutable private uInt64 : UInt64
375+
[<DefaultValue>]
376+
static val mutable private bigInt : bigint
377+
[<DefaultValue>]
378+
static val mutable private nativeInt : nativeint
379+
[<DefaultValue>]
380+
static val mutable private uNativeInt : unativeint
381+
[<DefaultValue>]
382+
static val mutable private float32 : float32
383+
[<DefaultValue>]
384+
static val mutable private float64 : float
385+
[<DefaultValue>]
386+
static val mutable private decimal : Decimal
387+
388+
[<DefaultValue>]
389+
static val mutable private timeSpan: TimeSpan
390+
[<DefaultValue>]
391+
static val mutable private allowNullLiteralClass: MyUser
392+
[<DefaultValue>]
393+
static val mutable private nullableInt: Nullable<int>
394+
[<DefaultValue>]
395+
static val mutable private tuple2: TimeSpan * DateTime
396+
[<DefaultValue>]
397+
static val mutable private structTuple2: struct (TimeSpan * DateTime)
398+
399+
static member IncrCount() =
400+
ClassWithDefaultValueStaticFields.int <- ClassWithDefaultValueStaticFields.int + 1
401+
ClassWithDefaultValueStaticFields.int
402+
403+
static member Int = ClassWithDefaultValueStaticFields.int
404+
static member Bool = ClassWithDefaultValueStaticFields.bool
405+
static member String = ClassWithDefaultValueStaticFields.string
406+
static member Guid = ClassWithDefaultValueStaticFields.guid
407+
static member Char = ClassWithDefaultValueStaticFields.char
408+
static member TimeSpan = ClassWithDefaultValueStaticFields.timeSpan
409+
static member AllowNullLiteralClass = ClassWithDefaultValueStaticFields.allowNullLiteralClass
410+
static member NullableInt = ClassWithDefaultValueStaticFields.nullableInt
411+
static member Tuple2 = ClassWithDefaultValueStaticFields.tuple2
412+
static member DateTime = ClassWithDefaultValueStaticFields.dateTime
413+
static member TimeOnly = ClassWithDefaultValueStaticFields.timeOnly
414+
static member DateOnly = ClassWithDefaultValueStaticFields.dateOnly
415+
static member DateTimeOffset = ClassWithDefaultValueStaticFields.dateTimeOffset
416+
static member Int8 = ClassWithDefaultValueStaticFields.int8
417+
static member UInt8 = ClassWithDefaultValueStaticFields.uInt8
418+
static member Int16 = ClassWithDefaultValueStaticFields.int16
419+
static member UInt16 = ClassWithDefaultValueStaticFields.uInt16
420+
static member Int32 = ClassWithDefaultValueStaticFields.int32
421+
static member UInt32 = ClassWithDefaultValueStaticFields.uInt32
422+
static member Int64 = ClassWithDefaultValueStaticFields.int64
423+
static member UInt64 = ClassWithDefaultValueStaticFields.uInt64
424+
static member BigInt = ClassWithDefaultValueStaticFields.bigInt
425+
static member NativeInt = ClassWithDefaultValueStaticFields.nativeInt
426+
static member UNativeInt = ClassWithDefaultValueStaticFields.uNativeInt
427+
static member Float32 = ClassWithDefaultValueStaticFields.float32
428+
static member Float64 = ClassWithDefaultValueStaticFields.float64
429+
static member Decimal = ClassWithDefaultValueStaticFields.decimal
430+
static member StructTuple2 = ClassWithDefaultValueStaticFields.structTuple2
431+
432+
332433
type Default1 = int
333434

334435
type Distinct1 =
@@ -1470,4 +1571,33 @@ let tests =
14701571

14711572
let result3 = getTwoValues<TestTypeB, TestTypeC>()
14721573
result3 |> equal ("B", "C")
1574+
1575+
// See https://github.com/fable-compiler/Fable/issues/2739
1576+
testCase "Static [<DefaultValue>] fields are zero-initialized" <| fun () ->
1577+
ClassWithDefaultValueStaticFields.Int |> equal 0
1578+
ClassWithDefaultValueStaticFields.Bool |> equal false
1579+
ClassWithDefaultValueStaticFields.String |> equal null
1580+
ClassWithDefaultValueStaticFields.Guid |> equal Guid.Empty
1581+
ClassWithDefaultValueStaticFields.Char |> equal '\000'
1582+
ClassWithDefaultValueStaticFields.TimeSpan |> equal TimeSpan.Zero
1583+
ClassWithDefaultValueStaticFields.TimeOnly |> equal TimeOnly.MinValue
1584+
ClassWithDefaultValueStaticFields.DateOnly |> equal DateOnly.MinValue
1585+
ClassWithDefaultValueStaticFields.DateTime |> equal DateTime.MinValue
1586+
ClassWithDefaultValueStaticFields.DateTimeOffset |> equal DateTimeOffset.MinValue
1587+
ClassWithDefaultValueStaticFields.Int8 |> equal 0y
1588+
ClassWithDefaultValueStaticFields.UInt8 |> equal 0uy
1589+
ClassWithDefaultValueStaticFields.Int16 |> equal 0s
1590+
ClassWithDefaultValueStaticFields.UInt16 |> equal 0us
1591+
ClassWithDefaultValueStaticFields.Int32 |> equal 0
1592+
ClassWithDefaultValueStaticFields.UInt32 |> equal 0u
1593+
ClassWithDefaultValueStaticFields.Int64 |> equal 0L
1594+
ClassWithDefaultValueStaticFields.UInt64 |> equal 0UL
1595+
ClassWithDefaultValueStaticFields.BigInt |> equal 0I
1596+
ClassWithDefaultValueStaticFields.NativeInt |> equal 0n
1597+
ClassWithDefaultValueStaticFields.UNativeInt |> equal 0un
1598+
ClassWithDefaultValueStaticFields.Float32 |> equal 0.f
1599+
ClassWithDefaultValueStaticFields.Float64 |> equal 0.
1600+
ClassWithDefaultValueStaticFields.Decimal |> equal 0M
1601+
1602+
ClassWithDefaultValueStaticFields.IncrCount() |> equal 1
14731603
]

0 commit comments

Comments
 (0)