diff --git a/src/fable-library-py/fable_library/reflection.py b/src/fable-library-py/fable_library/reflection.py index 8366b29421..76d6e18948 100644 --- a/src/fable-library-py/fable_library/reflection.py +++ b/src/fable-library-py/fable_library/reflection.py @@ -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 @@ -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: @@ -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) @@ -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)) diff --git a/src/fable-library-ts/Reflection.ts b/src/fable-library-ts/Reflection.ts index 001d9b43a2..3b9e55735b 100644 --- a/src/fable-library-ts/Reflection.ts +++ b/src/fable-library-ts/Reflection.ts @@ -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; @@ -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 { @@ -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`); @@ -478,6 +499,10 @@ export function makeUnion(uci: CaseInfo, values: MutableArray): 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 {}; diff --git a/tests/Js/Main/ReflectionTests.fs b/tests/Js/Main/ReflectionTests.fs index 180c4908d2..20cdbda9c6 100644 --- a/tests/Js/Main/ReflectionTests.fs +++ b/tests/Js/Main/ReflectionTests.fs @@ -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.Error "foo")) + // See https://github.com/fable-compiler/Fable/issues/4082 + testCase "FSharp.Reflection: Option is a union type" <| fun () -> + let typ = typeof + 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 + 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> let ucis = FSharpType.GetUnionCases typ diff --git a/tests/Python/TestReflection.fs b/tests/Python/TestReflection.fs index 5c29a5aa0c..21a1527926 100644 --- a/tests/Python/TestReflection.fs +++ b/tests/Python/TestReflection.fs @@ -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.Error "foo")) +// See https://github.com/fable-compiler/Fable/issues/4082 +[] +let ``test FSharp.Reflection: Option is a union type`` () = + let typ = typeof + 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) + +[] +let ``test FSharp.Reflection: Option round-trips through Some(None) and Some(Some x)`` () = + let typ = typeof + 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))) + [] let ``test FSharp.Reflection: Choice`` () = let typ = typeof>