diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index b9ff3043a..6eb9b1733 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -850,6 +850,9 @@ module Helpers = // typ.TypeDefinition.DisplayName = "inref" || // typ.TypeDefinition.DisplayName = "outref") + let isInRefType (typ: FSharpType) = + typ.HasTypeDefinition && typ.TypeDefinition.DisplayName = "inref" + let isByRefValue (value: FSharpMemberOrFunctionOrValue) = // Value type "this" is passed as inref, so it has to be excluded // (Note: the non-abbreviated type of inref and outref is byref) diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 37e6eb62d..4521f3e4a 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -770,6 +770,26 @@ let private withDefaultValueFields (com: IFableCompiler) (ctx: Context) (fsType: else args +let private derefByRefCallee + (com: IFableCompiler) + (ctx: Context) + r + (callee: FSharpExpr) + (calleeType: FSharpType) + (transformedCallee: Fable.Expr) + = + match callee with + | FSharpExprPatterns.Value var when + com.Options.Language <> Rust + && isByRefType callee.Type + && isByRefValue var + // An inref argument is passed as the value itself when its source is immutable + && not (isInRefType var.FullType) + && not (var.IsCompilerGenerated && var.CompiledName = "inputRecord") + -> + Replacements.Api.getRefCell com r (makeType ctx.GenericArgs calleeType) transformedCallee + | _ -> transformedCallee + let rec private transformDecisionTargets (com: IFableCompiler) (ctx: Context) @@ -1100,6 +1120,17 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs return (expr, bindings) ||> List.fold (fun e (i, v) -> Fable.Let(i, v, e)) + | FSharpExprPatterns.AddressOf inner when + var.IsCompilerGenerated + && var.CompiledName = "inputRecord" + && com.Options.Language <> Rust + -> + let! value = transformExpr com ctx [] inner + let ctx, ident = putIdentInScope com ctx var (Some value) + let ident = { ident with Type = value.Type } + let! body = transformExpr com ctx [] body + return Fable.Let(ident, value, body) + | _ when isInline var -> let ctx = { ctx with ScopeInlineValues = (var, value) :: ctx.ScopeInlineValues } @@ -1402,7 +1433,14 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs // Getters and Setters | FSharpExprPatterns.AnonRecordGet(callee, calleeType, fieldIndex) -> let r = makeRangeFrom fsExpr - let! callee = transformExpr com ctx [] callee + + let callee = + match callee with + | IgnoreAddressOf inner when com.Options.Language <> Rust -> inner + | _ -> callee + + let! transformedCallee = transformExpr com ctx [] callee + let callee = derefByRefCallee com ctx r callee calleeType transformedCallee let fieldName = calleeType.AnonRecordTypeDetails.SortedFieldNames[fieldIndex] @@ -1414,7 +1452,19 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | FSharpExprPatterns.FSharpFieldGet(callee, calleeType, field) -> let r = makeRangeFrom fsExpr - let! callee = transformCallee com ctx callee calleeType + + let callee = + match callee with + | Some(IgnoreAddressOf inner) when com.Options.Language <> Rust -> Some inner + | callee -> callee + + let! transformedCallee = transformCallee com ctx callee calleeType + + let callee = + match callee with + | Some callee -> derefByRefCallee com ctx r callee calleeType transformedCallee + | None -> transformedCallee + // let typ = makeType ctx.GenericArgs fsExpr.Type // Doesn't always work let typ = resolveFieldType ctx calleeType field.FieldType diff --git a/tests/Beam/RecordTests.fs b/tests/Beam/RecordTests.fs index 8d13df653..8c408a97f 100644 --- a/tests/Beam/RecordTests.fs +++ b/tests/Beam/RecordTests.fs @@ -48,6 +48,38 @@ type IntegrityRecord = override this.ToString() = this.Level +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +[] +type StructRecordOrdered = { OrdA: int; OrdB: int; OrdC: int } + +type ByRefClassRecord = { BcP: int; BcQ: int } + +type ByRefClass = + val mutable BkF: int + val BkV: int + new() = { BkF = 0; BkV = 3 } + +let sumStructRecordByRef (r: byref) = r.OrdA + r.OrdC + +let copyStructRecordByRef (r: byref) = { r with OrdB = 99 } + +let sumClassRecordByRef (r: byref) = r.BcP + r.BcQ + +let setClassRecordOutRef (r: outref) = + r <- { BcP = 4; BcQ = 1 } + r.BcP + r.BcQ + +let sumClassByRef (r: byref) = r.BkF + r.BkV + +let sumAnonymousStructRecordByRef (x: byref) = x.AnX + x.AnY + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + [] let ``test simple record creation works`` () = let p = { Name = "Alice"; Age = 30 } @@ -311,3 +343,58 @@ let ``test Records with ref-type interior mutability`` () = x.MutRefValue |> equal "ab" x.MutRefValue <- x.MutRefValue + "c" x.MutRefValue |> equal "abc" + +[] +let ``test copy-and-update of a let mutable struct record keeps the unchanged fields`` () = + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + +[] +let ``test copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields`` () = + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + +[] +let ``test copy-and-update of a module-level mutable struct record keeps the unchanged fields`` () = + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + +[] +let ``test field reads and copy-and-update through a byref struct record parameter work`` () = + let mutable r = { OrdA = 1; OrdB = 2; OrdC = 3 } + sumStructRecordByRef &r |> equal 4 + let x = copyStructRecordByRef &r + (x.OrdA, x.OrdB, x.OrdC) |> equal (1, 99, 3) + +[] +let ``test field reads through a byref or outref to a reference type work`` () = + let mutable c = { BcP = 1; BcQ = 2 } + sumClassRecordByRef &c |> equal 3 + let mutable o = { BcP = 0; BcQ = 0 } + setClassRecordOutRef &o |> equal 5 + let mutable k = ByRefClass() + k.BkF <- 4 + sumClassByRef &k |> equal 7 + +[] +let ``test copy-and-update of a let mutable anonymous struct record keeps the unchanged fields`` () = + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) + +[] +let ``test field reads through a byref to an anonymous struct record work`` () = + let mutable a = struct {| AnX = 1; AnY = 2 |} + sumAnonymousStructRecordByRef &a |> equal 3 diff --git a/tests/Dart/src/RecordTests.fs b/tests/Dart/src/RecordTests.fs index 492b44ec1..04d12ebe7 100644 --- a/tests/Dart/src/RecordTests.fs +++ b/tests/Dart/src/RecordTests.fs @@ -58,6 +58,14 @@ type RecordShadowingFileScope = member this.Wrap() = { Value = this.Wrapped } member this.Add() = addOne this.addOne +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + let tests() = testCase "Anonymous records work" <| fun () -> let r = makeAnonRec() @@ -181,3 +189,32 @@ let tests() = let record = { Wrapped = 3; addOne = 7 } :> IWrap record.Wrap().Value |> equal 3 record.Add() |> equal 8 + + testCase "Copy-and-update of a let mutable struct record keeps the unchanged fields" <| fun () -> + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + + testCase "Copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields" <| fun () -> + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + + testCase "Copy-and-update of a module-level mutable struct record keeps the unchanged fields" <| fun () -> + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + + testCase "Copy-and-update of a let mutable anonymous struct record keeps the unchanged fields" <| fun () -> + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) diff --git a/tests/Js/Main/RecordTypeTests.fs b/tests/Js/Main/RecordTypeTests.fs index daabe9015..6b71bde75 100644 --- a/tests/Js/Main/RecordTypeTests.fs +++ b/tests/Js/Main/RecordTypeTests.fs @@ -70,6 +70,38 @@ let holdStructRecord (data: StructRecord) (path: int list) = Data = { data with Path = List.rev path } } +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +[] +type StructRecordOrdered = { OrdA: int; OrdB: int; OrdC: int } + +type ByRefClassRecord = { BcP: int; BcQ: int } + +type ByRefClass = + val mutable BkF: int + val BkV: int + new() = { BkF = 0; BkV = 3 } + +let sumStructRecordByRef (r: byref) = r.OrdA + r.OrdC + +let copyStructRecordByRef (r: byref) = { r with OrdB = 99 } + +let sumClassRecordByRef (r: byref) = r.BcP + r.BcQ + +let setClassRecordOutRef (r: outref) = + r <- { BcP = 4; BcQ = 1 } + r.BcP + r.BcQ + +let sumClassByRef (r: byref) = r.BkF + r.BkV + +let sumAnonymousStructRecordByRef (x: byref) = x.AnX + x.AnY + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + let tests = testList "RecordTypes" [ @@ -210,4 +242,52 @@ let tests = equal 1 a.xs.Count equal 1 b.xs.Count + testCase "Copy-and-update of a let mutable struct record keeps the unchanged fields" <| fun () -> + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + + testCase "Copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields" <| fun () -> + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + + testCase "Copy-and-update of a module-level mutable struct record keeps the unchanged fields" <| fun () -> + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + + testCase "Field reads and copy-and-update through a byref struct record parameter work" <| fun () -> + let mutable r = { OrdA = 1; OrdB = 2; OrdC = 3 } + sumStructRecordByRef &r |> equal 4 + let x = copyStructRecordByRef &r + (x.OrdA, x.OrdB, x.OrdC) |> equal (1, 99, 3) + + testCase "Field reads through a byref or outref to a reference type work" <| fun () -> + let mutable c = { BcP = 1; BcQ = 2 } + sumClassRecordByRef &c |> equal 3 + let mutable o = { BcP = 0; BcQ = 0 } + setClassRecordOutRef &o |> equal 5 + let mutable k = ByRefClass() + k.BkF <- 4 + sumClassByRef &k |> equal 7 + + testCase "Copy-and-update of a let mutable anonymous struct record keeps the unchanged fields" <| fun () -> + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) + + testCase "Field reads through a byref to an anonymous struct record work" <| fun () -> + let mutable a = struct {| AnX = 1; AnY = 2 |} + sumAnonymousStructRecordByRef &a |> equal 3 + ] diff --git a/tests/Python/TestRecordType.fs b/tests/Python/TestRecordType.fs index 8d9ef0139..08a158aaf 100644 --- a/tests/Python/TestRecordType.fs +++ b/tests/Python/TestRecordType.fs @@ -61,6 +61,38 @@ type StructRecord = { Size: int; Seed: int; Path: int list } type StructRecordHolder = { Tag: int; Data: StructRecord } +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +[] +type StructRecordOrdered = { OrdA: int; OrdB: int; OrdC: int } + +type ByRefClassRecord = { BcP: int; BcQ: int } + +type ByRefClass = + val mutable BkF: int + val BkV: int + new() = { BkF = 0; BkV = 3 } + +let sumStructRecordByRef (r: byref) = r.OrdA + r.OrdC + +let copyStructRecordByRef (r: byref) = { r with OrdB = 99 } + +let sumClassRecordByRef (r: byref) = r.BcP + r.BcQ + +let setClassRecordOutRef (r: outref) = + r <- { BcP = 4; BcQ = 1 } + r.BcP + r.BcQ + +let sumClassByRef (r: byref) = r.BkF + r.BkV + +let sumAnonymousStructRecordByRef (x: byref) = x.AnX + x.AnY + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + [] let ``test sprintf formats record fields with their F# names`` () = sprintf "%A" { Size = 1; Seed = 2; Path = [] } @@ -258,3 +290,58 @@ let ``test Copy-and-update of a struct record argument works`` () = equal 1 held.Data.Size equal 2 held.Data.Seed equal [ 8; 7 ] held.Data.Path + +[] +let ``test copy-and-update of a let mutable struct record keeps the unchanged fields`` () = + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + +[] +let ``test copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields`` () = + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + +[] +let ``test copy-and-update of a module-level mutable struct record keeps the unchanged fields`` () = + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + +[] +let ``test field reads and copy-and-update through a byref struct record parameter work`` () = + let mutable r = { OrdA = 1; OrdB = 2; OrdC = 3 } + sumStructRecordByRef &r |> equal 4 + let x = copyStructRecordByRef &r + (x.OrdA, x.OrdB, x.OrdC) |> equal (1, 99, 3) + +[] +let ``test field reads through a byref or outref to a reference type work`` () = + let mutable c = { BcP = 1; BcQ = 2 } + sumClassRecordByRef &c |> equal 3 + let mutable o = { BcP = 0; BcQ = 0 } + setClassRecordOutRef &o |> equal 5 + let mutable k = ByRefClass() + k.BkF <- 4 + sumClassByRef &k |> equal 7 + +[] +let ``test copy-and-update of a let mutable anonymous struct record keeps the unchanged fields`` () = + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) + +[] +let ``test field reads through a byref to an anonymous struct record work`` () = + let mutable a = struct {| AnX = 1; AnY = 2 |} + sumAnonymousStructRecordByRef &a |> equal 3 diff --git a/tests/Rust/tests/src/RecordTests.fs b/tests/Rust/tests/src/RecordTests.fs index 4913a8361..422f3238b 100644 --- a/tests/Rust/tests/src/RecordTests.fs +++ b/tests/Rust/tests/src/RecordTests.fs @@ -11,6 +11,19 @@ type Scene = { let getCamera (scene: Scene) = scene.Camera +[] +type StructRecordCopied = { T: float; S: string } + +[] +let ``Copy-and-update of a let mutable struct record keeps the unchanged fields`` () = + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + [] let ``Class-valued record fields compile`` () = let scene = { Camera = Camera() }