Skip to content

WIP: Nullable/Nullness support #4018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
94 changes: 75 additions & 19 deletions src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,6 @@ let makeRefFromMutableFunc com ctx r t (value: Expr) =

makeRefCell com r t [ getter; setter ]

let toChar (arg: Expr) =
match arg.Type with
| Char -> arg
| String -> TypeCast(arg, Char)
| _ -> Helper.GlobalCall("String", Char, [ arg ], memb = "fromCharCode")

let toString com (ctx: Context) r (args: Expr list) =
match args with
| [] ->
Expand Down Expand Up @@ -232,19 +226,44 @@ let needToCast fromKind toKind =

/// Conversions to floating point
let toFloat com (ctx: Context) r targetType (args: Expr list) : Expr =
match args.Head.Type with
| Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ])
| String -> Helper.LibCall(com, "Double", "parse", targetType, args)
| Number(kind, _) ->
match kind with
| Decimal -> Helper.LibCall(com, "Decimal", "toNumber", targetType, args)
| BigIntegers _ -> Helper.LibCall(com, "BigInt", "toFloat64", targetType, args)
| _ -> TypeCast(args.Head, targetType)
| _ ->
let warn () =
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"

TypeCast(args.Head, targetType)

let convertType (typ: Type) =
match typ with
| Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ])
| String -> Helper.LibCall(com, "Double", "parse", targetType, args)
| Number(kind, _) ->
match kind with
| Decimal -> Helper.LibCall(com, "Decimal", "toNumber", targetType, args)
| BigIntegers _ ->
let coreMember =
match targetType with
| Number(Float16, _) -> "toFloat16"
| Number(Float32, _) -> "toFloat32"
| Number(Float64, _)
| _ -> "toFloat64"

Helper.LibCall(com, "BigInt", coreMember, targetType, args)
| _ -> TypeCast(args.Head, targetType)
| _ ->
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"

TypeCast(args.Head, targetType)

match args.Head.Type with
| DeclaredType(entityRef, genericArgs) ->
if
entityRef.FullName = "System.Nullable`1"
&& entityRef.Path = CoreAssemblyName "System.Runtime"
then
convertType genericArgs.Head
else
warn ()
| _ -> convertType args.Head.Type

let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr =
match args.Head.Type with
| Char ->
Expand Down Expand Up @@ -339,6 +358,19 @@ let toInt com (ctx: Context) r targetType (args: Expr list) =
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"
TypeCast(args.Head, targetType)

let toChar com (ctx: Context) r targetType (args: Expr list) =
match args.Head.Type with
| Char -> args.Head
| String -> TypeCast(args.Head, Char)
// We need to convert BigInt to number first
| Number(Int64, _)
| Number(UInt64, _) ->
let valueToNumber = toInt com ctx None Int32.Number [ args.Head ]

Helper.GlobalCall("String", Char, [ valueToNumber ], memb = "fromCharCode")

| _ -> Helper.GlobalCall("String", Char, [ args.Head ], memb = "fromCharCode")

let round com (args: Expr list) =
match args.Head.Type with
| Number(Decimal, _) ->
Expand Down Expand Up @@ -370,7 +402,8 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) =
let toUInt16 e = toInt com ctx None UInt16.Number [ e ]

Operation(Binary(op, toUInt16 left, toUInt16 right), Tags.empty, UInt16.Number, r)
|> toChar
|> List.singleton
|> toChar com ctx r t

let truncateUnsigned operation = // see #1550
match t with
Expand Down Expand Up @@ -1206,7 +1239,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o
| ("ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr"), _ -> toLong com ctx r t args |> Some
| ("ToSingle" | "ToDouble"), _ -> toFloat com ctx r t args |> Some
| "ToDecimal", _ -> toDecimal com ctx r t args |> Some
| "ToChar", _ -> toChar args.Head |> Some
| "ToChar", _ -> toChar com ctx r t args |> Some
| "ToString", _ -> toString com ctx r args |> Some
| "CreateSequence", [ xs ] -> TypeCast(xs, t) |> Some
| ("CreateDictionary" | "CreateReadOnlyDictionary"), [ arg ] -> makeDictionary com ctx r t arg |> Some
Expand Down Expand Up @@ -2195,12 +2228,22 @@ let results (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr o
Helper.LibCall(com, "Result", meth, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r)
)

let nullables (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) =
let nullables (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) =
match i.CompiledName, thisArg with
| ".ctor", None -> List.tryHead args
// | "get_Value", Some c -> Get(c, OptionValue, t, r) |> Some // Get(OptionValue) doesn't do a null check
| "get_Value", Some c -> Helper.LibCall(com, "Option", "value", t, [ c ], ?loc = r) |> Some
| "get_HasValue", Some c -> Test(c, OptionTest true, r) |> Some
| "op_Explicit", _ ->
match t with
| Number(kind, _) ->
match kind with
| BigIntegers _ -> toLong com ctx r t args |> Some
| Integers _ -> toInt com ctx r t args |> Some
| Floats _ -> toFloat com ctx r t args |> Some
| Decimal -> toDecimal com ctx r t args |> Some
| _ -> None
| _ -> None
| _ -> None

// See fable-library-ts/Option.ts for more info on how options behave in Fable runtime
Expand Down Expand Up @@ -2451,6 +2494,11 @@ let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg:
| Floats _ -> toFloat com ctx r t args |> Some
| Decimal -> toDecimal com ctx r t args |> Some
| _ -> None
| Char ->
let decimalToNumber =
Helper.LibCall(com, "Decimal", "toNumber", UInt16.Number, args)

Some(Helper.GlobalCall("String", Char, [ decimalToNumber ], memb = "fromCharCode"))
| _ -> None
| ("Ceiling" | "Floor" | "Round" | "Truncate" | "Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Add" | "Subtract" | "Multiply" | "Divide" | "Remainder" | "Negate" as meth),
_ ->
Expand Down Expand Up @@ -2915,7 +2963,7 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (
| "ToSingle"
| "ToDouble" -> toFloat com ctx r t args |> Some
| "ToDecimal" -> toDecimal com ctx r t args |> Some
| "ToChar" -> toChar args.Head |> Some
| "ToChar" -> toChar com ctx r t args |> Some
| "ToString" -> toString com ctx r args |> Some
| "ToBase64String"
| "FromBase64String" ->
Expand Down Expand Up @@ -3953,6 +4001,13 @@ let makeMethodInfo com r (name: string) (parameters: (string * Type) list) (retu

Helper.LibCall(com, "Reflection", "MethodInfo", t, args, isConstructor = true, ?loc = r)

let linqNullableModule (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) =
match i.CompiledName with
| "ToFloat32"
| "ToFloat" -> toFloat com ctx r t args |> Some
| "ToChar" -> toChar com ctx r t args |> Some
| _ -> None

let tryField com returnTyp ownerTyp fieldName =
match ownerTyp, fieldName with
| Number(Decimal, _), _ -> Helper.LibValue(com, "Decimal", "get_" + fieldName, returnTyp) |> Some
Expand Down Expand Up @@ -4110,6 +4165,7 @@ let private replacedModules =
"Microsoft.FSharp.Control.ObservableModule", observable
Types.type_, types
"System.Reflection.TypeInfo", types
"Microsoft.FSharp.Linq.NullableModule", linqNullableModule
]

let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr option) (args: Expr list) =
Expand Down
2 changes: 1 addition & 1 deletion src/fable-library-ts/BigInt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function toUInt128(x: bigint): uint128 { return BigInt.asUintN(128, x); }
export function toNativeInt(x: bigint): nativeint { return BigInt.asIntN(64, x); }
export function toUNativeInt(x: bigint): unativeint { return BigInt.asUintN(64, x); }

export function toFloat16(x: bigint): float32 { return Number(x); }
export function toFloat16(x: bigint): float16 { return Number(x); }
export function toFloat32(x: bigint): float32 { return Number(x); }
export function toFloat64(x: bigint): float64 { return Number(x); }

Expand Down
157 changes: 76 additions & 81 deletions src/quicktest/QuickTest.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,90 +11,85 @@ open Fable.Core
open Fable.Core.JsInterop
open Fable.Core.Testing

let log (o: obj) = JS.console.log (o)
// printfn "%A" o

let equal expected actual =
let areEqual = expected = actual
printfn "%A = %A > %b" expected actual areEqual
printfn "%A = %A > 2dd%b" expected actual areEqual

if not areEqual then
failwithf "[ASSERT ERROR] Expected %A but got %A" expected actual

let throwsError (expected: string) (f: unit -> 'a) : unit =
let success =
try
f () |> ignore
true
with e ->
if not <| String.IsNullOrEmpty(expected) then
equal e.Message expected

false
// TODO better error messages
equal false success

let testCase (msg: string) f : unit =
try
printfn "%s" msg
f ()
with ex ->
printfn "%s" ex.Message

if
ex.Message <> null
&& ex.Message.StartsWith("[ASSERT ERROR]", StringComparison.Ordinal) |> not
then
printfn "%s" (ex.StackTrace ??= "")

printfn ""

let testCaseAsync msg f =
testCase
msg
(fun () ->
async {
try
do! f ()
with ex ->
printfn "%s" ex.Message

if
ex.Message <> null
&& ex.Message.StartsWith("[ASSERT ERROR]", StringComparison.Ordinal) |> not
then
printfn "%s" (ex.StackTrace ??= "")
}
|> Async.StartImmediate
)

let throwsAnyError (f: unit -> 'a) : unit =
let success =
try
f () |> ignore
true
with e ->
printfn "Got expected error: %s" e.Message
false

if success then
printfn "[ERROR EXPECTED]"

let measureTime (f: unit -> unit) : unit =
emitJsStatement
()
"""
//js
const startTime = process.hrtime();
f();
const elapsed = process.hrtime(startTime);
console.log("Ms:", elapsed[0] * 1e3 + elapsed[1] / 1e6);
//!js
"""

printfn "Running quick tests..."

// Write here your unit test, you can later move it
// to Fable.Tests project. For example:
// testCase "Addition works" <| fun () ->
// 2 + 2 |> equal 4
// let x = 12
// let nullableX = Nullable<int> x

// let x2 = 12
// let nullableX2 = Nullable x

// let mutable a = Nullable 42
// a <- Nullable()

// open System
open FSharp.Linq

// let nullableFloat = Nullable 10.0
// let standardString = float nullableFloat
//
// // let standardString = string nullableString
// equal (Nullable.float (Nullable 1uy)) (Nullable 1.0)
// equal (Nullable.float (Nullable 2y)) (Nullable 2.0)
// equal (Nullable.float (Nullable 3s)) (Nullable 3.0)
// equal (Nullable.float (Nullable 4us)) (Nullable 4.0)
// equal (Nullable.float (Nullable 5)) (Nullable 5.0)
// equal (Nullable.float (Nullable 6u)) (Nullable 6.0)
// equal (Nullable.float (Nullable 7L)) (Nullable 7.0)
// equal (Nullable.float (Nullable 8UL)) (Nullable 8.0)
// equal (Nullable.float (Nullable 9m)) (Nullable 9.0)
// equal (Nullable.float (Nullable 10.0)) (Nullable 10.0)
// equal (Nullable.float (Nullable 11.0f)) (Nullable 11.0)
// equal (Nullable.float (Nullable 'c')) (Nullable 99.0)
// equal (Nullable.float (Nullable.enum(Nullable 2 ): Nullable<DayOfWeek>)) (Nullable 2.0)
//
//
// equal (Nullable.float32 (Nullable 1uy)) (Nullable 1.0f)
// equal (Nullable.float32 (Nullable 2y)) (Nullable 2.0f)
// equal (Nullable.float32 (Nullable 3s)) (Nullable 3.0f)
// equal (Nullable.float32 (Nullable 4us)) (Nullable 4.0f)
// equal (Nullable.float32 (Nullable 5)) (Nullable 5.0f)
// equal (Nullable.float32 (Nullable 6u)) (Nullable 6.0f)
// equal (Nullable.float32 (Nullable 7L)) (Nullable 7.0f)
// equal (Nullable.float32 (Nullable 8UL)) (Nullable 8.0f)
// equal (Nullable.float32 (Nullable 9m)) (Nullable 9.0f)
// equal (Nullable.float32 (Nullable 10.0)) (Nullable 10.0f)
// equal (Nullable.float32 (Nullable 11.0f)) (Nullable 11.0f)
// equal (Nullable.float32 (Nullable 'c')) (Nullable 99.0f)
// equal (Nullable.float32 (Nullable.enum(Nullable 2 ): Nullable<DayOfWeek>)) (Nullable 2.0f)
//
// let float32Value = float32 2L
// // float32(1y) |> equal 2.0f

// equal (Nullable.char (Nullable 49uy)) (Nullable '1')
// equal (Nullable.char (Nullable 50y)) (Nullable '2')
// equal (Nullable.char (Nullable 51s)) (Nullable '3')
// equal (Nullable.char (Nullable 52us)) (Nullable '4')
// equal (Nullable.char (Nullable 53)) (Nullable '5')
// equal (Nullable.char (Nullable 54u)) (Nullable '6')
// equal (Nullable.char (Nullable 55L)) (Nullable '7')
// equal (Nullable.char (Nullable 56UL)) (Nullable '8')
// equal (Nullable.char (Nullable 57m)) (Nullable '9')
// equal (Nullable.char (Nullable 58.0)) (Nullable ':')
// equal (Nullable.char (Nullable 59.0f)) (Nullable ';')
// equal (Nullable.char (Nullable 'a')) (Nullable 'a')
//

// let c = char 57m
//
// let test : decimal = 57m
//
// let t : char = Decimal.op_Explicit 57m

let test2 = uint16 655356

let test = 65535us

let te = char 57m

printfn "%A" te
1 change: 1 addition & 0 deletions src/quicktest/QuickTest.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<RollForward>Major</RollForward>
<LangVersion>Preview</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="QuickTest.fs" />
Expand Down
1 change: 1 addition & 0 deletions tests/Js/Main/Fable.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
<Compile Include="TimeOnlyTests.fs" />
<Compile Include="TimeSpanTests.fs" />
<Compile Include="ListCollectorTests.fs" />
<Compile Include="NullableTests.fs" />
<Compile Include="Main.fs" />
</ItemGroup>

Expand Down
1 change: 1 addition & 0 deletions tests/Js/Main/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ let allTests =
UnionTypes.tests
Uri.tests
ListCollector.tests
Nullable.tests
|]

#if FABLE_COMPILER
Expand Down
Loading
Loading