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
24 changes: 20 additions & 4 deletions src/fable-library-py/fable_library/reflection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
from typing import Any, cast

from .array_ import Array
from .core import FSharpRef, int32
from .core import FSharpRef, int32, option
from .record import Record
from .types import IntegerTypes
from .union import Union
from .union import Union as FsUnion
from .util import combine_hash_codes, equal_arrays_with

Expand Down Expand Up @@ -111,7 +110,12 @@ def anon_record_type(*fields: FieldInfo) -> TypeInfo:


def option_type(generic: TypeInfo) -> TypeInfo:
return TypeInfo("Microsoft.FSharp.Core.FSharpOption`1", Array([generic]))
t = TypeInfo("Microsoft.FSharp.Core.FSharpOption`1", Array([generic]))
t.cases = lambda: [
CaseInfo(t, 0, "None", []),
CaseInfo(t, 1, "Some", [("value", generic)]),
]
return t


def list_type(generic: TypeInfo) -> TypeInfo:
Expand Down Expand Up @@ -427,6 +431,10 @@ def make_union(uci: CaseInfo, values: Array[Any]) -> Any:
if len(values) != expectedLength:
raise ValueError(f"Expected an array of length {expectedLength} but got {len(values)}")

# Options are erased at runtime: None -> None, Some(x) -> x or SomeWrapper
if uci.declaringType.fullname == "Microsoft.FSharp.Core.FSharpOption`1":
return None if uci.tag == 0 else option.some(values[0])

# Use case constructor if available (new tagged_union pattern)
if uci.case_constructor is not None:
return uci.case_constructor(*values)
Expand All @@ -442,8 +450,16 @@ def get_union_cases(t: TypeInfo) -> Array[CaseInfo]:
raise ValueError(f"{t.fullname} is not an F# union type")


def get_union_fields(v: Union, t: TypeInfo) -> tuple[CaseInfo, Array[Any]]:
# `v` is `Any` because option values are erased at runtime (None / raw value /
# SomeWrapper) and don't share a base class with `Union`.
def get_union_fields(v: Any, t: TypeInfo) -> tuple[CaseInfo, Array[Any]]:
cases: Array[CaseInfo] = get_union_cases(t)
# Options are erased at runtime: None -> None, Some(x) -> x or SomeWrapper
if t.fullname == "Microsoft.FSharp.Core.FSharpOption`1":
if v is None:
return (cases[0], Array[Any]([]))
inner = v.value if isinstance(v, option.SomeWrapper) else v
return (cases[1], Array[Any]([inner]))
case: CaseInfo = cases[v.tag]

return (case, Array[Any](v.fields))
Expand Down
27 changes: 26 additions & 1 deletion src/fable-library-ts/Reflection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FSharpRef, Record, Union } from "./Types.ts";
import { Exception, MutableArray, combineHashCodes, equalArraysWith, IEquatable, stringHash } from "./Util.ts";
import Decimal from "./Decimal.ts";
import { Some, some } from "./Option.ts";

export type FieldInfo = [string, TypeInfo];
export type PropertyInfo = FieldInfo;
Expand Down Expand Up @@ -155,7 +156,18 @@ export function lambda_type(argType: TypeInfo, returnType: TypeInfo): TypeInfo {
}

export function option_type(generic: TypeInfo): TypeInfo {
return new TypeInfo("Microsoft.FSharp.Core.FSharpOption`1", [generic]);
const t: TypeInfo = new TypeInfo(
"Microsoft.FSharp.Core.FSharpOption`1",
[generic],
undefined,
undefined,
undefined,
() => [
new CaseInfo(t, 0, "None"),
new CaseInfo(t, 1, "Some", [["value", generic]])
]
);
return t;
}

export function list_type(generic: TypeInfo): TypeInfo {
Expand Down Expand Up @@ -443,6 +455,15 @@ export function isFunction(t: TypeInfo): boolean {

export function getUnionFields(v: any, t: TypeInfo): [CaseInfo, any[]] {
const cases = getUnionCases(t);
// Special handling for option types (None is undefined, Some is the value or a Some wrapper)
if (t.fullname === "Microsoft.FSharp.Core.FSharpOption`1") {
if (v == null) {
return [cases[0], []]; // None case
} else {
const innerValue = v instanceof Some ? v.value : v;
return [cases[1], [innerValue]]; // Some case
}
}
const case_ = cases[v.tag];
if (case_ == null) {
throw new Exception(`Cannot find case ${v.name} in union type`);
Expand Down Expand Up @@ -478,6 +499,10 @@ export function makeUnion(uci: CaseInfo, values: MutableArray<any>): any {
if (values.length !== expectedLength) {
throw new Exception(`Expected an array of length ${expectedLength} but got ${values.length}`);
}
// Special handling for option types
if (uci.declaringType.fullname === "Microsoft.FSharp.Core.FSharpOption`1") {
return uci.tag === 0 ? undefined : some(values[0]);
}
const construct = uci.declaringType.construct;
if (construct == null) {
return {};
Expand Down
30 changes: 30 additions & 0 deletions tests/Js/Main/ReflectionTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,36 @@ let reflectionTests = [
FSharpValue.MakeUnion(ucis.[0], [|box 5|]) |> equal (box (Result<_,string>.Ok 5))
FSharpValue.MakeUnion(ucis.[1], [|box "foo"|]) |> equal (box (Result<int,_>.Error "foo"))

// See https://github.com/fable-compiler/Fable/issues/4082
testCase "FSharp.Reflection: Option is a union type" <| fun () ->
let typ = typeof<int option>
FSharpType.IsUnion(typ) |> equal true
let ucis = FSharpType.GetUnionCases(typ)
ucis.Length |> equal 2
ucis.[0].Name |> equal "None"
ucis.[1].Name |> equal "Some"
FSharpValue.MakeUnion(ucis.[0], [||]) |> equal (box (None: int option))
FSharpValue.MakeUnion(ucis.[1], [|box 42|]) |> equal (box (Some 42))
let noneCase, noneFields = FSharpValue.GetUnionFields(box (None: int option), typ)
noneCase.Name |> equal "None"
noneFields.Length |> equal 0
let someCase, someFields = FSharpValue.GetUnionFields(box (Some 42), typ)
someCase.Name |> equal "Some"
someFields.Length |> equal 1
someFields.[0] |> equal (box 42)

testCase "FSharp.Reflection: Option round-trips through Some(None) and Some(Some x)" <| fun () ->
let typ = typeof<int option option>
let ucis = FSharpType.GetUnionCases(typ)
let someCase, someFields = FSharpValue.GetUnionFields(box (Some (None: int option)), typ)
someCase.Name |> equal "Some"
someFields.[0] |> equal (box (None: int option))
let someCase2, someFields2 = FSharpValue.GetUnionFields(box (Some (Some 42)), typ)
someCase2.Name |> equal "Some"
someFields2.[0] |> equal (box (Some 42))
FSharpValue.MakeUnion(ucis.[1], [|box (None: int option)|]) |> equal (box (Some (None: int option)))
FSharpValue.MakeUnion(ucis.[1], [|box (Some 42)|]) |> equal (box (Some (Some 42)))

testCase "FSharp.Reflection: Choice" <| fun () ->
let typ = typeof<Choice<int,string>>
let ucis = FSharpType.GetUnionCases typ
Expand Down
32 changes: 32 additions & 0 deletions tests/Python/TestReflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,38 @@ let ``test FSharp.Reflection: Result`` () =
FSharpValue.MakeUnion(ucis.[0], [|box 5|]) |> equal (box (Result<_,string>.Ok 5))
FSharpValue.MakeUnion(ucis.[1], [|box "foo"|]) |> equal (box (Result<int,_>.Error "foo"))

// See https://github.com/fable-compiler/Fable/issues/4082
[<Fact>]
let ``test FSharp.Reflection: Option is a union type`` () =
let typ = typeof<int option>
FSharpType.IsUnion(typ) |> equal true
let ucis = FSharpType.GetUnionCases(typ)
ucis.Length |> equal 2
ucis.[0].Name |> equal "None"
ucis.[1].Name |> equal "Some"
FSharpValue.MakeUnion(ucis.[0], [||]) |> equal (box (None: int option))
FSharpValue.MakeUnion(ucis.[1], [|box 42|]) |> equal (box (Some 42))
let noneCase, noneFields = FSharpValue.GetUnionFields(box (None: int option), typ)
noneCase.Name |> equal "None"
noneFields.Length |> equal 0
let someCase, someFields = FSharpValue.GetUnionFields(box (Some 42), typ)
someCase.Name |> equal "Some"
someFields.Length |> equal 1
someFields.[0] |> equal (box 42)

[<Fact>]
let ``test FSharp.Reflection: Option round-trips through Some(None) and Some(Some x)`` () =
let typ = typeof<int option option>
let ucis = FSharpType.GetUnionCases(typ)
let someCase, someFields = FSharpValue.GetUnionFields(box (Some (None: int option)), typ)
someCase.Name |> equal "Some"
someFields.[0] |> equal (box (None: int option))
let someCase2, someFields2 = FSharpValue.GetUnionFields(box (Some (Some 42)), typ)
someCase2.Name |> equal "Some"
someFields2.[0] |> equal (box (Some 42))
FSharpValue.MakeUnion(ucis.[1], [|box (None: int option)|]) |> equal (box (Some (None: int option)))
FSharpValue.MakeUnion(ucis.[1], [|box (Some 42)|]) |> equal (box (Some (Some 42)))

[<Fact>]
let ``test FSharp.Reflection: Choice`` () =
let typ = typeof<Choice<int,string>>
Expand Down
Loading