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
17 changes: 11 additions & 6 deletions src/Fable.Transforms/FSharp2Fable.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -832,10 +832,19 @@ module Helpers =
// typ.TypeDefinition.DisplayName = "inref" ||
// typ.TypeDefinition.DisplayName = "outref")

/// F# types the locals it generates for copy-and-update expressions as byrefs, but Fable
/// binds the struct value itself to them.
let isCopyUpdateLocal (value: FSharpMemberOrFunctionOrValue) =
value.IsCompilerGenerated
&& (value.CompiledName = "copyOfStruct" || value.CompiledName = "inputRecord")

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)
value.IsValue && not value.IsMemberThisValue && isByRefType value.FullType
value.IsValue
&& not value.IsMemberThisValue
&& not (isCopyUpdateLocal value)
&& isByRefType value.FullType

let tryFindAttrib fullName (attributes: FSharpAttribute seq) =
attributes
Expand Down Expand Up @@ -1888,11 +1897,7 @@ module Identifiers =
open TypeHelpers

let isMutableOrByRefValue (fsRef: FSharpMemberOrFunctionOrValue) =
(fsRef.IsMutable || isByRefValue fsRef)
&& not (
fsRef.IsCompilerGenerated
&& (fsRef.CompiledName = "copyOfStruct" || fsRef.CompiledName = "inputRecord")
)
not (isCopyUpdateLocal fsRef) && (fsRef.IsMutable || isByRefValue fsRef)

let makeIdentFrom (com: IFableCompiler) (ctx: Context) (fsRef: FSharpMemberOrFunctionOrValue) : Fable.Ident =
let part = Naming.NoMemberPart
Expand Down
23 changes: 21 additions & 2 deletions src/Fable.Transforms/FSharp2Fable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,28 @@ let private transformTraitCall
|> addErrorAndReturnNull com ctx.InlinePath r
)

/// F# keeps the byref type on a use that goes through the address, like reading a field of a
/// struct passed by reference. Fable compiles byrefs to ref cells, which cannot be addressed
/// that way, so such a use has to read the cell.
let private derefByRefAddress (com: IFableCompiler) (fsExpr: FSharpExpr) (expr: Fable.Expr) =
match fsExpr with
| FSharpExprPatterns.Value var when isByRefValue var && isByRefType fsExpr.Type && com.Options.Language <> Rust ->
Replacements.Api.getRefCell com expr.Range (List.head expr.Type.Generics) expr
| _ -> expr

let private transformCalleeExprOpt com ctx (callee: FSharpExpr option) =
trampoline {
let! calleeExpr = transformExprOpt com ctx callee

return
match callee, calleeExpr with
| Some fsCallee, Some calleeExpr -> Some(derefByRefAddress com fsCallee calleeExpr)
| _ -> calleeExpr
}

let private transformCallee com ctx callee (calleeType: FSharpType) =
trampoline {
let! callee = transformExprOpt com ctx callee
let! callee = transformCalleeExprOpt com ctx callee

let callee =
match callee with
Expand Down Expand Up @@ -1068,7 +1087,7 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs

| callee, _ ->
let r = makeRangeFrom fsExpr
let! callee = transformExprOpt com ctx callee
let! callee = transformCalleeExprOpt com ctx callee

let! ctx =
trampoline {
Expand Down
6 changes: 3 additions & 3 deletions src/Fable.Transforms/Replacements.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -981,10 +981,10 @@ let (|IsByRefType|_|) (com: Compiler) =
| _ -> ValueNone
| _ -> ValueNone

/// F# types the locals it generates for copy-and-update expressions (`inputRecord`, `copyOfStruct`)
/// as byrefs, but Fable binds the struct value itself to them.
/// F# types a struct's `this` argument and the locals it generates for copy-and-update
/// expressions (`inputRecord`, `copyOfStruct`) as byrefs, but Fable binds the value itself to them.
let getIdentValueType (com: Compiler) (ident: Ident) (typ: Type) =
if ident.IsCompilerGenerated then
if ident.IsCompilerGenerated || ident.IsThisArgument then
match typ with
| IsByRefType com innerType -> innerType
| typ -> typ
Expand Down
25 changes: 25 additions & 0 deletions tests/Js/Main/TypeTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,21 @@ let inline inlinedFunc(n: 't[]) =
let genericByrefFunc(n: byref<'t[]>) =
inlinedFunc n

[<Struct>]
type ByRefStruct =
{ Size: int; Seed: int }
member this.Sum() = this.Size + this.Seed
member this.Total = this.Size + this.Seed

[<Struct>]
type MutableByRefStruct = { mutable Count: int }

let readStructByRef (data: byref<ByRefStruct>) = data.Size + data.Seed
let callStructByRefMember (data: byref<ByRefStruct>) = data.Sum()
let readStructByRefProperty (data: byref<ByRefStruct>) = data.Total
let copyUpdateStructByRef (data: byref<ByRefStruct>) = { data with Size = data.Size + 10 }
let bumpStructByRef (data: byref<MutableByRefStruct>) = data.Count <- data.Count + 5

[<Fable.Core.AttachMembers>]
type GenericClassWithStaticMember<'T>() =
static member Length(xs: 'T list) = xs.Length
Expand Down Expand Up @@ -1693,6 +1708,16 @@ let tests =
let result = genericByrefFunc &arr
result |> equal 3

testCase "Struct passed by reference works" <| fun () ->
let mutable data: ByRefStruct = { Size = 1; Seed = 2 }
readStructByRef &data |> equal 3
callStructByRefMember &data |> equal 3
readStructByRefProperty &data |> equal 3
(copyUpdateStructByRef &data).Size |> equal 11
let mutable counter: MutableByRefStruct = { Count = 7 }
bumpStructByRef &counter
counter.Count |> equal 12

testCase "mangled method on interface works"
<| fun () ->
let upper = UpperMangledClass()
Expand Down
26 changes: 26 additions & 0 deletions tests/Python/TestType.fs
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,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 636 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 636 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 636 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 636 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 636 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 636 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 @@ -705,6 +705,21 @@
let update (model: Model) =
model, ()

[<Struct>]
type ByRefStruct =
{ Size: int; Seed: int }
member this.Sum() = this.Size + this.Seed
member this.Total = this.Size + this.Seed

[<Struct>]
type MutableByRefStruct = { mutable Count: int }

let readStructByRef (data: byref<ByRefStruct>) = data.Size + data.Seed
let callStructByRefMember (data: byref<ByRefStruct>) = data.Sum()
let readStructByRefProperty (data: byref<ByRefStruct>) = data.Total
let copyUpdateStructByRef (data: byref<ByRefStruct>) = { data with Size = data.Size + 10 }
let bumpStructByRef (data: byref<MutableByRefStruct>) = data.Count <- data.Count + 5

[<Fact>]
let ``test Unit arguments work`` () =
update () |> equal ((), ())
Expand Down Expand Up @@ -1869,3 +1884,14 @@
obj.Attach("hello")
// Each override delegates to base before logging, so the chain unwinds Base -> Mid -> Leaf
log |> List.ofSeq |> equal [ "Base"; "Mid"; "Leaf" ]

[<Fact>]
let ``test Struct passed by reference works`` () =
let mutable data: ByRefStruct = { Size = 1; Seed = 2 }
readStructByRef &data |> equal 3
callStructByRefMember &data |> equal 3
readStructByRefProperty &data |> equal 3
(copyUpdateStructByRef &data).Size |> equal 11
let mutable counter: MutableByRefStruct = { Count = 7 }
bumpStructByRef &counter
counter.Count |> equal 12
Loading