-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPythonToLaurel.lean
More file actions
3022 lines (2774 loc) · 142 KB
/
Copy pathPythonToLaurel.lean
File metadata and controls
3022 lines (2774 loc) · 142 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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/-
Copyright Strata Contributors
SPDX-License-Identifier: Apache-2.0 OR MIT
-/
module
public import Strata.Languages.Core.Program
public import Strata.Languages.Laurel.LaurelAST
public import StrataPython.OverloadTable
public import StrataPython.PythonDialect
import StrataPython.PythonRuntimeLaurelPart
import Std.Tactic.BVDecide.Normalize.Prop
import Strata.Util.Tactics
/-!
# Python to Laurel Translation
This module translates Python AST to Laurel intermediate representation.
## Design Goals
- Support fully type-annotated Python functions
- Start with heap-free programs (no classes/objects initially)
- Incremental feature addition
## Current Limitations
- No heap operations (classes, objects, fields)
- No collections (lists, dicts, sets)
- No exceptions
- No imports
- No function calls (initially)
- Basic control flow only (if/while/return)
-/
open Strata
open StrataDDM
open Laurel
namespace StrataPython.ToLaurel
public section
/-! ## Translation Context
The translation context tracks information needed during translation:
- Variable types (from type annotations)
- Function signatures (for call resolution)
- Current scope information
-/
structure CoreProcedureSignature where
inputs : List String
outputs : List String
deriving Inhabited
inductive UnmodeledFunctionBehavior where
| havocOutputs
| havocInputsAndOutputs
deriving Inhabited
structure PyArgInfo where
name : String
source : Option FileRange
laurelType : HighTypeMd
typeTesters : Array String
default : Option (expr SourceRange)
structure PyRetInfo where
source : Option FileRange
laurelType : HighTypeMd
typeTesters : Array String
structure PythonFunctionDecl where
name : String
args : List PyArgInfo
kwargsName: Option String
ret : Option PyRetInfo
deriving Inhabited
/-- A symbol imported from a PySpec module, carrying its Laurel-internal
name and enough metadata for the translator to use it directly. -/
inductive ImportedSymbol where
/-- A composite type (user-defined class from PySpec). -/
| compositeType (laurelName : String)
/-- A procedure with its signature and whether it can be inlined. -/
| procedure (laurelName : String) (sig : CoreProcedureSignature)
(inlinable : Bool)
/-- A function (pure, functional callable). -/
| function (laurelName : String)
deriving Inhabited
structure TranslationContext where
variableTypes : List (String × String) := []
/-- List of function signatures -/
functionSignatures : List PythonFunctionDecl := []
/-- Names of user-defined functions -/
userFunctions : List String := []
/-- Function that may return Any of exception -/
maybeExceptionFunctions : List String := []
/-- Names of user-defined classes -/
userClasses : List String := []
/-- Map ClassName -> (FieldName -> HighType) for field access coercions and type inference -/
classFieldHighType: Std.HashMap String (Std.HashMap String HighType) := {}
/-- Names of prelude types -/
preludeTypes : Std.HashSet String := {}
/-- Prelude procedure signatures (for multi-output detection) -/
preludeProcedures : Std.HashMap String CoreProcedureSignature := {}
/-- Overload dispatch table from PySpec: function name → overloads -/
overloadTable : OverloadTable := {}
/-- Behavior for unmodeled functions -/
unmodeledBehavior : UnmodeledFunctionBehavior := .havocOutputs
/-- File path for source location metadata -/
filePath : String := ""
/-- Maps Python-visible names to their structured symbol info. -/
importedSymbols : Std.HashMap String ImportedSymbol := {}
/-- Reverse map: laurelName → pythonName for compositeType entries.
Precomputed from importedSymbols for O(1) reverse lookups. -/
compositeTypeReverse : Std.HashMap String String := {}
/-- Classes whose spec is considered exhaustive (lists all methods). -/
exhaustiveClasses : Std.HashSet String := {}
/-- Track current class during method translation -/
currentClassName : Option String := none
/-- Dispatch-detected field types: ClassName → FieldName → CompositeTypeName.
Used only by refineFunctionCallExpr for self.field.method() dispatch. -/
dispatchFieldTypes : Std.HashMap String (Std.HashMap String String) := {}
/-- Suppress factory dispatch for the current expression (set when translating
the RHS of a self.field assignment to avoid composite coercion issues). -/
suppressDispatch : Bool := false
/-- Classes involved in inheritance (have bases or are subclassed).
Method calls on these classes may be dynamically dispatched,
so call sites conservatively emit holes. -/
classesInHierarchy : Std.HashSet String := {}
loopBreakLabel : Option String := none
loopContinueLabel : Option String := none
deriving Inhabited
/-! ## Error Handling -/
/-- Translation errors with context -/
inductive TranslationError where
| unsupportedConstruct (msg : String) (pyAst : String)
| typeError (msg : String)
| nameError (name : String)
| internalError (msg : String)
/-- A detected bug in the user's Python code (e.g., missing required arguments,
unknown method calls, invalid service names). Unlike other errors which indicate
tool limitations, this means the analysis successfully identified a problem. -/
| userPythonError (range : SourceRange := .none) (msg : String)
deriving Repr
def TranslationError.toString : TranslationError → String
| .unsupportedConstruct msg ast => s!"Unsupported construct: {msg}\nAST: {ast}"
| .typeError msg => s!"Type error: {msg}"
| .nameError name => s!"Name not found: {name}"
| .internalError msg => s!"Internal error: {msg}"
| .userPythonError _ msg => s!"User code error: {msg}"
instance : ToString TranslationError where
toString := TranslationError.toString
/-- Throw a user code error indicating a detected bug in the Python source. -/
def throwUserError [MonadExceptOf TranslationError m] (range : SourceRange := .none) (msg : String) : m α :=
throw (.userPythonError range msg)
/-- Runtime assertion that a decidable proposition holds; throws an internal
error when it does not. Used to obtain array-size proofs that Lean cannot
infer through mutable `for`-loop state.
TODO: This is generically useful — consider moving to `Strata.Util`. -/
private def guardProp {p : Prop} [Decidable p] (msg : String)
: Except TranslationError (PLift p) :=
if h : p then .ok ⟨h⟩ else .error (.internalError msg)
/-! ## Helper Functions -/
/-- Create a FileRange from a SourceRange for attaching to Laurel statements. -/
def sourceRangeToFileRange (filePath : String) (sr : SourceRange) : FileRange :=
let uri : Uri := .file filePath
⟨ uri, sr ⟩
/-- Backward-compatible: create source from a SourceRange. -/
def sourceRangeToSource (filePath : String) (sr : SourceRange) : Option FileRange :=
some (sourceRangeToFileRange filePath sr)
/-- Create a HighTypeMd with default metadata -/
def mkHighTypeMd (ty : HighType) : HighTypeMd :=
{ val := ty, source := none }
/-- Create a HighTypeMd with source location metadata. -/
def mkHighTypeMdWithLoc (ty : HighType) (source : Option FileRange) : HighTypeMd :=
{ val := ty, source := source }
def mkCoreType (s: String): HighTypeMd :=
{val := .TCore s, source := none }
/-- Create a StmtExprMd with default metadata -/
def mkStmtExprMd (expr : StmtExpr) : StmtExprMd :=
{ val := expr, source := none }
/-- Create a VariableMd with default metadata -/
def mkVariableMd (v : Variable) : VariableMd :=
{ val := v, source := none }
/-- Extract a Variable from a StmtExpr. Throws if the expression is not a Var. -/
def stmtExprToVar (e : StmtExprMd) : Except TranslationError VariableMd :=
match e.val with
| .Var v => .ok { val := v, source := e.source }
| _ => .error (.internalError "stmtExprToVar: expected Var node")
/-- A wildcard modifies list, meaning the procedure may modify anything. -/
def wildcardModifies : List StmtExprMd := [mkStmtExprMd .All]
/-- Create a StmtExprMd with source location metadata. -/
def mkStmtExprMdWithLoc (expr : StmtExpr) (source : Option FileRange) : StmtExprMd :=
{ val := expr, source := source }
/-- Create a local variable declaration with initializer. -/
def mkVarDeclInit (name : Identifier) (ty : AstNode HighType) (init : StmtExprMd) : StmtExprMd :=
mkStmtExprMd (.Assign [mkVariableMd (.Declare ⟨name, ty⟩)] init)
/-- Create a local variable declaration with initializer and source location. -/
def mkVarDeclInitWithLoc (name : Identifier) (ty : AstNode HighType) (init : StmtExprMd) (source : Option FileRange) : StmtExprMd :=
mkStmtExprMdWithLoc (.Assign [mkVariableMd (.Declare ⟨name, ty⟩)] init) source
/-- Mangle a class name and method name into a flat procedure name: `ClassName@methodName`. -/
def manglePythonMethod (className : String) (methodName : String) : String :=
className ++ "@" ++ methodName
/-- Build a StaticCall for an instance method: ClassName@methodName(self, args...).
For Any-typed receivers (no model available), returns a Hole instead. -/
def mkInstanceMethodCall (className : String) (methodName : String)
(self : StmtExprMd) (args : List StmtExprMd)
(source : Option FileRange := none) : StmtExprMd :=
if className == "Any" then mkStmtExprMdWithLoc .Hole source
else mkStmtExprMdWithLoc (StmtExpr.StaticCall (manglePythonMethod className methodName) (self :: args)) source
/-- Extract string representation from Python expression (for type annotations) -/
partial def pyExprToString (e : expr SourceRange) : String :=
match e with
| .Name _ n _ => n.val
| .Constant _ (.ConString _ s) _ => s.val
| .Subscript _ val slice _ =>
let base := pyExprToString val
let arg := pyExprToString slice
s!"{base}[{arg}]"
| .Attribute _ val attr _ =>
let base := pyExprToString val
s!"{base}_{attr.val}"
| .Tuple _ elts _ =>
let args := elts.val.toList.map pyExprToString
String.intercalate ", " args
| _ => "<unknown>"
/-- Walk through nested subscripts to find the root variable name.
e.g. `a[b][c]` → `a`, `params["key"]` → `params` -/
partial def getSubscriptBaseName (e : expr SourceRange) : String :=
match e with
| .Name _ n _ => n.val
| .Subscript _ val _ _ => getSubscriptBaseName val
| _ => pyExprToString e
def PyLauType.None := "None"
def PyLauType.Int := "int"
def PyLauType.Bool := "bool"
def PyLauType.Str := "str"
def PyLauType.Float := "float"
def PyLauType.Bytes := "bytes"
def PyLauType.Datetime := "datetime"
def PyLauType.DictStrAny := "DictStrAny"
def PyLauType.ListAny := "ListAny"
def PyLauType.ListStr := "ListStr"
def PyLauType.Package := "Package"
def PyLauType.Any := "Any"
def AnyConstructor.None := "from_None"
def isOfAnyType (ty: String): Bool := ty ∈ [PyLauType.None, PyLauType.Bool, PyLauType.Int, PyLauType.Float,
PyLauType.Str, PyLauType.Datetime, PyLauType.Bytes, PyLauType.ListAny, PyLauType.DictStrAny, PyLauType.Any]
def pyLauTypeTesters (tys : List String) : Array String :=
tys.foldl (init := #[]) fun acc ty =>
if isOfAnyType ty && ty ≠ "Any" then acc.push ("Any..isfrom_" ++ ty) else acc
def PyLauFuncReturnVar := "LaurelResult"
/-- Convert a Laurel HighType to a PyLauType string for type inference. -/
def highTypeToPyLauType : HighType → String
| .TInt => PyLauType.Int
| .TBool => PyLauType.Bool
| .TString => PyLauType.Str
| .TFloat64 => PyLauType.Any
| .TReal => PyLauType.Any
| .UserDefined name => name.text
| _ => PyLauType.Any
/-- Map Python type strings to Core type names -/
def pythonTypeToCoreType (typeStr : String) : Option String :=
match typeStr with
| "Dict[str, Any]" => some PyLauType.DictStrAny
| "List[str]" => some PyLauType.ListStr
| "Any" => some PyLauType.Any
| "None" => some PyLauType.Any
| "datetime" => some PyLauType.Datetime
| "timedelta" => some PyLauType.Int
| _ => none
/-- Check if a type string is recognized (primitive, core mapping, or prelude/composite type). -/
def isKnownType (ctx : TranslationContext) (typeStr : String) : Bool :=
typeStr ∈ ["int", "bool", "str"] ||
(pythonTypeToCoreType typeStr).isSome ||
typeStr ∈ ctx.importedSymbols ||
typeStr ∈ ctx.preludeTypes
/-- Translate Python type annotation to Laurel HighType -/
def translateType (ctx : TranslationContext) (typeStr : String) : Except TranslationError HighTypeMd :=
-- Check if it matches a known composite type (user-defined or PySpec)
match ctx.importedSymbols[typeStr]? with
| some (ImportedSymbol.compositeType laurelName) =>
.ok (mkHighTypeMd (.UserDefined laurelName))
| some _ =>
.error (.userPythonError .none s!"'{typeStr}' is not a type")
| none =>
-- Check if it's a prelude type (Core types like DictStrAny)
if typeStr ∈ ctx.preludeTypes then
.ok (mkCoreType typeStr)
else
-- Map it to a core PyAnyType
.ok (mkCoreType PyLauType.Any)
/-- Translate a field type annotation: builtins become Any, composites stay Composite.
This matches the two-kind system where method signatures use Any for builtins. -/
def translateFieldType (ctx : TranslationContext) (typeStr : String) : Except TranslationError HighTypeMd :=
match ctx.importedSymbols[typeStr]? with
| some (ImportedSymbol.compositeType laurelName) =>
.ok (mkHighTypeMd (.UserDefined laurelName))
| _ => .ok (mkCoreType PyLauType.Any)
def AnyTy := mkCoreType PyLauType.Any
def compositeToStringName (typeName : String) : String := "$composite_to_string_" ++ typeName
def compositeToStringAnyName (typeName : String) : String := "$composite_to_string_any_" ++ typeName
def isCompositeType (ctx : TranslationContext) (typeName : String) : Bool :=
typeName != PyLauType.Any && (ctx.importedSymbols[typeName]?.any fun s =>
match s with | .compositeType _ => true | _ => false)
def pyArgLaurelType (ctx : TranslationContext) (tys : List String) : HighTypeMd :=
match tys with
| [ty] => if isCompositeType ctx ty then mkHighTypeMd (.UserDefined { text := ty }) else AnyTy
| _ => AnyTy
def strToAny (s: String) := mkStmtExprMd (.StaticCall "from_str" [mkStmtExprMd (StmtExpr.LiteralString s)])
def intToAny (i: Int) := mkStmtExprMd (.StaticCall "from_int" [mkStmtExprMd (StmtExpr.LiteralInt i)])
def boolToAny (b: Bool) := mkStmtExprMd (.StaticCall "from_bool" [mkStmtExprMd (StmtExpr.LiteralBool b)])
def AnyNone := mkStmtExprMd (.StaticCall "from_None" [])
/-- Parse a Python float literal string (e.g. "0.0", "1.5", "1e10") into a Decimal.
Returns `none` for formats that cannot be represented (e.g. "inf", "nan").
Handles underscores in numeric literals (e.g. "1_000.5") by stripping them.
-- TODO: prove round-trip: ∀ s d, parseFloatString s = some d → the Decimal d
-- represents the same real number as the Python float literal s. -/
private def parseFloatString (s : String) : Option Decimal := do
-- Non-finite floats cannot be represented as Decimal
let lower := s.toLower
if lower == "inf" || lower == "-inf" || lower == "nan" then none
else
-- Strip underscores (Python allows e.g. 1_000.5)
let s := s.replace "_" ""
-- Split on 'e'/'E' for scientific notation
let (coeffStr, expPart) :=
match s.splitOn "e" with
| [c, e] => (c, (if e.startsWith "+" then e.drop 1 else e).toInt?)
| _ => match s.splitOn "E" with
| [c, e] => (c, (if e.startsWith "+" then e.drop 1 else e).toInt?)
| _ => (s, some 0)
let sciExp ← expPart
-- Parse the coefficient, which may have a decimal point
match coeffStr.splitOn "." with
| [intPart, fracPart] =>
let digits := intPart ++ fracPart
let mantissa ← digits.toInt?
let exponent := sciExp - fracPart.length
some { mantissa, exponent }
| [intPart] =>
let mantissa ← intPart.toInt?
some { mantissa, exponent := sciExp }
| _ => none
def floatToAny (d : Decimal) := mkStmtExprMd (.StaticCall "from_float" [mkStmtExprMd (StmtExpr.LiteralDecimal d)])
def Any_to_bool (b: StmtExprMd) := mkStmtExprMd (.StaticCall "Any_to_bool" [b])
/-- The set of PyLauType names that have runtime type-tester predicates
(`Any..isfrom_<type>`). -/
private def pyLauTypesWithTesters : List String := ["int", "str", "bool", "float"]
/-- Return the Laurel type-tester predicate name for a Python type annotation, if known.
E.g., `"int"` → `"Any..isfrom_int"`. The recognized type set is defined by
`pyLauTypesWithTesters` above. -/
def typeTester? (typeName : String) : Option String :=
if typeName ∈ pyLauTypesWithTesters then some ("Any..isfrom_" ++ typeName)
else none
/-- Wrap a field access expression in the appropriate Any constructor based on HighType.
After heap parameterization, field reads return concrete types (int, bool, etc.)
but Python operators expect Any. This coercion bridges the gap. -/
def wrapFieldInAny (ty : HighType) (expr : StmtExprMd) : Except TranslationError StmtExprMd :=
match ty with
| .TInt => .ok <| mkStmtExprMd (.StaticCall "from_int" [expr])
| .TBool => .ok <| mkStmtExprMd (.StaticCall "from_bool" [expr])
| .TFloat64 => .ok <| mkStmtExprMd (.StaticCall "from_float" [expr])
| .TReal => .ok <| mkStmtExprMd (.StaticCall "from_float" [expr])
| .TString => .ok <| mkStmtExprMd (.StaticCall "from_str" [expr])
| .TCore "Any" => .ok expr
| .UserDefined name => .error (.unsupportedConstruct
s!"Coercion from user-defined class '{name.text}' to Any is not yet supported" name.text)
| other => .error (.typeError s!"wrapFieldInAny: no Any constructor for field type '{repr other}'")
/-- Look up a field's HighType, returning `none` if the class or field is not found. -/
def tryLookupFieldHighType (ctx : TranslationContext) (className fieldName : String) : Option HighType :=
ctx.classFieldHighType[className]?.bind (·[fieldName]?)
/-- Look up a field's HighType, throwing a TranslationError if the class is known but the field is not. -/
def lookupFieldHighType (ctx : TranslationContext) (className fieldName : String) : Except TranslationError HighType :=
match ctx.classFieldHighType[className]? with
| none => .error (.typeError s!"lookupFieldHighType: class '{className}' not found in classFieldHighType")
| some fields =>
match fields[fieldName]? with
| none => .error (.typeError s!"lookupFieldHighType: field '{fieldName}' not found on class '{className}'")
| some ty => .ok ty
def NoError : StmtExprMd := mkStmtExprMd (StmtExpr.StaticCall "NoError" [])
def optNone := mkStmtExprMd (StmtExpr.StaticCall "OptNone" [])
def getSubscriptList (e : expr SourceRange) : List (expr SourceRange) :=
match e with
| .Subscript _ val slice _ => (getSubscriptList val) ++ [slice]
| _ => [e]
def pyOptExprToString (e : opt_expr SourceRange) : Except TranslationError String := do
match e with
| .some_expr _ (.Constant _ (.ConString _ s) _) => return s.val
| _ => throw (.internalError s!"Expected some constant string: {repr e}")
def DictStrAny_mk_aux
(kv: List (String × StmtExprMd)) (acc: StmtExprMd): StmtExprMd :=
match kv with
| [] => acc
| (k,v)::t =>
let dict_insert := StmtExpr.StaticCall "DictStrAny_insert" [acc, mkStmtExprMd (StmtExpr.LiteralString k), v]
DictStrAny_mk_aux t (mkStmtExprMd dict_insert)
def DictStrAny_empty:= mkStmtExprMd (StmtExpr.StaticCall "DictStrAny_empty" [])
def DictStrAny_mk (kv: List (String × StmtExprMd)) := DictStrAny_mk_aux kv DictStrAny_empty
/-- Extract a value from a dictionary for a function parameter.
For required params, generates `Any_get(dict, from_str(key))` (with precondition).
For optional params, generates `Any_get_or_none(dict, from_str(key))` (returns `None` if absent).
Both operate on `Any`-typed dictionaries. -/
def DictStrAny_get_param (dict : StmtExprMd) (key : String) (isOptional : Bool) : StmtExprMd :=
let func := if isOptional then "Any_get_or_none" else "Any_get"
mkStmtExprMd (.StaticCall func [dict, strToAny key])
/-- Look up a function call in the overload dispatch table.
Extracts the bare function name from the call target, then
returns the class name if the first arg is a string literal
matching an overload entry. -/
def resolveDispatch (ctx : TranslationContext)
(f : expr SourceRange)
(args : Array (expr SourceRange))
(kwords : List (keyword SourceRange) := [])
: Except TranslationError (Option String) := do
let funcName := match f with
| .Attribute _ _ attr _ => attr.val
| .Name _ n _ => n.val
| _ => ""
match ctx.overloadTable[funcName]? with
| none => return none
| some fnOverloads =>
let kwPairs := kwords.map keyword.nameAndValue
let some firstArg := fnOverloads.findDispatchArg args kwPairs
| let msg := match kwPairs.filterMap (·.1) with
| provided@(_ :: _) =>
s!"Dispatched function '{funcName}' called with wrong \
keyword argument, expected '{fnOverloads.paramName}' but got \
'{String.intercalate "', '" provided}'"
| _ =>
s!"Dispatched function '{funcName}' called with no \
arguments (expected a string literal first argument)"
throw (.typeError msg)
match firstArg with
| .Constant range (.ConString _ s) _ =>
let some ident := fnOverloads.entries[s.val]?
| let knownServices := fnOverloads.entries.keysArray.insertionSort.take 2
let suffix := if fnOverloads.entries.size > 2 then s!" ... ({fnOverloads.entries.size} total)" else ""
throwUserError range
s!"'{funcName}' called with unknown string \"{s.val}\"; known services: {knownServices}{suffix}"
return some <| ident.toString (sep := "_")
| _ => return none
/-! ## Expression Translation -/
/-- Check if a function has a model (is in prelude or user-defined) -/
def hasModel (ctx : TranslationContext) (funcName : String) : Bool :=
funcName ∈ ctx.importedSymbols || funcName ∈ ctx.userFunctions
/-- Check if a type's spec is exhaustive, meaning unmodeled method
calls should be reported as errors. -/
def isExhaustiveType (ctx : TranslationContext) (typeName : String) : Bool :=
ctx.exhaustiveClasses.contains typeName
def ListAny_mk (es: List StmtExprMd) : StmtExprMd := match es with
| [] => mkStmtExprMd (.StaticCall "ListAny_nil" [])
| e::t => mkStmtExprMd (.StaticCall "ListAny_cons" [e, ListAny_mk t])
def createBoolOrExpr (exprs: List StmtExprMd) : StmtExprMd :=
match exprs with
| [] => mkStmtExprMd (.LiteralBool false)
| [expr] => expr
| expr::exprs => mkStmtExprMd (.PrimitiveOp .Or [expr, createBoolOrExpr exprs])
mutual
partial def translateList (ctx : TranslationContext) (elmts: List (expr SourceRange))
: Except TranslationError StmtExprMd := do
let trans_elmts ← elmts.mapM (translateExpr ctx)
return mkStmtExprMd (.StaticCall "from_ListAny" [ListAny_mk trans_elmts])
partial def translateDictStrAny (ctx : TranslationContext)
(keys: List (opt_expr SourceRange)) (values: List (expr SourceRange))
: Except TranslationError StmtExprMd := do
if keys.length != values.length then
throw (.internalError s!"Invalid Dict: number of keys not match number of values" )
let kv := keys.zip values
let val_trans ← kv.unzip.snd.mapM (translateExpr ctx)
let keys ← keys.mapM pyOptExprToString
return mkStmtExprMd (.StaticCall "from_DictStrAny" [DictStrAny_mk (keys.zip val_trans)])
partial def translateSlice (ctx : TranslationContext) (start stop step: Option (expr SourceRange))
: Except TranslationError StmtExprMd := do
if step.isSome then
throw (.unsupportedConstruct "Expression type not yet supported" (toString (repr step)))
match start, stop with
| some start, some stop =>
let start ← translateExpr ctx start
let stop ← translateExpr ctx stop
let start := mkStmtExprMd (.StaticCall "Any..as_int!" [start])
let stop := mkStmtExprMd (.StaticCall "OptSome" [mkStmtExprMd (.StaticCall "Any..as_int!" [stop])])
return mkStmtExprMd (.StaticCall "from_Slice" [start, stop])
| some start, none =>
let start ← translateExpr ctx start
let start := mkStmtExprMd (.StaticCall "Any..as_int!" [start])
return mkStmtExprMd (.StaticCall "from_Slice" [start, optNone])
| none, some stop =>
let start := mkStmtExprMd (.LiteralInt 0)
let stop ← translateExpr ctx stop
let stop := mkStmtExprMd (.StaticCall "OptSome" [mkStmtExprMd (.StaticCall "Any..as_int!" [stop])])
return mkStmtExprMd (.StaticCall "from_Slice" [start, stop])
| _ , _ =>
let start := mkStmtExprMd (.LiteralInt 0)
return mkStmtExprMd (.StaticCall "from_Slice" [start, optNone])
/-- Translate Python expression to Laurel StmtExpr -/
partial def translateExpr (ctx : TranslationContext) (e : expr SourceRange)
: Except TranslationError StmtExprMd := do
let md := sourceRangeToSource ctx.filePath e.toAst.ann
match e with
-- Integer literals
| .Constant _ (.ConPos _ n) _ => return intToAny n.val
| .Constant _ (.ConNeg _ n) _ => return intToAny (-n.val)
-- String literals
| .Constant _ (.ConString _ s) _ => return strToAny s.val
-- Boolean literals
| .Constant _ (.ConTrue _) _ => return boolToAny true
| .Constant _ (.ConFalse _) _ => return boolToAny false
| .Constant _ (.ConNone _) _ => return AnyNone
-- Bytes literal
| .Constant _ (.ConBytes _ _) _ =>
return mkStmtExprMd .Hole
-- Float literal
| .Constant _ (.ConFloat _ f) _ =>
match parseFloatString f.val with
| some d => return floatToAny d
| none => return mkStmtExprMd .Hole
-- Complex literal
| .Constant _ (.ConComplex _ _ _) _ =>
return mkStmtExprMd .Hole
-- Ellipsis literal
| .Constant _ (.ConEllipsis _) _ =>
return mkStmtExprMd .Hole
-- Variable references
| .Name _ name _ =>
return mkStmtExprMd (StmtExpr.Var (.Local name.val))
-- Binary operations
| .BinOp _ left op right => do
let leftExpr ← translateExpr ctx left
let rightExpr ← translateExpr ctx right
let preludeOpnames ← match op with
-- Arithmetic
| .Add _ => .ok "PAdd"
| .Sub _ => .ok "PSub"
| .Mult _ => .ok "PMul"
| .Div _ => return mkStmtExprMd .Hole -- Floating-point are not supported yet
| .FloorDiv _ => .ok "PFloorDiv" -- Python // maps to Laurel Div
| .Mod _ => .ok "PMod"
| .Pow _ => .ok "PPow"
| .LShift _ => .ok "PLShift"
| .RShift _ => .ok "PRShift"
| .BitAnd _ => return mkStmtExprMd .Hole --TODO: Adding BitVector subtype in Any type, then the related operations
| .BitOr _ => return mkStmtExprMd .Hole
| .BitXor _ => return mkStmtExprMd .Hole
-- Unsupported for now
| _ => throw (.unsupportedConstruct s!"Binary operator not yet supported: {repr op}" (toString (repr e)))
return mkStmtExprMdWithLoc (StmtExpr.StaticCall preludeOpnames [leftExpr, rightExpr]) md
-- Comparison operations
| .Compare _ left ops comparators => do
let n := ops.val.size
if h: (n == 0 || comparators.val.size != n) then
throw (.internalError "Compare: ops/comparators size mismatch")
else
have ⟨hN, hComp⟩ : 0 < n ∧ comparators.val.size = n := by
simp_all [Bool.or_eq_true, beq_iff_eq, bne_iff_ne]; omega
-- Translate a single comparison operator to its Laurel prelude name.
-- `is`/`is not` are only sound when the RHS is None, because Python's
-- `is` checks object identity, not equality (e.g., True == 1 but
-- True is not 1). In the Any value model, None is a singleton so
-- identity and equality coincide for None comparisons.
let cmpopName (i : Nat) (hi : i < n) : Except TranslationError String := do
match ops.val[i]'hi with
| .Eq _ => .ok "PEq"
| .NotEq _ => .ok "PNEq"
| .Lt _ => .ok "PLt"
| .LtE _ => .ok "PLe"
| .Gt _ => .ok "PGt"
| .GtE _ => .ok "PGe"
| .In _ => .ok "PIn"
| .NotIn _ => .ok "PNotIn"
| .Is _ => match comparators.val[i]'(by omega) with
| .Constant _ (.ConNone _) _ => .ok "PEq"
| _ => throw (.unsupportedConstruct "`is` is only supported with None" (toString (repr e)))
| .IsNot _ => match comparators.val[i]'(by omega) with
| .Constant _ (.ConNone _) _ => .ok "PNEq"
| _ => throw (.unsupportedConstruct "`is not` is only supported with None" (toString (repr e)))
-- Check if a Python expression is simple (no side effects when duplicated).
-- Only `Name` and `Constant` are treated as simple. `Subscript` (e.g.,
-- `a[0]`) and `Attribute` (e.g., `self.x`) are intentionally non-simple
-- because Python's `__getitem__`/`__getattr__` can have side effects.
-- Similarly, `BoolOp`, `UnaryOp`, and `BinOp` with simple sub-expressions
-- are technically pure, but treating them as non-simple is correct since
-- the temp variable overhead is negligible and avoids subtle bugs.
let isSimple (pyExpr : expr SourceRange) : Bool :=
match pyExpr with
| .Name .. | .Constant .. => true
| _ => false
-- Translate all operands
let leftExpr ← translateExpr ctx left
let mut compExprs : Array StmtExprMd := #[]
for c in comparators.val do
compExprs := compExprs.push (← translateExpr ctx c)
let ⟨hCompSize⟩ ← guardProp (p := compExprs.size ≥ n) "compExprs size < n"
-- For chained comparisons (n > 1), introduce temp variables for
-- intermediate operands that are not simple names/literals.
-- This preserves Python's evaluate-once semantics for side-effecting
-- intermediate expressions (e.g., `a < f() < b` calls `f()` only once).
let mut tempDecls : Array StmtExprMd := #[]
let mut operandRefs : Array StmtExprMd := #[leftExpr]
for h : i in [:n] do
have hi : i < n := Membership.mem.upper h
have : i < comparators.val.size := by omega
have : i < compExprs.size := by omega
let comp := compExprs[i]
if n > 1 && i < n - 1 && !isSimple (comparators.val[i]) then
-- TODO: Prove that this naming scheme cannot conflict with existing
-- user variables. Shadowing is semantically fine, but a formal
-- non-collision proof would require threading variable-scope info
-- through the translator.
let freshVar := s!"$cmp_tmp_{e.toAst.ann.start.byteIdx}_{i}"
let varDecl := mkVarDeclInit { text := freshVar } AnyTy comp
tempDecls := tempDecls.push varDecl
operandRefs := operandRefs.push (mkStmtExprMd (StmtExpr.Var (.Local { text := freshVar })))
else
operandRefs := operandRefs.push comp
let ⟨hOpSize⟩ ← guardProp (p := operandRefs.size ≥ n + 1) "operandRefs size < n+1"
-- Build pairwise comparisons and chain with PAnd.
-- operandRefs has n+1 elements (leftExpr + n comparators).
let mut pairs : Array StmtExprMd := #[]
for h : i in [:n] do
have hi : i < n := Membership.mem.upper h
have : i < operandRefs.size := by omega
have : i + 1 < operandRefs.size := by omega
let opName ← cmpopName i hi
let lhs := operandRefs[i]
let rhs := operandRefs[i+1]
pairs := pairs.push (mkStmtExprMd (StmtExpr.StaticCall opName [lhs, rhs]))
let ⟨hPairsSize⟩ ← guardProp (p := pairs.size ≥ 1) "pairs is empty"
-- Fold pairs with PAnd (pairs has n ≥ 1 elements)
have : 0 < pairs.size := by omega
let mut result := pairs[0]
for h : i in [1:pairs.size] do
have hi : i < pairs.size := Membership.mem.upper h
result := mkStmtExprMd (StmtExpr.StaticCall "PAnd" [result, pairs[i]])
-- Wrap in a block if we emitted temp variable declarations
if tempDecls.isEmpty then
return { result with source := md }
else
return mkStmtExprMdWithLoc (StmtExpr.Block (tempDecls.toList ++ [result]) none) md
-- Boolean operations
| .BoolOp _ op values => do
if values.val.size < 2 then
throw (.internalError "BoolOp must have at least 2 operands")
let preludeOpnames ← match op with
| .And _ => .ok "PAnd"
| .Or _ => .ok "POr"
-- Translate all operands
let mut exprs : List StmtExprMd := []
for val in values.val do
let expr ← translateExpr ctx val
exprs := exprs ++ [expr]
-- Chain binary operations: a && b && c becomes (a && b) && c
let mut result := exprs[0]!
for i in [1:exprs.length] do
result := mkStmtExprMd (StmtExpr.StaticCall preludeOpnames [result, exprs[i]!])
return {result with source := md}
-- Unary operations
| .UnaryOp _ op operand => do
let operandExpr ← translateExpr ctx operand
let preludeOpnames ← match op with
| .Not _ => .ok "PNot"
| .USub _ => .ok "PNeg"
| .Invert _ => .ok "PBitNot"
| _ => throw (.unsupportedConstruct s!"Unary operator not yet supported: {repr op}" (toString (repr e)))
return mkStmtExprMdWithLoc (StmtExpr.StaticCall preludeOpnames [operandExpr]) md
-- FormattedValue (f-string interpolation {expr}) - convert to string-typed Any
| .FormattedValue _ value _ _ =>
let ty ← inferExprType ctx value
let inner ← translateExpr ctx value
let asAny ← if isCompositeType ctx ty then
let fields := (ctx.classFieldHighType[ty]?.getD {}).toList
let dict ← fields.foldlM (fun acc (fname, fty) =>
return mkStmtExprMd (.StaticCall "DictStrAny_cons"
[mkStmtExprMd (.LiteralString fname),
← wrapFieldInAny fty (mkStmtExprMd (.Var (.Field inner fname))), acc]))
(mkStmtExprMd (.StaticCall "DictStrAny_empty" []))
pure <| mkStmtExprMd (.StaticCall "from_ClassInstance"
[mkStmtExprMd (.LiteralString ty), dict])
else pure inner
return mkStmtExprMd (.StaticCall "to_string_any" [asAny])
-- JoinedStr (f-strings) - concatenate string parts via str.concat
| .JoinedStr _ values =>
if values.val.isEmpty then
return strToAny ""
else
let parts ← values.val.toList.mapM (translateExpr ctx ·)
let unwrap (e : StmtExprMd) := mkStmtExprMd (.StaticCall "Any..as_string!" [e])
let concat := parts.foldl (fun acc part =>
mkStmtExprMd (.PrimitiveOp .StrConcat [acc, unwrap part]))
(mkStmtExprMd (.LiteralString ""))
return mkStmtExprMd (.StaticCall "from_str" [concat])
-- Interpolation / TemplateStr (Python 3.14+ t-strings) - not yet supported
| .Interpolation .. => return mkStmtExprMd .Hole
| .TemplateStr .. => return mkStmtExprMd .Hole
| .IfExp _ cond thenb elseb =>
let condExpr ← translateExpr ctx cond
let thenExpr ← translateExpr ctx thenb
let elseExpr ← translateExpr ctx elseb
return mkStmtExprMdWithLoc (StmtExpr.IfThenElse (Any_to_bool condExpr) thenExpr elseExpr) md
| .Call _ f args kwargs =>
let result ← translateCall ctx f args.val.toList kwargs.val.toList
return {result with source := md}
-- Subscript access: dict['key'] or list[0]
-- Abstract: return havoc'd value (sound for any dict/list operation)
-- Note: Creates free variables which cause type errors in some contexts (if conditions)
-- TODO: Handle by creating explicit variable declarations
| .Subscript _ val slice _ =>
let dictOrList ← translateExpr ctx val
match slice with
| .Slice _ start stop step =>
let index ← translateSlice ctx start.val stop.val step.val
return mkStmtExprMdWithLoc (.StaticCall "Any_get_slice" [dictOrList, index]) md
| _ =>
let index ← translateExpr ctx slice
-- Emit bounds check for negative integer indices on lists (e.g., xs[-1])
-- and convert to positive index: xs[-n] becomes xs[len(xs) - n].
-- Skip for dicts, where negative integer keys are valid dict lookups.
-- Note: Python's AST represents `-1` as UnaryOp(USub, Constant(1)),
-- not Constant(-1), so we must match both forms.
let valType := (inferExprType ctx val).toOption.getD PyLauType.Any
let isDictType := valType == PyLauType.DictStrAny
let negLitVal? := match slice with
| .Constant _ (.ConNeg _ n) _ => some n.val
| .UnaryOp _ (.USub _) (.Constant _ (.ConPos _ n) _) => some n.val
| _ => none
let index := match negLitVal? with
| some n =>
if isDictType then index
else
-- xs[-n] becomes xs[len(xs) - n]
let listExpr := mkStmtExprMd (.StaticCall "Any..as_ListAny!" [dictOrList])
let lenExpr := mkStmtExprMd (.StaticCall "List_len" [listExpr])
let nLit := mkStmtExprMd (.LiteralInt n)
mkStmtExprMd (.StaticCall "from_int"
[mkStmtExprMd (.PrimitiveOp .Sub [lenExpr, nLit])])
| none => index
return mkStmtExprMdWithLoc (.StaticCall "Any_get" [dictOrList, index]) md
-- Attribute access: obj.attr or obj.method
| .Attribute _ obj attr _ => do
-- Check if this is self.field access in a method
match obj with
| .Name _ name _ =>
if name.val == "self" && ctx.currentClassName.isSome then
-- self.field in a method - field type is Any (builtins) or Composite (classes)
let fieldExpr := mkStmtExprMd (StmtExpr.Var (.Field
(mkStmtExprMd (StmtExpr.Var (.Local "self")))
attr.val))
let className := ctx.currentClassName.get!
match tryLookupFieldHighType ctx className attr.val with
| some (.UserDefined _) =>
-- Only return Hole if this field has a dispatch type (tracked in
-- dispatchFieldTypes). Method dispatch on self.field.method() is
-- handled by refineFunctionCallExpr via dispatchFieldTypes.
-- For non-dispatch UserDefined fields, return the field expression
-- so standalone reads like `x = self.field` are not silently lost.
let hasDispatch := ctx.dispatchFieldTypes[className]?.any (·.contains attr.val)
if hasDispatch then
return mkStmtExprMd .Hole
else
return fieldExpr
| _ =>
return fieldExpr
else if isPackage ctx obj then
-- FIXME: Module attribute (e.g., sys.argv): modules are not modeled as
-- Laurel values, so return Hole like we do for unmodeled package calls.
return mkStmtExprMd .Hole
else
-- Regular object.field access
let objExpr ← translateExpr ctx obj
let fieldExpr := mkStmtExprMd (StmtExpr.Var (.Field objExpr attr.val))
let objType ← inferExprType ctx obj
match tryLookupFieldHighType ctx objType attr.val with
| some ty => wrapFieldInAny ty fieldExpr
| none => return fieldExpr
| _ =>
-- Complex object expression - translate and access field
let objExpr ← translateExpr ctx obj
let fieldExpr := mkStmtExprMd (StmtExpr.Var (.Field objExpr attr.val))
let objType ← inferExprType ctx obj
match tryLookupFieldHighType ctx objType attr.val with
| some ty => wrapFieldInAny ty fieldExpr
| none => return fieldExpr
-- List literal: [1, 2, 3]
| .List _ elems _ => translateList ctx elems.val.toList
-- Dict literal: {'a': 1}
| .Dict _ keys vals => translateDictStrAny ctx keys.val.toList vals.val.toList
-- Set literal: {1, 2, 3}
-- Abstract: return havoc'd set (sound abstraction)
| .Set .. => return mkStmtExprMd .Hole
-- Tuple literal: (1, 2)
| .Tuple _ elems _ => translateList ctx elems.val.toList
-- List comprehension: [x for x in items]
-- Abstract: return havoc'd list (sound abstraction)
| .ListComp .. => return mkStmtExprMd .Hole
-- Set comprehension: {x for x in items}
-- Abstract: return havoc'd set (sound abstraction)
| .SetComp .. => return mkStmtExprMd .Hole
-- Dict comprehension: {k: v for k, v in items}
| .DictComp .. => return mkStmtExprMd .Hole
-- Generator expression: (x for x in items)
| .GeneratorExp .. => return mkStmtExprMd .Hole
| _ => throw (.unsupportedConstruct "Expression type not yet supported" (toString (repr e)))
partial def getListAttributes (e: expr SourceRange): (expr SourceRange) × List String :=
match e with
| .Attribute _ v attr _ =>
let ret := (getListAttributes v)
(ret.fst , ret.snd ++ [attr.val])
| _ => (e, [])
partial def reMapFunctionName (_ctx: TranslationContext) (fname: String) : String :=
match fname with
| "str" => "to_string_any"
| "int" => "to_int_any"
| "bool" => "to_bool_any"
| "len" => "Any_len_to_Any"
| "timedelta" => "timedelta_func" -- We handle timedelta as an int, not a class
| _ => fname
partial def isPackage (ctx : TranslationContext) (expr: expr SourceRange) : Bool :=
let (root, _):= getListAttributes expr
match root with
| .Name _ n _ => n.val ∉ ctx.variableTypes.unzip.fst
| _ => false
partial def inferExprType (ctx : TranslationContext) (e: expr SourceRange) : Except TranslationError String := do
match e with
-- Integer literals
| .Constant _ (.ConPos _ _) _
| .Constant _ (.ConNeg _ _) _ => return PyLauType.Int
-- String literals
| .Constant _ (.ConString _ _) _ => return PyLauType.Str
-- Boolean literals
| .Constant _ (.ConTrue _) _
| .Constant _ (.ConFalse _) _
| .BoolOp _ _ _
| .Compare _ _ _ _=> return PyLauType.Bool
| .Constant _ (.ConNone _) _ => return PyLauType.None
-- Variable references
| .Name _ n _ =>
match ctx.variableTypes.find? (λ v => v.fst == n.val) with
| some (_, ty) => return ty
| _ => return PyLauType.Package
| .Attribute _ v attr _ =>
let vty ← inferExprType ctx v
match tryLookupFieldHighType ctx vty attr.val with
| some ty => return (highTypeToPyLauType ty)
| none => return PyLauType.Any
-- Binary operations
| .BinOp _ _ _ _ => return PyLauType.Any
-- Unary operations
| .UnaryOp _ _ _ => return PyLauType.Any
-- JoinedStr (f-strings) produce strings
| .JoinedStr _ _ => return PyLauType.Str
-- FormattedValue produces string-typed Any
| .FormattedValue _ _ _ _ => return PyLauType.Str
| .Call _ f args kwargs =>
getFunctionReturnType ctx f args.val kwargs.val.toList
| _ => return PyLauType.Any
partial def getFunctionReturnType (ctx : TranslationContext) (func: expr SourceRange) (args : Array (expr SourceRange))
(kwords : List (keyword SourceRange) := [])
: Except TranslationError String := do
match resolveDispatch ctx func args kwords with
| .ok (some classname) => return classname
| .error e => throw e
| .ok none =>
let (fname, _) ← refineFunctionCallExpr ctx func
match ctx.functionSignatures.find? (λ f => f.name == fname) with
| some funcDecl =>
match funcDecl.ret with
| some retInfo => return highTypeToPyLauType retInfo.laurelType.val
| none => return PyLauType.Any
| _ => return PyLauType.Any
/-
Python function call can be caller.function_name(arg1, arg2, ...)
If a is a variable, and type of a can be inferred, then the call should be translated to TypeOfcaller_function_name (caller, arg1, arg2)
If a is a variable, and type of a can NOT be inferred, then the call should be translated to AnyType_function_name (TypeOf(caller), caller, arg1, arg2)
If a is a package, this should be translated to a_function_name (arg1, arg2)
If the function_name is a class, add __init__ into it
The following function return a tuple (translated function name, first argument, is the first argument of unknown type)
-/
/-- Coerce an expression to Any if its inferred type is a Composite class.
Composite values are replaced with a Hole (unconstrained Any value)
since Composite→Any coercion is not yet modeled. This limits
bug-finding ability but avoids type unification errors. -/
partial def coerceToAny (ctx : TranslationContext) (expr : expr SourceRange)
(translated : StmtExprMd) : Except TranslationError StmtExprMd := do
let ty ← inferExprType ctx expr
if isCompositeType ctx ty then
pure <| mkStmtExprMd (.Hole)
else pure translated
partial def refineFunctionCallExpr (ctx : TranslationContext) (func: expr SourceRange) :
Except TranslationError (String × Option (expr SourceRange) × Bool) := do
match func with
| .Name _ n _ => return (reMapFunctionName ctx n.val, none , false)
| .Attribute _ v attr _ =>
let callerTy ← inferExprType ctx v
let callname := attr.val
if isPackage ctx v then
return (pyExprToString func, none, false)
else