Skip to content

Commit a4c7548

Browse files
authored
fix(all): pass raw value for optional arguments of native bindings (#4688)
1 parent 15daf62 commit a4c7548

6 files changed

Lines changed: 126 additions & 4 deletions

File tree

src/Fable.Transforms/FSharp2Fable.Util.fs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2908,6 +2908,21 @@ module Util =
29082908
else
29092909
ValueNone
29102910

2911+
/// Member bound to native code (binding) rather than implemented in F#.
2912+
let private callsExternalBinding (memb: FSharpMemberOrFunctionOrValue) =
2913+
isEmittedOrImportedMember memb
2914+
|| match memb.DeclaringEntity with
2915+
| Some ent ->
2916+
isGlobalOrImportedFSharpEntity ent
2917+
|| isErasedOrStringEnumFSharpEntity ent
2918+
// Fable's own bindings carry no attribute, match by namespace.
2919+
|| (
2920+
match ent.TryFullName with
2921+
| Some name -> name.StartsWithAny("Fable.Core.JS.", "Fable.Core.Py.")
2922+
| None -> false
2923+
)
2924+
| None -> false
2925+
29112926
/// Removes optional arguments set to None in tail position
29122927
let transformOptionalArguments
29132928
(com: IFableCompiler)
@@ -2923,11 +2938,16 @@ module Util =
29232938
then
29242939
args
29252940
else
2941+
// Native bindings expect the raw value, not F#'s `Some` wrapper for `?` args.
2942+
let unwrapProvidedOptional = callsExternalBinding memb
2943+
29262944
(memb.CurriedParameterGroups[0], args, (true, []))
29272945
|||> Seq.foldBack2 (fun par arg (keepChecking, acc) ->
2928-
if keepChecking && par.IsOptionalArg then
2946+
if par.IsOptionalArg then
29292947
match arg with
2930-
| Fable.Value(Fable.NewOption(None, _, _), _) -> true, acc
2948+
| Fable.Value(Fable.NewOption(None, _, _), _) when keepChecking -> true, acc
2949+
| Fable.Value(Fable.NewOption(Some value, _, _), _) when unwrapProvidedOptional ->
2950+
false, value :: acc
29312951
| _ -> false, arg :: acc
29322952
else
29332953
false, arg :: acc
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module OptionalArgs
2+
3+
open Fable.Core
4+
open Fable.Core.JsInterop
5+
6+
// An optional argument (`?x: obj`) passed to a native binding must reach the native
7+
// function as the raw value. `obj`-typed optionals are not flattened, so without
8+
// unwrapping the F# `Some` wrapper (`some(...)`) would leak into the generated code.
9+
10+
// JS binding (Fable.Core.JS)
11+
let jsBinding (v: obj) (sp: obj) : string = JS.JSON.stringify (v, space = sp)
12+
13+
// Imported binding
14+
[<Import("Thing", "my-lib")>]
15+
type Thing =
16+
abstract doIt: x: int * ?y: obj -> string
17+
18+
let imported (t: Thing) (n: obj) : string = t.doIt (1, y = n)
19+
20+
// Erased binding
21+
[<Erase>]
22+
type Erased =
23+
abstract run: a: int * ?b: obj -> string
24+
25+
let erased (e: Erased) (n: obj) : string = e.run (1, b = n)
26+
27+
// Emit member
28+
type IFoo =
29+
[<Emit("$0.foo($1, $2)")>]
30+
abstract foo: x: int * ?y: obj -> string
31+
32+
let emitted (foo: IFoo) (n: obj) : string = foo.foo (1, y = n)
33+
34+
// Plain F# member: the implementation consumes the argument as `obj option`,
35+
// so the `some(...)` wrapper must be kept.
36+
type Native() =
37+
static member Show(x: int, ?y: obj) : string = string x + string (defaultArg y (box "-"))
38+
39+
let fSharpMember (n: obj) : string = Native.Show(1, y = n)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { class_type } from "./fable_modules/fable-library-js.5.4.0/Reflection.js";
2+
import { int32ToString } from "./fable_modules/fable-library-js.5.4.0/Util.js";
3+
import { toString } from "./fable_modules/fable-library-js.5.4.0/Types.js";
4+
import { some, defaultArg } from "./fable_modules/fable-library-js.5.4.0/Option.js";
5+
6+
export function jsBinding(v, sp) {
7+
return JSON.stringify(v, undefined, sp);
8+
}
9+
10+
export function imported(t, n) {
11+
return t.doIt(1, n);
12+
}
13+
14+
export function erased(e, n) {
15+
return e.run(1, n);
16+
}
17+
18+
export function emitted(foo, n) {
19+
return foo.foo(1, n);
20+
}
21+
22+
export class Native {
23+
constructor() {
24+
}
25+
}
26+
27+
export function Native_$reflection() {
28+
return class_type("OptionalArgs.Native", undefined, Native);
29+
}
30+
31+
export function Native_$ctor() {
32+
return new Native();
33+
}
34+
35+
export function Native_Show_21A309C4(x, y) {
36+
return int32ToString(x) + toString(defaultArg(y, "-"));
37+
}
38+
39+
export function fSharpMember(n) {
40+
return Native_Show_21A309C4(1, some(n));
41+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<RollForward>Major</RollForward>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Compile Include="OptionalArgs.fs" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Fable.Core" Version="4.5.0" />
14+
</ItemGroup>
15+
16+
</Project>

tests/Integration/Integration/data/signatureHidesFunction/HideType.jsx.expected

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Union } from "./fable_modules/fable-library-js.4.13.0/Types.js";
22
import { union_type, string_type, int32_type } from "./fable_modules/fable-library-js.4.13.0/Reflection.js";
3-
import { some } from "./fable_modules/fable-library-js.4.13.0/Option.js";
43

54
class Foobar extends Union {
65
constructor(tag, fields) {
@@ -18,6 +17,6 @@ function Foobar_$reflection() {
1817
}
1918

2019
export function someFunction() {
21-
console.log(some("meh"));
20+
console.log("meh");
2221
}
2322

tests/Js/Main/MiscTests.fs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,13 @@ let tests =
560560
parsed.widget.text.vOffset |> equal 500.
561561
#endif
562562

563+
testCase "Named optional argument to a JS binding is not wrapped in an option" <| fun () ->
564+
// `space` is an optional parameter of the JSON.stringify binding; passing it as a
565+
// named argument must forward the raw value (not a Fable option), otherwise the
566+
// generated TypeScript references an Option<number> and fails to type-check.
567+
let json = Fable.Core.JS.JSON.stringify({| value = 1 |}, space = 2)
568+
json.Contains("\n") |> equal true
569+
563570
testCase "Lambdas returning member expression accessing JS object work" <| fun () -> // #2311
564571
let x = inlineLambdaWithAnonRecord (fun x -> x.A)
565572
x() |> equal 1

0 commit comments

Comments
 (0)