Skip to content

Commit ee5188a

Browse files
authored
feat(python): support quotation derived patterns (#4928)
1 parent a496573 commit ee5188a

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

src/fable-library-py/fable_library/quotation.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,3 +653,48 @@ def sub(e: Expr) -> Expr:
653653
return e
654654

655655
return sub(expr)
656+
657+
658+
# ===================================================================
659+
# DerivedPatterns
660+
# F# defines these on top of Patterns. AndAlso and OrElse recover the
661+
# shape `&&` and `||` desugar into; SpecificCall matches a call by the
662+
# identity of a template quotation rather than by compiled name.
663+
# ===================================================================
664+
665+
666+
def is_and_also(expr: Expr) -> tuple[Expr, Expr] | None:
667+
"""Match `a && b`, represented as `if a then b else false`."""
668+
if isinstance(expr, ExprIfThenElse) and isinstance(expr.else_expr, ExprValue) and expr.else_expr.value is False:
669+
return (expr.guard, expr.then_expr)
670+
return None
671+
672+
673+
def is_or_else(expr: Expr) -> tuple[Expr, Expr] | None:
674+
"""Match `a || b`, represented as `if a then true else b`."""
675+
if isinstance(expr, ExprIfThenElse) and isinstance(expr.then_expr, ExprValue) and expr.then_expr.value is True:
676+
return (expr.guard, expr.else_expr)
677+
return None
678+
679+
680+
def _template_call(expr: Expr) -> ExprCall | None:
681+
"""Find the call identifying a template quotation under its lambdas."""
682+
current = expr
683+
while isinstance(current, ExprLambda):
684+
current = current.body
685+
return current if isinstance(current, ExprCall) else None
686+
687+
688+
def is_specific_call(template: Expr, expr: Expr) -> tuple[Expr | None, FSharpList[str], FSharpList[Expr]] | None:
689+
"""Match a call by the method identity represented by a template quotation."""
690+
wanted = _template_call(template)
691+
if not isinstance(expr, ExprCall) or wanted is None:
692+
return None
693+
if expr.method != wanted.method or expr.declaring_type != wanted.declaring_type:
694+
return None
695+
696+
instance = None if isinstance(expr.instance, ExprValue) and expr.instance.type == "novalue" else expr.instance
697+
# The middle slot is F#'s generic-argument list. This runtime models types as
698+
# names rather than System.Type, so it is always empty; callers match it with
699+
# a wildcard.
700+
return (instance, of_array(Array([])), of_array(expr.args))

tests/Python/TestQuotation.fs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ open Fable.Tests.Util
44
open Util.Testing
55
open Microsoft.FSharp.Quotations
66
open Microsoft.FSharp.Quotations.Patterns
7+
open Microsoft.FSharp.Quotations.DerivedPatterns
78
open Microsoft.FSharp.Linq.RuntimeHelpers
89

910
type QuotationTestUnion =
@@ -12,6 +13,26 @@ type QuotationTestUnion =
1213

1314
let inline quotTestDouble x = x * 2
1415

16+
// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar
17+
// into, and SpecificCall matches a call by the identity of a template quotation
18+
// rather than by its compiled name. All three previously reported
19+
// "not supported by Fable".
20+
type private DpRec = { Country: string; Balance: float; Id: int }
21+
22+
let rec private dpToSql (e: Expr) : string =
23+
match e with
24+
| Lambda(_, body) -> dpToSql body
25+
| AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")"
26+
| OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")"
27+
| SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")"
28+
| SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")"
29+
| SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")"
30+
// Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the
31+
// bare name on the other targets, and this test is about DerivedPatterns.
32+
| PropertyGet _ -> "col"
33+
| Value _ -> "?"
34+
| _ -> "<unsupported>"
35+
1536
[<Fact>]
1637
let ``test Simple integer value quotation`` () =
1738
let q = <@ 42 @>
@@ -283,3 +304,35 @@ let ``test Call on an instance whose value is null keeps a Some instance`` () =
283304

284305
if not (hasSomeInstancePropertyGet q) then
285306
failwith "Expected a PropertyGet with a Some instance, even though its value is null"
307+
308+
[<Fact>]
309+
let ``test DerivedPatterns AndAlso, OrElse and SpecificCall work`` () =
310+
dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)"
311+
312+
dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @>
313+
|> equal "((col = ?) AND (col > ?))"
314+
315+
dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @>
316+
|> equal "((col < ?) OR (col > ?))"
317+
318+
[<Fact>]
319+
let ``test a captured local is a Value, not a Var`` () =
320+
let captured = "SE"
321+
322+
let describe (e: Expr) =
323+
match e with
324+
| Lambda(_, body) ->
325+
match body with
326+
| Value(v, _) -> "Value:" + unbox<string> v
327+
| Var v -> "Var:" + v.Name
328+
| _ -> "other"
329+
| _ -> "not a lambda"
330+
331+
describe <@ fun (_: int) -> captured @> |> equal "Value:SE"
332+
describe <@ fun (_: int) -> "SE" @> |> equal "Value:SE"
333+
334+
// Checks that a bound variable remains a Var, rather than being spliced in.
335+
(match <@ fun (x: string) -> x @> with
336+
| Lambda(_, Var _) -> "Var"
337+
| _ -> "?")
338+
|> equal "Var"

0 commit comments

Comments
 (0)