Skip to content

Commit fae5093

Browse files
ThoriumMangelMaxime
authored andcommitted
feat(all): Quotations: DerivedPatterns, and captured locals as Value nodes (#4919)
1 parent d329a82 commit fae5093

12 files changed

Lines changed: 524 additions & 75 deletions

File tree

src/Fable.Transforms/FableTransforms.fs

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -255,40 +255,56 @@ let noSideEffectBeforeIdent identName expr =
255255

256256
findIdentOrSideEffect expr && not sideEffect
257257

258+
/// A binding referenced from inside a quotation must not be inlined. `visit`
259+
/// deliberately does not rewrite quoted expressions, so the substitution would
260+
/// never reach that reference, and removing the binding would leave the captured
261+
/// value dangling.
262+
let isReferencedInsideQuote identName body =
263+
body
264+
|> deepExists (
265+
function
266+
| Quote(quotedExpr, _, _) -> countReferencesUntil 1 identName quotedExpr >= 1
267+
| _ -> false
268+
)
269+
258270
let canInlineArg (com: Compiler) identName value body =
259-
match value with
260-
| Value((Null _ | UnitConstant | TypeInfo _ | BoolConstant _ | NumberConstant _ | CharConstant _), _) -> true
261-
| Value(StringConstant s, _) ->
262-
match com.Options.Language with
263-
| Python ->
264-
// Only inline short strings if they're referenced at most once,
265-
// to avoid duplicating the literal in generated code (which can cause
266-
// issues like property access on string literals in Python)
267-
s.Length < 100 && countReferencesUntil 2 identName body <= 1
268-
| _ -> s.Length < 100
269-
| _ ->
270-
let refCount = countReferencesUntil 2 identName body
271-
272-
// Don't inline values that create new mutable state (e.g. ResizeArray(), mutable arrays)
273-
// into closures: even though creation is side-effect-free, inlining into a closure
274-
// called multiple times would create a new instance per call instead of sharing the
275-
// single captured instance
276-
let createsMutableState =
277-
match value with
278-
| Value(NewArray(_, _, kind), _) ->
279-
match kind with
280-
| MutableArray
281-
| ResizeArray -> true
282-
| ImmutableArray -> false
283-
| _ -> false
284-
285-
(refCount <= 1
286-
&& not (canHaveSideEffects com value)
287-
&& not (createsMutableState && isIdentCaptured identName body))
288-
// If it can have side effects, make sure is at least referenced once so the expression is not erased
289-
|| (refCount = 1
290-
&& noSideEffectBeforeIdent identName body
291-
&& not (isIdentCaptured identName body))
271+
if isReferencedInsideQuote identName body then
272+
false
273+
else
274+
275+
match value with
276+
| Value((Null _ | UnitConstant | TypeInfo _ | BoolConstant _ | NumberConstant _ | CharConstant _), _) -> true
277+
| Value(StringConstant s, _) ->
278+
match com.Options.Language with
279+
| Python ->
280+
// Only inline short strings if they're referenced at most once,
281+
// to avoid duplicating the literal in generated code (which can cause
282+
// issues like property access on string literals in Python)
283+
s.Length < 100 && countReferencesUntil 2 identName body <= 1
284+
| _ -> s.Length < 100
285+
| _ ->
286+
let refCount = countReferencesUntil 2 identName body
287+
288+
// Don't inline values that create new mutable state (e.g. ResizeArray(), mutable arrays)
289+
// into closures: even though creation is side-effect-free, inlining into a closure
290+
// called multiple times would create a new instance per call instead of sharing the
291+
// single captured instance
292+
let createsMutableState =
293+
match value with
294+
| Value(NewArray(_, _, kind), _) ->
295+
match kind with
296+
| MutableArray
297+
| ResizeArray -> true
298+
| ImmutableArray -> false
299+
| _ -> false
300+
301+
(refCount <= 1
302+
&& not (canHaveSideEffects com value)
303+
&& not (createsMutableState && isIdentCaptured identName body))
304+
// If it can have side effects, make sure is at least referenced once so the expression is not erased
305+
|| (refCount = 1
306+
&& noSideEffectBeforeIdent identName body
307+
&& not (isIdentCaptured identName body))
292308

293309
/// Returns arity of lambda (or lambda option) types
294310
let (|Arity|) typ =

src/Fable.Transforms/QuotationEmitter.fs

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@ open Replacements.Util
1010
/// to construct a quotation AST. The input is the Fable.Expr captured
1111
/// inside a Quote node; the output is a Fable.Expr that calls the
1212
/// quotation runtime library to build the AST at runtime.
13-
let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
13+
let rec private emitQuotedExprIn (com: Compiler) (bound: Set<string>) (expr: Expr) : Expr =
1414
match expr with
15-
| Value(kind, r) -> emitQuotedValue com kind r
15+
| Value(kind, r) -> emitQuotedValue com bound kind r
16+
17+
| IdentExpr ident when not (Set.contains ident.Name bound) ->
18+
// Free in the quotation, so it is a local captured from the enclosing
19+
// scope rather than a quotation variable. .NET splices the captured
20+
// *value* in as a Value node; emitting a Var here instead left consumers
21+
// with a name and no value to bind -- which is precisely what a query
22+
// translator needs in order to turn a captured local into a parameter.
23+
Helper.LibCall(com, "quotation", "mkValue", Any, [ IdentExpr ident; makeStrConst (typeToString ident.Type) ])
1624

1725
| IdentExpr ident ->
18-
// Reference to a variable already introduced by a lambda/let in the quotation.
19-
// Emit: quotation.mkVar(Var)
20-
// We need a var reference. Create a var and then wrap it.
26+
// Bound by a lambda or let inside the quotation: a genuine Var.
2127
let varExpr =
2228
Helper.LibCall(
2329
com,
@@ -47,14 +53,14 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
4753
]
4854
)
4955

50-
let bodyExpr = emitQuotedExpr com body
56+
let bodyExpr = emitQuotedExprIn com (Set.add arg.Name bound) body
5157
Helper.LibCall(com, "quotation", "mkLambda", Any, [ varExpr; bodyExpr ])
5258

5359
| Delegate(args, body, _name, _tags) ->
5460
// Multi-arg delegate: nest as curried lambdas
55-
let rec nestLambdas args body =
61+
let rec nestLambdas bound args body =
5662
match args with
57-
| [] -> emitQuotedExpr com body
63+
| [] -> emitQuotedExprIn com bound body
5864
| (arg: Ident) :: rest ->
5965
let varExpr =
6066
Helper.LibCall(
@@ -69,10 +75,10 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
6975
]
7076
)
7177

72-
let innerBody = nestLambdas rest body
78+
let innerBody = nestLambdas (Set.add arg.Name bound) rest body
7379
Helper.LibCall(com, "quotation", "mkLambda", Any, [ varExpr; innerBody ])
7480

75-
nestLambdas args body
81+
nestLambdas bound args body
7682

7783
| Let(ident, value, body) ->
7884
let varExpr =
@@ -88,33 +94,33 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
8894
]
8995
)
9096

91-
let valueExpr = emitQuotedExpr com value
92-
let bodyExpr = emitQuotedExpr com body
97+
let valueExpr = emitQuotedExprIn com bound value
98+
let bodyExpr = emitQuotedExprIn com (Set.add ident.Name bound) body
9399

94100
Helper.LibCall(com, "quotation", "mkLet", Any, [ varExpr; valueExpr; bodyExpr ])
95101

96102
| IfThenElse(guardExpr, thenExpr, elseExpr, _r) ->
97-
let guard = emitQuotedExpr com guardExpr
98-
let thenE = emitQuotedExpr com thenExpr
99-
let elseE = emitQuotedExpr com elseExpr
103+
let guard = emitQuotedExprIn com bound guardExpr
104+
let thenE = emitQuotedExprIn com bound thenExpr
105+
let elseE = emitQuotedExprIn com bound elseExpr
100106
Helper.LibCall(com, "quotation", "mkIfThenElse", Any, [ guard; thenE; elseE ])
101107

102108
| CurriedApply(applied, args, _typ, _r) ->
103109
// Emit nested applications: Application(Application(f, a1), a2)
104-
let appliedExpr = emitQuotedExpr com applied
110+
let appliedExpr = emitQuotedExprIn com bound applied
105111

106112
args
107113
|> List.fold
108114
(fun acc arg ->
109-
let argExpr = emitQuotedExpr com arg
115+
let argExpr = emitQuotedExprIn com bound arg
110116
Helper.LibCall(com, "quotation", "mkApplication", Any, [ acc; argExpr ])
111117
)
112118
appliedExpr
113119

114120
| Call(callee, info, _typ, _r) ->
115121
let instanceExpr =
116122
match info.ThisArg with
117-
| Some thisArg -> emitQuotedExpr com thisArg
123+
| Some thisArg -> emitQuotedExprIn com bound thisArg
118124
// Static/operator call: no instance.
119125
| None -> mkNoInstanceExpr com
120126

@@ -135,16 +141,16 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
135141
else
136142
let methodExpr = makeStrConst methodName
137143
let declTypeExpr = makeStrConst declaringType
138-
let argExprs = mkExprArray com (info.Args |> List.map (emitQuotedExpr com))
144+
let argExprs = mkExprArray com (info.Args |> List.map (emitQuotedExprIn com bound))
139145
Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs; declTypeExpr ])
140146

141147
| Sequential exprs ->
142148
match exprs with
143-
| [] -> emitQuotedExpr com (Value(UnitConstant, None))
144-
| [ single ] -> emitQuotedExpr com single
149+
| [] -> emitQuotedExprIn com bound (Value(UnitConstant, None))
150+
| [ single ] -> emitQuotedExprIn com bound single
145151
| first :: rest ->
146-
let restExpr = emitQuotedExpr com (Sequential rest)
147-
let firstExpr = emitQuotedExpr com first
152+
let restExpr = emitQuotedExprIn com bound (Sequential rest)
153+
let firstExpr = emitQuotedExprIn com bound first
148154
Helper.LibCall(com, "quotation", "mkSequential", Any, [ firstExpr; restExpr ])
149155

150156
| Operation(kind, _tags, _typ, _r) ->
@@ -195,13 +201,13 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
195201
let methodExpr = makeStrConst opName
196202
let instanceExpr = mkNoInstanceExpr com
197203

198-
let argExprs = mkExprArray com (args |> List.map (emitQuotedExpr com))
204+
let argExprs = mkExprArray com (args |> List.map (emitQuotedExprIn com bound))
199205

200206
// Operators have no declaring type; pass an empty string.
201207
Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs; makeStrConst "" ])
202208

203209
| Get(expr, kind, _typ, _r) ->
204-
let target = emitQuotedExpr com expr
210+
let target = emitQuotedExprIn com bound expr
205211

206212
match kind with
207213
| TupleIndex index -> Helper.LibCall(com, "quotation", "mkTupleGet", Any, [ target; makeIntConst index ])
@@ -227,8 +233,8 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
227233
Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ])
228234

229235
| Set(expr, kind, _typ, value, _r) ->
230-
let target = emitQuotedExpr com expr
231-
let valueExpr = emitQuotedExpr com value
236+
let target = emitQuotedExprIn com bound expr
237+
let valueExpr = emitQuotedExprIn com bound value
232238

233239
match kind with
234240
| ValueSet ->
@@ -242,7 +248,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
242248

243249
| TypeCast(innerExpr, _typ) ->
244250
// Coerce/cast: just emit the inner expression for now
245-
emitQuotedExpr com innerExpr
251+
emitQuotedExprIn com bound innerExpr
246252

247253
| DecisionTree(decisionExpr, targets) ->
248254
// Inline each DecisionTreeSuccess leaf into its target body via nested Lets, turning
@@ -261,18 +267,18 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
261267
| e -> e
262268
)
263269

264-
emitQuotedExpr com inlined
270+
emitQuotedExprIn com bound inlined
265271

266272
| DecisionTreeSuccess(idx, boundValues, _typ) ->
267273
match boundValues with
268-
| [] -> emitQuotedExpr com (Value(UnitConstant, None))
269-
| [ single ] -> emitQuotedExpr com single
274+
| [] -> emitQuotedExprIn com bound (Value(UnitConstant, None))
275+
| [ single ] -> emitQuotedExprIn com bound single
270276
| _ ->
271277
let msg = "Unsupported quotation node: DecisionTreeSuccess"
272278
Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ])
273279

274280
| Test(testExpr, kind, _r) ->
275-
let target = emitQuotedExpr com testExpr
281+
let target = emitQuotedExprIn com bound testExpr
276282

277283
// Note: real .NET quotations use dedicated UnionCaseTest/TypeTest node kinds here, not
278284
// Call — this emitter renders all of these as synthetic Calls instead (e.g. "get_IsCons",
@@ -282,7 +288,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr =
282288
| UnionCaseTest tag ->
283289
// Represent as: (unionTag target) = tag
284290
let tagExpr = Helper.LibCall(com, "quotation", "mkUnionTag", Any, [ target ])
285-
let tagConst = emitQuotedExpr com (makeIntConst tag)
291+
let tagConst = emitQuotedExprIn com bound (makeIntConst tag)
286292

287293
Helper.LibCall(
288294
com,
@@ -364,7 +370,7 @@ and private mkNoInstanceExpr (com: Compiler) : Expr =
364370
// so isCall/isFieldGet don't conflate the two.
365371
mkNullExpr com "novalue"
366372

367-
and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocation option) : Expr =
373+
and private emitQuotedValue (com: Compiler) (bound: Set<string>) (kind: ValueKind) (_r: SourceLocation option) : Expr =
368374
match kind with
369375
| BoolConstant b -> Helper.LibCall(com, "quotation", "mkValue", Any, [ makeBoolConst b; makeStrConst "bool" ])
370376

@@ -388,7 +394,8 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio
388394
Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(CharConstant c, None); makeStrConst "char" ])
389395

390396
| NewTuple(values, _isStruct) ->
391-
let emittedValues = mkExprArray com (values |> List.map (emitQuotedExpr com))
397+
let emittedValues =
398+
mkExprArray com (values |> List.map (emitQuotedExprIn com bound))
392399

393400
Helper.LibCall(com, "quotation", "mkNewTuple", Any, [ emittedValues ])
394401

@@ -398,7 +405,8 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio
398405
| Some ent -> ent.FullName
399406
| None -> entRef.FullName
400407

401-
let emittedValues = mkExprArray com (values |> List.map (emitQuotedExpr com))
408+
let emittedValues =
409+
mkExprArray com (values |> List.map (emitQuotedExprIn com bound))
402410

403411
if com.Options.Language = Rust then
404412
// Rust needs the case name to build a real UnionCaseInfo; other targets keep (name, tag, fields).
@@ -429,7 +437,8 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio
429437
| Some ent -> ent.FSharpFields |> List.map (fun f -> makeStrConst f.Name) |> makeArray Any
430438
| None -> makeArray Any []
431439

432-
let emittedValues = mkExprArray com (values |> List.map (emitQuotedExpr com))
440+
let emittedValues =
441+
mkExprArray com (values |> List.map (emitQuotedExprIn com bound))
433442

434443
if com.Options.Language = Rust then
435444
// Rust needs the record type name; other targets keep (fieldNames, values).
@@ -449,7 +458,7 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio
449458

450459
match value with
451460
| Some v ->
452-
let emitted = mkExprArray com [ emitQuotedExpr com v ]
461+
let emitted = mkExprArray com [ emitQuotedExprIn com bound v ]
453462

454463
Helper.LibCall(
455464
com,
@@ -472,15 +481,15 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio
472481
| NewOption(value, _typ, _isStruct) ->
473482
match value with
474483
| Some v ->
475-
let emitted = emitQuotedExpr com v
484+
let emitted = emitQuotedExprIn com bound v
476485
Helper.LibCall(com, "quotation", "mkValue", Any, [ emitted; makeStrConst "option" ])
477486
| None -> mkNullExpr com "option"
478487

479488
| NewList(headAndTail, _typ) ->
480489
match headAndTail with
481490
| Some(head, tail) ->
482-
let headExpr = emitQuotedExpr com head
483-
let tailExpr = emitQuotedExpr com tail
491+
let headExpr = emitQuotedExprIn com bound head
492+
let tailExpr = emitQuotedExprIn com bound tail
484493
Helper.LibCall(com, "quotation", "mkNewList", Any, [ headExpr; tailExpr ])
485494
| None -> mkNullExpr com "list"
486495

@@ -549,3 +558,7 @@ and private mkExprArray (com: Compiler) (elements: Expr list) : Expr =
549558
match elements with
550559
| [] when com.Options.Language = Rust -> Helper.LibCall(com, "quotation", "emptyExprArray", Any, [])
551560
| _ -> makeArray Any elements
561+
562+
/// Entry point. Nothing is bound at the top of a quotation, so any identifier
563+
/// still free by the time it is reached is a captured local.
564+
let emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = emitQuotedExprIn com Set.empty expr

0 commit comments

Comments
 (0)