-
Notifications
You must be signed in to change notification settings - Fork 326
Expand file tree
/
Copy pathFable2Python.Bases.fs
More file actions
406 lines (362 loc) · 18 KB
/
Copy pathFable2Python.Bases.fs
File metadata and controls
406 lines (362 loc) · 18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
/// Utilities for Python ABC base classes and protocol support.
/// Handles interface-to-ABC mapping, dunder generation, and abstract class stubs.
module Fable.Transforms.Python.Bases
open Fable
open Fable.AST
open Fable.Transforms
open Fable.Transforms.Python.AST
open Fable.Transforms.Python.Types
open Fable.Transforms.Python.Util
open Fable.Transforms.Python.Annotation
/// Maps F# interface names to Python ABC base class names.
/// Returns None if the interface should not be mapped to an ABC.
/// Note: F# nested modules use + not . in full names (e.g., Fable.Core.Py+Mapping+IMapping`2)
let getAbcClassesForInterface (name: string) (fullName: string) : string list option =
match name with
// .NET interfaces - these have method contracts that guarantee methods exist
| "IDisposable" -> Some [ "DisposableBase" ]
| "IEnumerator_1" -> Some [ "EnumeratorBase"; "DisposableBase" ]
| "IEnumerable_1" -> Some [ "EnumerableBase" ]
// Py.* marker interfaces - these assume methods exist (opt-in by F# developer)
| "ContextManager" -> Some [ "DisposableBase" ]
| "Stringable" -> Some [ "StringableBase" ]
| "Equatable" -> Some [ "EquatableBase" ]
| "Comparable" -> Some [ "ComparableBase" ]
| "Hashable" -> Some [ "HashableBase" ]
| "Sized" -> Some [ "SizedBase" ]
| "Iterable" -> Some [ "EnumerableBase" ]
| "Iterator" -> Some [ "EnumeratorBase" ]
// Py.Mapping protocol interfaces (Fable.Core.Py.MappingModule.*)
// These map to collections.abc classes directly
| "IMapping_2" when fullName.Contains "Fable.Core.Py.MappingModule" -> Some [ "Mapping" ]
| "IMutableMapping_2" when fullName.Contains "Fable.Core.Py.MappingModule" -> Some [ "MutableMapping" ]
// Py.Set protocol interfaces (Fable.Core.Py.SetModule.*)
// These map to collections.abc classes directly
| "ISet_1" when fullName.Contains "Fable.Core.Py.SetModule" -> Some [ "Set" ]
| "IMutableSet_1" when fullName.Contains "Fable.Core.Py.SetModule" -> Some [ "MutableSet" ]
| _ -> None
/// ABC classes that accept type parameters (generic)
let genericAbcClasses =
Set.ofList
[
"EnumeratorBase"
"EnumerableBase"
// collections.abc classes (generic)
"Mapping"
"MutableMapping"
"Set"
"MutableSet"
]
/// ABC classes from collections.abc (not from bases module)
let collectionsAbcClasses =
Set.ofList [ "Mapping"; "MutableMapping"; "Set"; "MutableSet" ]
/// Interfaces that are allowed to contribute base classes
let allowedInterfaces =
[
// .NET interfaces - these have method contracts that guarantee methods exist
// The base classes delegate to these known methods
"IDisposable" // Dispose() -> DisposableBase (__enter__, __exit__)
"IEnumerator_1" // MoveNext(), get_Current() -> EnumeratorBase (__iter__, __next__)
"IEnumerable_1" // GetEnumerator() -> EnumerableBase (__iter__)
// Py.* marker interfaces - these assume methods exist (opt-in by F# developer)
// The base classes delegate to methods that should be implemented
"ContextManager" // assumes Dispose() -> DisposableBase
"Stringable" // assumes ToString() -> StringableBase (__str__, __repr__)
"Equatable" // assumes Equals() -> EquatableBase (__eq__, __ne__)
"Comparable" // assumes CompareTo() -> ComparableBase (__lt__, __le__, __gt__, __ge__)
"Hashable" // assumes GetHashCode() -> HashableBase (__hash__)
"Sized" // assumes Count -> SizedBase (__len__)
"Iterable" // assumes GetEnumerator() -> EnumerableBase (__iter__)
"Iterator" // assumes MoveNext(), get_Current() -> EnumeratorBase (__iter__, __next__)
// Py.Mapping.* protocol interfaces - map to collections.abc.Mapping/MutableMapping
// Compiler generates dunders (__getitem__, __contains__, __len__, __iter__)
"IMapping_2"
"IMutableMapping_2"
// Py.Set.* protocol interfaces - map to collections.abc.Set/MutableSet
// Compiler generates dunders (__contains__, __len__, __iter__)
"ISet_1"
"IMutableSet_1"
]
/// Creates an ABC base class expression with optional generic type arguments.
/// Handles importing from collections.abc or bases module as appropriate.
let makeAbcBaseExpr (com: IPythonCompiler) ctx (abcName: string) (genArgs: Fable.Type list) : Expression =
// Determine import source: collections.abc or bases module
let isCollectionsAbc = collectionsAbcClasses.Contains abcName
let baseExpr =
if isCollectionsAbc then
com.GetImportExpr(ctx, "collections.abc", abcName)
else
libValue com ctx "bases" abcName
// Only apply type args to generic ABC classes
if List.isEmpty genArgs || not (genericAbcClasses.Contains abcName) then
baseExpr
else
let typeArgs =
genArgs
|> List.map (fun genArg ->
let arg, _ = typeAnnotation com ctx None genArg
arg
)
Expression.subscript (baseExpr, Expression.tuple typeArgs)
/// Generate Python protocol dunder methods for Py.Mapping and Py.Set interfaces.
/// When a class implements IMapping, IMutableMapping, ISet, or IMutableSet,
/// we generate the corresponding Python dunders that call the attached interface methods.
let generatePythonProtocolDunders (com: IPythonCompiler) ctx (classEnt: Fable.Entity) : Statement list =
let self = Expression.name "self"
// Helper to create: self.methodName(args)
let selfCall methodName args =
Expression.call (Expression.attribute (self, Identifier methodName, Load), args)
// Helper to create: return self.methodName(args)
let returnSelfCall methodName args =
Statement.return' (selfCall methodName args)
// Check which Python protocol interfaces are implemented
// Note: F# modules compile with "Module" suffix (e.g., Fable.Core.Py.MappingModule.IMapping`2)
let hasIMapping =
classEnt.AllInterfaces
|> Seq.exists (fun int -> int.Entity.FullName = "Fable.Core.Py.MappingModule.IMapping`2")
let hasIMutableMapping =
classEnt.AllInterfaces
|> Seq.exists (fun int -> int.Entity.FullName = "Fable.Core.Py.MappingModule.IMutableMapping`2")
let hasISet =
classEnt.AllInterfaces
|> Seq.exists (fun int -> int.Entity.FullName = "Fable.Core.Py.SetModule.ISet`1")
let hasIMutableSet =
classEnt.AllInterfaces
|> Seq.exists (fun int -> int.Entity.FullName = "Fable.Core.Py.SetModule.IMutableSet`1")
// Generate IMapping dunders: __getitem__, __contains__, __len__, __iter__
// Note: Method names use Python naming convention (lowercase with underscores)
let mappingDunders =
if hasIMapping || hasIMutableMapping then
[
// def __getitem__(self, key): return self.get_item(key)
Statement.functionDef (
Identifier "__getitem__",
Arguments.arguments [ Arg.arg "self"; Arg.arg "key" ],
body = [ returnSelfCall "get_item" [ Expression.name "key" ] ]
)
// def __contains__(self, key): return self.ContainsKey(key)
Statement.functionDef (
Identifier "__contains__",
Arguments.arguments [ Arg.arg "self"; Arg.arg "key" ],
body = [ returnSelfCall "ContainsKey" [ Expression.name "key" ] ]
)
// def __len__(self): return self.Count
Statement.functionDef (
Identifier "__len__",
Arguments.arguments [ Arg.arg "self" ],
body = [ Statement.return' (Expression.attribute (self, Identifier "Count", Load)) ]
)
// def __iter__(self):
// for kv in to_iterator(self.GetEnumerator()):
// yield kv[0] # kv is a tuple (key, value)
let toIterator = com.GetImportExpr(ctx, "fable_library.util", "to_iterator")
let kvVar = Expression.name "kv"
Statement.functionDef (
Identifier "__iter__",
Arguments.arguments [ Arg.arg "self" ],
body =
[
Statement.for' (
kvVar,
Expression.call (toIterator, [ selfCall "GetEnumerator" [] ]),
// Access kv[0] since the enumerator yields tuples (key, value)
[
Statement.expr (
Yield(Some(Expression.subscript (kvVar, Expression.intConstant 0, Load)))
)
]
)
]
)
]
else
[]
// Generate IMutableMapping dunders: __setitem__, __delitem__
let mutableMappingDunders =
if hasIMutableMapping then
[
// def __setitem__(self, key, value): self.set_item(key, value)
Statement.functionDef (
Identifier "__setitem__",
Arguments.arguments [ Arg.arg "self"; Arg.arg "key"; Arg.arg "value" ],
body =
[
Statement.expr (selfCall "set_item" [ Expression.name "key"; Expression.name "value" ])
]
)
// def __delitem__(self, key): self.Remove(key)
Statement.functionDef (
Identifier "__delitem__",
Arguments.arguments [ Arg.arg "self"; Arg.arg "key" ],
body = [ Statement.expr (selfCall "Remove" [ Expression.name "key" ]) ]
)
]
else
[]
// Generate ISet dunders: __contains__, __len__, __iter__
let setDunders =
if hasISet || hasIMutableSet then
[
// def __contains__(self, item): return self.Contains(item)
Statement.functionDef (
Identifier "__contains__",
Arguments.arguments [ Arg.arg "self"; Arg.arg "item" ],
body = [ returnSelfCall "Contains" [ Expression.name "item" ] ]
)
// def __len__(self): return self.Count
Statement.functionDef (
Identifier "__len__",
Arguments.arguments [ Arg.arg "self" ],
body = [ Statement.return' (Expression.attribute (self, Identifier "Count", Load)) ]
)
// def __iter__(self): return to_iterator(self.GetEnumerator())
let toIterator = com.GetImportExpr(ctx, "fable_library.util", "to_iterator")
Statement.functionDef (
Identifier "__iter__",
Arguments.arguments [ Arg.arg "self" ],
body =
[
Statement.return' (Expression.call (toIterator, [ selfCall "GetEnumerator" [] ]))
]
)
]
else
[]
// Generate IMutableSet dunders: add, discard
// Note: Parameter name must be "value" to match collections.abc.MutableSet
let mutableSetDunders =
if hasIMutableSet then
[
// def add(self, value): self.Add(value)
Statement.functionDef (
Identifier "add",
Arguments.arguments [ Arg.arg "self"; Arg.arg "value" ],
body = [ Statement.expr (selfCall "Add" [ Expression.name "value" ]) ]
)
// def discard(self, value): self.Remove(value)
Statement.functionDef (
Identifier "discard",
Arguments.arguments [ Arg.arg "self"; Arg.arg "value" ],
body = [ Statement.expr (selfCall "Remove" [ Expression.name "value" ]) ]
)
]
else
[]
// Return all generated dunders, avoiding duplicates
// Note: IMapping and ISet both generate __contains__, __len__, __iter__
// If a class implements both (unlikely), we'd have duplicates - but in practice
// a collection is either a mapping or a set, not both
mappingDunders @ mutableMappingDunders @ setDunders @ mutableSetDunders
/// Known core interfaces and their method members.
/// These interfaces are injected by the compiler and their entities may not be available.
/// Maps interface full name -> set of method member names.
let knownInterfaceMethods =
Map
[
"Fable.Core.IGenericAdder`1", set [ "GetZero"; "Add" ]
"Fable.Core.IGenericAverager`1", set [ "GetZero"; "Add"; "DivideByInt" ]
"System.Collections.Generic.IComparer`1", set [ "Compare" ]
"System.Collections.Generic.IEqualityComparer`1", set [ "Equals"; "GetHashCode" ]
]
/// All known method names from core interfaces (for untyped object expressions).
let knownInterfaceMethodNames =
knownInterfaceMethods |> Map.values |> Seq.collect id |> Set.ofSeq
/// Check if the interface member is a method (vs property).
/// Methods have parameters; properties (even returning functions) don't.
/// Used in object expression code generation to determine whether to emit
/// a method or a @property decorator.
let isInterfaceMethod (com: Compiler) (typ: Fable.Type) (memberName: string) : bool =
match typ with
| Fable.DeclaredType(entRef, _) ->
// Check known interfaces first (handles compiler-injected interfaces)
match knownInterfaceMethods.TryFind entRef.FullName with
| Some methods -> methods.Contains memberName
| None ->
// Not a known interface, try entity lookup for user-defined interfaces
match com.TryGetEntity entRef with
| Some ent ->
ent.MembersFunctionsAndValues
|> Seq.tryFind (fun m -> m.DisplayName = memberName || m.CompiledName = memberName)
|> Option.map (fun m -> m.CurriedParameterGroups |> List.exists (not << List.isEmpty))
|> Option.defaultValue false
| None -> false
| Fable.Any ->
// For untyped object expressions (compiler-injected), check known method names
knownInterfaceMethodNames.Contains memberName
| _ -> false
/// Utilities for interface and abstract class member naming.
module InterfaceNaming =
/// Computes the overload suffix for an interface/abstract class member based on parameter types.
/// Returns empty string for getters/setters.
let getOverloadSuffix (ent: Fable.Entity) (memb: Fable.MemberFunctionOrValue) =
if memb.IsGetter || memb.IsSetter then
""
else
let entityGenericParameters = ent.GenericParameters |> List.map (fun g -> g.Name)
memb.CurriedParameterGroups
|> List.collect (List.map (fun pg -> pg.Type))
|> List.singleton
|> OverloadSuffix.getHash entityGenericParameters
/// Generates a mangled member name for interfaces/abstract classes.
/// Format: EntityFullPath_MemberName + OverloadSuffix (dots replaced with underscores)
let getMangledMemberName (ent: Fable.Entity) (memb: Fable.MemberFunctionOrValue) =
let overloadSuffix = getOverloadSuffix ent memb
let lastDotIndex = memb.FullName.LastIndexOf '.'
let fullNamePath = memb.FullName.Substring(0, lastDotIndex)
$"%s{fullNamePath}.%s{memb.CompiledName}%s{overloadSuffix}".Replace(".", "_")
/// Generates abstract method stubs for abstract classes.
/// Creates stubs for dispatch slots (abstract members) that don't have default implementations.
let generateAbstractMethodStubs
(com: IPythonCompiler)
ctx
(classEnt: Fable.Entity)
(attachedMembers: Fable.MemberDecl list)
: Statement list
=
if not classEnt.IsAbstractClass then
[]
else
let implementedMemberNames =
attachedMembers
|> List.map (fun m -> (com.GetMember m.MemberRef).CompiledName)
|> Set.ofList
let isAbstractMember (memb: Fable.MemberFunctionOrValue) =
memb.IsDispatchSlot
&& not memb.IsProperty
&& not (Util.hasAnyEmitAttribute memb.Attributes)
&& not (Set.contains memb.CompiledName implementedMemberNames)
let makeAbstractMethodStub (memb: Fable.MemberFunctionOrValue) =
let name =
InterfaceNaming.getMangledMemberName classEnt memb
|> fun n -> com.GetIdentifier(ctx, n)
let posOnlyArgs =
[
if memb.IsInstance then
Arg.arg "self"
for n, parameterGroup in Seq.indexed memb.CurriedParameterGroups do
for m, pg in Seq.indexed parameterGroup do
let paramType = FableTransforms.uncurryType pg.Type
let annotation, _ = typeAnnotation com ctx None paramType
let paramName = pg.Name |> Option.defaultValue $"__arg%d{n + m}"
Arg.arg (paramName, annotation = annotation)
]
let returnType, _ =
memb.ReturnParameter.Type
|> FableTransforms.uncurryType
|> typeAnnotation com ctx None
Statement.functionDef (
name,
Arguments.arguments (posonlyargs = posOnlyArgs),
body = [ Statement.ellipsis ],
returns = returnType,
decoratorList = [ com.GetImportExpr(ctx, "abc", "abstractmethod") ],
typeParams = Annotation.makeMemberTypeParams com ctx memb.GenericParameters
)
classEnt.MembersFunctionsAndValues
|> Seq.choose (fun memb ->
if isAbstractMember memb then
Some(makeAbstractMethodStub memb)
else
None
)
|> Seq.toList