diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 46978db5..432834f2 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -12,6 +12,13 @@
```text
/
+├── src/Cosmos.Quotations/ – translator of F# quotations into queries (FSharp.Azure.Cosmos.Quotations), built stage by stage and not packed yet
+│ ├── TranslationError.fs – translation errors and the place of a quotation in the source
+│ ├── Normalizer.fs – normal form of a quotation
+│ ├── Evaluator.fs – value of a part of a quotation that reads no variable
+│ ├── Query.fs – functions that say whether a value is a constant or a parameter of the query
+│ ├── Parameterizer.fs – shape of a quotation, by which its translation is found again, and the values of its parameters
+│ └── JsonLayout.fs – how the serializer writes the members of a document: their names and what a member without a value is
├── src/Cosmos/ – main library (FSharp.Azure.Cosmos)
│ ├── SubStatusCodes.fs – documented Cosmos DB sub-status codes
│ ├── Cosmos.fs – core types and container extensions
@@ -28,6 +35,7 @@
├── src/Shared/ – source files that every F# project under src and tests compiles
│ └── ValueCollections.fs – `Seq`, `List` and `Array` functions that return `voption` and struct tuples
├── tests/Cosmos.Tests/ – MSTest integration test project
+├── tests/Cosmos.Quotations.Tests/ – MSTest unit tests of FSharp.Azure.Cosmos.Quotations, no emulator needed
├── tests/Cosmos.Tests.Infrastructure/ – shared test fixtures, emulator settings and assertion helpers
├── build/ – FAKE build scripts
└── docsSrc/ – FSharp.Formatting documentation source
@@ -39,6 +47,7 @@
* [`FSharp.Control.TaskSeq`](https://github.com/fsprojects/FSharp.Control.TaskSeq) – `IAsyncEnumerable` / `taskSeq` support
* [`FSharp.Control.Reactive`](https://github.com/fsprojects/FSharp.Reactive) – Rx extensions for F#
* [`FsToolkit.ErrorHandling`](https://github.com/demystifyfp/FsToolkit.ErrorHandling) – `taskResult`, `Result`, `voption` CEs
+* [`FSharp.SystemTextJson`](https://github.com/Tarmil/FSharp.SystemTextJson) – F# types in `System.Text.Json`; `FSharp.Azure.Cosmos.Quotations` reads its options to know how documents are written
* [`Unquote`](https://github.com/SwensenSoftware/unquote) – test assertions
* [`MSTest`](https://github.com/microsoft/testfx) – test framework
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 69f8e798..279a2585 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,6 +8,7 @@
+
diff --git a/FSharp.Azure.Cosmos.slnf b/FSharp.Azure.Cosmos.slnf
index 1a0488cc..bb4984ca 100644
--- a/FSharp.Azure.Cosmos.slnf
+++ b/FSharp.Azure.Cosmos.slnf
@@ -2,7 +2,9 @@
"solution": {
"path": "FSharp.Azure.Cosmos.slnx",
"projects": [
+ "src\\Cosmos.Quotations\\FSharp.Azure.Cosmos.Quotations.fsproj",
"src\\Cosmos\\FSharp.Azure.Cosmos.fsproj",
+ "tests\\Cosmos.Quotations.Tests\\FSharp.Azure.Cosmos.Quotations.Tests.fsproj",
"tests\\Cosmos.Tests.Infrastructure\\FSharp.Azure.Cosmos.Tests.Infrastructure.fsproj",
"tests\\Cosmos.Tests\\FSharp.Azure.Cosmos.Tests.fsproj"
]
diff --git a/FSharp.Azure.Cosmos.slnx b/FSharp.Azure.Cosmos.slnx
index 4ec89bbd..3b8b2185 100644
--- a/FSharp.Azure.Cosmos.slnx
+++ b/FSharp.Azure.Cosmos.slnx
@@ -10,7 +10,9 @@
+
+
diff --git a/src/Cosmos.Quotations/AssemblyInfo.fs b/src/Cosmos.Quotations/AssemblyInfo.fs
new file mode 100644
index 00000000..08966c65
--- /dev/null
+++ b/src/Cosmos.Quotations/AssemblyInfo.fs
@@ -0,0 +1,23 @@
+// Auto-Generated by FAKE; do not edit
+namespace System
+open System.Reflection
+
+[]
+[]
+[]
+[]
+[]
+[]
+[]
+[]
+do ()
+
+module internal AssemblyVersionInformation =
+ let [] AssemblyTitle = "FSharp.Azure.Cosmos.Quotations"
+ let [] AssemblyProduct = "FSharp.Azure.Cosmos"
+ let [] AssemblyVersion = "1.2.0"
+ let [] AssemblyMetadata_ReleaseDate = "2026-10-01T00:00:00.0000000+02:00"
+ let [] AssemblyFileVersion = "1.2.0"
+ let [] AssemblyInformationalVersion = "1.2.0"
+ let [] AssemblyMetadata_ReleaseChannel = "release"
+ let [] AssemblyMetadata_GitHash = "210cf041a0ce44fcc5a5df369550a77a1f75d436"
diff --git a/src/Cosmos.Quotations/Evaluator.fs b/src/Cosmos.Quotations/Evaluator.fs
new file mode 100644
index 00000000..9814adb9
--- /dev/null
+++ b/src/Cosmos.Quotations/Evaluator.fs
@@ -0,0 +1,693 @@
+///
+/// Computes the value of a part of a quotation that reads no variable. Such a part does not depend on the document, so
+/// its value is known before the query runs and becomes a parameter of the query.
+///
+///
+///
+/// The part has to be in the normal form that
+/// gives:
+/// bindings, pipes and calls of functions with reflected definitions are gone. What is left to compute is done by
+/// reflection: reading properties and fields, calling methods and constructors, building and reading tuples, records,
+/// unions and arrays, testing a type or a union case, choosing between two branches and applying a captured function.
+///
+///
+/// An operator of FSharp.Core that refuses to be called by reflection, such as -, / and the conversions
+/// between number types, is computed by the function that FSharp.Core has for operands of any types.
+///
+///
+/// Nothing is compiled. Turning a quotation into a delegate costs hundreds of microseconds, a thousand times what
+/// reflection costs for a property chain, so a part that holds a function written in the quotation has no value
+/// here.
+///
+///
+/// A choice computes only the branch that its condition chooses, as compiled code does. && and
+/// || are choices, so text.IsSome && text.Value.Length > 0 over a captured option without a
+/// value is and raises nothing.
+///
+///
+/// A node is computed once. The normalizer writes a value that a bound at every place it is
+/// used, as the very same node, and the code computes a bound value once: let made = obj () in (made, made) is
+/// a pair of one object, here as in compiled code.
+///
+///
+/// One node at two places is one value whatever put it there: one quotation that is spliced into two places, or a
+/// tree that is built through the API with one node twice, is computed once as well, where compiled code would run it
+/// at each place. For code without side effects the two cannot differ.
+///
+///
+module internal FSharp.Azure.Cosmos.Quotations.Evaluator
+
+open System
+open System.Collections.Concurrent
+open System.Collections.Frozen
+open System.Collections.Generic
+open System.Collections.Immutable
+open System.Reflection
+open System.Runtime.CompilerServices
+open FSharp.Quotations
+open FSharp.Quotations.DerivedPatterns
+open FSharp.Quotations.Patterns
+open FSharp.Reflection
+
+/// A part that has no value without compiling it or without a variable: a function, a variable, a quotation.
+exception private NotComputable of part : Expr
+
+/// The members of a type whatever their accessibility: a record or union of the caller may be internal or private.
+[]
+let private AnyAccessibility = BindingFlags.Public ||| BindingFlags.NonPublic
+
+// The reflection functions of FSharp.Core declare the values they take as not null. Null is a value they handle all
+// the same: it is what a tuple, a record or a union case holds for a reference that is null, and how a union such as
+// option represents its case without fields. These two functions are the one place that says so.
+#nowarn "3261"
+
+///
+/// as the reflection functions of FSharp.Core take them.
+///
+let private asValues (values : objnull array) : obj array = values
+
+///
+/// as the reflection functions of FSharp.Core take it.
+///
+let private asValue (value : objnull) : obj = value
+
+#warnon "3261"
+
+///
+/// as a function that computes its result once for every key and keeps it for the process.
+/// What is kept here is what reflection finds out about a type or a method, which takes microseconds to find and never
+/// changes.
+///
+let private memoize<'Key, 'Value when 'Key : not null> (create : 'Key -> 'Value) : 'Key -> 'Value =
+ let results = ConcurrentDictionary<'Key, 'Value>()
+ // The delegate is created once: a new one for every lookup would be an allocation for every lookup.
+ let factory = Func<'Key, 'Value> create
+ fun key -> results.GetOrAdd (key, factory)
+
+/// The function that builds a tuple of a type from its items.
+let private tupleConstructorOf =
+ memoize (fun (tupleType : Type) -> FSharpValue.PreComputeTupleConstructor tupleType)
+
+/// The function that takes a tuple of a type apart into its items, also the items beyond the seventh.
+let private tupleReaderOf =
+ memoize (fun (tupleType : Type) -> FSharpValue.PreComputeTupleReader tupleType)
+
+/// The function that builds a record of a type, also of an anonymous record type, from the values of its fields.
+let private recordConstructorOf =
+ memoize (fun (recordType : Type) -> FSharpValue.PreComputeRecordConstructor (recordType, AnyAccessibility))
+
+/// The function that builds a value of a union case from the values of its fields.
+let private unionConstructorOf =
+ memoize (fun (case : UnionCaseInfo) -> FSharpValue.PreComputeUnionConstructor (case, AnyAccessibility))
+
+/// The function that reads the tag of a value of a union type.
+let private tagReaderOf =
+ memoize (fun (unionType : Type) -> FSharpValue.PreComputeUnionTagReader (unionType, AnyAccessibility))
+
+///
+/// The method that applies a function value of a type to one argument:
+/// of that type.
+///
+let private invokerOf =
+ memoize (fun (functionType : Type) ->
+ functionType.GetMethod ("Invoke", [| FSharpType.GetFunctionElements functionType |> fst |])
+ |> nonNull
+ )
+
+///
+/// The generic definition of the method that calls.
+///
+let private calledDefinition (call : Expr) : MethodInfo =
+ match call with
+ | Call (_, method, _) -> method.GetGenericMethodDefinition ()
+ | _ -> invalidArg (nameof call) $"The quotation is not a call: %A{call}"
+
+///
+/// An operator of FSharp.Core that refuses to be called by reflection, with the function that stands in for it:
+/// the one that the compiler passes to , a call of the operator on integers, as the
+/// witness of the operator. Both as generic definitions.
+///
+let private operatorWithWitness (template : Expr) : struct (MethodInfo * MethodInfo) =
+ match template with
+ | CallWithWitnesses (None, operator, _, [ Lambdas (_, Call (None, witness, _)) ], _) ->
+ struct (operator.GetGenericMethodDefinition (), witness.GetGenericMethodDefinition ())
+ | _ -> invalidArg (nameof template) $"The quotation is not a call of an operator with one witness: %A{template}"
+
+// FS1204 says that these two functions are for dynamic invocation and not for direct use. Dynamic invocation is what
+// they are taken for: the quotations only name them and are never run.
+#nowarn "1204"
+
+///
+/// The function of FSharp.Core that negates a value of any type and raises
+/// when the result does not fit.
+///
+let private checkedNegation =
+ calledDefinition <@ LanguagePrimitives.CheckedUnaryNegationDynamic 1 @>
+
+///
+/// The function of FSharp.Core that converts a value of any type to another type and raises
+/// when the value does not fit.
+///
+let private checkedConversion =
+ calledDefinition <@ LanguagePrimitives.CheckedExplicitDynamic 1 @>
+
+#warnon "1204"
+
+///
+/// The operators of FSharp.Core that refuse to be called by reflection, by the metadata token of the method, each with
+/// the function of FSharp.Core that computes it for operands whose types are known only at run time.
+///
+///
+///
+/// These operators are compiled into the code that uses them, and their own methods raise
+/// , "Dynamic invocation of op_Subtraction is not supported". For every one
+/// of them has a function that does the same for any types:
+/// for the built-in number types directly, and for another type through the operator that the type defines. The
+/// compiler passes that function to the operator as its witness, which is where the pairs are taken from.
+///
+///
+/// The checked operators are paired by hand, because the compiler passes them the unchecked functions: a checked
+/// conversion that does not fit must raise, as it does in compiled code.
+///
+///
+/// +, *, the comparisons and the mathematical functions are not here: reflection can call them.
+/// int is here although it can be called, because its own method fails for an enumeration value.
+///
+///
+let dynamicOperators : FrozenDictionary =
+ seq {
+ yield!
+ seq {
+ <@@ 1 - 1 @@>
+ <@@ 1 / 1 @@>
+ <@@ 1 % 1 @@>
+ <@@ -(1) @@>
+ <@@ +(1) @@>
+ <@@ ~~~1 @@>
+ <@@ 1 &&& 1 @@>
+ <@@ 1 ||| 1 @@>
+ <@@ 1 ^^^ 1 @@>
+ <@@ 1 <<< 1 @@>
+ <@@ 1 >>> 1 @@>
+ <@@ byte 1 @@>
+ <@@ sbyte 1 @@>
+ <@@ int16 1 @@>
+ <@@ uint16 1 @@>
+ <@@ int 1L @@>
+ <@@ int32 1L @@>
+ <@@ uint32 1 @@>
+ <@@ int64 1 @@>
+ <@@ uint64 1 @@>
+ <@@ nativeint 1 @@>
+ <@@ unativeint 1 @@>
+ <@@ float 1 @@>
+ <@@ float32 1 @@>
+ <@@ decimal 1 @@>
+ <@@ char 1 @@>
+ }
+ |> Seq.map operatorWithWitness
+
+ yield struct (calledDefinition <@@ Checked.(~-) 1 @@>, checkedNegation)
+
+ yield!
+ seq {
+ <@@ Checked.byte 1 @@>
+ <@@ Checked.sbyte 1 @@>
+ <@@ Checked.int16 1 @@>
+ <@@ Checked.uint16 1 @@>
+ <@@ Checked.int 1L @@>
+ <@@ Checked.int32 1L @@>
+ <@@ Checked.uint32 1 @@>
+ <@@ Checked.int64 1 @@>
+ <@@ Checked.uint64 1 @@>
+ <@@ Checked.nativeint 1 @@>
+ <@@ Checked.unativeint 1 @@>
+ <@@ Checked.char 1 @@>
+ }
+ |> Seq.map (fun template -> struct (calledDefinition template, checkedConversion))
+ }
+ |> _.ToFrozenDictionary((fun (struct (operator, _)) -> operator.MetadataToken), (fun (struct (_, dynamic)) -> dynamic))
+
+/// The module of FSharp.Core, in which the metadata tokens of its operators are unique.
+let private coreModule = typeof.Module
+
+///
+/// The function that stands in for a call of one of the
+/// with its type arguments.
+///
+[]
+type private DynamicCall = {
+ /// The function of FSharp.Core, made for the types of the operands and of the result.
+ Function : MethodInfo
+ ///
+ /// The enumeration type of the result, when the operator gives one. The functions of FSharp.Core fail for an
+ /// enumeration type, so they are made for the underlying number type; reflection passes them an enumeration value
+ /// as its number, and a number that comes back becomes a value of this type again.
+ ///
+ ResultEnumeration : Type voption
+}
+
+///
+/// , or the number type under it when it is an enumeration type.
+///
+let private numberTypeOf (valueType : Type) : Type =
+ if valueType.IsEnum then
+ Enum.GetUnderlyingType valueType
+ else
+ valueType
+
+///
+/// The members of as functions that take the nullable.
+///
+///
+/// Reflection has nothing to call a member of a nullable on: a nullable is boxed as its value or, when it has none, as
+/// , never as itself. As an argument it can be passed, because there is
+/// taken for the nullable without a value.
+///
+[]
+type private NullableMembers<'Value when 'Value : struct and 'Value : (new : unit -> 'Value) and 'Value :> ValueType> =
+
+ ///
+ /// of .
+ ///
+ static member HasValue (nullable : Nullable<'Value>) : bool = nullable.HasValue
+
+ ///
+ /// of .
+ ///
+ static member Value (nullable : Nullable<'Value>) : 'Value = nullable.Value
+
+ ///
+ /// of .
+ ///
+ static member GetValueOrDefault (nullable : Nullable<'Value>) : 'Value = nullable.GetValueOrDefault ()
+
+ ///
+ /// of .
+ ///
+ static member GetValueOrDefault (nullable : Nullable<'Value>, defaultValue : 'Value) : 'Value =
+ nullable.GetValueOrDefault defaultValue
+
+ ///
+ /// of .
+ ///
+ static member Equals (nullable : Nullable<'Value>, other : objnull) : bool = nullable.Equals other
+
+ ///
+ /// of .
+ ///
+ static member GetHashCode (nullable : Nullable<'Value>) : int = nullable.GetHashCode ()
+
+ ///
+ /// of .
+ ///
+ static member ToString (nullable : Nullable<'Value>) : string | null = nullable.ToString ()
+
+///
+/// Whether is a member of .
+///
+let private isOfNullable (memberOfType : MemberInfo) : bool =
+ match memberOfType.DeclaringType with
+ | null -> false
+ | declaringType ->
+ // Nearly every member is one of a class, which the first test tells.
+ declaringType.IsValueType
+ && declaringType.IsGenericType
+ && Type.(=) (declaringType.GetGenericTypeDefinition (), typedefof>)
+
+///
+/// The function that stands in for a method of , also for the getter of a property:
+/// the function of the same name that takes the nullable before the arguments of the method.
+///
+let private nullableStandInOf =
+ memoize (fun (method : MethodInfo) ->
+ let nullableType = nonNull method.DeclaringType
+ // The getter of a property has the name of the property after "get_".
+ let name =
+ if method.IsSpecialName then
+ method.Name.Substring 4
+ else
+ method.Name
+
+ let parameterTypes = [|
+ nullableType
+ for parameter in method.GetParameters () do
+ parameter.ParameterType
+ |]
+
+ typedefof>
+ .MakeGenericType(nullableType.GetGenericArguments ())
+ .GetMethod(
+ name,
+ BindingFlags.Static
+ ||| BindingFlags.DeclaredOnly
+ ||| AnyAccessibility,
+ parameterTypes
+ )
+ |> nonNull
+ )
+
+///
+/// The function that stands in for a call of a method with its type arguments, when the method is one of the
+/// ; no value for a method that is called
+/// itself. The type arguments of the function are the types of the operands and then the type of the result, as many
+/// as the function takes.
+///
+let private dynamicCallOf =
+ memoize (fun (method : MethodInfo) ->
+ if method.Module.Equals coreModule then
+ match dynamicOperators.TryGetValue method.MetadataToken with
+ | true, dynamic ->
+ let types = [|
+ for parameter in method.GetParameters () do
+ numberTypeOf parameter.ParameterType
+ numberTypeOf method.ReturnType
+ |]
+
+ ValueSome {
+ Function = dynamic.MakeGenericMethod (types |> Array.take (dynamic.GetGenericArguments().Length))
+ ResultEnumeration =
+ if method.ReturnType.IsEnum then
+ ValueSome method.ReturnType
+ else
+ ValueNone
+ }
+ | _ -> ValueNone
+ else
+ ValueNone
+ )
+
+///
+/// as a value of , as a cast gives it: the value itself when it
+/// is of that type, and the exception of a failed cast when it is not.
+///
+///
+/// The compiler writes a coercion only where the cast cannot fail, for :> and for an argument that is passed
+/// as a base type. A quotation that is built through the API can hold one that fails, and without this check its
+/// value would be of another type than the part it is the value of. A cast converts nothing: a boxed integer is no
+/// byte, in compiled code either.
+///
+let private castTo (targetType : Type) (value : objnull) : objnull =
+ match value with
+ | null ->
+ // Null is a value of every reference type and of a nullable; unboxing it as any other value type fails.
+ match targetType.IsValueType, Nullable.GetUnderlyingType targetType with
+ | true, null -> raise (NullReferenceException ())
+ | _ -> null
+ | value when targetType.IsInstanceOfType value -> value
+ | value ->
+ raise (
+ InvalidCastException
+ $"Unable to cast object of type '%s{value.GetType().FullName}' to type '%s{targetType.FullName}'."
+ )
+
+///
+/// The values of the nodes of quotations that have been computed, by the identity of the node. The normalizer writes a
+/// value that a bound at every place it is used, as the very same node; with the values kept
+/// here such a node is computed at the first place that needs its value and never again, as the code computes a
+/// bound value once.
+///
+///
+/// One of these serves one part, or all the parts of one quotation for a caller that computes several of them: see
+/// .
+///
+[]
+type Computed () =
+
+ // By identity, because a quotation compares and hashes itself by walking all of itself. Made when the first value
+ // is kept: a part that is a captured value keeps none.
+ let mutable values : Dictionary | null = null
+
+ ///
+ /// The value of , when it has been computed.
+ ///
+ member _.TryFind (node : Expr) : objnull voption =
+ match values with
+ | null -> ValueNone
+ | values ->
+ match values.TryGetValue node with
+ | true, value -> ValueSome value
+ | _ -> ValueNone
+
+ ///
+ /// Remembers that has .
+ ///
+ member _.Add (node : Expr, value : objnull) : unit =
+ match values with
+ | null ->
+ let created = Dictionary ReferenceEqualityComparer.Instance
+ created.Add (node, value)
+ values <- created
+ | values -> values.Add (node, value)
+
+///
+/// , the value that a member is called on or that is applied as a function. When it is null,
+/// this raises what compiled code raises for it, and not what reflection raises for a call on nothing.
+///
+///
+/// Compiled code computes the arguments of a call before it finds that the receiver is null, so this is asked after
+/// the arguments are computed: when one of them raises, its exception is the one that the code raises.
+///
+let private called (target : objnull) : obj =
+ match target with
+ | null -> raise (NullReferenceException ())
+ | target -> target
+
+///
+/// as a member takes its receiver: the value for a member of a value, checked with
+/// , and for a
+/// static member.
+///
+let private calledOn (receiver : Expr option) (target : objnull) : objnull =
+ match receiver with
+ | None -> null
+ | Some _ -> called target
+
+///
+/// The value of . Exceptions of the code it runs come out as they are raised, mostly wrapped
+/// in a .
+///
+///
+/// What has been computed so far: the value of a node that is in it is taken from it, and the value of every other
+/// node that runs code is added to it.
+///
+/// The part to compute.
+let rec private valueOf (computed : Computed) (expr : Expr) : objnull =
+ match expr with
+ | Value (value, _) -> value
+ | _ ->
+ match computed.TryFind expr with
+ | ValueSome value -> value
+ | ValueNone ->
+ let value = computedValueOf computed expr
+ computed.Add (expr, value)
+ value
+
+///
+/// The value of a node that is no value written in the quotation: what its code gives now.
+///
+and private computedValueOf (computed : Computed) (expr : Expr) : objnull =
+ match expr with
+ | PropertyGet (receiver, property, arguments) ->
+ match receiver with
+ | Some nullable when isOfNullable property -> nullableMember computed (nonNull property.GetMethod) nullable arguments
+ | _ ->
+ let target = receiverOf computed receiver
+ let values = valuesOf computed arguments
+ property.GetValue (calledOn receiver target, values)
+ | FieldGet (receiver, field) -> field.GetValue (calledOn receiver (receiverOf computed receiver))
+ | Call (None, method, arguments) ->
+ match dynamicCallOf method with
+ | ValueSome dynamic ->
+ match dynamic.ResultEnumeration, dynamic.Function.Invoke (null, valuesOf computed arguments) with
+ | ValueSome enumeration, (NonNull number) -> Enum.ToObject (enumeration, number)
+ | _, result -> result
+ | ValueNone -> method.Invoke (null, valuesOf computed arguments)
+ | Call (receiver, method, arguments) ->
+ match receiver with
+ | Some nullable when isOfNullable method -> nullableMember computed method nullable arguments
+ | _ ->
+ let target = receiverOf computed receiver
+ let values = valuesOf computed arguments
+ method.Invoke (calledOn receiver target, values)
+ | IfThenElse (condition, whenTrue, whenFalse) ->
+ if valueOf computed condition :?> bool then
+ valueOf computed whenTrue
+ else
+ valueOf computed whenFalse
+ | NewObject (constructor, arguments) -> constructor.Invoke (valuesOf computed arguments)
+ | NewTuple items -> tupleConstructorOf expr.Type (asValues (valuesOf computed items))
+ | NewRecord (recordType, items) -> recordConstructorOf recordType (asValues (valuesOf computed items))
+ | NewUnionCase (case, items) -> unionConstructorOf case (asValues (valuesOf computed items))
+ | NewArray (elementType, items) ->
+ let array = Array.CreateInstance (elementType, items.Length)
+ items
+ |> List.iteri (fun index item -> array.SetValue (valueOf computed item, index))
+ array
+ | TupleGet (tuple, index) -> (tupleReaderOf tuple.Type (asValue (valueOf computed tuple)))[index]
+ | UnionCaseTest (union, case) -> tagReaderOf case.DeclaringType (asValue (valueOf computed union)) = case.Tag
+ | TypeTest (value, tested) -> tested.IsInstanceOfType (valueOf computed value)
+ | Coerce (value, targetType) -> castTo targetType (valueOf computed value)
+ | DefaultValue valueType ->
+ // The zeroed value of a struct, for which no constructor runs: a struct of another language can declare a
+ // constructor without parameters, and the default value is not what that constructor makes. A nullable
+ // without a value is null as an object, like the default value of a class.
+ match Nullable.GetUnderlyingType valueType with
+ | null when valueType.IsValueType -> RuntimeHelpers.GetUninitializedObject valueType
+ | _ -> null
+ | Application (func, argument) ->
+ let target = valueOf computed func
+ let value = valueOf computed argument
+ (invokerOf func.Type).Invoke(called target, [| value |])
+ | _ -> raise (NotComputable expr)
+
+///
+/// The value of the receiver of a member, for a static member and for a receiver that is null.
+///
+and private receiverOf (computed : Computed) (receiver : Expr option) : objnull =
+ match receiver with
+ | None -> null
+ | Some receiver -> valueOf computed receiver
+
+///
+/// The value of a member of a nullable. The nullable is passed to the function that stands in for the member, so a
+/// nullable without a value is one, and not a receiver that is null.
+///
+and private nullableMember (computed : Computed) (method : MethodInfo) (nullable : Expr) (arguments : Expr list) : objnull =
+ (nullableStandInOf method).Invoke(null, [| valueOf computed nullable; yield! valuesOf computed arguments |])
+
+/// The values of the arguments of a member, in their order.
+and private valuesOf (computed : Computed) (arguments : Expr list) : objnull array =
+ arguments |> List.map (valueOf computed) |> List.toArray
+
+///
+/// The parts of quotations that
+///
+/// has refused, by their identity. A caller that asks about a part and
+/// then about the parts under it keeps one of these for all its questions: to refuse a part, every part on the way
+/// down to the reason was looked at, and is not looked at again.
+///
+[]
+type Refused () =
+
+ // By identity, because a quotation compares and hashes itself by walking all of itself.
+ let parts = HashSet ReferenceEqualityComparer.Instance
+
+ ///
+ /// Whether has been refused.
+ ///
+ member _.Holds (part : Expr) : bool = parts.Contains part
+
+ ///
+ /// Remembers that is refused.
+ ///
+ member _.Add (part : Expr) : unit = parts.Add part |> ignore
+
+///
+/// Whether is made only of what
+///
+/// computes. Such a part reads no variable and holds no function that is written in the quotation, so it has a value
+/// before the query runs, unless the code in it raises an exception.
+///
+///
+/// The answer is given without running any code of the part: a caller asks it to choose the parts to compute, and a
+/// part that turned out not to be computable half way would have run a half of its code for nothing.
+///
+///
+/// The parts refused so far. A part that is refused now is added, with every part between it and the reason.
+///
+/// The part of a quotation in normal form.
+let rec canEvaluate (refused : Refused) (expr : Expr) : bool =
+ not (refused.Holds expr)
+ && (let can =
+ match expr with
+ | Var _ -> false
+ | Value _
+ | DefaultValue _ -> true
+ | PropertyGet (receiver, _, arguments)
+ | Call (receiver, _, arguments) ->
+ receiver |> Option.forall (canEvaluate refused)
+ && arguments |> List.forall (canEvaluate refused)
+ | IfThenElse (condition, whenTrue, whenFalse) ->
+ canEvaluate refused condition
+ && canEvaluate refused whenTrue
+ && canEvaluate refused whenFalse
+ | FieldGet (receiver, _) -> receiver |> Option.forall (canEvaluate refused)
+ | NewObject (_, items)
+ | NewRecord (_, items)
+ | NewUnionCase (_, items)
+ | NewArray (_, items)
+ | NewTuple items -> items |> List.forall (canEvaluate refused)
+ | TupleGet (value, _)
+ | UnionCaseTest (value, _)
+ | TypeTest (value, _)
+ | Coerce (value, _) -> canEvaluate refused value
+ | Application (func, argument) -> canEvaluate refused func && canEvaluate refused argument
+ | _ -> false
+
+ if not can then
+ refused.Add expr
+
+ can)
+
+///
+/// The error for a part that raised while it was computed.
+///
+let private failed (part : Expr) (cause : exn) : TranslationError =
+ part
+ |> TranslationError.ofException
+ TranslationErrorCode.ClientEvaluationFailed
+ $"A part of the quotation that reads nothing of the document has to be computed before the query runs, and computing it raised %s{cause.GetType().Name}: %s{cause.Message}"
+ (ImmutableArray.Create
+ "Compute the value before the quotation, where the exception can be handled, and use the result in the quotation.")
+ cause
+
+///
+/// The error for a part that has no value without compiling it.
+///
+let private notComputable (part : Expr) : TranslationError =
+ part
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.Unsupported
+ "A part of the quotation that reads nothing of the document has to be computed before the query runs, but it holds a function, a variable or another quotation, which cannot be computed without compiling the quotation."
+ (ImmutableArray.Create "Compute the value before the quotation and use the result in the quotation.")
+
+///
+/// The value of or the error that says why it has none, with for
+/// the nodes that were computed before.
+///
+let private evaluate (computed : Computed) (expr : Expr) : Result =
+ try
+ Ok (valueOf computed expr)
+ with
+ | NotComputable part -> Error (notComputable part)
+ | :? TargetInvocationException as invocation ->
+ match invocation.InnerException with
+ | null -> Error (failed expr invocation)
+ | cause -> Error (failed expr cause)
+ | cause -> Error (failed expr cause)
+
+///
+/// The value of , a part of a quotation in normal form that reads no variable, or the error
+/// that says why it has none. A node that stands at several places of the part is computed once.
+///
+///
+///
+/// - The code of the part raised an exception:
+/// with the exception as
+/// its . Any exception is taken, because the code
+/// is the caller's and may raise any.
+/// - The part holds a function, a variable or another quotation:
+/// .
+///
+///
+let tryEvaluate (expr : Expr) : Result = evaluate (Computed ()) expr
+
+///
+/// for a
+/// caller that computes several parts of one quotation: a node that was computed for an earlier part is not computed
+/// again for this one.
+///
+/// What has been computed so far; what is computed now is added.
+/// A part of a quotation in normal form that reads no variable.
+let tryEvaluateOnce (computed : Computed) (expr : Expr) : Result = evaluate computed expr
diff --git a/src/Cosmos.Quotations/FSharp.Azure.Cosmos.Quotations.fsproj b/src/Cosmos.Quotations/FSharp.Azure.Cosmos.Quotations.fsproj
new file mode 100644
index 00000000..f393e3e0
--- /dev/null
+++ b/src/Cosmos.Quotations/FSharp.Azure.Cosmos.Quotations.fsproj
@@ -0,0 +1,40 @@
+
+
+
+ net10.0
+ $(AssemblyBaseName).Quotations
+ true
+ true
+
+
+ false
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/src/Cosmos.Quotations/InternalsVisibleTo.fs b/src/Cosmos.Quotations/InternalsVisibleTo.fs
new file mode 100644
index 00000000..f9f7880d
--- /dev/null
+++ b/src/Cosmos.Quotations/InternalsVisibleTo.fs
@@ -0,0 +1,9 @@
+// The stages of the translator are internal: a consumer sees the translator as a whole. Their tests call each stage
+// directly, so the test assembly sees the internals. AssemblyInfo.fs cannot hold the attribute, because FAKE rewrites
+// that file for every release.
+namespace FSharp.Azure.Cosmos.Quotations
+
+open System.Runtime.CompilerServices
+
+[]
+do ()
diff --git a/src/Cosmos.Quotations/JsonLayout.fs b/src/Cosmos.Quotations/JsonLayout.fs
new file mode 100644
index 00000000..11e1cdd1
--- /dev/null
+++ b/src/Cosmos.Quotations/JsonLayout.fs
@@ -0,0 +1,1750 @@
+namespace FSharp.Azure.Cosmos.Quotations
+
+open System
+open System.Collections.Concurrent
+open System.Collections.Generic
+open System.Collections.Immutable
+open System.Reflection
+open System.Text.Json
+open System.Text.Json.Serialization
+open System.Text.Json.Serialization.Metadata
+open FSharp.Reflection
+
+open FSharp.Azure.Cosmos
+
+///
+/// How a member of a document is written when it has no value: an
+/// or a
+/// without one, a that is skipped, a
+/// without a value or a reference that is . A query has to
+/// know it to ask whether the member has a value.
+///
+[]
+type OptionalEncoding =
+ /// Without a value the member is written as null; with one, as the value itself.
+ | AlwaysNull
+ /// Without a value the member is left out of the document; with one, it is written as the value itself.
+ | AlwaysUndefined
+ ///
+ /// Without a value the member is written as null or left out, and the layout cannot tell which: a rule that it
+ /// cannot read decides, such as a
+ /// that code has set. With a value, the member is written as the value itself.
+ ///
+ | NullOrUndefined
+ ///
+ /// The layout does not describe how the value is written: as a union with the name of its case, which is what
+ /// FSharp.SystemTextJson writes for an option when UnwrapOption is off; by a converter that writes
+ /// something of its own for a value without a value, or that fails to write one; or not at all, because the
+ /// serializer refuses the type.
+ ///
+ | NotDescribed
+
+///
+/// A member of a document as a query reads it: a property or a field, and the type of the value that it is read from.
+///
+///
+/// The member alone does not tell that type. A quotation names an inherited property as the type that declares it
+/// has it, also when the property is read from a value of a derived type, and the serializer can write the two types
+/// in different ways: one as an object, the other by a converter.
+///
+[]
+type DocumentMember = {
+ /// The type of the value that the member is read from. In a quotation it is the type of the receiver.
+ Holder : Type
+ /// The property or the field, as any type that has it names it.
+ Member : MemberInfo
+}
+
+///
+/// How a value of an enumeration is written. A query has to know it to compare such values by their order and to test
+/// their bits, which only a number allows.
+///
+///
+/// The encoding is the kind of JSON value, and not what is written for one value of the enumeration: a naming policy
+/// can spell a name, an attribute can declare one, and a converter of the application writes what it likes. So what
+/// stands for such a value in a query has to come from the serializer: the encoding does not say it.
+///
+[]
+type EnumEncoding =
+ /// As the number under the value.
+ | Number
+ ///
+ /// As a JSON string: the name of the value in the spelling that its converter gives it, or what a converter of the
+ /// application writes for it. The converter of System.Text.Json writes a value that has no name, alone or as a
+ /// combination of flags, as its number even then, unless it was made to refuse such a value.
+ ///
+ | String
+ ///
+ /// The layout does not describe how the values are written: neither as numbers nor as strings, some as numbers
+ /// and others as strings, or not all of them because the converter refuses one.
+ ///
+ | NotDescribed
+
+///
+/// How a tuple is written. A query has to know it to read an item of a tuple that a document holds.
+///
+[]
+type TupleEncoding =
+ /// As a JSON array of the items in their order. A tuple of more than seven items is one array as well.
+ | Array
+ ///
+ /// As a JSON object with a property for every item that the serializer writes.
+ ///
+ /// names the property of a member of the tuple type, such as Item1, and has no name for an item that is
+ /// not written. System.Text.Json writes a tuple this way; the items after the seventh are those of the tuple in
+ /// Rest.
+ ///
+ | Object
+ /// The layout does not describe how the tuple is written: a converter that it does not know writes it.
+ | NotDescribed
+
+///
+/// How a collection is written. A query has to know it to ask about the items of a collection that a document holds.
+///
+[]
+type CollectionEncoding =
+ ///
+ /// As a JSON array with an item for every item of the collection.
+ ///
+ /// The type of an item.
+ | Array of element : Type
+ ///
+ /// Not as an array of its items, or the layout does not know how: an array of bytes is written as one text, and a
+ /// converter that the layout does not know can write anything.
+ ///
+ | NotDescribed
+
+///
+/// How a map or a dictionary is written. A query has to know it to look a key up in a map that a document holds.
+///
+[]
+type MapEncoding =
+ ///
+ /// As a JSON object with a property for every key.
+ ///
+ /// The type of a key.
+ /// The type of a value.
+ | Object of key : Type * value : Type
+ ///
+ /// As a JSON array with an item for every key, which is an array of the key and its value. FSharp.SystemTextJson
+ /// writes a map this way when its keys are no texts.
+ ///
+ /// The type of a key.
+ /// The type of a value.
+ | ArrayOfPairs of key : Type * value : Type
+ /// The layout does not describe how the map is written: a converter that it does not know writes it.
+ | NotDescribed
+
+///
+/// A step from a JSON value to a value that it holds. The steps to a part of a union start at the value of the union.
+///
+[]
+type JsonStep =
+ ///
+ /// To the property of a JSON object that has this name.
+ ///
+ /// The name of the property.
+ | Property of name : string
+ ///
+ /// To the item of a JSON array at this index.
+ ///
+ /// The index of the item, counted from zero.
+ | Item of index : int
+
+///
+/// What a union writes to name a case. FSharp.SystemTextJson writes the name of the case, and the number or the
+/// boolean that a case declares in place of a name with
+/// .
+///
+[]
+type UnionTag =
+ ///
+ /// A JSON string.
+ ///
+ /// The string.
+ | String of text : string
+ ///
+ /// A JSON number.
+ ///
+ /// The number.
+ | Number of number : int
+ ///
+ /// A JSON boolean.
+ ///
+ /// The boolean.
+ | Boolean of flag : bool
+
+///
+/// What tells that a value of a union is of one case. A query has to know it to ask for the values of a case.
+///
+[]
+type UnionCaseMark =
+ ///
+ /// The JSON value at the end of is . With no step the value of the
+ /// union itself is the tag, as for a case without fields that is written as its name alone.
+ ///
+ /// The steps from the value of the union to the tag.
+ /// The tag of the case.
+ | Tag of path : ImmutableArray * tag : UnionTag
+ ///
+ /// The value of the union is a JSON object that has a property of this name, which holds the fields of the case.
+ ///
+ /// The name of the property.
+ | Property of name : string
+ ///
+ /// The value of the union is : the case is the one that a union marked with
+ /// represents by null.
+ ///
+ /// says how a member that holds such a null is written.
+ ///
+ | Null
+ ///
+ /// Nothing that is written tells the case: the union is written without tags, or it has one case, whose one
+ /// field is written in place of the union.
+ ///
+ | Unmarked
+
+///
+/// Where the value of a field of a union case is written.
+///
+[]
+type UnionFieldPlace =
+ ///
+ /// The value of the field is the JSON value at the end of . With no step it is written in
+ /// place of the union.
+ ///
+ /// The steps from the value of the union to the value of the field.
+ | Value of path : ImmutableArray
+ ///
+ /// The field is a record, and its members are properties of the JSON object at the end of
+ /// , which holds the tag of the case next to them: the object is more than the record.
+ ///
+ /// names the property of a member of the record.
+ ///
+ /// The steps from the value of the union to the object that holds the members.
+ | Members of path : ImmutableArray
+
+///
+/// How a value of one case of a union is written. A query has to know it to test the case of a union that a document
+/// holds and to read a field of that case.
+///
+[]
+type UnionCaseEncoding =
+ ///
+ /// The case is written in the way that is described.
+ ///
+ /// What tells that a value is of this case.
+ /// Where the fields of the case are written, in the order of the fields.
+ | Written of mark : UnionCaseMark * fields : ImmutableArray
+ ///
+ /// The layout does not describe how the case is written: the serializer refuses the union, a converter that the
+ /// layout does not know writes it, or it is an option, a value option or a skippable value that is written as
+ /// its value, which
+ ///
+ /// describes.
+ ///
+ | NotDescribed
+
+///
+/// What a query has to know about how documents were serialized. The translator asks it for every member that a
+/// quotation reads, so that the query reads the property that the serializer wrote, and for every value that is no
+/// JSON object by itself: an enumeration, a tuple, a collection, a map and a union are each written in more than one
+/// way.
+///
+///
+///
+/// gives the layout of System.Text.Json with or without FSharp.SystemTextJson. A layout of another serializer
+/// implements this interface.
+///
+type IJsonLayout =
+
+ ///
+ /// The name of the JSON property that holds , a property or a field of a type
+ /// that is written as a JSON object; no value when the documents hold no such property, as far as the layout can
+ /// tell.
+ ///
+ ///
+ /// A member that the serializer never writes has a name all the same when the name is declared on it, with
+ /// : such a member stands for a property
+ /// that something else writes into the document, as a type discriminator or a computed property is.
+ ///
+ abstract MemberName : memberOfDocument : DocumentMember -> string voption
+
+ ///
+ /// How a value of is written when it has no value: an option, a value option, a
+ /// , a nullable or a reference.
+ ///
+ ///
+ /// A member can name a converter of its own. The serializer leaves a reference or a nullable of such a member
+ /// out, or writes it as , before it asks that converter, so the member is described as any
+ /// other, unless the converter asks for the null itself and writes something else for it. With a value the
+ /// member is what its converter writes.
+ /// An option, a value option or a skippable value of such a member is not described: the converter writes all of
+ /// it.
+ ///
+ ///
+ /// The member that holds the value, when it is a member of a type written as a JSON object: a member can be left
+ /// out. No value for an item of a collection or of a tuple that is written as an array, which is always written.
+ ///
+ /// The type of the value, which is the type of the member when there is one.
+ abstract OptionalEncoding : memberOfDocument : DocumentMember voption * valueType : Type -> OptionalEncoding
+
+ ///
+ /// How a value of the enumeration is written.
+ ///
+ ///
+ ///
+ /// This member and the three after it are asked alike. is the member that
+ /// holds the value, when it is a member of a type written as a JSON object: a member can name a converter of its
+ /// own, which writes its value and no other. It has no value for an item of a collection and for an item of a
+ /// tuple that is written as an array, which no member holds.
+ ///
+ ///
+ /// The type that is asked about is the type of the member, or the type under an option, a value option, a
+ /// skippable value or a nullable that the member is: what
+ ///
+ /// says of the member tells whether the value under it is written as itself. A converter of the member writes
+ /// the whole value of the member, so with such a converter only the type of the member, or the type that the
+ /// member is a nullable of, is described.
+ ///
+ ///
+ /// A member that the serializer never writes is not described: something else writes its property, in a way of
+ /// its own.
+ ///
+ ///
+ /// The member that holds the value, when a member does.
+ /// The type of the value: an enumeration, or a nullable of one.
+ abstract EnumEncoding : memberOfDocument : DocumentMember voption * enumType : Type -> EnumEncoding
+
+ ///
+ /// How a value of the tuple type is written. It is asked as
+ ///
+ /// is.
+ ///
+ /// The member that holds the tuple, when a member does.
+ /// The type of the tuple, a struct tuple or a reference tuple.
+ abstract TupleEncoding : memberOfDocument : DocumentMember voption * tupleType : Type -> TupleEncoding
+
+ ///
+ /// How a value of is written, when it is a collection. It is asked as
+ ///
+ /// is.
+ ///
+ /// The member that holds the collection, when a member does.
+ /// The type of the collection.
+ abstract CollectionEncoding : memberOfDocument : DocumentMember voption * collectionType : Type -> CollectionEncoding
+
+ ///
+ /// How a value of is written, when it is a map or a dictionary. It is asked as
+ ///
+ /// is.
+ ///
+ /// The member that holds the map, when a member does.
+ /// The type of the map.
+ abstract MapEncoding : memberOfDocument : DocumentMember voption * mapType : Type -> MapEncoding
+
+ ///
+ /// How a value of the union case is written: what tells that a value is of the case, and
+ /// where its fields are. The union is the one that declares the case. It is asked as
+ ///
+ /// is.
+ ///
+ ///
+ ///
+ /// A member that names a converter of its own is not described: nothing shows how that converter writes a union.
+ ///
+ ///
+ /// Neither is a union one of whose cases is written as an object with two properties of the name of the tag, the
+ /// tag and a field: a query reads one property of a name, and nothing says which of the two.
+ ///
+ ///
+ /// The member that holds the union, when a member does.
+ /// The case.
+ abstract UnionCaseEncoding : memberOfDocument : DocumentMember voption * case : UnionCaseInfo -> UnionCaseEncoding
+
+///
+/// The layouts of the serializers that comes with.
+///
+[]
+module JsonLayout =
+
+ /// The members of a type whatever their accessibility: a record of the caller may be internal or private.
+ []
+ let private AnyAccessibility = BindingFlags.Public ||| BindingFlags.NonPublic
+
+ /// The assembly of System.Text.Json, whose own converters are the ones that it documents.
+ let private serializerAssembly = typeof.Assembly
+
+ /// The assembly of FSharp.SystemTextJson, whose own converters are the ones whose way to write is known here.
+ let private fsharpSerializerAssembly = typeof.Assembly
+
+ ///
+ /// Whether is
+ /// or a class that is derived from it.
+ ///
+ let rec private isDerivedFromTupleConverter (converterType : Type | null) : bool =
+ match converterType with
+ | null -> false
+ | converterType ->
+ (converterType.IsGenericType
+ && Type.(=) (converterType.GetGenericTypeDefinition (), typedefof>))
+ || isDerivedFromTupleConverter converterType.BaseType
+
+ ///
+ /// Whether is one of the converters of FSharp.SystemTextJson for tuples: it has a
+ /// class for two, three and four items of either kind of tuple and one for every other number, and
+ /// is the base of all of them.
+ ///
+ ///
+ /// The base class is public, and so are the classes of the library that are derived from it: a converter of an
+ /// application can be derived from any of them and write a tuple in a way of its own. So only a class of the
+ /// library itself is one of its converters.
+ ///
+ let private isTupleConverter (converterType : Type) : bool =
+ converterType.Assembly.Equals fsharpSerializerAssembly
+ && isDerivedFromTupleConverter converterType
+
+ /// Whether the steps of begin with the steps of .
+ let private beginsWith (start : ImmutableArray) (path : ImmutableArray) : bool =
+ path.Length >= start.Length
+ && path.AsSpan(0, start.Length).SequenceEqual(start.AsSpan ())
+
+ ///
+ /// The kinds of values that can be absent, as far as the way they are written depends on the kind.
+ ///
+ []
+ type private OptionalKind =
+ /// An option: without a value it is a null reference.
+ | Option
+ /// A value option: a struct, so without a value it is not null but the default value of its type.
+ | ValueOption
+ /// The skippable value of FSharp.SystemTextJson: a struct like a value option.
+ | Skippable
+ /// A nullable or a reference: written as itself or as null.
+ | Nullable
+ /// A value type that always has a value.
+ | Never
+
+ ///
+ /// Whether is made of the generic type .
+ ///
+ let private isMadeOf (definition : Type) (valueType : Type) : bool =
+ valueType.IsGenericType
+ && Type.(=) (valueType.GetGenericTypeDefinition (), definition)
+
+ ///
+ /// The kind of .
+ ///
+ let private optionalKindOf (valueType : Type) : OptionalKind =
+ if isMadeOf typedefof<_ option> valueType then
+ OptionalKind.Option
+ elif isMadeOf typedefof<_ voption> valueType then
+ OptionalKind.ValueOption
+ elif isMadeOf typedefof> valueType then
+ OptionalKind.Skippable
+ elif not valueType.IsValueType then
+ OptionalKind.Nullable
+ elif isMadeOf typedefof> valueType then
+ OptionalKind.Nullable
+ else
+ OptionalKind.Never
+
+ ///
+ /// Whether itself carries an attribute of the type
+ /// . An attribute of a declaration that the member overrides does not count: the
+ /// serializers do not read it for the override.
+ ///
+ let private isMarked (attributeType : Type) (memberOfDocument : MemberInfo) : bool =
+ memberOfDocument.IsDefined (attributeType, false)
+
+ ///
+ /// The name that declares for itself, when it does: the first name of the
+ /// of FSharp.SystemTextJson, which that library
+ /// reads before the of System.Text.Json.
+ /// The attributes of the member itself count, and not those of a declaration that it overrides: the serializers
+ /// read them that way.
+ ///
+ /// Whether the type of the member is written by FSharp.SystemTextJson.
+ /// The member.
+ let private declaredNameOf (readsJsonName : bool) (memberOfDocument : MemberInfo) : string voption =
+ let ofJsonName =
+ if readsJsonName then
+ memberOfDocument.GetCustomAttributes false
+ // A name with a field is the name of a field of a union case, not of this member. The library
+ // declares the field as a string that is never null and leaves it null when none is given.
+ |> Seq.tryFind (fun attribute ->
+ match withNull attribute.Field with
+ | null -> true
+ | _ -> false
+ )
+ |> ValueOption.map _.Name.AsString()
+ else
+ ValueNone
+
+ ofJsonName
+ |> ValueOption.orElseWith (fun () ->
+ match memberOfDocument.GetCustomAttribute false with
+ | null -> ValueNone
+ | attribute -> ValueSome attribute.Name
+ )
+
+ ///
+ /// Whether and are one member, also when one of them was asked
+ /// for through a derived class.
+ ///
+ let private isSameMember (first : MemberInfo) (second : MemberInfo) : bool =
+ first.MetadataToken = second.MetadataToken
+ && first.Module.Equals second.Module
+ && Type.(=) (first.DeclaringType, second.DeclaringType)
+
+ ///
+ /// Whether is the property as a derived class overrides
+ /// it. A quotation names a property that is overridden as the class that declares it first has it, and the
+ /// contract of the derived class lists the override.
+ ///
+ let private isOverrideOf (declared : MemberInfo) (candidate : MemberInfo) : bool =
+ match declared, candidate with
+ | (:? PropertyInfo as declared), (:? PropertyInfo as candidate) ->
+ match declared.GetMethod, candidate.GetMethod with
+ | null, _
+ | _, null -> false
+ | declaredGetter, candidateGetter ->
+ isSameMember (declaredGetter.GetBaseDefinition ()) (candidateGetter.GetBaseDefinition ())
+ | _ -> false
+
+ ///
+ /// Whether a union type has a case that is represented by . To FSharp.SystemTextJson such a
+ /// null is a value that can be left out, while a null of any other union is not.
+ ///
+ let private hasNullCase (unionType : Type) : bool =
+ unionType.GetCustomAttributes (typeof, false)
+ |> Seq.cast
+ |> Seq.exists _.Flags.HasFlag(CompilationRepresentationFlags.UseNullAsTrueValue)
+
+ ///
+ /// with what it has answered kept: a question is answered once, and every later asking
+ /// gets the answer that was found.
+ ///
+ ///
+ /// An answer of a layout is a fact of the options, and finding it out asks the serializer, which can run code of
+ /// the application: a converter factory, a modifier of a contract.
+ /// can run
+ /// its function more than once for a key that several threads ask for at a time, so what is kept for a question
+ /// is the answer as a , which is computed by one of them.
+ ///
+ let private remembered (answer : 'Question -> 'Answer) : 'Question -> 'Answer =
+ let answers = ConcurrentDictionary<'Question, Lazy<'Answer>>()
+ let create = Func<'Question, Lazy<'Answer>>(fun question -> lazy answer question)
+ fun question -> answers.GetOrAdd(question, create).Value
+
+ ///
+ /// What FSharp.SystemTextJson reads from its options and from the options of the serializer when it writes the
+ /// fields of a record.
+ ///
+ []
+ type private RecordOptions = {
+ /// The naming policy for a field that declares no name; none when names are written as they are.
+ NamingPolicy : JsonNamingPolicy | null
+ /// Whether a reference that is null is left out: the serializer ignores null values.
+ IgnoresNull : bool
+ /// When an option without a value is left out.
+ SkippableOptionFields : SkippableOptionFields
+ /// Whether the properties of a record that are not its fields are written too.
+ IncludesProperties : bool
+ /// How unions are written: a union that is written as the one value it wraps is null where that value is.
+ UnionEncoding : JsonUnionEncoding
+ /// Whether a field of any type may be null in a document.
+ AllowsNullFields : bool
+ }
+
+ ///
+ /// Whether a type is no union, record or tuple of F#. FSharp.SystemTextJson leaves a null of such a type out when
+ /// null values are ignored, and writes a null of the other types.
+ ///
+ let private isNoTypeOfFSharp (valueType : Type) : bool =
+ not (
+ FSharpType.IsUnion (valueType, AnyAccessibility)
+ || FSharpType.IsRecord (valueType, AnyAccessibility)
+ || FSharpType.IsTuple valueType
+ )
+
+ ///
+ /// Whether FSharp.SystemTextJson has a value of that it reads from a null. A null of
+ /// such a type is a value to the library, which leaves it out when null values are ignored. The rules are the
+ /// ones of the function of the library that finds that value.
+ ///
+ /// The options of the record whose field is asked about.
+ /// The type of the field.
+ let rec private hasNullValue (recordOptions : RecordOptions) (valueType : Type) : bool =
+ let isOption =
+ isMadeOf typedefof<_ option> valueType
+ || isMadeOf typedefof<_ voption> valueType
+
+ if hasNullCase valueType || Type.(=) (valueType, typeof) then
+ true
+ elif
+ isMadeOf typedefof<_ voption> valueType
+ && recordOptions.UnionEncoding.HasFlag JsonUnionEncoding.UnwrapOption
+ then
+ true
+ elif
+ isMadeOf typedefof> valueType
+ || (isOption
+ && recordOptions.SkippableOptionFields = SkippableOptionFields.Always)
+ then
+ hasNullValue recordOptions (valueType.GetGenericArguments()[0])
+ elif recordOptions.AllowsNullFields && isNoTypeOfFSharp valueType then
+ true
+ else
+ // A union of one case with one field that is written as that field is null where the field is.
+ recordOptions.UnionEncoding.HasFlag JsonUnionEncoding.UnwrapSingleCaseUnions
+ && FSharpType.IsUnion (valueType, AnyAccessibility)
+ && (
+ match FSharpType.GetUnionCases (valueType, AnyAccessibility) with
+ | [| case |] ->
+ match case.GetFields () with
+ | [| field |] -> hasNullValue recordOptions field.PropertyType
+ | _ -> false
+ | _ -> false
+ )
+
+ ///
+ /// The options that FSharp.SystemTextJson makes the converter of one type with.
+ ///
+ []
+ type private ConverterOptions = {
+ /// The options of FSharp.SystemTextJson for the type: the ones the type has for itself, or the common ones.
+ Effective : JsonFSharpOptions
+ /// The options of the converter among the options that takes the type; none when an attribute on the type
+ /// makes the converter by itself.
+ Common : JsonFSharpOptions voption
+ /// The naming policy of the options of the serializer that the converter is made with; none when names are
+ /// written as they are.
+ NamingPolicy : JsonNamingPolicy | null
+ /// Whether those options of the serializer ignore null values.
+ IgnoresNull : bool
+ }
+
+ ///
+ /// What FSharp.SystemTextJson reads from its options and from the options of the serializer when it writes a
+ /// union.
+ ///
+ []
+ type private UnionOptions = {
+ /// How unions are written, with the encoding that the options of a type inherit put in.
+ Encoding : JsonUnionEncoding
+ /// The name of the property that holds the tag of a case.
+ TagName : string
+ /// The name of the property that holds the fields of a case.
+ FieldsName : string
+ /// The naming policy for a case that declares no name; none when names are written as they are.
+ TagNamingPolicy : JsonNamingPolicy | null
+ /// The naming policy for a field that declares no name; none when names are written as they are.
+ FieldNamingPolicy : JsonNamingPolicy | null
+ ///
+ /// The common options, when the union is written with them and has none of its own: the ones of the
+ /// converter among the options that takes the union. A record that a case unwraps is written as that
+ /// converter writes it.
+ ///
+ Common : JsonFSharpOptions voption
+ }
+
+ ///
+ /// The bits of that choose where the tag is.
+ ///
+ []
+ let private UnionFormatBits = 0xFE
+
+ ///
+ /// The bit of among those bits.
+ ///
+ []
+ let private UntaggedBit = 0x08
+
+ ///
+ /// as spells it; as it is without a policy.
+ ///
+ let private spelled (policy : JsonNamingPolicy | null) (name : string) : string =
+ match policy with
+ | null -> name
+ | policy -> policy.ConvertName name
+
+ ///
+ /// The tag that FSharp.SystemTextJson writes for a case: the first name of the
+ /// of the case that names no field, then the name
+ /// of its , then the name of the case as
+ /// spells it. A name that is declared is written as it is.
+ ///
+ let private unionTagOf (policy : JsonNamingPolicy | null) (case : UnionCaseInfo) : UnionTag =
+ let declared =
+ case.GetCustomAttributes typeof
+ |> Seq.cast
+ // The library declares the field as a string that is never null and leaves it null when none is given.
+ |> Seq.tryFind (fun attribute ->
+ match withNull attribute.Field with
+ | null -> true
+ | _ -> false
+ )
+
+ match declared with
+ | ValueSome attribute ->
+ match attribute.Name with
+ | JsonName.String text -> UnionTag.String text
+ | JsonName.Int number -> UnionTag.Number number
+ | JsonName.Bool flag -> UnionTag.Boolean flag
+ | ValueNone ->
+ match
+ case.GetCustomAttributes typeof
+ |> Seq.cast
+ |> Seq.tryHead
+ with
+ | ValueSome attribute -> UnionTag.String attribute.Name
+ | ValueNone -> UnionTag.String (spelled policy case.Name)
+
+ ///
+ /// A tag as the name of a JSON property, which is how a union with external tags writes it.
+ ///
+ let private propertyNameOf (tag : UnionTag) : string =
+ match tag with
+ | UnionTag.String text -> text
+ | UnionTag.Number number -> string number
+ | UnionTag.Boolean true -> "true"
+ | UnionTag.Boolean false -> "false"
+
+ ///
+ /// The names that FSharp.SystemTextJson writes the fields of a case under when it writes them as properties, in
+ /// the order of the fields.
+ ///
+ ///
+ /// A field has the name that F# gives it. With
+ /// a field that the
+ /// source does not name, Item or Item1, Item2 and so on, has the name of its type, and a
+ /// name that the case then has more than once gets the number of its occurrence. Under that name the case can
+ /// declare another one, with a that names the
+ /// field; a name without one is spelled by the policy.
+ ///
+ let private unionFieldNamesOf (options : UnionOptions) (case : UnionCaseInfo) : string array =
+ let fields = case.GetFields ()
+ let occurrences = Dictionary StringComparer.Ordinal
+
+ let own =
+ if options.Encoding.HasFlag JsonUnionEncoding.UnionFieldNamesFromTypes then
+ fields
+ |> Array.mapi (fun index field ->
+ let isUnnamed =
+ if fields.Length = 1 then
+ String.Equals (field.Name, "Item", StringComparison.Ordinal)
+ else
+ String.Equals (field.Name, $"Item%d{index + 1}", StringComparison.Ordinal)
+
+ let name = if isUnnamed then field.PropertyType.Name else field.Name
+
+ let occurrence =
+ match occurrences.TryGetValue name with
+ | true, seen -> seen + 1
+ | _ -> 1
+
+ occurrences[name] <- occurrence
+ struct (name, occurrence)
+ )
+ else
+ fields |> Array.map (fun field -> struct (field.Name, 1))
+
+ let declared = Dictionary StringComparer.Ordinal
+
+ for attribute in Seq.cast(case.GetCustomAttributes typeof) do
+ match withNull attribute.Field with
+ | null -> ()
+ | field -> declared[field] <- attribute.Name.AsString ()
+
+ own
+ |> Array.map (fun (struct (name, occurrence)) ->
+ let isRepeated =
+ match occurrences.TryGetValue name with
+ | true, seen -> seen > 1
+ | _ -> false
+
+ let name =
+ if occurrence = 1 && not isRepeated then
+ name
+ else
+ $"%s{name}%d{occurrence}"
+
+ match declared.TryGetValue name with
+ | true, given -> given
+ | _ -> spelled options.FieldNamingPolicy name
+ )
+
+ // JsonSerializerOptions.IgnoreNullValues is obsolete, and System.Text.Json and FSharp.SystemTextJson still follow
+ // it, so the layout has to read it. This function is the one place that does.
+ #nowarn "44"
+
+ ///
+ /// The condition on which leave a member out that has no condition of its own.
+ /// , the setting of old, stands for
+ /// ; the options refuse the two
+ /// settings together.
+ ///
+ let private defaultIgnoreConditionOf (options : JsonSerializerOptions) : JsonIgnoreCondition =
+ if options.IgnoreNullValues then
+ JsonIgnoreCondition.WhenWritingNull
+ else
+ options.DefaultIgnoreCondition
+
+ #warnon "44"
+
+ ///
+ /// Who writes the members of a type, which decides the rules that their names and their absence follow.
+ ///
+ []
+ type private Writer =
+ /// System.Text.Json writes the type as an object, and its contract lists the properties.
+ | Serializer of JsonTypeInfo
+ /// FSharp.SystemTextJson writes the type as a record.
+ | Record of RecordOptions
+ /// A converter that the layout does not know writes the type, or the type is not written as an object.
+ | Unknown
+
+ ///
+ /// The layout of what System.Text.Json writes with the options it is made of, with or without the converter of
+ /// FSharp.SystemTextJson among them.
+ ///
+ ///
+ /// The layout asks the serializer itself who writes a type: the contract that the options resolve for a type names
+ /// the converter. So the order of the converters, the kinds of types that FSharp.SystemTextJson is set to handle
+ /// and a converter that a type names in an attribute all count as they do when a document is written.
+ ///
+ []
+ type private SerializerLayout (options : JsonSerializerOptions) =
+
+ // A copy, so that the options of the caller stay as they are: the contract can be asked only from options
+ // that have a resolver and that are not changed any more. The resolver is the one of the caller, because a
+ // resolver cannot be copied: it can be any object. So the layout asks who the serializer asks, and a resolver
+ // of System.Text.Json takes no change after either has asked it, which keeps the two from disagreeing.
+ let contract =
+ let copy =
+ match options.TypeInfoResolver with
+ | null -> JsonSerializerOptions (options, TypeInfoResolver = DefaultJsonTypeInfoResolver ())
+ | _ -> JsonSerializerOptions options
+
+ copy.MakeReadOnly ()
+ copy
+
+ /// The condition on which the options leave a member out that has no condition of its own.
+ let defaultIgnoreCondition = defaultIgnoreConditionOf contract
+
+ /// The contract of a type; none for a type that the serializer refuses.
+ let typeInfoOf (documentType : Type) : JsonTypeInfo voption =
+ try
+ ValueSome (contract.GetTypeInfo documentType)
+ with
+ | :? NotSupportedException
+ | :? InvalidOperationException
+ | :? ArgumentException
+ // FSharp.SystemTextJson makes its converters by reflection, so what one of them refuses a type with comes
+ // wrapped in this one: a union with a case that has two attributes with names is refused that way.
+ | :? TargetInvocationException -> ValueNone
+
+ ///
+ /// The options that FSharp.SystemTextJson writes a record or a union type with: the ones the type is given in
+ /// the overrides, then the ones of the attribute on the type when the options allow that, then the common
+ /// ones. The common ones are those of the converter that takes the type, which is the first one among the
+ /// options that does, as the serializer chooses it: the options can hold several converters of
+ /// FSharp.SystemTextJson for different kinds of types. No value when nothing shows the options of the
+ /// converter: a converter that stands among the options by itself keeps them to itself, and so does one that
+ /// an attribute of the caller makes.
+ ///
+ let converterOptionsOf (valueType : Type) : ConverterOptions voption =
+ let ofAttribute () =
+ match valueType.GetCustomAttributes (typeof, true) with
+ | [||] -> ValueNone
+ | attributes -> ValueSome (attributes[0] :?> IJsonFSharpConverterAttribute).Options
+
+ let ofOverrides (common : JsonFSharpOptions) =
+ // The library declares the overrides as never null and gives null when there are none.
+ match withNull (common.Overrides common) with
+ | null -> ValueNone
+ | overrides ->
+ match overrides.TryGetValue valueType with
+ | true, given -> ValueSome given
+ | _ when valueType.IsGenericType ->
+ match overrides.TryGetValue (valueType.GetGenericTypeDefinition ()) with
+ | true, given -> ValueSome given
+ | _ -> ValueNone
+ | _ -> ValueNone
+
+ match contract.Converters |> Seq.tryFind _.CanConvert(valueType) with
+ // The factory of FSharp.SystemTextJson itself, and no class that an application derives from it: such a
+ // class can make the converter of a record with other options than the ones it shows.
+ | ValueSome (:? JsonFSharpConverter as factory) when Type.(=) (factory.GetType (), typeof) ->
+ let common = factory.Options
+
+ let effective =
+ ofOverrides common
+ |> ValueOption.orElseWith (fun () -> if common.AllowOverride then ofAttribute () else ValueNone)
+ |> ValueOption.defaultValue common
+
+ ValueSome {
+ Effective = effective
+ Common = ValueSome common
+ NamingPolicy = contract.PropertyNamingPolicy
+ IgnoresNull = defaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ }
+ | ValueSome _ -> ValueNone
+ | ValueNone ->
+ // No converter among the options takes the type, so an attribute on the type made the converter. The
+ // attribute of FSharp.SystemTextJson shows the options it makes the converter with, and gives it
+ // options of the serializer of its own: no naming policy, and nothing is ignored. Any other attribute
+ // can make the converter with options that nothing shows.
+ ofAttribute ()
+ |> ValueOption.map (fun effective -> {
+ Effective = effective
+ Common = ValueNone
+ NamingPolicy = null
+ IgnoresNull = false
+ })
+
+ ///
+ /// How FSharp.SystemTextJson writes unions with the options of a converter.
+ ///
+ ///
+ /// The options that a type has for itself can say that they inherit the encoding of unions, with
+ /// : the library then takes the whole
+ /// encoding from the common options, also the parts that the options of the type set. The options that the
+ /// attribute makes say so unless the attribute sets the whole encoding.
+ ///
+ let unionEncodingOf (found : ConverterOptions) : JsonUnionEncoding =
+ match found.Common with
+ | ValueSome common when found.Effective.UnionEncoding.HasFlag JsonUnionEncoding.Inherit -> common.UnionEncoding
+ | _ -> found.Effective.UnionEncoding
+
+ /// What FSharp.SystemTextJson reads when it writes the fields of a record type.
+ let recordOptionsOf (recordType : Type) : RecordOptions voption =
+ converterOptionsOf recordType
+ |> ValueOption.map (fun found -> {
+ NamingPolicy = found.NamingPolicy
+ IgnoresNull = found.IgnoresNull
+ SkippableOptionFields = found.Effective.SkippableOptionFields
+ IncludesProperties = found.Effective.IncludeRecordProperties
+ UnionEncoding = unionEncodingOf found
+ AllowsNullFields = found.Effective.AllowNullFields
+ })
+
+ /// What FSharp.SystemTextJson reads when it writes a union type.
+ let unionOptionsOf (unionType : Type) : UnionOptions voption =
+ converterOptionsOf unionType
+ |> ValueOption.map (fun found ->
+ let effective = found.Effective
+
+ {
+ Encoding = unionEncodingOf found
+ TagName = effective.UnionTagName
+ FieldsName = effective.UnionFieldsName
+ TagNamingPolicy = effective.UnionTagNamingPolicy
+ FieldNamingPolicy =
+ match withNull effective.UnionFieldNamingPolicy with
+ | null -> found.NamingPolicy
+ | policy -> policy
+ Common =
+ found.Common
+ |> ValueOption.filter (fun common -> obj.ReferenceEquals (common, effective))
+ }
+ )
+
+ ///
+ /// Whether the serializer made a contract itself. It chooses the converter by rules that the layout follows: the
+ /// first one among the options that takes the type, then the one that an attribute names, then one of its own.
+ /// A resolver of the application can make a contract from any converter instead, also from a converter of
+ /// FSharp.SystemTextJson that it made with options of its own, and nothing shows how it made that converter.
+ ///
+ ///
+ /// names the resolver
+ /// that made a contract. The resolver of the serializer names itself, also as the class that an application
+ /// derives from it to change the contracts it makes, while a contract that an application makes from a
+ /// converter names none.
+ ///
+ let isMadeBySerializer (info : JsonTypeInfo) : bool =
+ match info.OriginatingResolver with
+ | :? DefaultJsonTypeInfoResolver -> true
+ | _ -> false
+
+ /// Who writes the members of a type.
+ let writerOf =
+ remembered (fun (documentType : Type) ->
+ match typeInfoOf documentType with
+ | ValueSome info when info.Kind = JsonTypeInfoKind.Object -> Writer.Serializer info
+ | ValueSome info when
+ isMadeBySerializer info
+ && isMadeOf typedefof> (info.Converter.GetType ())
+ ->
+ match recordOptionsOf documentType with
+ | ValueSome recordOptions -> Writer.Record recordOptions
+ | ValueNone -> Writer.Unknown
+ | _ -> Writer.Unknown
+ )
+
+ ///
+ /// How a value without a value is written with a contract where it is not left out. The serializer is asked
+ /// to write one: a null, or the value option or the skippable value that has no value.
+ ///
+ ///
+ /// The serializer writes a null as null itself, unless the converter asks for the null with
+ /// . Such a converter writes what
+ /// it likes, and some cannot write a null at all: the converters of FSharp.SystemTextJson for tuples, for maps
+ /// and for unions without fields ask for the null and fail on it. Only null is described.
+ ///
+ let absentWrittenWith (contractOfValue : JsonTypeInfo voption) : OptionalEncoding =
+ match contractOfValue with
+ // The serializer has no contract for the type, and it needs one also to write a null of it.
+ | ValueNone -> OptionalEncoding.NotDescribed
+ | ValueSome info ->
+ let absent : objnull =
+ if info.Type.IsValueType then
+ Activator.CreateInstance info.Type
+ else
+ null
+
+ try
+ if JsonSerializer.SerializeToElement(absent, info).ValueKind = JsonValueKind.Null then
+ OptionalEncoding.AlwaysNull
+ else
+ OptionalEncoding.NotDescribed
+ with _ ->
+ // Whatever a converter raises for the value, no document holds what it would have written.
+ OptionalEncoding.NotDescribed
+
+ ///
+ /// Whether a value of that is present is written as the value itself: by a
+ /// converter of FSharp.SystemTextJson that unwraps it, or by System.Text.Json, which writes an option as its
+ /// value and a nullable and a reference as themselves.
+ ///
+ let isWrittenAsItself (kind : OptionalKind) (valueType : Type) : bool =
+ match kind with
+ | OptionalKind.Nullable -> true
+ | OptionalKind.Never -> false
+ | OptionalKind.Option
+ | OptionalKind.ValueOption
+ | OptionalKind.Skippable ->
+ match typeInfoOf valueType with
+ | ValueSome info ->
+ let converter = info.Converter.GetType ()
+
+ isMadeOf typedefof> converter
+ || isMadeOf typedefof> converter
+ || isMadeOf typedefof> converter
+ // System.Text.Json has converters of its own for the two option types, and none for a Skippable.
+ || (kind <> OptionalKind.Skippable
+ && converter.Assembly.Equals serializerAssembly)
+ | ValueNone -> false
+
+ ///
+ /// The entry that a contract has for a member, with the member that the entry was made of: the member itself,
+ /// or the override of it that the type of the contract has when the contract lists no other entry for it. What
+ /// the serializer reads from a member, it reads from the one the entry was made of.
+ ///
+ let propertyOf (info : JsonTypeInfo) (memberOfDocument : MemberInfo) : struct (JsonPropertyInfo * MemberInfo) voption =
+ let entries = seq {
+ for property in info.Properties do
+ match property.AttributeProvider with
+ | :? MemberInfo as own -> struct (property, own)
+ | _ -> ()
+ }
+
+ entries
+ |> Seq.tryFind (fun (struct (_, own)) -> isSameMember own memberOfDocument)
+ |> ValueOption.orElseWith (fun () ->
+ entries
+ |> Seq.tryFind (fun (struct (_, own)) -> isOverrideOf memberOfDocument own)
+ )
+
+ ///
+ /// The contract that a member with a converter of its own writes a value of
+ /// with. The serializer has no such contract, it keeps the converter with the member; options that hold the
+ /// converter before every other one give it.
+ ///
+ ///
+ ///
+ /// Those options have the resolver of the serializer and not the one of the caller. The serializer writes the
+ /// member with its converter whatever contract the resolver of the caller makes for the type, and such a
+ /// resolver can make that contract without looking at the converters of the options it is asked with.
+ ///
+ ///
+ /// A member can name a factory, which the serializer has make the converter with the options. Such a factory
+ /// may ask those options for the converter that they have for the type, to add something around it. Asked by
+ /// options that hold the factory itself, it would be asked without end, so the factory makes the converter
+ /// with the options of the layout first, and the options that are asked for the contract hold what it made.
+ /// Nothing is caught here: the serializer has made the converter the same way before it listed the member,
+ /// and would have refused a member whose converter does not fit its type.
+ ///
+ ///
+ let ownInfoOf (converter : JsonConverter) (valueType : Type) : JsonTypeInfo voption =
+ let made =
+ match converter with
+ | :? JsonConverterFactory as factory -> factory.CreateConverter (valueType, contract)
+ | converter -> converter
+
+ match made with
+ // The serializer refuses a member whose factory makes nothing, so no contract lists such a member.
+ | null -> ValueNone
+ | made ->
+ let own = JsonSerializerOptions (contract, TypeInfoResolver = DefaultJsonTypeInfoResolver ())
+ own.Converters.Insert (0, made)
+ ValueSome (own.GetTypeInfo valueType)
+
+ ///
+ /// Whether System.Text.Json never writes a member of an object: the member has no getter the serializer may
+ /// call, or it is read-only and the options ignore read-only members.
+ ///
+ ///
+ ///
+ /// The contract does not show the second case: such a property keeps its getter and gets no rule. The rule is
+ /// the one of the serializer. A member that has a rule is not left out, because the serializer asks the rule
+ /// instead: the contract has one for a member that says itself when it is ignored, with a
+ /// of any condition, and for a member that
+ /// code gave one. Neither is a collection or a dictionary left out, which the serializer writes also when
+ /// nothing can assign it. What writes the member decides whether it is one: the serializer, which writes the
+ /// items, or a converter of the options or of the member, which writes what it likes.
+ ///
+ ///
+ /// One case cannot be told. Code that sets the rule of a member to has set a rule to
+ /// the serializer, which then writes the read-only member; it remembers that in a field that is not public,
+ /// and the contract shows the same as for a member that nothing touched. Such a member
+ /// is taken to be left out.
+ ///
+ ///
+ let isNeverWritten (property : JsonPropertyInfo) (memberOfDocument : MemberInfo) : bool =
+ let ignoresReadOnly =
+ match memberOfDocument with
+ | :? PropertyInfo -> contract.IgnoreReadOnlyProperties
+ | :? FieldInfo -> contract.IgnoreReadOnlyFields
+ | _ -> false
+
+ let isReadOnly () =
+ match property.Set with
+ | null -> true
+ | _ -> false
+
+ let isCollection () =
+ let info =
+ match property.CustomConverter with
+ | null -> typeInfoOf property.PropertyType
+ | converter -> ownInfoOf converter property.PropertyType
+
+ match info with
+ | ValueSome info ->
+ info.Kind = JsonTypeInfoKind.Enumerable
+ || info.Kind = JsonTypeInfoKind.Dictionary
+ | ValueNone -> false
+
+ match property.Get with
+ | null -> true
+ | _ ->
+ ignoresReadOnly
+ && isReadOnly ()
+ && not (isCollection ())
+ && (
+ match property.ShouldSerialize with
+ | null -> true
+ | _ -> false
+ )
+
+ ///
+ /// Whether FSharp.SystemTextJson writes a member of a record: its fields, whatever their accessibility, and
+ /// of its other properties the public ones, when the options or the member say so. It never writes a member
+ /// that is marked with , whatever the
+ /// condition of the attribute says.
+ ///
+ let isWrittenByRecord (recordOptions : RecordOptions) (memberOfDocument : MemberInfo) : bool =
+ match memberOfDocument with
+ | :? PropertyInfo as property when not (isMarked typeof property) ->
+ let isField =
+ match property.DeclaringType with
+ | null -> false
+ | recordType ->
+ FSharpType.GetRecordFields (recordType, AnyAccessibility)
+ |> Array.exists (fun field -> isSameMember field property)
+
+ let isPublic =
+ match property.GetMethod with
+ | null -> false
+ | getter -> getter.IsPublic
+
+ isField
+ || (isPublic
+ && (recordOptions.IncludesProperties
+ || isMarked typeof property))
+ | _ -> false
+
+ /// The name of a member, by the rules of who writes its type.
+ let nameOf (memberOfDocument : DocumentMember) : string voption =
+ match writerOf memberOfDocument.Holder with
+ | Writer.Serializer info ->
+ match propertyOf info memberOfDocument.Member with
+ | ValueSome (struct (property, _)) when property.IsExtensionData -> ValueNone
+ | ValueSome (struct (property, own)) when isNeverWritten property own -> declaredNameOf false own
+ | ValueSome (struct (property, _)) -> ValueSome property.Name
+ | ValueNone -> ValueNone
+ | Writer.Record recordOptions ->
+ let field = memberOfDocument.Member
+ let declared = declaredNameOf true field
+
+ if isWrittenByRecord recordOptions field then
+ declared
+ |> ValueOption.orElseWith (fun () ->
+ match recordOptions.NamingPolicy with
+ | null -> ValueSome field.Name
+ | policy -> ValueSome (policy.ConvertName field.Name)
+ )
+ else
+ // A member that is never written has the name it declares, when it declares one.
+ declared
+ | Writer.Unknown -> ValueNone
+
+ ///
+ /// How a member of a type that System.Text.Json writes as an object is written without a value. The member
+ /// decides with its own , and the options
+ /// decide for a member that has none.
+ ///
+ /// The entry of the member in the contract.
+ /// The member.
+ /// The kind of its value.
+ /// How the value is written without one when it is not left out.
+ let absenceInObject
+ (property : JsonPropertyInfo)
+ (memberOfDocument : MemberInfo)
+ (kind : OptionalKind)
+ (whenWritten : OptionalEncoding)
+ : OptionalEncoding =
+ let condition =
+ // The attribute of the member itself: an override has none of the declaration it overrides.
+ match memberOfDocument.GetCustomAttribute false with
+ | null ->
+ match property.ShouldSerialize with
+ // A rule that code has set: it is a function, which says nothing before it is called.
+ | null -> ValueSome defaultIgnoreCondition
+ | _ -> ValueNone
+ | attribute -> ValueSome attribute.Condition
+
+ match condition, kind with
+ // The serializer never writes the member, so whatever writes the property decides. A member that is
+ // always ignored is such a member: the serializer keeps no getter for it.
+ | _ when isNeverWritten property memberOfDocument -> OptionalEncoding.NullOrUndefined
+ // A rule that code has set chooses between leaving the member out and writing it.
+ | ValueNone, _ when whenWritten.IsAlwaysNull -> OptionalEncoding.NullOrUndefined
+ | ValueNone, _ -> OptionalEncoding.NotDescribed
+ | ValueSome JsonIgnoreCondition.WhenWritingDefault, _ -> OptionalEncoding.AlwaysUndefined
+ // An option without a value and a nullable without a value are null; a value option and a Skippable
+ // without a value are structs, which are not null, so they are written, as null.
+ | ValueSome JsonIgnoreCondition.WhenWritingNull, (OptionalKind.Option | OptionalKind.Nullable) ->
+ OptionalEncoding.AlwaysUndefined
+ | ValueSome _, _ -> whenWritten
+
+ ///
+ /// How a field of a record that FSharp.SystemTextJson writes is written without a value.
+ ///
+ /// The options that the record is written with.
+ /// The kind of the value of the field.
+ /// The type of the value.
+ /// How the value is written without one when it is not left out.
+ let absenceInRecord
+ (recordOptions : RecordOptions)
+ (kind : OptionalKind)
+ (valueType : Type)
+ (whenWritten : OptionalEncoding)
+ : OptionalEncoding =
+ let isLeftOut =
+ match kind with
+ | OptionalKind.Skippable -> true
+ | OptionalKind.Option ->
+ recordOptions.SkippableOptionFields = SkippableOptionFields.Always
+ || (recordOptions.SkippableOptionFields = SkippableOptionFields.FromJsonSerializerOptions
+ && recordOptions.IgnoresNull)
+ // A value option without a value is not null, so only the setting for every option leaves it out.
+ | OptionalKind.ValueOption -> recordOptions.SkippableOptionFields = SkippableOptionFields.Always
+ | OptionalKind.Nullable ->
+ // A record, a union and a tuple of F# are written also when they are null and null values are
+ // ignored. A union with a case that is null is the exception, and so is a union that is written
+ // as the one value it wraps where that value can be null: their null is a value to the library,
+ // which is left out.
+ recordOptions.IgnoresNull
+ && (isNoTypeOfFSharp valueType
+ || hasNullValue recordOptions valueType)
+ | OptionalKind.Never -> false
+
+ if isLeftOut then
+ OptionalEncoding.AlwaysUndefined
+ else
+ whenWritten
+
+ /// How a value is written without one, by the rules of who writes the type of its member.
+ let encodingOf (struct (memberOfDocument : DocumentMember voption, valueType : Type)) : OptionalEncoding =
+ let kind = optionalKindOf valueType
+
+ if not (isWrittenAsItself kind valueType) then
+ OptionalEncoding.NotDescribed
+ else
+ // What is written for the value where it is not left out, by the converter of the type or by the one
+ // that a member names.
+ let whenWritten = absentWrittenWith (typeInfoOf valueType)
+
+ match memberOfDocument with
+ | ValueNone -> whenWritten
+ | ValueSome memberOfDocument ->
+ match writerOf memberOfDocument.Holder with
+ | Writer.Serializer info ->
+ match propertyOf info memberOfDocument.Member with
+ // The entries of the extension data are properties of the document itself, and nothing is
+ // written for extension data that is null: no property of its name is null or missing.
+ | ValueSome (struct (property, _)) when property.IsExtensionData -> OptionalEncoding.NotDescribed
+ | ValueSome (struct (property, own)) ->
+ match property.CustomConverter with
+ | null -> absenceInObject property own kind whenWritten
+ // The serializer leaves a null out, or writes it, before it asks the converter of the
+ // member, unless that converter asks for the null: what it writes then is asked of the
+ // serializer. So a reference or a nullable of such a member is described as any other. An
+ // option, a value option or a skippable value is not: the converter writes all of it, and
+ // nothing shows what it writes for one with a value.
+ | converter when kind = OptionalKind.Nullable ->
+ absenceInObject property own kind (absentWrittenWith (ownInfoOf converter property.PropertyType))
+ | _ -> OptionalEncoding.NotDescribed
+ | ValueNone -> OptionalEncoding.NotDescribed
+ | Writer.Record recordOptions ->
+ // Whatever writes the property of a member that FSharp.SystemTextJson never writes decides.
+ if isWrittenByRecord recordOptions memberOfDocument.Member then
+ absenceInRecord recordOptions kind valueType whenWritten
+ else
+ OptionalEncoding.NullOrUndefined
+ | Writer.Unknown -> OptionalEncoding.NotDescribed
+
+ ///
+ /// The contract that the serializer writes a value with: the one of its type, or the one of the converter
+ /// that the member of the value names. No contract when the layout does not know what writes the value. The
+ /// value is asked about by the member that holds it, when a member does, and by its type.
+ ///
+ ///
+ /// Whether the contract of a converter that the member names is given. Without it such a member has no
+ /// contract: the question is one that the class of a converter does not answer.
+ ///
+ let valueInfoWith
+ (followsConverter : bool)
+ (struct (memberOfDocument : DocumentMember voption, valueType : Type))
+ : JsonTypeInfo voption =
+ match memberOfDocument with
+ | ValueNone -> typeInfoOf valueType
+ | ValueSome memberOfDocument ->
+ match writerOf memberOfDocument.Holder with
+ | Writer.Serializer info ->
+ match propertyOf info memberOfDocument.Member with
+ // The entries of the extension data are properties of the document itself, so no property holds
+ // the dictionary.
+ | ValueSome (struct (property, _)) when property.IsExtensionData -> ValueNone
+ // Whatever writes a property that the serializer never writes decides how.
+ | ValueSome (struct (property, own)) when isNeverWritten property own -> ValueNone
+ | ValueSome (struct (property, _)) ->
+ let memberType = property.PropertyType
+
+ // The converter of a member writes the whole value of the member, also the nullable that
+ // the member is. What it writes for a part of the value, such as the value of an option,
+ // nothing shows.
+ let isWholeValue () =
+ Type.(=) (valueType, memberType)
+ || (
+ match Nullable.GetUnderlyingType memberType with
+ | null -> false
+ | underlying -> Type.(=) (valueType, underlying)
+ )
+
+ match property.CustomConverter with
+ | null -> typeInfoOf valueType
+ | converter when followsConverter && isWholeValue () -> ownInfoOf converter memberType
+ | _ -> ValueNone
+ | ValueNone -> ValueNone
+ | Writer.Record recordOptions ->
+ // FSharp.SystemTextJson does not read the converter that a member of a record names. A member
+ // that it does not write is described by nothing: whatever writes its property decides how.
+ if isWrittenByRecord recordOptions memberOfDocument.Member then
+ typeInfoOf valueType
+ else
+ ValueNone
+ | Writer.Unknown -> ValueNone
+
+ /// The contract that the serializer writes a value with, also the one of a converter that its member names.
+ let valueInfoOf (question : struct (DocumentMember voption * Type)) : JsonTypeInfo voption = valueInfoWith true question
+
+ ///
+ /// How an enumeration is written. The serializer is asked to write its values, because nothing else tells:
+ /// System.Text.Json has one converter for numbers and for names, which keeps to itself what it writes.
+ ///
+ ///
+ /// Every value that has a name is written, and all of them have to come out as one kind of JSON value: a
+ /// converter of the application can write one value as its name and another as a number, and such an
+ /// enumeration is written in no one way.
+ ///
+ let enumEncodingOf (struct (_, valueType : Type) as question) : EnumEncoding =
+ let enumType =
+ match Nullable.GetUnderlyingType valueType with
+ | null -> valueType
+ | underlying -> underlying
+
+ if not enumType.IsEnum then
+ EnumEncoding.NotDescribed
+ else
+ match valueInfoOf question with
+ | ValueNone -> EnumEncoding.NotDescribed
+ | ValueSome info ->
+ // The values with a name: a converter of names writes a value without one as its number. An
+ // enumeration that declares no value has only such a value to write.
+ let values = Enum.GetValues enumType
+
+ let named =
+ if values.Length = 0 then
+ Seq.singleton (Enum.ToObject (enumType, 0))
+ else
+ Seq.cast values
+
+ try
+ let kinds =
+ named
+ |> Seq.map (fun value -> JsonSerializer.SerializeToElement(value, info).ValueKind)
+ |> Seq.distinct
+ |> Seq.toArray
+
+ match kinds with
+ | [| JsonValueKind.Number |] -> EnumEncoding.Number
+ | [| JsonValueKind.String |] -> EnumEncoding.String
+ | _ -> EnumEncoding.NotDescribed
+ with
+ | :? JsonException
+ | :? NotSupportedException ->
+ // The converter refuses a value, as the one of names does when it was made to. These are the
+ // two exceptions that the serializer has for a value that a converter does not write.
+ EnumEncoding.NotDescribed
+
+ /// How a tuple is written: System.Text.Json writes it as an object, FSharp.SystemTextJson as an array.
+ let tupleEncodingOf (struct (_, tupleType : Type) as question) : TupleEncoding =
+ if not (FSharpType.IsTuple tupleType) then
+ TupleEncoding.NotDescribed
+ else
+ match valueInfoOf question with
+ | ValueSome info when info.Kind = JsonTypeInfoKind.Object -> TupleEncoding.Object
+ | ValueSome info when isTupleConverter (info.Converter.GetType ()) -> TupleEncoding.Array
+ | _ -> TupleEncoding.NotDescribed
+
+ ///
+ /// How a collection is written: as an array when the contract says that the serializer writes its items, or
+ /// when a converter of FSharp.SystemTextJson for lists or sets writes it.
+ ///
+ let collectionEncodingOf (question : struct (DocumentMember voption * Type)) : CollectionEncoding =
+ match valueInfoOf question with
+ // The contract of a type whose items the serializer writes names the type of an item.
+ | ValueSome info when info.Kind = JsonTypeInfoKind.Enumerable -> CollectionEncoding.Array (nonNull info.ElementType)
+ | ValueSome info ->
+ let converter = info.Converter.GetType ()
+
+ if
+ isMadeOf typedefof> converter
+ || isMadeOf typedefof> converter
+ then
+ CollectionEncoding.Array (converter.GetGenericArguments()[0])
+ else
+ CollectionEncoding.NotDescribed
+ | ValueNone -> CollectionEncoding.NotDescribed
+
+ ///
+ /// How a map is written: as an object when the contract says that the serializer writes it as a dictionary, and
+ /// as the converter of FSharp.SystemTextJson that writes it does.
+ ///
+ let mapEncodingOf (question : struct (DocumentMember voption * Type)) : MapEncoding =
+ match valueInfoOf question with
+ // The contract of a dictionary names the types of a key and of a value.
+ | ValueSome info when info.Kind = JsonTypeInfoKind.Dictionary ->
+ MapEncoding.Object (nonNull info.KeyType, nonNull info.ElementType)
+ | ValueSome info ->
+ let converter = info.Converter.GetType ()
+
+ if isMadeOf typedefof> converter then
+ let arguments = converter.GetGenericArguments ()
+ MapEncoding.Object (arguments[0], arguments[1])
+ elif isMadeOf typedefof> converter then
+ let arguments = converter.GetGenericArguments ()
+ MapEncoding.ArrayOfPairs (arguments[0], arguments[1])
+ else
+ MapEncoding.NotDescribed
+ | ValueNone -> MapEncoding.NotDescribed
+
+ ///
+ /// How FSharp.SystemTextJson writes a case of a union with the options that it has for the union. The rules
+ /// are read from the source of the library and compared with what it writes.
+ ///
+ ///
+ ///
+ /// A value that is null is written as null before anything else, and a case without fields as its tag alone
+ /// when such tags are unwrapped. Then the encoding decides where the tag is: in a property next to the
+ /// property of the fields, as the name of the property of the fields, as the first item or a property among
+ /// the fields, or nowhere.
+ ///
+ ///
+ /// The fields are an array of their values or an object with a property for each. A case with one field
+ /// writes that value in place of the fields where the encoding unwraps such cases, which the internal tag and
+ /// the encoding without tags do not do. A case whose one field is a record writes the members of the record
+ /// in place of the fields where the encoding unwraps records. The library writes such a record with the
+ /// options of the union, so it is described only when those are the common ones and the record by itself is
+ /// written by the library as well: then the object is the record as it is written anywhere, and
+ ///
+ /// names its members. Only next to an internal tag is the object more than the record.
+ ///
+ ///
+ let writtenCaseOf (options : UnionOptions) (case : UnionCaseInfo) : UnionCaseEncoding =
+ let encoding = options.Encoding
+ let fields = case.GetFields ()
+ let tag = unionTagOf options.TagNamingPolicy case
+ let hasNamedFields = encoding.HasFlag JsonUnionEncoding.NamedFields
+
+ // The case that a union marked to do so represents by null. The compiler lets a union be marked so only
+ // when it has a case with fields as well (FS1196), so the converter of the unions without fields, which
+ // writes a tag for every case, never writes such a union.
+ let isNullCase =
+ fields.Length = 0
+ && (
+ match FSharpValue.MakeUnion (case, [||], AnyAccessibility) with
+ | null -> true
+ | _ -> false
+ )
+
+ // The flag that unwraps records holds the one that names the fields.
+ let unwrapsRecord =
+ fields.Length = 1
+ && FSharpType.IsRecord (fields[0].PropertyType, AnyAccessibility)
+ && encoding.HasFlag JsonUnionEncoding.UnwrapRecordCases
+
+ let unwrapsField =
+ not unwrapsRecord
+ && fields.Length = 1
+ && encoding.HasFlag JsonUnionEncoding.UnwrapSingleFieldCases
+
+ // Whether the record that the case unwraps is written as the layout describes a record of its type. The
+ // converter of the union writes the members of the record with its own options of the serializer and
+ // with the options of FSharp.SystemTextJson that it has, or with the ones that it finds for the record.
+ // The record by itself is written the same way only by a converter that the same converter among the
+ // options makes. One that an attribute of the record makes by itself has no naming policy, and another
+ // converter among the options has other options.
+ let isRecordDescribed () =
+ let recordType = fields[0].PropertyType
+
+ match options.Common, writerOf recordType, converterOptionsOf recordType with
+ | ValueSome common, Writer.Record _, ValueSome ofRecord ->
+ ofRecord.Common
+ |> ValueOption.exists (fun forRecords -> obj.ReferenceEquals (forRecords, common))
+ | _ -> false
+
+ let step (container : ImmutableArray) (next : JsonStep) : ImmutableArray = container.Add next
+
+ // The fields as the properties of the object at the end of the steps, which holds the tag as well or not.
+ let asProperties (container : ImmutableArray) (holdsTag : bool) : ImmutableArray voption =
+ if unwrapsRecord then
+ if not (isRecordDescribed ()) then
+ ValueNone
+ elif holdsTag then
+ ValueSome (ImmutableArray.Create (UnionFieldPlace.Members container))
+ else
+ // Without a tag in it the object is the record as it is written anywhere.
+ ValueSome (ImmutableArray.Create (UnionFieldPlace.Value container))
+ else
+ unionFieldNamesOf options case
+ |> Seq.map (fun name -> UnionFieldPlace.Value (step container (JsonStep.Property name)))
+ |> ImmutableArray.CreateRange
+ |> ValueSome
+
+ // The fields as the items of the array at the end of the steps, from the index that is given.
+ let asItems (container : ImmutableArray) (first : int) : ImmutableArray voption =
+ fields
+ |> Seq.mapi (fun index _ -> UnionFieldPlace.Value (step container (JsonStep.Item (first + index))))
+ |> ImmutableArray.CreateRange
+ |> ValueSome
+
+ // The fields as what the property at the end of the steps holds.
+ let asFields (container : ImmutableArray) : ImmutableArray voption =
+ if unwrapsField then
+ ValueSome (ImmutableArray.Create (UnionFieldPlace.Value container))
+ elif hasNamedFields then
+ asProperties container false
+ else
+ asItems container 0
+
+ let written (mark : UnionCaseMark) (places : ImmutableArray voption) : UnionCaseEncoding =
+ match places with
+ | ValueSome places -> UnionCaseEncoding.Written (mark, places)
+ | ValueNone -> UnionCaseEncoding.NotDescribed
+
+ let root = ImmutableArray.Empty
+
+ if isNullCase then
+ UnionCaseEncoding.Written (UnionCaseMark.Null, ImmutableArray.Empty)
+ elif
+ fields.Length = 0
+ && encoding.HasFlag JsonUnionEncoding.UnwrapFieldlessTags
+ then
+ UnionCaseEncoding.Written (UnionCaseMark.Tag (root, tag), ImmutableArray.Empty)
+ else
+ match int encoding &&& UnionFormatBits with
+ // No bit says where the tag is: next to the fields.
+ | 0 ->
+ written
+ (UnionCaseMark.Tag (ImmutableArray.Create (JsonStep.Property options.TagName), tag))
+ (asFields (ImmutableArray.Create (JsonStep.Property options.FieldsName)))
+ | bits when bits = int JsonUnionEncoding.ExternalTag ->
+ let name = propertyNameOf tag
+ written (UnionCaseMark.Property name) (asFields (ImmutableArray.Create (JsonStep.Property name)))
+ | bits when bits = int JsonUnionEncoding.InternalTag ->
+ if hasNamedFields then
+ written
+ (UnionCaseMark.Tag (ImmutableArray.Create (JsonStep.Property options.TagName), tag))
+ (asProperties root true)
+ else
+ written (UnionCaseMark.Tag (ImmutableArray.Create (JsonStep.Item 0), tag)) (asItems root 1)
+ | UntaggedBit -> written UnionCaseMark.Unmarked (asProperties root false)
+ // Two places for the tag at once: the library fails to write such a union.
+ | _ -> UnionCaseEncoding.NotDescribed
+
+ ///
+ /// Whether a case is written as an object with two properties of the name of the tag. FSharp.SystemTextJson
+ /// writes one when the options give the property of the fields the name of the tag, and when a field with a
+ /// name, or a member of the record whose members the case writes, has that name next to an internal tag.
+ ///
+ ///
+ /// The place of the tag is then the place of a field or on the way to one. A query reads one property of a
+ /// name, and nothing says which of the two it is: the tag cannot be told, and neither can the field.
+ ///
+ let writesTagTwice (options : UnionOptions) (case : UnionCaseInfo) : bool =
+ match writtenCaseOf options case with
+ | UnionCaseEncoding.Written (UnionCaseMark.Tag (tagPath, _), places) ->
+ places
+ |> Seq.exists (fun place ->
+ match place with
+ | UnionFieldPlace.Value path -> beginsWith tagPath path
+ | UnionFieldPlace.Members container ->
+ // The members of the record are properties of the object that holds the tag.
+ let fields = case.GetFields ()
+ let recordType = fields[0].PropertyType
+
+ FSharpType.GetRecordFields (recordType, AnyAccessibility)
+ |> Array.exists (fun field ->
+ nameOf { Holder = recordType; Member = field }
+ |> ValueOption.exists (fun name -> beginsWith tagPath (container.Add (JsonStep.Property name)))
+ )
+ )
+ | _ -> false
+
+ ///
+ /// How a case of a union is written. The contract of the union names the converter, and the converters of
+ /// FSharp.SystemTextJson for unions are told by their classes; the options that the union is written with
+ /// say the rest.
+ ///
+ ///
+ /// No case of a union is described when one of its cases is written with the name of the tag twice: the
+ /// second property of such a document can hold what is the tag of another case.
+ ///
+ let unionCaseEncodingOf (struct (memberOfDocument : DocumentMember voption, case : UnionCaseInfo)) : UnionCaseEncoding =
+ let unionType = case.DeclaringType
+
+ match valueInfoWith false (struct (memberOfDocument, unionType)) with
+ | ValueSome info ->
+ let converter = info.Converter.GetType ()
+
+ if isMadeOf typedefof> converter then
+ // The class says all of it: the one field of the one case is written in place of the union.
+ UnionCaseEncoding.Written (
+ UnionCaseMark.Unmarked,
+ ImmutableArray.Create (UnionFieldPlace.Value ImmutableArray.Empty)
+ )
+ elif
+ isMadeBySerializer info
+ && (isMadeOf typedefof> converter
+ || isMadeOf typedefof> converter)
+ then
+ match unionOptionsOf unionType with
+ | ValueSome options when
+ FSharpType.GetUnionCases (unionType, AnyAccessibility)
+ |> Array.exists (writesTagTwice options)
+ ->
+ UnionCaseEncoding.NotDescribed
+ | ValueSome options -> writtenCaseOf options case
+ | ValueNone -> UnionCaseEncoding.NotDescribed
+ else
+ UnionCaseEncoding.NotDescribed
+ | ValueNone -> UnionCaseEncoding.NotDescribed
+
+ let rememberedName = remembered nameOf
+ let rememberedEncoding = remembered encodingOf
+ let rememberedEnumEncoding = remembered enumEncodingOf
+ let rememberedTupleEncoding = remembered tupleEncodingOf
+ let rememberedCollectionEncoding = remembered collectionEncodingOf
+ let rememberedMapEncoding = remembered mapEncodingOf
+ let rememberedUnionCaseEncoding = remembered unionCaseEncodingOf
+
+ interface IJsonLayout with
+
+ ///
+ member _.MemberName (memberOfDocument : DocumentMember) : string voption = rememberedName memberOfDocument
+
+ ///
+ member _.OptionalEncoding (memberOfDocument : DocumentMember voption, valueType : Type) : OptionalEncoding =
+ rememberedEncoding (struct (memberOfDocument, valueType))
+
+ ///
+ member _.EnumEncoding (memberOfDocument : DocumentMember voption, enumType : Type) : EnumEncoding =
+ rememberedEnumEncoding (struct (memberOfDocument, enumType))
+
+ ///
+ member _.TupleEncoding (memberOfDocument : DocumentMember voption, tupleType : Type) : TupleEncoding =
+ rememberedTupleEncoding (struct (memberOfDocument, tupleType))
+
+ ///
+ member _.CollectionEncoding (memberOfDocument : DocumentMember voption, collectionType : Type) : CollectionEncoding =
+ rememberedCollectionEncoding (struct (memberOfDocument, collectionType))
+
+ ///
+ member _.MapEncoding (memberOfDocument : DocumentMember voption, mapType : Type) : MapEncoding =
+ rememberedMapEncoding (struct (memberOfDocument, mapType))
+
+ ///
+ member _.UnionCaseEncoding (memberOfDocument : DocumentMember voption, case : UnionCaseInfo) : UnionCaseEncoding =
+ rememberedUnionCaseEncoding (struct (memberOfDocument, case))
+
+ ///
+ /// The layout of the documents that System.Text.Json writes with , with or without the
+ /// converter of FSharp.SystemTextJson among them. Pass the options that the Cosmos DB client serializes with.
+ ///
+ ///
+ ///
+ /// The layout reads when it is made and keeps a copy, so a change of them afterwards
+ /// does not reach it. What it finds out about a member is kept for as long as the layout is.
+ ///
+ ///
+ /// The resolver of the contracts, , is not
+ /// copied: it can be any object, so the layout asks the one that the serializer asks. A change of that resolver
+ /// before the layout is asked for the first time reaches the layout as it reaches the serializer. From that
+ /// question on a refuses a
+ /// change of its ,
+ /// as it does after the first document is written, so the layout and the serializer cannot come to disagree
+ /// unnoticed. A resolver of the application has to give one answer for a type every time it is asked, which the
+ /// serializer needs as well.
+ ///
+ ///
+ /// It describes the members of the types that System.Text.Json writes as objects and of the records that
+ /// FSharp.SystemTextJson writes. A member of a type that another converter writes has no name here, and neither
+ /// has a member of a record whose contract a resolver of the application made from a converter: nothing shows how
+ /// that converter was made.
+ ///
+ ///
+ /// An enumeration, a tuple, a collection and a map are described as System.Text.Json and FSharp.SystemTextJson
+ /// write them, by the converter that the options have for the type or by the one that a member names for itself.
+ /// What a converter of the application writes is not described, except for an enumeration: the layout has the
+ /// serializer write every value of it that has a name, and describes the enumeration when numbers come out for
+ /// all of them or strings for all of them. An enumeration whose values come out in both ways is not described,
+ /// and neither is one whose converter refuses a value.
+ ///
+ ///
+ /// The options of the serializer.
+ /// The layout.
+ let ofSerializerOptions (options : JsonSerializerOptions) : IJsonLayout = SerializerLayout options
diff --git a/src/Cosmos.Quotations/Normalizer.fs b/src/Cosmos.Quotations/Normalizer.fs
new file mode 100644
index 00000000..81dcc39d
--- /dev/null
+++ b/src/Cosmos.Quotations/Normalizer.fs
@@ -0,0 +1,1277 @@
+///
+/// Brings a quotation into its normal form, the one form that every spelling of the same condition has. Pipes,
+/// composition, bindings, calls of functions marked with
+/// and splices of other quotations are only ways of
+/// writing; after normalization they are gone, and what is left says what is computed from the document.
+///
+///
+///
+/// A quotation is in normal form when none of these is left in it:
+///
+///
+/// - a call of a pipe or composition operator of FSharp.Core;
+/// - a function written in place and applied in place, or a whose variable
+/// is never assigned to;
+/// - a call of a function, method or property getter that has a reflected definition and can be
+/// replaced by it. Four kinds of them stay: a call of a member that a derived class can override, the reading of a
+/// module-level value, a call that does not pass the arguments its definition takes, which only a quotation built
+/// through the API can hold, and a call of a function that calls itself, directly or through the functions it calls,
+/// because writing its definition in its place would never end;
+/// - a call that passes witnesses, which the compiler writes for an operator with a member
+/// constraint such as +;
+/// - the address of a value as the receiver of a property, which the compiler writes for a struct
+/// anonymous record;
+/// - the reading of a part of a tuple, record or union case that is built in the same
+/// place;
+/// - a condition that is a constant, a comparison of a boolean with a constant, or a double
+/// negation.
+///
+///
+/// What a query cannot express even then is an error: see
+/// .
+///
+///
+/// A value bound by is written at every place it is used, and so is the argument of a function
+/// that is written and applied in place. Every such place holds the very same node, also after what is rewritten
+/// around the node and inside it later, so a stage that computes a value outside the query can tell that the places
+/// are one value and computes it once, as the code does.
+///
+///
+/// A node that stands at several places of the quotation as it comes stays one node in the same way: a quotation that
+/// is spliced into two places is the same nodes at both, and code can build a tree with one node twice.
+///
+///
+module internal FSharp.Azure.Cosmos.Quotations.Normalizer
+
+open System
+open System.Collections.Concurrent
+open System.Collections.Frozen
+open System.Collections.Generic
+open System.Collections.Immutable
+open System.Reflection
+open System.Runtime.CompilerServices
+open FSharp.Quotations
+open FSharp.Quotations.ExprShape
+open FSharp.Quotations.Patterns
+open FSharp.Reflection
+
+open FSharp.Azure.Cosmos
+
+/// The functions of FSharp.Core that normalization rewrites.
+[]
+type private KnownFunction =
+ /// The forward pipe of one value.
+ | PipeRight
+ /// The forward pipe of two values.
+ | PipeRight2
+ /// The forward pipe of three values.
+ | PipeRight3
+ /// The backward pipe of one value.
+ | PipeLeft
+ /// The backward pipe of two values.
+ | PipeLeft2
+ /// The backward pipe of three values.
+ | PipeLeft3
+ /// The forward composition of two functions.
+ | ComposeRight
+ /// The backward composition of two functions.
+ | ComposeLeft
+ /// The negation of a boolean.
+ | Not
+ /// The equality operator.
+ | Equality
+ /// The inequality operator.
+ | Inequality
+ /// The first item of a pair.
+ | Fst
+ /// The second item of a pair.
+ | Snd
+
+///
+/// The largest number of rewriting steps one normalization takes before it gives up with
+/// . A step removes one pipe,
+/// binding, call or projection, so a quotation written by hand needs a few dozen.
+///
+[]
+let private MaxRewrites = 10_000
+
+/// The name of a variable that normalization introduces; the compiler names its own generated variables this way.
+[]
+let private GeneratedVariableName = "_arg1"
+
+///
+/// The method that calls.
+///
+let private calledMethod (call : Expr) : MethodInfo =
+ match call with
+ | Call (_, method, _) -> method
+ | _ -> invalidArg (nameof call) $"The quotation is not a call: %A{call}"
+
+///
+/// The negation of FSharp.Core, .
+///
+let private notMethod = calledMethod <@ not true @>
+
+///
+/// The module of FSharp.Core that declares every
+/// :
+/// .
+///
+let private operatorsModule = notMethod.DeclaringType
+
+///
+/// The functions of FSharp.Core that normalization rewrites, by the metadata token of the method, which every
+/// instantiation of a generic function shares. The methods are taken from quotations that call them, never from names,
+/// so another function with the name of an operator is not mistaken for it.
+///
+let private knownFunctions : FrozenDictionary =
+ seq {
+ struct (calledMethod <@ true |> not @>, KnownFunction.PipeRight)
+ struct (calledMethod <@ (true, true) ||> (&&) @>, KnownFunction.PipeRight2)
+ struct (calledMethod
+ <@
+ (true, true, true)
+ |||> (fun first second third -> first && second && third)
+ @>,
+ KnownFunction.PipeRight3)
+ struct (calledMethod <@ not <| true @>, KnownFunction.PipeLeft)
+ struct (calledMethod <@ (&&) <|| (true, true) @>, KnownFunction.PipeLeft2)
+ struct (calledMethod
+ <@
+ (fun first second third -> first && second && third)
+ <||| (true, true, true)
+ @>,
+ KnownFunction.PipeLeft3)
+ struct (calledMethod <@ not >> not @>, KnownFunction.ComposeRight)
+ struct (calledMethod <@ not << not @>, KnownFunction.ComposeLeft)
+ struct (notMethod, KnownFunction.Not)
+ struct (calledMethod <@ true = true @>, KnownFunction.Equality)
+ struct (calledMethod <@ true <> true @>, KnownFunction.Inequality)
+ struct (calledMethod <@ fst (true, true) @>, KnownFunction.Fst)
+ struct (calledMethod <@ snd (true, true) @>, KnownFunction.Snd)
+ }
+ |> _.ToFrozenDictionary((fun (struct (method, _)) -> method.MetadataToken), (fun (struct (_, known)) -> known))
+
+///
+/// Which is, when it
+/// is one.
+///
+let private knownFunctionOf (method : MethodInfo) : KnownFunction voption =
+ if Type.(=) (method.DeclaringType, operatorsModule) then
+ match knownFunctions.TryGetValue method.MetadataToken with
+ | true, known -> ValueSome known
+ | _ -> ValueNone
+ else
+ ValueNone
+
+///
+/// A call of one of the s, with its arguments.
+///
+[]
+let private (|KnownCall|_|) (expr : Expr) : struct (KnownFunction * Expr list) voption =
+ match expr with
+ | Call (None, method, arguments) ->
+ match knownFunctionOf method with
+ | ValueSome known -> ValueSome struct (known, arguments)
+ | ValueNone -> ValueNone
+ | _ -> ValueNone
+
+///
+/// A boolean that is written in the quotation or left there by a rewrite. A captured boolean is not one: it is a
+/// parameter of the query, and the text of a query never depends on the value of a parameter.
+///
+///
+/// The default value of the type is written another way: the compiler writes it for a
+/// boolean that is made with its constructor, bool (), and a quotation that is built by hand may hold it. Left
+/// as a condition, it would keep both branches in the quotation as code that runs.
+///
+[]
+let private (|Constant|_|) (expr : Expr) : bool voption =
+ match expr with
+ | ValueWithName _ -> ValueNone
+ | Value (:? bool as value, _) -> ValueSome value
+ | DefaultValue valueType when Type.(=) (valueType, typeof) -> ValueSome false
+ | _ -> ValueNone
+
+///
+/// The reflected definitions that were looked up, by method; no value for a method that has none. FSharp.Core searches
+/// the resources of the declaring assembly on every lookup of a method without a definition, which takes microseconds,
+/// and a predicate asks for every call in it, operators included. A reflected definition is a fact of the assembly,
+/// so the answers are kept for the process.
+///
+let private reflectedDefinitions = ConcurrentDictionary()
+
+///
+/// Looks the reflected definition of a method up in FSharp.Core; a delegate that is created once, for
+/// .
+///
+let private lookUpReflectedDefinition =
+ Func(fun method ->
+ Expr.TryGetReflectedDefinition method
+ |> ValueOption.ofOption
+ )
+
+///
+/// The reflected definition of , when it has one.
+///
+let private reflectedDefinitionOf (method : MethodBase) : Expr voption =
+ reflectedDefinitions.GetOrAdd (method, lookUpReflectedDefinition)
+
+/// Whether two methods are one method of one type, whatever types its generic parameters stand for.
+let private isSameDefinition (first : MethodBase) (second : MethodBase) : bool =
+ first.MetadataToken = second.MetadataToken
+ && first.Module.Equals second.Module
+
+///
+/// Whether is one of , whatever types its generic parameters
+/// stand for. A loop over the indexes, because the check of what is left asks for every call of a quotation, and the
+/// list is empty nearly always.
+///
+let private isAmong (methods : IReadOnlyList) (method : MethodBase) : bool =
+ let mutable found = false
+
+ for index in 0 .. methods.Count - 1 do
+ found <- found || isSameDefinition methods[index] method
+
+ found
+
+/// Whether two lists hold the very same nodes, which means that a rewrite of the first changed nothing.
+let rec private areSame (first : Expr list) (second : Expr list) : bool =
+ match first, second with
+ | [], [] -> true
+ | firstHead :: firstTail, secondHead :: secondTail ->
+ obj.ReferenceEquals (firstHead, secondHead)
+ && areSame firstTail secondTail
+ | _ -> false
+
+///
+/// What is known about the body of a binding before the bound value is written into it.
+///
+[]
+type private Survey = {
+ /// How many places read the variable.
+ Uses : int
+ ///
+ /// Whether the value cannot simply be written at those places: the body binds the variable again, it binds a free
+ /// variable of the value, or it holds a node that stands at several places and has to stay one node.
+ ///
+ NeedsCare : bool
+}
+
+///
+/// Looks at , the body of a binding of , before a value with the
+/// free variables is written into it.
+///
+///
+/// The case of a function sees every binding, whatever construct makes it. A quotation keeps the variable of a
+/// , of a recursive function, of a loop, of an exception handler and of a delegate in a function
+/// among the parts of that node, and those parts are what the shape of the node gives.
+///
+/// The variable that the value is bound to.
+/// The free variables of the value.
+/// The nodes that stand at several places, by their identity.
+/// The body of the binding, or a part of it.
+let rec private survey (variable : Var) (freeInValue : HashSet) (shared : HashSet) (expr : Expr) : Survey =
+ match expr with
+ | ShapeVar candidate -> {
+ Uses = (if obj.ReferenceEquals (candidate, variable) then 1 else 0)
+ NeedsCare = false
+ }
+ | ShapeLambda (bound, body) ->
+ let inside = survey variable freeInValue shared body
+
+ {
+ inside with
+ NeedsCare =
+ inside.NeedsCare
+ || obj.ReferenceEquals (bound, variable)
+ || freeInValue.Contains bound
+ || (shared.Count > 0 && shared.Contains expr)
+ }
+ | ShapeCombination (_, parts) ->
+ let mutable uses = 0
+ let mutable needsCare = shared.Count > 0 && shared.Contains expr
+
+ for part in parts do
+ let inside = survey variable freeInValue shared part
+ uses <- uses + inside.Uses
+ needsCare <- needsCare || inside.NeedsCare
+
+ { Uses = uses; NeedsCare = needsCare }
+
+///
+/// Replaces the free occurrences of in with
+/// , with care for the bindings inside and for the nodes in it that
+/// stand at several places.
+///
+///
+///
+/// One variable can be bound at several places of a quotation: a quotation that is spliced twice arrives as the same
+/// nodes each time, and every call of a function with a reflected definition gets nodes of its own that bind the
+/// variables of the one definition. So an inner binding of
+/// hides it and is left alone, and a binding that would capture a free variable of
+/// is renamed first.
+///
+///
+/// A node without the variable is returned as it is, so a node that stands at several places stays one node. When such
+/// a node holds the variable, it is built anew at the first of its places, and gives the
+/// other places the same new node.
+///
+///
+/// The variable to replace.
+/// What takes its place.
+/// The free variables of .
+///
+/// The nodes that stand at several places, by their identity. A node that takes the place of one of them is added.
+///
+///
+/// What this replacing has made of the nodes that stand at several places so far, by their identity.
+///
+/// The quotation to replace it in.
+let rec private substituteWithCare
+ (variable : Var)
+ (value : Expr)
+ (freeInValue : HashSet)
+ (shared : HashSet)
+ (replaced : Dictionary)
+ (expr : Expr)
+ : Expr =
+ if shared.Count > 0 && shared.Contains expr then
+ match replaced.TryGetValue expr with
+ | true, known -> known
+ | _ ->
+ let replacement = substituteInParts variable value freeInValue shared replaced expr
+ replaced.Add (expr, replacement)
+ shared.Add replacement |> ignore
+ replacement
+ else
+ substituteInParts variable value freeInValue shared replaced expr
+
+///
+/// What
+///
+/// makes of a node that it has not replaced before.
+///
+and private substituteInParts
+ (variable : Var)
+ (value : Expr)
+ (freeInValue : HashSet)
+ (shared : HashSet)
+ (replaced : Dictionary)
+ (expr : Expr)
+ : Expr =
+ match expr with
+ | ShapeVar candidate ->
+ if obj.ReferenceEquals (candidate, variable) then
+ value
+ else
+ expr
+ | ShapeLambda (bound, _) when obj.ReferenceEquals (bound, variable) -> expr
+ | ShapeLambda (bound, body) when freeInValue.Contains bound ->
+ let renamed = Var (bound.Name, bound.Type, bound.IsMutable)
+
+ // The renaming is a replacing of its own, with its own account of what it made of which node.
+ let renamedBody =
+ substituteWithCare
+ bound
+ (Expr.Var renamed)
+ (HashSet [ renamed ])
+ shared
+ (Dictionary ReferenceEqualityComparer.Instance)
+ body
+
+ Expr.Lambda (renamed, substituteWithCare variable value freeInValue shared replaced renamedBody)
+ | ShapeLambda (bound, body) ->
+ let newBody = substituteWithCare variable value freeInValue shared replaced body
+ if obj.ReferenceEquals (newBody, body) then
+ expr
+ else
+ Expr.Lambda (bound, newBody)
+ | ShapeCombination (shape, parts) ->
+ let newParts =
+ parts
+ |> List.map (substituteWithCare variable value freeInValue shared replaced)
+ if areSame parts newParts then
+ expr
+ else
+ RebuildShapeCombination (shape, newParts)
+
+///
+/// with at every place where is used.
+///
+///
+///
+/// A value that is written at more than one place is the same node at all of them, and it is added to
+/// , the nodes that have to stay one node from then on. The code computes such a value once,
+/// and a later stage tells by the node that the places are one value.
+///
+///
+/// When nothing inside needs care, which is nearly always, the replacing is left to
+/// .
+/// It builds its nodes without validating them, while
+///
+/// asks reflection whether the declaring type of every member it rebuilds an access to is a union, which takes
+/// microseconds for each access.
+///
+///
+/// Otherwise
+///
+/// does it. The function of FSharp.Core replaces a variable inside an inner binding of the same variable too, and it
+/// builds every node of the body anew, so a node that stands at two places would come out as two nodes.
+///
+///
+let private bind (shared : HashSet) (variable : Var) (value : Expr) (body : Expr) : Expr =
+ let freeInValue = HashSet(value.GetFreeVars ())
+ let found = survey variable freeInValue shared body
+
+ if found.Uses = 0 then
+ body
+ else
+ // A variable is no node to keep: every place that reads it is a node of its own already.
+ match value with
+ | Var _ -> ()
+ | _ when found.Uses > 1 -> shared.Add value |> ignore
+ | _ -> ()
+
+ if found.NeedsCare then
+ substituteWithCare variable value freeInValue shared (Dictionary ReferenceEqualityComparer.Instance) body
+ else
+ body.Substitute (fun candidate ->
+ if obj.ReferenceEquals (candidate, variable) then
+ Some value
+ else
+ None
+ )
+
+///
+/// Whether assigns to or takes its address, through which it can
+/// be assigned to.
+///
+///
+/// A binding of the same variable inside hides it: what is assigned to under that binding is
+/// the variable of that binding. One variable is bound at two nested places in a quotation that is built through the
+/// API or spliced into itself.
+///
+let rec private isChanged (variable : Var) (expr : Expr) : bool =
+ match expr with
+ | VarSet (target, _) when obj.ReferenceEquals (target, variable) -> true
+ | AddressOf (Var target) when obj.ReferenceEquals (target, variable) -> true
+ | ShapeVar _ -> false
+ | ShapeLambda (bound, _) when obj.ReferenceEquals (bound, variable) -> false
+ | ShapeLambda (_, body) -> isChanged variable body
+ | ShapeCombination (_, parts) -> parts |> List.exists (isChanged variable)
+
+///
+/// The function that applies and then , written as a function of
+/// one parameter. The parameter takes the name of the parameter of when that is written as a
+/// function, so that a name the user chose survives.
+///
+let private compose (first : Expr) (second : Expr) : Expr =
+ let name =
+ match first with
+ | Lambda (parameter, _) -> parameter.Name
+ | _ -> GeneratedVariableName
+
+ let parameter = Var (name, FSharpType.GetFunctionElements first.Type |> fst)
+ Expr.Lambda (parameter, Expr.Application (second, Expr.Application (first, Expr.Var parameter)))
+
+///
+/// Pipes and composition: x |> f and f <| x are f x, the pipes of two and three values
+/// apply the function to the values in turn, and f >> g and g << f are
+/// fun x -> g (f x).
+///
+let private eliminatePipeOrComposition (known : KnownFunction) (arguments : Expr list) : Expr voption =
+ match known, arguments with
+ | KnownFunction.PipeRight, [ argument; func ]
+ | KnownFunction.PipeLeft, [ func; argument ] -> ValueSome (Expr.Application (func, argument))
+ | KnownFunction.PipeRight2, [ first; second; func ]
+ | KnownFunction.PipeLeft2, [ func; first; second ] -> ValueSome (Expr.Applications (func, [ [ first ]; [ second ] ]))
+ | KnownFunction.PipeRight3, [ first; second; third; func ]
+ | KnownFunction.PipeLeft3, [ func; first; second; third ] ->
+ ValueSome (Expr.Applications (func, [ [ first ]; [ second ]; [ third ] ]))
+ | KnownFunction.ComposeRight, [ first; second ]
+ | KnownFunction.ComposeLeft, [ second; first ] -> ValueSome (compose first second)
+ | _ -> ValueNone
+
+///
+/// and
+/// are the reading of an item of a
+/// tuple, the one form the other rules and the later stages know.
+///
+let private eliminatePairFunction (known : KnownFunction) (arguments : Expr list) : Expr voption =
+ match known, arguments with
+ | KnownFunction.Fst, [ pair ] -> ValueSome (Expr.TupleGet (pair, 0))
+ | KnownFunction.Snd, [ pair ] -> ValueSome (Expr.TupleGet (pair, 1))
+ | _ -> ValueNone
+
+///
+/// The negation of .
+///
+let private negate (condition : Expr) : Expr = Expr.Call (notMethod, [ condition ])
+
+///
+/// Booleans in one form, the calls: a boolean compared with a constant is itself or its negation, the negation of a
+/// constant is a constant, and a double negation is gone.
+///
+let private canonicalBooleanCall (known : KnownFunction) (arguments : Expr list) : Expr voption =
+ match known, arguments with
+ | KnownFunction.Not, [ Constant value ] -> ValueSome (Expr.Value (not value))
+ | KnownFunction.Not, [ KnownCall (KnownFunction.Not, [ condition ]) ] -> ValueSome condition
+ | KnownFunction.Equality, [ condition; Constant true ]
+ | KnownFunction.Equality, [ Constant true; condition ]
+ | KnownFunction.Inequality, [ condition; Constant false ]
+ | KnownFunction.Inequality, [ Constant false; condition ] -> ValueSome condition
+ | KnownFunction.Equality, [ condition; Constant false ]
+ | KnownFunction.Equality, [ Constant false; condition ]
+ | KnownFunction.Inequality, [ condition; Constant true ]
+ | KnownFunction.Inequality, [ Constant true; condition ] -> ValueSome (negate condition)
+ | _ -> ValueNone
+
+///
+/// Booleans in one form, the choices: a condition that is a constant is the branch it chooses, and a choice between
+/// and is its condition or the negation of it. && and
+/// || need no rule of their own: the compiler writes them as a choice already.
+///
+let private canonicalChoice (condition : Expr) (whenTrue : Expr) (whenFalse : Expr) : Expr voption =
+ match condition, whenTrue, whenFalse with
+ | Constant chosen, _, _ -> ValueSome (if chosen then whenTrue else whenFalse)
+ | _, Constant true, Constant false -> ValueSome condition
+ | _, Constant false, Constant true -> ValueSome (negate condition)
+ | _ -> ValueNone
+
+///
+/// The index of the item that the property of a reference tuple with reads:
+/// Item1 to Item7.
+///
+let private tupleItemIndex (name : string) : int voption =
+ if
+ name.Length = 5
+ && name.StartsWith ("Item", StringComparison.Ordinal)
+ && name[4] >= '1'
+ && name[4] <= '7'
+ then
+ ValueSome (int name[4] - int '1')
+ else
+ ValueNone
+
+///
+/// The value among that reads, when it is one of
+/// , which are in the order of the values.
+///
+let private valueOfField (fields : PropertyInfo array) (values : Expr list) (property : PropertyInfo) : Expr voption =
+ fields
+ |> Array.tryFindIndex (fun field -> String.Equals (field.Name, property.Name, StringComparison.Ordinal))
+ |> ValueOption.map (fun index -> values[index])
+
+///
+/// The reading of a field of a record, an anonymous record or a union case that is built in the same place is the value
+/// the field was given. The properties Item1 to Item7 of a reference tuple become the reading of an item
+/// of the tuple.
+///
+let private eliminatePropertyOfBuiltValue (receiver : Expr) (property : PropertyInfo) : Expr voption =
+ match receiver with
+ | NewRecord (recordType, values) ->
+ property
+ |> valueOfField (FSharpType.GetRecordFields (recordType, BindingFlags.Public ||| BindingFlags.NonPublic)) values
+ | NewUnionCase (case, values) -> property |> valueOfField (case.GetFields ()) values
+ | _ ->
+ match tupleItemIndex property.Name with
+ | ValueSome index when
+ FSharpType.IsTuple receiver.Type
+ && not receiver.Type.IsValueType
+ ->
+ ValueSome (Expr.TupleGet (receiver, index))
+ | _ -> ValueNone
+
+///
+/// Whether a call of always runs the code of . A member that a
+/// derived class can override does not: its definition says nothing about what the override does, so such a call is
+/// left a call.
+///
+let private isStaticallyBound (method : MethodBase) : bool =
+ not method.IsVirtual
+ || method.IsFinal
+ || (
+ match method.DeclaringType with
+ | null -> false
+ | declaringType -> declaringType.IsSealed
+ )
+
+///
+/// The sizes of the groups that the definitions of methods take their arguments in, by method; empty for a method that
+/// takes them as one group. They are kept for the process like the definitions, because reading an attribute takes
+/// microseconds too.
+///
+let private argumentGroupSizes = ConcurrentDictionary>()
+
+///
+/// Reads the sizes of the argument groups of a method from its
+/// ; a delegate that is created once, for
+/// .
+///
+let private readArgumentGroupSizes =
+ Func>(fun method ->
+ match method.GetCustomAttribute() with
+ | null -> ImmutableArray.Empty
+ | counts -> ImmutableArray.CreateRange counts.Counts
+ )
+
+///
+/// Splits the arguments of a call into the groups that the definition of the called function takes one after another.
+/// The compiler passes the arguments of a curried function as one flat list and records the sizes of the groups in
+/// ; without it there is one group. An empty
+/// group stands for the unit argument.
+///
+let private argumentGroups (method : MethodBase) (arguments : Expr list) : Expr list list =
+ let sizes = argumentGroupSizes.GetOrAdd (method, readArgumentGroupSizes)
+
+ if sizes.IsEmpty then
+ [ arguments ]
+ else
+ sizes
+ |> Seq.mapFold (fun rest size -> rest |> List.splitAt size) arguments
+ |> fst
+ |> Seq.toList
+
+///
+/// as takes it after its first
+/// parameters, which take the witnesses: a member of a base class is defined for the base class, so a receiver of a
+/// derived class is passed as the base class.
+///
+let rec private receiverFor (skipped : int) (definition : Expr) (receiver : Expr) : Expr =
+ match definition with
+ | Lambda (_, body) when skipped > 0 -> receiverFor (skipped - 1) body receiver
+ | Lambda (parameter, _) when not (Type.(=) (parameter.Type, receiver.Type)) -> Expr.Coerce (receiver, parameter.Type)
+ | _ -> receiver
+
+///
+/// The reflected definition of an inline function or member that is called with witnesses, with the method that the
+/// definition is kept for. The compiler keeps the definition of a function of a module for the form of the function
+/// that takes the witnesses, and the definition of a member of a type for the member itself. Either definition takes
+/// the witnesses first, then the receiver, then the arguments.
+///
+let private definitionWithWitnesses (method : MethodInfo) (witnessMethod : MethodInfo) : struct (MethodBase * Expr) voption =
+ match reflectedDefinitionOf witnessMethod with
+ | ValueSome definition -> ValueSome struct (witnessMethod :> MethodBase, definition)
+ | ValueNone ->
+ reflectedDefinitionOf method
+ |> ValueOption.map (fun definition -> struct (method :> MethodBase, definition))
+
+///
+/// Whether a function of takes one after another, every
+/// group as one argument: a tuple when the group has several values, the unit value when it has none.
+///
+///
+/// A definition is applied only to arguments it takes. The compiler calls a method with the arguments of its
+/// definition, but a quotation that is built through the API can call an inline member without the witnesses that the
+/// definition of the member takes first, and
+///
+/// raises an exception for an argument of another type.
+///
+let rec private takes (functionType : Type) (groups : Expr list list) : bool =
+ match groups with
+ | [] -> true
+ | group :: rest ->
+ functionType.IsGenericType
+ && Type.(=) (functionType.GetGenericTypeDefinition (), typedefof<_ -> _>)
+ && (let types = functionType.GetGenericArguments ()
+
+ let argumentType =
+ match group with
+ | [] -> typeof
+ | [ argument ] -> argument.Type
+ | arguments -> FSharpType.MakeTupleType (arguments |> Seq.map _.Type |> Seq.toArray)
+
+ Type.(=) (types[0], argumentType) && takes types[1] rest)
+
+///
+/// The error for a call of a function with a reflected definition that calls itself, when the call reads a value of
+/// the document.
+///
+/// What calls itself, as the message names it.
+/// The call.
+let private recursiveDefinition (subject : string) (call : Expr) : TranslationError =
+ call
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.NormalizationDiverged
+ $"%s{subject} calls itself, directly or through the functions it calls, so its definition cannot be written into a query."
+ (ImmutableArray.Create
+ "Call the function outside the quotation and use its result, or write the condition without recursion.")
+
+///
+/// The error for a quotation whose simplification does not end. It names no part of the quotation: what is at hand
+/// when the limit is reached is a product of the rewriting, not something the user wrote.
+///
+let private tooManyRewrites : TranslationError =
+ TranslationError.ofWhole
+ TranslationErrorCode.NormalizationDiverged
+ $"The quotation was not brought into its simplest form within %d{MaxRewrites} rewriting steps: its simplification does not end."
+
+///
+/// The error for a quotation that is nested more deeply than the stack of the thread can follow. It names no part of
+/// the quotation, because printing that part would need the stack that has run out.
+///
+let private nestedTooDeeply : TranslationError =
+ TranslationError.ofWhole
+ TranslationErrorCode.NormalizationDiverged
+ "The quotation is nested too deeply to be brought into its simplest form."
+
+///
+/// The error for a construct of F# that a query has no counterpart for.
+///
+let private unsupported (construct : string) (hint : string) (expr : Expr) : TranslationError =
+ expr
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.Unsupported
+ $"A query cannot express %s{construct}."
+ (ImmutableArray.Create hint)
+
+///
+/// The error for when it is a construct that a query cannot express whatever stands inside it:
+/// a recursive function, an assignment, a sequence of statements, a loop or exception handling.
+///
+let private neverExpressed (expr : Expr) : TranslationError voption =
+ match expr with
+ | LetRecursive _ ->
+ expr
+ |> unsupported "a recursive function" "Write the condition without recursion."
+ |> ValueSome
+ | VarSet _
+ | PropertySet _
+ | FieldSet _
+ | AddressSet _ ->
+ expr
+ |> unsupported "an assignment" "A query only reads the document."
+ |> ValueSome
+ | Sequential _ ->
+ expr
+ |> unsupported "a sequence of statements" "Write one expression."
+ |> ValueSome
+ | ForIntegerRangeLoop _
+ | WhileLoop _ ->
+ expr
+ |> unsupported "a loop" "Use the functions of the List, Array and Seq modules on the collections of the document."
+ |> ValueSome
+ | TryWith _
+ | TryFinally _ ->
+ expr
+ |> unsupported "exception handling" "A query does not raise exceptions."
+ |> ValueSome
+ | _ -> ValueNone
+
+///
+/// The nodes of that stand at more than one place of it, by their identity.
+///
+///
+///
+/// The compiler makes a node of its own for every place of a quotation, so a node stands at two places only when
+/// something put it there: a quotation that is spliced into two places is the same nodes at both, and code can build
+/// a tree with one node twice. A later stage takes one node for one value, so such a node has to stay one node
+/// through every rewrite, as a value that a binds does.
+///
+///
+/// A variable is no such node: every place that reads it reads the one variable anyway. A quotation that is nested
+/// too deeply is not walked to its end; the normalization that follows reports it.
+///
+///
+let private nodesAtSeveralPlacesOf (expr : Expr) : HashSet =
+ let seen = HashSet ReferenceEqualityComparer.Instance
+ let several = HashSet ReferenceEqualityComparer.Instance
+
+ let rec visit (expr : Expr) : unit =
+ match expr with
+ | Var _ -> ()
+ | _ ->
+ if not (seen.Add expr) then
+ // Its parts were walked at the first place.
+ several.Add expr |> ignore
+ elif RuntimeHelpers.TryEnsureSufficientExecutionStack () then
+ match expr with
+ | ShapeLambda (_, body) -> visit body
+ | ShapeCombination (_, parts) -> List.iter visit parts
+ | ShapeVar _ -> ()
+
+ visit expr
+ several
+
+///
+/// One normalization: it holds what the rewriting of one quotation has to remember.
+///
+/// The quotation that is normalized.
+type private Normalization (quotation : Expr) =
+
+ ///
+ /// The functions whose reflected definitions are being normalized right now, the innermost on top. A call of one
+ /// of them inside such a definition is a recursion.
+ ///
+ let inlining = Stack()
+
+ ///
+ /// The methods whose reflected definitions have been written into the quotation already. The definition of a
+ /// method is one tree for the process, so a second call of the method needs a copy of it: the nodes of a call are
+ /// its own, as the values are that the call computes.
+ ///
+ let expanded = HashSet()
+
+ ///
+ /// The functions that were found to call themselves, directly or through the functions they call. A call of one
+ /// of them stays a call.
+ ///
+ let recursive = List()
+
+ ///
+ /// The nodes that stand at several places of the quotation, by their identity: the ones that it comes with, and
+ /// every value that was bound by or passed as an argument and is used more than once. Such
+ /// a node has to stay one node whatever is rewritten around it and inside it.
+ ///
+ let shared = nodesAtSeveralPlacesOf quotation
+
+ ///
+ /// The normal forms of the nodes that stand at several places, by their identity, so that every place gets the
+ /// same one.
+ ///
+ let normalOfShared = Dictionary ReferenceEqualityComparer.Instance
+
+ /// The number of rewriting steps taken so far.
+ let mutable rewrites = 0
+
+ /// The first error; after it nothing is rewritten any more.
+ let mutable failure : TranslationError voption = ValueNone
+
+ /// Whether the error is that the quotation is nested too deeply, so that no part of it can be printed.
+ let mutable isTooDeep = false
+
+ /// The error that stopped the normalization, when one did.
+ member _.Failure = failure
+
+ /// The functions that the normalization found to call themselves, whose calls it left calls.
+ member _.Recursive : IReadOnlyList = recursive
+
+ ///
+ /// The reflected definition of applied to the arguments of the call. No value when the
+ /// definition does not take these arguments, and none when calls itself, directly or
+ /// through the functions it calls.
+ ///
+ ///
+ ///
+ /// A function that calls itself is found while its definition is normalized: the definition holds a call of a
+ /// function whose definition is being normalized. That call stays a call, and so does the call that led to it,
+ /// whose normalized definition is not used. What becomes of such calls is decided when the whole quotation is
+ /// normalized: one that is left and reads a value of the document is the error, and one in code that never runs
+ /// is gone by then.
+ ///
+ ///
+ /// The normal form of a definition is not kept for the next call of the method. A value that the definition binds
+ /// and reads twice is one node at both places, which says that it is computed once; kept, it would be that one
+ /// node in every call, and the calls would share a value that each of them computes for itself. So every call
+ /// after the first normalizes a copy of the definition:
+ ///
+ /// builds every node anew, also when it replaces nothing.
+ ///
+ ///
+ /// The called method.
+ /// Its reflected definition.
+ /// The arguments, in the groups the definition takes them in.
+ member private this.Inline (method : MethodBase, definition : Expr, groups : Expr list list) : Expr voption =
+ if inlining |> Seq.exists (isSameDefinition method) then
+ recursive.Add method
+ ValueNone
+ else
+ let ownDefinition =
+ if expanded.Add method then
+ definition
+ else
+ definition.Substitute (fun _ -> None)
+
+ inlining.Push method
+ let normalDefinition = this.Normalize ownDefinition
+ inlining.Pop () |> ignore
+
+ if
+ failure.IsNone
+ && not (isAmong recursive method)
+ && takes normalDefinition.Type groups
+ then
+ ValueSome (Expr.Applications (normalDefinition, groups))
+ else
+ ValueNone
+
+ ///
+ /// A call of a function or method that has a reflected definition is that definition applied to the receiver and
+ /// the arguments of the call, unless a derived class can override the method, the definition does not take these
+ /// arguments, or the function calls itself.
+ ///
+ member private this.InlineCall (receiver : Expr option, method : MethodInfo, arguments : Expr list) : Expr voption =
+ match reflectedDefinitionOf method with
+ | ValueSome definition when isStaticallyBound method ->
+ let groups =
+ match receiver with
+ | Some receiver ->
+ [ receiverFor 0 definition receiver ]
+ :: argumentGroups method arguments
+ | None -> argumentGroups method arguments
+
+ this.Inline (method, definition, groups)
+ | _ -> ValueNone
+
+ ///
+ /// The reading of a property whose getter has a reflected definition is that definition applied to the receiver
+ /// and the arguments, unless a derived class can override the getter, the definition does not take these
+ /// arguments, or the getter calls itself. A module-level value is not such a reading: it is computed once, when
+ /// its module is initialized, so it stays a reference to that value. It is told from a static property by its
+ /// definition, which is the value and not a function of the unit value.
+ ///
+ member private this.InlineGetter (receiver : Expr option, property : PropertyInfo, arguments : Expr list) : Expr voption =
+ match property.GetMethod with
+ | null -> ValueNone
+ | getter ->
+ match reflectedDefinitionOf getter, receiver with
+ | ValueSome definition, Some receiver when isStaticallyBound getter ->
+ this.Inline (getter, definition, [ [ receiverFor 0 definition receiver ]; arguments ])
+ | ValueSome definition, None when not (Type.(=) (definition.Type, property.PropertyType)) ->
+ this.Inline (getter, definition, [ arguments ])
+ | _ -> ValueNone
+
+ ///
+ /// The rewrites that look at a node before its parts are normalized, because they replace what the parts are.
+ ///
+ ///
+ ///
+ /// - A quotation that carries its compiled value, which a parameter marked with
+ /// receives, is the
+ /// quotation.
+ /// - The address of a value as the receiver of a property is the value. Only a struct anonymous
+ /// record is read this way, and such a node has to go first:
+ ///
+ /// refuses to rebuild it.
+ /// - A call that passes witnesses is the call without them, or, when the called function or
+ /// member is and has a reflected definition, that definition applied to the witnesses,
+ /// the receiver and the arguments. The witnesses are functions the compiler writes for the operators of a member
+ /// constraint; they are not a part of what the user wrote and are not normalized.
+ ///
+ ///
+ member private this.RewriteBeforeParts (expr : Expr) : Expr voption =
+ match expr with
+ | WithValue (_, _, quotation) -> ValueSome quotation
+ | PropertyGet (Some (AddressOf receiver), property, arguments) ->
+ ValueSome (Expr.PropertyGet (receiver, property, arguments))
+ | CallWithWitnesses (receiver, method, witnessMethod, witnesses, arguments) ->
+ let inlined =
+ match definitionWithWitnesses method witnessMethod with
+ | ValueSome (struct (defined, definition)) when isStaticallyBound method ->
+ let groups = [
+ for witness in witnesses do
+ [ witness ]
+ match receiver with
+ | Some receiver -> [ receiverFor witnesses.Length definition receiver ]
+ | None -> ()
+ yield! argumentGroups method arguments
+ ]
+
+ this.Inline (defined, definition, groups)
+ | _ -> ValueNone
+
+ match inlined, receiver with
+ | ValueSome _, _ -> inlined
+ | ValueNone, _ when failure.IsSome -> ValueNone
+ | ValueNone, Some receiver -> ValueSome (Expr.Call (receiver, method, arguments))
+ | ValueNone, None -> ValueSome (Expr.Call (method, arguments))
+ | _ -> ValueNone
+
+ ///
+ /// One rewriting step at a node whose parts are in normal form, when a rule applies to it.
+ ///
+ ///
+ ///
+ /// - A function written in place and applied in place is its body with the argument at the places
+ /// of the parameter. A function of a tuple that is applied to a tuple is covered by that: its body reads the items
+ /// of the tuple, which the rule for items takes from the tuple.
+ /// - A is its body with the bound value at the places of the variable,
+ /// unless the variable is mutable and the body assigns to it. This covers the bindings the compiler writes itself:
+ /// the copy of a struct before a member of it is read, which is a mutable variable that nothing assigns to when the
+ /// member is not known to leave the struct as it is, the value a looks at, and the parts of
+ /// a tuple pattern.
+ /// - The reading of an item of a tuple that is built in the same place is that item, and the test
+ /// for a case of a union that is built in the same place is a constant.
+ /// - The rules for choices, for the functions of FSharp.Core, for properties of values built in
+ /// place and for reflected definitions are functions and members of their own.
+ ///
+ ///
+ member private this.Rewrite (expr : Expr) : Expr voption =
+ match expr with
+ | Application (Lambda (parameter, body), argument) -> ValueSome (bind shared parameter argument body)
+ | Let (variable, value, body) when not variable.IsMutable || not (isChanged variable body) ->
+ ValueSome (bind shared variable value body)
+ | IfThenElse (condition, whenTrue, whenFalse) -> canonicalChoice condition whenTrue whenFalse
+ | TupleGet (NewTuple items, index) -> ValueSome items[index]
+ | UnionCaseTest (NewUnionCase (case, _), tested) -> ValueSome (Expr.Value (case.Tag = tested.Tag))
+ | Call (receiver, method, arguments) ->
+ match receiver, knownFunctionOf method with
+ | None, ValueSome known ->
+ eliminatePipeOrComposition known arguments
+ |> ValueOption.orElseWith (fun () -> eliminatePairFunction known arguments)
+ |> ValueOption.orElseWith (fun () -> canonicalBooleanCall known arguments)
+ | _ -> this.InlineCall (receiver, method, arguments)
+ | PropertyGet (receiver, property, arguments) ->
+ match receiver, arguments with
+ | Some built, [] ->
+ eliminatePropertyOfBuiltValue built property
+ |> ValueOption.orElseWith (fun () -> this.InlineGetter (receiver, property, arguments))
+ | _ -> this.InlineGetter (receiver, property, arguments)
+ | _ -> ValueNone
+
+ ///
+ /// with its parts in normal form; the same node when they already were.
+ ///
+ ///
+ /// A choice has its condition normalized first. When the condition is a constant then, the branches are left as
+ /// they are: the rewrite that follows takes the branch the constant chooses, and the other branch is code that
+ /// never runs, which may have no normal form at all.
+ ///
+ member private this.NormalizeParts (expr : Expr) : Expr =
+ match expr with
+ | IfThenElse (condition, whenTrue, whenFalse) ->
+ let normalCondition = this.Normalize condition
+
+ let struct (normalWhenTrue, normalWhenFalse) =
+ match normalCondition with
+ | Constant _ -> struct (whenTrue, whenFalse)
+ | _ -> struct (this.Normalize whenTrue, this.Normalize whenFalse)
+
+ if
+ obj.ReferenceEquals (normalCondition, condition)
+ && obj.ReferenceEquals (normalWhenTrue, whenTrue)
+ && obj.ReferenceEquals (normalWhenFalse, whenFalse)
+ then
+ expr
+ else
+ Expr.IfThenElse (normalCondition, normalWhenTrue, normalWhenFalse)
+ | ShapeVar _ -> expr
+ | ShapeLambda (parameter, body) ->
+ let normalBody = this.Normalize body
+
+ if obj.ReferenceEquals (normalBody, body) then
+ expr
+ else
+ Expr.Lambda (parameter, normalBody)
+ | ShapeCombination (shape, parts) ->
+ let normalParts = parts |> List.map this.Normalize
+
+ if areSame parts normalParts then
+ expr
+ else
+ RebuildShapeCombination (shape, normalParts)
+
+ ///
+ /// The normal form of , or any quotation and a
+ /// .
+ ///
+ ///
+ /// A node that stands at several places is normalized at the first of them, and the other places get the same
+ /// normal form: normalizing it again at every place would give every place a node of its own.
+ ///
+ member this.Normalize (expr : Expr) : Expr =
+ if shared.Count > 0 && shared.Contains expr then
+ match normalOfShared.TryGetValue expr with
+ | true, normal -> normal
+ | _ ->
+ let normal = this.NormalizeAnew expr
+
+ if failure.IsNone then
+ normalOfShared.Add (expr, normal)
+
+ // The normal form stands at those places from now on, and it is its own normal form.
+ if shared.Add normal then
+ normalOfShared.Add (normal, normal)
+
+ normal
+ else
+ this.NormalizeAnew expr
+
+ ///
+ /// The normal form of , worked out now, or any quotation and a
+ /// .
+ ///
+ ///
+ ///
+ /// The parts of a node are normalized first, then the node is rewritten, and what a rewrite gives is normalized
+ /// again, in a loop: a quotation that has no normal form would otherwise use up the stack before it uses up
+ /// .
+ ///
+ ///
+ /// When the parts of a node fail and the node is a construct that a query cannot express whatever is inside it,
+ /// the node is the failure. So the outermost such construct is reported, as it is when nothing fails inside it.
+ /// A quotation that is nested too deeply stays the failure whatever stands around it: the error of a construct
+ /// shows the construct, and nothing that deep can be printed with the stack that is left.
+ ///
+ ///
+ member private this.NormalizeAnew (expr : Expr) : Expr =
+ let mutable current = expr
+ let mutable isNormal = false
+
+ while not isNormal && failure.IsNone do
+ if not (RuntimeHelpers.TryEnsureSufficientExecutionStack ()) then
+ failure <- ValueSome nestedTooDeeply
+ isTooDeep <- true
+ else
+ let rewritten =
+ match this.RewriteBeforeParts current with
+ | ValueSome _ as rewritten -> rewritten
+ | ValueNone when failure.IsSome -> ValueNone
+ | ValueNone ->
+ let node = current
+ current <- this.NormalizeParts node
+
+ if failure.IsSome then
+ if not isTooDeep then
+ failure <- neverExpressed node |> ValueOption.orElse failure
+
+ ValueNone
+ else
+ this.Rewrite current
+
+ match rewritten with
+ | ValueSome next when rewrites < MaxRewrites ->
+ rewrites <- rewrites + 1
+ current <- next
+ | ValueSome _ -> failure <- ValueSome tooManyRewrites
+ | ValueNone -> isNormal <- true
+
+ current
+
+/// The function at the head of a chain of applications: what is applied to the first argument.
+let rec private appliedFunction (expr : Expr) : Expr =
+ match expr with
+ | Application (func, _) -> appliedFunction func
+ | _ -> expr
+
+///
+/// The error for a function that is applied to a value of the document and is not written in the quotation.
+///
+///
+/// A function that reads no variable of the quotation is a compiled value that the quotation has captured, however
+/// the quotation gets to it: by its name, or from a property or a field of a captured value. What a call returns is
+/// compiled whatever the call is given, because it is the definition of the called function that cannot be read. Any
+/// other function that reads a variable is chosen or computed for every document.
+///
+let private appliedFunctionValue (application : Expr) : TranslationError =
+ let applied = appliedFunction application
+
+ let compiledFunction =
+ match applied with
+ | Call (_, method, _) -> ValueSome $"The function that '%s{method.Name}' returns"
+ | _ when not (Seq.isEmpty (applied.GetFreeVars ())) -> ValueNone
+ | ValueWithName (_, _, name) -> ValueSome $"The function '%s{name}'"
+ | PropertyGet (_, property, _) -> ValueSome $"The function '%s{property.Name}'"
+ | FieldGet (_, field) -> ValueSome $"The function '%s{field.Name}'"
+ | _ -> ValueSome "A function"
+
+ match compiledFunction with
+ | ValueSome compiledFunction ->
+ application
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.CapturedFunctionValue
+ $"%s{compiledFunction} is a compiled value and is applied to a value of the document. A query cannot apply it, because its definition cannot be read."
+ (ImmutableArray.Create(
+ "Mark the function with [], so that its definition can be read.",
+ "Or write the condition as a quotation and splice it into the query with %."
+ ))
+ | ValueNone ->
+ application
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.Unsupported
+ "A function that is chosen or computed when the query runs is applied to a value of the document. A query can apply only a function whose definition is written in the quotation."
+ ImmutableArray.Empty
+
+///
+/// The error for when it is a construct that a query cannot express, whatever its parts are.
+///
+///
+/// An application of a function that is not written in the quotation is an error only when it depends on a variable
+/// of the quotation, which is how a value of the document is reached. Without one it is a value like any other captured
+/// value, and the stage that computes such values outside the query computes it. The same holds for a call of a
+/// function that calls itself, which the normalization left a call: the compiled function computes such a value.
+///
+/// The functions that the normalization found to call themselves.
+/// A part of a quotation in normal form.
+let private residueAt (recursive : IReadOnlyList) (expr : Expr) : TranslationError voption =
+ let readsDocument () = not (Seq.isEmpty (expr.GetFreeVars ()))
+
+ let callsItself (method : MethodBase | null) =
+ match method with
+ | null -> false
+ | method -> isAmong recursive method
+
+ match expr with
+ | Application _ when readsDocument () -> ValueSome (appliedFunctionValue expr)
+ | Call (_, method, _) when callsItself method && readsDocument () ->
+ ValueSome (recursiveDefinition $"The function '%s{method.Name}'" expr)
+ | PropertyGet (_, property, _) when callsItself property.GetMethod && readsDocument () ->
+ ValueSome (recursiveDefinition $"The property '%s{property.Name}'" expr)
+ | Let _ ->
+ expr
+ |> unsupported "a mutable variable that is assigned to" "Bind every value once, with an immutable let."
+ |> ValueSome
+ | AddressOf _ ->
+ expr
+ |> unsupported "the address of a value" "Read the value itself."
+ |> ValueSome
+ | _ -> neverExpressed expr
+
+///
+/// The first part of a quotation in normal form that a query cannot express, as an error; an outer part before the
+/// parts inside it.
+///
+let rec private findResidue (recursive : IReadOnlyList) (expr : Expr) : TranslationError voption =
+ match expr with
+ | ShapeVar _ -> ValueNone
+ | ShapeLambda (_, body) -> findResidue recursive body
+ | ShapeCombination (_, parts) ->
+ residueAt recursive expr
+ |> ValueOption.orElseWith (fun () -> parts |> List.tryPick (findResidue recursive))
+
+///
+/// The normal form of , or the reason it has none that a query can express.
+///
+///
+///
+/// The normal form computes what computes, as long as the code in it has no side effects: a
+/// value bound by is written at every place it is used, and a bound value or a part of a tuple
+/// or record that nothing reads is dropped with its code. An exception is such an effect: code that is dropped raises
+/// none, and code that is written at the places of its variable runs where they are.
+///
+///
+/// These are errors:
+///
+///
+/// - a call of a function with a reflected definition that calls itself, when the call reads a value
+/// of the document, and a quotation whose simplification does not end,
+/// ;
+/// - a function that is a compiled value and is applied to a value of the document,
+/// ;
+/// - a recursive function, a mutable variable that is assigned to, the address of a value, a sequence of
+/// statements, a loop and exception handling,
+/// .
+///
+///
+/// Of the constructs that stay in the normal form, the outermost one is the error. A recursive function, an assignment,
+/// a sequence of statements, a loop and exception handling are errors whatever is inside them, so the outermost of
+/// these is the error also when the quotation inside it has no normal form at all.
+///
+///
+/// Only what stays is an error. Code that never runs is not in the normal form, so it may hold any of these: the
+/// branch that a constant condition does not choose, also when the constant gets to the condition through a
+/// , a parameter or a reflected definition. What cannot be told that way is a quotation whose
+/// simplification does not end, which stops the normalization where it is met.
+///
+///
+let normalize (expr : Expr) : Result =
+ let normalization = Normalization expr
+ let normal = normalization.Normalize expr
+
+ match
+ normalization.Failure
+ |> ValueOption.orElseWith (fun () -> findResidue normalization.Recursive normal)
+ with
+ | ValueSome failure -> Error failure
+ | ValueNone -> Ok normal
diff --git a/src/Cosmos.Quotations/Parameterizer.fs b/src/Cosmos.Quotations/Parameterizer.fs
new file mode 100644
index 00000000..d712f54b
--- /dev/null
+++ b/src/Cosmos.Quotations/Parameterizer.fs
@@ -0,0 +1,941 @@
+namespace FSharp.Azure.Cosmos.Quotations
+
+open System
+open System.Collections.Immutable
+open System.Reflection
+open System.Text
+open FSharp.Quotations
+open FSharp.Quotations.ExprShape
+open FSharp.Quotations.Patterns
+open FSharp.Reflection
+
+///
+/// What a says about the node of a quotation it stands for.
+///
+type internal ShapeTokenKind =
+ /// A variable that the quotation binds. The number says how many bindings lie between the variable and its own.
+ | Variable = 0
+ /// A function written in the quotation. The subject is the type of its variable.
+ | Lambda = 1
+ /// A delegate written in the quotation. The subject is the type of the delegate, the number counts its variables.
+ | NewDelegate = 2
+ /// A value that is a part of the text of the query. The subject is its type, the detail the value.
+ | Constant = 3
+ ///
+ /// A . The number is its index, the subject its type and the
+ /// detail the name of the captured variable, when it has one.
+ ///
+ | Slot = 4
+ /// The value a type has before anything is assigned. The subject is the type.
+ | DefaultValue = 5
+ /// A union case that is built. The subject is the case.
+ | NewUnionCase = 6
+ /// A tuple that is built. The subject is the type of the tuple, which tells a struct tuple from a reference tuple.
+ | NewTuple = 7
+ /// A record that is built. The subject is the type of the record.
+ | NewRecord = 8
+ /// An array that is built. The subject is the type of its items, the number counts them.
+ | NewArray = 9
+ /// A value seen as a value of another type. The subject is that type.
+ | Coerce = 10
+ /// A choice between two branches.
+ | IfThenElse = 11
+ /// A call of a method. The subject is the method, with its type arguments.
+ | Call = 12
+ /// A reading of a property. The subject is the property.
+ | PropertyGet = 13
+ /// A reading of a field. The subject is the field.
+ | FieldGet = 14
+ /// A call of a constructor. The subject is the constructor.
+ | NewObject = 15
+ /// A reading of an item of a tuple. The number is the index of the item.
+ | TupleGet = 16
+ /// A test for a union case. The subject is the case.
+ | UnionCaseTest = 17
+ /// A test for a type. The subject is the type.
+ | TypeTest = 18
+ /// An application of a function value to an argument.
+ | Application = 19
+
+///
+/// One node of a quotation, as a keeps it: the kind of the
+/// node and what tells it from another node of that kind. The nodes under it follow it in the key.
+///
+///
+/// Two tokens are equal when they say the same about a node. A member, a type and a union case are compared the way
+/// reflection compares them, a constant by its value. A floating-point constant is compared bit by bit, because
+/// 0.0 and -0.0 are equal as numbers and are written differently in the text of a query.
+///
+[]
+type internal ShapeToken = {
+ /// What the token says about its node.
+ Kind : ShapeTokenKind
+ /// The number that the kind gives a meaning to; 0 for a kind that has none.
+ Number : int
+ ///
+ /// The member, type or union case that the kind gives a meaning to; for a kind that
+ /// has none.
+ ///
+ Subject : objnull
+ ///
+ /// The value of a constant or the name of a slot; for a kind that has neither, for the
+ /// constant and for a slot without a name.
+ ///
+ Detail : objnull
+} with
+
+ ///
+ /// Whether and are the same token.
+ ///
+ static member private AreSame (first : ShapeToken, second : ShapeToken) : bool =
+ let sameDetail =
+ match first.Detail, second.Detail with
+ | (:? float as first), (:? float as second) ->
+ BitConverter.DoubleToInt64Bits first = BitConverter.DoubleToInt64Bits second
+ | (:? float32 as first), (:? float32 as second) ->
+ BitConverter.SingleToInt32Bits first = BitConverter.SingleToInt32Bits second
+ | first, second -> Object.Equals (first, second)
+
+ first.Kind = second.Kind
+ && first.Number = second.Number
+ && Object.Equals (first.Subject, second.Subject)
+ && sameDetail
+
+ ///
+ override token.Equals (other : objnull) : bool =
+ match other with
+ | :? ShapeToken as other -> ShapeToken.AreSame (token, other)
+ | _ -> false
+
+ ///
+ override token.GetHashCode () : int = HashCode.Combine (int token.Kind, token.Number, token.Subject, token.Detail)
+
+ /// The token as text, for the message of a failed assertion and for a debugger.
+ override token.ToString () : string =
+ let text = StringBuilder (string token.Kind)
+
+ if
+ token.Number <> 0
+ || token.Kind = ShapeTokenKind.Variable
+ || token.Kind = ShapeTokenKind.Slot
+ then
+ text.Append(' ').Append(token.Number) |> ignore
+
+ match token.Subject with
+ | null -> ()
+ | subject -> text.Append(' ').Append(subject) |> ignore
+
+ match token.Kind, token.Detail with
+ | ShapeTokenKind.Constant, detail -> text.Append(" = ").Append($"%A{detail}") |> ignore
+ | _, null -> ()
+ | _, detail -> text.Append(' ').Append(detail) |> ignore
+
+ text.ToString ()
+
+ interface IEquatable with
+
+ ///
+ member token.Equals (other : ShapeToken) : bool = ShapeToken.AreSame (token, other)
+
+///
+/// The shape of a quotation: everything in it that decides the text of the query, and nothing that only decides the
+/// values of the parameters. Two quotations with equal keys give the same text, so the second one does not have to be
+/// translated again.
+///
+///
+///
+/// A quotation cannot be its own key. It compares its variables by identity, and every evaluation of a quotation
+/// literal creates new variables, so no two evaluations of one literal are equal. It also compares the values it
+/// captured, which are what a key has to leave out.
+///
+///
+/// The key is the list of the nodes of the quotation, every node before the nodes under it. A variable is in it as
+/// the distance to its binding, so the names of variables do not matter. A
+/// is in it as its index, type and name, without its value.
+///
+///
+[]
+type internal ShapeKey = {
+ /// The nodes of the quotation, every node before the nodes under it.
+ Tokens : ImmutableArray
+ /// The hash code of the tokens, computed once when the key is made.
+ Hash : int
+} with
+
+ ///
+ /// Whether and are the same key.
+ ///
+ static member private AreSame (first : ShapeKey, second : ShapeKey) : bool =
+ // Spans, because the key that no code has filled holds an array that is not there, which a span reads as empty.
+ first.Hash = second.Hash
+ && MemoryExtensions.SequenceEqual (first.Tokens.AsSpan (), second.Tokens.AsSpan ())
+
+ ///
+ override key.Equals (other : objnull) : bool =
+ match other with
+ | :? ShapeKey as other -> ShapeKey.AreSame (key, other)
+ | _ -> false
+
+ ///
+ override key.GetHashCode () : int = key.Hash
+
+ /// The tokens as text, one on a line, for the message of a failed assertion and for a debugger.
+ override key.ToString () : string =
+ let text = StringBuilder ()
+
+ for token in key.Tokens.AsSpan () do
+ text.Append(token.ToString ()).Append('\n') |> ignore
+
+ text.ToString ()
+
+ interface IEquatable with
+
+ ///
+ member key.Equals (other : ShapeKey) : bool = ShapeKey.AreSame (key, other)
+
+///
+/// A place in a query for a value that is known before the query runs and is not a part of its text: a value that the
+/// quotation captured from the code around it, or the value of a part of the quotation that reads nothing of the
+/// document. The parameters of the query are made of the values of its slots.
+///
+[]
+type internal Slot = {
+ ///
+ /// The variable that stands for the value in the .
+ /// Its type is the type of the value.
+ ///
+ Variable : Var
+ /// The name of the captured variable that the value comes from; none for a value that is computed.
+ Name : string voption
+}
+
+///
+/// What is known about a quotation without translating it: its
+/// , by which a translation made earlier is found, and the
+/// values to run that translation with.
+///
+[]
+type internal Shape = {
+ /// The shape of the quotation.
+ Key : ShapeKey
+ ///
+ /// The value of every , by the index of the slot. A slot has no
+ /// value when the quotation computes it only in a branch that it does not take this time: the query does not
+ /// take that branch either, so whatever stands in for the value there changes no result.
+ ///
+ Values : ImmutableArray
+}
+
+///
+/// A quotation prepared for translation: what its holds, and the
+/// quotation itself with a variable at every place where a value goes into the query as a parameter.
+///
+[]
+type internal Template = {
+ /// The shape of the quotation.
+ Key : ShapeKey
+ ///
+ /// The value of every slot, by the index of the slot: see
+ /// .
+ ///
+ Values : ImmutableArray
+ ///
+ /// The quotation with the variable of a at every place of a
+ /// value that is a parameter. Every value that is left in it is a part of the text of the query.
+ ///
+ Body : Expr
+ /// The slots, by their index.
+ Slots : ImmutableArray
+}
+
+///
+/// Tells the shape of a quotation from the values it was written with: which values are parameters of the query, what
+/// their values are this time, and what is left as the .
+///
+///
+///
+/// The quotation has to be in the normal form that
+/// gives.
+///
+///
+/// These are the rules.
+///
+///
+/// - A literal is a part of the text of the query, and so of the key: a boolean, a character, a
+/// string, an integer or a floating-point number of a built-in type, a value of an enumeration, the unit value and
+/// . A value of one of these types that is declared with
+/// is such a literal, because the compiler writes it into the
+/// quotation as one.
+/// - A decimal is not a literal, however it is written: the compiler writes 18m as a call
+/// that makes the decimal, so it is computed like any call and is a slot. A parameter keeps its digits; a number in
+/// the text of a query is read as a floating-point number.
+/// - A captured value is a named after its
+/// variable. A value of another type that code put into a quotation it built is a slot without a
+/// name.
+/// - A part that
+///
+/// accepts, the largest one at its place, is computed and is a slot without a name: a property of a captured value, a
+/// call, a module-level value.
+/// - A tuple, record, union case or array that the quotation builds stays what it is, with its parts
+/// handled on their own, even when no part reads the document. The translator has to see that Some limit is an
+/// option with a value, and that a list is written item by item.
+/// - and
+/// say the opposite for their argument. A
+/// constant has to be a value of one of the types of a literal: the key holds it, and equal keys have to mean one
+/// text.
+///
+///
+/// A slot stands for one node of the quotation, at every place the node is at. A value that a
+/// bound is such a node: the normalizer writes the bound node at every place the value is used.
+/// So the value is one slot, and its code runs once, as it does in compiled code, also when the places are inside
+/// two different parts that are computed: in let limit = read () in p.Age > limit + 1 || p.Age < limit + 2
+/// both sums are slots of their own, and read is called once. One node at two places is one slot whatever put it
+/// there: one quotation that is spliced into two places is one slot for both, with one value.
+///
+///
+/// A captured variable that is used at several places is a slot at each of them. The compiler writes every use as a
+/// node of its own, and nothing in those nodes says that they are one variable: two quotations that are spliced
+/// together can each have captured a variable of the same name. Their values must not say it either. Were two such
+/// nodes one slot whenever they hold one value, the key would depend on whether two values happen to be equal this
+/// time, and one query would have two texts. A value that has to be one parameter is bound by a
+/// inside the quotation.
+///
+///
+/// A choice whose condition is known before the query runs computes only the parts of the branch it takes, as compiled
+/// code does: in filter.IsNone || p.Name = filter.Value.Trim () nothing is trimmed when there is no filter. The
+/// parts of the other branch are slots without a value. The key does not depend on which branch is taken.
+///
+///
+[]
+module internal Parameterizer =
+
+ /// The reason a quotation has no shape, on its way out of the walk over the quotation.
+ []
+ type private Refusal (error : TranslationError) =
+ inherit Exception (error.Message)
+
+ /// The reason.
+ member _.Error = error
+
+ ///
+ /// What a function of says about its argument.
+ ///
+ []
+ type private Marker =
+ /// The argument is a part of the text of the query.
+ | AsConstant
+ /// The argument is a parameter of the query.
+ | AsParameter
+
+ ///
+ /// The generic definition of the method that calls.
+ ///
+ let private calledDefinition (call : Expr) : MethodInfo =
+ match call with
+ | Call (None, method, _) -> method.GetGenericMethodDefinition ()
+ | _ -> invalidArg (nameof call) $"The quotation is not a call of a function: %A{call}"
+
+ ///
+ /// .
+ ///
+ let private constantFunction = calledDefinition <@ Query.constant 0 @>
+
+ ///
+ /// .
+ ///
+ let private parameterFunction = calledDefinition <@ Query.parameter 0 @>
+
+ ///
+ /// What says about its argument, when it is a function of
+ /// that says something.
+ ///
+ []
+ let private (|MarkerFunction|_|) (method : MethodInfo) : Marker voption =
+ // The declaring type is compared first: it is one reference comparison, and it fails for nearly every method.
+ if Type.(=) (method.DeclaringType, constantFunction.DeclaringType) then
+ let token = method.MetadataToken
+
+ if token = constantFunction.MetadataToken then
+ ValueSome AsConstant
+ elif token = parameterFunction.MetadataToken then
+ ValueSome AsParameter
+ else
+ ValueNone
+ else
+ ValueNone
+
+ ///
+ /// Whether the compiler could have written into a quotation as a literal of the type
+ /// . Any other value without a name was put there by code that builds quotations, and
+ /// that code has no other way to pass data, so the value is taken for data.
+ ///
+ ///
+ /// The declared type decides, and not the type of the value: a nullable with a value is boxed as that value, and a
+ /// nullable without one is a null. So is a case of a union that is represented by null, as the None of an option
+ /// is. The compiler writes none of them as a value: a case of a union it writes as a case.
+ ///
+ let private isLiteral (value : objnull) (declared : Type) : bool =
+ match value with
+ // The null of a reference type that is no union, and the unit value.
+ | null ->
+ not declared.IsValueType
+ && not (FSharpType.IsUnion (declared, true))
+ | _ ->
+ // An enumeration has the type code of the number under it, and a nullable has the one of an object. There
+ // is no decimal here: the compiler never writes one as a value.
+ match Type.GetTypeCode declared with
+ | TypeCode.Boolean
+ | TypeCode.Char
+ | TypeCode.String
+ | TypeCode.SByte
+ | TypeCode.Byte
+ | TypeCode.Int16
+ | TypeCode.UInt16
+ | TypeCode.Int32
+ | TypeCode.UInt32
+ | TypeCode.Int64
+ | TypeCode.UInt64
+ | TypeCode.Single
+ | TypeCode.Double -> true
+ | _ -> false
+
+ ///
+ /// Whether is the type of F# functions or a class made of it, as the class of every
+ /// compiled function is and as a class that code declares can be.
+ ///
+ let rec private isFunctionClass (valueType : Type | null) : bool =
+ match valueType with
+ | null -> false
+ | valueType ->
+ (valueType.IsGenericType
+ && Type.(=) (valueType.GetGenericTypeDefinition (), typedefof<_ -> _>))
+ || isFunctionClass valueType.BaseType
+
+ ///
+ /// Whether a value of is a function or a delegate.
+ ///
+ let private isFunction (valueType : Type) : bool =
+ // A function and a delegate are objects, so a value type is neither, and nearly every slot is of one or a string.
+ not valueType.IsValueType
+ && (typeof.IsAssignableFrom valueType
+ || isFunctionClass valueType)
+
+ ///
+ /// Whether a part of the type has a function or a delegate as its
+ /// although its type does not say so. Only an object and an interface can hold one
+ /// unseen, so a value of any other type is not looked at.
+ ///
+ let private isHiddenFunction (valueType : Type) (value : objnull) : bool =
+ (Type.(=) (valueType, typeof) || valueType.IsInterface)
+ && (
+ match value with
+ | null -> false
+ | value -> isFunction (value.GetType ())
+ )
+
+ ///
+ /// The error for a function value that would have to be a parameter of the query.
+ ///
+ let private functionValue (part : Expr) : TranslationError =
+ part
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.CapturedFunctionValue
+ "A function that exists only as a compiled value is passed into the query. A query cannot use it, because its definition cannot be read, and a function is not a value that a parameter of a query can hold."
+ (ImmutableArray.Create(
+ "Mark the function with [], so that its definition can be read.",
+ "Or write the function as a quotation and splice it into the query with %."
+ ))
+
+ ///
+ /// The error for an argument of a function of that is not
+ /// known before the query runs.
+ ///
+ let private notKnownBefore (marker : Marker) (call : Expr) : TranslationError =
+ let name =
+ match marker with
+ | AsConstant -> nameof Query.constant
+ | AsParameter -> nameof Query.parameter
+
+ call
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.Unsupported
+ $"The argument of Query.%s{name} has to be known before the query runs, but it reads the document or holds a function written in the quotation."
+ (ImmutableArray.Create $"Pass Query.%s{name} a value that does not depend on the document.")
+
+ ///
+ /// The error for an argument of that cannot
+ /// be written into the text of a query as a literal.
+ ///
+ let private notLiteral (valueType : Type) (call : Expr) : TranslationError =
+ call
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.Unsupported
+ $"The argument of Query.constant has to be a value that a query can hold as a literal: a boolean, a character, a string, an integer or a floating-point number of a built-in type, a value of an enumeration, or the null of a reference type that is no union of F#, as an option is. This one is a %s{valueType.Name}."
+ (ImmutableArray.Create
+ "Use the value without Query.constant: it becomes a parameter of the query, which holds a value of any type.")
+
+ ///
+ /// The error for a variable that nothing in the quotation binds.
+ ///
+ let private unbound (variable : Var) (part : Expr) : TranslationError =
+ part
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.Unsupported
+ $"The quotation reads the variable '%s{variable.Name}', which nothing in it binds, so the variable has no value."
+ (ImmutableArray.Create
+ "Bind the variable with a function around the part that reads it, or put its value into the quotation.")
+
+ ///
+ /// The error for a construct that is in no quotation in normal form except as a part that is not computed.
+ ///
+ let private inexpressible (part : Expr) : TranslationError =
+ part
+ |> TranslationError.ofQuotation
+ TranslationErrorCode.Unsupported
+ "A query cannot express this part of the quotation: it is neither a value known before the query runs nor an operation on the document."
+ ImmutableArray.Empty
+
+ ///
+ /// A slot that the walk has found.
+ ///
+ []
+ type private Found = {
+ /// The part of the quotation that the slot stands for, by which another use of the very same part is told.
+ Part : Expr
+ /// The name of the captured variable; none for a value that is computed.
+ Name : string voption
+ /// The type of the value.
+ Type : Type
+ }
+
+ ///
+ /// One walk over a quotation. It writes the tokens of the key and collects the slots with their values; when
+ /// is set, it also builds the quotation with a variable at the place of every slot.
+ ///
+ ///
+ /// Both results come from one walk, so that the key of a quotation is the same whether or not it is rebuilt.
+ /// Rebuilding is left out when the key is all that is asked for: building a node of a quotation costs microseconds,
+ /// and a quotation whose translation is already known needs no node built.
+ ///
+ []
+ type private Walk (rebuild : bool) =
+
+ let tokens = ImmutableArray.CreateBuilder 32
+ let mutable hash = HashCode ()
+ let values = ImmutableArray.CreateBuilder 4
+ let found = ResizeArray 4
+ let slots = ImmutableArray.CreateBuilder()
+ // The variables that the functions around the current node bind, the innermost one last.
+ let bound = ResizeArray 4
+ // The nodes from the quotation down to the current node. When the walk is left, it tells where it was.
+ let path = ResizeArray 16
+ // The parts that the evaluator has refused. The walk asks about a part and then about the parts under it, and
+ // the evaluator went down those parts to refuse the first one, so it answers for them from here.
+ let refused = Evaluator.Refused ()
+ // What the walk has computed, node by node, so that a node that stands at several places is computed once,
+ // also when the places are inside different parts.
+ let computed = Evaluator.Computed ()
+ // The node that was made a slot last, and the index of that slot: how a choice knows its condition.
+ let mutable latestPart : Expr | null = null
+ let mutable latestValue : objnull voption = ValueNone
+
+ /// Writes a token into the key.
+ let emit (kind : ShapeTokenKind) (number : int) (subject : objnull) (detail : objnull) : unit =
+ let token = { Kind = kind; Number = number; Subject = subject; Detail = detail }
+
+ tokens.Add token
+ hash.Add token
+
+ /// Whether a part can be computed before the query runs.
+ let canCompute (part : Expr) : bool = Evaluator.canEvaluate refused part
+
+ /// The value of a part that reads nothing of the document; leaves the walk when the code of the part raises.
+ let compute (part : Expr) : objnull =
+ match Evaluator.tryEvaluateOnce computed part with
+ | Ok value -> value
+ | Error error -> raise (Refusal error)
+
+ /// The index of the slot that stands for this very part, or -1.
+ let indexOf (part : Expr) : int =
+ let mutable index = found.Count - 1
+
+ while index >= 0
+ && not (obj.ReferenceEquals (found[index].Part, part)) do
+ index <- index - 1
+
+ index
+
+ ///
+ /// The type of the value of a part that becomes a slot or a constant. A function is refused here, before
+ /// anything is computed for it.
+ ///
+ let valueTypeOf (part : Expr) : Type =
+ let valueType = part.Type
+
+ if isFunction valueType then
+ raise (Refusal (functionValue part))
+
+ valueType
+
+ ///
+ /// , the value of a part that becomes a slot or a constant. A function that is passed
+ /// as an object is refused here: the type of the part did not say what it is.
+ ///
+ let noFunction (part : Expr) (valueType : Type) (value : objnull) : objnull =
+ if isHiddenFunction valueType value then
+ raise (Refusal (functionValue part))
+
+ value
+
+ /// The index of a new slot.
+ let add (part : Expr) (valueType : Type) (name : string voption) (value : objnull voption) : int =
+ let index = found.Count
+
+ found.Add { Part = part; Name = name; Type = valueType }
+
+ values.Add value
+
+ if rebuild then
+ let variableName =
+ match name with
+ | ValueSome name -> name
+ | ValueNone -> $"p%d{index}"
+
+ slots.Add { Variable = Var (variableName, valueType); Name = name }
+
+ index
+
+ ///
+ /// The index of the slot of a value that the quotation holds: a captured variable at one of the places it is
+ /// used at, which has a , or a value that code put into a quotation it built, which
+ /// has none. Another place is another slot unless it holds this very part: nothing else says that two parts
+ /// are one variable, and their values must not decide it, or the key would depend on them.
+ ///
+ let held (part : Expr) (name : string voption) (value : objnull) : int =
+ match indexOf part with
+ | -1 ->
+ let valueType = valueTypeOf part
+ add part valueType name (ValueSome (noFunction part valueType value))
+ | index -> index
+
+ ///
+ /// The index of the slot of a part that is computed. The part is computed when says
+ /// that the quotation takes the branch it is in, and once for all the places the very same part is at.
+ ///
+ let computed (live : bool) (part : Expr) : int =
+ match indexOf part with
+ | -1 ->
+ let valueType = valueTypeOf part
+
+ add
+ part
+ valueType
+ ValueNone
+ (if live then
+ ValueSome (noFunction part valueType (compute part))
+ else
+ ValueNone)
+ | index ->
+ // The part was met before in a branch that is not taken, and is met now where its value is needed.
+ if live && values[index].IsNone then
+ values[index] <- ValueSome (noFunction part found[index].Type (compute part))
+
+ index
+
+ /// Writes the token of a slot; the node that stands at the place of the slot from now on.
+ let slot (index : int) (node : Expr) : Expr =
+ let entry = found[index]
+
+ let name : objnull =
+ match entry.Name with
+ | ValueSome name -> name
+ | ValueNone -> null
+
+ emit ShapeTokenKind.Slot index entry.Type name
+ latestPart <- node
+ latestValue <- values[index]
+
+ if rebuild then Expr.Var slots[index].Variable else node
+
+ /// Writes the token of a variable: how many bindings lie between the variable and its own.
+ let variable (bindingOf : Var) (node : Expr) : unit =
+ let mutable index = bound.Count - 1
+
+ while index >= 0
+ && not (obj.ReferenceEquals (bound[index], bindingOf)) do
+ index <- index - 1
+
+ if index < 0 then
+ raise (Refusal (unbound bindingOf node))
+
+ emit ShapeTokenKind.Variable (bound.Count - 1 - index) null null
+
+ ///
+ /// The value of the condition of a choice that has just been walked, when it is known: the condition is a slot
+ /// with a value. A literal is never a condition in normal form.
+ ///
+ let chosen (condition : Expr) : bool voption =
+ if obj.ReferenceEquals (latestPart, condition) then
+ match latestValue with
+ | ValueSome (:? bool as value) -> ValueSome value
+ | _ -> ValueNone
+ else
+ ValueNone
+
+ ///
+ /// Walks and the nodes under it. says whether the quotation
+ /// takes the branch the node is in. The result is the node, or the node rebuilt when a slot is under it and
+ /// the walk rebuilds.
+ ///
+ let rec visit (live : bool) (node : Expr) : Expr =
+ path.Add node
+ let visited = inspect live node
+ path.RemoveAt (path.Count - 1)
+ visited
+
+ /// What a visit does for a node, between entering it and leaving it.
+ and inspect (live : bool) (node : Expr) : Expr =
+ // The kinds of nodes that a query is mostly made of come first: every case that does not match costs a
+ // test, and a node is tested against the cases in their order.
+ match node with
+ | Var bindingOf ->
+ variable bindingOf node
+ node
+ | ValueWithName (value, _, name) -> slot (held node (ValueSome name) value) node
+ | Value (value, valueType) ->
+ if isLiteral value valueType then
+ // A literal that is not null is of no function type. A null is the literal of every class, of a
+ // function type too, and there it is refused as a function is: the query can use neither.
+ let valueType =
+ match value with
+ | null -> valueTypeOf node
+ | _ -> valueType
+
+ emit ShapeTokenKind.Constant 0 valueType value
+ node
+ else
+ slot (held node ValueNone value) node
+ | PropertyGet (receiver, property, arguments) ->
+ if canCompute node then
+ slot (computed live node) node
+ else
+ emit ShapeTokenKind.PropertyGet 0 property null
+ members live node receiver arguments
+ | Call (receiver, method, arguments) ->
+ match method, arguments with
+ | MarkerFunction marker, [ argument ] -> marked live marker node argument
+ | _ when canCompute node -> slot (computed live node) node
+ | _ ->
+ emit ShapeTokenKind.Call 0 method null
+ members live node receiver arguments
+ | IfThenElse (condition, whenTrue, whenFalse) ->
+ if canCompute node then
+ slot (computed live node) node
+ else
+ emit ShapeTokenKind.IfThenElse 0 null null
+ let visitedCondition = visit live condition
+
+ // A branch that the quotation does not take is still walked: the key holds both branches,
+ // whichever is taken this time.
+ let taken = if live then chosen condition else ValueNone
+ let visitedWhenTrue = visit (live && taken <> ValueSome false) whenTrue
+ let visitedWhenFalse = visit (live && taken <> ValueSome true) whenFalse
+
+ if
+ obj.ReferenceEquals (visitedCondition, condition)
+ && obj.ReferenceEquals (visitedWhenTrue, whenTrue)
+ && obj.ReferenceEquals (visitedWhenFalse, whenFalse)
+ then
+ node
+ else
+ Expr.IfThenElse (visitedCondition, visitedWhenTrue, visitedWhenFalse)
+ | Lambda (parameter, body) ->
+ emit ShapeTokenKind.Lambda 0 parameter.Type null
+ bound.Add parameter
+ let visited = visit live body
+ bound.RemoveAt (bound.Count - 1)
+
+ if obj.ReferenceEquals (visited, body) then
+ node
+ else
+ Expr.Lambda (parameter, visited)
+ | NewUnionCase (case, items) ->
+ emit ShapeTokenKind.NewUnionCase 0 case null
+ parts live node items
+ | NewTuple items ->
+ emit ShapeTokenKind.NewTuple 0 node.Type null
+ parts live node items
+ | NewRecord (recordType, items) ->
+ emit ShapeTokenKind.NewRecord 0 recordType null
+ parts live node items
+ | NewArray (itemType, items) ->
+ emit ShapeTokenKind.NewArray items.Length itemType null
+ parts live node items
+ | Coerce (value, targetType) ->
+ emit ShapeTokenKind.Coerce 0 targetType null
+ parts live node [ value ]
+ | DefaultValue _ ->
+ // The default value of a function type is the null of it.
+ emit ShapeTokenKind.DefaultValue 0 (valueTypeOf node) null
+ node
+ | NewDelegate (delegateType, parameters, body) ->
+ emit ShapeTokenKind.NewDelegate parameters.Length delegateType null
+ bound.AddRange parameters
+ let visited = visit live body
+ bound.RemoveRange (bound.Count - parameters.Length, parameters.Length)
+
+ if obj.ReferenceEquals (visited, body) then
+ node
+ else
+ Expr.NewDelegate (delegateType, parameters, visited)
+ // Every kind of node below is computed as a whole when the evaluator can compute it.
+ | _ when canCompute node -> slot (computed live node) node
+ | FieldGet (receiver, field) ->
+ emit ShapeTokenKind.FieldGet 0 field null
+ members live node receiver []
+ | NewObject (constructor, arguments) ->
+ emit ShapeTokenKind.NewObject 0 constructor null
+ parts live node arguments
+ | TupleGet (tuple, index) ->
+ emit ShapeTokenKind.TupleGet index null null
+ parts live node [ tuple ]
+ | UnionCaseTest (union, case) ->
+ emit ShapeTokenKind.UnionCaseTest 0 case null
+ parts live node [ union ]
+ | TypeTest (value, testedType) ->
+ emit ShapeTokenKind.TypeTest 0 testedType null
+ parts live node [ value ]
+ | Application (func, argument) ->
+ emit ShapeTokenKind.Application 0 null null
+ parts live node [ func; argument ]
+ | _ -> raise (Refusal (inexpressible node))
+
+ ///
+ /// Walks the argument of a function of , which says what
+ /// the argument is. The call itself leaves no trace: it only says how its argument is read.
+ ///
+ and marked (live : bool) (marker : Marker) (call : Expr) (argument : Expr) : Expr =
+ match marker, argument with
+ | AsParameter, ValueWithName (value, _, name) -> slot (held argument (ValueSome name) value) call
+ | AsParameter, Value (value, _) -> slot (held argument ValueNone value) call
+ | AsParameter, _ when canCompute argument -> slot (computed live argument) call
+ | AsConstant, _ when canCompute argument ->
+ // The value is a part of the key, so it is computed whichever branch the call is in.
+ let valueType = valueTypeOf argument
+ let value = noFunction argument valueType (compute argument)
+
+ // The key holds the value, and equal keys have to mean one text. Only a literal says by its equality
+ // what is written for it: an object is equal to itself after it has changed, and two objects can be
+ // equal and be written differently.
+ if not (isLiteral value valueType) then
+ raise (Refusal (notLiteral valueType call))
+
+ emit ShapeTokenKind.Constant 0 valueType value
+ latestPart <- call
+ latestValue <- ValueSome value
+
+ if rebuild then Expr.Value (value, valueType) else call
+ | _ -> raise (Refusal (notKnownBefore marker call))
+
+ /// Walks the receiver and the arguments of a member.
+ and members (live : bool) (node : Expr) (receiver : Expr option) (arguments : Expr list) : Expr =
+ match receiver with
+ | Some receiver -> parts live node (receiver :: arguments)
+ | None -> parts live node arguments
+
+ ///
+ /// Walks , the nodes under in their order. The result is the
+ /// node, or the node built anew over the items that the walk rebuilt.
+ ///
+ and parts (live : bool) (node : Expr) (items : Expr list) : Expr =
+ if rebuild then
+ let visited = items |> List.map (visit live)
+
+ if List.forall2 (fun (item : Expr) (after : Expr) -> obj.ReferenceEquals (item, after)) items visited then
+ node
+ else
+ match node with
+ | ShapeCombination (shape, _) -> RebuildShapeCombination (shape, visited)
+ | _ -> node
+ else
+ for item in items do
+ visit live item |> ignore
+
+ node
+
+ ///
+ /// Walks , which the walk has not been used for before; the quotation, rebuilt when the
+ /// walk rebuilds.
+ ///
+ member _.Run (expr : Expr) : Expr = visit true expr
+
+ ///
+ /// , which the walk was left with, with a place in the source: its own, or the place
+ /// of the nearest node around the part it is about that has one. A captured value has none of its own, and a
+ /// node that the normalizer built has the place of a part under it.
+ ///
+ member _.Located (error : TranslationError) : TranslationError =
+ if error.Location.IsSome then
+ error
+ else
+ let mutable index = path.Count - 1
+ let mutable place = ValueNone
+
+ while index >= 0 && place.IsNone do
+ place <- SourceRange.tryOfQuotation path[index]
+ index <- index - 1
+
+ { error with Location = place }
+
+ /// The key of the quotation that was walked.
+ member _.Key : ShapeKey = { Tokens = tokens.ToImmutable (); Hash = hash.ToHashCode () }
+
+ /// The values of the slots of the quotation that was walked.
+ member _.Values : ImmutableArray = values.ToImmutable ()
+
+ /// The slots of the quotation that was walked, when the walk rebuilds.
+ member _.Slots : ImmutableArray = slots.ToImmutable ()
+
+ ///
+ /// The key of , a quotation in normal form, and the values of its slots, or the reason it
+ /// has none. This is all that a quotation needs when its translation is known already.
+ ///
+ ///
+ /// These are errors:
+ ///
+ /// - a part that has to be computed raised an exception,
+ /// ;
+ /// - a function value would have to be a parameter,
+ /// ;
+ /// - the argument of a function of depends
+ /// on the document, the argument of
+ /// is no value that a query can hold as a
+ /// literal, a variable is bound nowhere, or the quotation holds a construct that is neither a value nor an
+ /// operation on the document, .
+ ///
+ ///
+ ///
+ let shapeOf (expr : Expr) : Result =
+ let walk = Walk false
+
+ try
+ walk.Run expr |> ignore
+
+ Ok { Key = walk.Key; Values = walk.Values }
+ with :? Refusal as refusal ->
+ Error (walk.Located refusal.Error)
+
+ ///
+ /// , a quotation in normal form, prepared for translation, or the reason it cannot be:
+ /// the errors of .
+ /// The key and the values are the ones that function gives.
+ ///
+ let templateOf (expr : Expr) : Result =
+ let walk = Walk true
+
+ try
+ let body = walk.Run expr
+
+ Ok { Key = walk.Key; Values = walk.Values; Body = body; Slots = walk.Slots }
+ with :? Refusal as refusal ->
+ Error (walk.Located refusal.Error)
diff --git a/src/Cosmos.Quotations/Query.fs b/src/Cosmos.Quotations/Query.fs
new file mode 100644
index 00000000..6b3688ae
--- /dev/null
+++ b/src/Cosmos.Quotations/Query.fs
@@ -0,0 +1,52 @@
+///
+/// Functions that say, inside a quotation, how a value gets into the query.
+///
+///
+///
+/// A query is its text and the values of its parameters. A value that a quotation writes as a literal, such as
+/// 18 in p.Age > 18, is a part of the text. A value that the quotation takes from the code around it
+/// or computes from such values, such as limit in p.Age > limit, is a parameter: the text stays the
+/// same when the value changes.
+///
+///
+/// The functions here say the opposite for one value. Outside a quotation each of them returns its argument.
+///
+///
+[]
+module FSharp.Azure.Cosmos.Quotations.Query
+
+///
+/// Makes a part of the text of the query, although the quotation does not write it as a
+/// literal: p.Kind = Query.constant kind.
+///
+///
+///
+/// Every different value gives a query with a text of its own, which is translated on its own. So this is for a value
+/// that takes few different values, never for a value that a user of the application enters.
+///
+///
+/// The value has to be known before the query runs: it cannot depend on the document. And it has to be a value that a
+/// query can hold as a literal: a boolean, a character, a string, an integer or a floating-point number of a built-in
+/// type, a value of an enumeration or the of a reference type. A value of any other type can
+/// only be a parameter: a decimal, a date, and a nullable or an option, with a value or without one.
+///
+///
+/// The value, which the quotation takes from the code around it or computes from such values.
+/// .
+let constant (value : 'T) : 'T = value
+
+///
+/// Makes a parameter of the query, although the quotation writes it as a literal:
+/// p.Age > Query.parameter 18.
+///
+///
+///
+/// Two queries that differ only in such a value have the same text, so the second one is not translated again.
+///
+///
+/// The value has to be known before the query runs: it cannot depend on the document.
+///
+///
+/// The value.
+/// .
+let parameter (value : 'T) : 'T = value
diff --git a/src/Cosmos.Quotations/TranslationError.fs b/src/Cosmos.Quotations/TranslationError.fs
new file mode 100644
index 00000000..a658a454
--- /dev/null
+++ b/src/Cosmos.Quotations/TranslationError.fs
@@ -0,0 +1,178 @@
+namespace FSharp.Azure.Cosmos.Quotations
+
+open System.Collections.Immutable
+open FSharp.Quotations
+open FSharp.Quotations.DerivedPatterns
+open FSharp.Quotations.ExprShape
+open FSharp.Quotations.Patterns
+
+open FSharp.Azure.Cosmos
+
+///
+/// The place in a source file where a part of a quotation was written. The compiler records it only when it is run with
+/// --quotations-debug, so a has one only for
+/// code compiled that way.
+///
+[]
+type SourceRange = {
+ /// The path of the source file, as the compiler recorded it.
+ File : string
+ /// The line the part starts at; the first line of a file is 1.
+ StartLine : int
+ /// The column the part starts at; the first column of a line is 0.
+ StartColumn : int
+ /// The line the part ends at.
+ EndLine : int
+ /// The column the part ends at.
+ EndColumn : int
+}
+
+///
+/// The reason a reports.
+///
+[]
+type TranslationErrorCode =
+ /// A construct of F# that a query has no counterpart for, such as a loop, an assignment or a recursive function.
+ | Unsupported
+ ///
+ /// A function that exists only as a compiled value is applied to a value of the document, or is passed into
+ /// the query as a value. A compiled function cannot be read, so it cannot become a part of the query, and it is
+ /// no value that a parameter of a query can hold; a quotation or a function marked with
+ /// can be read.
+ ///
+ | CapturedFunctionValue
+ ///
+ /// The quotation cannot be brought into its simplest form: a function marked with
+ /// calls itself, or the simplification did not
+ /// finish within its limit of steps.
+ ///
+ | NormalizationDiverged
+ ///
+ /// A part of the quotation that reads nothing of the document has to be computed before the query runs, and
+ /// computing it failed: the code in it raised an exception, which is the
+ /// .
+ ///
+ | ClientEvaluationFailed
+
+///
+/// Why a quotation cannot be translated into a query, with the part of the quotation that is the reason.
+///
+///
+/// The type has no equality, because is an
+/// , which is equal to another one only when both wrap
+/// the same array. Compare the .
+///
+[]
+type TranslationError = {
+ /// The reason, for code that handles the error.
+ Code : TranslationErrorCode
+ /// What is wrong, in English.
+ Message : string
+ /// The part of the quotation that is the reason, as text; empty when the error is about the quotation as a whole.
+ Fragment : string
+ ///
+ /// Where the part was written, when the compiler recorded it: see
+ /// .
+ ///
+ Location : SourceRange voption
+ /// What to write instead, in English; empty when there is no general advice.
+ Hints : ImmutableArray
+ /// The exception that code of the quotation raised, when that is the reason.
+ Cause : exn voption
+}
+
+///
+/// Reads the that the compiler attached to a part of a
+/// quotation.
+///
+[]
+module internal SourceRange =
+
+ ///
+ /// The range in one custom attribute of a quotation node. The compiler writes it as the pair of the name
+ /// DebugRange and the tuple of the file, the start line and column and the end line and column.
+ ///
+ []
+ let private (|DebugRange|_|) (attribute : Expr) : SourceRange voption =
+ match attribute with
+ | NewTuple [ String "DebugRange"
+ NewTuple [ String file; Int32 startLine; Int32 startColumn; Int32 endLine; Int32 endColumn ] ] ->
+ ValueSome {
+ File = file
+ StartLine = startLine
+ StartColumn = startColumn
+ EndLine = endLine
+ EndColumn = endColumn
+ }
+ | _ -> ValueNone
+
+ ///
+ /// The range of , or of the first part inside it that has one: a node that a rewrite built
+ /// has none of its own, but the parts it was built from keep theirs.
+ ///
+ let rec tryOfQuotation (expr : Expr) : SourceRange voption =
+ let own =
+ expr.CustomAttributes
+ |> List.tryPick (fun attribute ->
+ match attribute with
+ | DebugRange range -> ValueSome range
+ | _ -> ValueNone
+ )
+
+ match own with
+ | ValueSome _ -> own
+ | ValueNone ->
+ match expr with
+ | ShapeVar _ -> ValueNone
+ | ShapeLambda (_, body) -> tryOfQuotation body
+ | ShapeCombination (_, parts) -> parts |> List.tryPick tryOfQuotation
+
+///
+/// Builds a about a part of a quotation.
+///
+[]
+module internal TranslationError =
+
+ ///
+ /// The error with , and about
+ /// , which gives the error its text and, when the compiler recorded one, its place in
+ /// the source.
+ ///
+ let ofQuotation
+ (code : TranslationErrorCode)
+ (message : string)
+ (hints : ImmutableArray)
+ (fragment : Expr)
+ : TranslationError = {
+ Code = code
+ Message = message
+ Fragment = string fragment
+ Location = SourceRange.tryOfQuotation fragment
+ Hints = hints
+ Cause = ValueNone
+ }
+
+ ///
+ /// The error with , and about
+ /// that the exception is the reason of.
+ ///
+ let ofException
+ (code : TranslationErrorCode)
+ (message : string)
+ (hints : ImmutableArray)
+ (cause : exn)
+ (fragment : Expr)
+ : TranslationError = { ofQuotation code message hints fragment with Cause = ValueSome cause }
+
+ ///
+ /// The error with and about a quotation as a whole: it names no
+ /// part of it.
+ ///
+ let ofWhole (code : TranslationErrorCode) (message : string) : TranslationError = {
+ Code = code
+ Message = message
+ Fragment = ""
+ Location = ValueNone
+ Hints = ImmutableArray.Empty
+ Cause = ValueNone
+ }
diff --git a/tests/Cosmos.Quotations.Tests/EvaluatorTests.fs b/tests/Cosmos.Quotations.Tests/EvaluatorTests.fs
new file mode 100644
index 00000000..59cde783
--- /dev/null
+++ b/tests/Cosmos.Quotations.Tests/EvaluatorTests.fs
@@ -0,0 +1,659 @@
+namespace FSharp.Azure.Cosmos.Quotations.Tests
+
+open System
+open System.Collections.Generic
+open System.Collections.Immutable
+open System.Globalization
+open System.Reflection
+open System.Reflection.Emit
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open FSharp.Quotations
+open FSharp.Quotations.Patterns
+
+open FSharp.Azure.Cosmos.Quotations
+open FSharp.Azure.Cosmos.Tests
+
+///
+/// A value written as code that reads no variable of a quotation: the part of the quotation to compute, and what the
+/// compiled code gives for it.
+///
+type Computation = {
+ /// What kind of code computes the value; the name of a test result starts with it.
+ Name : string
+ /// The code in the normal form of the normalizer, without the function it was written in.
+ Part : Expr
+ /// Fails unless a value is what the compiled code gives.
+ IsExpected : objnull -> unit
+} with
+
+ ///
+ /// The part of a quotation that computes. The compiler passes the code together with its
+ /// quotation, so the expected value is what the code gives when it runs.
+ ///
+ static member Of<'Result when 'Result : equality>
+ (name : string, [] code : Expr 'Result>)
+ : Computation
+ =
+ let code = Quoted.Split code
+
+ {
+ Name = name
+ Part = Computation.PartOf code.Quotation
+ IsExpected =
+ fun value ->
+ Assert.AreEqual<'Result>(
+ code.Compiled (),
+ (value :?> 'Result),
+ "The evaluator should compute what the compiled code computes."
+ )
+
+ // A boxed number can be taken out as an enumeration value, so the cast above does not tell them
+ // apart, but a parameter of a query is written by the type its value has.
+ match value with
+ | null -> ()
+ | value when typeof<'Result>.IsEnum ->
+ Assert.AreEqual(
+ typeof<'Result>,
+ value.GetType (),
+ "A value of an enumeration type should be of that type, not the number under it."
+ )
+ | _ -> ()
+ }
+
+ ///
+ /// The body of the function of the unit value that is, in normal form: a part that reads no
+ /// variable.
+ ///
+ static member PartOf (code : Expr) : Expr =
+ match NormalizerAssert.WantNormal code with
+ | Lambda (_, part) -> part
+ | normal -> invalidArg (nameof code) $"The code should be a function of the unit value: %A{normal}"
+
+///
+/// : the
+/// value of a part of a quotation that reads no variable is what the compiled code of that part gives, and an exception
+/// of that code is an error with the exception as its cause.
+///
+[]
+type EvaluatorTests () =
+
+ static let computations =
+ // Captured by the code below, as a query captures the values it is asked with.
+ let limit = 30
+ let price = 12.5
+ let amount = 19.99m
+ let name = "Bob"
+ // A date is read as the field of a captured record: the compiler refuses to read a member of a captured local
+ // of a value type in a quotation (FS3155).
+ let meeting = {| At = DateTime (2024, 2, 29, 13, 30, 0, DateTimeKind.Utc) |}
+ let nick = Some "bobby"
+ let noNick : string option = None
+ let alias = ValueSome "bo"
+ let pair = (3, "three")
+ let structPair = struct (4, "four")
+ let eight = (1, 2, 3, 4, 5, 6, 7, "eight")
+ let range = { Low = 18; High = 65 }
+ let tags = [ "vip"; "new" ]
+ let filter = OlderThan 18
+ let addOne = fun (number : int) -> number + 1
+ let add = fun (first : int) (second : int) -> first + second
+ let wait = {| For = TimeSpan.FromMinutes 90.0 |}
+ // A nullable is read as the field of a captured record, like the date above.
+ let optional = {| Limit = Nullable 30; Missing = Nullable() |}
+ let flags = BindingFlags.Public ||| BindingFlags.Instance
+
+ seq {
+ // Values and operators
+ Computation.Of ("a captured value", (fun () -> limit))
+ Computation.Of ("a constant", (fun () -> 42))
+ Computation.Of ("the sum of a captured value and a constant", (fun () -> limit + 1))
+ Computation.Of ("arithmetic on integers", (fun () -> (limit - 4) * 3 / 2 % 7))
+ Computation.Of ("the negation of a number", (fun () -> -limit))
+ Computation.Of ("arithmetic on floating-point numbers", (fun () -> price * 2.0 + 0.5))
+ Computation.Of ("arithmetic on decimals", (fun () -> amount * 2m + 0.01m))
+ Computation.Of ("arithmetic on 64-bit integers", (fun () -> int64 limit * 4_000_000_000L))
+ Computation.Of ("a conversion to a floating-point number", (fun () -> float limit / 4.0))
+ Computation.Of ("a conversion to an integer", (fun () -> int price))
+ Computation.Of ("a conversion to a string", (fun () -> string limit))
+ Computation.Of ("the concatenation of strings", (fun () -> name + "!"))
+ Computation.Of ("a comparison", (fun () -> limit > 18))
+ Computation.Of ("an equality of strings", (fun () -> name = "Bob"))
+ Computation.Of ("a negation", (fun () -> not (limit > 18)))
+
+ // Operators that FSharp.Core does not let reflection call
+ Computation.Of ("a subtraction", (fun () -> limit - 4))
+ Computation.Of ("a division", (fun () -> limit / 4))
+ Computation.Of ("a remainder", (fun () -> limit % 4))
+ Computation.Of ("a division of floating-point numbers", (fun () -> price / 4.0))
+ Computation.Of ("a number with a plus sign", (fun () -> +limit))
+ Computation.Of ("the complement of the bits of a number", (fun () -> ~~~limit))
+ Computation.Of ("a bitwise and", (fun () -> limit &&& 12))
+ Computation.Of ("a bitwise or", (fun () -> limit ||| 1))
+ Computation.Of ("a bitwise exclusive or", (fun () -> limit ^^^ 5))
+ Computation.Of ("a shift to the left", (fun () -> limit <<< 2))
+ Computation.Of ("a shift to the right", (fun () -> limit >>> 2))
+ Computation.Of ("a conversion to a byte", (fun () -> byte limit))
+ Computation.Of ("a conversion to a signed byte", (fun () -> sbyte limit))
+ Computation.Of ("a conversion to a 16-bit integer", (fun () -> int16 limit))
+ Computation.Of ("a conversion to an unsigned 16-bit integer", (fun () -> uint16 limit))
+ Computation.Of ("a conversion to a 32-bit integer", (fun () -> int32 price))
+ Computation.Of ("a conversion to an unsigned 32-bit integer", (fun () -> uint32 limit))
+ Computation.Of ("a conversion to a 64-bit integer", (fun () -> int64 price))
+ Computation.Of ("a conversion to an unsigned 64-bit integer", (fun () -> uint64 limit))
+ Computation.Of ("a conversion to a native integer", (fun () -> nativeint limit))
+ Computation.Of ("a conversion to an unsigned native integer", (fun () -> unativeint limit))
+ Computation.Of ("a conversion to a single-precision number", (fun () -> float32 limit))
+ Computation.Of ("a conversion to a decimal", (fun () -> decimal limit))
+ Computation.Of ("a conversion to a character", (fun () -> char (limit + 35)))
+ Computation.Of ("a conversion that cuts the value off", (fun () -> byte (limit * 10)))
+ Computation.Of ("a checked negation", (fun () -> Checked.(~-) limit))
+ Computation.Of ("a checked conversion to a byte", (fun () -> Checked.byte limit))
+ Computation.Of ("a checked conversion to a signed byte", (fun () -> Checked.sbyte limit))
+ Computation.Of ("a checked conversion to a 16-bit integer", (fun () -> Checked.int16 limit))
+ Computation.Of ("a checked conversion to an unsigned 16-bit integer", (fun () -> Checked.uint16 limit))
+ Computation.Of ("a checked conversion to a 32-bit integer", (fun () -> Checked.int32 price))
+ Computation.Of ("a checked conversion to an unsigned 32-bit integer", (fun () -> Checked.uint32 limit))
+ Computation.Of ("a checked conversion to a 64-bit integer", (fun () -> Checked.int64 price))
+ Computation.Of ("a checked conversion to an unsigned 64-bit integer", (fun () -> Checked.uint64 limit))
+ Computation.Of ("a checked conversion to a native integer", (fun () -> Checked.nativeint limit))
+ Computation.Of ("a checked conversion to an unsigned native integer", (fun () -> Checked.unativeint limit))
+ Computation.Of ("a checked conversion to a character", (fun () -> Checked.char (limit + 35)))
+ Computation.Of ("a subtraction of decimals", (fun () -> amount - 0.99m))
+ Computation.Of ("a division of a type with its own operator", (fun () -> wait.For / 2.0))
+ Computation.Of ("the negation of a type with its own operator", (fun () -> -wait.For))
+ Computation.Of ("the number of an enumeration value", (fun () -> int meeting.At.DayOfWeek))
+ Computation.Of ("the 64-bit number of an enumeration value", (fun () -> int64 meeting.At.DayOfWeek))
+ Computation.Of ("the checked number of an enumeration value", (fun () -> Checked.int meeting.At.DayOfWeek))
+ Computation.Of ("the enumeration value of a number", (fun () -> enum(limit - 25)))
+ Computation.Of ("a combination of flags", (fun () -> BindingFlags.Public ||| BindingFlags.Static))
+ Computation.Of ("the flags that two values share", (fun () -> flags &&& BindingFlags.Public))
+ Computation.Of ("the complement of flags", (fun () -> ~~~flags))
+ Computation.Of ("flags without one of them", (fun () -> flags &&& ~~~BindingFlags.Instance))
+
+ // Choices, which compute one branch only
+ Computation.Of ("a choice", (fun () -> if limit > 18 then "adult" else "minor"))
+ Computation.Of (
+ "a conjunction whose second condition would fail",
+ (fun () -> noNick.IsSome && noNick.Value.Length > 0)
+ )
+ Computation.Of (
+ "a disjunction whose second condition would fail",
+ (fun () -> noNick.IsNone || noNick.Value.Length > 0)
+ )
+ Computation.Of ("a conjunction of two conditions that hold", (fun () -> nick.IsSome && nick.Value.Length > 0))
+
+ // Members
+ Computation.Of ("a static field", (fun () -> DateTime.UnixEpoch))
+ Computation.Of ("a static property", (fun () -> CultureInfo.InvariantCulture.Name))
+ Computation.Of ("a module-level value", (fun () -> Conditions.retirementAge))
+ Computation.Of ("a property of a string", (fun () -> name.Length))
+ Computation.Of ("a chain of properties", (fun () -> meeting.At.Date.Year))
+ Computation.Of ("a method with an argument", (fun () -> meeting.At.AddDays 2.0))
+ Computation.Of ("a method of a string", (fun () -> name.ToUpperInvariant().Substring 1))
+ Computation.Of ("a static method", (fun () -> Math.Max (limit, 18)))
+ Computation.Of ("a static method that parses", (fun () -> Guid.Parse "0d8f4b51-7d0d-4c35-9a53-4e0b3a1f6f10"))
+ Computation.Of ("a generic function", (fun () -> List.length tags))
+ Computation.Of ("a function of a module", (fun () -> String.concat "," tags))
+ Computation.Of ("a function without a reflected definition", (fun () -> Conditions.isTeenager Persons.child))
+ Computation.Of ("a function with a reflected definition that calls itself", (fun () -> Conditions.factorial 5))
+ Computation.Of ("a method of a captured record", (fun () -> Persons.retired.IsRetired ()))
+ Computation.Of ("an operator of a type", (fun () -> (meeting.At - DateTime.UnixEpoch).TotalDays))
+ Computation.Of ("a member of an enumeration value", (fun () -> meeting.At.DayOfWeek = DayOfWeek.Thursday))
+
+ // Values that are built
+ Computation.Of ("a constructor", (fun () -> DateTime (2020, 1, 2)))
+ Computation.Of ("a nullable value", (fun () -> Nullable limit))
+ Computation.Of ("a tuple", (fun () -> (limit, name)))
+ Computation.Of ("a struct tuple", (fun () -> struct (limit, name)))
+ Computation.Of ("a record", (fun () -> { Low = limit; High = 65 }))
+ Computation.Of ("an anonymous record", (fun () -> {| Name = name; Limit = limit |}))
+ Computation.Of ("a struct anonymous record", (fun () -> struct {| Name = name; Limit = limit |}))
+ Computation.Of ("a union case with a field", (fun () -> ByName name))
+ Computation.Of ("an option with a value", (fun () -> Some limit))
+ Computation.Of ("an option without a value", (fun () -> (None : int option)))
+ Computation.Of ("a value option with a value", (fun () -> ValueSome name))
+ Computation.Of ("a list", (fun () -> [ limit; 1 ]))
+ Computation.Of ("a default value of a struct", (fun () -> Unchecked.defaultof))
+ Computation.Of ("a default value of a class", (fun () -> Unchecked.defaultof))
+ Computation.Of ("a struct that is made without arguments", (fun () -> DateTime ()))
+ Computation.Of ("a nullable without a value", (fun () -> Nullable()))
+
+ // Values that are taken apart
+ Computation.Of ("the first item of a captured pair", (fun () -> fst pair))
+ Computation.Of ("the second item of a captured pair", (fun () -> snd pair))
+ Computation.Of (
+ "an item of a captured struct tuple",
+ (fun () ->
+ let struct (number, _) = structPair
+ number
+ )
+ )
+ Computation.Of (
+ "the eighth item of a captured tuple",
+ (fun () ->
+ let (_, _, _, _, _, _, _, last) = eight
+ last
+ )
+ )
+ Computation.Of ("a field of a captured record", (fun () -> range.High))
+ Computation.Of (
+ "a match on a captured option with a value",
+ (fun () ->
+ match nick with
+ | Some text -> text
+ | None -> ""
+ )
+ )
+ Computation.Of (
+ "a match on a captured option without a value",
+ (fun () ->
+ match noNick with
+ | Some text -> text
+ | None -> "nobody"
+ )
+ )
+ Computation.Of (
+ "a match on a captured value option",
+ (fun () ->
+ match alias with
+ | ValueSome text -> text
+ | ValueNone -> ""
+ )
+ )
+ Computation.Of (
+ "a match on a captured union",
+ (fun () ->
+ match filter with
+ | ByName text -> text
+ | OlderThan age -> string age
+ )
+ )
+ Computation.Of (
+ "a test of the type of a value",
+ (fun () ->
+ match box name with
+ | :? string as text -> text.Length
+ | _ -> -1
+ )
+ )
+ Computation.Of (
+ "a test of the type of a value of another type",
+ (fun () ->
+ match box limit with
+ | :? string as text -> text.Length
+ | _ -> -1
+ )
+ )
+ Computation.Of ("a cast to an interface", (fun () -> (tags :> seq) |> Seq.length))
+
+ // Members of a nullable, which reflection has nothing to call on: a nullable is boxed as its value or as null
+ Computation.Of ("a nullable with a value that is asked whether it has one", (fun () -> optional.Limit.HasValue))
+ Computation.Of ("a nullable without a value that is asked whether it has one", (fun () -> optional.Missing.HasValue))
+ Computation.Of ("the value of a nullable", (fun () -> optional.Limit.Value))
+ Computation.Of ("the value of a nullable or the default of its type", (fun () -> optional.Limit.GetValueOrDefault ()))
+ Computation.Of (
+ "the default of its type for a nullable without a value",
+ (fun () -> optional.Missing.GetValueOrDefault ())
+ )
+ Computation.Of ("the value of a nullable or a default", (fun () -> optional.Limit.GetValueOrDefault 5))
+ Computation.Of ("a default for a nullable without a value", (fun () -> optional.Missing.GetValueOrDefault 5))
+ Computation.Of ("a nullable with a value as text", (fun () -> optional.Limit.ToString ()))
+ Computation.Of ("a nullable without a value as text", (fun () -> optional.Missing.ToString ()))
+ Computation.Of ("a nullable that is compared with a value", (fun () -> optional.Limit.Equals 30))
+ Computation.Of ("a nullable without a value that is compared with null", (fun () -> optional.Missing.Equals null))
+ Computation.Of ("the hash code of a nullable", (fun () -> optional.Limit.GetHashCode ()))
+ Computation.Of ("the hash code of a nullable without a value", (fun () -> optional.Missing.GetHashCode ()))
+ Computation.Of ("a nullable that is made of a value", (fun () -> Nullable limit))
+ Computation.Of ("a nullable that a function takes", (fun () -> Option.ofNullable optional.Missing))
+
+ // Captured functions
+ Computation.Of ("a captured function that is applied", (fun () -> addOne limit))
+ Computation.Of ("a captured function of two arguments", (fun () -> add limit 1))
+ Computation.Of ("a captured function that is applied to what another gives", (fun () -> addOne (add limit 1)))
+ }
+ |> ImmutableArray.CreateRange
+
+ static let failures =
+ let name = "Bob"
+ let limit = 30
+ let noNick : string option = None
+ let refuse : unit -> int = fun () -> invalidOp "No value."
+ let optional = {| Missing = Nullable() |}
+ let missing : int -> int = Unchecked.defaultof int>
+ let noName : string = Unchecked.defaultof
+
+ seq {
+ struct ("a text that is no number", Computation.PartOf <@ fun () -> Int32.Parse name @>, typeof)
+ struct ("a division by zero", Computation.PartOf <@ fun () -> limit / (limit - 30) @>, typeof)
+ struct ("a checked conversion that does not fit",
+ Computation.PartOf <@ fun () -> Checked.byte (limit * 10) @>,
+ typeof)
+ struct ("a checked negation that does not fit",
+ Computation.PartOf <@ fun () -> Checked.(~-) (Int32.MinValue + limit - 30) @>,
+ typeof)
+ struct ("a member of a value that is null",
+ Computation.PartOf <@ fun () -> noNick.Value.Length @>,
+ typeof)
+ struct ("a captured function that raises",
+ Computation.PartOf <@ fun () -> refuse () + 1 @>,
+ typeof)
+ struct ("the value of a nullable without a value",
+ Computation.PartOf <@ fun () -> optional.Missing.Value @>,
+ typeof)
+ struct ("a function that is null and is applied",
+ Computation.PartOf <@ fun () -> missing limit @>,
+ typeof)
+ // Compiled code computes the arguments before it finds that there is nothing to call.
+ struct ("a method of a value that is null with an argument that raises",
+ Computation.PartOf <@ fun () -> noName.Substring (limit / (limit - 30)) @>,
+ typeof)
+ struct ("an indexed property of a value that is null with an argument that raises",
+ Computation.PartOf <@ fun () -> noName.Chars (limit / (limit - 30)) @>,
+ typeof)
+ struct ("a function that is null and is applied to an argument that raises",
+ Computation.PartOf <@ fun () -> missing (limit / (limit - 30)) @>,
+ typeof)
+ }
+ |> ImmutableArray.CreateRange
+
+ ///
+ /// Every computation as a row of a data-driven test. The name of a row replaces the name of the test method in the
+ /// results, so it says what is asserted.
+ ///
+ static member Computations : IEnumerable> =
+ computations
+ |> Seq.map (fun computation ->
+ TestDataRow(
+ computation,
+ DisplayName = $"%s{computation.Name} must have the value that the compiled code gives"
+ )
+ )
+
+ ///
+ /// Every code that raises an exception, with the type of the exception, as a row of a data-driven test.
+ ///
+ static member Failures : IEnumerable> =
+ failures
+ |> Seq.map (fun (struct (name, part, cause)) ->
+ TestDataRow(
+ struct (part, cause),
+ DisplayName = $"%s{name} must be reported with the exception as the cause"
+ )
+ )
+
+ []
+ member _.``A part that reads no variable must have the value that the compiled code gives`` (computation : Computation) =
+ match Evaluator.tryEvaluate computation.Part with
+ | Ok value -> computation.IsExpected value
+ | Error error -> Assert.Fail $"The part should have a value, but: %s{error.Message}\n%s{error.Fragment}"
+
+ []
+ member _.``Code that raises an exception must be reported with the exception as the cause`` (part : Expr, cause : Type) =
+ let error =
+ Assert.WantError (Evaluator.tryEvaluate part, "The code raises an exception, so the part has no value.")
+
+ Assert.AreEqual(
+ TranslationErrorCode.ClientEvaluationFailed,
+ error.Code,
+ "An exception of the caller's code is a failed evaluation."
+ )
+
+ let raised = Assert.WantValueSome (error.Cause, "The error should carry the exception.")
+ Assert.IsInstanceOfType (raised, cause, "The cause should be the exception of the code, not a wrapper of reflection.")
+ Assert.Contains (cause.Name, error.Message, StringComparison.Ordinal, "The message should name the exception.")
+ Assert.IsNotEmpty (error.Hints, "The error should say what to write instead.")
+ Assert.IsValueSome (error.Location, "The part was written in this file, so it has its place.")
+
+ []
+ member _.``Every operator that FSharp.Core does not let reflection call must have a function that stands in for it`` () =
+ // FSharp.Core marks the methods that raise when reflection calls them. Rethrow and Reraise are among them, and
+ // have nothing to compute.
+ let isMarked (method : MethodInfo) =
+ method.GetCustomAttributesData ()
+ |> Seq.exists (fun attribute -> attribute.AttributeType.Name = "NoDynamicInvocationAttribute")
+
+ let withoutStandIn =
+ seq {
+ typeof.Assembly.GetType("Microsoft.FSharp.Core.Operators", true)
+ typeof.Assembly.GetType("Microsoft.FSharp.Core.Operators+Checked", true)
+ }
+ |> Seq.collect (fun operators ->
+ match operators with
+ | null -> Seq.empty
+ | operators ->
+ operators.GetMethods (BindingFlags.Public ||| BindingFlags.Static)
+ |> Seq.ofArray
+ )
+ |> Seq.filter (fun method ->
+ isMarked method
+ && not (method.Name.EndsWith ("$W", StringComparison.Ordinal))
+ && method.Name <> "Rethrow"
+ && method.Name <> "Reraise"
+ && not (Evaluator.dynamicOperators.ContainsKey method.MetadataToken)
+ )
+ |> Seq.map (fun method -> $"%s{(nonNull method.DeclaringType).Name}.%s{method.Name}")
+ |> Seq.toArray
+
+ Assert.IsEmpty (
+ withoutStandIn,
+ "A version of FSharp.Core that keeps another operator from reflection needs another row in the evaluator."
+ )
+
+ []
+ member _.``Every part that the evaluator computes must be told computable`` () =
+ let refused =
+ seq {
+ for computation in computations do
+ if not (Evaluator.canEvaluate (Evaluator.Refused ()) computation.Part) then
+ computation.Name
+
+ for struct (name, part, _) in failures do
+ if not (Evaluator.canEvaluate (Evaluator.Refused ()) part) then
+ name
+ }
+ |> Seq.toArray
+
+ Assert.IsEmpty (
+ refused,
+ "A part that has a value, or that raises while it is computed, is made only of what the evaluator computes."
+ )
+
+ []
+ member _.``A part that holds a function, a variable or a quotation must be told not computable, and none of it must run`` () =
+ let meter = Meter ()
+ let variable = Expr.Var (Var ("number", typeof))
+
+ // In each of the first two the part before the one that cannot be computed can be: asking must not run it.
+ let withVariable = <@@ [| meter.Read 1; %%variable |] @@>
+ let withFunction = <@@ List.map (fun number -> number + 1) [ meter.Read 1 ] @@>
+ let quotation = Expr.QuoteTyped <@@ 1 @@>
+
+ let refused = Evaluator.Refused ()
+
+ Assert.IsFalse (Evaluator.canEvaluate refused withVariable, "A variable has a value only inside the query.")
+ Assert.IsFalse (Evaluator.canEvaluate refused withFunction, "A function cannot be computed without compiling it.")
+ Assert.IsFalse (Evaluator.canEvaluate refused quotation, "A quotation inside the quotation is not computed.")
+ Assert.AreEqual(0, meter.Readings, "Asking whether a part can be computed should run no code of it.")
+
+ Assert.IsFalse (Evaluator.canEvaluate refused withVariable, "A part that was refused stays refused.")
+ Assert.IsTrue (refused.Holds variable, "The variable is the reason, so it is remembered with the array around it.")
+
+ match withVariable with
+ | NewArray (_, [ first; _ ]) ->
+ Assert.IsTrue (Evaluator.canEvaluate refused first, "The item before the variable can be computed on its own.")
+ | other -> Assert.Fail $"The quotation should be an array of two items: %A{other}"
+
+ []
+ member _.``A value that is bound once must be computed once for all its places in a part`` () =
+ let meter = Meter ()
+
+ let part =
+ Computation.PartOf
+ <@
+ fun () ->
+ let made = meter.Read (obj ())
+ obj.ReferenceEquals (made, made)
+ @>
+
+ Assert.AreEqual(
+ true,
+ Assert.WantOk (Evaluator.tryEvaluate part, "A comparison of values that read no variable has a value."),
+ "Both places hold the node that was bound, so both should get the one object it makes, as in compiled code."
+ )
+
+ Assert.AreEqual(1, meter.Readings, "The code of the bound value should run once.")
+
+ []
+ member _.``A node that was computed must not be computed again for a caller that keeps what was computed`` () =
+ let meter = Meter ()
+
+ // Both parts hold the very same node: a splice puts the node it is given into the quotation.
+ let reading = <@ meter.Read 18 @>
+ let first = NormalizerAssert.WantNormal <@@ %reading + 1 @@>
+ let second = NormalizerAssert.WantNormal <@@ %reading + 2 @@>
+ let computed = Evaluator.Computed ()
+
+ Assert.AreEqual(
+ 19,
+ Assert.WantOk (Evaluator.tryEvaluateOnce computed first, "A sum of values that read no variable has a value."),
+ "The first part should be computed with the value of the node."
+ )
+
+ Assert.AreEqual(
+ 20,
+ Assert.WantOk (Evaluator.tryEvaluateOnce computed second, "A sum of values that read no variable has a value."),
+ "The second part should be computed with the value that the node had for the first one."
+ )
+
+ Assert.AreEqual(1, meter.Readings, "The code of the node should run once for both parts.")
+
+ Assert.WantOk (Evaluator.tryEvaluate second, "A sum of values that read no variable has a value.")
+ |> ignore
+
+ Assert.AreEqual(2, meter.Readings, "A caller that keeps nothing has the part computed anew.")
+
+ []
+ member _.``An array must be built with its items in their order`` () =
+ let limit = 30
+ let part = Computation.PartOf <@ fun () -> [| limit; 1; limit + 1 |] @>
+ let value =
+ Assert.WantOk (Evaluator.tryEvaluate part, "An array of values that read no variable has a value.")
+
+ match value with
+ | :? (int array) as items ->
+ CollectionAssert.AreEqual (
+ [| 30; 1; 31 |],
+ items,
+ "The array should hold the three values in the order they are written."
+ )
+ | other -> Assert.Fail $"The value should be an array of integers, but is %A{other}."
+
+ []
+ member _.``The default value of a struct must be its zeroed value when the struct has a constructor without parameters`` () =
+ // F# declares no struct with a constructor without parameters, as C# can, so the test makes one: its
+ // constructor sets the field to 7.
+ let declared =
+ AssemblyBuilder
+ .DefineDynamicAssembly(AssemblyName "EvaluatorTests.Dynamic", AssemblyBuilderAccess.Run)
+ .DefineDynamicModule("Structs")
+ .DefineType(
+ "Seven",
+ TypeAttributes.Public
+ ||| TypeAttributes.Sealed
+ ||| TypeAttributes.SequentialLayout,
+ typeof
+ )
+
+ let field = declared.DefineField ("Value", typeof, FieldAttributes.Public)
+
+ let code =
+ declared.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes).GetILGenerator()
+
+ code.Emit OpCodes.Ldarg_0
+ code.Emit (OpCodes.Ldc_I4, 7)
+ code.Emit (OpCodes.Stfld, field)
+ code.Emit OpCodes.Ret
+
+ let seven = declared.CreateType ()
+ let valueOf (instance : objnull) = (nonNull (seven.GetField "Value")).GetValue instance
+
+ Assert.AreEqual(
+ box 7,
+ valueOf (Activator.CreateInstance seven),
+ "The constructor of the struct sets the field, so a value that it made is told from the default value."
+ )
+
+ match Evaluator.tryEvaluate (Expr.DefaultValue seven) with
+ | Ok value ->
+ Assert.AreEqual(
+ box 0,
+ valueOf value,
+ "The default value is the zeroed value: no constructor runs for it, as none runs in compiled code."
+ )
+ | Error error -> Assert.Fail $"The default value should be computed, but: %s{error.Message}"
+
+ []
+ member _.``A coercion must give the value only when the value is of the target type`` () =
+ // The compiler writes a coercion only where the cast cannot fail, so these are built through the API.
+ let coerced (value : Expr) (targetType : Type) = Evaluator.tryEvaluate (Expr.Coerce (value, targetType))
+
+ let causeOf (result : Result) =
+ let error = Assert.WantError (result, "A cast that fails in compiled code has no value.")
+
+ Assert.AreEqual(
+ TranslationErrorCode.ClientEvaluationFailed,
+ error.Code,
+ "A failed cast is an exception of the code that is run."
+ )
+
+ Assert.WantValueSome (error.Cause, "The error should carry the exception.")
+
+ Assert.AreEqual(
+ 5,
+ Assert.WantOk (coerced (Expr.Value 5) typeof>, "A number is a value of the nullable of its type."),
+ "The value should be the number."
+ )
+
+ Assert.IsNull (
+ Assert.WantOk (coerced (Expr.Value (null, typeof)) typeof