From 2a1c6a789c370b10b2eed3dd0c57f35f0e7b4aca Mon Sep 17 00:00:00 2001 From: Tom Atwood Date: Sat, 26 Sep 2026 12:22:23 -0400 Subject: [PATCH 1/3] fix: null target tryGet and exists, and explicitConvert of null (#111) A null target is None or false, the same answer as a missing member. explicitConvert of null follows implicitConvert. Dyn.set on a null target still throws. --- CHANGELOG.md | 4 + FSharp.Interop.Dynamic/Dyn.fs | 365 +++++++++++++++++---------------- FSharp.Interop.Dynamic/Dyn.fsi | 238 ++++++++++----------- README.md | 2 +- Tests/Invocation.fs | 16 ++ Tests/TryGet.fs | 11 +- docfx/docs/caveats.md | 2 +- docfx/docs/tryget.md | 4 +- 8 files changed, 340 insertions(+), 302 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3466d2f..90bde09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Each release has a `## [version]` section. When a `v*.*.*` tag is pushed, the Pu ## [Unreleased] +### Fixed + +- `Dyn.tryGet` and `Dyn.exists` on a null target return `None` and `false`. `Dyn.explicitConvert` and `explicitConvertTo` of null follow `implicitConvert` (null for a reference result type; a value type still fails). `Dyn.set` on a null target still throws `NullReferenceException` (#111). + ### Docs - Caveats document 15-argument `TypeLoadException`, C# optional parameters, and `tryGet` / `exists` on a null target (#110). diff --git a/FSharp.Interop.Dynamic/Dyn.fs b/FSharp.Interop.Dynamic/Dyn.fs index 03d8dec..ae4e579 100644 --- a/FSharp.Interop.Dynamic/Dyn.fs +++ b/FSharp.Interop.Dynamic/Dyn.fs @@ -1,207 +1,216 @@ -// -// Copyright 2011 Ekon Benefits -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and +// +// Copyright 2011 Ekon Benefits +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and // limitations under the License. -namespace FSharp.Interop.Dynamic -open System - +namespace FSharp.Interop.Dynamic +open System + type Calling = | GenericMember of string * Type array - | Member of string - | Direct - -module Dyn = - open Dynamitey - open Microsoft.CSharp.RuntimeBinder - open FSharp.Reflection - - let staticContext (target:Type) = InvokeContext.CreateStatic.Invoke(target) - - let staticTarget<'TTarget> = InvokeContext.CreateStatic.Invoke(typeof<'TTarget>) + | Member of string + | Direct + +module Dyn = + open Dynamitey + open Microsoft.CSharp.RuntimeBinder + open FSharp.Reflection + + let staticContext (target:Type) = InvokeContext.CreateStatic.Invoke(target) + + let staticTarget<'TTarget> = InvokeContext.CreateStatic.Invoke(typeof<'TTarget>) let implicitConvertTo (convertType:Type) (target:obj) : 'TResult = match target with - | null -> target + | null -> target | t -> Dynamic.InvokeConvert(t, convertType, explicit = false) |> unbox<'TResult> - let implicitConvert(target:obj) : 'TResult = + let implicitConvert(target:obj) : 'TResult = implicitConvertTo typeof<'TResult> target let explicitConvertTo (convertType:Type) (target:obj) : 'TResult = - Dynamic.InvokeConvert(target, convertType, explicit = true) |> unbox<'TResult> - + match target with + | null -> target + | t -> Dynamic.InvokeConvert(t, convertType, explicit = true) + |> unbox<'TResult> + let explicitConvert (target:obj) : 'TResult = - explicitConvertTo typeof<'TResult> target - - let namedArg (name:string) (argValue:obj) = - InvokeArg(name, argValue) - - let memberAddAssign (memberName:string) (value:obj) (target:obj) = + explicitConvertTo typeof<'TResult> target + + let namedArg (name:string) (argValue:obj) = + InvokeArg(name, argValue) + + let memberAddAssign (memberName:string) (value:obj) (target:obj) = Dynamic.InvokeAddAssignMember(target, memberName, value) - let memberSubtractAssign (memberName:string) (value:obj) (target:obj) = + let memberSubtractAssign (memberName:string) (value:obj) (target:obj) = Dynamic.InvokeSubtractAssignMember(target, memberName, value) - let invocation (target:obj) (memberName:Calling) : 'TResult = - let resultType = typeof<'TResult> - //Helper to dynamically call call FSharpFuncs - let fsharpInvoke target' memberName' arg' = - let invokeName = InvokeMemberName("Invoke", null) //FSharpFunc Invoke - let invokeContext t = InvokeContext(t, typeof) //Improve cache hits by using the same context - let start = match memberName' with - | GenericMember (name, _) - | Member name -> Dynamic.InvokeGet(target', name) - | Direct -> target' - Dynamic.InvokeMember(invokeContext(start), invokeName, [|arg'|]) + let invocation (target:obj) (memberName:Calling) : 'TResult = + let resultType = typeof<'TResult> + //Helper to dynamically call call FSharpFuncs + let fsharpInvoke target' memberName' arg' = + let invokeName = InvokeMemberName("Invoke", null) //FSharpFunc Invoke + let invokeContext t = InvokeContext(t, typeof) //Improve cache hits by using the same context + let start = match memberName' with + | GenericMember (name, _) + | Member name -> Dynamic.InvokeGet(target', name) + | Direct -> target' + Dynamic.InvokeMember(invokeContext(start), invokeName, [|arg'|]) let (|NoConversion| Conversion|) t = - if t = typeof then NoConversion else Conversion + if t = typeof then NoConversion else Conversion let finalConvertResult finalType result :'TResult = - match finalType with - | x when FSharpType.IsFunction x -> // if return type is a function + match finalType with + | x when FSharpType.IsFunction x -> // if return type is a function let rec curriedLambda target type' arg' = - let result' = fsharpInvoke target Direct arg' - let _,retType = FSharpType.GetFunctionElements type' - if FSharpType.IsFunction retType then - FSharpValue.MakeFunction(retType, curriedLambda result' retType) - else - result' - FSharpValue.MakeFunction(finalType, curriedLambda result finalType) - | NoConversion -> result - | Conversion -> implicitConvert result - |> unbox - if not (FSharpType.IsFunction resultType) - then - match memberName with - | GenericMember (name, _) - | Member name -> Dynamic.InvokeGet(target, name) - | Direct -> target - |> finalConvertResult resultType - else - let lambda (arg:obj) = - let argType,returnType = FSharpType.GetFunctionElements resultType - let argArray = - match argType with - | a when FSharpType.IsTuple(a) -> FSharpValue.GetTupleFields(arg) - | a when a = typeof -> [| |] - | ____________________________ -> [|arg|] + let result' = fsharpInvoke target Direct arg' + let _,retType = FSharpType.GetFunctionElements type' + if FSharpType.IsFunction retType then + FSharpValue.MakeFunction(retType, curriedLambda result' retType) + else + result' + FSharpValue.MakeFunction(finalType, curriedLambda result finalType) + | NoConversion -> result + | Conversion -> implicitConvert result + |> unbox + if not (FSharpType.IsFunction resultType) + then + match memberName with + | GenericMember (name, _) + | Member name -> Dynamic.InvokeGet(target, name) + | Direct -> target + |> finalConvertResult resultType + else + let lambda (arg:obj) = + let argType,returnType = FSharpType.GetFunctionElements resultType + let argArray = + match argType with + | a when FSharpType.IsTuple(a) -> FSharpValue.GetTupleFields(arg) + | a when a = typeof -> [| |] + | ____________________________ -> [|arg|] let invoker k = - let memberName = - memberName |> function | GenericMember (name, targs) -> - InvokeMemberName(name, targs) + let memberName = + memberName |> function | GenericMember (name, targs) -> + InvokeMemberName(name, targs) | Member name -> - InvokeMemberName(name, null) - | Direct -> null - Invocation(k, memberName).Invoke(target, argArray) - let (|Action|Func|) t = if t = typeof then Action else Func - let result = - try //Either it has a member or it's something directly callable - match (returnType, memberName) with - | (Action, Direct) -> invoker(InvocationKind.InvokeAction) - | (Action, GenericMember _) - | (Action, Member _) -> invoker(InvocationKind.InvokeMemberAction) - | (Func, Direct) -> invoker(InvocationKind.Invoke) - | (Func, GenericMember _) - | (Func, Member _) -> invoker(InvocationKind.InvokeMember) - with //Last chance incase we are trying to invoke an fsharpfunc - | :? RuntimeBinderException as e -> - try - fsharpInvoke target memberName arg - with - | :? RuntimeBinderException as e2 -> - AggregateException(e, e2) |> raise - | _ -> - reraise() - | _ -> - reraise() - match returnType with - | Action | NoConversion -> result - | _____________________ -> result |> finalConvertResult returnType - FSharpValue.MakeFunction(resultType, lambda) |> unbox<'TResult> - - let invokeDirect value (target:obj) : 'TResult = + InvokeMemberName(name, null) + | Direct -> null + Invocation(k, memberName).Invoke(target, argArray) + let (|Action|Func|) t = if t = typeof then Action else Func + let result = + try //Either it has a member or it's something directly callable + match (returnType, memberName) with + | (Action, Direct) -> invoker(InvocationKind.InvokeAction) + | (Action, GenericMember _) + | (Action, Member _) -> invoker(InvocationKind.InvokeMemberAction) + | (Func, Direct) -> invoker(InvocationKind.Invoke) + | (Func, GenericMember _) + | (Func, Member _) -> invoker(InvocationKind.InvokeMember) + with //Last chance incase we are trying to invoke an fsharpfunc + | :? RuntimeBinderException as e -> + try + fsharpInvoke target memberName arg + with + | :? RuntimeBinderException as e2 -> + AggregateException(e, e2) |> raise + | _ -> + reraise() + | _ -> + reraise() + match returnType with + | Action | NoConversion -> result + | _____________________ -> result |> finalConvertResult returnType + FSharpValue.MakeFunction(resultType, lambda) |> unbox<'TResult> + + let invokeDirect value (target:obj) : 'TResult = invocation target Direct value - let invokeMember (memberName:string) value (target:obj) : 'TResult = + let invokeMember (memberName:string) value (target:obj) : 'TResult = invocation target (Member memberName) value - let invokeGeneric (memberName:string) (typeArgs:Type seq) value (target:obj) : 'TResult = - let typeArgs' = typeArgs |> Array.ofSeq - let genericMember = GenericMember (memberName, typeArgs') - invocation target genericMember value - - let get (propertyName:string) (target:obj) : 'TResult = - invocation target (Member propertyName) - - let tryGet (propertyName:string) (target:obj) : 'T option = - let found = - try - Some (Dynamic.InvokeGet(target, propertyName)) - with - | :? RuntimeBinderException -> None - | _ -> reraise() - match found with - | None -> None - | Some value -> Some (invocation value Direct) - - let exists (propertyName:string) (target:obj) : bool = - try - Dynamic.InvokeGet(target, propertyName) |> ignore - true - with - | :? RuntimeBinderException -> false - | _ -> reraise() - - let getChain (chainOfMembers:string seq) (target:obj) : 'TResult = - let chainOfMembers' = String.concat "." chainOfMembers - let value = Dynamic.InvokeGetChain(target, chainOfMembers') - invocation value Direct - - let getIndexer (indexers: 'T seq) (target:obj): 'TResult = - let indexes = indexers |> Seq.map box |> Seq.toArray - let value = Dynamic.InvokeGetIndex(target, indexes) - invocation value Direct - - let set (propertyName:string) (value:obj) (target:obj) = - Dynamic.InvokeSet(target, propertyName, value) |> ignore - - let setChain (chainOfMembers: string seq) (value:obj) (target:obj) = + let invokeGeneric (memberName:string) (typeArgs:Type seq) value (target:obj) : 'TResult = + let typeArgs' = typeArgs |> Array.ofSeq + let genericMember = GenericMember (memberName, typeArgs') + invocation target genericMember value + + let get (propertyName:string) (target:obj) : 'TResult = + invocation target (Member propertyName) + + let tryGet (propertyName:string) (target:obj) : 'T option = + match target with + | null -> None + | target -> + let found = + try + Some (Dynamic.InvokeGet(target, propertyName)) + with + | :? RuntimeBinderException -> None + | _ -> reraise() + match found with + | None -> None + | Some value -> Some (invocation value Direct) + + let exists (propertyName:string) (target:obj) : bool = + match target with + | null -> false + | target -> + try + Dynamic.InvokeGet(target, propertyName) |> ignore + true + with + | :? RuntimeBinderException -> false + | _ -> reraise() + + let getChain (chainOfMembers:string seq) (target:obj) : 'TResult = + let chainOfMembers' = String.concat "." chainOfMembers + let value = Dynamic.InvokeGetChain(target, chainOfMembers') + invocation value Direct + + let getIndexer (indexers: 'T seq) (target:obj): 'TResult = + let indexes = indexers |> Seq.map box |> Seq.toArray + let value = Dynamic.InvokeGetIndex(target, indexes) + invocation value Direct + + let set (propertyName:string) (value:obj) (target:obj) = + Dynamic.InvokeSet(target, propertyName, value) |> ignore + + let setChain (chainOfMembers: string seq) (value:obj) (target:obj) = let chainOfMembers' = String.concat "." chainOfMembers - Dynamic.InvokeSetChain(target, chainOfMembers', value) |> ignore - - /// dynamically call set index - let setIndexer (indexers: 'T seq) (value:obj) (target:obj) = - let indexes = indexers |> Seq.map box |> Seq.toArray - Dynamic.InvokeSetValueOnIndexes(target, value, indexes) |> ignore - - [] - let getIndex (target:obj) (indexers: 'T seq) : 'TResult = - target |> getIndexer indexers - - [] - let setIndex (target:obj) (indexers: 'T seq) (value:obj) = - target |> setIndexer indexers value - - [] - let addAssignMember (target:obj) (memberName:string) (value:obj) = - target |> memberAddAssign memberName value - - [] - let subtractAssignMember (target:obj) (memberName:string) (value:obj) = - target |> memberSubtractAssign memberName value - - [] - let invoke (target:obj) (memberName:string option) : 'TResult = - let memberOrDirect = match memberName with | Some mn -> Member mn | None -> Direct + Dynamic.InvokeSetChain(target, chainOfMembers', value) |> ignore + + /// dynamically call set index + let setIndexer (indexers: 'T seq) (value:obj) (target:obj) = + let indexes = indexers |> Seq.map box |> Seq.toArray + Dynamic.InvokeSetValueOnIndexes(target, value, indexes) |> ignore + + [] + let getIndex (target:obj) (indexers: 'T seq) : 'TResult = + target |> getIndexer indexers + + [] + let setIndex (target:obj) (indexers: 'T seq) (value:obj) = + target |> setIndexer indexers value + + [] + let addAssignMember (target:obj) (memberName:string) (value:obj) = + target |> memberAddAssign memberName value + + [] + let subtractAssignMember (target:obj) (memberName:string) (value:obj) = + target |> memberSubtractAssign memberName value + + [] + let invoke (target:obj) (memberName:string option) : 'TResult = + let memberOrDirect = match memberName with | Some mn -> Member mn | None -> Direct invocation target memberOrDirect \ No newline at end of file diff --git a/FSharp.Interop.Dynamic/Dyn.fsi b/FSharp.Interop.Dynamic/Dyn.fsi index 2c5302d..a6d4cc3 100644 --- a/FSharp.Interop.Dynamic/Dyn.fsi +++ b/FSharp.Interop.Dynamic/Dyn.fsi @@ -1,121 +1,121 @@ -namespace FSharp.Interop.Dynamic - /// Names the dynamic member to get or invoke, or Direct to invoke the target itself. - type Calling = - /// Named member with generic type arguments. - | GenericMember of string * System.Type array - /// Named member (property, field, or method). - | Member of string - /// Invoke the target itself, not a named member. - | Direct +namespace FSharp.Interop.Dynamic + /// Names the dynamic member to get or invoke, or Direct to invoke the target itself. + type Calling = + /// Named member with generic type arguments. + | GenericMember of string * System.Type array + /// Named member (property, field, or method). + | Member of string + /// Invoke the target itself, not a named member. + | Direct - /// - /// Function surface behind the operators. Use when you want piping, a name as data, or an operation ? does not spell. Target is last on most members so target |> Dyn.get "Name" works; invocation is the exception. - /// - module Dyn = begin - /// Static invoke context for dynamic calls to static members of . - val staticContext : target:System.Type -> Dynamitey.InvokeContext - /// Static invoke context for dynamic calls to static members of the type argument. - val staticTarget<'TTarget> : Dynamitey.InvokeContext - /// Implicit conversion of to . - val implicitConvertTo : convertType:System.Type -> target:obj -> 'TResult - /// Implicit conversion of to the inferred result type. - val implicitConvert : target:obj -> 'TResult - - /// Explicit conversion of to . - val explicitConvertTo : convertType:System.Type -> target:obj -> 'TResult - /// Explicit conversion of to the inferred result type. - val explicitConvert : target:obj -> 'TResult - /// Marks an argument with a name for DLR named-argument invoke. - val namedArg : name:string -> argValue:obj -> Dynamitey.InvokeArg - /// Dynamically call += on the named member (DLR add-assign, including events). - val memberAddAssign : memberName:string -> value:obj -> target:obj -> unit - /// Dynamically call -= on the named member (DLR subtract-assign, including events). - val memberSubtractAssign : - memberName:string -> value:obj -> target:obj -> unit - /// - /// Workhorse for dynamic get and invoke. Target is first because a function-typed result is a callable, not a get. - /// - /// Object, boxed function, or static context to read or invoke. - /// - /// Member or GenericMember to get or invoke that member; - /// Direct to invoke itself. - /// - /// - /// If the inferred result type is not a function: a DLR get (InvokeGet) for - /// Member / GenericMember; for Direct, - /// converted to that type (no get). - /// If the inferred result type is a function, a callable: applying it does a DLR invoke - /// (InvokeMember for a named member, Invoke for Direct). - /// A void CLR method is an unit-returning function. - /// - val invocation : target:obj -> memberName:Calling -> 'TResult - /// Invoke itself with . If the inferred result type is a function, returns a callable rather than applying immediately. - val invokeDirect : value:'a -> target:obj -> 'TResult - /// Invoke the named member of with . If the inferred result type is a function, returns a callable rather than applying immediately. - val invokeMember : memberName:string -> value:'a -> target:obj -> 'TResult - /// Invoke the named generic member of with and . If the inferred result type is a function, returns a callable rather than applying immediately. - val invokeGeneric : - memberName:string -> - typeArgs:seq -> value:'a -> target:obj -> 'TResult - /// - /// Gets a named property or field (InvokeGet). - /// If the inferred result type is a function, returns a callable rather than applying immediately. - /// - /// Member name to read. - /// Object to read from. - /// The converted member value, or a callable if the inferred result type is a function. - val get : propertyName:string -> target:obj -> 'TResult - /// - /// Gets a named member, or None when InvokeGet cannot find it. - /// Conversion failures still throw; they are not treated as missing. - /// - /// Member name to look up. - /// Object to read from. - /// - /// None if the binder cannot find the member. - /// Some value if the member is present and converts to 'T - /// (a present null is Some null for a reference 'T). - /// Throws if the member is present but cannot convert to 'T. - /// When 'T is a function type and the member is missing, returns None - /// immediately rather than a lazy callable. - /// - val tryGet : propertyName:string -> target:obj -> 'T option - /// - /// true when InvokeGet succeeds. A present null is still present. - /// - /// Member name to look up. - /// Object to read from. - /// - /// true if InvokeGet succeeds, including when the value is null. - /// false if the binder cannot find the member. - /// Does not convert the value, so this can be true while tryGet as a given type throws. - /// A getter that throws something other than a binder miss is not reported as missing. - /// - val exists : propertyName:string -> target:obj -> bool - /// Gets a chain of members (for example A.B from names A then B). - val getChain : chainOfMembers:seq -> target:obj -> 'TResult - /// Dynamically get an indexer value. - val getIndexer : indexers:seq<'T> -> target:obj -> 'TResult - /// Sets a named property or field (InvokeSet). - val set : propertyName:string -> value:obj -> target:obj -> unit - /// Sets a chain of members (for example A.B from names A then B). - val setChain : chainOfMembers:seq -> value:obj -> target:obj -> unit - /// Dynamically set an indexer value. - val setIndexer : indexers:seq<'T> -> value:obj -> target:obj -> unit - /// Obsolete. Use getIndexer (target last) instead. - [] - val getIndex : target:obj -> indexers:seq<'T> -> 'TResult - /// Obsolete. Use setIndexer (target last) instead. - [] - val setIndex : target:obj -> indexers:seq<'T> -> value:obj -> unit - /// Obsolete. Use memberAddAssign (target last) instead. - [] - val addAssignMember : target:obj -> memberName:string -> value:obj -> unit - /// Obsolete. Use memberSubtractAssign (target last) instead. - [] - val subtractAssignMember : - target:obj -> memberName:string -> value:obj -> unit - /// Obsolete. Use invocation with Member or Direct instead of Some name or None. - [] - val invoke : target:obj -> memberName:string option -> 'TResult + /// + /// Function surface behind the operators. Use when you want piping, a name as data, or an operation ? does not spell. Target is last on most members so target |> Dyn.get "Name" works; invocation is the exception. + /// + module Dyn = begin + /// Static invoke context for dynamic calls to static members of . + val staticContext : target:System.Type -> Dynamitey.InvokeContext + /// Static invoke context for dynamic calls to static members of the type argument. + val staticTarget<'TTarget> : Dynamitey.InvokeContext + /// Implicit conversion of to . + val implicitConvertTo : convertType:System.Type -> target:obj -> 'TResult + /// Implicit conversion of to the inferred result type. + val implicitConvert : target:obj -> 'TResult + + /// Explicit conversion of to . A null follows implicitConvertTo: null for a reference result type. + val explicitConvertTo : convertType:System.Type -> target:obj -> 'TResult + /// Explicit conversion of to the inferred result type. A null follows implicitConvert: null for a reference result type. + val explicitConvert : target:obj -> 'TResult + /// Marks an argument with a name for DLR named-argument invoke. + val namedArg : name:string -> argValue:obj -> Dynamitey.InvokeArg + /// Dynamically call += on the named member (DLR add-assign, including events). + val memberAddAssign : memberName:string -> value:obj -> target:obj -> unit + /// Dynamically call -= on the named member (DLR subtract-assign, including events). + val memberSubtractAssign : + memberName:string -> value:obj -> target:obj -> unit + /// + /// Workhorse for dynamic get and invoke. Target is first because a function-typed result is a callable, not a get. + /// + /// Object, boxed function, or static context to read or invoke. + /// + /// Member or GenericMember to get or invoke that member; + /// Direct to invoke itself. + /// + /// + /// If the inferred result type is not a function: a DLR get (InvokeGet) for + /// Member / GenericMember; for Direct, + /// converted to that type (no get). + /// If the inferred result type is a function, a callable: applying it does a DLR invoke + /// (InvokeMember for a named member, Invoke for Direct). + /// A void CLR method is an unit-returning function. + /// + val invocation : target:obj -> memberName:Calling -> 'TResult + /// Invoke itself with . If the inferred result type is a function, returns a callable rather than applying immediately. + val invokeDirect : value:'a -> target:obj -> 'TResult + /// Invoke the named member of with . If the inferred result type is a function, returns a callable rather than applying immediately. + val invokeMember : memberName:string -> value:'a -> target:obj -> 'TResult + /// Invoke the named generic member of with and . If the inferred result type is a function, returns a callable rather than applying immediately. + val invokeGeneric : + memberName:string -> + typeArgs:seq -> value:'a -> target:obj -> 'TResult + /// + /// Gets a named property or field (InvokeGet). + /// If the inferred result type is a function, returns a callable rather than applying immediately. + /// + /// Member name to read. + /// Object to read from. + /// The converted member value, or a callable if the inferred result type is a function. + val get : propertyName:string -> target:obj -> 'TResult + /// + /// Gets a named member, or None when InvokeGet cannot find it. + /// Conversion failures still throw; they are not treated as missing. + /// + /// Member name to look up. + /// Object to read from. + /// + /// None if is null or the binder cannot find the member. + /// Some value if the member is present and converts to 'T + /// (a present null is Some null for a reference 'T). + /// Throws if the member is present but cannot convert to 'T. + /// When 'T is a function type and the member is missing, returns None + /// immediately rather than a lazy callable. + /// + val tryGet : propertyName:string -> target:obj -> 'T option + /// + /// true when InvokeGet succeeds. A present null is still present. + /// + /// Member name to look up. + /// Object to read from. + /// + /// true if InvokeGet succeeds, including when the value is null. + /// false if is null or the binder cannot find the member. + /// Does not convert the value, so this can be true while tryGet as a given type throws. + /// A getter that throws something other than a binder miss is not reported as missing. + /// + val exists : propertyName:string -> target:obj -> bool + /// Gets a chain of members (for example A.B from names A then B). + val getChain : chainOfMembers:seq -> target:obj -> 'TResult + /// Dynamically get an indexer value. + val getIndexer : indexers:seq<'T> -> target:obj -> 'TResult + /// Sets a named property or field (InvokeSet). + val set : propertyName:string -> value:obj -> target:obj -> unit + /// Sets a chain of members (for example A.B from names A then B). + val setChain : chainOfMembers:seq -> value:obj -> target:obj -> unit + /// Dynamically set an indexer value. + val setIndexer : indexers:seq<'T> -> value:obj -> target:obj -> unit + /// Obsolete. Use getIndexer (target last) instead. + [] + val getIndex : target:obj -> indexers:seq<'T> -> 'TResult + /// Obsolete. Use setIndexer (target last) instead. + [] + val setIndex : target:obj -> indexers:seq<'T> -> value:obj -> unit + /// Obsolete. Use memberAddAssign (target last) instead. + [] + val addAssignMember : target:obj -> memberName:string -> value:obj -> unit + /// Obsolete. Use memberSubtractAssign (target last) instead. + [] + val subtractAssignMember : + target:obj -> memberName:string -> value:obj -> unit + /// Obsolete. Use invocation with Member or Direct instead of Some name or None. + [] + val invoke : target:obj -> memberName:string option -> 'TResult end \ No newline at end of file diff --git a/README.md b/README.md index 9be5953..0fa006a 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ You can still `open Dynamitey` for `Build`, `Dynamic.Curry`, `DynamicObjects.Dic - **Not trim-safe or NativeAOT-safe.** - **15 or more arguments** throw `TypeLoadException` (ImpromptuInterface was never shipped). Fourteen work. - **C# optional parameters** must all be passed. The DLR does not fill defaults. -- `tryGet` / `exists` on a **null target** throw `NullReferenceException` (not `None` / `false`). +- `tryGet` / `exists` on a **null target** are `None` / `false`. `Dyn.set` on a null target still throws `NullReferenceException`. - Do not build member names from untrusted input. [SECURITY.md](SECURITY.md). - Historical: .NET Core 2.0.0–2.0.2 broke `dynamic` on nested types inside generics ([#11](https://github.com/fsprojects/FSharp.Interop.Dynamic/issues/11)). Current TFMs are fine. diff --git a/Tests/Invocation.fs b/Tests/Invocation.fs index 4f1568f..80f721d 100644 --- a/Tests/Invocation.fs +++ b/Tests/Invocation.fs @@ -111,6 +111,22 @@ module Invocation = let toDecimal: int -> decimal = Dyn.implicitConvert toDecimal 50 |> should equal 50M + [] + let ``explicitConvert of null matches implicitConvert for a reference type`` () = + let viaImplicit: string = null |> Dyn.implicitConvert + let viaExplicit: string = null |> Dyn.explicitConvert + let viaExplicitTo: string = null |> Dyn.explicitConvertTo typeof + Assert.Null viaImplicit + Assert.Null viaExplicit + Assert.Null viaExplicitTo + + [] + let ``explicitConvert of null to a value type fails like implicitConvert`` () = + let implicitFail () : int = null |> Dyn.implicitConvert + let explicitFail () : int = null |> Dyn.explicitConvert + (fun () -> implicitFail () |> ignore) |> should throw typeof + (fun () -> explicitFail () |> ignore) |> should throw typeof + [] let ``getChain on a missing link raises a binder exception`` () = let target = ExpandoObject() diff --git a/Tests/TryGet.fs b/Tests/TryGet.fs index 052b4bb..52095cb 100644 --- a/Tests/TryGet.fs +++ b/Tests/TryGet.fs @@ -78,4 +78,13 @@ module TryGet = let ``tryGet returns None for a missing member even when the result type is a function`` () = let o = ExpandoObject() let missing: (int -> int) option = o |> Dyn.tryGet "nope" - missing |> should equal None \ No newline at end of file + missing |> should equal None + + [] + let ``tryGet on a null target is None`` () = + let missing: string option = (null: obj) |> Dyn.tryGet "X" + missing |> should equal None + + [] + let ``exists on a null target is false`` () = + (null: obj) |> Dyn.exists "X" |> should equal false \ No newline at end of file diff --git a/docfx/docs/caveats.md b/docfx/docs/caveats.md index a6123bc..060bf16 100644 --- a/docfx/docs/caveats.md +++ b/docfx/docs/caveats.md @@ -20,7 +20,7 @@ Annotate `'T` / `unit` so the binder and the conversion agree. ## Null target -`Dyn.tryGet` and `Dyn.exists` on a **null target** throw `NullReferenceException`. That is not a binder miss, so it is not `None` / `false`. Same for `Dyn.set`. A present null *value* is still `Some null` / `exists` true. Changing the null-target contract is [#111](https://github.com/fsprojects/FSharp.Interop.Dynamic/issues/111). +`Dyn.tryGet` on a null target is `None`. `Dyn.exists` is `false`. That is the same answer as a missing member. `Dyn.set` on a null target still throws `NullReferenceException`. A present null *value* is still `Some null` / `exists` true. ## More than 14 arguments diff --git a/docfx/docs/tryget.md b/docfx/docs/tryget.md index 9543fa6..d5ec7aa 100644 --- a/docfx/docs/tryget.md +++ b/docfx/docs/tryget.md @@ -27,7 +27,7 @@ Lookup is `Dynamic.InvokeGet` only. Then the existing conversion / callable wrap | Member present, value cannot convert to `'T` | throws (`InvalidCastException` or a binder exception) — **not** `None` | | Member missing, `'T` is a function type | `None` immediately (does not return a lazy callable) | | Getter throws something else | that exception propagates | -| Target is null | `NullReferenceException` (not `None`). [#110](https://github.com/fsprojects/FSharp.Interop.Dynamic/issues/110), [#111](https://github.com/fsprojects/FSharp.Interop.Dynamic/issues/111) | +| Target is null | `None` | ## `Dyn.exists` @@ -36,7 +36,7 @@ o |> Dyn.exists "myProp" // true o |> Dyn.exists "nope" // false ``` -True when `InvokeGet` succeeds. A present null is still present. A throwing getter is not reported as missing. A **null target** throws `NullReferenceException`, not `false`. +True when `InvokeGet` succeeds. A present null is still present. A throwing getter is not reported as missing. A null target is `false`. `exists` does not convert the value. That is the difference from `tryGet >> Option.isSome` when conversion would fail: `exists` can be true while `tryGet` as `int` throws. From bd142f01eae6326bd092f003faa4641f5b2c3fa6 Mon Sep 17 00:00:00 2001 From: Tom Atwood Date: Sat, 26 Sep 2026 12:25:57 -0400 Subject: [PATCH 2/3] chore: leave the rest of Dyn.fs byte-for-byte alone The first commit rewrote newlines in Dyn.fs. This puts back every line that was not the null check. --- FSharp.Interop.Dynamic/Dyn.fs | 374 ++++++++++++++++----------------- FSharp.Interop.Dynamic/Dyn.fsi | 240 ++++++++++----------- 2 files changed, 307 insertions(+), 307 deletions(-) diff --git a/FSharp.Interop.Dynamic/Dyn.fs b/FSharp.Interop.Dynamic/Dyn.fs index ae4e579..33052c9 100644 --- a/FSharp.Interop.Dynamic/Dyn.fs +++ b/FSharp.Interop.Dynamic/Dyn.fs @@ -1,216 +1,216 @@ -// -// Copyright 2011 Ekon Benefits -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and +// +// Copyright 2011 Ekon Benefits +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and // limitations under the License. -namespace FSharp.Interop.Dynamic -open System - +namespace FSharp.Interop.Dynamic +open System + type Calling = | GenericMember of string * Type array - | Member of string - | Direct - -module Dyn = - open Dynamitey - open Microsoft.CSharp.RuntimeBinder - open FSharp.Reflection - - let staticContext (target:Type) = InvokeContext.CreateStatic.Invoke(target) - - let staticTarget<'TTarget> = InvokeContext.CreateStatic.Invoke(typeof<'TTarget>) + | Member of string + | Direct + +module Dyn = + open Dynamitey + open Microsoft.CSharp.RuntimeBinder + open FSharp.Reflection + + let staticContext (target:Type) = InvokeContext.CreateStatic.Invoke(target) + + let staticTarget<'TTarget> = InvokeContext.CreateStatic.Invoke(typeof<'TTarget>) let implicitConvertTo (convertType:Type) (target:obj) : 'TResult = match target with - | null -> target + | null -> target | t -> Dynamic.InvokeConvert(t, convertType, explicit = false) |> unbox<'TResult> - let implicitConvert(target:obj) : 'TResult = + let implicitConvert(target:obj) : 'TResult = implicitConvertTo typeof<'TResult> target let explicitConvertTo (convertType:Type) (target:obj) : 'TResult = - match target with - | null -> target - | t -> Dynamic.InvokeConvert(t, convertType, explicit = true) - |> unbox<'TResult> - + match target with + | null -> target + | t -> Dynamic.InvokeConvert(t, convertType, explicit = true) + |> unbox<'TResult> + let explicitConvert (target:obj) : 'TResult = - explicitConvertTo typeof<'TResult> target - - let namedArg (name:string) (argValue:obj) = - InvokeArg(name, argValue) - - let memberAddAssign (memberName:string) (value:obj) (target:obj) = + explicitConvertTo typeof<'TResult> target + + let namedArg (name:string) (argValue:obj) = + InvokeArg(name, argValue) + + let memberAddAssign (memberName:string) (value:obj) (target:obj) = Dynamic.InvokeAddAssignMember(target, memberName, value) - let memberSubtractAssign (memberName:string) (value:obj) (target:obj) = + let memberSubtractAssign (memberName:string) (value:obj) (target:obj) = Dynamic.InvokeSubtractAssignMember(target, memberName, value) - let invocation (target:obj) (memberName:Calling) : 'TResult = - let resultType = typeof<'TResult> - //Helper to dynamically call call FSharpFuncs - let fsharpInvoke target' memberName' arg' = - let invokeName = InvokeMemberName("Invoke", null) //FSharpFunc Invoke - let invokeContext t = InvokeContext(t, typeof) //Improve cache hits by using the same context - let start = match memberName' with - | GenericMember (name, _) - | Member name -> Dynamic.InvokeGet(target', name) - | Direct -> target' - Dynamic.InvokeMember(invokeContext(start), invokeName, [|arg'|]) + let invocation (target:obj) (memberName:Calling) : 'TResult = + let resultType = typeof<'TResult> + //Helper to dynamically call call FSharpFuncs + let fsharpInvoke target' memberName' arg' = + let invokeName = InvokeMemberName("Invoke", null) //FSharpFunc Invoke + let invokeContext t = InvokeContext(t, typeof) //Improve cache hits by using the same context + let start = match memberName' with + | GenericMember (name, _) + | Member name -> Dynamic.InvokeGet(target', name) + | Direct -> target' + Dynamic.InvokeMember(invokeContext(start), invokeName, [|arg'|]) let (|NoConversion| Conversion|) t = - if t = typeof then NoConversion else Conversion + if t = typeof then NoConversion else Conversion let finalConvertResult finalType result :'TResult = - match finalType with - | x when FSharpType.IsFunction x -> // if return type is a function + match finalType with + | x when FSharpType.IsFunction x -> // if return type is a function let rec curriedLambda target type' arg' = - let result' = fsharpInvoke target Direct arg' - let _,retType = FSharpType.GetFunctionElements type' - if FSharpType.IsFunction retType then - FSharpValue.MakeFunction(retType, curriedLambda result' retType) - else - result' - FSharpValue.MakeFunction(finalType, curriedLambda result finalType) - | NoConversion -> result - | Conversion -> implicitConvert result - |> unbox - if not (FSharpType.IsFunction resultType) - then - match memberName with - | GenericMember (name, _) - | Member name -> Dynamic.InvokeGet(target, name) - | Direct -> target - |> finalConvertResult resultType - else - let lambda (arg:obj) = - let argType,returnType = FSharpType.GetFunctionElements resultType - let argArray = - match argType with - | a when FSharpType.IsTuple(a) -> FSharpValue.GetTupleFields(arg) - | a when a = typeof -> [| |] - | ____________________________ -> [|arg|] + let result' = fsharpInvoke target Direct arg' + let _,retType = FSharpType.GetFunctionElements type' + if FSharpType.IsFunction retType then + FSharpValue.MakeFunction(retType, curriedLambda result' retType) + else + result' + FSharpValue.MakeFunction(finalType, curriedLambda result finalType) + | NoConversion -> result + | Conversion -> implicitConvert result + |> unbox + if not (FSharpType.IsFunction resultType) + then + match memberName with + | GenericMember (name, _) + | Member name -> Dynamic.InvokeGet(target, name) + | Direct -> target + |> finalConvertResult resultType + else + let lambda (arg:obj) = + let argType,returnType = FSharpType.GetFunctionElements resultType + let argArray = + match argType with + | a when FSharpType.IsTuple(a) -> FSharpValue.GetTupleFields(arg) + | a when a = typeof -> [| |] + | ____________________________ -> [|arg|] let invoker k = - let memberName = - memberName |> function | GenericMember (name, targs) -> - InvokeMemberName(name, targs) + let memberName = + memberName |> function | GenericMember (name, targs) -> + InvokeMemberName(name, targs) | Member name -> - InvokeMemberName(name, null) - | Direct -> null - Invocation(k, memberName).Invoke(target, argArray) - let (|Action|Func|) t = if t = typeof then Action else Func - let result = - try //Either it has a member or it's something directly callable - match (returnType, memberName) with - | (Action, Direct) -> invoker(InvocationKind.InvokeAction) - | (Action, GenericMember _) - | (Action, Member _) -> invoker(InvocationKind.InvokeMemberAction) - | (Func, Direct) -> invoker(InvocationKind.Invoke) - | (Func, GenericMember _) - | (Func, Member _) -> invoker(InvocationKind.InvokeMember) - with //Last chance incase we are trying to invoke an fsharpfunc - | :? RuntimeBinderException as e -> - try - fsharpInvoke target memberName arg - with - | :? RuntimeBinderException as e2 -> - AggregateException(e, e2) |> raise - | _ -> - reraise() - | _ -> - reraise() - match returnType with - | Action | NoConversion -> result - | _____________________ -> result |> finalConvertResult returnType - FSharpValue.MakeFunction(resultType, lambda) |> unbox<'TResult> - - let invokeDirect value (target:obj) : 'TResult = + InvokeMemberName(name, null) + | Direct -> null + Invocation(k, memberName).Invoke(target, argArray) + let (|Action|Func|) t = if t = typeof then Action else Func + let result = + try //Either it has a member or it's something directly callable + match (returnType, memberName) with + | (Action, Direct) -> invoker(InvocationKind.InvokeAction) + | (Action, GenericMember _) + | (Action, Member _) -> invoker(InvocationKind.InvokeMemberAction) + | (Func, Direct) -> invoker(InvocationKind.Invoke) + | (Func, GenericMember _) + | (Func, Member _) -> invoker(InvocationKind.InvokeMember) + with //Last chance incase we are trying to invoke an fsharpfunc + | :? RuntimeBinderException as e -> + try + fsharpInvoke target memberName arg + with + | :? RuntimeBinderException as e2 -> + AggregateException(e, e2) |> raise + | _ -> + reraise() + | _ -> + reraise() + match returnType with + | Action | NoConversion -> result + | _____________________ -> result |> finalConvertResult returnType + FSharpValue.MakeFunction(resultType, lambda) |> unbox<'TResult> + + let invokeDirect value (target:obj) : 'TResult = invocation target Direct value - let invokeMember (memberName:string) value (target:obj) : 'TResult = + let invokeMember (memberName:string) value (target:obj) : 'TResult = invocation target (Member memberName) value - let invokeGeneric (memberName:string) (typeArgs:Type seq) value (target:obj) : 'TResult = - let typeArgs' = typeArgs |> Array.ofSeq - let genericMember = GenericMember (memberName, typeArgs') - invocation target genericMember value - - let get (propertyName:string) (target:obj) : 'TResult = - invocation target (Member propertyName) - - let tryGet (propertyName:string) (target:obj) : 'T option = - match target with - | null -> None - | target -> - let found = - try - Some (Dynamic.InvokeGet(target, propertyName)) - with - | :? RuntimeBinderException -> None - | _ -> reraise() - match found with - | None -> None - | Some value -> Some (invocation value Direct) - - let exists (propertyName:string) (target:obj) : bool = - match target with - | null -> false - | target -> - try - Dynamic.InvokeGet(target, propertyName) |> ignore - true - with - | :? RuntimeBinderException -> false - | _ -> reraise() - - let getChain (chainOfMembers:string seq) (target:obj) : 'TResult = - let chainOfMembers' = String.concat "." chainOfMembers - let value = Dynamic.InvokeGetChain(target, chainOfMembers') - invocation value Direct - - let getIndexer (indexers: 'T seq) (target:obj): 'TResult = - let indexes = indexers |> Seq.map box |> Seq.toArray - let value = Dynamic.InvokeGetIndex(target, indexes) - invocation value Direct - - let set (propertyName:string) (value:obj) (target:obj) = - Dynamic.InvokeSet(target, propertyName, value) |> ignore - - let setChain (chainOfMembers: string seq) (value:obj) (target:obj) = + let invokeGeneric (memberName:string) (typeArgs:Type seq) value (target:obj) : 'TResult = + let typeArgs' = typeArgs |> Array.ofSeq + let genericMember = GenericMember (memberName, typeArgs') + invocation target genericMember value + + let get (propertyName:string) (target:obj) : 'TResult = + invocation target (Member propertyName) + + let tryGet (propertyName:string) (target:obj) : 'T option = + match target with + | null -> None + | target -> + let found = + try + Some (Dynamic.InvokeGet(target, propertyName)) + with + | :? RuntimeBinderException -> None + | _ -> reraise() + match found with + | None -> None + | Some value -> Some (invocation value Direct) + + let exists (propertyName:string) (target:obj) : bool = + match target with + | null -> false + | target -> + try + Dynamic.InvokeGet(target, propertyName) |> ignore + true + with + | :? RuntimeBinderException -> false + | _ -> reraise() + + let getChain (chainOfMembers:string seq) (target:obj) : 'TResult = + let chainOfMembers' = String.concat "." chainOfMembers + let value = Dynamic.InvokeGetChain(target, chainOfMembers') + invocation value Direct + + let getIndexer (indexers: 'T seq) (target:obj): 'TResult = + let indexes = indexers |> Seq.map box |> Seq.toArray + let value = Dynamic.InvokeGetIndex(target, indexes) + invocation value Direct + + let set (propertyName:string) (value:obj) (target:obj) = + Dynamic.InvokeSet(target, propertyName, value) |> ignore + + let setChain (chainOfMembers: string seq) (value:obj) (target:obj) = let chainOfMembers' = String.concat "." chainOfMembers - Dynamic.InvokeSetChain(target, chainOfMembers', value) |> ignore - - /// dynamically call set index - let setIndexer (indexers: 'T seq) (value:obj) (target:obj) = - let indexes = indexers |> Seq.map box |> Seq.toArray - Dynamic.InvokeSetValueOnIndexes(target, value, indexes) |> ignore - - [] - let getIndex (target:obj) (indexers: 'T seq) : 'TResult = - target |> getIndexer indexers - - [] - let setIndex (target:obj) (indexers: 'T seq) (value:obj) = - target |> setIndexer indexers value - - [] - let addAssignMember (target:obj) (memberName:string) (value:obj) = - target |> memberAddAssign memberName value - - [] - let subtractAssignMember (target:obj) (memberName:string) (value:obj) = - target |> memberSubtractAssign memberName value - - [] - let invoke (target:obj) (memberName:string option) : 'TResult = - let memberOrDirect = match memberName with | Some mn -> Member mn | None -> Direct + Dynamic.InvokeSetChain(target, chainOfMembers', value) |> ignore + + /// dynamically call set index + let setIndexer (indexers: 'T seq) (value:obj) (target:obj) = + let indexes = indexers |> Seq.map box |> Seq.toArray + Dynamic.InvokeSetValueOnIndexes(target, value, indexes) |> ignore + + [] + let getIndex (target:obj) (indexers: 'T seq) : 'TResult = + target |> getIndexer indexers + + [] + let setIndex (target:obj) (indexers: 'T seq) (value:obj) = + target |> setIndexer indexers value + + [] + let addAssignMember (target:obj) (memberName:string) (value:obj) = + target |> memberAddAssign memberName value + + [] + let subtractAssignMember (target:obj) (memberName:string) (value:obj) = + target |> memberSubtractAssign memberName value + + [] + let invoke (target:obj) (memberName:string option) : 'TResult = + let memberOrDirect = match memberName with | Some mn -> Member mn | None -> Direct invocation target memberOrDirect \ No newline at end of file diff --git a/FSharp.Interop.Dynamic/Dyn.fsi b/FSharp.Interop.Dynamic/Dyn.fsi index a6d4cc3..0c76b3f 100644 --- a/FSharp.Interop.Dynamic/Dyn.fsi +++ b/FSharp.Interop.Dynamic/Dyn.fsi @@ -1,121 +1,121 @@ -namespace FSharp.Interop.Dynamic - /// Names the dynamic member to get or invoke, or Direct to invoke the target itself. - type Calling = - /// Named member with generic type arguments. - | GenericMember of string * System.Type array - /// Named member (property, field, or method). - | Member of string - /// Invoke the target itself, not a named member. - | Direct - - /// - /// Function surface behind the operators. Use when you want piping, a name as data, or an operation ? does not spell. Target is last on most members so target |> Dyn.get "Name" works; invocation is the exception. - /// - module Dyn = begin - /// Static invoke context for dynamic calls to static members of . - val staticContext : target:System.Type -> Dynamitey.InvokeContext - /// Static invoke context for dynamic calls to static members of the type argument. - val staticTarget<'TTarget> : Dynamitey.InvokeContext - /// Implicit conversion of to . - val implicitConvertTo : convertType:System.Type -> target:obj -> 'TResult - /// Implicit conversion of to the inferred result type. - val implicitConvert : target:obj -> 'TResult - - /// Explicit conversion of to . A null follows implicitConvertTo: null for a reference result type. - val explicitConvertTo : convertType:System.Type -> target:obj -> 'TResult - /// Explicit conversion of to the inferred result type. A null follows implicitConvert: null for a reference result type. - val explicitConvert : target:obj -> 'TResult - /// Marks an argument with a name for DLR named-argument invoke. - val namedArg : name:string -> argValue:obj -> Dynamitey.InvokeArg - /// Dynamically call += on the named member (DLR add-assign, including events). - val memberAddAssign : memberName:string -> value:obj -> target:obj -> unit - /// Dynamically call -= on the named member (DLR subtract-assign, including events). - val memberSubtractAssign : - memberName:string -> value:obj -> target:obj -> unit - /// - /// Workhorse for dynamic get and invoke. Target is first because a function-typed result is a callable, not a get. - /// - /// Object, boxed function, or static context to read or invoke. - /// - /// Member or GenericMember to get or invoke that member; - /// Direct to invoke itself. - /// - /// - /// If the inferred result type is not a function: a DLR get (InvokeGet) for - /// Member / GenericMember; for Direct, - /// converted to that type (no get). - /// If the inferred result type is a function, a callable: applying it does a DLR invoke - /// (InvokeMember for a named member, Invoke for Direct). - /// A void CLR method is an unit-returning function. - /// - val invocation : target:obj -> memberName:Calling -> 'TResult - /// Invoke itself with . If the inferred result type is a function, returns a callable rather than applying immediately. - val invokeDirect : value:'a -> target:obj -> 'TResult - /// Invoke the named member of with . If the inferred result type is a function, returns a callable rather than applying immediately. - val invokeMember : memberName:string -> value:'a -> target:obj -> 'TResult - /// Invoke the named generic member of with and . If the inferred result type is a function, returns a callable rather than applying immediately. - val invokeGeneric : - memberName:string -> - typeArgs:seq -> value:'a -> target:obj -> 'TResult - /// - /// Gets a named property or field (InvokeGet). - /// If the inferred result type is a function, returns a callable rather than applying immediately. - /// - /// Member name to read. - /// Object to read from. - /// The converted member value, or a callable if the inferred result type is a function. - val get : propertyName:string -> target:obj -> 'TResult - /// - /// Gets a named member, or None when InvokeGet cannot find it. - /// Conversion failures still throw; they are not treated as missing. - /// - /// Member name to look up. - /// Object to read from. - /// - /// None if is null or the binder cannot find the member. - /// Some value if the member is present and converts to 'T - /// (a present null is Some null for a reference 'T). - /// Throws if the member is present but cannot convert to 'T. - /// When 'T is a function type and the member is missing, returns None - /// immediately rather than a lazy callable. - /// - val tryGet : propertyName:string -> target:obj -> 'T option - /// - /// true when InvokeGet succeeds. A present null is still present. - /// - /// Member name to look up. - /// Object to read from. - /// - /// true if InvokeGet succeeds, including when the value is null. - /// false if is null or the binder cannot find the member. - /// Does not convert the value, so this can be true while tryGet as a given type throws. - /// A getter that throws something other than a binder miss is not reported as missing. - /// - val exists : propertyName:string -> target:obj -> bool - /// Gets a chain of members (for example A.B from names A then B). - val getChain : chainOfMembers:seq -> target:obj -> 'TResult - /// Dynamically get an indexer value. - val getIndexer : indexers:seq<'T> -> target:obj -> 'TResult - /// Sets a named property or field (InvokeSet). - val set : propertyName:string -> value:obj -> target:obj -> unit - /// Sets a chain of members (for example A.B from names A then B). - val setChain : chainOfMembers:seq -> value:obj -> target:obj -> unit - /// Dynamically set an indexer value. - val setIndexer : indexers:seq<'T> -> value:obj -> target:obj -> unit - /// Obsolete. Use getIndexer (target last) instead. - [] - val getIndex : target:obj -> indexers:seq<'T> -> 'TResult - /// Obsolete. Use setIndexer (target last) instead. - [] - val setIndex : target:obj -> indexers:seq<'T> -> value:obj -> unit - /// Obsolete. Use memberAddAssign (target last) instead. - [] - val addAssignMember : target:obj -> memberName:string -> value:obj -> unit - /// Obsolete. Use memberSubtractAssign (target last) instead. - [] - val subtractAssignMember : - target:obj -> memberName:string -> value:obj -> unit - /// Obsolete. Use invocation with Member or Direct instead of Some name or None. - [] - val invoke : target:obj -> memberName:string option -> 'TResult +namespace FSharp.Interop.Dynamic + /// Names the dynamic member to get or invoke, or Direct to invoke the target itself. + type Calling = + /// Named member with generic type arguments. + | GenericMember of string * System.Type array + /// Named member (property, field, or method). + | Member of string + /// Invoke the target itself, not a named member. + | Direct + + /// + /// Function surface behind the operators. Use when you want piping, a name as data, or an operation ? does not spell. Target is last on most members so target |> Dyn.get "Name" works; invocation is the exception. + /// + module Dyn = begin + /// Static invoke context for dynamic calls to static members of . + val staticContext : target:System.Type -> Dynamitey.InvokeContext + /// Static invoke context for dynamic calls to static members of the type argument. + val staticTarget<'TTarget> : Dynamitey.InvokeContext + /// Implicit conversion of to . + val implicitConvertTo : convertType:System.Type -> target:obj -> 'TResult + /// Implicit conversion of to the inferred result type. + val implicitConvert : target:obj -> 'TResult + + /// Explicit conversion of to . A null follows implicitConvertTo: null for a reference result type. + val explicitConvertTo : convertType:System.Type -> target:obj -> 'TResult + /// Explicit conversion of to the inferred result type. A null follows implicitConvert: null for a reference result type. + val explicitConvert : target:obj -> 'TResult + /// Marks an argument with a name for DLR named-argument invoke. + val namedArg : name:string -> argValue:obj -> Dynamitey.InvokeArg + /// Dynamically call += on the named member (DLR add-assign, including events). + val memberAddAssign : memberName:string -> value:obj -> target:obj -> unit + /// Dynamically call -= on the named member (DLR subtract-assign, including events). + val memberSubtractAssign : + memberName:string -> value:obj -> target:obj -> unit + /// + /// Workhorse for dynamic get and invoke. Target is first because a function-typed result is a callable, not a get. + /// + /// Object, boxed function, or static context to read or invoke. + /// + /// Member or GenericMember to get or invoke that member; + /// Direct to invoke itself. + /// + /// + /// If the inferred result type is not a function: a DLR get (InvokeGet) for + /// Member / GenericMember; for Direct, + /// converted to that type (no get). + /// If the inferred result type is a function, a callable: applying it does a DLR invoke + /// (InvokeMember for a named member, Invoke for Direct). + /// A void CLR method is an unit-returning function. + /// + val invocation : target:obj -> memberName:Calling -> 'TResult + /// Invoke itself with . If the inferred result type is a function, returns a callable rather than applying immediately. + val invokeDirect : value:'a -> target:obj -> 'TResult + /// Invoke the named member of with . If the inferred result type is a function, returns a callable rather than applying immediately. + val invokeMember : memberName:string -> value:'a -> target:obj -> 'TResult + /// Invoke the named generic member of with and . If the inferred result type is a function, returns a callable rather than applying immediately. + val invokeGeneric : + memberName:string -> + typeArgs:seq -> value:'a -> target:obj -> 'TResult + /// + /// Gets a named property or field (InvokeGet). + /// If the inferred result type is a function, returns a callable rather than applying immediately. + /// + /// Member name to read. + /// Object to read from. + /// The converted member value, or a callable if the inferred result type is a function. + val get : propertyName:string -> target:obj -> 'TResult + /// + /// Gets a named member, or None when InvokeGet cannot find it. + /// Conversion failures still throw; they are not treated as missing. + /// + /// Member name to look up. + /// Object to read from. + /// + /// None if is null or the binder cannot find the member. + /// Some value if the member is present and converts to 'T + /// (a present null is Some null for a reference 'T). + /// Throws if the member is present but cannot convert to 'T. + /// When 'T is a function type and the member is missing, returns None + /// immediately rather than a lazy callable. + /// + val tryGet : propertyName:string -> target:obj -> 'T option + /// + /// true when InvokeGet succeeds. A present null is still present. + /// + /// Member name to look up. + /// Object to read from. + /// + /// true if InvokeGet succeeds, including when the value is null. + /// false if is null or the binder cannot find the member. + /// Does not convert the value, so this can be true while tryGet as a given type throws. + /// A getter that throws something other than a binder miss is not reported as missing. + /// + val exists : propertyName:string -> target:obj -> bool + /// Gets a chain of members (for example A.B from names A then B). + val getChain : chainOfMembers:seq -> target:obj -> 'TResult + /// Dynamically get an indexer value. + val getIndexer : indexers:seq<'T> -> target:obj -> 'TResult + /// Sets a named property or field (InvokeSet). + val set : propertyName:string -> value:obj -> target:obj -> unit + /// Sets a chain of members (for example A.B from names A then B). + val setChain : chainOfMembers:seq -> value:obj -> target:obj -> unit + /// Dynamically set an indexer value. + val setIndexer : indexers:seq<'T> -> value:obj -> target:obj -> unit + /// Obsolete. Use getIndexer (target last) instead. + [] + val getIndex : target:obj -> indexers:seq<'T> -> 'TResult + /// Obsolete. Use setIndexer (target last) instead. + [] + val setIndex : target:obj -> indexers:seq<'T> -> value:obj -> unit + /// Obsolete. Use memberAddAssign (target last) instead. + [] + val addAssignMember : target:obj -> memberName:string -> value:obj -> unit + /// Obsolete. Use memberSubtractAssign (target last) instead. + [] + val subtractAssignMember : + target:obj -> memberName:string -> value:obj -> unit + /// Obsolete. Use invocation with Member or Direct instead of Some name or None. + [] + val invoke : target:obj -> memberName:string option -> 'TResult end \ No newline at end of file From 36ffbccc65118cfd381bae3be899f8fd7ae94bca Mon Sep 17 00:00:00 2001 From: Tom Atwood Date: Sat, 26 Sep 2026 12:28:28 -0400 Subject: [PATCH 3/3] test: cover explicitConvertTo of null to int (#111) Copilot asked for the value-type path on explicitConvertTo, not only the wrapper. --- Tests/Invocation.fs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/Invocation.fs b/Tests/Invocation.fs index 80f721d..dbd25d0 100644 --- a/Tests/Invocation.fs +++ b/Tests/Invocation.fs @@ -124,8 +124,10 @@ module Invocation = let ``explicitConvert of null to a value type fails like implicitConvert`` () = let implicitFail () : int = null |> Dyn.implicitConvert let explicitFail () : int = null |> Dyn.explicitConvert + let explicitToFail () : int = null |> Dyn.explicitConvertTo typeof (fun () -> implicitFail () |> ignore) |> should throw typeof (fun () -> explicitFail () |> ignore) |> should throw typeof + (fun () -> explicitToFail () |> ignore) |> should throw typeof [] let ``getChain on a missing link raises a binder exception`` () =