Skip to content

feat(quotations): bring a quotation into its normal form - #58

Open
xperiandri wants to merge 3 commits into
mainfrom
feat/quotations-normalizer
Open

xperiandri wants to merge 3 commits into
mainfrom
feat/quotations-normalizer

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

The first step of phase 1 of ADR 0001 (#34): the first stage of the translator of F# quotations into Cosmos DB queries. It needs nothing from the open pull requests of phase 0, so the branch is built on main and holds only its own three commits.

The problem. One condition can be written in F# in many ways:

<@ fun p -> p.Age > 18 @>
<@ fun p -> p.Age |> isOlderThan 18 @>      // isOlderThan is marked [<ReflectedDefinition>]
<@ fun p -> let age = p.Age in age > 18 @>
<@ fun p -> (%olderThan18) p @>              // another quotation, spliced in
<@ isOlderThan 18 @>                         // not written as a function at all

They mean the same and have to become the same query. If every later stage (the one that turns captured values into parameters, the one that writes the query text) had to know every way of writing, each would know them a little differently.

What this adds. Normalizer.normalize : Expr -> Result<Expr, TranslationError> rewrites a quotation into its normal form: the one form that every way of writing a condition has. Only operators, members of the document and calls whose code cannot be read are left in it. All five quotations above become the first one.

Written Becomes
x |> f, f <| x, (a, b) ||> f, f <|| (a, b) and the pipes of three values f x, f a b
f >> g, g << f fun x -> g (f x), with the parameter name of f when f is written as a function
(fun x -> body) value, let x = value in body body with value at the places of x: the very same node at every place, so that the next stage can tell that the places are one value and computes it once, as the code does
a call of a function, method, property or extension member marked [<ReflectedDefinition>] its definition with the arguments written in
fst (a, b), { Low = a; High = b }.Low, {| A = a |}.A, match Some a with Some x -> … a
flag = true, flag <> false, not (not flag), if flag then true else false, flag && true flag
if true then a else b, false && b a, false. The branch that the constant does not choose is not read at all: code that never runs needs no normal form
what the compiler writes itself: the value a match looks at, the parts of a tuple pattern, the copy of a struct before a member of it is read, the hidden arguments of + and other operators with a member constraint gone

What it reports instead of rewriting. A TranslationError with a code, an English message, hints on what to write instead, the offending part of the quotation as text and, when the code is compiled with --quotations-debug, the place in the source:

Quotation Code
a function that exists only as compiled code is applied to a value of the document: let isVip = fun p -> … in <@ fun p -> isVip p @> CapturedFunctionValue, with the hints to mark the function [<ReflectedDefinition>] or to splice a quotation
a call of a function marked [<ReflectedDefinition>] that calls itself, directly or through others, when the call reads a value of the document NormalizationDiverged, naming the function or the property
the rewriting does not end NormalizationDiverged after 10 000 steps, or earlier when the stack of the thread runs low
a mutable variable that is assigned to, an assignment, a sequence of statements, a loop, try, let rec, the address of a value Unsupported, naming the construct

Of the constructs that stay in the normal form, the outermost one is reported. An assignment, a sequence of statements, a loop, try and let rec are errors whatever is inside them, so the outermost of these is reported also when the quotation inside it has no normal form at all: a loop whose condition calls a function that calls itself is reported as the loop. (Found by the review: the function was reported, because the parts of a node are rewritten before the node is looked at.)

Only what stays is an error. A call of a function that calls itself is not replaced by its definition, which would never end, so it stays a call, and what becomes of it is decided when the whole quotation is normalized:

  • when it reads a value of the document, it is the error of the table;
  • when it reads none, p.Age > factorial 5, it is a value to compute outside the query, as a call of any compiled function is;
  • when it stands in code that never runs, it is gone by then. So the branch that a constant does not choose may hold one however the constant gets to the condition: written there, bound by a let, passed to a function or to a function with a definition, isActiveOrCounted true p.

(Found by the review: such a call was an error at the moment it was met, so it was one also in a branch that a constant passed as an argument does not choose, while the same branch behind a constant written in the condition was not read at all.)

New projects

  • src/Cosmos.Quotations is the assembly FSharp.Azure.Cosmos.Quotations. It holds the public error contract (TranslationError, TranslationErrorCode, SourceRange) and the internal normalizer. No package is made of it until the stages produce a query end to end (IsPackable=false).
  • tests/Cosmos.Quotations.Tests holds its unit tests; none needs the emulator.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

A new assembly that is not packed yet; the packages are unchanged.

Checklist

  • Build and tests pass locally
  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have added necessary documentation (if appropriate)

Further comments

  • Tests: 302, in three classes.
    • NormalizerSpellingTests: 84 pairs of one way to write a function and the plain way to write the same function. Three things are checked for every pair: both have one normal form; the normal form is returned as the very same node when it is normalized again; and it computes what the compiled code computes on sample values. The compiled code and the quotation come from one lambda: the compiler passes both to a parameter marked [<ReflectedDefinition(true)>]. Unquote evaluates the quotation.
    • NormalizerTests: what a pair cannot state. What must stay as it is (a captured boolean, a module-level value, also one that is a function of the unit value, a call without a definition, a member that a derived class can override), what must be gone (the hidden arguments of operators, the address of a struct record, the copies of structs), how one variable that is bound at two nested places is treated, under a function and under a delegate, and that a value which is bound once stays one node at all its places: after a second let, after a variable inside it is replaced, and after a later rewrite changes it. A value that a function with a definition binds is one node in a call and another node in another call of the function (found by the review: the normal form of a definition was kept for the next call, so two calls shared the node of a value that each of them computes for itself; now every call after the first normalizes a copy of the definition). And that a call of an inline member which is built through the API without its witnesses stays a call.
    • NormalizerErrorTests: every error, with its code, what its message names and its place in the source. And which of several errors is reported: the outermost construct that a query cannot express, and the binding that is assigned to when an outer binding has the same variable (found by the review: the outer one was reported, which nothing assigns to). For a function that calls itself: that its call is shown as the quotation has it, that the first of two is reported, that a property is named as a property, and that a call which reads nothing of the document stays as it is written.
  • Can the tests fail? 46 one-line defects were planted in the normalizer, one at a time, and reverted: arguments applied in the other order, the wrong item of a tuple, an inverted test, a constant choosing the other branch, a rule switched off, a check removed, a bound value that is not kept one node. Every one of them fails at least one test. The rule that leaves the branch of a constant condition unread came with the review of feat(quotations): compute the parts of a quotation that read no variable #59; without it the 9 tests of its three pairs fail.
  • Cost, measured in a Release build on a machine under other load, so orders of magnitude: a condition of four comparisons that is already in normal form takes about 5 µs, and one with a few pipes, bindings or functions with definitions 15 to 25 µs. For comparison (test(benchmarks): measure what a quotation query costs on the client #56): evaluating the quotation literal itself costs 24 to 75 µs, the cache key of a query about 1 µs, a request to the emulator 2 ms. Most of the time goes into FSharp.Core, see below. What follows for the next stage: the cache key should be computed from the quotation as it is written, so that a query that was seen before is not normalized again. That is a decision for the pull request of that stage.
  • Departures from the ADR text (§7 of the design document in docs: add ADR 0001 for the FSharp.Azure.Cosmos.Quotations architecture #34):
    • Rounds. The ADR runs every pass over the whole quotation, up to 32 rounds. Here one walk rewrites a node until no rule applies to it, children first. The limits are 10 000 rewriting steps and the stack of the thread; a function that calls itself is found exactly, when its definition meets a call of itself, instead of followed until a limit. With rounds, a function that calls itself twice would double the quotation every round.
    • Substitution. The ADR uses Expr.Substitute. It is used where it is safe, which is nearly always. One variable can be bound at two places, one inside the other, because a quotation that is spliced twice arrives as the same nodes, variables included; there Expr.Substitute replaces the variable inside the inner binding too, which is wrong, so those cases are substituted by a function of this assembly. So is a body that holds a value which was bound once and is used at several places (review of feat(quotations): tell the values of a quotation from its shape #60). Such a value has to stay one node, and Expr.Substitute builds every node of the body anew: with two lets, writing the second value into the body made two nodes of the first, and the next stage would have computed it twice. The nodes that stand at several places are remembered for one normalization; one of them is rewritten and normalized once for all its places. A quotation without such a value pays nothing for it, one with two lets about a seventh more.
    • Struct receivers. The address of a struct anonymous record is removed at its node before the parts of the node are looked at, not in a pass of its own.
    • Copies of structs. For invoice.Note.IsSome, where Note is a voption property of a class, the compiler binds the value to a mutable variable that nothing assigns to. Such a variable is written into its uses like an immutable one; without that, a document that is a class could not be queried by a voption property.
    • Not inlined although marked: a module-level value, which F# computes once, and a member that a derived class can override, whose definition in the base class says nothing about the override.
    • Inline functions. The ADR rejects them with a hint. An inline function that is marked [<ReflectedDefinition>] is inlined here: the compiler keeps a second definition of it that takes its operators as arguments. One that is not marked stays a call, and the stage that translates calls will report it. For an inline member of a type, static or not, the compiler keeps that definition for the member itself and not for a second form of it; it takes the operators first all the same, then the receiver, then the arguments (found by the review: such a call raised ArgumentException out of the normalizer, because the receiver was given to the definition in the place of the operator). A definition is applied only to arguments of the types it takes, so a call that is built through the API without the operators stays a call.
    • Later steps, because they need the table of members that have a translation: giving a parameter to a function-typed argument (etaExpand), delegates, sprintf and interpolated strings, and never inlining a member that has a translation of its own.
    • The text of the offending part is how F# prints a quotation (Call (None, op_GreaterThan, …)). Printing it as F# source is a later step; the contract does not change with it.
  • Found on the way:
    • FSharp.Core reads a reflected definition from the assembly on every Expr.TryGetReflectedDefinition call: 16 µs for a method that has one, 1 µs for one that has none. The normalizer keeps the answers for the process.
    • Building or rebuilding one access to an instance member through the public API (Expr.PropertyGet, ExprShape.RebuildShapeCombination) costs 4 µs: FSharp.Core asks reflection whether the declaring type is a union, every time. Expr.Substitute builds nodes without that check and takes 0.2 µs, which is the other reason it is used where it is safe.
    • The compiler refuses [<ReflectedDefinition>] on an instance member of a struct (FS1220), so there are no such definitions to inline.
    • MSTest 4.2.3 unpacks a TestDataRow<struct (Expr * string)> into the two parameters of a plain test method. The ADR had this as an open check.
  • Verification: dotnet build FSharp.Azure.Cosmos.slnx in Debug and Release with the warnings main has and none in the two new projects, also with -p:WarnOn=3390; the tests pass in both configurations; Fantomas clean; the strict documentation build passes; every cref of the new library files names a member that a documentation file declares; each of the three commits builds, and the last one passes the tests.
  • No changelog entry: nothing is packed.

🤖 Generated with Claude Code

FSharp.Azure.Cosmos.Quotations is going to translate F# quotations into
Azure Cosmos DB queries (ADR 0001). The translator is built stage by
stage, so no package is made of the project until the stages produce a
query end to end.

A stage that cannot translate a quotation says why with a
TranslationError: a code for the calling program, a message and hints
for the person who reads it, and the offending part of the quotation as
text. When the code was compiled with --quotations-debug, the error also
carries the place in the source that the compiler recorded for that
part; a part that a rewrite built has no place of its own, so the place
of the first part inside it is taken.

The error has no equality: its hints are an ImmutableArray, which equals
another one only when both wrap the same array.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 11, 2026 14:55
xperiandri added a commit that referenced this pull request Oct 11, 2026
#58 is the first stage of the translator: it brings a quotation into
its normal form. The record states where the implementation departs
from the design and why: one walk instead of rounds, with recursion
found exactly and two limits; substitution with care for a variable
that is bound at two nested places, which Expr.Substitute gets wrong;
the mutable copy of a struct that the compiler binds; what stays
although it has a reflected definition; and the passes that wait for
the registry.

It also records what the stage costs and where the time goes in
FSharp.Core, with the proposal that follows for the plan cache, and how
the tests are built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The extensive quotation transformer covers substitution, reflected-definition inlining, recursion limits, and compiler-generated forms that warrant final human validation.

0 open findings

What changed in this PR

Introduces the first quotation-translation stage: normalization into a canonical form with structured translation errors.

Changes:

  • Adds the internal quotation normalizer and public error contract.
  • Adds extensive emulator-free normalization tests.
  • Registers the new projects and updates repository guidance.
File Description
.github/​copilot-instructions.md Documents the new projects.
FSharp.Azure.Cosmos.slnf Adds projects to the solution filter.
FSharp.Azure.Cosmos.slnx Adds projects to the solution.
src/​Cosmos.Quotations/​AssemblyInfo.fs Defines assembly metadata.
src/​Cosmos.Quotations/​FSharp.Azure.Cosmos.Quotations.fsproj Configures the non-packable library.
src/​Cosmos.Quotations/​InternalsVisibleTo.fs Exposes internals to tests.
src/​Cosmos.Quotations/​Normalizer.fs Implements quotation normalization.
src/​Cosmos.Quotations/​TranslationError.fs Defines translation errors and source ranges.
tests/​Cosmos.Quotations.Tests/​FSharp.Azure.Cosmos.Quotations.Tests.fsproj Configures the test project.
tests/​Cosmos.Quotations.Tests/​Model.fs Supplies test quotation models.
tests/​Cosmos.Quotations.Tests/​NormalizerErrorTests.fs Tests normalization failures.
tests/​Cosmos.Quotations.Tests/​NormalizerSpellingTests.fs Tests equivalent quotation spellings.
tests/​Cosmos.Quotations.Tests/​NormalizerTests.fs Tests normalization invariants and edge cases.
tests/​Cosmos.Quotations.Tests/​QuotationAssert.fs Adds quotation assertion utilities.
tests/​Cosmos.Quotations.Tests/​TestCategories.fs Defines the normalizer test category.
tests/​Cosmos.Quotations.Tests/​testconfig.json Enables parallel test execution.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Instance inline members with reflected definitions are incorrectly left unnormalized.

1 open finding

🧠 Review effort: Balanced

Comment thread src/Cosmos.Quotations/Normalizer.fs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The normalizer can misclassify unsupported constructs and mishandle shadowed mutable variables.

2 open findings
1 resolved since last review

🧠 Review effort: Balanced

Comment thread src/Cosmos.Quotations/Normalizer.fs
Comment thread src/Cosmos.Quotations/Normalizer.fs Outdated
xperiandri added a commit that referenced this pull request Oct 11, 2026
What the reviews of #58 to #60 on the evening of 2026-10-11 changed:

- the normalizer inlines an inline member of a type, whose definition
  the compiler keeps for the member itself and not for its form with
  witnesses, and applies a definition only to arguments it takes;
- the evaluator computes a node once, for one part and for a caller
  that keeps what was computed;
- the parameterizer tells a function by a class made of the type of
  functions too, and Query.constant takes only a value that a literal
  can be.

The counts of tests and of planted defects follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feat/quotations-normalizer branch from e998031 to 2ed58b2 Compare October 11, 2026 21:37
xperiandri added a commit that referenced this pull request Oct 11, 2026
The normalizer reports the outermost construct that a query cannot
express also when the rewriting fails inside it, and an inner binding
of a variable hides an assignment from the outer one (#58). The layout
reads the obsolete setting that ignores null, which both serializers
still follow (#61). The counts of tests and planted defects follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri requested a balanced review from Copilot October 11, 2026 21:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cached reflected definitions can incorrectly share let-bound values across separate call sites.

1 open finding
2 resolved since last review

🧠 Review effort: Balanced

Comment thread src/Cosmos.Quotations/Normalizer.fs Outdated
@xperiandri
xperiandri force-pushed the feat/quotations-normalizer branch from 2ed58b2 to cc18c2c Compare October 11, 2026 22:42
xperiandri added a commit that referenced this pull request Oct 11, 2026
Every call of a function with a reflected definition gets nodes of its
own (#58). The type that a value is declared with decides whether it
is a literal, so a nullable and an option are none (#60). In the
layout an override has no condition of the declaration it overrides, a
property of a record that is not public is not written, and a value
whose converter writes null itself is not described (#61). The counts
of tests and planted defects follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri requested a balanced review from Copilot October 11, 2026 22:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Repeated splices can be incorrectly conflated by identity, and some captured function applications receive the wrong error classification.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment thread src/Cosmos.Quotations/Normalizer.fs Outdated
@xperiandri
xperiandri force-pushed the feat/quotations-normalizer branch from cc18c2c to 9ac526f Compare October 12, 2026 00:20
@xperiandri
xperiandri requested a balanced review from Copilot October 12, 2026 00:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Binding and reflected-call arguments are applied too late, causing valid dead branches to be normalized and rejected.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment thread src/Cosmos.Quotations/Normalizer.fs
xperiandri and others added 2 commits October 12, 2026 03:12
One condition can be written in many ways: with pipes, composition,
let bindings, functions marked with ReflectedDefinition and spliced
quotations. The stages after this one should not have to know every
spelling, so the normalizer rewrites them away. What is left holds
operators, members of the document and calls whose code cannot be read.

Rewritten away:

- |>, <|, ||>, |||>, <||, <|||, >> and <<, recognised by the methods of
  FSharp.Core and never by name;
- a function that is written and applied in place, and a let whose
  variable is never assigned to. That includes what the compiler binds
  itself: the value a match looks at, the parts of a tuple pattern and
  the copy of a struct, which is a mutable variable when the compiler
  cannot tell that the called member leaves the struct alone;
- a call of a function, method, property getter or extension member
  with a reflected definition, also an inline one that takes its
  operators as witnesses, a generic one and a member of a base class.
  A member that a derived class can override stays a call, and so does
  a module-level value, which is computed once;
- the reading of an item, field or case of a tuple, record, anonymous
  record or union that is built in the same place, and fst, snd and
  Item1..Item7, which become the reading of an item;
- a comparison of a boolean with true or false, a choice between true
  and false, a condition that is a constant and a double negation. The
  branch that a constant condition does not choose is not read at all:
  code that never runs needs no normal form. A captured boolean is not
  a constant: it is a parameter of the query;
- the witnesses of a call, the address of a struct anonymous record and
  the compiled value that an auto-quoted argument carries.

Reported as a TranslationError instead:

- a call of a function with a reflected definition that calls itself,
  directly or through others, when the call reads a value of the
  document;
- a quotation whose rewriting does not end, after 10 000 steps or when
  the stack of the thread runs low, whichever comes first;
- a compiled function that is applied to a value of the document. An
  application to captured values only is left for the stage that
  computes values outside the query;
- a mutable variable that is assigned to, an assignment, the address of
  a value, a sequence of statements, a loop, exception handling and a
  recursive function.

Of the constructs that stay in the normal form, the outermost one is
reported. An assignment, a sequence of statements, a loop, exception
handling and a recursive function are errors whatever is inside them,
so the outermost of these is reported also when the quotation inside
it has no normal form at all.

Only what stays is an error. A call of a function that calls itself is
not replaced by its definition, which would never end: it stays a
call, and so does the call that led to it. What becomes of such a call
is decided when the whole quotation is normalized. One that reads a
value of the document is the error. One that reads none is a value to
compute outside the query, as a call of any compiled function is. And
one in code that never runs is gone by then, so the branch that a
constant does not choose may hold one however the constant gets to the
condition: bound by a let, passed to a function written in place or to
a function with a definition.

The compiler keeps the definition of an inline function of a module
for the form of the function that takes the witnesses, and the
definition of an inline member of a type for the member itself; both
take the witnesses first, then the receiver, then the arguments. A
definition is applied only to arguments of the types it takes: a call
that is built through the API can pass an inline member no witnesses,
and such a call stays a call.

Reflected definitions and the argument groups of their methods are kept
for the process: FSharp.Core unpickles a definition on every lookup,
which took 16 microseconds here, and searches the resources of the
assembly for a method that has none, which took one. Substitution is
left to FSharp.Core where nothing inside the body needs care, because
it builds nodes without validating them, while rebuilding one member
access through the public API cost 4 microseconds of reflection.

Two things need care. One variable can be bound at several places,
since a quotation that is spliced twice arrives as the same nodes, so a
binding that hides the variable or would capture a variable of the
value is respected, also when the question is whether a mutable
variable is assigned to. And a value that is bound once and used at several
places has to be one node at all of them: the code computes it once,
and the stage that computes values outside the query tells by the node
that the places are one value. FSharp.Core builds every node of a body
anew, which makes two nodes of such a value as soon as something else
is written into the body that holds it. So a body with such a node is
substituted node by node, and the node is rewritten and normalized once
for all its places. The places are those of one call: the definition of
a function is one tree for the process, so every call after the first
normalizes a copy of it, and a value that the function binds is not one
node across calls, which each compute it for themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new test project, FSharp.Azure.Cosmos.Quotations.Tests, with unit
tests that need no emulator.

Most tests are pairs: one way to write a function next to the plain way
to write the same function. For every pair three things are checked:
both ways have one normal form, the normal form is returned as the same
node when it is normalized again, and it computes what the compiled
code computes on sample values. The compiled code and the quotation come
from one lambda, which the compiler passes both ways to a parameter
marked with ReflectedDefinition(true), and Unquote evaluates the
quotation.

The other tests state what a pair cannot: what must stay as it is, what
must be gone, how one variable that is bound at two nested places is
treated, that a value which is bound once stays one node at all its
places, what every error says and which of several errors is reported.

The test project is compiled with --quotations-debug, so the errors
carry the place in the source; a quotation built through the API covers
the case without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The reflection-driven rewrite engine has a broad semantic surface, and one project-guideline issue remains.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment on lines +198 to +202
let private lookUpReflectedDefinition =
Func<MethodBase, Expr voption>(fun method ->
Expr.TryGetReflectedDefinition method
|> ValueOption.ofOption
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed. The guidance is about unwrapping: an option that an API hands over is unwrapped with Option.defaultValue directly, and not turned into a value option first only to call ValueOption.defaultValue. Here nothing is unwrapped. The answer is kept, in a cache that every later lookup reads, and a value that is kept is a value option by the same guidance. So one conversion at the border is needed, and ValueOption.ofOption is that conversion; a match that maps Some to ValueSome and None to ValueNone is the same function written out.

I leave the thread open for the maintainer, whose rule it is.

xperiandri added a commit that referenced this pull request Oct 12, 2026
A call of a function that calls itself stays a call and is an error
only when it is left in the normal form and reads the document, so
code that never runs may hold one however the constant gets to its
condition (#58). The layout is asked about a member together with the
type it is read from, because a quotation names an inherited property
as its declaring type has it, and a union with a case that is null is
left out where a null is left out (#61).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants