diff --git a/Strata.lean b/Strata.lean index 5457e4d54a..4ab15c65f1 100644 --- a/Strata.lean +++ b/Strata.lean @@ -44,6 +44,8 @@ import Strata.Languages.Core.VerifierProofs import Strata.Languages.Dyn.Dyn import Strata.Languages.Dyn.Verify import Strata.Languages.Laurel.FilterPrelude +import Strata.Languages.FineGrainLaurel.FineGrainLaurel +import Strata.Languages.FineGrainLaurel.Elaborate /- DDM -/ import StrataDDM diff --git a/Strata/Languages/FineGrainLaurel/Elaborate.lean b/Strata/Languages/FineGrainLaurel/Elaborate.lean new file mode 100644 index 0000000000..751139c20c --- /dev/null +++ b/Strata/Languages/FineGrainLaurel/Elaborate.lean @@ -0,0 +1,1659 @@ +/- + Copyright Strata Contributors + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module + +import Strata.Languages.FineGrainLaurel.FineGrainLaurel +public import Strata.Languages.Laurel.LaurelAST +public import Strata.Languages.Laurel.HeapParameterizationConstants +public import Strata.Languages.Laurel.CoreDefinitionsForLaurel + +/-! +# Pass 3: Elaboration + +Elaboration transforms Laurel programs (impure CBV, effects implicit) into +Laurel programs where effects are explicit via calling conventions. The +theoretical foundation is **Fine-Grain Call-By-Value** (FGCBV) with graded +effects and bidirectional typing. + +## Why FGCBV? + +In plain CBV, every expression can have effects. You cannot tell by looking +at `f(x, g(y))` whether `g(y)` allocates, throws, or is pure. This matters +for verification because the calling convention depends on it: a pure call +returns a value directly; an effectful call returns through output parameters +(heap, error status). + +FGCBV separates **values** (pure, duplicable) from **producers** (effectful, +sequenced). A producer must be explicitly sequenced — this makes the +elaborator syntax-directed. At every point, the structure of the term tells +you whether you are looking at a value or a producer. + +## Bidirectional Typing + +The elaborator has three mutually recursive functions: + +- `synthValue`: value synthesis — literals, variables, pure calls, field access +- `checkValue`: value checking — synthesize then coerce (the ONE place subsumption lives) +- `checkProducer`: producer checking — if, while, assign, block, exit, assert, etc. + +Values synthesize their types bottom-up. Producers are checked against an +ambient grade and output type top-down. The mode discipline guarantees +deterministic choices at every point. + +## Graded Effects + +Each producer carries a grade from `{pure, proc, err, heap, heapErr}`. The +grade determines the calling convention (extra heap parameters, error outputs). +Grade inference proceeds by coinduction over the call graph: try each grade +from `pure` upward, the first that succeeds is the procedure's grade. + +## Two Passes + +1. **Grade inference** (coinductive fixpoint): for each user procedure, find + the minimal grade at which elaboration succeeds. +2. **Term production**: elaborate each procedure at its inferred grade, + project the FGCBV term back to Laurel statements. +-/ + +namespace Strata.FineGrainLaurel +open Strata.Laurel +open StrataDDM -- for `Decimal` (used by FGLValue.litDecimal), as LaurelAST does +public section + +/-! ## Internal Types + +Elaboration builds its own environment from `Laurel.Program` declarations. +Ideally call sites would carry callee signatures directly (no lookup needed), +but the Laurel AST uses string-named `StaticCall` nodes. -/ + +/-- Elaboration's internal function signature (built from Laurel.Procedure declarations). -/ +structure FuncSig where + /-- Procedure name (string, matching StaticCall callee names). -/ + name : String + /-- Input parameters as (name, type) pairs. -/ + params : List (String × HighType) + /-- Return type (first non-error output). -/ + returnType : HighType + +instance : Inhabited FuncSig where + default := { name := "", params := [], returnType := .TCore "Any" } + +/-- What a name resolves to in Elaboration's type environment. -/ +inductive NameInfo where + /-- A callable procedure with its signature. -/ + | function (sig : FuncSig) + /-- A variable binding with its type. -/ + | variable (ty : HighType) + +instance : Inhabited NameInfo where + default := .variable (.TCore "Any") + +/-- The typing environment: maps names to their info and class names to field lists. -/ +structure ElabTypeEnv where + /-- All known names (procedures, variables, datatype constructors). -/ + names : Std.HashMap String NameInfo := {} + /-- Class fields: class name -> list of (field name, field type). -/ + classFields : Std.HashMap String (List (String × HighType)) := {} + deriving Inhabited + +/-- Builds the type environment from a Laurel program's declarations. Scans all + procedures (user + runtime) for signatures, all types for class fields. -/ +def buildElabEnvFromProgram (program : Laurel.Program) (runtime : Laurel.Program := default) : ElabTypeEnv := Id.run do + let mut names : Std.HashMap String NameInfo := {} + let mut classFields : Std.HashMap String (List (String × HighType)) := {} + for proc in program.staticProcedures ++ runtime.staticProcedures do + let params := proc.inputs.map fun p => (p.name.text, p.type.val) + let retTy := match proc.outputs.head? with + | some o => o.type.val | none => HighType.TVoid + names := names.insert proc.name.text (.function { name := proc.name.text, params, returnType := retTy }) + for td in program.types ++ runtime.types do + match td with + | .Composite ct => + let fields := ct.fields.map fun f => (f.name.text, f.type.val) + classFields := classFields.insert ct.name.text fields + -- Register the class as a callable constructor: CircularBuffer(args) → CircularBuffer + let retTy := HighType.UserDefined { text := ct.name.text, uniqueId := none } + names := names.insert ct.name.text (.function { name := ct.name.text, params := [], returnType := retTy }) + | .Datatype dt => + for ctor in dt.constructors do + let ctorParams := ctor.args.map fun p => (p.name.text, p.type.val) + let retTy := HighType.UserDefined { text := dt.name.text, uniqueId := none } + names := names.insert ctor.name.text (.function { name := ctor.name.text, params := ctorParams, returnType := retTy }) + | .Constrained _ => pure () + | .Alias _ => pure () + { names, classFields } + +def mkLaurel (md : Option FileRange) (e : StmtExpr) : StmtExprMd := + { val := e, source := md } +def mkHighTypeMd (md : Option FileRange) (ty : HighType) : HighTypeMd := + { val := ty, source := md } + +/-! ## The Grade Monoid + +Grades classify which effects a producer performs. The monoid structure +ensures compositionality: sequencing two producers joins their grades. +The left residual `d \ e` ("what grade remains for the continuation after +a call at grade `d` within ambient grade `e`") drives grade inference — +if `d \ e` is undefined (d > e), elaboration fails and the grade is +pushed upward. -/ + +/-- The effect grade lattice: pure < proc < {err, heap} < heapErr. -/ +inductive Grade where + /-- No effects. Value-level `staticCall`, no extra params. -/ + | pure + /-- Effectful but no error or heap. Outputs: `[result]`. -/ + | proc + /-- May throw. Outputs: `[result, maybe_except]`. -/ + | err + /-- Reads/writes heap. Inputs: `[$heap]`. Outputs: `[$heap, result]`. -/ + | heap + /-- Heap + error. Inputs: `[$heap]`. Outputs: `[$heap, result, maybe_except]`. -/ + | heapErr + deriving Inhabited, BEq, Repr + +/-- Join (least upper bound) of two grades. Sequencing two producers joins their grades. -/ +def Grade.join : Grade → Grade → Grade + | .pure, e => e | e, .pure => e + | .proc, .proc => .proc + | .proc, .err => .err | .err, .proc => .err + | .proc, .heap => .heap | .heap, .proc => .heap + | .proc, .heapErr => .heapErr | .heapErr, .proc => .heapErr + | .err, .err => .err + | .err, .heap => .heapErr | .heap, .err => .heapErr + | .err, .heapErr => .heapErr | .heapErr, .err => .heapErr + | .heap, .heap => .heap + | .heap, .heapErr => .heapErr | .heapErr, .heap => .heapErr + | .heapErr, .heapErr => .heapErr + +/-- Left residual: `d\e` = grade for the continuation after a call at grade `d` + within ambient grade `e`. Returns `none` if `d > e` (elaboration fails). + + Satisfies the residuation law for an idempotent semilattice: + `d ⊔ x ≤ e` iff `x ≤ d\e`. Since `⊔` is idempotent (join), + the largest `x` with `d ⊔ x ≤ e` is `e` itself (when `d ≤ e`). + So `d\e = e` whenever `d ≤ e`, and undefined otherwise. +``` +d\e = e if d ≤ e +d\e = ⊥ otherwise +``` +-/ +def Grade.leftResidual : Grade → Grade → Option Grade + | .pure, e => some e + | .proc, e => if e == .pure then none else some e + | .err, e => match e with | .err | .heapErr => some e | _ => none + | .heap, e => match e with | .heap | .heapErr => some e | _ => none + | .heapErr, .heapErr => some .heapErr + | _, _ => none + +/-! ## Type Erasure + +Elaboration operates on `LowType` — the erased version of `HighType`. +User-defined types erase to `Composite` (they live on the heap). The +subtyping/coercion system operates on `LowType` values. -/ + +/-- The erased type system. User-defined types become `Composite` (heap-allocated). + Subsumption and coercion operate on `LowType` values. -/ +inductive LowType where + /-- Machine integer. -/ + | TInt + /-- Boolean. -/ + | TBool + /-- String. -/ + | TString + /-- 64-bit float. -/ + | TFloat64 + /-- Unit/void. -/ + | TVoid + /-- Named core type (Any, Error, Heap, Composite, ListAny, DictStrAny, etc.). -/ + | TCore (name : String) + /-- A user-defined class, name preserved. Erases to `Composite` for boxing/subtyping + purposes (see `eraseForSubtype`), but kept distinct here so a `var self : Account` + declaration round-trips back to `.UserDefined "Account"` — the kbd Laurel resolver + resolves `self.field` via the receiver's static `.UserDefined` type, so collapsing + every class to `Composite` would lose field resolution. -/ + | TUser (name : String) + deriving Inhabited, Repr, BEq + +/-- Type erasure: HighType -> LowType. Primitives map directly, user-defined classes + keep their name (`.TUser`), unknown/complex types become Any. -/ +def eraseType : HighType → LowType + | .TInt => .TInt | .TBool => .TBool | .TString => .TString + | .TFloat64 => .TFloat64 | .TVoid => .TVoid | .TCore n => .TCore n + | .UserDefined id => match id.text with + | "Any" => .TCore "Any" | "Error" => .TCore "Error" + | "ListAny" => .TCore "ListAny" | "DictStrAny" => .TCore "DictStrAny" + | "OptionInt" => .TCore "OptionInt" + | "Box" => .TCore "Box" | "Field" => .TCore "Field" | "TypeTag" => .TCore "TypeTag" + | _ => .TUser id.text + | .TReal => .TCore "real" + | .TSet _ | .TMap _ _ | .Applied _ _ | .Intersection _ | .Unknown + | .TBv _ | .MultiValuedExpr _ => .TCore "Any" + | .Pure _ => .TCore "Composite" + +/-- Collapse a LowType to its subtyping/boxing representative: every user-defined class + is `Composite` for the purposes of the `subtype` coercion table. -/ +def eraseForSubtype : LowType → LowType + | .TUser _ => .TCore "Composite" + | other => other + +/-- Inverse of erasure (partial): lifts a LowType back to HighType for env extension. -/ +def liftType : LowType → HighType + | .TUser name => .UserDefined { text := name, uniqueId := none } + | .TInt => .TInt | .TBool => .TBool | .TString => .TString + | .TFloat64 => .TFloat64 | .TVoid => .TVoid | .TCore n => .TCore n + +/-! ## FGL Terms + +The intermediate representation between Laurel input and Laurel output. +Values are pure (can appear in any context). Producers are effectful +(must be sequenced). Every constructor carries source metadata so +provenance is preserved through elaboration. -/ + +abbrev Md := Option FileRange + +/-- A pure value in the FGCBV intermediate term. Can appear in any context. + Every constructor carries source metadata for provenance. -/ +inductive FGLValue where + /-- Integer literal. -/ + | litInt (md : Md) (n : Int) + /-- Boolean literal. -/ + | litBool (md : Md) (b : Bool) + /-- String literal. -/ + | litString (md : Md) (s : String) + /-- Decimal/real literal (Python float). -/ + | litDecimal (md : Md) (d : Decimal) + /-- Variable reference. -/ + | var (md : Md) (name : String) + /-- Coercion: int → Any. -/ + | fromInt (md : Md) (inner : FGLValue) + /-- Coercion: string → Any. -/ + | fromStr (md : Md) (inner : FGLValue) + /-- Coercion: bool → Any. -/ + | fromBool (md : Md) (inner : FGLValue) + /-- Coercion: float → Any. -/ + | fromFloat (md : Md) (inner : FGLValue) + /-- Coercion: Composite → Any. -/ + | fromComposite (md : Md) (inner : FGLValue) + /-- Coercion: ListAny → Any. -/ + | fromListAny (md : Md) (inner : FGLValue) + /-- Coercion: DictStrAny → Any. -/ + | fromDictStrAny (md : Md) (inner : FGLValue) + /-- Coercion: None → Any. -/ + | fromNone (md : Md) + /-- Field access (pre-heap-resolution). -/ + | fieldAccess (md : Md) (obj : FGLValue) (field : String) + /-- Pure function call. -/ + | staticCall (md : Md) (name : String) (args : List FGLValue) + /-- Object creation (pre-heap-resolution). heapParameterizationPass allocates. -/ + | new (md : Md) (className : String) + deriving Inhabited + +def FGLValue.getMd : FGLValue → Md + | .litInt md _ | .litBool md _ | .litString md _ | .litDecimal md _ | .var md _ + | .fromInt md _ | .fromStr md _ | .fromBool md _ | .fromFloat md _ + | .fromComposite md _ | .fromListAny md _ | .fromDictStrAny md _ | .fromNone md + | .fieldAccess md _ _ | .staticCall md _ _ | .new md _ => md + +/-- An effectful producer in the FGCBV intermediate term. Must be sequenced. + Each form carries a continuation (`body`/`after`) — the CPS structure + makes projection to Laurel statements trivial. -/ +inductive FGLProducer where + /-- Return a value (terminal — no continuation). -/ + | produce (md : Md) (v : FGLValue) + /-- Assign to an existing variable, then continue. RHS is a producer whose + resolved value is assigned to target. -/ + | assign (md : Md) (target : FGLValue) (val : FGLProducer) (body : FGLProducer) + /-- Declare a local variable, then continue in extended scope. Init is a + producer whose resolved value initializes the variable. -/ + | varDecl (md : Md) (name : String) (ty : LowType) (init : FGLProducer) (body : FGLProducer) + /-- Conditional: check condition, branch, then continue after. -/ + | ifThenElse (md : Md) (cond : FGLValue) (thn : FGLProducer) (els : FGLProducer) (after : FGLProducer) + /-- Loop: check condition, iterate body, then continue after. -/ + | whileLoop (md : Md) (cond : FGLValue) (body : FGLProducer) (after : FGLProducer) + /-- Assert condition holds, then continue. -/ + | assert (md : Md) (cond : FGLValue) (body : FGLProducer) + /-- Assume condition holds, then continue. -/ + | assume (md : Md) (cond : FGLValue) (body : FGLProducer) + /-- Effectful call: bind outputs, then continue in extended scope. -/ + | procedureCall (md : Md) (callee : String) (args : List FGLValue) + (outputs : List (String × LowType)) (body : FGLProducer) + /-- Exit to enclosing labeled block (non-returning). -/ + | exit (md : Md) (label : String) + /-- Labeled block: body may exit to label, then continue after. -/ + | labeledBlock (md : Md) (label : String) (body : FGLProducer) (after : FGLProducer) + /-- Empty continuation (end of block). -/ + | skip + deriving Inhabited + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Monad +-- ═══════════════════════════════════════════════════════════════════════════════ + +/-- Reader environment for elaboration. Carries the type environment, program, + runtime, inferred grades, and current procedure's input list (for hole args). -/ +structure ElabEnv where + /-- The typing context (names + class fields). -/ + typeEnv : ElabTypeEnv + /-- The user program being elaborated. -/ + program : Laurel.Program + /-- The runtime prelude (builtins, data structure operations). -/ + runtime : Laurel.Program := default + /-- Inferred grades for all procedures. -/ + procGrades : Std.HashMap String Grade := {} + /-- Current procedure's input params (used as hole arguments). -/ + procInputs : List (String × HighType) := [] + +/-- Mutable state for elaboration: fresh name counter and hole collector. -/ +structure ElabState where + /-- Counter for generating fresh variable names. -/ + freshCounter : Nat := 0 + /-- Hole functions used (emitted as opaque procedure declarations in output). -/ + usedHoles : List (String × Bool × HighType) := [] + +abbrev ElabM := ReaderT ElabEnv (StateT ElabState Option) + +private def freshVar (pfx : String := "tmp") : ElabM String := do + let s ← get; set { s with freshCounter := s.freshCounter + 1 }; pure s!"{pfx}${s.freshCounter}" + + +/-- Reads a runtime procedure's grade structurally from its signature: does it + have a Heap input? An Error output? The combination determines the grade. + User procedure grades are inferred by coinduction, not read from signature. -/ +def gradeFromSignature (proc : Laurel.Procedure) : Grade := + -- Exceptions-only principle: heap is owned by heapParameterizationPass, so + -- a runtime proc that takes a Heap input is just `.proc` (or `.err` if it can also throw). + let hasError := proc.outputs.any fun o => eraseType o.type.val == .TCore "Error" + match hasError with + | true => .err + | false => if proc.isFunctional then .pure else .proc + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Env helpers +-- ═══════════════════════════════════════════════════════════════════════════════ + +def lookupEnv (name : String) : ElabM NameInfo := do + match (← read).typeEnv.names[name]? with | some info => pure info | none => failure +def extendEnv (name : String) (ty : HighType) (action : ElabM α) : ElabM α := + withReader (fun env => { env with typeEnv := { env.typeEnv with names := env.typeEnv.names.insert name (.variable ty) } }) action +def lookupFuncSig (name : String) : ElabM FuncSig := do + match (← read).typeEnv.names[name]? with | some (.function sig) => pure sig | _ => failure +def lookupFieldType (className fieldName : String) : ElabM HighType := do + match (← read).typeEnv.classFields[className]? with + | some fields => match fields.find? (fun (n, _) => n == fieldName) with + | some (_, ty) => pure ty + | none => failure + | none => failure + +/-! ## HOAS Smart Constructors + +These construct effectful call nodes using higher-order abstract syntax: +the continuation is a Lean function from fresh output variables to the +body producer. This ensures output variables are always correctly scoped +(extended in the environment before the body is elaborated). -/ + +def mkEffectfulCall (md : Md) (callee : String) (args : List FGLValue) + (outputSpecs : List (String × HighType)) + (body : List FGLValue → ElabM FGLProducer) : ElabM FGLProducer := do + let mut names : List String := [] + let mut lowOutputs : List (String × LowType) := [] + for (pfx, ty) in outputSpecs do + let n ← freshVar pfx + names := names ++ [n] + lowOutputs := lowOutputs ++ [(n, eraseType ty)] + let vars := names.map (FGLValue.var md) + let cont ← names.zip (outputSpecs.map (·.2)) |>.foldr + (fun (n, ty) acc => extendEnv n ty acc) (body vars) + pure (.procedureCall md callee args lowOutputs cont) + +def mkVarDecl (md : Md) (name : String) (ty : LowType) (init : FGLProducer) + (body : FGLValue → ElabM FGLProducer) : ElabM FGLProducer := do + let cont ← extendEnv name (liftType ty) (body (.var md name)) + pure (.varDecl md name ty init cont) + +/-- Subgrading witness: `d ≤ e ↦ (pre, outs)`. Constructs a `procedureCall` + with the correct calling convention based on grade. +``` +d ≤ e ↦ (args_prepended, outputs_declared, resultIdx) + +pure: ([], [], —) — value-level, no procedureCall +proc: ([], [result:B], 0) +err: ([], [result:B, except:Error], 0) +heap: ([heap_var], [heap:Heap, result:B], 1) +heapErr: ([heap_var], [heap:Heap, result:B, except:Error], 1) +``` +-/ +def mkGradedCall (md : Md) (callee : String) (args : List FGLValue) + (declaredOutputs : List (String × HighType)) + (body : FGLValue → ElabM FGLProducer) : ElabM FGLProducer := do + mkEffectfulCall md callee args declaredOutputs fun outs => do + let resultVar := outs[0]? + match resultVar with + | some rv => body rv + | none => body (.fromNone md) + +/-! ## Subsumption + +A subtyping judgment `A <= B` has a witness: a coercion function. Upward +coercions (T <= Any) are value constructors (boxing). Downward coercions +(Any <= T) are pure function calls (unboxing). `applySubtype` is called +ONLY from `checkValue` — this is the bidirectional discipline. -/ + +/-- The result of a subsumption check: identity (types equal), a coercion witness + (function to apply), or unrelated (no subtyping relationship). -/ +inductive CoercionResult where + /-- Types are equal — no coercion needed. -/ + | refl + /-- Subtyping holds — apply this coercion function. -/ + | coerce (w : Md → FGLValue → FGLValue) + /-- No subtyping relationship. -/ + | unrelated + deriving Inhabited + +/-- Subtyping judgment `A ≤ B ↦ c` as a total case analysis: every `(A, B)` pair +is decided. `.refl` when `A = B`; `.coerce w` when Python implicitly converts +`A → B`, witnessed by one direct runtime function; `.unrelated` otherwise — a +deliberate verdict, never a forgotten case. `TCore` names outside the finite set +`eraseType` produces are `.unrelated` (sound default for an unknown type). +``` +A ≤ A ↦ id (reflexivity) + +box T ≤ Any: TInt↦fromInt TBool↦fromBool TString↦fromStr TFloat64↦fromFloat + Composite↦fromComposite ListAny↦fromListAny + DictStrAny↦fromDictStrAny TVoid↦fromNone +unbox Any ≤ T: bool↦Any_to_bool int↦as_int! str↦as_string! float↦as_float! + Composite↦as_Composite! DictStrAny↦as_Dict! ListAny↦as_ListAny! +truth T ≤ bool: TInt↦int_to_bool TString↦str_to_bool TFloat64↦float_to_bool + ListAny↦list_to_bool DictStrAny↦dict_to_bool + TVoid↦false Composite↦true +num bool≤int≤float: TBool↦int bool_to_int TInt↦float int_to_real + TBool↦float bool_to_real +``` +-/ +def subtype (actual0 expected0 : LowType) : CoercionResult := + if actual0 == expected0 then .refl else + -- Collapse user-defined classes to `Composite` for the coercion table: a class value + -- boxes to Any exactly as a Composite does, and unboxes the same way. The distinct + -- `.TUser` name only matters for var-decl projection / field resolution, not coercion. + let actual := eraseForSubtype actual0 + let expected := eraseForSubtype expected0 + if actual == expected then .refl else match expected, actual with + -- box: T ≤ Any + | .TCore "Any", .TInt => .coerce (fun md => .fromInt md) + | .TCore "Any", .TBool => .coerce (fun md => .fromBool md) + | .TCore "Any", .TString => .coerce (fun md => .fromStr md) + | .TCore "Any", .TFloat64 => .coerce (fun md => .fromFloat md) + -- `eraseType .TReal = .TCore "real"` (Python floats are reals). Box real→Any via the + -- same `from_float` (which takes a `real`), matching the .TFloat64 arm. + | .TCore "Any", .TCore "real" => .coerce (fun md => .fromFloat md) + | .TCore "Any", .TCore "Composite" => .coerce (fun md => .fromComposite md) + | .TCore "Any", .TCore "ListAny" => .coerce (fun md => .fromListAny md) + | .TCore "Any", .TCore "DictStrAny" => .coerce (fun md => .fromDictStrAny md) + | .TCore "Any", .TVoid => .coerce (fun md _ => .fromNone md) + | .TCore "Any", _ => .unrelated + -- to bool: unbox from Any, else per-type truthiness + | .TBool, .TCore "Any" => .coerce (fun md v => .staticCall md "Any_to_bool" [v]) + | .TBool, .TInt => .coerce (fun md v => .staticCall md "int_to_bool" [v]) + | .TBool, .TString => .coerce (fun md v => .staticCall md "str_to_bool" [v]) + | .TBool, .TFloat64 => .coerce (fun md v => .staticCall md "float_to_bool" [v]) + | .TBool, .TCore "ListAny" => .coerce (fun md v => .staticCall md "list_to_bool" [v]) + | .TBool, .TCore "DictStrAny" => .coerce (fun md v => .staticCall md "dict_to_bool" [v]) + | .TBool, .TVoid => .coerce (fun md _ => .litBool md false) + | .TBool, .TCore "Composite" => .coerce (fun md _ => .litBool md true) + | .TBool, _ => .unrelated + -- to int: unbox from Any, else bool widening + | .TInt, .TCore "Any" => .coerce (fun md v => .staticCall md "Any..as_int!" [v]) + | .TInt, .TBool => .coerce (fun md v => .staticCall md "bool_to_int" [v]) + | .TInt, _ => .unrelated + -- to float: unbox from Any, else int/bool widening + | .TFloat64, .TCore "Any" => .coerce (fun md v => .staticCall md "Any..as_float!" [v]) + | .TCore "real", .TCore "Any" => .coerce (fun md v => .staticCall md "Any..as_float!" [v]) + | .TFloat64, .TInt => .coerce (fun md v => .staticCall md "int_to_real" [v]) + | .TFloat64, .TBool => .coerce (fun md v => .staticCall md "bool_to_real" [v]) + | .TFloat64, _ => .unrelated + -- to string: unbox from Any + | .TString, .TCore "Any" => .coerce (fun md v => .staticCall md "Any..as_string!" [v]) + | .TString, _ => .unrelated + -- to container/Composite: unbox from Any + | .TCore "Composite", .TCore "Any" => .coerce (fun md v => .staticCall md "Any..as_Composite!" [v]) + | .TCore "DictStrAny", .TCore "Any" => .coerce (fun md v => .staticCall md "Any..as_Dict!" [v]) + | .TCore "ListAny", .TCore "Any" => .coerce (fun md v => .staticCall md "Any..as_ListAny!" [v]) + | _, _ => .unrelated + +/-- Effects-only subsumption: the elaborator goes *untyped Laurel → effect-typed + (FGCBV) Laurel*, threading EFFECTS (grades, via `leftResidual`), NOT value + (box/unbox) coercions. The pure-type coercions are now inserted by the Laurel + resolver's proof-relevant subtyping judgment (`coerce` + the frontend's + `realizeCoercion`), which runs after elaboration. So `applySubtype` is the + identity on values — it must NOT box/unbox, or the resolver would double-coerce + (the elaborator and resolver are mutually exclusive coercers). `subtype` / + `CoercionResult` remain as the reference the Python realizer is transcribed from. -/ +def applySubtype (val : FGLValue) (_actual _expected : LowType) : FGLValue := + val + +/-! ## The Translation ⟦·⟧ : Laurel → GFGL + +Three functions: synthValue (⟦·⟧⇒ᵥ), checkValue (⟦·⟧⇐ᵥ), checkProducer (⟦·⟧⇐ₚ). +Entry point is checkProducer — every Laurel derivation maps to a GFGL producer. +synthValue/checkValue are internal helpers for building value sub-terms. +Producer synthesis (⟦·⟧⇒ₚ) is applied by inversion inside the call clause. -/ + +/-- Fetch the declared outputs of a proc from the runtime or user program. -/ +private def lookupProcDeclaredOutputs (callee : String) : ElabM (List (String × HighType)) := do + let env ← read + let findProc (procs : List Laurel.Procedure) : Option Laurel.Procedure := + procs.find? (fun p => p.name.text == callee) + match findProc env.runtime.staticProcedures with + | some proc => pure (proc.outputs.map fun o => (o.name.text, o.type.val)) + | none => match findProc env.program.staticProcedures with + | some proc => pure (proc.outputs.map fun o => (o.name.text, o.type.val)) + | none => failure + +/-- Rewrite declared outputs for a given inferred grade: strip any existing Error + output and re-add it for err/heapErr grades. Heap is owned by the downstream + `heapParameterizationPass` — no `$heap` output is emitted here. -/ +private def rewriteOutputsForGrade (declaredOutputs : List (String × HighType)) (g : Grade) : List (String × HighType) := + let resultOutputs := declaredOutputs.filter fun (_, ty) => eraseType ty != .TCore "Error" + match g with + | .err | .heapErr => resultOutputs ++ [("maybe_except", .TCore "Error")] + | _ => resultOutputs + +/-- Look up a proc's outputs rewritten for its inferred grade. -/ +partial def lookupProcOutputs (callee : String) : ElabM (List (String × HighType)) := do + let g := (← read).procGrades[callee]?.getD .pure + let declared ← lookupProcDeclaredOutputs callee + pure (rewriteOutputsForGrade declared g) + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- The Translation ⟦·⟧ : Laurel → GFGL +-- +-- Three functions: synthValue (⟦·⟧⇒ᵥ), checkValue (⟦·⟧⇐ᵥ), checkProducer (⟦·⟧⇐ₚ) +-- Entry point is checkProducer. synthValue/checkValue are internal helpers. +-- Producer synthesis (⟦·⟧⇒ₚ) is applied by inversion inside the call clause. +-- ═══════════════════════════════════════════════════════════════════════════════ + +mutual + +/-- ⟦·⟧⇒ᵥ (literal): +``` +D :: Γ ⊢ n : int [lit] + + ↦ + +⟦D⟧⇒ᵥ :: ⟦Γ⟧ ⊢ litInt n ⇒ TInt [litInt] +``` +(analogous for bool, string) +-/ +partial def synthValueLiteral (md : Md) (expr : StmtExpr) : Option (FGLValue × HighType) := + match expr with + | .LiteralInt n => some (.litInt md n, .TInt) + | .LiteralBool b => some (.litBool md b, .TBool) + | .LiteralString s => some (.litString md s, .TString) + | .LiteralDecimal d => some (.litDecimal md d, .TReal) + | _ => none + +/-- ⟦·⟧⇒ᵥ (variable): +``` +D :: Γ ⊢ x : A [var, (x:A) ∈ Γ] + + ↦ + +⟦D⟧⇒ᵥ :: ⟦Γ⟧ ⊢ var x ⇒ ⟦A⟧ [var, (x:⟦A⟧) ∈ ⟦Γ⟧] +``` +-/ +partial def synthValueVar (md : Md) (id : Identifier) : ElabM (FGLValue × HighType) := do + match (← lookupEnv id.text) with + | .variable ty => pure (.var md id.text, ty) + | _ => failure + +/-- ⟦·⟧⇒ᵥ (field access): +``` +D :: Γ ⊢ obj.f : T [fieldSelect] +└─ D_obj :: Γ ⊢ obj : C + + ↦ precondition: ($heap : Heap) ∈ ⟦Γ⟧ + +⟦D⟧⇒ᵥ :: ⟦Γ⟧ ⊢ functionCall unbox_T [functionCall readField [$heap, V_obj, $field.C.f]] ⇒ ⟦T⟧ [functionCall] +└─ ⟦Γ⟧ ⊢ functionCall readField [$heap, V_obj, $field.C.f] ⇐ Box [subsumption] + ├─ ⟦Γ⟧ ⊢ functionCall readField [$heap, V_obj, $field.C.f] ⇒ Box [functionCall] + │ ├─ ⟦Γ⟧ ⊢ $heap ⇐ Heap [subsumption] + │ │ ├─ ⟦Γ⟧ ⊢ $heap ⇒ Heap [var] + │ │ └─ Heap ≤ Heap ↦ id + │ ├─ ⟦D_obj⟧⇐ᵥ :: ⟦Γ⟧ ⊢ V_obj ⇐ Composite [subsumption] + │ │ ├─ ⟦D_obj⟧⇒ᵥ :: ⟦Γ⟧ ⊢ V_obj ⇒ Composite (since ⟦C⟧ = Composite for user-defined C) + │ │ └─ Composite ≤ Composite ↦ id + │ └─ ⟦Γ⟧ ⊢ functionCall $field.C.f [] ⇐ Field [subsumption] + │ ├─ ⟦Γ⟧ ⊢ functionCall $field.C.f [] ⇒ Field [functionCall] + │ └─ Field ≤ Field ↦ id + └─ Box ≤ Box ↦ id +``` +-/ +partial def synthValueFieldSelect (md : Md) (obj : StmtExprMd) (field : Identifier) : ElabM (FGLValue × HighType) := do + let (ov, objTy) ← synthValue obj + -- Synth rule for field access: e ⇒ &{… l : A_l …} ⊢ e.l ⇒ A_l ; e ⇒ Any ⊢ e.l ⇒ Any. + -- We trust the user's field annotations (the frontend's contract) and let the coercion + -- mechanism reconcile A_l with whatever the use-site demands. The bare `.fieldAccess` is + -- lowered by heapParameterizationPass to `Box..Val!(readField …)`, which unboxes to + -- exactly A_l — so synthesizing A_l here is faithful, not optimistic. `Any` is the genuine + -- FALLTHROUGH: only when the receiver's type isn't a known composite (e.g. `self`/`Any`). + let fieldTy ← + match objTy with + | .UserDefined cls => + match ← (do match (← read).typeEnv.classFields[cls.text]? with + | some fields => pure (fields.find? (fun (n, _) => n == field.text)) + | none => pure none) with + | some (_, ty) => pure ty + | none => pure (.TCore "Any") + | _ => pure (.TCore "Any") + pure (.fieldAccess md ov field.text, fieldTy) + +/-- ⟦·⟧⇒ᵥ (pure call): +``` +D :: Γ ⊢ f(e₁,…,eₙ) : B [call, f : (Aᵢ) → B & pure] +└─ D_i :: Γ ⊢ eᵢ : Aᵢ (for each i) + + ↦ + +⟦D⟧⇒ᵥ :: ⟦Γ⟧ ⊢ functionCall f [V₁,…,Vₙ] ⇒ ⟦B⟧ [functionCall] +└─ ⟦D_i⟧⇐ᵥ :: ⟦Γ⟧ ⊢ Vᵢ ⇐ ⟦Aᵢ⟧ (for each i) [subsumption] + ├─ ⟦D_i⟧⇒ᵥ :: ⟦Γ⟧ ⊢ Vᵢ ⇒ Bᵢ (Bᵢ discovered by recursive synthValue) + └─ Bᵢ ≤ ⟦Aᵢ⟧ ↦ cᵢ +``` +-/ +partial def synthValueStaticCall (md : Md) (callee : Identifier) (args : List StmtExprMd) : ElabM (FGLValue × HighType) := do + -- A name carrying a function signature but no explicit procedure grade is pure: + -- datatype constructors (from_None, from_int, ...) and pure runtime functions + -- live in typeEnv.names but not in procGrades. Default to pure, as elaborateCall + -- and lookupProcOutputs do; only a name graded above pure is rejected here. + let g := (← read).procGrades[callee.text]?.getD .pure + guard (g == .pure) + let sig : FuncSig := match (← read).typeEnv.names[callee.text]? with + | some (.function s) => s + | _ => { name := callee.text, params := [], returnType := HighType.Unknown } + let checkedArgs ← checkArgValues args sig.params + pure (.staticCall md callee.text checkedArgs, sig.returnType) + +/-- ⟦·⟧⇒ᵥ: Value synthesis. Dispatches to clause helpers. -/ +partial def synthValue (expr : StmtExprMd) : ElabM (FGLValue × HighType) := do + let md := expr.source + match expr.val with + | .LiteralInt _ | .LiteralBool _ | .LiteralString _ | .LiteralDecimal _ => + match synthValueLiteral md expr.val with + | some r => pure r + | none => failure + | .Var (.Local id) => synthValueVar md id + | .Var (.Field obj field) => synthValueFieldSelect md obj field + | .StaticCall callee args => synthValueStaticCall md callee args + | _ => failure + +/-- Helper: check a list of arguments as values against parameter types. -/ +partial def checkArgValues (args : List StmtExprMd) (params : List (String × HighType)) : ElabM (List FGLValue) := do + match args, params with + | [], _ => pure [] + | arg :: rest, (_, pty) :: prest => do + let v ← checkValue arg pty + let vs ← checkArgValues rest prest + pure (v :: vs) + | _ :: _, [] => failure + +/-- ⟦·⟧⇐ᵥ: Value checking. Synthesizes then applies subtyping coercion. +``` +⟦D⟧⇐ᵥ (deterministic hole) :: ⟦Γ⟧ ⊢ functionCall hole_N [input₁,...,inputₖ] ⇐ ⟦A⟧ [functionCall] +└─ (hole_N : (⟦T₁⟧,...,⟦Tₖ⟧) → ⟦A⟧ & pure) ∈ ⟦Γ⟧ +``` +-/ +partial def checkValue (expr : StmtExprMd) (expected : HighType) : ElabM FGLValue := do + let md := expr.source + match expr.val with + | .Hole _ _ => + -- A hole in pure value position (a contract, or an argument of a pure call) + -- denotes a deterministic uninterpreted function of the procedure's inputs: + -- nondeterminism is meaningless in a pure value, so even a hole Translation + -- marked nondeterministic (e.g. an unresolved `re.search(...)` inside a + -- `requires`) is elaborated here as the deterministic `hole_N(inputs)`. This + -- keeps the contract well-typed; the caller obligation is sound but + -- uninterpretable (verification stays inconclusive, never unsound). + let hv ← freshVar "hole" + let args := (← read).procInputs.map fun (name, _) => FGLValue.var md name + modify fun s => { s with usedHoles := s.usedHoles ++ [(hv, true, expected)] } + pure (.staticCall md hv args) + | _ => + let (val, actual) ← synthValue expr + pure (applySubtype val (eraseType actual) (eraseType expected)) + +/-- ⟦·⟧⇐ₚ*: Check a list of statements as a producer (list extension). -/ +partial def checkProducers (stmts : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + match stmts with + | [] => pure .skip + | stmt :: rest => checkProducer stmt rest retTy grade + +/-- ⟦·⟧⇐ₚ (if): +``` +D :: Γ ⊢ (if c then t else f); k : A [if] +├─ D_c :: Γ ⊢ c : bool +├─ D_t :: Γ ⊢ t : A +├─ D_f :: Γ ⊢ f : A +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ varDecl x_c bool M_c (ifThenElse x_c M_t M_f M_k) ⇐ ⟦A⟧ & d [varDecl] +├─ ⟦D_c⟧⇐ₚ :: ⟦Γ⟧ ⊢ M_c ⇐ bool & d +└─ ⟦Γ⟧, x_c:bool ⊢ ifThenElse x_c M_t M_f M_k ⇐ ⟦A⟧ & d [ifThenElse] + ├─ ⟦Γ⟧, x_c:bool ⊢ x_c ⇐ bool [subsumption] + │ ├─ ⟦Γ⟧, x_c:bool ⊢ x_c ⇒ bool [var] + │ └─ bool ≤ bool ↦ id + ├─ ⟦D_t⟧⇐ₚ :: ⟦Γ⟧, x_c:bool ⊢ M_t ⇐ ⟦A⟧ & d + ├─ ⟦D_f⟧⇐ₚ :: ⟦Γ⟧, x_c:bool ⊢ M_f ⇐ ⟦A⟧ & d + └─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧, x_c:bool ⊢ M_k ⇐ ⟦A⟧ & d +``` +-/ +partial def checkProducerIf (md : Md) (cond thn : StmtExprMd) (els : Option StmtExprMd) + (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + let M_c ← checkProducer cond [] .TBool grade + let x_c ← freshVar "cond" + let body ← extendEnv x_c .TBool do + let M_t ← checkProducer thn [] retTy grade + let M_f ← match els with + | some e => checkProducer e [] retTy grade + | none => pure .skip + let M_k ← checkProducers rest retTy grade + pure (.ifThenElse md (.var md x_c) M_t M_f M_k) + pure (.varDecl md x_c .TBool M_c body) + +/-- ⟦·⟧⇐ₚ (while): +``` +D :: Γ ⊢ (while c do body); k : A [while] +├─ D_c :: Γ ⊢ c : bool +├─ D_b :: Γ ⊢ body : A +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ varDecl x_c bool M_c (whileLoop x_c M_b' M_k) ⇐ ⟦A⟧ & d [varDecl] +├─ ⟦D_c⟧⇐ₚ :: ⟦Γ⟧ ⊢ M_c ⇐ bool & d (initial guard evaluation) +└─ ⟦Γ⟧, x_c:bool ⊢ whileLoop x_c M_b' M_k ⇐ ⟦A⟧ & d [whileLoop] + ├─ ⟦Γ⟧, x_c:bool ⊢ x_c ⇐ bool [subsumption] + │ ├─ ⟦Γ⟧, x_c:bool ⊢ x_c ⇒ bool [var] + │ └─ bool ≤ bool ↦ id + ├─ ⟦D_b ; (x_c := c)⟧⇐ₚ :: ⟦Γ⟧, x_c:bool ⊢ M_b' ⇐ ⟦A⟧ & d + │ where M_b' = ⟦body ; (x_c := c)⟧ — the guard is RE-EVALUATED at the end of + │ each iteration, so the loop tests a fresh value, not a frozen one. The + │ re-evaluation is threaded by appending the source-level assignment `x_c := c` + │ to `body`'s statement block before elaboration (so M_b' is a single derivation + │ over the extended body, sharing the body's sequencing). This is REQUIRED for + │ soundness: with a frozen `x_c`, downstream loop-elimination (which havocs + │ loop-modified vars) lets the verifier prove post-loop facts the loop should + │ not establish. + └─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧, x_c:bool ⊢ M_k ⇐ ⟦A⟧ & d +``` +-/ +partial def checkProducerWhile (md : Md) (cond loopBody : StmtExprMd) + (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + -- The condition must be RE-EVALUATED every iteration. We bind it to `x_c` before the + -- loop AND re-assign `x_c := cond` at the END of the loop body, so the next guard test + -- sees the updated value. Hoisting it once (looping on a frozen `x_c`) was a real bug: + -- the guard never changed, and after LoopElim havoc the loop was mis-modeled (it let the + -- verifier "prove" post-loop facts the loop should havoc — see test_while_loop). The + -- proven PythonToLaurel inlines the condition expression into the While for the same + -- reason. We keep the value-bound form (the elaborator needs a value guard) but refresh + -- it inside the body. + let M_c ← checkProducer cond [] .TBool grade + let x_c ← freshVar "cond" + -- Append `x_c := cond` to the END of the loop body (at the Laurel level) so it elaborates + -- with the body's natural sequencing and refreshes the guard each iteration. `loopBody` + -- is a Block; we extend its statement list with the reassignment. + let reassign : StmtExprMd := mkLaurel md (.Assign [⟨.Local { text := x_c }, md⟩] cond) + let loopBody' : StmtExprMd := match loopBody.val with + | .Block stmts lbl => mkLaurel md (.Block (stmts ++ [reassign]) lbl) + | _ => mkLaurel md (.Block [loopBody, reassign] none) + let body ← extendEnv x_c .TBool do + let M_b ← checkProducer loopBody' [] retTy grade + let M_k ← checkProducers rest retTy grade + pure (.whileLoop md (.var md x_c) M_b M_k) + pure (.varDecl md x_c .TBool M_c body) + +/-- ⟦·⟧⇐ₚ (varDecl): +``` +D :: Γ ⊢ (var x:T := e); k : A [varDecl] +├─ D_e :: Γ ⊢ e : T +└─ D_k :: Γ, x:T ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ varDecl x ⟦T⟧ M_e M_k ⇐ ⟦A⟧ & d [varDecl] +├─ ⟦D_e⟧⇐ₚ :: ⟦Γ⟧ ⊢ M_e ⇐ ⟦T⟧ & d +└─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧, x:⟦T⟧ ⊢ M_k ⇐ ⟦A⟧ & d +``` +-/ +partial def checkProducerVarDecl (md : Md) (nameId : Identifier) (typeMd : HighTypeMd) + (initOpt : Option StmtExprMd) (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + let M_e ← match initOpt with + | some init => checkProducer init [] typeMd.val grade + | none => do + let v ← checkValue (mkLaurel md (.Hole true none)) typeMd.val + pure (.produce md v) + let body ← extendEnv nameId.text typeMd.val do + checkProducers rest retTy grade + pure (.varDecl md nameId.text (eraseType typeMd.val) M_e body) + +/-- ⟦·⟧⇐ₚ (assert): +``` +D :: Γ ⊢ (assert c); k : A [assert] +├─ D_c :: Γ ⊢ c : bool +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ varDecl x_c bool M_c (assert x_c M_k) ⇐ ⟦A⟧ & d [varDecl] +├─ ⟦D_c⟧⇐ₚ :: ⟦Γ⟧ ⊢ M_c ⇐ bool & d +└─ ⟦Γ⟧, x_c:bool ⊢ assert x_c M_k ⇐ ⟦A⟧ & d [assert] + ├─ ⟦Γ⟧, x_c:bool ⊢ x_c ⇐ bool [subsumption] + │ ├─ ⟦Γ⟧, x_c:bool ⊢ x_c ⇒ bool [var] + │ └─ bool ≤ bool ↦ id + └─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧, x_c:bool ⊢ M_k ⇐ ⟦A⟧ & d +``` +-/ +partial def checkProducerAssert (md : Md) (cond : StmtExprMd) + (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + let M_c ← checkProducer cond [] .TBool grade + let x_c ← freshVar "cond" + let body ← extendEnv x_c .TBool do + let M_k ← checkProducers rest retTy grade + pure (.assert md (.var md x_c) M_k) + pure (.varDecl md x_c .TBool M_c body) + +/-- ⟦·⟧⇐ₚ (assume): +``` +D :: Γ ⊢ (assume c); k : A [assume] +├─ D_c :: Γ ⊢ c : bool +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ varDecl x_c bool M_c (assume x_c M_k) ⇐ ⟦A⟧ & d [varDecl] +├─ ⟦D_c⟧⇐ₚ :: ⟦Γ⟧ ⊢ M_c ⇐ bool & d +└─ ⟦Γ⟧, x_c:bool ⊢ assume x_c M_k ⇐ ⟦A⟧ & d [assume] + ├─ ⟦Γ⟧, x_c:bool ⊢ x_c ⇐ bool [subsumption] + │ ├─ ⟦Γ⟧, x_c:bool ⊢ x_c ⇒ bool [var] + │ └─ bool ≤ bool ↦ id + └─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧, x_c:bool ⊢ M_k ⇐ ⟦A⟧ & d +``` +-/ +partial def checkProducerAssume (md : Md) (cond : StmtExprMd) + (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + let M_c ← checkProducer cond [] .TBool grade + let x_c ← freshVar "cond" + let body ← extendEnv x_c .TBool do + let M_k ← checkProducers rest retTy grade + pure (.assume md (.var md x_c) M_k) + pure (.varDecl md x_c .TBool M_c body) + +partial def elaborateCall (md : Md) (callee : Identifier) (args : List StmtExprMd) + (grade : Grade) (body : FGLValue → Grade → ElabM FGLProducer) : ElabM FGLProducer := do + let callGrade := (← read).procGrades[callee.text]?.getD .pure + let some residual := Grade.leftResidual callGrade grade | failure + let sig ← lookupFuncSig callee.text + -- Runtime `function` procs (isFunctional=true) are called as StaticCalls regardless of + -- their grade. Their exceptions are encoded as values (returning `exception(...)` inside + -- `Any`), not as a Laurel `Error` output. Only user procs and runtime `procedure`s get the + -- full procedureCall calling convention. + let env ← read + let isFunctionalRuntime : Bool := + match env.runtime.staticProcedures.find? (fun p => p.name.text == callee.text) with + | some rp => rp.isFunctional + | none => false + bindArgs md args sig.params grade fun boundVars => do + if isFunctionalRuntime || callGrade == .pure then + let rv := FGLValue.staticCall md callee.text boundVars + body rv residual + else + let declaredOutputs ← lookupProcOutputs callee.text + mkGradedCall md callee.text boundVars declaredOutputs fun rv => + body rv residual + +/-- ⟦·⟧⇐ₚ (bare call, discards return value): +``` +D :: Γ ⊢ g(e₁,…,eₙ); k : A [call] +├─ (g : (A₁,...,Aₙ) → B) ∈ Γ +├─ Dᵢ :: Γ ⊢ eᵢ : Aᵢ (for each i) +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ varDecl x₁ ⟦A₁⟧ M₁ (...(varDecl xₙ ⟦Aₙ⟧ Mₙ (procedureCall g (pre ++ [x₁,...,xₙ]) outs M_k))) ⇐ ⟦A⟧ & d +├─ ⟦D₁⟧⇐ₚ :: ⟦Γ⟧ ⊢ M₁ ⇐ ⟦A₁⟧ & d +├─ ... [varDecl] +├─ ⟦Dₙ⟧⇐ₚ :: ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ₋₁:⟦Aₙ₋₁⟧ ⊢ Mₙ ⇐ ⟦Aₙ⟧ & d +└─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧ ⊢ procedureCall g (pre ++ [x₁,...,xₙ]) outs M_k ⇐ ⟦A⟧ & d [producerSubsumption] + ├─ (g : (⟦A₁⟧,...,⟦Aₙ⟧) → ⟦B⟧ & d') ∈ ⟦Γ⟧ + ├─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧ ⊢ xᵢ ⇐ ⟦Aᵢ⟧ [subsumption] + │ ├─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧ ⊢ xᵢ ⇒ ⟦Aᵢ⟧ [var] + │ └─ ⟦Aᵢ⟧ ≤ ⟦Aᵢ⟧ ↦ id + ├─ d' ≤ d ↦ (pre, outs) + └─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧, outs ⊢ M_k ⇐ ⟦A⟧ & (d'\d) +``` +-/ +partial def checkProducerStaticCall (md : Md) (callee : Identifier) (args : List StmtExprMd) + (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + elaborateCall md callee args grade fun rv residual => do + match rest with + | [] => + let sig ← lookupFuncSig callee.text + pure (.produce md (applySubtype rv (eraseType sig.returnType) (eraseType retTy))) + | _ => checkProducers rest retTy residual + +/-- ⟦·⟧⇐ₚ (block): +``` +D :: Γ ⊢ {body}_l; k : A [block] +├─ D_b :: Γ, l ⊢ body : A +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ labeledBlock l M_b M_k ⇐ ⟦A⟧ & d [labeledBlock] +├─ ⟦D_b⟧⇐ₚ :: ⟦Γ⟧, l ⊢ M_b ⇐ ⟦A⟧ & d +└─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧ ⊢ M_k ⇐ ⟦A⟧ & d +``` +Unlabeled blocks are flattened into the enclosing scope. +-/ +partial def checkProducerBlock (md : Md) (stmts : List StmtExprMd) (label : Option String) + (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + match label with + | some l => + let M_b ← checkProducers stmts retTy grade + let M_k ← checkProducers rest retTy grade + pure (.labeledBlock md l M_b M_k) + | none => checkProducers (stmts ++ rest) retTy grade + +/-- ⟦·⟧⇐ₚ: Producer checking. Entry point of the translation. + Dispatches on statement form to clause helpers. -/ +partial def checkProducer (stmt : StmtExprMd) (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + let md := stmt.source + match stmt.val with + | .IfThenElse cond thn els => checkProducerIf md cond thn els rest retTy grade + | .While cond _invs _dec loopBody => checkProducerWhile md cond loopBody rest retTy grade + | .Exit target => pure (.exit md target) + | .Var (.Declare ⟨nameId, typeMd⟩) => checkProducerVarDecl md nameId typeMd none rest retTy grade + | .Assert cond => checkProducerAssert md cond.condition rest retTy grade + | .Assume cond => checkProducerAssume md cond rest retTy grade + | .Assign targets value => match targets with + | [target] => checkAssign target value rest retTy grade + | _ => failure + | .StaticCall callee args => checkProducerStaticCall md callee args rest retTy grade + | .Block stmts label => checkProducerBlock md stmts label rest retTy grade + | .New _ => failure + | .Hole deterministic _ => do + let hv ← freshVar "havoc" + modify fun s => { s with usedHoles := s.usedHoles ++ [(hv, deterministic, retTy)] } + -- A deterministic hole is a pure function of the procedure's inputs, so it is + -- declared with those inputs (see emission below) and must be applied to them + -- here — same as the value-judgment `.Hole` case. A nondeterministic hole + -- (havoc) is declared with no inputs and called with none. + let env ← read + let args := if deterministic then env.procInputs.map (fun (name, _) => FGLValue.var md name) else [] + let declaredOutputs := [("result", retTy)] + mkGradedCall md hv args declaredOutputs fun rv => do + let M_k ← checkProducers rest retTy grade + match rest with + | [] => pure (.produce md rv) + | _ => pure M_k + | _ => do + let v ← checkValue stmt retTy + match rest with + | [] => pure (.produce md v) + | _ => failure + +/-- Bind a list of arguments as producers via nested varDecls. + Each arg is checked as a producer, bound to a fresh var, and the + continuation receives the list of bound values. -/ +partial def bindArgs (md : Md) (args : List StmtExprMd) (params : List (String × HighType)) + (grade : Grade) (cont : List FGLValue → ElabM FGLProducer) : ElabM FGLProducer := do + match args, params with + | [], _ => cont [] + | arg :: restArgs, (_, pty) :: restParams => do + let M_arg ← checkProducer arg [] pty grade + let x_arg ← freshVar "arg" + let body ← extendEnv x_arg pty do + bindArgs md restArgs restParams grade fun restVars => + cont (.var md x_arg :: restVars) + pure (.varDecl md x_arg (eraseType pty) M_arg body) + | _ :: _, [] => failure + +/-- ⟦·⟧⇐ₚ (field write): +``` +D :: Γ ⊢ (obj.f := v); k : A [fieldWrite] +├─ D_obj :: Γ ⊢ obj : C (C discovered by synthesis on obj) +├─ fieldType(C, f) = T +├─ D_v :: Γ ⊢ v : T +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ varDecl x_obj ⟦C⟧ M_obj (varDecl x_v ⟦T⟧ M_v (varDecl h' Heap M_update M_k)) ⇐ ⟦A⟧ & d [varDecl] +├─ ⟦D_obj⟧⇐ₚ :: ⟦Γ⟧ ⊢ M_obj ⇐ ⟦C⟧ & d +└─ ⟦Γ⟧, x_obj:⟦C⟧ ⊢ varDecl x_v ⟦T⟧ M_v (varDecl h' Heap M_update M_k) ⇐ ⟦A⟧ & d [varDecl] + ├─ ⟦D_v⟧⇐ₚ :: ⟦Γ⟧, x_obj ⊢ M_v ⇐ ⟦T⟧ & d + └─ ⟦Γ⟧, x_obj, x_v ⊢ varDecl h' Heap M_update M_k ⇐ ⟦A⟧ & d [varDecl] + ├─ ⟦Γ⟧, x_obj, x_v ⊢ produce (functionCall updateField [$heap, x_obj, $field.C.f, functionCall box_T [x_v]]) ⇐ Heap & d [produce] + │ └─ ⟦Γ⟧, x_obj, x_v ⊢ functionCall updateField [$heap, x_obj, $field.C.f, functionCall box_T [x_v]] ⇐ Heap [subsumption] + │ ├─ ⟦Γ⟧, x_obj, x_v ⊢ functionCall updateField [$heap, x_obj, $field.C.f, functionCall box_T [x_v]] ⇒ Heap [functionCall] + │ │ ├─ ⟦Γ⟧, x_obj, x_v ⊢ $heap ⇐ Heap [subsumption] + │ │ │ ├─ ⟦Γ⟧, x_obj, x_v ⊢ $heap ⇒ Heap [var] + │ │ │ └─ Heap ≤ Heap ↦ id + │ │ ├─ ⟦Γ⟧, x_obj, x_v ⊢ x_obj ⇐ Composite [subsumption] + │ │ │ ├─ ⟦Γ⟧, x_obj, x_v ⊢ x_obj ⇒ Composite [var] + │ │ │ └─ Composite ≤ Composite ↦ id + │ │ ├─ ⟦Γ⟧, x_obj, x_v ⊢ functionCall $field.C.f [] ⇐ Field [subsumption] + │ │ │ ├─ ⟦Γ⟧, x_obj, x_v ⊢ functionCall $field.C.f [] ⇒ Field [functionCall] + │ │ │ └─ Field ≤ Field ↦ id + │ │ └─ ⟦Γ⟧, x_obj, x_v ⊢ functionCall box_T [x_v] ⇐ Box [subsumption] + │ │ ├─ ⟦Γ⟧, x_obj, x_v ⊢ functionCall box_T [x_v] ⇒ Box [functionCall] + │ │ │ └─ ⟦Γ⟧, x_obj, x_v ⊢ x_v ⇐ ⟦T⟧ [subsumption] + │ │ │ ├─ ⟦Γ⟧, x_obj, x_v ⊢ x_v ⇒ ⟦T⟧ [var] + │ │ │ └─ ⟦T⟧ ≤ ⟦T⟧ ↦ id + │ │ └─ Box ≤ Box ↦ id + │ └─ Heap ≤ Heap ↦ id + └─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧, x_obj, x_v, h':Heap ⊢ M_k ⇐ ⟦A⟧ & d +``` +-/ +partial def checkAssignFieldWrite (md : Md) (obj : StmtExprMd) (field : Identifier) + (value : StmtExprMd) (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + -- Write rule for field access: Γ ⊢ e.l := v ; k with v ⇐ A_l (the DECLARED field + -- type). We check the RHS against `A_l` so the coercion mechanism inserts the right + -- boxing — we trust the user's annotation and let coercion handle impedance. `A_l` is + -- looked up from the receiver's composite type; `Any` is the genuine fallthrough when + -- the receiver isn't a known composite (e.g. `self`/dynamic). + -- The heap is threaded by heapParameterizationPass, which consumes a BARE field-write + -- `.Assign [.Field obj f] rhs` and rewrites it into `updateField($heap, obj, $field.C.f, + -- box_(rhs))`. We emit it DIRECTLY as `.assign (.fieldAccess obj f) rhs` — no + -- intermediate fresh temp (a `val$N` temp collides with a same-named field/param, e.g. + -- `self.val = val`). This matches the proven pipeline's single `self#val := val`. + let (ov, objTy) ← synthValue obj + let fieldTy ← + match objTy with + | .UserDefined cls => + match ← (do match (← read).typeEnv.classFields[cls.text]? with + | some fields => pure (fields.find? (fun (n, _) => n == field.text)) + | none => pure none) with + | some (_, ty) => pure ty + | none => pure (.TCore "Any") + | _ => pure (.TCore "Any") + let M_v ← checkProducer value [] fieldTy grade + let M_k ← checkProducers rest retTy grade + pure (.assign md (.fieldAccess md ov field.text) M_v M_k) + +/-- Dispatches on LHS to get assignee, then on RHS form. -/ +partial def checkAssign (target : VariableMd) (value : StmtExprMd) (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + let md := target.source + match target.val with + | .Field obj field => checkAssignFieldWrite md obj field value rest retTy grade + -- A `.Declare` target is a local-variable declaration with an initializer + -- (`var x : T := value`); route it to `checkProducerVarDecl` with the initializer. + | .Declare ⟨nameId, typeMd⟩ => checkProducerVarDecl md nameId typeMd (some value) rest retTy grade + | .Local id => + let .variable targetTy := (← lookupEnv id.text) | failure + match value.val with + | .StaticCall callee args => checkAssignStaticCall md id.text targetTy callee args rest retTy grade + | .New classId => checkAssignNew md id.text targetTy classId rest retTy grade + | _ => checkAssignVar md id.text targetTy value rest retTy grade + +/-- ⟦·⟧⇐ₚ (assign, generic RHS): +``` +D :: Γ ⊢ (x := e); k : A [assign] +├─ D_e :: Γ ⊢ e : Γ(x) +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ assign x M M_k ⇐ ⟦A⟧ & d [assign] +├─ ⟦D_e⟧⇐ₚ :: ⟦Γ⟧ ⊢ M ⇐ ⟦Γ(x)⟧ & d +└─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧ ⊢ M_k ⇐ ⟦A⟧ & d +``` +-/ +partial def checkAssignVar (md : Md) (targetName : String) (targetTy : HighType) + (value : StmtExprMd) (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + let M ← checkProducer value [] targetTy grade + let M_k ← checkProducers rest retTy grade + pure (.assign md (.var md targetName) M M_k) + +/-- ⟦·⟧⇐ₚ (assign + call): +``` +D :: Γ ⊢ (x := f(e₁,...,eₙ)); k : A [assign] +├─ D_e :: Γ ⊢ f(e₁,...,eₙ) : Γ(x) [call] +│ ├─ (f : (A₁,...,Aₙ) → B) ∈ Γ +│ └─ Dᵢ :: Γ ⊢ eᵢ : Aᵢ (for i = 1,...,n) +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ varDecl x₁ ⟦A₁⟧ M₁ (...(varDecl xₙ ⟦Aₙ⟧ Mₙ (procedureCall f (pre ++ [x₁,...,xₙ]) outs (assign x (produce c(rv)) M_k)))) ⇐ ⟦A⟧ & d [varDecl] +├─ ⟦D₁⟧⇐ₚ :: ⟦Γ⟧ ⊢ M₁ ⇐ ⟦A₁⟧ & d +├─ ... [varDecl] +├─ ⟦Dₙ⟧⇐ₚ :: ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ₋₁:⟦Aₙ₋₁⟧ ⊢ Mₙ ⇐ ⟦Aₙ⟧ & d +└─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧ ⊢ procedureCall f (pre ++ [x₁,...,xₙ]) outs (assign x (produce c(rv)) M_k) ⇐ ⟦A⟧ & d [producerSubsumption] + ├─ (f : (⟦A₁⟧,...,⟦Aₙ⟧) → ⟦B⟧ & d') ∈ ⟦Γ⟧ + ├─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧ ⊢ xᵢ ⇐ ⟦Aᵢ⟧ [subsumption] + │ ├─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧ ⊢ xᵢ ⇒ ⟦Aᵢ⟧ [var] + │ └─ ⟦Aᵢ⟧ ≤ ⟦Aᵢ⟧ ↦ id + ├─ d' ≤ d ↦ (pre, outs) where (rv : ⟦B⟧) ∈ outs + └─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧, outs ⊢ assign x (produce c(rv)) M_k ⇐ ⟦A⟧ & (d'\d) [assign] + ├─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧, outs ⊢ produce c(rv) ⇐ ⟦Γ(x)⟧ & (d'\d) [produce] + │ └─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧, outs ⊢ c(rv) ⇐ ⟦Γ(x)⟧ [subsumption] + │ ├─ ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧, outs ⊢ rv ⇒ ⟦B⟧ [var] + │ └─ ⟦B⟧ ≤ ⟦Γ(x)⟧ ↦ c + └─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧, x₁:⟦A₁⟧,...,xₙ:⟦Aₙ⟧, outs ⊢ M_k ⇐ ⟦A⟧ & (d'\d) +``` +-/ +partial def checkAssignStaticCall (md : Md) (targetName : String) (targetTy : HighType) + (callee : Identifier) (args : List StmtExprMd) + (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + let sig ← lookupFuncSig callee.text + elaborateCall md callee args grade fun rv residual => do + let coerced := applySubtype rv (eraseType sig.returnType) (eraseType targetTy) + let M_k ← checkProducers rest retTy residual + pure (.assign md (.var md targetName) (.produce md coerced) M_k) + +/-- ⟦·⟧⇐ₚ (assign + new): +``` +D :: Γ ⊢ (x := new C); k : A [assign] +├─ D_e :: Γ ⊢ new C : Γ(x) [new] +│ └─ C is a class ∈ Γ +└─ D_k :: Γ ⊢ k : A + + ↦ + +⟦D⟧⇐ₚ :: ⟦Γ⟧ ⊢ varDecl h' Heap (produce (functionCall increment [$heap])) (assign x (produce c(functionCall MkComposite [functionCall Heap..nextReference! [$heap], functionCall C_TypeTag []])) M_k) ⇐ ⟦A⟧ & d [varDecl] +├─ ⟦Γ⟧ ⊢ produce (functionCall increment [$heap]) ⇐ Heap & d [produce] +│ └─ ⟦Γ⟧ ⊢ functionCall increment [$heap] ⇐ Heap [subsumption] +│ ├─ ⟦Γ⟧ ⊢ functionCall increment [$heap] ⇒ Heap [functionCall] +│ │ └─ ⟦Γ⟧ ⊢ $heap ⇐ Heap [subsumption] +│ │ ├─ ⟦Γ⟧ ⊢ $heap ⇒ Heap [var] +│ │ └─ Heap ≤ Heap ↦ id +│ └─ Heap ≤ Heap ↦ id +└─ ⟦Γ⟧, h':Heap ⊢ assign x (produce c(functionCall MkComposite [functionCall Heap..nextReference! [$heap], functionCall C_TypeTag []])) M_k ⇐ ⟦A⟧ & d [assign] + ├─ ⟦Γ⟧, h':Heap ⊢ produce c(functionCall MkComposite [...]) ⇐ ⟦Γ(x)⟧ & d [produce] + │ └─ ⟦Γ⟧, h':Heap ⊢ c(functionCall MkComposite [...]) ⇐ ⟦Γ(x)⟧ [subsumption] + │ ├─ ⟦Γ⟧, h':Heap ⊢ functionCall MkComposite [functionCall Heap..nextReference! [$heap], functionCall C_TypeTag []] ⇒ Composite [functionCall] + │ │ ├─ ⟦Γ⟧, h':Heap ⊢ functionCall Heap..nextReference! [$heap] ⇐ int [subsumption] + │ │ │ ├─ ⟦Γ⟧, h':Heap ⊢ functionCall Heap..nextReference! [$heap] ⇒ int [functionCall] + │ │ │ │ └─ ⟦Γ⟧, h':Heap ⊢ $heap ⇐ Heap [subsumption] + │ │ │ │ ├─ ⟦Γ⟧, h':Heap ⊢ $heap ⇒ Heap [var] + │ │ │ │ └─ Heap ≤ Heap ↦ id + │ │ │ └─ int ≤ int ↦ id + │ │ └─ ⟦Γ⟧, h':Heap ⊢ functionCall C_TypeTag [] ⇐ TypeTag [subsumption] + │ │ ├─ ⟦Γ⟧, h':Heap ⊢ functionCall C_TypeTag [] ⇒ TypeTag [functionCall] + │ │ └─ TypeTag ≤ TypeTag ↦ id + │ └─ Composite ≤ ⟦Γ(x)⟧ ↦ c + └─ ⟦D_k⟧⇐ₚ* :: ⟦Γ⟧, h':Heap ⊢ M_k ⇐ ⟦A⟧ & d +``` +-/ +partial def checkAssignNew (md : Md) (targetName : String) (targetTy : HighType) + (classId : Identifier) (rest : List StmtExprMd) (retTy : HighType) (grade : Grade) : ElabM FGLProducer := do + -- Exceptions-only: allocation is heapParameterizationPass's job. It consumes a BARE + -- `.New classId` node and rewrites it into a heap-allocated `MkComposite` with a fresh + -- reference + threads `$heap`. So here we emit the bare `.new` value and assign it. + let _ := targetTy + let M_k ← checkProducers rest retTy grade + pure (.assign md (.var md targetName) (.produce md (.new md classId.text)) M_k) + +end + +/-! ## Grade Inference + +Grade inference is coinductive over the call graph. For each procedure, +try elaboration at successively higher grades until one succeeds. When a +callee's grade exceeds the trial grade, the left residual is undefined, +elaboration fails (returns `none`), and the next grade is tried. The +finite lattice guarantees convergence. -/ + +/-- Try elaborating a procedure body at each grade in order. Returns the + first grade that succeeds, or `heapErr` as fallback. -/ +partial def tryGrades (callee : String) (env : ElabEnv) (body : StmtExprMd) + (retTy : HighType) (grades : List Grade) : Option Grade := + match grades with + | [] => some .err -- Exceptions-only: top of the lattice we use is .err + | g :: rest => + let st : ElabState := { freshCounter := 0 } + let trialEnv := { env with procGrades := env.procGrades.insert callee g } + match (checkProducer body [] retTy g).run trialEnv |>.run st with + | some _ => some g + | none => tryGrades callee env body retTy rest + +/-! ## Projection (Destination Passing Style) + +Projection reverses elaboration: GFGL derivations → Laurel derivations. +Uses a writer monad that accumulates declarations (hoisted to procedure top). + +``` +⟦D⟧ₓ⁻¹ : (⟦Γ⟧ ⊢ M ⇐ ⟦A⟧ & d) → ∃e⃗. (Γ, x : A ⊢ e⃗ : TVoid) +``` +-/ + +structure ProjM (α : Type) where + run : α × List StmtExprMd + +instance : Monad ProjM where + pure a := ⟨(a, [])⟩ + bind ma f := let (a, d1) := ma.run; let (b, d2) := (f a).run; ⟨(b, d1 ++ d2)⟩ + +def projDecl (decl : StmtExprMd) : ProjM Unit := ⟨((), [decl])⟩ + +def projectValue : FGLValue → StmtExprMd + | .litInt md n => mkLaurel md (.LiteralInt n) + | .litBool md b => mkLaurel md (.LiteralBool b) + | .litString md s => mkLaurel md (.LiteralString s) + | .litDecimal md d => mkLaurel md (.LiteralDecimal d) + | .var md name => mkLaurel md (.Var (.Local { text := name })) + | .fromInt md v => mkLaurel md (.StaticCall { text := "from_int" } [projectValue v]) + | .fromStr md v => mkLaurel md (.StaticCall { text := "from_str" } [projectValue v]) + | .fromBool md v => mkLaurel md (.StaticCall { text := "from_bool" } [projectValue v]) + | .fromFloat md v => mkLaurel md (.StaticCall { text := "from_float" } [projectValue v]) + | .fromComposite md v => + -- kbd has no structural `Composite → Any` constructor (from_ClassInstance takes + -- (classname, attr-dict), a different representation). Use the value-PRESERVING + -- uninterpreted stub `Any..from_Composite(v)` so the term type-checks (Composite⇒Any) + -- and stays sound-but-uninterpreted, rather than discarding `v` into an empty + -- from_ClassInstance("", {}) (which both loses the value and mis-types). + mkLaurel md (.StaticCall { text := "Any..from_Composite" } [projectValue v]) + | .fromListAny md v => mkLaurel md (.StaticCall { text := "from_ListAny" } [projectValue v]) + | .fromDictStrAny md v => mkLaurel md (.StaticCall { text := "from_DictStrAny" } [projectValue v]) + | .fromNone md => mkLaurel md (.StaticCall { text := "from_None" } []) + | .fieldAccess md obj f => mkLaurel md (.Var (.Field (projectValue obj) { text := f })) + | .staticCall md name args => mkLaurel md (.StaticCall { text := name } (args.map projectValue)) + | .new md className => mkLaurel md (.New { text := className }) + +/-- Project an FGL value used as an assignment destination into a `VariableMd`. + Assignment/declaration destinations are always variables (`.var`) or, for + field writes, field selections (`.fieldAccess`). -/ +def projectVarTarget : FGLValue → VariableMd + | .var md name => ⟨.Local { text := name }, md⟩ + | .fieldAccess md obj f => ⟨.Field (projectValue obj) { text := f }, md⟩ + | v => ⟨.Local default, v.getMd⟩ + +mutual + +/-- Destination-passing projection. +``` +⟦·⟧ₓ⁻¹ : (⟦Γ⟧ ⊢ M ⇔ ⟦A⟧ & d) → ∃e⃗. (Γ, x : A ⊢ e⃗ : TVoid) +⟦·⟧⁻¹ : (⟦Γ⟧ ⊢ V ⇔ ⟦A⟧) → ∃e. (Γ ⊢ e : A) +``` +Dispatches to per-constructor helpers. -/ +partial def proj (dest : Option VariableMd) : FGLProducer → ProjM (List StmtExprMd) + | .produce md v => projProduce dest md v + | .varDecl md name ty init body => projVarDecl dest md name ty init body + | .assign md target val body => projAssign dest md target val body + | .ifThenElse md cond thn els after => projIfThenElse dest md cond thn els after + | .whileLoop md cond body after => projWhileLoop dest md cond body after + | .procedureCall md callee args outputs body => projProcedureCall dest md callee args outputs body + | .assert md cond body => projAssert dest md cond body + | .assume md cond body => projAssume dest md cond body + | .labeledBlock md label body after => projLabeledBlock dest md label body after + | .exit md label => projExit md label + | .skip => projSkip + +/-- projProduce: +``` +D :: ⟦Γ⟧ ⊢ produce V ⇐ ⟦A⟧ & d [produce] +└─ D_V :: ⟦Γ⟧ ⊢ V ⇐ ⟦A⟧ + + ↦ (destination x : A present) + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ (x := e_V); skip : TVoid [assign] +├─ ⟦D_V⟧⁻¹ :: Γ ⊢ e_V : A +└─ Γ ⊢ skip : TVoid [skip] +``` +With no destination (a `TVoid` command — the body, or a control-flow path with +no `x : A` in context), the produced value has nowhere to go and projects to the +empty statement list. -/ +partial def projProduce (dest : Option VariableMd) (md : Md) (v : FGLValue) : ProjM (List StmtExprMd) := + match dest with + | some d => pure [mkLaurel md (.Assign [d] (projectValue v))] + | none => pure [] + +/-- projVarDecl: +``` +D :: ⟦Γ⟧ ⊢ varDecl y T M N ⇐ ⟦A⟧ & d +├─ D_M :: ⟦Γ⟧ ⊢ M ⇐ T & d +└─ D_N :: ⟦Γ⟧, y:T ⊢ N ⇐ ⟦A⟧ & d + + ↦ + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ (var y : T; e⃗_M; e⃗_N) : TVoid [varDecl] +├─ ⟦D_M⟧ᵧ⁻¹ :: Γ, y : T ⊢ e⃗_M : TVoid +└─ ⟦D_N⟧ₓ⁻¹ :: Γ, x : A, y : T ⊢ e⃗_N : TVoid +``` +-/ +partial def projVarDecl (dest : Option VariableMd) (md : Md) (name : String) (ty : LowType) + (init : FGLProducer) (body : FGLProducer) : ProjM (List StmtExprMd) := do + let nameVar : VariableMd := ⟨.Local { text := name }, md⟩ + let decl := mkLaurel md (.Var (.Declare { name := { text := name }, type := mkHighTypeMd md (liftType ty) })) + projDecl decl + let initStmts ← proj (some nameVar) init + let bodyStmts ← proj dest body + pure (initStmts ++ bodyStmts) + +/-- projAssign: +``` +D :: ⟦Γ⟧ ⊢ assign y M K ⇐ ⟦A⟧ & d +├─ D_M :: ⟦Γ⟧ ⊢ M ⇐ ⟦Γ(y)⟧ & d +└─ D_K :: ⟦Γ⟧ ⊢ K ⇐ ⟦A⟧ & d + + ↦ + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ (e⃗_M; e⃗_K) : TVoid [assign] +├─ ⟦D_M⟧ᵧ⁻¹ :: Γ, y : Γ(y) ⊢ e⃗_M : TVoid +└─ ⟦D_K⟧ₓ⁻¹ :: Γ, x : A ⊢ e⃗_K : TVoid +``` +-/ +partial def projAssign (dest : Option VariableMd) (_md : Md) (target : FGLValue) + (val : FGLProducer) (body : FGLProducer) : ProjM (List StmtExprMd) := do + let valStmts ← proj (some (projectVarTarget target)) val + let bodyStmts ← proj dest body + pure (valStmts ++ bodyStmts) + +/-- projIfThenElse: +``` +D :: ⟦Γ⟧ ⊢ ifThenElse V M N K ⇐ ⟦A⟧ & d +├─ D_V :: ⟦Γ⟧ ⊢ V ⇐ bool +├─ D_M :: ⟦Γ⟧ ⊢ M ⇐ ⟦A⟧ & d +├─ D_N :: ⟦Γ⟧ ⊢ N ⇐ ⟦A⟧ & d +└─ D_K :: ⟦Γ⟧ ⊢ K ⇐ ⟦A⟧ & d + + ↦ + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ (if e_V then {e⃗_M} else {e⃗_N}); e⃗_K : TVoid [if] +├─ ⟦D_V⟧⁻¹ :: Γ ⊢ e_V : bool +├─ ⟦D_M⟧ₓ⁻¹ :: Γ, x : A ⊢ e⃗_M : TVoid +├─ ⟦D_N⟧ₓ⁻¹ :: Γ, x : A ⊢ e⃗_N : TVoid +└─ ⟦D_K⟧ₓ⁻¹ :: Γ, x : A ⊢ e⃗_K : TVoid +``` +-/ +partial def projIfThenElse (dest : Option VariableMd) (md : Md) (cond : FGLValue) + (thn els after : FGLProducer) : ProjM (List StmtExprMd) := do + let thnStmts ← proj dest thn + let elsStmts ← proj dest els + -- kbd-forced: the Laurel resolver types BOTH `if` branches and rejects a value + -- branch paired with an empty (void) branch ('if' branches have incompatible + -- types 'int' and 'void'). When one branch is empty, emit a one-armed `if` + -- (kbd `IfThenElse` takes `elseBranch : Option`). For an empty THEN, flip the + -- condition with `Any_to_bool(PNot ·)` — conditions are Any-typed here, exactly + -- as the proven PythonToLaurel pipeline wraps them. + let ite : StmtExprMd := + match thnStmts.isEmpty, elsStmts.isEmpty with + | true, true => mkLaurel md (.Block [] none) + | false, true => mkLaurel md (.IfThenElse (projectValue cond) (mkLaurel md (.Block thnStmts none)) none) + | true, false => + -- Empty THEN: emit only the else under the negated condition. `cond` was checked + -- at `.TBool`, so it projects to a bool — negate with the boolean `PrimitiveOp .Not` + -- (NOT `Any_to_bool(PNot ·)`, which assumes an Any-typed cond and yields an + -- arrow-type mismatch when cond is already bool, e.g. `if x > 10: pass`). + let negCond := mkLaurel md (.PrimitiveOp .Not [projectValue cond]) + mkLaurel md (.IfThenElse negCond (mkLaurel md (.Block elsStmts none)) none) + | false, false => + mkLaurel md (.IfThenElse (projectValue cond) (mkLaurel md (.Block thnStmts none)) + (some (mkLaurel md (.Block elsStmts none)))) + let afterStmts ← proj dest after + pure ([ite] ++ afterStmts) + +/-- projWhileLoop: +``` +D :: ⟦Γ⟧ ⊢ whileLoop V M K ⇐ ⟦A⟧ & d +├─ D_V :: ⟦Γ⟧ ⊢ V ⇐ bool +├─ D_M :: ⟦Γ⟧ ⊢ M ⇐ ⟦A⟧ & d +└─ D_K :: ⟦Γ⟧ ⊢ K ⇐ ⟦A⟧ & d + + ↦ + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ (while e_V {e⃗_M}); e⃗_K : TVoid [while] +├─ ⟦D_V⟧⁻¹ :: Γ ⊢ e_V : bool +├─ ⟦D_M⟧ₓ⁻¹ :: Γ, x : A ⊢ e⃗_M : TVoid +└─ ⟦D_K⟧ₓ⁻¹ :: Γ, x : A ⊢ e⃗_K : TVoid +``` +-/ +partial def projWhileLoop (dest : Option VariableMd) (md : Md) (cond : FGLValue) + (body after : FGLProducer) : ProjM (List StmtExprMd) := do + let bodyStmts ← proj dest body + let bodyBlock := mkLaurel md (.Block bodyStmts none) + let loop := mkLaurel md (.While (projectValue cond) [] none bodyBlock) + let afterStmts ← proj dest after + pure ([loop] ++ afterStmts) + +/-- projProcedureCall: +``` +D :: ⟦Γ⟧ ⊢ procedureCall f [Vᵢ] [outⱼ : Tⱼ] K ⇐ ⟦A⟧ & d +├─ D_Vᵢ :: ⟦Γ⟧ ⊢ Vᵢ ⇐ ⟦Aᵢ⟧ +└─ D_K :: ⟦Γ⟧, outⱼ:Tⱼ ⊢ K ⇐ ⟦A⟧ & d + + ↦ + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ (var out₁:T₁; ...; var outₙ:Tₙ; (out₁,...,outₙ) := f(e_Vᵢ); e⃗_K) : TVoid [call] +├─ ⟦D_Vᵢ⟧⁻¹ :: Γ ⊢ e_Vᵢ : Aᵢ +└─ ⟦D_K⟧ₓ⁻¹ :: Γ, x : A, out₁:T₁, ..., outₙ:Tₙ ⊢ e⃗_K : TVoid +``` +-/ +partial def projProcedureCall (dest : Option VariableMd) (md : Md) (callee : String) + (args : List FGLValue) (outputs : List (String × LowType)) (body : FGLProducer) : ProjM (List StmtExprMd) := do + for (n, ty) in outputs do + projDecl (mkLaurel md (.Var (.Declare { name := { text := n }, type := mkHighTypeMd md (liftType ty) }))) + let targets : List VariableMd := outputs.map fun (n, _) => ⟨.Local { text := n }, md⟩ + let call := mkLaurel md (.Assign targets (mkLaurel md (.StaticCall { text := callee } (args.map projectValue)))) + let bodyStmts ← proj dest body + pure ([call] ++ bodyStmts) + +/-- projAssert: +``` +D :: ⟦Γ⟧ ⊢ assert V K ⇐ ⟦A⟧ & d +├─ D_V :: ⟦Γ⟧ ⊢ V ⇐ bool +└─ D_K :: ⟦Γ⟧ ⊢ K ⇐ ⟦A⟧ & d + + ↦ + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ (assert e_V); e⃗_K : TVoid [assert] +├─ ⟦D_V⟧⁻¹ :: Γ ⊢ e_V : bool +└─ ⟦D_K⟧ₓ⁻¹ :: Γ, x : A ⊢ e⃗_K : TVoid +``` +-/ +partial def projAssert (dest : Option VariableMd) (md : Md) (cond : FGLValue) + (body : FGLProducer) : ProjM (List StmtExprMd) := do + let bodyStmts ← proj dest body + pure ([mkLaurel md (.Assert { condition := projectValue cond })] ++ bodyStmts) + +/-- projAssume: +``` +D :: ⟦Γ⟧ ⊢ assume V K ⇐ ⟦A⟧ & d +├─ D_V :: ⟦Γ⟧ ⊢ V ⇐ bool +└─ D_K :: ⟦Γ⟧ ⊢ K ⇐ ⟦A⟧ & d + + ↦ + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ (assume e_V); e⃗_K : TVoid [assume] +├─ ⟦D_V⟧⁻¹ :: Γ ⊢ e_V : bool +└─ ⟦D_K⟧ₓ⁻¹ :: Γ, x : A ⊢ e⃗_K : TVoid +``` +-/ +partial def projAssume (dest : Option VariableMd) (md : Md) (cond : FGLValue) + (body : FGLProducer) : ProjM (List StmtExprMd) := do + let bodyStmts ← proj dest body + pure ([mkLaurel md (.Assume (projectValue cond))] ++ bodyStmts) + +/-- projLabeledBlock: +``` +D :: ⟦Γ⟧ ⊢ labeledBlock l M K ⇐ ⟦A⟧ & d +├─ D_M :: ⟦Γ⟧, l ⊢ M ⇐ ⟦A⟧ & d +└─ D_K :: ⟦Γ⟧ ⊢ K ⇐ ⟦A⟧ & d + + ↦ + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ {e⃗_M}_l; e⃗_K : TVoid [labeledBlock] +├─ ⟦D_M⟧ₓ⁻¹ :: Γ, x : A, l ⊢ e⃗_M : TVoid +└─ ⟦D_K⟧ₓ⁻¹ :: Γ, x : A ⊢ e⃗_K : TVoid +``` +-/ +partial def projLabeledBlock (dest : Option VariableMd) (md : Md) (label : String) + (body after : FGLProducer) : ProjM (List StmtExprMd) := do + let bodyStmts ← proj dest body + let bodyBlock := mkLaurel md (.Block bodyStmts (some label)) + let afterStmts ← proj dest after + pure ([bodyBlock] ++ afterStmts) + +/-- projExit: +``` +D :: ⟦Γ⟧ ⊢ exit l ⇐ ⟦A⟧ & d + + ↦ + +⟦D⟧ₓ⁻¹ :: Γ, x : A ⊢ exit l : TVoid [exit] +└─ l ∈ Γ +``` +-/ +partial def projExit (md : Md) (label : String) : ProjM (List StmtExprMd) := + pure [mkLaurel md (.Exit label)] + +/-- projSkip: +``` +⟦skip⟧ₓ⁻¹ :: Γ, x : A ⊢ skip : TVoid [skip] +``` +-/ +partial def projSkip : ProjM (List StmtExprMd) := pure [] + +end + +/-- Run projection of a procedure body. The body is a command (`TVoid`), so it + has no destination: its return value reaches `LaurelResult` only through the + explicit `LaurelResult := e` assignments Translation emits for `return e`, not + through a tail value. Declarations hoisted to top. -/ +def projectProducer (prod : FGLProducer) : List StmtExprMd := + let (stmts, decls) := (proj none prod).run + decls ++ stmts + +/-- Run projection, return as a block. -/ +def projectBody (md : Md) (prod : FGLProducer) : StmtExprMd := + mkLaurel md (.Block (projectProducer prod) none) + +/-! ## Entry Point + +`fullElaborate` orchestrates both passes. Pass 1 iterates to a fixpoint on +grades. Pass 2 elaborates each procedure at its final grade and projects +back to Laurel. Also emits auxiliary datatypes (TypeTag, Composite, Field, +Box) and hole procedure declarations needed by the output program. -/ + +/-- Entry point: elaborates a Laurel program. Returns the elaborated program + and a list of procedure names that failed to elaborate (emitted unchanged). -/ +def fullElaborate (program : Laurel.Program) (runtime : Laurel.Program := default) (initialGrades : Std.HashMap String Grade := {}) : Except String (Laurel.Program × List String) := do + let typeEnv := buildElabEnvFromProgram program runtime + let baseEnv : ElabEnv := { typeEnv := typeEnv, program := program, runtime := runtime } + + -- PASS 1: Coinductive fixpoint iteration + let mut knownGrades : Std.HashMap String Grade := initialGrades + let mut changed := true + while changed do + changed := false + for proc in program.staticProcedures do + let bodyOpt := match proc.body with + | .Transparent b => some b + | .Opaque _ (some impl) _ => some impl + | _ => none + match bodyOpt with + | some bodyExpr => + let extEnv := (proc.inputs ++ proc.outputs).foldl + (fun (e : ElabTypeEnv) p => { e with names := e.names.insert p.name.text (.variable p.type.val) }) typeEnv + let inputList := proc.inputs.map fun p => (p.name.text, p.type.val) + let procEnv : ElabEnv := { baseEnv with typeEnv := extEnv, procGrades := knownGrades, procInputs := inputList } + -- The body is a command (DPS): checked at TVoid, not the return type. The + -- return value flows only through explicit `LaurelResult := e` assigns. + match tryGrades proc.name.text procEnv bodyExpr .TVoid [.pure, .proc, .err] with + | some g => + -- A proc with >1 output carries a trailing `maybe_except` (it can throw), + -- so its grade is at least `.err`. Without this join a caller elaborated at + -- `.pure`/`.proc` cannot call it (leftResidual .err _ = none) and the call is + -- silently dropped. (v2 verbatim; independent of heap removal.) + let g := if proc.outputs.length > 1 then Grade.join g .err else g + if knownGrades[proc.name.text]? != some g then + knownGrades := knownGrades.insert proc.name.text g + changed := true + | none => pure () + | none => pure () + + -- PASS 2: Elaborate each proc with final grades + let mut procs : List Laurel.Procedure := [] + let mut allHoles : List (String × Bool × List (String × HighType) × HighType) := [] + let mut elabFailures : List String := [] + let mut globalCounter : Nat := 0 + for proc in program.staticProcedures do + let bodyOpt2 : Option (StmtExprMd × Bool) := match proc.body with + | .Transparent b => some (b, false) + | .Opaque _ (some impl) _ => some (impl, true) + | _ => none + match bodyOpt2 with + | some (bodyExpr, isOpaque) => + let extEnv := (proc.inputs ++ proc.outputs).foldl + (fun (e : ElabTypeEnv) p => { e with names := e.names.insert p.name.text (.variable p.type.val) }) typeEnv + let inputList := proc.inputs.map fun p => (p.name.text, p.type.val) + let procEnv : ElabEnv := { baseEnv with typeEnv := extEnv, procGrades := knownGrades, procInputs := inputList } + let g := knownGrades[proc.name.text]?.getD .pure + let st : ElabState := { freshCounter := globalCounter } + -- Elaborate preconditions: a `requires` is a pure value of type bool, not an + -- effect-sequenced statement, so it elaborates with the value judgment + -- (checkValue) rather than the producer judgment. checkValue synthesizes the + -- term and applies subtyping coercions — from_int/from_str on argument + -- literals (the runtime operators take Any parameters) and Any_to_bool on the + -- Any-typed result — then projectValue yields the single Core expression. + -- Holes are collected as for bodies. + let mut elabPreconditions : List Condition := [] + for pre in proc.preconditions do + let preSt : ElabState := { freshCounter := globalCounter } + match (checkValue pre.condition .TBool).run procEnv |>.run preSt with + | some (preVal, preSt') => + globalCounter := preSt'.freshCounter + let newHoles := (preSt'.usedHoles.map fun (name, det, outTy) => (name, det, inputList, outTy)).filter + (fun (n, _, _, _) => !allHoles.any (fun (n2, _, _, _) => n == n2)) + allHoles := allHoles ++ newHoles + elabPreconditions := elabPreconditions ++ [{ condition := ⟨(projectValue preVal).val, pre.condition.source⟩ }] + | none => elabPreconditions := elabPreconditions ++ [pre] + let proc := { proc with preconditions := elabPreconditions } + match (checkProducer bodyExpr [] .TVoid g).run procEnv |>.run st with + | some (fgl, st') => + globalCounter := st'.freshCounter + let newHoles := (st'.usedHoles.map fun (name, det, outTy) => (name, det, inputList, outTy)).filter + (fun (n, _, _, _) => !allHoles.any (fun (n2, _, _, _) => n == n2)) + allHoles := allHoles ++ newHoles + let projected := projectBody bodyExpr.source fgl + let md := bodyExpr.source + let errOutParam : Laurel.Parameter := { name := { text := "maybe_except" }, type := mkHighTypeMd md (.TCore "Error") } + let resultOutputs := proc.outputs.filter fun o => eraseType o.type.val != .TCore "Error" + let mkBody (b : StmtExprMd) : Laurel.Body := + if isOpaque then .Opaque [] (some b) [] else .Transparent b + match g with + | .err => + procs := procs ++ [{ proc with + outputs := resultOutputs ++ [errOutParam] + body := mkBody projected }] + | _ => + procs := procs ++ [{ proc with body := mkBody projected }] + | none => + elabFailures := elabFailures ++ [proc.name.text] + procs := procs ++ [proc] + | none => procs := procs ++ [proc] + -- Hole procs are emitted because grade inference may introduce them. + -- Everything else (heap, box types, auxiliary datatypes) is owned by downstream passes. + let holeProcs := allHoles.map fun (name, deterministic, inputs, outTy) => + let params := inputs.map fun (pName, pType) => + ({ name := { text := pName }, type := ⟨pType, none⟩ } : Laurel.Parameter) + let outputParam : Laurel.Parameter := { name := { text := "result" }, type := ⟨outTy, none⟩ } + { name := { text := name } + inputs := if deterministic then params else [] + outputs := [outputParam] + preconditions := [] + decreases := none + isFunctional := true + body := .Opaque [] none [] : Laurel.Procedure } + let result : Laurel.Program := + { program with + staticProcedures := holeProcs ++ procs } + pure (result, elabFailures) + +end +end Strata.FineGrainLaurel + diff --git a/Strata/Languages/FineGrainLaurel/FineGrainLaurel.dialect.st b/Strata/Languages/FineGrainLaurel/FineGrainLaurel.dialect.st new file mode 100644 index 0000000000..eb2448a1b4 --- /dev/null +++ b/Strata/Languages/FineGrainLaurel/FineGrainLaurel.dialect.st @@ -0,0 +1,213 @@ +// FineGrainLaurel Dialect: FGCBV (Fine-Grain Call-By-Value) with explicit polarity +// This dialect extends Laurel with separate Value and Producer categories, +// making polarity a representation-level invariant rather than a runtime predicate. +// +// Changes in this file are not automatically tracked by the build system. +// Modify FineGrainLaurel.lean (e.g. update its comment) to trigger a rebuild after changing this file. + +dialect FineGrainLaurel; +// Note: Not importing Laurel for now - FineGrainLaurel is self-contained + +// Import Laurel types for reuse +category LaurelType; +op intType : LaurelType => "int"; +op boolType : LaurelType => "bool"; +op realType : LaurelType => "real"; +op float64Type : LaurelType => "float64"; +op stringType : LaurelType => "string"; +op coreType (name: Ident): LaurelType => "Core " name; +op mapType (keyType: LaurelType, valueType: LaurelType): LaurelType => "Map " keyType " " valueType; +op compositeType (name: Ident): LaurelType => name; + +// =========================================================================== +// FGCBV Core: Separate Value and Producer categories +// =========================================================================== + +// Value category: inert terms (no effects, can be duplicated/discarded) +category Value; + +// Producer category: effectful terms (must be sequenced, single-use) +category Producer; + +// =========================================================================== +// Value Operators (Inert Terms) +// =========================================================================== + +// Literals +op valLiteralInt (n: Num): Value => n; +op valLiteralBool (b: Bool): Value => b; +op valLiteralReal (d: Decimal): Value => d; +op valLiteralString (s: Str): Value => s; + +// Variables +op valVar (name: Ident): Value => name; + +// Pure binary operations (no effects) +op valAdd (lhs: Value, rhs: Value): Value => @[prec(60), leftassoc] lhs " + " rhs; +op valSub (lhs: Value, rhs: Value): Value => @[prec(60), leftassoc] lhs " - " rhs; +op valMul (lhs: Value, rhs: Value): Value => @[prec(70), leftassoc] lhs " * " rhs; +op valDiv (lhs: Value, rhs: Value): Value => @[prec(70), leftassoc] lhs " / " rhs; +op valMod (lhs: Value, rhs: Value): Value => @[prec(70), leftassoc] lhs " % " rhs; + +// Pure comparison operations +op valEq (lhs: Value, rhs: Value): Value => @[prec(40)] lhs " == " rhs; +op valNeq (lhs: Value, rhs: Value): Value => @[prec(40)] lhs " != " rhs; +op valLt (lhs: Value, rhs: Value): Value => @[prec(40)] lhs " < " rhs; +op valLe (lhs: Value, rhs: Value): Value => @[prec(40)] lhs " <= " rhs; +op valGt (lhs: Value, rhs: Value): Value => @[prec(40)] lhs " > " rhs; +op valGe (lhs: Value, rhs: Value): Value => @[prec(40)] lhs " >= " rhs; + +// Pure logical operations +op valAnd (lhs: Value, rhs: Value): Value => @[prec(30), leftassoc] lhs " & " rhs; +op valOr (lhs: Value, rhs: Value): Value => @[prec(20), leftassoc] lhs " | " rhs; +op valNot (inner: Value): Value => @[prec(80)] "!" inner; + +// Pure unary operations +op valNeg (inner: Value): Value => @[prec(80)] "-" inner; + +// Field access (pure) +op valFieldAccess (obj: Value, field: Ident): Value => @[prec(90)] obj "#" field; + +// Parenthesis (for grouping) +op valParens (inner: Value): Value => "(" inner ")"; + +// =========================================================================== +// Producer Operators (Effectful Terms) +// =========================================================================== + +// Return a value (terminal producer) +op prodReturnValue (value: Value): Producer => @[prec(0)] "return " value:0; + +// Call a procedure (effectful) +op prodCall (callee: Ident, args: CommaSepBy Value): Producer => callee "(" args ")"; + +// Let-binding for producers (sequence effects) +// let x: ty = prod in body +op prodLetProd (var: Ident, ty: LaurelType, prod: Producer, body: Producer): Producer => + @[prec(0)] "let " var ": " ty " = " prod:0 " in " body:0; + +// Let-binding for values (introduce binding for a value) +// let x: ty = value in body +op prodLetValue (var: Ident, ty: LaurelType, value: Value, body: Producer): Producer => + @[prec(0)] "let " var ": " ty " = " value:0 " in " body:0; + +// Assignment (mutation) +op prodAssign (target: Value, value: Value, body: Producer): Producer => + @[prec(0)] target " := " value:0 ";" body:0; + +// Variable declaration with initialization +op prodVarDecl (name: Ident, ty: LaurelType, init: Value, body: Producer): Producer => + @[prec(0)] "var " name ": " ty " := " init:0 ";" body:0; + +// Conditional (if-then-else) +op prodIfThenElse (cond: Value, thenBranch: Producer, elseBranch: Producer): Producer => + @[prec(0)] "if " cond " then " thenBranch:0 " else " elseBranch:0; + +// Assert (specification) +op prodAssert (cond: Value, body: Producer): Producer => + @[prec(0)] "assert " cond:0 ";" body:0; + +// Assume (specification) +op prodAssume (cond: Value, body: Producer): Producer => + @[prec(0)] "assume " cond:0 ";" body:0; + +// While loop +category Invariant; +op invariant (cond: Value): Invariant => "invariant " cond:0; + +op prodWhile (cond: Value, invariants: Seq Invariant, body: Producer, after: Producer): Producer => + @[prec(0)] "while (" cond ")" invariants " " body:0 after:0; + +// Instantiation (heap allocation) +op prodNew (name: Ident, resultVar: Ident, ty: LaurelType, body: Producer): Producer => + @[prec(0)] "let " resultVar ": " ty " = new " name " in " body:0; + +// Call with error handling +op prodCallWithError (callee: Ident, args: CommaSepBy Value, + resultVar: Ident, errorVar: Ident, + resultTy: LaurelType, errorTy: LaurelType, + body: Producer): Producer => + @[prec(0)] "let [" resultVar ": " resultTy ", " errorVar ": " errorTy "] = " callee "(" args ") in " body:0; + +// Sequence (statement sequencing) +op prodSeq (first: Producer, second: Producer): Producer => + @[prec(5)] first:5 ";" second:5; + +// Block with multiple producers +op prodBlock (stmts: SemicolonSepBy Producer): Producer => + @[prec(1000)] "{" stmts "}"; + +// Exit a labelled block (break/continue control flow) +op prodExit (label: Str): Producer => "exit " label; + +// Labeled block (target of prodExit — models break/continue) +op prodLabeledBlock (label: Str, body: Producer): Producer => + @[prec(0)] "block " label " {" body:0 "}"; + +// =========================================================================== +// Top-level Declarations (reuse Laurel structure) +// =========================================================================== + +category Parameter; +op parameter (name: Ident, paramType: LaurelType): Parameter => name ":" paramType; + +category ReturnParameters; +op returnParameters (parameters: CommaSepBy Parameter): ReturnParameters => "returns" "(" parameters ")"; + +category ErrorSummary; +op errorSummary (msg: Str): ErrorSummary => "summary" msg; + +category RequiresClause; +op requiresClause (cond: Value, errorMessage: Option ErrorSummary): RequiresClause => "requires" cond:0 errorMessage; + +category EnsuresClause; +op ensuresClause (cond: Value, errorMessage: Option ErrorSummary): EnsuresClause => "ensures" cond:0 errorMessage; + +category ModifiesClause; +op modifiesClause (refs: CommaSepBy Value): ModifiesClause => "modifies" refs; + +category ProcedureBody; +op procedureBody (body: Producer): ProcedureBody => body:0; +op externalBody: ProcedureBody => "external"; + +category Procedure; +op procedure (name: Ident, parameters: CommaSepBy Parameter, + returnParameters: Option ReturnParameters, + requires: Seq RequiresClause, + ensures: Seq EnsuresClause, + modifies: Seq ModifiesClause, + body: Option ProcedureBody): Procedure => + "procedure " name "(" parameters ")" returnParameters requires ensures modifies body ";"; + +category Field; +op mutableField (name: Ident, fieldType: LaurelType): Field => "var " name ":" fieldType; +op immutableField (name: Ident, fieldType: LaurelType): Field => name ":" fieldType; + +category Extends; +op extends (parents: CommaSepBy Ident): Extends => "extends " parents; + +category Composite; +op composite (name: Ident, extending: Option Extends, fields: Seq Field, procedures: Seq Procedure): Composite => + "composite " name extending "{" fields procedures "}"; + +// =========================================================================== +// Value-Level Coercion Operators (Subtyping: infallible, value→value) +// =========================================================================== + +// Upcasts: inject concrete types into Any (pure injections into the sum type) +op valFromInt (inner: Value): Value => "from_int(" inner ")"; +op valFromStr (inner: Value): Value => "from_str(" inner ")"; +op valFromBool (inner: Value): Value => "from_bool(" inner ")"; +op valFromFloat (inner: Value): Value => "from_float(" inner ")"; +op valFromComposite (inner: Value): Value => "from_Composite(" inner ")"; +op valFromListAny (inner: Value): Value => "from_ListAny(" inner ")"; +op valFromDictStrAny (inner: Value): Value => "from_DictStrAny(" inner ")"; +op valFromNone: Value => "from_None()"; + +// =========================================================================== +// Top-level Declarations +// =========================================================================== + +// Top-level commands +op compositeCommand (composite: Composite): Command => composite; +op procedureCommand (procedure: Procedure): Command => procedure; diff --git a/Strata/Languages/FineGrainLaurel/FineGrainLaurel.lean b/Strata/Languages/FineGrainLaurel/FineGrainLaurel.lean new file mode 100644 index 0000000000..161047fef2 --- /dev/null +++ b/Strata/Languages/FineGrainLaurel/FineGrainLaurel.lean @@ -0,0 +1,24 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +-- FineGrainLaurel dialect definition, loaded from FineGrainLaurel.dialect.st +-- NOTE: Changes to FineGrainLaurel.dialect.st are not automatically tracked by the build system. +-- Update this file (e.g. this comment) to trigger a recompile after modifying FineGrainLaurel.dialect.st. +-- Last grammar change: added prodExit for break/continue control flow preservation. + +module + +public import StrataDDM.Integration.Lean +public meta import StrataDDM.Integration.Lean + +namespace Strata.FineGrainLaurel + +public section + +#load_dialect "Strata/Languages/FineGrainLaurel/FineGrainLaurel.dialect.st" + +#strata_gen FineGrainLaurel + +end diff --git a/Strata/Languages/Laurel/LaurelAST.lean b/Strata/Languages/Laurel/LaurelAST.lean index fb02d7cd59..a4aa027eb5 100644 --- a/Strata/Languages/Laurel/LaurelAST.lean +++ b/Strata/Languages/Laurel/LaurelAST.lean @@ -563,6 +563,27 @@ instance : BEq HighTypeMd where deriving instance BEq for HighType +/-- The abstract verdict of the proof-relevant subtyping judgment `coerce`. + GENERIC — it names the KIND of coercion, never a source-language runtime + function. A language frontend supplies a `realizeCoercion` (on `TypeLattice`) + that turns each verdict into a concrete term; the generic resolver never + mentions `from_int`, `Any..as_Composite!`, etc. + + - `refl` — types equal after unfolding (and the both-gradual case); identity. + - `inject A` — `A ≤ dynamic-top` (e.g. boxing a concrete value into the gradual + `Any`). `A` is the source type, so the realizer can pick the + right injector (int→from_int, a class→from_Composite, …). + - `project A`— `dynamic-top ≤ A` (e.g. unboxing/downcasting out of `Any` to a + concrete `A`). `A` is the target type. + - `upcast` — nominal composite ≤ ancestor composite; no runtime operation. + -- (no `truthify`: truthiness is realized via `project … bool` on the Python side) -/ +inductive Coercion where + | refl + | inject (source : HighType) + | project (target : HighType) + | upcast + deriving Inhabited + /-- Lookup tables threaded through subtyping/consistency checks. Built from the program's `TypeDefinition`s by the resolution pass: - `unfoldMap` maps an alias or constrained type's name to the type it @@ -583,6 +604,16 @@ deriving instance BEq for HighType structure TypeLattice where unfoldMap : Std.HashMap String HighTypeMd := {} extendingMap : Std.HashMap String (List String) := {} + /-- Type names that are treated as the gradual/dynamic top type (consistent with everything). + Set by language frontends (e.g. Python pipeline registers `"Any"` here). -/ + gradualTypes : Std.HashSet String := {} + /-- Caller-supplied REALIZER for an abstract `Coercion` verdict: maps the verdict + plus the term being coerced to a rewritten term carrying the concrete runtime + coercion call. `none` (the default, for native Laurel) means "identity" — no + coercion term is inserted. The Python frontend sets this to its box/unbox + vocabulary. This REALIZES an already-decided verdict; it makes no subtyping + decision, so it can never disagree with `coerce`. -/ + realizeCoercion : Option (Coercion → StmtExprMd → StmtExprMd) := none deriving Inhabited /-- Unfold aliases and constrained types to their underlying type. @@ -686,34 +717,102 @@ def isConsistent (ctx : TypeLattice) (a b : HighTypeMd) : Bool := | _, _ => let a' := ctx.unfold a let b' := ctx.unfold b - match a'.val, b'.val with - | .Unknown, _ | _, .Unknown => true - | .TCore _, _ | _, .TCore _ => true - | _, _ => highEq a' b' + let isGradual (t : HighType) := match t with + | .Unknown => true + | .TCore _ => true + | .UserDefined id => ctx.gradualTypes.contains id.text + | _ => false + if isGradual a'.val || isGradual b'.val then true + else highEq a' b' termination_by (SizeOf.sizeOf a) decreasing_by all_goals (cases a; cases b; try term_by_mem) cases t1; term_by_mem -/-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For our flat lattice - this collapses to `sub ~ sup ∨ sub <: sup` — the standard collapse. - - Used by rule `[⇐] Sub` (and every bespoke check rule). That single - choice is what makes the system *gradual*: an expression of type - `Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely - into any typed slot, and any expression flows freely into a slot of - type `Unknown`. Strict checking is applied between fully-known types - only. - - A previous iteration was synth-only with two *bivariantly-compatible* - wildcards: `Unknown` and `UserDefined`. The `UserDefined` carve-out was - load-bearing: no assignment, call argument, or comparison involving a - user type was ever rejected. The bidirectional design retires that - carve-out — user-defined types are now a regular participant in `<:`, - with `isSubtype` walking inheritance chains and unwrapping aliases - and constrained types to deliver real checking on user-defined code. -/ +/-- Test whether a type is gradual (consistent with everything): `Unknown`, any + `TCore _` (pending removal from the representation), or a frontend-registered + gradual `UserDefined` (e.g. Python `Any`). Mirrors the `isGradual` local inside + `isConsistent` so `coerce`'s DECISION classifies identically. -/ +private def TypeLattice.isGradualTop (ctx : TypeLattice) (t : HighType) : Bool := + match t with + | .Unknown => true + | .TCore _ => true + | .UserDefined id => ctx.gradualTypes.contains id.text + | _ => false + +/-- Test whether a type is the BOXABLE dynamic type — Python `Any`, which appears + BOTH as `.TCore "Any"` (the erased/runtime form, the common case) and as a + frontend-registered gradual `.UserDefined "Any"`. This is the SUBSET of + `isGradualTop` that has a runtime representation you can inject into / project + out of. `Unknown` and OTHER `.TCore _` (`Heap`, `Box`, `Error`, …) are gradual + *wildcards* (a synth gap, a hole, an unresolved accessor, internal plumbing): + they flow freely but carry NO box/unbox coercion, so a coercion against them is + `refl` (identity) — coercing them would wrap concrete-typed prelude code + (`ListAny..tail!` synth'd as `Unknown`) or heap plumbing in a bogus box/unbox. -/ +private def TypeLattice.isDynamicBoxable (ctx : TypeLattice) (t : HighType) : Bool := + match t with + | .TCore "Any" => true + | .UserDefined id => ctx.gradualTypes.contains id.text + | _ => false + +/-- PROOF-RELEVANT consistent subtyping: the ONE subtyping judgment. Returns the + abstract `Coercion` verdict witnessing `sub ≤ sup`, or `none` when unrelated. + Its `.isSome` is exactly the old boolean `isConsistentSubtype` (`isConsistent ∨ + isSubtype`), so every boolean call site is unchanged — but a check-mode site + that rebuilds the term can now obtain the witness and realize it. GENERIC: the + verdict names the KIND of coercion (inject/project/upcast/refl), never a runtime + function; the frontend's `realizeCoercion` turns it into a concrete term. + + The gradual cases split by WHICH gradual: only the boxable dynamic type (`Any`) + yields a runtime `inject`/`project`; a bare wildcard (`Unknown`/`TCore`) yields + `refl` (it flows with no coercion). The DECISION (`.isSome`) is unchanged either + way — both are `some` — so `isConsistentSubtype` matches the old boolean exactly. + + Case-for-case (mirrors `isConsistent ∨ isSubtype` for the decision): + - `MultiValuedExpr` (proc-output tuples): delegate to `isConsistent`; `refl`. + - equal after unfold → `refl`. + - `sup` is `Any`, `sub` concrete → `inject sub'` (box into the dynamic type). + - `sub` is `Any`, `sup` concrete → `project sup'` (unbox/downcast out of it). + - either side a bare wildcard (`Unknown`/`TCore`) → `refl` (gradual, no runtime op). + - both `UserDefined` with `sub`'s ancestors ∋ `sup` → `upcast` (nominal). -/ +def coerce (ctx : TypeLattice) (sub sup : HighTypeMd) : Option Coercion := + match sub.val, sup.val with + | .MultiValuedExpr _, .MultiValuedExpr _ => + if isConsistent ctx sub sup then some .refl else none + | _, _ => + let sub' := ctx.unfold sub + let sup' := ctx.unfold sup + let subBoxable := ctx.isDynamicBoxable sub'.val + let supBoxable := ctx.isDynamicBoxable sup'.val + -- `Unknown` is the only PURE wildcard: a synth gap / hole / unresolved accessor + -- with no runtime form, so a coercion against it is `refl` (it flows freely, no + -- box/unbox). NB this is narrower than `isGradualTop` (which also treats every + -- `.TCore _` as gradual for the *decision*): a concrete `.TCore` container like + -- `ListAny`/`DictStrAny` is NOT a wildcard — it is a real type that boxes/unboxes + -- against `Any` (`from_ListAny`/`Any..as_ListAny!`). Distinguishing them here is + -- what lets `Any ↔ ListAny` insert a witness while `Any ↔ ` stays `refl`. + let isWildcard (t : HighType) : Bool := match t with | .Unknown => true | _ => false + if subBoxable && supBoxable then some .refl -- Any ↔ Any + else if isWildcard sub'.val || isWildcard sup'.val then some .refl -- wildcard: no op + else if supBoxable then some (.inject sub'.val) -- concrete → Any (box) + else if subBoxable then some (.project sup'.val) -- Any → concrete (unbox) + else if highEq sub' sup' then some .refl + else match sub'.val, sup'.val with + | .UserDefined subName, .UserDefined supName => + if (ctx.ancestors subName.text).contains supName.text then some .upcast else none + -- Two distinct gradual `.TCore` names (e.g. `Heap`/`Box` plumbing, or a + -- `.TCore` that is consistent-but-not-coercible) — consistent, no runtime op. + | _, _ => + if ctx.isGradualTop sub'.val || ctx.isGradualTop sup'.val then some .refl else none + +/-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. DERIVED from the + proof-relevant `coerce` so the yes/no answer and the inserted coercion can + never disagree (ONE judgment). Used by rule `[⇐] Sub` and every bespoke check + rule. That single choice is what makes the system *gradual*: an expression of + type `Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely into + any typed slot, and any expression flows freely into a slot of type `Unknown`. -/ def isConsistentSubtype (ctx : TypeLattice) (sub sup : HighTypeMd) : Bool := - isConsistent ctx sub sup || isSubtype ctx sub sup + (coerce ctx sub sup).isSome def HighType.isBool : HighType → Bool | TBool => true diff --git a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean index 298b93f1b6..df4a5c89eb 100644 --- a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean +++ b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean @@ -137,8 +137,9 @@ private def runLaurelPasses -- Step 0: the input program before any passes emit "Initial" "laurel.st" program - -- Initial resolution - let result := resolve program + -- Initial resolution (with optional pre-registered external names from the frontend) + let result := resolve program (externalNames := options.externalNames) (gradualTypes := options.gradualTypes) + (realizeCoercion := options.realizeCoercion) let resolutionErrors : Std.HashSet DiagnosticModel := Std.HashSet.ofArray result.errors let (program, model) := (result.program, result.model) @@ -154,7 +155,8 @@ private def runLaurelPasses allStats := allStats.merge stats -- Run resolve after the pass if needed if pass.needsResolves then - let result := resolve program (some model) + let result := resolve program (some model) (externalNames := options.externalNames) (gradualTypes := options.gradualTypes) + (realizeCoercion := options.realizeCoercion) let newErrors := result.errors.filter fun e => !resolutionErrors.contains e if !newErrors.isEmpty then let newDiags := newErrors.toList.map fun d => @@ -216,7 +218,7 @@ def translateWithLaurel (options : LaurelTranslateOptions) (program : Program) unorderedCore := (pass.run unorderedCore fnModel options).1 if pass.needsResolves then let compositeTypes := program.types.filter (fun t => match t with | .Composite _ => true | _ => false) - let (uc', m', errors) := resolveUnorderedCore unorderedCore (some fnModel) compositeTypes + let (uc', m', errors) := resolveUnorderedCore unorderedCore (some fnModel) compositeTypes options.externalNames options.gradualTypes if !errors.isEmpty then let newDiags := errors.toList.map fun d => { d with message := diff --git a/Strata/Languages/Laurel/LaurelPass.lean b/Strata/Languages/Laurel/LaurelPass.lean index 130ae2cf09..9b7df53cb1 100644 --- a/Strata/Languages/Laurel/LaurelPass.lean +++ b/Strata/Languages/Laurel/LaurelPass.lean @@ -17,6 +17,17 @@ structure LaurelTranslateOptions where inlineFunctionsWhenPossible : Bool := false overflowChecks : Core.OverflowChecks := {} keepAllFilesPrefix : Option String := none + /-- Names to pre-register as `.unresolved` before resolution. Used by language + frontends to inject unmodeled external names without patching the resolver. -/ + externalNames : Std.HashSet String := {} + /-- Type names treated as the gradual/dynamic top type in `isConsistent`. + Used by language frontends (e.g. Python registers "Any" here). -/ + gradualTypes : Std.HashSet String := {} + /-- Frontend-supplied realizer for the abstract `Coercion` verdict of the + proof-relevant subtyping judgment: maps a verdict + the coerced term to a + rewritten term carrying the concrete box/unbox call. `none` = identity + (native Laurel inserts no coercion). Threaded onto `TypeLattice`. -/ + realizeCoercion : Option (Coercion → StmtExprMd → StmtExprMd) := none instance : Inhabited LaurelTranslateOptions where default := {} diff --git a/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean index 3c6cda03cd..53a298d6f0 100644 --- a/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean +++ b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean @@ -92,13 +92,15 @@ def translateType (ty : HighTypeMd) : TranslateM LMonoTy := do match model.get? name with | some (.datatypeDefinition dt) => return .tcons dt.name.text [] | some (.datatypeConstructor typeName _) => return .tcons typeName.text [] - | _ => do -- resolution should have already emitted a diagnostic - emitCoreDiagnostic (diagnosticFromSource ty.source s!"UserDefined type {name} could not be resolved to a composite or datatype" DiagnosticType.StrataBug) - return .tcons "Composite" [] + | _ => + -- Unknown UserDefined type: treat as gradual Any (unmodeled external type). + -- Any legitimate type resolution failure will have already been reported by the resolver. + return .tcons "Any" [] | .TCore s => return .tcons s [] | .TReal => return LMonoTy.real + | .TFloat64 => return LMonoTy.real | .MultiValuedExpr _ => invalidCoreType ty.source "MultiValuedExpr type encountered during Core translation" - | .Unknown => invalidCoreType ty.source "Unknown type encountered during Core translation" + | .Unknown => return .tcons "Any" [] -- gradual type: map to Any | _ => do invalidCoreType ty.source s!"cannot translate type to Core: not supported yet" diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index 971bafbf91..f0779f201d 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -298,7 +298,13 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do return seqCall else let callResultVar ← freshTempVar - let callResultType ← computeType expr + let callResultTypeFull ← computeType expr + -- Strip trailing Error outputs: `(T, Error, ...)` → `T` for the temp var. + let callResultType := match callResultTypeFull.val with + | .MultiValuedExpr (first :: rest) => + if rest.all (fun o => match o.val with | .TCore "Error" => true | _ => false) + then first else callResultTypeFull + | _ => callResultTypeFull let prepends ← asLifted (transformStmtAssignImperativeCall [⟨ .Declare ⟨callResultVar, callResultType⟩, source⟩] callee args source source) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index a3e3e5090d..fa29b96e63 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -210,6 +210,7 @@ def defineNameCheckDup (iden : Identifier) (node : ResolvedNode) (overrideResolu currentScopeNames := s.currentScopeNames.insert resolutionName } return name' + /-- Resolve a reference: look up the name in scope and assign the definition's ID. Returns the identifier with its ID filled in. When `expected` is provided, emits a diagnostic if the resolved node's kind is not @@ -227,6 +228,10 @@ def resolveRef (name : Identifier) (source : Option FileRange := none) modify fun s => { s with errors := s.errors.push diag } return name' | none => + -- TODO: Move this list to the Python pipeline using `resolveWithExternalNames`. + -- For now, names registered via `preRegisterExternalNames` will resolve silently + -- (they'll be in scope as `.unresolved`). This fallback catches names that were not + -- pre-registered. let diag := diagnosticFromSource (source.orElse fun _ => name.source) s!"Resolution failed: '{name}' is not defined" modify fun s => { s with errors := s.errors.push diag } return { name with uniqueId := none } @@ -301,7 +306,18 @@ def resolveFieldRef (target : StmtExprMd) (fieldName : Identifier) if let some instTypeName := (← get).instanceTypeName then if let some resolved ← resolveFieldInTypeScope instTypeName fieldName then return resolved - resolveRef fieldName source + -- Field name (an ATTRIBUTE, not a variable) didn't resolve in any type scope. + -- Distinguish two cases: + -- • the receiver's type is UNKNOWN/Any (typeName? = none, no instance type) — this is a + -- legitimate DYNAMIC field access (`err.response`, `client.foo().bar`). It is sound- + -- but-uninterpreted, NOT an error; leave it unresolved with no diagnostic. (v2 routed + -- this through `resolveRef`, which mis-treats the attribute as a variable and emits a + -- spurious "not defined" — the dominant benchmark failure on boto3-style code.) + -- • the receiver's type IS a known composite but lacks this field — that is a REAL bug + -- (typo'd attribute). Preserve the diagnostic via `resolveRef` so it is still caught. + match typeName?, (← get).instanceTypeName with + | none, none => return { fieldName with uniqueId := none } + | _, _ => resolveRef fieldName source /-- Save and restore scope around a block (for lexical scoping). -/ def withScope (action : ResolveM α) : ResolveM α := do @@ -394,15 +410,59 @@ private def typeMismatch (source : Option FileRange) (construct : Option StmtExp let diag := diagnosticFromSource source s!"{constructor}{problem}{suffix}" modify fun s => { s with errors := s.errors.push diag } +/-- Coercer hook at the `[⇐] Sub` boundary: when a multi-output proc `(T, Error, ...)` + is used in a single-output position, strip the trailing Error outputs and use `T`. + This is the standard Laurel coercion for exception-threaded procs. -/ +private def stripTrailingErrors (actual : HighTypeMd) : HighTypeMd := + match actual.val with + | .MultiValuedExpr (first :: rest) => + if rest.all (fun o => match o.val with | .TCore "Error" => true | _ => false) + then first else actual + | _ => actual + +/-- `void` and `()` (unit) are mutually compatible — they both denote "no value." -/ +private def isVoidLikeHT (t : HighType) : Bool := match t with + | .TVoid | .TCore "()" | .MultiValuedExpr [] => true | _ => false + /-- Type-level subtype check: emits the standard "expected/got" diagnostic when `actual` is not a consistent subtype of `expected`. Used at sites where the actual type is already in hand (assignment, call args, body vs declared output) — equivalent to `Check.resolveStmtExpr e expected` but without re-synthesizing. -/ private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do let ctx := (← get).typeLattice - unless isConsistentSubtype ctx actual expected do + let actual' := stripTrailingErrors actual + let compatible := + (isVoidLikeHT actual'.val && isVoidLikeHT expected.val) || + isConsistentSubtype ctx actual' expected + unless compatible do typeMismatch source none s!"expected '{formatType expected}'" actual +/-- PROOF-RELEVANT `[⇐] Sub`: check `actual ≤ expected` AND, on success, REALIZE the + coercion witness onto the rewritten term `e` (returning the coerced term). This + is `checkSubtype` plus term-rewriting; use it wherever the resolver holds the + expression and rebuilds the AST (subsumption fallback, assignment RHS, …). + + The witness is the abstract `coerce` verdict; the concrete coercion is inserted + by the frontend-supplied `ctx.realizeCoercion` (identity for native Laurel, so + this is a no-op there). The decision and the realized coercion share the single + `coerce` judgment, so they cannot disagree. On failure, emits the same diagnostic + as `checkSubtype` and returns `e` unchanged. Void-like compatibility (statement + position) inserts no coercion. -/ +private def coerceTo (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) + (e : StmtExprMd) : ResolveM StmtExprMd := do + let ctx := (← get).typeLattice + let actual' := stripTrailingErrors actual + if isVoidLikeHT actual'.val && isVoidLikeHT expected.val then + pure e + else match coerce ctx actual' expected with + | some verdict => + match ctx.realizeCoercion with + | some realize => pure (realize verdict e) + | none => pure e + | none => + typeMismatch source none s!"expected '{formatType expected}'" actual + pure e + /-- Test whether a type is in the set of numeric primitives (`TInt` / `TReal` / `TFloat64` / `TBv`). `Unknown` is accepted as a gradual escape hatch. Aliases and constrained types are @@ -468,12 +528,16 @@ private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List Hig | [singleOutput] => singleOutput.type | outputs => { val := .MultiValuedExpr (outputs.map (·.type)), source := none } pure (retTy, proc.inputs.map (·.type)) - | some (_, .datatypeConstructor t _) => - -- Testers (e.g. "Color..isRed") return Bool; constructors return the type + | some (_, .datatypeConstructor t ctor) => + -- Testers (e.g. "Color..isRed") return Bool; constructors return the type. + -- A constructor's argument types ARE its parameter types: return them so the + -- call rule checks + coerces each argument against them (e.g. `ListAny_cons(1, + -- …)` coerces `1` into the `Any` head slot). Previously `[]` was returned and + -- the elaborator boxed; the resolver now owns coercion, so supply them here. if (callee.text.splitOn "..is").length > 1 then pure ({ val := .TBool, source := callee.source }, []) else - pure ({ val := .UserDefined t, source := callee.source }, []) + pure ({ val := .UserDefined t, source := callee.source }, ctor.args.map (·.type)) | some (_, .parameter p) => pure (p.type, []) | some (_, .constant c) => pure (c.type, []) | _ => pure ({ val := .Unknown, source := callee.source }, []) @@ -828,10 +892,12 @@ def Check.resolveStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : Resolv | .PrimitiveOp .Implies args skipProof => Check.primitiveOp exprMd .Implies args skipProof expected source (by rw [h_node]) | _ => - -- Subsumption fallback: synth then check `actual <: expected`. + -- Subsumption fallback `[⇐] Sub`: synth, then check `actual <: expected` AND + -- realize the coercion witness onto the term. This chokepoint covers call + -- arguments, return values, functional bodies, and primitive-op subsumption — + -- every check-mode boundary without a bespoke rule funnels here. let (e', actual) ← Synth.resolveStmtExpr exprMd - checkSubtype source expected actual - pure e' + coerceTo source expected actual e' termination_by (exprMd, 3) decreasing_by all_goals first | (apply Prod.Lex.left; term_by_mem) @@ -1322,7 +1388,8 @@ def Synth.ifThenElse (exprMd : StmtExprMd) let (e', elseTy) ← Synth.resolveStmtExpr e let ctx := (← get).typeLattice let ty ← - if isConsistent ctx thenTy elseTy then + if isConsistent ctx (stripTrailingErrors thenTy) (stripTrailingErrors elseTy) || + isVoidLikeHT (stripTrailingErrors thenTy).val && isVoidLikeHT (stripTrailingErrors elseTy).val then pure ((join ctx thenTy elseTy).getD thenTy) else let diag := diagnosticFromSource source @@ -2616,7 +2683,13 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do let inputs' ← proc.inputs.mapM resolveParameter let inputNames := inputs'.map (·.name.text) let outputs' ← proc.outputs.mapM (resolveOutputParameter inputNames) - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) + -- Preconditions are boolean: check the condition against `TBool` so the + -- coercion (`Any_to_bool` via the frontend realizer) is inserted when the + -- condition is an `Any`-typed expression (a Python `assert` → `PLt(...) : Any` + -- lifted into a `bool`-returning `$pre` function). The elaborator no longer + -- coerces; the resolver owns it. + let pres' ← proc.preconditions.mapM (·.mapM (fun c => + Check.resolveStmtExpr c { val := .TBool, source := c.source })) let dec' ← proc.decreases.mapM resolveStmtExpr let savedAnswer := (← get).answerType modify fun s => { s with answerType := some (outputs'.map (·.type)) } @@ -2662,7 +2735,13 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv let inputs' ← proc.inputs.mapM resolveParameter let inputNames := inputs'.map (·.name.text) let outputs' ← proc.outputs.mapM (resolveOutputParameter inputNames) - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) + -- Preconditions are boolean: check the condition against `TBool` so the + -- coercion (`Any_to_bool` via the frontend realizer) is inserted when the + -- condition is an `Any`-typed expression (a Python `assert` → `PLt(...) : Any` + -- lifted into a `bool`-returning `$pre` function). The elaborator no longer + -- coerces; the resolver owns it. + let pres' ← proc.preconditions.mapM (·.mapM (fun c => + Check.resolveStmtExpr c { val := .TBool, source := c.source })) let dec' ← proc.decreases.mapM resolveStmtExpr let savedAnswer := (← get).answerType modify fun s => { s with answerType := some (outputs'.map (·.type)) } @@ -3115,10 +3194,50 @@ private def preRegisterTopLevel (program : Program) : ResolveM Unit := do /-! ## Entry point -/ -/-- Run the full resolution pass on a Laurel program. -/ -public def resolve (program : Program) (existingModel: Option SemanticModel := none) : ResolutionResult := - -- Phase 1: pre-register all top-level names, then assign IDs and resolve references +/-- Pre-register a set of external/unmodeled names as `.unresolved` scope entries so that + `resolveRef` finds them and emits no "not defined" diagnostics. The caller (e.g. the + Python pipeline) should use this to register names that are deliberately unmodeled + rather than patching `resolveRef` with a hardcoded list. -/ +public def preRegisterExternalNames (names : Std.HashSet String) : ResolveM Unit := do + for name in names do + -- Only register if not already in scope (don't clobber real definitions) + unless (← get).scope.get? name |>.isSome do + let id ← freshId + modify fun s => { + s with + scope := s.scope.insert name (id, .unresolved none) + idToNode := s.idToNode.insert id (.unresolved none) } + +/-- Like `resolve` but pre-registers a set of external/unmodeled names so they resolve + silently without "not defined" diagnostics. Used by language frontends (e.g. Python) + to inject unmodeled stdlib names without patching the resolver itself. -/ +public def resolveWithExternalNames (program : Program) (externalNames : Std.HashSet String) + (existingModel: Option SemanticModel := none) + (gradualTypes : Std.HashSet String := {}) : ResolutionResult := + let nextId := existingModel.elim 1 (fun m => m.nextId) + let typeLattice := { TypeLattice.ofTypes program.types with gradualTypes := gradualTypes } + let phase1 : ResolveM Program := do + preRegisterExternalNames externalNames + preRegisterTopLevel program + let types' ← program.types.mapM resolveTypeDefinition + let constants' ← program.constants.mapM resolveConstant + let staticFields' ← program.staticFields.mapM (resolveField "$static") + let staticProcs' ← program.staticProcedures.mapM resolveProcedure + return { staticProcedures := staticProcs', staticFields := staticFields', + types := types', constants := constants' } + let (program', finalState) := phase1.run { nextId := nextId, typeLattice } + let refToDef := buildRefToDef program' + let semanticModel := { compositeCount := program.types.length, refToDef := refToDef, nextId := finalState.nextId } + let diamondErrors := validateDiamondFieldAccesses semanticModel program' + { program := program', model := semanticModel, errors := finalState.errors ++ diamondErrors } + +public def resolve (program : Program) (existingModel: Option SemanticModel := none) + (externalNames : Std.HashSet String := {}) + (gradualTypes : Std.HashSet String := {}) + (realizeCoercion : Option (Coercion → StmtExprMd → StmtExprMd) := none) : ResolutionResult := + -- Phase 1: pre-register external names, then all top-level names, then resolve references let phase1 : ResolveM Program := do + preRegisterExternalNames externalNames preRegisterTopLevel program let types' ← program.types.mapM resolveTypeDefinition let constants' ← program.constants.mapM resolveConstant @@ -3127,7 +3246,8 @@ public def resolve (program : Program) (existingModel: Option SemanticModel := n return { staticProcedures := staticProcs', staticFields := staticFields', types := types', constants := constants' } let nextId := existingModel.elim 1 (fun m => m.nextId) - let typeLattice := TypeLattice.ofTypes program.types + let typeLattice := { TypeLattice.ofTypes program.types with + gradualTypes := gradualTypes, realizeCoercion := realizeCoercion } let (program', finalState) := phase1.run { nextId := nextId, typeLattice } -- Phase 2: build refToDef from the resolved program (all definitions now have UUIDs) let refToDef := buildRefToDef program' @@ -3183,9 +3303,11 @@ but they are because certain type references have incorrectly not been updated. public def resolveUnorderedCore (uc : UnorderedCoreWithLaurelTypes) (existingModel : Option SemanticModel := none) (additionalTypes : List TypeDefinition := []) + (externalNames : Std.HashSet String := {}) + (gradualTypes : Std.HashSet String := {}) : UnorderedCoreWithLaurelTypes × SemanticModel × Array DiagnosticModel := let fnProgram := unorderedCoreToProgram uc additionalTypes - let fnResolveResult := resolve fnProgram existingModel + let fnResolveResult := resolve fnProgram existingModel (externalNames := externalNames) (gradualTypes := gradualTypes) (fromResolvedProgram fnResolveResult.program, fnResolveResult.model, fnResolveResult.errors) end -- public section diff --git a/StrataPython/PR_v2_pipeline.md b/StrataPython/PR_v2_pipeline.md new file mode 100644 index 0000000000..e0a607fbbc --- /dev/null +++ b/StrataPython/PR_v2_pipeline.md @@ -0,0 +1,162 @@ +# PR: Python→Core v2 pipeline (proof-relevant coercion in the Laurel resolver) + +## Summary + +This PR adds `pyAnalyzeV2` (invoked as `pyAnalyzeLaurel --v2`): a second Python→Core +analysis pipeline that sits beside the existing `pyAnalyzeLaurel` (v1). v1 stays the +default frontend and continues to back the CBMC/GOTO bridge (`pyAnalyzeLaurelToGoto`). +v2 is opt-in and is checked against v1 as its oracle. + +v2's defining choice: coercion between Python's dynamic `Any` and concrete types is a +single, generic subtyping judgment in the Laurel resolver. The judgment returns the +coercion to apply; the Python frontend plugs in the concrete runtime functions that +realize it. + +## The pipeline (v2) + +`pyAnalyzeV2ToCore` (PySpecPipeline.lean) runs five stages. + +1. **Read** — parse the pre-generated `.python.st.ion` into the Python AST. + (`.py → .ion` is produced by `strata.gen py_to_strata`, with `PYTHON_DIALECT_ST_ION` + set to `Tools/Python/dialects/Python.dialect.st.ion`.) + +2. **Resolution** (`StrataPython/Resolution.lean`) — resolves every name over the Python + AST: locals, parameters, fields, classes, methods, module-level locals, and builtins. + It takes each declaration's type from the user's annotation, and treats a module-level + `MyInt = int` as a type alias so later uses of `MyInt` resolve to `int`. + +3. **Translation** (`StrataPython/Translation.lean`) — produces untyped Laurel. Every + Python value is wrapped in `Any`; operators become runtime calls (`PAdd`, `PLt`, …); + classes become composites; field reads and writes become `.Var (.Field obj f)` and + `.Assign [.Field obj f] v`; object creation becomes `.New C`. This Laurel becomes + well-typed only after exceptions are threaded, which is why elaboration runs next. + +4. **Elaboration** (`Strata/Languages/FineGrainLaurel/Elaborate.lean`) — turns untyped + Laurel into effect-typed (FGCBV) Laurel. It infers each procedure's grade in the + monoid `{pure, proc, err}` and threads exceptions through the calling convention + (`leftResidual` computes the residual grade for a continuation). The heap is handled + later by `heapParameterizationPass`, which consumes the bare field/`New` nodes from + stage 3. Elaboration's subsumption carries effects; pure-type coercion is the + resolver's job (stage 5). + +5. **Laurel → Core** (`translateCombinedLaurelV2` → `LaurelCompilationPipeline`) — the + generic Laurel resolver assigns pure types and inserts coercions (below); the lowering + passes run (heap parameterization, type hierarchy, contracts, …); the result is + translated to Core and verified by SMT. + +## Coercion + +### One judgment (`Strata/Languages/Laurel/LaurelAST.lean`) + +Subtyping returns the coercion that witnesses it: + +``` +inductive Coercion + | refl -- equal types: apply the value unchanged + | inject (source) -- a concrete value becomes `Any` (boxing) + | project (target) -- an `Any` becomes a concrete type (unboxing / downcast) + | upcast -- a composite becomes one of its ancestors + +def coerce (ctx) (sub sup) : Option Coercion +def isConsistentSubtype (ctx) (sub sup) : Bool := (coerce ctx sub sup).isSome +``` + +`coerce` decides subtyping using the type lattice (`unfold` for aliases/constrained +types, `gradualTypes` for the dynamic type, `ancestors` for composite inheritance) and +returns the kind of coercion required. `isConsistentSubtype` is defined as "`coerce` +succeeds," so the verdict and the coercion come from the same source. + +`Any` is the dynamic type that boxes and unboxes (`inject`/`project`). `Unknown` is the +gradual wildcard for synth gaps and holes; it coerces by `refl`. Composite values relate +to ancestors by `upcast`. Python truthiness is just a `project` to `bool`: a value used +where `bool` is expected is `Any` (every Python value is boxed), so `coerce Any bool = +project bool`, which the frontend realizes as `Any_to_bool`. There is no separate +`truthify` verdict — keeping `bool` out of `coerce`'s concrete cases is what makes the +generic relation behave identically for non-Python callers (`int ≤ bool` stays false). + +### The frontend realizes the coercion (`StrataPython/PySpecPipeline.lean`) + +`coerce` yields an abstract verdict; the Python frontend supplies a realizer (threaded +on `TypeLattice` next to `gradualTypes`) that turns each verdict into the concrete +runtime call: + +``` +inject int → from_int project int → Any..as_int! +inject str → from_str project str → Any..as_string! +inject float → from_float project float → Any..as_float! +inject ListAny → from_ListAny project ListAny → Any..as_ListAny! +inject DictStrAny → from_DictStrAny project DictStrAny → Any..as_Dict! +inject Composite → from_Composite project Composite → Any..as_Composite! + project bool → Any_to_bool (truthiness) +refl, upcast → the value unchanged +``` + +The judgment is generic; the runtime vocabulary is Python's, supplied at the call site. +A Laurel-native program supplies no realizer and runs with refl-only coercion. + +### Insertion sites (`Strata/Languages/Laurel/Resolution.lean`) + +`coerceTo` checks `actual ≤ expected` and applies the realized witness to the term. The +resolver calls it at each check-mode boundary it rebuilds: + +- call arguments, return values, functional bodies, AND assignment / annotated-initializer + right-hand sides — all funnel through the one subsumption chokepoint in + `Check.resolveStmtExpr` (`Synth.assign`/`Check.assign` resolve the RHS in CHECK mode via + `Check.resolveStmtExpr value expectedTy`, which reaches that chokepoint; no bespoke + assignment coercion code is needed, and the native check-mode diagnostics are preserved); +- datatype-constructor arguments (`getCallInfo` supplies the constructor's field types); +- precondition conditions (checked against `TBool`; for an `Any`-typed Python condition + this is `project bool` → `Any_to_bool`). + +The `from_Composite` / `Any..as_Composite!` bridge stubs are typed via the `re_Match` +composite, which the type-hierarchy pass flattens to the synthesized `Composite` +datatype, giving them the correct Core types at the point `Composite` exists. + +## Files + +New (v2): `StrataPython/Resolution.lean`, `StrataPython/Translation.lean`, +`Strata/Languages/FineGrainLaurel/Elaborate.lean` (+ dialect), +`StrataPython/Specs/Error.lean`, `Scripts/pyAnalyzeV2.lean`, and the wiring in +`PySpecPipeline.lean`, `Pipeline/PyAnalyzeLaurel.lean`, `Cli.lean`, `StrataMain.lean`. + +Modified (generic, additive): `Strata/Languages/Laurel/LaurelAST.lean` (`Coercion`, +`coerce`, the realizer field on `TypeLattice`), `Resolution.lean` (`coerceTo` and its +insertion sites), `LaurelPass.lean` and `LaurelCompilationPipeline.lean` (thread the +realizer), `PythonRuntimeLaurelPart.lean` (the Composite bridge stubs and stubs for the +`type`/`abs`/`chr`/`ord`/`getattr`/`setattr` builtins). + +## Verification + +**Unit suite (220 tests, `--solver z3`):** 167 Analysis success, 47 Inconclusive, +4 Failures, 2 Internal error, 0 crash. The 2 internal errors come from ill-formed +input (an unannotated `c = Cell(1)`; an ill-typed `x:int=42; x="hello"`). + +**Against v1 golden files (`expected_laurel/`, by RESULT):** 192/220 identical. The 28 +differences: + +- **14** are v1 Analysis success → v2 Inconclusive: on these v2 produces weaker + verification conditions and proves less than v1. These are precision regressions + (sound, but worse than v1). (bitnot, class_field_use, class_mixed_init, + composite_return, datetime, datetime_now_tz, for_range, list_slice, regex_positive, + timedelta_expr, try_except_{basic,modeled,scoping}, with_void_enter.) +- **2** are v1 Analysis success → v2 Internal error, both from ill-formed input v1 + tolerated (test_field_write, test_reassign_different_type). +- **2** are v1 Analysis success → v2 Failures found, from bugFinding-mode reachability + precision (test_class_methods, test_class_with_methods). +- **10** are v2 producing a verdict v1 did not (v1 User error / Inconclusive / Known + limitation → v2 Analysis success). These are pending a soundness audit. + +So v2 is worse than v1 on 18 of 220 (14 precision + 2 internal + 2 bugFinding), +identical on 192, and possibly ahead on up to 10 (unaudited). + +**Benchmarks (StrataInternalBenchmarks, 420 `.py`, bugFinding, 120s cap):** 78 Analysis +success, 225 Inconclusive, 26 Failures, 57 Internal error, 35 timeout. v1 was not run on +this corpus, so this is a standalone v2 number, not a comparison. + +## Follow-ups + +- Root-cause the 14 precision regressions (v1 success → v2 inconclusive). +- Audit the 10 v2-ahead cases for soundness. +- Triage the 57 benchmark internal errors; the 35 benchmark timeouts are speed, not + correctness. +- v2 is a parallel, opt-in pipeline; it does not replace v1 in this PR. diff --git a/StrataPython/Scripts/pyAnalyzeV2.lean b/StrataPython/Scripts/pyAnalyzeV2.lean new file mode 100644 index 0000000000..478555169a --- /dev/null +++ b/StrataPython/Scripts/pyAnalyzeV2.lean @@ -0,0 +1,12 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module +import StrataPython.Cli + +/-- `pyAnalyzeV2` is the V2 pipeline: Resolution → Translation → Elaboration → Core. + Implementation: alias of `pyAnalyzeLaurel --v2`. -/ +public def main (args : List String) : IO Unit := + runCommand StrataPython.Cli.pyAnalyzeLaurelCommand ("--v2" :: args) diff --git a/StrataPython/StrataMain.lean b/StrataPython/StrataMain.lean new file mode 100644 index 0000000000..ec62e9dbc0 --- /dev/null +++ b/StrataPython/StrataMain.lean @@ -0,0 +1,16 @@ +/- + Copyright Strata Contributors + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +-- Minimal strata entrypoint for the incremental hybrid branch. +-- Supports `pyAnalyzeV2` by routing to `pyAnalyzeLaurel --v2`. +import StrataPython.Cli + +def main (args : List String) : IO Unit := do + match args with + | "pyAnalyzeV2" :: rest => + -- Route pyAnalyzeV2 to pyAnalyzeLaurel with --v2 flag + runCommand StrataPython.Cli.pyAnalyzeLaurelCommand ("--v2" :: rest) + | _ => + IO.println s!"strata: unknown command '{args.headD ""}'" + IO.Process.exit 1 diff --git a/StrataPython/StrataPython/Cli.lean b/StrataPython/StrataPython/Cli.lean index bea3454845..b3e9bd262f 100644 --- a/StrataPython/StrataPython/Cli.lean +++ b/StrataPython/StrataPython/Cli.lean @@ -251,6 +251,9 @@ def pyAnalyzeLaurelCommand (mkDischarge : Core.MkDischargeFn := Core.mkDischarge takesArg := .arg "file" }, { name := "skip-verification", help := "Run Python-to-Laurel and Laurel-to-Core translation only (skip SMT verification).", + takesArg := .none }, + { name := "v2", + help := "Use the V2 pipeline (Resolution → Translation → Elaboration → Core).", takesArg := .none }] help := "Verify a Python Ion program via the Laurel pipeline. Translates Python to Laurel to Core, then runs SMT verification." callback := fun v pflags => do @@ -310,6 +313,7 @@ def pyAnalyzeLaurelCommand (mkDischarge : Core.MkDischargeFn := Core.mkDischarge else if quiet then .quiet else .default let skipVerification := pflags.getBool "skip-verification" + let useV2 := pflags.getBool "v2" let (outcome, laurelPassStats, pctx) ← StrataPython.Pipeline.runPyAnalyzePipeline { filePath, specDir @@ -319,6 +323,7 @@ def pyAnalyzeLaurelCommand (mkDischarge : Core.MkDischargeFn := Core.mkDischarge entryPoint, isBugFinding outputMode, skipVerification metricsHandle, mkDischarge + useV2 } let msgs ← pctx.getMessages diff --git a/StrataPython/StrataPython/Pipeline/PyAnalyzeLaurel.lean b/StrataPython/StrataPython/Pipeline/PyAnalyzeLaurel.lean index 169daecf10..45bcd5bc74 100644 --- a/StrataPython/StrataPython/Pipeline/PyAnalyzeLaurel.lean +++ b/StrataPython/StrataPython/Pipeline/PyAnalyzeLaurel.lean @@ -43,40 +43,60 @@ public structure PyAnalyzeConfig where profilePipeline : Bool := true metricsHandle : Option IO.FS.Handle := none mkDischarge : Core.MkDischargeFn := Core.mkDischargeFn + /-- When true, route Python→Core through the V2 pipeline (Resolution → Translation → + Elaboration → resolve+coerce → laurel passes → Core), bypassing the old + `pythonAndSpecToLaurel`. -/ + useV2 : Bool := false private def runPipeline (config : PyAnalyzeConfig) : PipelineM (PyAnalyzeOutcome × Statistics) := do - let combinedLaurel ← withPhase "pythonAndSpecToLaurel" do - StrataPython.pythonAndSpecToLaurel - (specDir := config.specDir) - config.filePath config.dispatchModules config.pyspecModules config.sourcePath - - if config.outputMode == .verbose then - let _ ← (show IO Unit from do - IO.println "---- BEGIN Laurel Program ----" - IO.println (toString (Std.format combinedLaurel)) - IO.println "---- END Laurel Program ----").toBaseIO - let uri := config.sourcePath.getD config.filePath - let (coreProgram, laurelPassStats) ← withPhase "laurelToCore" do - let ctx ← read - let laurelResult ← - StrataPython.translateCombinedLaurelWithLowered combinedLaurel - (keepAllFilesPrefix := config.keepAllFilesPrefix) - (pipelineCtx := some ctx) |>.toBaseIO - match laurelResult with - | .ok (coreOpt, diags, _, stats) => - let phase ← getPhase - for msg in PipelineMessage.fromDiagnostics phase diags do - addMessage msg - if msg.kind.impact.isFatal then throw () - match coreOpt with - | some core => pure (core, stats) - | none => - emitMessageAndAbort (file := uri) .laurelToCoreError s!"Laurel to Core translation failed: {diags}" - | .error e => - emitMessageAndAbort (file := uri) .laurelToCoreError s!"Laurel translation error: {e}" + let (coreProgram, laurelPassStats) ← + if config.useV2 then + withPhase "pyAnalyzeV2ToCore" do + let v2Result ← StrataPython.pyAnalyzeV2ToCore config.filePath config.sourcePath |>.toBaseIO + match v2Result with + | .ok (.ok (some core, diags)) => + let phase ← getPhase + for msg in PipelineMessage.fromDiagnostics phase diags do + addMessage msg + if msg.kind.impact.isFatal then throw () + pure (core, ({} : Statistics)) + | .ok (.ok (none, diags)) => + emitMessageAndAbort (file := uri) .laurelToCoreError s!"V2 pipeline produced no Core: {diags}" + | .ok (.error msg) => + emitMessageAndAbort (file := uri) .laurelToCoreError s!"V2 pipeline failed: {msg}" + | .error e => + emitMessageAndAbort (file := uri) .laurelToCoreError s!"V2 pipeline IO error: {e}" + else do + let combinedLaurel ← withPhase "pythonAndSpecToLaurel" do + StrataPython.pythonAndSpecToLaurel + (specDir := config.specDir) + config.filePath config.dispatchModules config.pyspecModules config.sourcePath + if config.outputMode == .verbose then + let _ ← (show IO Unit from do + IO.println "---- BEGIN Laurel Program ----" + IO.println (toString (Std.format combinedLaurel)) + IO.println "---- END Laurel Program ----").toBaseIO + withPhase "laurelToCore" do + let ctx ← read + let laurelResult ← + StrataPython.translateCombinedLaurelWithLowered combinedLaurel + (keepAllFilesPrefix := config.keepAllFilesPrefix) + (pipelineCtx := some ctx) |>.toBaseIO + match laurelResult with + | .ok (coreOpt, diags, _, stats) => + let phase ← getPhase + for msg in PipelineMessage.fromDiagnostics phase diags do + addMessage msg + if msg.kind.impact.isFatal then throw () + match coreOpt with + | some core => pure (core, stats) + | none => + emitMessageAndAbort (file := uri) .laurelToCoreError s!"Laurel to Core translation failed: {diags}" + | .error e => + emitMessageAndAbort (file := uri) .laurelToCoreError s!"Laurel translation error: {e}" if config.outputMode == .verbose then let _ ← (show IO Unit from do diff --git a/StrataPython/StrataPython/PySpecPipeline.lean b/StrataPython/StrataPython/PySpecPipeline.lean index 02322f079b..6f7be6f8cb 100644 --- a/StrataPython/StrataPython/PySpecPipeline.lean +++ b/StrataPython/StrataPython/PySpecPipeline.lean @@ -19,6 +19,9 @@ import StrataPython.Specs.MessageKind import StrataPython.Specs.ToLaurel public import Strata.Pipeline.Context public import Strata.Util.Statistics +import StrataPython.Resolution +import StrataPython.Translation +import Strata.Languages.FineGrainLaurel.Elaborate /-! ## PySpec Pipeline @@ -409,6 +412,7 @@ public def translateCombinedLaurel (combined : Laurel.Program) (keepAllFilesPref let (coreOption, errors, _, _) ← translateCombinedLaurelWithLowered combined keepAllFilesPrefix return (coreOption, errors) + /-- Run the pyAnalyzeLaurel pipeline: read a Python Ion program, resolve overloads from dispatch files, load PySpec declarations, translate Python to Laurel, and combine with PySpec Laurel. @@ -462,4 +466,190 @@ public def pythonAndSpecToLaurel let combined := combinePySpecLaurel filteredPrelude laurelProgram return combined +/-- Names that are unmodeled in the Python runtime (external stdlib, boto3 clients, etc.). + These are pre-registered in the Laurel resolver's scope as `.unresolved` entries so + that references to them produce no "not defined" diagnostics. + Lives here in the Python pipeline, not in the generic Laurel resolver. -/ +public def pythonUnmodeledNames : Std.HashSet String := + ([ "datetime", "timedelta", "Client", "bytes", "MyInt", + "Any..as_Composite!", "Any..isfrom_Composite", "from_Composite", + "date", "timezone", "UTC", "BotocoreError", "ClientError", + "Dict", "list_to_bool", "dict_to_bool", "float_to_bool", + "Any_set_to_Any", "Any_list_to_Any", "Any_enumerate_to_Any", + "Any_dict_to_Any", "Any_range_to_Any", + "PDiv", "Any_sum_to_Any", "to_int_any", "to_float_any", + "Any_isinstance_to_bool", "RDS", + "PBitAnd", "PBitOr", "PBitXor", "PLShift", "PRShift", + "Any_max_to_Any", "Any_min_to_Any", "Any_hasattr_to_bool", + "Any_any_to_bool", "Any_all_to_bool", "Any_zip_to_Any", + "Any_map_to_Any", "Any_filter_to_Any", "Any_sorted_to_Any", + "Any_reversed_to_Any", "Any_tuple_to_Any", "Any_frozenset_to_Any", + "Callable", "OperationModel", "StructureShape", "IAMClient", + "STSClient", "SFNClient", "GetCallerIdentityResponseTypeDef", + "CreateStateMachineOutputTypeDef", "CreateRoleResponseTypeDef", + "EC2Client", "S3Client", "DynamoDBClient", "LambdaClient", + "KMSClient", "SNSClient", "SQSClient", "BedrockClient", + "BedrockRuntimeClient", "ECSClient", "EKSClient", "ECRClient", + "CloudWatchClient", "CloudFormationClient", "SecretsManagerClient", + "SSMClient", "GlacierClient", "PinpointClient", "NeptuneClient", + "IoTSiteWiseClient", "HealthLakeClient", "DeepLensClient", + "client", "session", "AsyncIterator", "ServiceResource", + "SchedulerWrapper", "Iterator", "Generator" + ] : List String).foldl (fun s n => s.insert n) {} + +/-- Python gradual types: names consistent with everything (the dynamic top type). + `Any` is Python's dynamic type. `re_Match` types the `from_Composite`/ + `Any..as_Composite!` bridge stubs (the prelude cannot name the synthesized + `Composite`, so it borrows a named composite that flattens to it); making it + gradual lets a class instance of ANY class flow into the bridge at the + pre-flatten resolves; post-flatten both sides are `Composite`. -/ +public def pythonGradualTypes : Std.HashSet String := + (["Any", "re_Match"] : List String).foldl (fun s n => s.insert n) {} + +/-- Wrap `e` in a unary `StaticCall` to the named prelude function. -/ +private def pyCoerceCall (name : String) (e : Laurel.StmtExprMd) : Laurel.StmtExprMd := + { val := .StaticCall { text := name, uniqueId := none } [e], source := e.source } + +/-- Classify a Python/Laurel `HighType` to the prelude box/unbox vocabulary key. + Mirrors the elaborator's `eraseType` (Elaborate.lean): user-defined classes are + `Composite`; `Any`/containers keep their core name; Python `float` is `real`. -/ +private def pyTypeKey : Laurel.HighType → String + | .TInt => "int" | .TBool => "bool" | .TString => "str" + | .TFloat64 => "float" | .TReal => "float" | .TVoid => "void" + | .TCore "real" => "float" + | .TCore n => n + | .UserDefined id => match id.text with + | "Any" => "Any" | "ListAny" => "ListAny" | "DictStrAny" => "DictStrAny" + | "Error" | "OptionInt" | "Box" | "Field" | "TypeTag" => id.text + | _ => "Composite" -- every user class boxes/unboxes as Composite + | _ => "Any" + +/-- Python REALIZER for the abstract `Coercion` verdict. Transcribes the gradual + (inject/project) rows of the elaborator's `subtype` table (Elaborate.lean:483-521) + into concrete prelude calls. `inject` boxes a concrete value into `Any` by the + SOURCE type; `project` unboxes/casts out of `Any` by the TARGET type (a `project` + to `bool` is Python truthiness, realized by `Any_to_bool`). `upcast` (nominal) and + `refl` are identity. -/ +public def pythonRealizeCoercion : Laurel.Coercion → Laurel.StmtExprMd → Laurel.StmtExprMd + | .refl, e => e + | .upcast, e => e + | .inject source, e => + match pyTypeKey source with + | "int" => pyCoerceCall "from_int" e + | "bool" => pyCoerceCall "from_bool" e + | "str" => pyCoerceCall "from_str" e + | "float" => pyCoerceCall "from_float" e + | "ListAny" => pyCoerceCall "from_ListAny" e + | "DictStrAny" => pyCoerceCall "from_DictStrAny" e + | "Composite" => pyCoerceCall "from_Composite" e + | "void" => { val := .StaticCall { text := "from_None", uniqueId := none } [], source := e.source } + | _ => e -- already Any or a type with no boxing witness: pass through + | .project target, e => + match pyTypeKey target with + | "int" => pyCoerceCall "Any..as_int!" e + | "bool" => pyCoerceCall "Any_to_bool" e + | "str" => pyCoerceCall "Any..as_string!" e + | "float" => pyCoerceCall "Any..as_float!" e + | "ListAny" => pyCoerceCall "Any..as_ListAny!" e + | "DictStrAny" => pyCoerceCall "Any..as_Dict!" e + | "Composite" => pyCoerceCall "Any..as_Composite!" e + | _ => e + +/-- V2 variant of `translateCombinedLaurel` that pre-registers Python's unmodeled + external names so the Laurel resolver emits no "not defined" diagnostics for them. + `extraExternalNames` adds program-specific unmodeled names (e.g. names imported from + unmodeled modules like `botocore.config.Config`, `pyspark.SparkContext`). -/ +private def translateCombinedLaurelV2 (combined : Laurel.Program) + (extraExternalNames : Std.HashSet String := {}) + : IO (Option Core.Program × List DiagnosticModel) := do + let allExternal := extraExternalNames.fold (fun s n => s.insert n) pythonUnmodeledNames + let (coreOption, errors, _, _) ← + Laurel.translateWithLaurel + { inlineFunctionsWhenPossible := true + externalNames := allExternal + gradualTypes := pythonGradualTypes + realizeCoercion := some pythonRealizeCoercion } + combined + return (coreOption.map appendCorePartOfRuntime, errors) + +/-- Collect names bound by `import`/`from … import …` at the top level of a Python program. + These are external (their defining modules are unmodeled), so the Laurel resolver must + treat them as `.unresolved` rather than emitting "'Config' is not defined". This makes + unmodeled-library usage (boto3 `Config`/`Session`, pyspark `SparkContext`, etc.) sound- + but-uninterpreted instead of a hard pipeline failure. -/ +private def collectImportedNames (stmts : Array (StrataPython.stmt SourceRange)) : Std.HashSet String := Id.run do + let mut names : Std.HashSet String := {} + for s in stmts do + match s with + | .Import _ aliases => + for a in aliases.val do + match a with + | .mk_alias _ modName asName => + match asName.val with + | some aliasName => names := names.insert aliasName.val + | none => names := names.insert modName.val + | .ImportFrom _ _ imports _ => + for a in imports.val do + match a with + | .mk_alias _ impName asName => + match asName.val with + | some aliasName => names := names.insert aliasName.val + | none => names := names.insert impName.val + | _ => pure () + return names + +/-- Assemble the Laurel program to elaborate: merge user code and demanded imported stubs. -/ +private def assembleElaborationInput + (userLaurel importedLaurel : Laurel.Program) : Laurel.Program := + { staticProcedures := userLaurel.staticProcedures ++ importedLaurel.staticProcedures + staticFields := userLaurel.staticFields + types := userLaurel.types ++ importedLaurel.types + constants := userLaurel.constants } + +/-- V2 pipeline: Resolution → Translation → Elaboration → resolve → Core. + Specs/imports enter via `Resolution.resolve` (loads `.python.st.ion` stubs) + → `Translation.runTranslation`; exceptions are threaded by `fullElaborate`; + the resolve + coerce + laurel passes happen in `translateCombinedLaurel`. -/ +public def pyAnalyzeV2ToCore (pythonIonPath : String) (sourcePath : Option String := none) + : IO (Except String (Option Core.Program × List DiagnosticModel)) := do + let baseDir := System.FilePath.mk pythonIonPath |>.parent.getD "." + let metadataPath := sourcePath.getD pythonIonPath + -- Step 1: Read + resolve + let stmts ← match ← (readPythonStrata pythonIonPath).toBaseIO with + | .error msg => return .error s!"read: {msg}" + | .ok s => pure s + let resolveResult ← match ← (Resolution.resolve stmts baseDir).toBaseIO with + | .error msg => return .error s!"resolution: {msg}" + | .ok r => pure r + -- Step 2: Translate (user code + demanded imports) + let importedLaurel : Laurel.Program := + match Translation.runTranslation { stmts := resolveResult.demandedStmts, moduleLocals := [] } metadataPath with + | .ok (prog, _) => prog + | .error _ => default + let userLaurel ← match Translation.runTranslation resolveResult.program metadataPath with + | .error e => return .error s!"translation: {repr e}" + | .ok (prog, _) => pure prog + -- Step 3: Elaborate (exception threading) + let toElaborate := assembleElaborationInput userLaurel importedLaurel + let fullRuntime := pythonRuntimeLaurelPart + -- Build runtime grade map: maps each proc name to its inferred grade. + let runtimeGrades := fullRuntime.staticProcedures.foldl + (fun acc proc => acc.insert proc.name.text (FineGrainLaurel.gradeFromSignature proc)) + ({} : Std.HashMap String FineGrainLaurel.Grade) + let elaboratedProgram ← match FineGrainLaurel.fullElaborate toElaborate fullRuntime runtimeGrades with + | .error e => return .error s!"elaboration: {e}" + | .ok (prog, failures) => + if !failures.isEmpty then return .error s!"elaboration failures: {String.intercalate ", " failures}" + pure prog + -- Step 4: Lower to Core (resolve + coerce + laurel passes). + -- Use the full runtime (not filtered) to preserve all datatype definitions + -- (e.g. ListAny, DictStrAny) needed by the Core verifier's termination checker. + let combined := combinePySpecLaurel fullRuntime elaboratedProgram + -- Names imported from unmodeled modules (e.g. `from botocore.config import Config`) are + -- external: register them so the Laurel resolver treats their uses as sound-but- + -- uninterpreted instead of "'Config' is not defined". + let importedNames := collectImportedNames stmts + let (coreOpt, errs) ← translateCombinedLaurelV2 combined importedNames + return .ok (coreOpt, errs) + end StrataPython diff --git a/StrataPython/StrataPython/PythonRuntimeLaurelPart.lean b/StrataPython/StrataPython/PythonRuntimeLaurelPart.lean index c1352bfd71..5d6e00e2be 100644 --- a/StrataPython/StrataPython/PythonRuntimeLaurelPart.lean +++ b/StrataPython/StrataPython/PythonRuntimeLaurelPart.lean @@ -545,7 +545,16 @@ function Any_sets! (indices: ListAny, dictOrList: Any, val: Any): Any Any_sets!(ListAny..tail!(indices), Any_get!(dictOrList, ListAny..head!(indices)), val)) }; +function Any_sets (indices: ListAny, dictOrList: Any, val: Any): Any +{ + if ListAny..isListAny_nil(indices) then dictOrList + else if ListAny..isListAny_nil(ListAny..tail!(indices)) then Any_set!(dictOrList, ListAny..head!(indices), val) + else Any_set!(dictOrList, ListAny..head!(indices), + Any_sets(ListAny..tail!(indices), Any_get!(dictOrList, ListAny..head!(indices)), val)) +}; + function Any_len (v: Any) : int; +function Any_range_to_Any (v: Any) : Any; function Any_len_to_Any (v: Any) : Any { from_int(Any_len(v)) @@ -613,6 +622,45 @@ function int_to_real (i: int) : real; // Used to in Python binary operators' modelling function bool_to_int (bval: bool) : int {if bval then 1 else 0}; function bool_to_real (b: bool) : real {if b then 1.0 else 0.0}; +function int_to_bool (n: int) : bool { !(n == 0) }; +function str_to_bool (s: string) : bool { !(s == "") }; +function list_to_bool (l: ListAny) : bool { !(l == ListAny_nil()) }; +function dict_to_bool (d: DictStrAny) : bool; +function float_to_bool (f: real) : bool { !(f == 0.0) }; +function Any_set_to_Any (v: Any) : Any; +function Any_list_to_Any (v: Any) : Any; +function Any_enumerate_to_Any (v: Any) : Any; +function Any_dict_to_Any (v: Any) : Any; +function Any_sum_to_Any (v: Any) : Any; +function Any_isinstance_to_bool (v: Any, t: Any) : bool; +// Unmodeled builtins the resolver remaps to (Resolution.lean builtinContext): declared +// as uninterpreted stubs (sound) so the elaborator's lookupFuncSig finds a signature — +// otherwise a `type(e)`/`abs(x)`/etc. is a hard elaboration failure instead of an opaque +// Any. Arities match the resolver's mkBuiltinSig; params are `Any` per prelude convention. +function Any_type_to_Any (obj: Any) : Any; +function Any_abs_to_Any (x: Any) : Any; +function Any_chr_to_Any (i: Any) : Any; +function Any_ord_to_Any (c: Any) : Any; +function Any_getattr_to_Any (obj: Any, name: Any) : Any; +function Any_setattr_to_Any (obj: Any, name: Any, value: Any) : Any; +function to_int_any (v: Any) : Any; +function to_float_any (v: Any) : Any; +function PDiv (v1: Any, v2: Any) : Any; +function PBitAnd (v1: Any, v2: Any) : Any; +function PBitOr (v1: Any, v2: Any) : Any; +function PBitXor (v1: Any, v2: Any) : Any; +function Any_max_to_Any (v1: Any, v2: Any) : Any; +function Any_min_to_Any (v1: Any, v2: Any) : Any; +function Any_hasattr_to_bool (v: Any, attr: Any) : bool; +function Any_any_to_bool (v: Any) : bool; +function Any_all_to_bool (v: Any) : bool; +function Any_zip_to_Any (v1: Any, v2: Any) : Any; +function Any_map_to_Any (f: Any, v: Any) : Any; +function Any_filter_to_Any (f: Any, v: Any) : Any; +function Any_sorted_to_Any (v: Any) : Any; +function Any_reversed_to_Any (v: Any) : Any; +function Any_tuple_to_Any (v: Any) : Any; +function Any_frozenset_to_Any (v: Any) : Any; // ///////////////////////////////////////////////////////////////////////////////////// // Modelling of Python unary operations @@ -751,6 +799,23 @@ function PMul (v1: Any, v2: Any) : Any exception(UndefinedError ("Operand Type is not defined")) }; +function PIs (v1: Any, v2: Any) : bool; +function PIsNot (v1: Any, v2: Any) : bool; +function PInvert (v1: Any) : Any; + +// Composite ↔ Any bridge stubs (uninterpreted, sound: the value round-trips). +// The resolver's coercion realizer boxes a class instance into Any via the bare +// constructor-style `from_Composite` and recovers the pointer via the accessor-style +// `Any..as_Composite!`. `Composite` cannot be named in the prelude (it is synthesized +// by heapParameterizationPass), so the parameter is typed via `re_Match` — a named +// composite that `compositeRefToComposite` (type-hierarchy pass) flattens to the flat +// `Composite` datatype, so at Core these are `Composite → Any` / `Any → Composite`, +// matching the boxed/unboxed class pointer. (`from_Composite` is BARE, not `Any..`: +// the `Any..` prefix triggers accessor `!`-name-mangling for a constructor-style name.) +function Any..as_Composite! (v: Any) : re_Match; +function Any..isfrom_Composite (v: Any) : bool; +function from_Composite (v: re_Match) : Any; + function PFloorDiv (v1: Any, v2: Any) : Any requires (Any..isfrom_bool(v2)==>Any..as_bool!(v2)) && (Any..isfrom_int(v2)==>Any..as_int!(v2)!=0) { @@ -1095,10 +1160,13 @@ procedure print(msg : Any, opt : Any, sep : Any, end : Any, file : Any, flush : Parse the Laurel DDM prelude into a Laurel Program. -/ --- Prelude functions that may return an exception value as Any. --- We should make sure that all functions in this list propagate the exceptions from their arguments. -public def AnyMaybeExceptionList := ["Any_get!", "Any_set!", "Any_sets!", "PNeg", "PBitNot", "PNot", "PAdd", "PSub", "PMul", - "PFloorDiv", "PLt", "PLe", "PGt", "PGe", "PPow", "PMod", "PLShift", "PRShift", "PAnd", "POr"] +-- Runtime functions that may propagate exception values encoded as Any. +-- These functions return `Any` where the value might be `exception(...)`. +-- Used to determine which procs can elevate their caller's grade to `.err`. +public def AnyMaybeExceptionList : Std.HashSet String := + (["Any_get!", "Any_set!", "Any_sets!", "PNeg", "PBitNot", "PNot", "PAdd", "PSub", "PMul", + "PFloorDiv", "PLt", "PLe", "PGt", "PGe", "PPow", "PMod", "PLShift", "PRShift", "PAnd", "POr"] + : List String).foldl (fun s n => s.insert n) {} public def pythonRuntimeLaurelPart : Laurel.Program := match Laurel.TransM.run (some $ .file "") (Laurel.parseProgram pythonRuntimeLaurelPartDDM) with diff --git a/StrataPython/StrataPython/Resolution.lean b/StrataPython/StrataPython/Resolution.lean new file mode 100644 index 0000000000..63254fd231 --- /dev/null +++ b/StrataPython/StrataPython/Resolution.lean @@ -0,0 +1,1825 @@ +/- + Copyright Strata Contributors + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module + +public import Strata.Languages.Laurel.LaurelAST +public import StrataPython.PythonDialect +import StrataDDM.Util.SourceRange +import StrataPython.ReadPython + +/-! +# Pass 1: Name Resolution + +Resolution is a fold over the Python AST that threads a growing context +as accumulator. Its job is to **disambiguate** what each AST node means +and attach the result as a `NodeInfo` annotation. The process of +disambiguation produces Laurel-ready identifiers and auxiliary data +(FuncSig, field lists) that Translation uses mechanically. + +**Input:** `Array (StrataPython.stmt SourceRange)` (raw, unscoped) +**Output:** `ResolvedPythonProgram` (scoped, every node annotated with NodeInfo) + +The output AST is the scoping derivation for the Python program — +every node carries proof of what it refers to. + +## Phase Distinction + +All Resolution types are purely Python-level. No `Laurel.Identifier` is +stored anywhere. Translation obtains Laurel identifiers by calling accessor +functions on the Python-level structures. This makes the phase boundary +explicit and prevents mixing. + +## What Resolution Does + +At the top level (module scope), each declaration extends the context: +- `def f(...)` → extends context, annotates FunctionDef with `.funcDecl sig` +- `class C` → extends context with class + methods, annotates with `.classDecl` +- `import M` → extends context internally (module tracked in Ctx only) +- `x : T = ...` → extends context with variable + +At each reference, Resolution annotates with the appropriate `NodeInfo`: +- Name use (variable/function/class) → `.variable name` +- Call (function) → `.funcCall sig` +- Call (class) → `.classNew className initSig` +- Call (method) → `.funcCall sig` (sig has `className = some _`) +- Attribute access → `.attribute name` (bare field name; Elaboration resolves based on receiver type) +- BinOp/Compare/UnaryOp → `.funcCall sig` (operator runtime procedure) +- Unresolvable → `.unresolved` +- Non-reference → `.irrelevant` + +## What Resolution Does NOT + +- Determine effects (Elaboration does that) +- Map PythonType → HighType (Translation does that) +- Emit Laurel constructs (Translation does that) +- Resolve field access to class (Elaboration does that via synthesized receiver type) +-/ + +namespace StrataPython.Resolution + +open Strata.Laurel +open StrataDDM +open StrataPython + +public section + +/-! ## Core Types + +`PythonIdentifier` is a newtype with a private constructor. The only ways to +create one are from the AST (`.fromAst`), from an import path (`.fromImport`), +or for builtins (`.builtin`). This prevents fabrication of identifiers like +`"ClassName@method"` — all identifiers trace back to source or builtins. -/ + +abbrev PythonExpr := StrataPython.expr SourceRange +abbrev PythonStmt := StrataPython.stmt SourceRange +abbrev PythonProgram := Array PythonStmt +abbrev PythonType := PythonExpr +/-- A Python identifier with a private constructor. Can only be created via `.fromAst`, + `.fromImport`, or `.builtin` — preventing fabrication of identifiers from arbitrary strings. -/ +structure PythonIdentifier where + private mk :: + private val : String + deriving BEq, Hashable, Inhabited, Repr + +def PythonIdentifier.fromAst (n : Ann String SourceRange) : PythonIdentifier := + ⟨n.val⟩ + +def PythonIdentifier.fromImport (modName : Ann String SourceRange) : PythonIdentifier := + match modName.val.splitOn "." with + | first :: _ => ⟨first⟩ + | [] => ⟨modName.val⟩ + +def PythonIdentifier.builtin (name : String) : PythonIdentifier := + ⟨name⟩ + +/-! ## Intermediate Types (mutually recursive) + +These types are mutually recursive because `ParamList` stores resolved default +expressions (`StrataPython.expr ResolvedAnn`) which depend on `ResolvedAnn` which +depends on `NodeInfo` which depends on `FuncSig` which depends on `ParamList`. + +**FuncParams** distinguishes instance methods (with explicit receiver) from +static functions. The receiver is NOT in `ParamList` — it's separated so that +`matchArgs` can handle it correctly (receiver gets its own slot in the zip-fold). + +**FuncSig** carries the Python-level function signature. `params` and `locals` +are private — Translation accesses them only via `matchArgs`, `laurelDeclInputs`, +and `laurelLocals` accessors. + +**NodeInfo** is the output annotation on each AST node. Pattern matching on it +determines Translation's action. Complements: +- `funcDecl` / `funcCall` — declaration and use site of a function +- `classDecl` / `classNew` — declaration and instantiation site of a class +- `withCtx` — resolved `__enter__`/`__exit__` sigs on a with-item +- Operators are `funcCall` with correct arity (2 for binary, 1 for unary) -/ + +mutual + +/-- The parameter list of a function/method, split into required, optional (with defaults), + and keyword-only parameters. Defaults are resolved expressions (carry `ResolvedAnn`). -/ +structure ParamList where + /-- Parameters with no default value — must be provided at every call site. -/ + required : List (PythonIdentifier × PythonType) + /-- Parameters with default values — may be omitted at call sites. -/ + optional : List (PythonIdentifier × PythonType × StrataPython.expr ResolvedAnn) + /-- Keyword-only parameters (after `*` in Python). Default is optional. -/ + kwonly : List (PythonIdentifier × PythonType × Option (StrataPython.expr ResolvedAnn)) + +/-- Distinguishes instance methods (with explicit receiver) from static functions. + The receiver is NOT in `ParamList` — it gets its own slot in `matchArgs`. -/ +inductive FuncParams where + /-- Instance method: first Python param is the receiver (typically `self`). -/ + | instance (receiver : PythonIdentifier) (params : ParamList) + /-- Static function or top-level function: no receiver. -/ + | static (params : ParamList) + +/-- The complete signature of a Python function or method. Carries everything Translation + needs to emit a Laurel procedure declaration and match call-site arguments. -/ +structure FuncSig where + /-- The Python name of the function/method. -/ + name : PythonIdentifier + /-- If this is a method, the class it belongs to. `none` for top-level functions. -/ + className : Option PythonIdentifier + /-- Instance vs static params (receiver separated from ParamList). -/ + params : FuncParams + /-- The declared return type annotation (defaults to Any if absent). -/ + returnType : PythonType + /-- All local variables in the function body (computed by `computeLocals`). -/ + locals : List (PythonIdentifier × PythonType) + /-- Overload index for disambiguated naming. `none` for non-overloaded functions. -/ + overloadIndex : Option Nat := none + /-- The `**kwargs` parameter name, if present. A declared input (Any-typed) but not + matched positionally by `matchArgs`. -/ + kwargName : Option PythonIdentifier := none + +/-- The resolution annotation on each Python AST node. + Each variant carries exactly what Translation needs to emit Laurel. -/ +inductive NodeInfo where + /-- A variable reference (local, param, or global). -/ + | variable (name : PythonIdentifier) + /-- A function/method call site with the callee's full signature. -/ + | funcCall (sig : FuncSig) + /-- A function/method declaration site with its signature. -/ + | funcDecl (sig : FuncSig) + /-- A class instantiation (`ClassName(...)`) with class name and `__init__` sig. -/ + | classNew (className : PythonIdentifier) (initSig : FuncSig) + /-- A class declaration with its fields and method signatures. -/ + | classDecl (name : PythonIdentifier) (attributes : List (PythonIdentifier × PythonType)) (methods : List FuncSig) + /-- An attribute access (bare field name; Elaboration resolves via receiver type). -/ + | attribute (name : PythonIdentifier) + /-- A `with` item with resolved `__enter__` and `__exit__` signatures. -/ + | withCtx (enterSig : FuncSig) (exitSig : FuncSig) + /-- A reference that could not be resolved (unknown name/module). -/ + | unresolved + /-- A non-reference node (literals, operators as nodes, etc.). -/ + | irrelevant + +/-- The annotation type on resolved AST nodes: source range plus resolution info. -/ +structure ResolvedAnn where + /-- Original source location. -/ + sr : SourceRange + /-- What Resolution determined about this node. -/ + info : NodeInfo + +end + +abbrev ResolvedPythonStmt := StrataPython.stmt ResolvedAnn +abbrev ResolvedPythonExpr := StrataPython.expr ResolvedAnn + +instance : Inhabited ParamList where default := { required := [], optional := [], kwonly := [] } +instance : Inhabited FuncParams where default := .static default +instance : Inhabited FuncSig where default := { name := default, className := none, params := default, returnType := .Name SourceRange.none ⟨SourceRange.none, "Any"⟩ (.Load SourceRange.none), locals := [] } +instance : Inhabited NodeInfo where default := .irrelevant +instance : Inhabited ResolvedAnn where default := { sr := .none, info := .irrelevant } + +/-- The output of Resolution: fully-annotated AST plus module-level local list. -/ +structure ResolvedPythonProgram where + /-- The resolved top-level statements. -/ + stmts : Array ResolvedPythonStmt + /-- Module-level local variables (assignment targets at module scope). -/ + moduleLocals : List (PythonIdentifier × PythonType) + +/-! ## Internal Context + +Resolution's working state — NOT exposed to Translation. `Ctx` maps +`PythonIdentifier` keys to `CtxEntry` values. Keys are bare Python names +from the AST (no fabricated compound keys like "ClassName@method"). + +Method lookup goes through `CtxEntry.class_`'s method list, not through +top-level keys. This prevents name collision between methods of different +classes with the same name. + +Within a class body, the context is extended with: +- `self` typed as the enclosing class (enables method resolution on `self`) +- All methods registered under their bare Python names (enables `self.method()` lookup) + +Within a function body, the context is extended with: +- Parameters (a param with no annotation does NOT override a more specific + type already in context, e.g. `self` typed by the enclosing class) +- Locals (Python's scoping rule: any assignment target in the body is function-local) +- FunctionDef/ClassDef names are NOT included in locals (they're declarations) -/ + +/-- An entry in Resolution's context. Determines what a `PythonIdentifier` key refers to. -/ +inductive CtxEntry where + /-- A function or method with its full signature. -/ + | function (sig : FuncSig) + /-- A class with its field list and method signatures. + `methods` holds eagerly-resolved sigs (user classes); `methodAsts` holds raw + method statements for lazy on-demand resolution (imported classes). -/ + | class_ (name : PythonIdentifier) (fields : List (PythonIdentifier × PythonType)) + (methods : List (PythonIdentifier × FuncSig)) + (methodAsts : List (PythonIdentifier × PythonStmt) := []) + /-- A variable with its type annotation. -/ + | variable (ty : PythonType) + /-- An overloaded function: signatures under the same name, matched in order. + Each carries its index, sig, and raw AST (for on-demand body resolution). -/ + | overloadedFunction (overloads : List (Nat × FuncSig × Option PythonStmt)) + /-- An imported module with its resolved context. -/ + | module_ (moduleCtx : Std.DHashMap.Raw PythonIdentifier (fun _ => CtxEntry)) + /-- An imported name whose type/kind is unknown. -/ + | unresolved + deriving Inhabited + +abbrev Ctx := Std.HashMap PythonIdentifier CtxEntry + +/-- An imported module with its source path (for cache filename) and resolved program. -/ +structure ImportedModule where + sourcePath : System.FilePath + program : ResolvedPythonProgram + +/-- State for the resolution monad: collects resolved imported module programs. -/ +structure ResolveState where + importedModules : Array ImportedModule := #[] + resolvedPaths : Std.HashMap String Ctx := {} + /-- Imported class methods resolved on demand (qualified name → resolved FunctionDef stmt). + The pipeline translates only these, not whole imported modules. -/ + demandedMethods : Std.HashMap String ResolvedPythonStmt := {} + /-- Imported top-level functions / overloads resolved on demand + (disambiguated name → resolved FunctionDef stmt). -/ + demandedFunctions : Std.HashMap String ResolvedPythonStmt := {} + /-- Imported classes whose methods/inits were demanded (class name → (id, fields)). + The pipeline emits a Composite type definition for each. -/ + demandedClasses : Std.HashMap String (PythonIdentifier × List (PythonIdentifier × PythonType)) := {} + +/-- The resolution monad. Reader carries baseDir, State collects imported module programs. -/ +abbrev ResolveM := ReaderT System.FilePath (StateT ResolveState (EIO String)) + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Annotation Extraction +-- ═══════════════════════════════════════════════════════════════════════════════ + +/-- Extract a PythonType from an optional annotation. No annotation defaults to Any. -/ +def annotationToPythonType (ann : Option PythonExpr) : PythonType := + match ann with + | some expr => expr + | none => .Name SourceRange.none ⟨SourceRange.none, "Any"⟩ (.Load SourceRange.none) + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Function Locals (Python scoping: assignment anywhere in body → function-local) +-- ═══════════════════════════════════════════════════════════════════════════════ + +mutual +/-- Collects walrus operator (`:=`) targets from comprehension iterables and filters. -/ +partial def collectWalrusFromComprehensions (comps : List (StrataPython.comprehension SourceRange)) : List PythonIdentifier := + comps.flatMap fun comp => + match comp with + | .mk_comprehension _ _target iter ifs _isAsync => + collectWalrusNames iter ++ ifs.val.toList.flatMap collectWalrusNames + +/-- Extracts assigned names from an assignment target (handles tuple/list unpacking, starred). -/ +partial def collectNamesFromTarget (target : PythonExpr) : List PythonIdentifier := + match target with + | .Name _ n _ => [PythonIdentifier.fromAst n] + | .Tuple _ elems _ => elems.val.toList.flatMap collectNamesFromTarget + | .List _ elems _ => elems.val.toList.flatMap collectNamesFromTarget + | .Starred _ inner _ => collectNamesFromTarget inner + | .Subscript _ _ _ _ => [] + | .Attribute _ _ _ _ => [] + | e => collectWalrusNames e + +/-- Recursively finds all walrus operator (`:=`) targets within an expression tree. -/ +partial def collectWalrusNames (expr : PythonExpr) : List PythonIdentifier := + match expr with + | .NamedExpr _ target _ => collectNamesFromTarget target + | .BinOp _ left _ right => collectWalrusNames left ++ collectWalrusNames right + | .BoolOp _ _ operands => operands.val.toList.flatMap collectWalrusNames + | .UnaryOp _ _ operand => collectWalrusNames operand + | .Compare _ left _ comparators => collectWalrusNames left ++ comparators.val.toList.flatMap collectWalrusNames + | .Call _ func args kwargs => + collectWalrusNames func ++ args.val.toList.flatMap collectWalrusNames ++ + kwargs.val.toList.flatMap fun kw => match kw with | .mk_keyword _ _ val => collectWalrusNames val + | .IfExp _ test body orelse => collectWalrusNames test ++ collectWalrusNames body ++ collectWalrusNames orelse + | .Dict _ keys vals => keys.val.toList.flatMap (fun k => match k with | .some_expr _ e => collectWalrusNames e | .missing_expr _ => []) ++ vals.val.toList.flatMap collectWalrusNames + | .Set _ elts => elts.val.toList.flatMap collectWalrusNames + | .ListComp _ elt generators => collectWalrusNames elt ++ collectWalrusFromComprehensions generators.val.toList + | .SetComp _ elt generators => collectWalrusNames elt ++ collectWalrusFromComprehensions generators.val.toList + | .DictComp _ key value generators => collectWalrusNames key ++ collectWalrusNames value ++ collectWalrusFromComprehensions generators.val.toList + | .GeneratorExp _ elt generators => collectWalrusNames elt ++ collectWalrusFromComprehensions generators.val.toList + | .Await _ inner => collectWalrusNames inner + | .Yield _ valOpt => match valOpt.val with | some v => collectWalrusNames v | none => [] + | .YieldFrom _ inner => collectWalrusNames inner + | .FormattedValue _ value _ _ => collectWalrusNames value + | .JoinedStr _ values => values.val.toList.flatMap collectWalrusNames + | .Subscript _ obj slice _ => collectWalrusNames obj ++ collectWalrusNames slice + | .Attribute _ obj _ _ => collectWalrusNames obj + | .Starred _ inner _ => collectWalrusNames inner + | .Tuple _ elems _ => elems.val.toList.flatMap collectWalrusNames + | .List _ elems _ => elems.val.toList.flatMap collectWalrusNames + | .Slice _ start stop step => + (match start.val with | some e => collectWalrusNames e | none => []) ++ + (match stop.val with | some e => collectWalrusNames e | none => []) ++ + (match step.val with | some e => collectWalrusNames e | none => []) + | .Name _ _ _ => [] + | .Constant _ _ _ => [] + | .Lambda _ _ _ => [] + | .TemplateStr _ _ => [] + | .Interpolation _ _ _ _ _ => [] +end + +/-- Collects all local variable bindings from a statement (assignment targets, for targets, + except-as names, with-as names, walrus targets). Recurses into sub-blocks but NOT into + nested FunctionDef/ClassDef (those introduce their own scope). -/ +partial def collectLocalsFromStmt (s : PythonStmt) : List (PythonIdentifier × PythonType) := + match s with + | .Assign _ targets value _ => + let targetNames := targets.val.toList.flatMap fun target => + (collectNamesFromTarget target).map fun n => (n, annotationToPythonType none) + let rhsWalrus := (collectWalrusNames value).map fun n => (n, annotationToPythonType none) + targetNames ++ rhsWalrus + | .AnnAssign _ target annotation valueOpt _ => + -- Trust the user's annotation (v2 verbatim): an annotated local keeps its declared + -- type. The coercion mechanism reconciles the boxed-`Any` RHS with it. + let targetNames := (collectNamesFromTarget target).map fun n => (n, annotation) + let rhsWalrus := match valueOpt.val with + | some v => (collectWalrusNames v).map fun n => (n, annotationToPythonType none) + | none => [] + targetNames ++ rhsWalrus + | .AugAssign _ target _ value => + let targetNames := (collectNamesFromTarget target).map fun n => (n, annotationToPythonType none) + let rhsWalrus := (collectWalrusNames value).map fun n => (n, annotationToPythonType none) + targetNames ++ rhsWalrus + | .If _ test bodyStmts elseStmts => + (collectWalrusNames test).map (fun n => (n, annotationToPythonType none)) ++ + bodyStmts.val.toList.flatMap collectLocalsFromStmt ++ + elseStmts.val.toList.flatMap collectLocalsFromStmt + | .For _ target iter bodyStmts orelse _ => + let targetNames := (collectNamesFromTarget target).map fun n => (n, annotationToPythonType none) + let iterWalrus := (collectWalrusNames iter).map fun n => (n, annotationToPythonType none) + targetNames ++ iterWalrus ++ + bodyStmts.val.toList.flatMap collectLocalsFromStmt ++ + orelse.val.toList.flatMap collectLocalsFromStmt + | .While _ cond bodyStmts orelse => + (collectWalrusNames cond).map (fun n => (n, annotationToPythonType none)) ++ + bodyStmts.val.toList.flatMap collectLocalsFromStmt ++ + orelse.val.toList.flatMap collectLocalsFromStmt + | .Try _ bodyStmts handlers orelse finalbody => + let handlerLocals := handlers.val.toList.flatMap fun h => + match h with + | .ExceptHandler _ _ maybeName handlerBody => + let errorVar := match maybeName.val with + | some n => [(PythonIdentifier.fromAst n, annotationToPythonType none)] + | none => [] + errorVar ++ handlerBody.val.toList.flatMap collectLocalsFromStmt + bodyStmts.val.toList.flatMap collectLocalsFromStmt ++ + handlerLocals ++ + orelse.val.toList.flatMap collectLocalsFromStmt ++ + finalbody.val.toList.flatMap collectLocalsFromStmt + | .TryStar _ bodyStmts handlers orelse finalbody => + let handlerLocals := handlers.val.toList.flatMap fun h => + match h with + | .ExceptHandler _ _ maybeName handlerBody => + let errorVar := match maybeName.val with + | some n => [(PythonIdentifier.fromAst n, annotationToPythonType none)] + | none => [] + errorVar ++ handlerBody.val.toList.flatMap collectLocalsFromStmt + bodyStmts.val.toList.flatMap collectLocalsFromStmt ++ + handlerLocals ++ + orelse.val.toList.flatMap collectLocalsFromStmt ++ + finalbody.val.toList.flatMap collectLocalsFromStmt + | .With _ items bodyStmts _ => + let itemLocals := items.val.toList.flatMap fun item => + match item with + | .mk_withitem _ ctxExpr optVars => + let ctxWalrus := (collectWalrusNames ctxExpr).map fun n => (n, annotationToPythonType none) + let varNames := match optVars.val with + | some varExpr => (collectNamesFromTarget varExpr).map fun n => (n, annotationToPythonType none) + | none => [] + ctxWalrus ++ varNames + itemLocals ++ bodyStmts.val.toList.flatMap collectLocalsFromStmt + | .AsyncWith _ items bodyStmts _ => + let itemLocals := items.val.toList.flatMap fun item => + match item with + | .mk_withitem _ ctxExpr optVars => + let ctxWalrus := (collectWalrusNames ctxExpr).map fun n => (n, annotationToPythonType none) + let varNames := match optVars.val with + | some varExpr => (collectNamesFromTarget varExpr).map fun n => (n, annotationToPythonType none) + | none => [] + ctxWalrus ++ varNames + itemLocals ++ bodyStmts.val.toList.flatMap collectLocalsFromStmt + | .AsyncFor _ target iter bodyStmts orelse _ => + let targetNames := (collectNamesFromTarget target).map fun n => (n, annotationToPythonType none) + let iterWalrus := (collectWalrusNames iter).map fun n => (n, annotationToPythonType none) + targetNames ++ iterWalrus ++ + bodyStmts.val.toList.flatMap collectLocalsFromStmt ++ + orelse.val.toList.flatMap collectLocalsFromStmt + | .Match _ subject cases => + let subjectW := (collectWalrusNames subject).map fun n => (n, annotationToPythonType none) + let caseLocals := cases.val.toList.flatMap fun c => + match c with + | .mk_match_case _ _pattern guardOpt caseBody => + -- TODO: extract pattern bindings from _pattern (requires walking StrataPython.pattern) + let guardW := match guardOpt.val with + | some g => (collectWalrusNames g).map fun n => (n, annotationToPythonType none) + | none => [] + guardW ++ caseBody.val.toList.flatMap collectLocalsFromStmt + subjectW ++ caseLocals + | .FunctionDef _ _ _ _ _ _ _ _ => [] + | .AsyncFunctionDef _ _ _ _ _ _ _ _ => [] + | .ClassDef _ _ _ _ _ _ _ => [] + | .Return _ valOpt => + match valOpt.val with + | some v => (collectWalrusNames v).map (fun n => (n, annotationToPythonType none)) + | none => [] + | .Delete _ targets => + targets.val.toList.flatMap fun t => (collectWalrusNames t).map fun n => (n, annotationToPythonType none) + | .Raise _ excOpt causeOpt => + let excW := match excOpt.val with | some e => collectWalrusNames e | none => [] + let causeW := match causeOpt.val with | some e => collectWalrusNames e | none => [] + (excW ++ causeW).map fun n => (n, annotationToPythonType none) + | .Assert _ test msgOpt => + let testW := collectWalrusNames test + let msgW := match msgOpt.val with | some e => collectWalrusNames e | none => [] + (testW ++ msgW).map fun n => (n, annotationToPythonType none) + | .Pass _ => [] + | .Break _ => [] + | .Continue _ => [] + | .Import _ aliases => + aliases.val.toList.filterMap fun alias => + match alias with + | .mk_alias _ modName asName => + let id := match asName.val with + | some aliasName => PythonIdentifier.fromAst aliasName + | none => PythonIdentifier.fromImport modName + some (id, annotationToPythonType none) + | .ImportFrom _ _ imports _ => + imports.val.toList.filterMap fun imp => + match imp with + | .mk_alias _ impName asName => + let id := match asName.val with + | some aliasName => PythonIdentifier.fromAst aliasName + | none => PythonIdentifier.fromAst impName + some (id, annotationToPythonType none) + | .Global _ _ => [] + | .Nonlocal _ _ => [] + | .Expr _ value => + (collectWalrusNames value).map (fun n => (n, annotationToPythonType none)) + | .TypeAlias _ nameExpr _ _ => + (collectNamesFromTarget nameExpr).map fun n => (n, annotationToPythonType none) + +/-- Collects names declared `global` or `nonlocal` in a function body (including nested blocks). + These are excluded from locals — they refer to enclosing/global scope. -/ +partial def collectGlobalNonlocalNames (s : PythonStmt) : List PythonIdentifier := + match s with + | .Global _ names => names.val.toList.map PythonIdentifier.fromAst + | .Nonlocal _ names => names.val.toList.map PythonIdentifier.fromAst + | .If _ _ body orelse => + body.val.toList.flatMap collectGlobalNonlocalNames ++ + orelse.val.toList.flatMap collectGlobalNonlocalNames + | .For _ _ _ body orelse _ => + body.val.toList.flatMap collectGlobalNonlocalNames ++ + orelse.val.toList.flatMap collectGlobalNonlocalNames + | .AsyncFor _ _ _ body orelse _ => + body.val.toList.flatMap collectGlobalNonlocalNames ++ + orelse.val.toList.flatMap collectGlobalNonlocalNames + | .While _ _ body orelse => + body.val.toList.flatMap collectGlobalNonlocalNames ++ + orelse.val.toList.flatMap collectGlobalNonlocalNames + | .Try _ body handlers orelse finalbody => + body.val.toList.flatMap collectGlobalNonlocalNames ++ + handlers.val.toList.flatMap (fun h => match h with + | .ExceptHandler _ _ _ hBody => hBody.val.toList.flatMap collectGlobalNonlocalNames) ++ + orelse.val.toList.flatMap collectGlobalNonlocalNames ++ + finalbody.val.toList.flatMap collectGlobalNonlocalNames + | .TryStar _ body handlers orelse finalbody => + body.val.toList.flatMap collectGlobalNonlocalNames ++ + handlers.val.toList.flatMap (fun h => match h with + | .ExceptHandler _ _ _ hBody => hBody.val.toList.flatMap collectGlobalNonlocalNames) ++ + orelse.val.toList.flatMap collectGlobalNonlocalNames ++ + finalbody.val.toList.flatMap collectGlobalNonlocalNames + | .With _ _ body _ => body.val.toList.flatMap collectGlobalNonlocalNames + | .AsyncWith _ _ body _ => body.val.toList.flatMap collectGlobalNonlocalNames + | .Match _ _ cases => + cases.val.toList.flatMap fun c => match c with + | .mk_match_case _ _ _ caseBody => caseBody.val.toList.flatMap collectGlobalNonlocalNames + | _ => [] + +/-- Python scoping: any assignment target in a function body is local to that function. + Collects all such names (excluding params, globals, nonlocals, and nested def/class names), + deduplicates preserving first-occurrence order. Used by `extractFuncSig` to populate `FuncSig.locals`. -/ +def computeLocals (body : PythonProgram) (paramNames : List PythonIdentifier) + : List (PythonIdentifier × PythonType) := + let allPairs := body.toList.flatMap collectLocalsFromStmt + let globalNonlocal := body.toList.flatMap collectGlobalNonlocalNames + let excluded : Std.HashSet PythonIdentifier := (paramNames ++ globalNonlocal).foldl (fun s n => s.insert n) {} + let (_, result) := allPairs.foldl (init := (excluded, ([] : List (PythonIdentifier × PythonType)))) fun acc pair => + let (seen, result) := acc + let (name, ty) := pair + if seen.contains name then (seen, result) + else (seen.insert name, result ++ [(name, ty)]) + result + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Extract FuncSig from a Python FunctionDef +-- ═══════════════════════════════════════════════════════════════════════════════ + +private def argToParam (arg : StrataPython.arg SourceRange) : PythonIdentifier × PythonType := + match arg with + | .mk_arg _ argName annotation _ => (PythonIdentifier.fromAst argName, annotationToPythonType annotation.val) + +private def extractAllParamNames (args : StrataPython.arguments SourceRange) : List PythonIdentifier := + match args with + | .mk_arguments _ posonlyargs argList vararg kwonlyargs _ kwarg _ => + let names := (posonlyargs.val.toList ++ argList.val.toList ++ kwonlyargs.val.toList).map fun arg => + match arg with | .mk_arg _ argName _ _ => PythonIdentifier.fromAst argName + let vaName := match vararg.val with | some (.mk_arg _ n _ _) => [PythonIdentifier.fromAst n] | none => [] + let kwName := match kwarg.val with | some (.mk_arg _ n _ _) => [PythonIdentifier.fromAst n] | none => [] + names ++ vaName ++ kwName + +private def hasStaticmethodDecorator (decorators : Array PythonExpr) : Bool := + decorators.any fun d => match d with + | .Name _ n _ => n.val == "staticmethod" + | _ => false + +private def hasOverloadDecorator (decorators : Array PythonExpr) : Bool := + decorators.any fun d => match d with + | .Name _ n _ => n.val == "overload" + | _ => false + +/-- Check if a call argument matches a parameter's type for overload resolution. + A Literal["value"] parameter matches a string constant with the same value. + All other parameter types match any argument (broad matching). -/ +private def argMatchesParam (arg : PythonExpr) (paramTy : PythonType) : Bool := + match paramTy with + | .Subscript _ (.Name _ tName _) (.Constant _ (.ConString _ litVal) _) _ => + if tName.val == "Literal" then + match arg with + | .Constant _ (.ConString _ argVal) _ => argVal == litVal + | _ => false + else true + | _ => true + +/-- Check if call arguments match an overload's parameter signature. -/ +private def matchOverload (sig : FuncSig) (args : Array PythonExpr) : Bool := + match sig.params with + | .static pl => + let params := pl.required + params.zip args.toList |>.all fun ((_, paramTy), arg) => argMatchesParam arg paramTy + | .instance _ pl => + let params := pl.required + params.zip args.toList |>.all fun ((_, paramTy), arg) => argMatchesParam arg paramTy + +/-! ## Python Name → Laurel Name Mapping + +The builtin mapping (`len` → `Any_len_to_Any`), method qualification +(`get_x` → `Account@get_x`), and module qualification +(`timedelta` → `datetime_timedelta`) are encoded in accessor functions. +Translation calls these accessors — it never fabricates Laurel identifiers +from strings or applies naming conventions itself. + +`PythonIdentifier.toLaurel` is identity — bare name to Laurel.Identifier. +`FuncSig.laurelName` applies the builtin mapping for top-level functions and +`ClassName@method` qualification for class methods. -/ + +def pythonNameToLaurel : String → String + | "len" => "Any_len_to_Any" + | "str" => "to_string_any" + | "int" => "to_int_any" + | "float" => "to_float_any" + | "bool" => "Any_to_bool" + | "abs" => "Any_abs_to_Any" + | "print" => "print" + | "repr" => "to_string_any" + | "type" => "Any_type_to_Any" + | "isinstance" => "Any_isinstance_to_bool" + | "hasattr" => "Any_hasattr_to_bool" + | "getattr" => "Any_getattr_to_Any" + | "setattr" => "Any_setattr_to_Any" + | "sorted" => "Any_sorted_to_Any" + | "reversed" => "Any_reversed_to_Any" + | "enumerate" => "Any_enumerate_to_Any" + | "zip" => "Any_zip_to_Any" + | "range" => "Any_range_to_Any" + | "list" => "Any_list_to_Any" + | "dict" => "Any_dict_to_Any" + | "set" => "Any_set_to_Any" + | "tuple" => "Any_tuple_to_Any" + | "min" => "Any_min_to_Any" + | "max" => "Any_max_to_Any" + | "sum" => "Any_sum_to_Any" + | "any" => "Any_any_to_bool" + | "all" => "Any_all_to_bool" + | "ord" => "Any_ord_to_Any" + | "chr" => "Any_chr_to_Any" + | "map" => "Any_map_to_Any" + | "filter" => "Any_filter_to_Any" + | "timedelta" => "timedelta_func" + | other => other + +def operatorToLaurel : StrataPython.operator SourceRange → String + | .Add _ => "PAdd" | .Sub _ => "PSub" | .Mult _ => "PMul" | .Div _ => "PDiv" + | .FloorDiv _ => "PFloorDiv" | .Mod _ => "PMod" | .Pow _ => "PPow" + | .BitAnd _ => "PBitAnd" | .BitOr _ => "PBitOr" | .BitXor _ => "PBitXor" + | .LShift _ => "PLShift" | .RShift _ => "PRShift" | .MatMult _ => "PMatMul" + +def cmpopToLaurel : StrataPython.cmpop SourceRange → String + | .Eq _ => "PEq" | .NotEq _ => "PNEq" | .Lt _ => "PLt" | .LtE _ => "PLe" + | .Gt _ => "PGt" | .GtE _ => "PGe" | .In _ => "PIn" | .NotIn _ => "PNotIn" + | .Is _ => "PIs" | .IsNot _ => "PIsNot" + +def unaryopToLaurel : StrataPython.unaryop SourceRange → String + | .Not _ => "PNot" | .USub _ => "PNeg" | .UAdd _ => "PPos" | .Invert _ => "PInvert" + +def boolopToLaurel : StrataPython.boolop SourceRange → String + | .And _ => "PAnd" | .Or _ => "POr" + +/-! ## Accessor Functions (Python → Laurel) + +Translation calls these to obtain `Laurel.Identifier` values on demand. +They encode the naming conventions in one place. Translation never +fabricates identifiers from raw strings — it calls these accessors. -/ + +/-- Identity: bare Python name → Laurel.Identifier. No mapping applied. + Used for variable names, param names, field names, local names. -/ +def PythonIdentifier.toLaurel (id : PythonIdentifier) : Identifier := + { text := id.val, uniqueId := none } + +/-- Produces the Laurel procedure name. Applies builtin mapping for top-level + functions (`len` → `Any_len_to_Any`) and class qualification for methods + (`get_x` with `className = some "Account"` → `Account@get_x`). -/ +def FuncSig.laurelName (sig : FuncSig) : Identifier := + let baseName := match sig.className with + | some cls => s!"{cls.val}@{sig.name.val}" + | none => pythonNameToLaurel sig.name.val + let name := match sig.overloadIndex with + | some idx => s!"{baseName}${idx}" + | none => baseName + { text := name, uniqueId := none } + +private def ParamList.allParams (pl : ParamList) : List (PythonIdentifier × PythonType) := + pl.required ++ pl.optional.map (fun (n, ty, _) => (n, ty)) ++ pl.kwonly.map (fun (n, ty, _) => (n, ty)) + +/-- All procedure inputs as `(Laurel.Identifier × PythonType)`. For instance + methods, includes the receiver as first element (typed as the OWNING CLASS, not + Any). For static functions, just the params. Translation uses this to declare + procedure inputs. Inputs are named `$in_X` at the Laurel level (body uses mutable + local `X`). + + The receiver MUST be typed as the class (not `Any`): the kbd Laurel resolver + resolves `self.field` via the receiver's static type (`resolveFieldRef` → + `targetTypeName` → type scope). With `self : Any` the field would instead resolve + to a same-named parameter (e.g. `__init__(self, name)` writing `self.name`), giving + "resolveQualifiedFieldName resolved to something other than a field". This matches + the proven PythonToLaurel pipeline, which types `self` as the class/Composite. -/ +def FuncSig.laurelDeclInputs (sig : FuncSig) : List (Identifier × PythonType) := + let anyTy : PythonType := .Name SourceRange.none ⟨SourceRange.none, "Any"⟩ (.Load SourceRange.none) + let base := match sig.params with + | .instance recv pl => + let selfTy : PythonType := match sig.className with + | some cls => .Name SourceRange.none ⟨SourceRange.none, cls.val⟩ (.Load SourceRange.none) + | none => anyTy + ({ text := recv.val, uniqueId := none }, selfTy) :: pl.allParams.map fun (id, ty) => ({ text := id.val, uniqueId := none }, ty) + | .static pl => + pl.allParams.map fun (id, ty) => ({ text := id.val, uniqueId := none }, ty) + match sig.kwargName with + | some kw => base ++ [({ text := kw.val, uniqueId := none }, anyTy)] + | none => base + +/-- Zip-fold arg matching. Each param slot is filled in order: + 1. If a positional arg remains → consume it + 2. Else if a kwarg matches by name → use it + 3. Else if a default exists → translate it via `translateDefault` + 4. Else → panic (Resolution bug: required param without arg) + + Includes receiver slot for instance methods. Lives in Resolution + because it accesses private `ParamList` fields and resolved defaults. -/ +def FuncSig.matchArgs [Monad m] [Inhabited (m α)] (sig : FuncSig) (posArgs : List α) (kwargs : List (String × α)) + (translateDefault : ResolvedPythonExpr → m α) (mkKwargs : m (Option α) := pure none) : m (List α) := do + let (receiverSlot, pl) := match sig.params with + | .instance recv pl => ([(recv.val, (none : Option ResolvedPythonExpr))], pl) + | .static pl => ([], pl) + let slots : List (String × Option ResolvedPythonExpr) := + receiverSlot ++ + pl.required.map (fun (id, _) => (id.val, none)) ++ + pl.optional.map (fun (id, _, dflt) => (id.val, some dflt)) ++ + pl.kwonly.map (fun (id, _, dflt) => (id.val, dflt)) + let (result, _) ← slots.foldlM (fun (acc, pos) (pName, dflt) => do + match pos with + | a :: rest => pure (acc ++ [a], rest) + | [] => + let v ← match kwargs.find? (fun (k, _) => k == pName) with + | some (_, v) => pure v + | none => match dflt with + | some d => translateDefault d + | none => panic! "Resolution bug: required param without arg" + pure (acc ++ [v], []) + ) ([], posArgs) + -- Append a value for the `**kwargs` declared input, if present. + if sig.kwargName.isSome then + let kwOpt ← mkKwargs + match kwOpt with + | some kw => return (result ++ [kw]) + | none => return result + else + return result + +/-- Locals as `(Laurel.Identifier × PythonType)` for `LocalVariable` declarations. -/ +def FuncSig.laurelLocals (sig : FuncSig) : List (Identifier × PythonType) := + sig.locals.map fun (id, ty) => ({ text := id.val, uniqueId := none }, ty) + +/-- The receiver's Laurel.Identifier, if this is an instance method. -/ +def FuncSig.laurelReceiver (sig : FuncSig) : Option Identifier := + match sig.params with + | .instance recv _ => some { text := recv.val, uniqueId := none } + | .static _ => none + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Initial Context: Python Builtins +-- ═══════════════════════════════════════════════════════════════════════════════ + +private def anyType : PythonType := .Name SourceRange.none ⟨SourceRange.none, "Any"⟩ (.Load SourceRange.none) +private def intType : PythonType := .Name SourceRange.none ⟨SourceRange.none, "int"⟩ (.Load SourceRange.none) +private def strType : PythonType := .Name SourceRange.none ⟨SourceRange.none, "str"⟩ (.Load SourceRange.none) +private def boolType : PythonType := .Name SourceRange.none ⟨SourceRange.none, "bool"⟩ (.Load SourceRange.none) + +private def mkBuiltinSig (pythonName : String) (params : List (String × PythonType)) (retTy : PythonType) : FuncSig := + let required := params.map fun (n, ty) => (PythonIdentifier.builtin n, ty) + { name := .builtin pythonName, className := none, + params := .static { required, optional := [], kwonly := [] }, + returnType := retTy, locals := [] } + +/-- A resolved `None` literal, used as the default for optional builtin params. -/ +private def resolvedNoneExpr : ResolvedPythonExpr := + .Constant default (.ConNone default) ⟨default, none⟩ + +/-- Like `mkBuiltinSig` but with extra OPTIONAL params (each defaulting to `None`). Needed + for builtins whose runtime Laurel proc has more params than are usually supplied — e.g. + `print(msg, opt, sep, end, file, flush)`: a call `print("x")` must be padded to the + proc's full arity (via `matchArgs` filling the `None` defaults) or the Core call-arity + check fails ("input length and args length mismatch"). -/ +private def mkBuiltinSigOpt (pythonName : String) (required : List (String × PythonType)) + (optional : List (String × PythonType)) (retTy : PythonType) : FuncSig := + let req := required.map fun (n, ty) => (PythonIdentifier.builtin n, ty) + let opt := optional.map fun (n, ty) => (PythonIdentifier.builtin n, ty, resolvedNoneExpr) + { name := .builtin pythonName, className := none, + params := .static { required := req, optional := opt, kwonly := [] }, + returnType := retTy, locals := [] } + +/-- The initial context: all Python builtins with their FuncSig (correct arity, param names, + return types). Resolution starts from this and extends with user-defined declarations. -/ +def builtinContext : Ctx := + let entries : List (PythonIdentifier × CtxEntry) := [ + (.builtin "len", .function (mkBuiltinSig "len" [("obj", anyType)] intType)), + (.builtin "str", .function (mkBuiltinSig "str" [("obj", anyType)] strType)), + (.builtin "int", .function (mkBuiltinSig "int" [("obj", anyType)] intType)), + (.builtin "float", .function (mkBuiltinSig "float" [("obj", anyType)] anyType)), + (.builtin "bool", .function (mkBuiltinSig "bool" [("obj", anyType)] boolType)), + (.builtin "print", .function (mkBuiltinSigOpt "print" [("msg", anyType)] + [("opt", anyType), ("sep", anyType), ("end", anyType), ("file", anyType), ("flush", anyType)] anyType)), + (.builtin "repr", .function (mkBuiltinSig "repr" [("obj", anyType)] strType)), + (.builtin "type", .function (mkBuiltinSig "type" [("obj", anyType)] anyType)), + (.builtin "isinstance", .function (mkBuiltinSig "isinstance" [("obj", anyType), ("cls", anyType)] boolType)), + (.builtin "hasattr", .function (mkBuiltinSig "hasattr" [("obj", anyType), ("name", strType)] boolType)), + (.builtin "getattr", .function (mkBuiltinSig "getattr" [("obj", anyType), ("name", strType)] anyType)), + (.builtin "setattr", .function (mkBuiltinSig "setattr" [("obj", anyType), ("name", strType), ("value", anyType)] anyType)), + (.builtin "sorted", .function (mkBuiltinSig "sorted" [("iterable", anyType)] anyType)), + (.builtin "reversed", .function (mkBuiltinSig "reversed" [("seq", anyType)] anyType)), + (.builtin "enumerate", .function (mkBuiltinSig "enumerate" [("iterable", anyType)] anyType)), + (.builtin "zip", .function (mkBuiltinSig "zip" [("a", anyType), ("b", anyType)] anyType)), + (.builtin "range", .function (mkBuiltinSig "range" [("stop", anyType)] anyType)), + (.builtin "list", .function (mkBuiltinSig "list" [("iterable", anyType)] anyType)), + (.builtin "dict", .function (mkBuiltinSig "dict" [("iterable", anyType)] anyType)), + (.builtin "set", .function (mkBuiltinSig "set" [("iterable", anyType)] anyType)), + (.builtin "tuple", .function (mkBuiltinSig "tuple" [("iterable", anyType)] anyType)), + (.builtin "min", .function (mkBuiltinSig "min" [("a", anyType), ("b", anyType)] anyType)), + (.builtin "max", .function (mkBuiltinSig "max" [("a", anyType), ("b", anyType)] anyType)), + (.builtin "sum", .function (mkBuiltinSig "sum" [("iterable", anyType)] anyType)), + (.builtin "any", .function (mkBuiltinSig "any" [("iterable", anyType)] boolType)), + (.builtin "all", .function (mkBuiltinSig "all" [("iterable", anyType)] boolType)), + (.builtin "abs", .function (mkBuiltinSig "abs" [("x", anyType)] anyType)), + (.builtin "ord", .function (mkBuiltinSig "ord" [("c", strType)] intType)), + (.builtin "chr", .function (mkBuiltinSig "chr" [("i", intType)] strType)), + (.builtin "map", .function (mkBuiltinSig "map" [("func", anyType), ("iterable", anyType)] anyType)), + (.builtin "filter", .function (mkBuiltinSig "filter" [("func", anyType), ("iterable", anyType)] anyType)) + ] + entries.foldl (fun ctx (name, info) => ctx.insert name info) {} + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Spine type resolution (chases .Name and .Attribute chains) +-- ═══════════════════════════════════════════════════════════════════════════════ + +-- typeOfExpr and resolveMethodCall moved into the mutual block below + +-- resolveMethodCall moved into the mutual block below + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- AST Annotation Mapping (f : SourceRange → ResolvedAnn through the tree) +-- ═══════════════════════════════════════════════════════════════════════════════ + +private def mapAnnVal (f : α → β) (a : Ann T α) : Ann T β := ⟨f a.ann, a.val⟩ +private def mapAnnOpt (f : α → β) (mapT : T₁ → T₂) (a : Ann (Option T₁) α) : Ann (Option T₂) β := + ⟨f a.ann, a.val.map mapT⟩ +private def mapAnnArr (f : α → β) (mapT : T₁ → T₂) (a : Ann (Array T₁) α) : Ann (Array T₂) β := + ⟨f a.ann, a.val.map mapT⟩ + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- The Fold: resolve +-- +-- Threads Ctx as accumulator. Declarations extend it. References look up from it. +-- Non-reference nodes get .none. Reference nodes get their lookup result. +-- ═══════════════════════════════════════════════════════════════════════════════ + +mutual + +/-- Extracts a `ParamList` from Python's `arguments` AST node. Resolves default expressions + via `resolveExpr` so they carry `ResolvedAnn` annotations for later Translation use. -/ +partial def extractParamList (ctx : Ctx) (f : SourceRange → ResolvedAnn) (args : StrataPython.arguments SourceRange) : ResolveM ParamList := do + match args with + | .mk_arguments _ posonlyargs argList _ kwonlyargs kwDefaults kwarg defaults => + let posAndRegular := posonlyargs.val.toList ++ argList.val.toList + let allPosParams := posAndRegular.map argToParam + let defaultCount := defaults.val.size + let requiredCount := allPosParams.length - defaultCount + let required := allPosParams.take requiredCount + let optionalParams := allPosParams.drop requiredCount + let mut optional : List (PythonIdentifier × PythonType × ResolvedPythonExpr) := [] + for ((n, ty), dflt) in optionalParams.zip (defaults.val.toList) do + optional := optional ++ [(n, ty, ← resolveExpr ctx f dflt)] + let kwParams := kwonlyargs.val.toList.map argToParam + let mut kwonly : List (PythonIdentifier × PythonType × Option ResolvedPythonExpr) := [] + for ((n, ty), optExpr) in kwParams.zip (kwDefaults.val.toList) do + match optExpr with + | .some_expr _ e => kwonly := kwonly ++ [(n, ty, some (← resolveExpr ctx f e))] + | .missing_expr _ => kwonly := kwonly ++ [(n, ty, none)] + let _ := kwarg -- `**kwargs` registered separately by resolveFunctionBody + return { required, optional, kwonly } + +/-- Builds a complete `FuncSig` for a function/method definition. Determines instance vs static + (if `className` is set and no `@staticmethod`, first param becomes receiver), computes locals, + and stores the resolved param list. This is the single point where FuncSig is created. -/ +partial def extractFuncSig (ctx : Ctx) (f : SourceRange → ResolvedAnn) + (pythonName : PythonIdentifier) (className : Option PythonIdentifier) + (args : StrataPython.arguments SourceRange) (decorators : Array PythonExpr) + (returns : Ann (Option PythonExpr) SourceRange) + (body : PythonProgram) : ResolveM FuncSig := do + let paramList ← extractParamList ctx f args + let retTy := annotationToPythonType returns.val + let allParamNames := extractAllParamNames args + let locals := computeLocals body allParamNames + -- Include global variables as Any-typed locals so the elaborator finds them in scope + let globalNames := body.toList.flatMap collectGlobalNonlocalNames + let anyTy : PythonType := .Name SourceRange.none ⟨SourceRange.none, "Any"⟩ (.Load SourceRange.none) + let globalLocals := globalNames.filterMap fun n => + if locals.any (fun (ln, _) => ln == n) || allParamNames.any (fun p => p == n) then none + else some (n, anyTy) + let locals := locals ++ globalLocals + let funcParams := + if className.isNone || hasStaticmethodDecorator decorators then + .static paramList + else match paramList.required with + | (recv, _) :: rest => .instance recv { paramList with required := rest } + | [] => .static paramList + let kwargName := match args with + | .mk_arguments _ _ _ _ _ _ kwarg _ => match kwarg.val with + | some (.mk_arg _ n _ _) => some (PythonIdentifier.fromAst n) + | none => none + return { name := pythonName, className, params := funcParams, returnType := retTy, locals, kwargName } + +/-- Builds the body context for resolving statements inside a function. Extends ctx with + all params (including vararg/kwarg) and locals. Used by `resolveFuncDef` to create the + scope in which the function body is resolved. -/ +partial def resolveFunctionBody (ctx : Ctx) (f : SourceRange → ResolvedAnn) (args : StrataPython.arguments SourceRange) (body : PythonProgram) : ResolveM Ctx := do + let pl ← extractParamList ctx f args + let allParams := pl.required ++ pl.optional.map (fun (n, ty, _) => (n, ty)) ++ pl.kwonly.map (fun (n, ty, _) => (n, ty)) + let varargKwarg : List (PythonIdentifier × PythonType) := match args with + | .mk_arguments _ _ _ vararg _ _ kwarg _ => + let va := match vararg.val with | some a => [argToParam a] | none => [] + let kw := match kwarg.val with | some a => [argToParam a] | none => [] + va ++ kw + let allParamNames := extractAllParamNames args + let locals := computeLocals body allParamNames + let bodyCtx := allParams.foldl (fun c (n, ty) => c.insert n (CtxEntry.variable ty)) ctx + let bodyCtx := varargKwarg.foldl (fun c (n, ty) => c.insert n (CtxEntry.variable ty)) bodyCtx + return locals.foldl (fun c (n, ty) => c.insert n (CtxEntry.variable ty)) bodyCtx + +partial def resolveExprCtx (f : SourceRange → ResolvedAnn) : StrataPython.expr_context SourceRange → StrataPython.expr_context ResolvedAnn + | .Load a => .Load (f a) | .Store a => .Store (f a) | .Del a => .Del (f a) + +partial def resolveConstant (f : SourceRange → ResolvedAnn) : StrataPython.constant SourceRange → StrataPython.constant ResolvedAnn + | .ConTrue a => .ConTrue (f a) | .ConFalse a => .ConFalse (f a) + | .ConPos a n => .ConPos (f a) (mapAnnVal f n) | .ConNeg a n => .ConNeg (f a) (mapAnnVal f n) + | .ConString a s => .ConString (f a) (mapAnnVal f s) | .ConFloat a s => .ConFloat (f a) (mapAnnVal f s) + | .ConComplex a r i => .ConComplex (f a) (mapAnnVal f r) (mapAnnVal f i) + | .ConNone a => .ConNone (f a) | .ConEllipsis a => .ConEllipsis (f a) + | .ConBytes a b => .ConBytes (f a) (mapAnnVal f b) + +partial def resolveInt (f : SourceRange → ResolvedAnn) : StrataPython.int SourceRange → StrataPython.int ResolvedAnn + | .IntPos a n => .IntPos (f a) (mapAnnVal f n) | .IntNeg a n => .IntNeg (f a) (mapAnnVal f n) + +partial def resolveOperator (f : SourceRange → ResolvedAnn) : StrataPython.operator SourceRange → StrataPython.operator ResolvedAnn + | .Add a => .Add (f a) | .Sub a => .Sub (f a) | .Mult a => .Mult (f a) | .Div a => .Div (f a) + | .FloorDiv a => .FloorDiv (f a) | .Mod a => .Mod (f a) | .Pow a => .Pow (f a) + | .BitAnd a => .BitAnd (f a) | .BitOr a => .BitOr (f a) | .BitXor a => .BitXor (f a) + | .LShift a => .LShift (f a) | .RShift a => .RShift (f a) | .MatMult a => .MatMult (f a) + +partial def resolveBoolop (f : SourceRange → ResolvedAnn) : StrataPython.boolop SourceRange → StrataPython.boolop ResolvedAnn + | .And a => .And (f a) | .Or a => .Or (f a) + +partial def resolveUnaryop (f : SourceRange → ResolvedAnn) : StrataPython.unaryop SourceRange → StrataPython.unaryop ResolvedAnn + | .Not a => .Not (f a) | .USub a => .USub (f a) | .UAdd a => .UAdd (f a) | .Invert a => .Invert (f a) + +partial def resolveCmpop (f : SourceRange → ResolvedAnn) : StrataPython.cmpop SourceRange → StrataPython.cmpop ResolvedAnn + | .Eq a => .Eq (f a) | .NotEq a => .NotEq (f a) | .Lt a => .Lt (f a) | .LtE a => .LtE (f a) + | .Gt a => .Gt (f a) | .GtE a => .GtE (f a) | .Is a => .Is (f a) | .IsNot a => .IsNot (f a) + | .In a => .In (f a) | .NotIn a => .NotIn (f a) + +partial def resolveOptExpr (ctx : Ctx) (f : SourceRange → ResolvedAnn) : StrataPython.opt_expr SourceRange → ResolveM (StrataPython.opt_expr ResolvedAnn) + | .some_expr a e => do return .some_expr (f a) (← resolveExpr ctx f e) + | .missing_expr a => return .missing_expr (f a) + +partial def resolveKeyword (ctx : Ctx) (f : SourceRange → ResolvedAnn) : StrataPython.keyword SourceRange → ResolveM (StrataPython.keyword ResolvedAnn) + | .mk_keyword a arg val => do return .mk_keyword (f a) (mapAnnOpt f (mapAnnVal f) arg) (← resolveExpr ctx f val) + +partial def resolveArg (ctx : Ctx) (f : SourceRange → ResolvedAnn) : StrataPython.arg SourceRange → ResolveM (StrataPython.arg ResolvedAnn) + | .mk_arg a name ann tc => do + let rAnn ← match ann.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + return .mk_arg (f a) (mapAnnVal f name) ⟨f ann.ann, rAnn⟩ (mapAnnOpt f (mapAnnVal f) tc) + +partial def resolveArguments (ctx : Ctx) (f : SourceRange → ResolvedAnn) : StrataPython.arguments SourceRange → ResolveM (StrataPython.arguments ResolvedAnn) + | .mk_arguments a posonlyargs args vararg kwonlyargs kwDefaults kwarg defaults => do + let mut rPosonlyargs : Array (StrataPython.arg ResolvedAnn) := #[] + for arg in posonlyargs.val do rPosonlyargs := rPosonlyargs.push (← resolveArg ctx f arg) + let mut rArgs : Array (StrataPython.arg ResolvedAnn) := #[] + for arg in args.val do rArgs := rArgs.push (← resolveArg ctx f arg) + let rVararg ← match vararg.val with + | some a => pure (some (← resolveArg ctx f a)) + | none => pure none + let mut rKwonlyargs : Array (StrataPython.arg ResolvedAnn) := #[] + for arg in kwonlyargs.val do rKwonlyargs := rKwonlyargs.push (← resolveArg ctx f arg) + let mut rKwDefaults : Array (StrataPython.opt_expr ResolvedAnn) := #[] + for oe in kwDefaults.val do rKwDefaults := rKwDefaults.push (← resolveOptExpr ctx f oe) + let rKwarg ← match kwarg.val with + | some a => pure (some (← resolveArg ctx f a)) + | none => pure none + let mut rDefaults : Array ResolvedPythonExpr := #[] + for d in defaults.val do rDefaults := rDefaults.push (← resolveExpr ctx f d) + return .mk_arguments (f a) + ⟨f posonlyargs.ann, rPosonlyargs⟩ + ⟨f args.ann, rArgs⟩ + ⟨f vararg.ann, rVararg⟩ + ⟨f kwonlyargs.ann, rKwonlyargs⟩ + ⟨f kwDefaults.ann, rKwDefaults⟩ + ⟨f kwarg.ann, rKwarg⟩ + ⟨f defaults.ann, rDefaults⟩ + +partial def resolveComprehension (ctx : Ctx) (f : SourceRange → ResolvedAnn) (comp : StrataPython.comprehension SourceRange) : ResolveM (Ctx × StrataPython.comprehension ResolvedAnn) := do + match comp with + | .mk_comprehension a target iter ifs isAsync => + let targetNames := collectNamesFromTarget target + let compCtx := targetNames.foldl (fun c n => c.insert n (CtxEntry.variable (annotationToPythonType Option.none))) ctx + let rTarget ← resolveExpr compCtx f target + let rIter ← resolveExpr ctx f iter + let mut rIfs : Array ResolvedPythonExpr := #[] + for i in ifs.val do rIfs := rIfs.push (← resolveExpr compCtx f i) + return (compCtx, .mk_comprehension (f a) rTarget rIter ⟨f ifs.ann, rIfs⟩ (resolveInt f isAsync)) + +partial def resolveComprehensions (ctx : Ctx) (f : SourceRange → ResolvedAnn) (comps : List (StrataPython.comprehension SourceRange)) : ResolveM (Ctx × List (StrataPython.comprehension ResolvedAnn)) := do + let mut c := ctx + let mut resolved : List (StrataPython.comprehension ResolvedAnn) := [] + for comp in comps do + let (c', r) ← resolveComprehension c f comp + c := c' + resolved := resolved ++ [r] + return (c, resolved) + +partial def resolveTypeParam (ctx : Ctx) (f : SourceRange → ResolvedAnn) : StrataPython.type_param SourceRange → ResolveM (StrataPython.type_param ResolvedAnn) + | .TypeVar a name bound def_ => do + let rBound ← match bound.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + let rDef ← match def_.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + return .TypeVar (f a) (mapAnnVal f name) ⟨f bound.ann, rBound⟩ ⟨f def_.ann, rDef⟩ + | .TypeVarTuple a name def_ => do + let rDef ← match def_.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + return .TypeVarTuple (f a) (mapAnnVal f name) ⟨f def_.ann, rDef⟩ + | .ParamSpec a name def_ => do + let rDef ← match def_.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + return .ParamSpec (f a) (mapAnnVal f name) ⟨f def_.ann, rDef⟩ + +/-- The core expression resolver. Annotates each expression node with appropriate `NodeInfo`: + - `.Name` → look up in ctx, annotate with `.variable` + - `.Call` → determine callee (function/class/method), annotate with `.funcCall` or `.classNew` + - `.Attribute` → annotate with `.attribute` (bare field name; Elaboration resolves via receiver type) + - `.BinOp`/`.UnaryOp`/`.Compare`/`.BoolOp` → create operator FuncSig, annotate with `.funcCall` + - Comprehensions → extend ctx with iteration variables before resolving element expression -/ +partial def resolveExpr (ctx : Ctx) (f : SourceRange → ResolvedAnn) (e : PythonExpr) : ResolveM ResolvedPythonExpr := do + match e with + | .Name a n ectx => + let nId := PythonIdentifier.fromAst n + let info := match ctx[nId]? with + | some (.variable _) => .variable nId + | some (.function _) => .unresolved + | some (.overloadedFunction _) => .unresolved + | some (.class_ _ _ _ _) => .unresolved + | some (.module_ _) => .irrelevant + | some .unresolved => .unresolved + | none => .unresolved + return .Name { sr := a, info } (mapAnnVal f n) (resolveExprCtx f ectx) + | .Call a func args kwargs => + let callInfo : NodeInfo ← match func with + | .Name _ n _ => + let nId := PythonIdentifier.fromAst n + match ctx[nId]? with + | some (.function sig) => pure (.funcCall sig) + | some (.overloadedFunction overloads) => + let matched := overloads.find? fun (_, olSig, _) => + matchOverload olSig args.val + match matched with + | some (idx, sig, astOpt) => do + let sig' := { sig with overloadIndex := some idx } + match astOpt with + | some fAst => resolveFunctionAstSig ctx f sig' fAst + | none => pure () + pure (.funcCall sig') + | none => pure .unresolved + | some (.class_ cId _ methods _) => + let initId := PythonIdentifier.builtin "__init__" + match methods.find? (fun (mName, _) => mName == initId) with + | some (_, sig) => pure (.classNew cId sig) + | none => + let emptySig : FuncSig := { name := initId, className := some cId, params := .static {required := [], optional := [], kwonly := []}, returnType := anyType, locals := [] } + pure (.classNew cId emptySig) + | _ => pure .unresolved + | .Attribute _ receiver methodName _ => + resolveMethodCall ctx receiver methodName args.val + | _ => pure .unresolved + let rFunc ← resolveExpr ctx f func + let mut rArgs : Array ResolvedPythonExpr := #[] + for arg in args.val do + rArgs := rArgs.push (← resolveExpr ctx f arg) + let mut rKwargs : Array (StrataPython.keyword ResolvedAnn) := #[] + for kw in kwargs.val do + rKwargs := rKwargs.push (← resolveKeyword ctx f kw) + return .Call { sr := a, info := callInfo } rFunc ⟨f args.ann, rArgs⟩ ⟨f kwargs.ann, rKwargs⟩ + | .Attribute a obj attr ectx => + let rObj ← resolveExpr ctx f obj + -- A field access requires a value receiver. If the object is a module + -- (.irrelevant) or unresolved, the attribute is not a field of a value + -- (e.g. `sys.argv` is a module member); it resolves to .unresolved (→ hole). + let info := match rObj.ann.info with + | .irrelevant | .unresolved => .unresolved + | _ => .attribute (PythonIdentifier.fromAst attr) + return .Attribute { sr := a, info } rObj (mapAnnVal f attr) (resolveExprCtx f ectx) + | .Constant a c tc => return .Constant (f a) (resolveConstant f c) (mapAnnOpt f (mapAnnVal f) tc) + | .BinOp a left op right => + let opSig : FuncSig := { name := .builtin (operatorToLaurel op), className := none, params := .static {required := [(.builtin "left", anyType), (.builtin "right", anyType)], optional := [], kwonly := []}, returnType := anyType, locals := [] } + let rLeft ← resolveExpr ctx f left + let rRight ← resolveExpr ctx f right + return .BinOp { sr := a, info := .funcCall opSig } rLeft (resolveOperator f op) rRight + | .BoolOp a op operands => + let opSig : FuncSig := { name := .builtin (boolopToLaurel op), className := none, params := .static {required := [(.builtin "left", anyType), (.builtin "right", anyType)], optional := [], kwonly := []}, returnType := anyType, locals := [] } + let mut rOperands : Array ResolvedPythonExpr := #[] + for operand in operands.val do + rOperands := rOperands.push (← resolveExpr ctx f operand) + return .BoolOp { sr := a, info := .funcCall opSig } (resolveBoolop f op) ⟨f operands.ann, rOperands⟩ + | .UnaryOp a op operand => + let opSig : FuncSig := { name := .builtin (unaryopToLaurel op), className := none, params := .static {required := [(.builtin "operand", anyType)], optional := [], kwonly := []}, returnType := anyType, locals := [] } + let rOperand ← resolveExpr ctx f operand + return .UnaryOp { sr := a, info := .funcCall opSig } (resolveUnaryop f op) rOperand + | .Compare a left ops comps => + let opName := match ops.val[0]? with | some op => cmpopToLaurel op | none => "PEq" + let opSig : FuncSig := { name := .builtin opName, className := none, params := .static {required := [(.builtin "left", anyType), (.builtin "right", anyType)], optional := [], kwonly := []}, returnType := anyType, locals := [] } + let rLeft ← resolveExpr ctx f left + let mut rComps : Array ResolvedPythonExpr := #[] + for comp in comps.val do + rComps := rComps.push (← resolveExpr ctx f comp) + return .Compare { sr := a, info := .funcCall opSig } rLeft (mapAnnArr f (resolveCmpop f) ops) ⟨f comps.ann, rComps⟩ + | .IfExp a test body orelse => + let rTest ← resolveExpr ctx f test + let rBody ← resolveExpr ctx f body + let rElse ← resolveExpr ctx f orelse + return .IfExp (f a) rTest rBody rElse + | .Dict a keys vals => + let mut rKeys : Array (StrataPython.opt_expr ResolvedAnn) := #[] + for k in keys.val do + rKeys := rKeys.push (← resolveOptExpr ctx f k) + let mut rVals : Array ResolvedPythonExpr := #[] + for v in vals.val do + rVals := rVals.push (← resolveExpr ctx f v) + return .Dict (f a) ⟨f keys.ann, rKeys⟩ ⟨f vals.ann, rVals⟩ + | .Set a elts => + let mut rElts : Array ResolvedPythonExpr := #[] + for elt in elts.val do + rElts := rElts.push (← resolveExpr ctx f elt) + return .Set (f a) ⟨f elts.ann, rElts⟩ + | .ListComp a elt gens => + let (compCtx, resolvedGens) ← resolveComprehensions ctx f gens.val.toList + let rElt ← resolveExpr compCtx f elt + return .ListComp (f a) rElt ⟨f gens.ann, resolvedGens.toArray⟩ + | .SetComp a elt gens => + let (compCtx, resolvedGens) ← resolveComprehensions ctx f gens.val.toList + let rElt ← resolveExpr compCtx f elt + return .SetComp (f a) rElt ⟨f gens.ann, resolvedGens.toArray⟩ + | .DictComp a key val gens => + let (compCtx, resolvedGens) ← resolveComprehensions ctx f gens.val.toList + let rKey ← resolveExpr compCtx f key + let rVal ← resolveExpr compCtx f val + return .DictComp (f a) rKey rVal ⟨f gens.ann, resolvedGens.toArray⟩ + | .GeneratorExp a elt gens => + let (compCtx, resolvedGens) ← resolveComprehensions ctx f gens.val.toList + let rElt ← resolveExpr compCtx f elt + return .GeneratorExp (f a) rElt ⟨f gens.ann, resolvedGens.toArray⟩ + | .Await a inner => return .Await (f a) (← resolveExpr ctx f inner) + | .Yield a valOpt => + let rVal ← match valOpt.val with + | some v => pure (some (← resolveExpr ctx f v)) + | none => pure none + return .Yield (f a) ⟨f valOpt.ann, rVal⟩ + | .YieldFrom a inner => return .YieldFrom (f a) (← resolveExpr ctx f inner) + | .FormattedValue a value conv fmt => + let rValue ← resolveExpr ctx f value + let rFmt ← match fmt.val with + | some fmtExpr => pure (some (← resolveExpr ctx f fmtExpr)) + | none => pure none + return .FormattedValue (f a) rValue (resolveInt f conv) ⟨f fmt.ann, rFmt⟩ + | .JoinedStr a values => + let mut rValues : Array ResolvedPythonExpr := #[] + for v in values.val do + rValues := rValues.push (← resolveExpr ctx f v) + return .JoinedStr (f a) ⟨f values.ann, rValues⟩ + | .Subscript a obj slice ectx => + let rObj ← resolveExpr ctx f obj + let rSlice ← resolveExpr ctx f slice + return .Subscript (f a) rObj rSlice (resolveExprCtx f ectx) + | .Starred a inner ectx => + return .Starred (f a) (← resolveExpr ctx f inner) (resolveExprCtx f ectx) + | .Tuple a elts ectx => + let mut rElts : Array ResolvedPythonExpr := #[] + for elt in elts.val do + rElts := rElts.push (← resolveExpr ctx f elt) + return .Tuple (f a) ⟨f elts.ann, rElts⟩ (resolveExprCtx f ectx) + | .List a elts ectx => + let mut rElts : Array ResolvedPythonExpr := #[] + for elt in elts.val do + rElts := rElts.push (← resolveExpr ctx f elt) + return .List (f a) ⟨f elts.ann, rElts⟩ (resolveExprCtx f ectx) + | .NamedExpr a target value => + let rTarget ← resolveExpr ctx f target + let rValue ← resolveExpr ctx f value + return .NamedExpr (f a) rTarget rValue + | .Lambda a args body => do + let pl ← extractParamList ctx f args + let allParams := pl.required ++ pl.optional.map (fun (n, ty, _) => (n, ty)) ++ pl.kwonly.map (fun (n, ty, _) => (n, ty)) + let lambdaCtx := allParams.foldl (fun c (n, ty) => c.insert n (CtxEntry.variable ty)) ctx + let rBody ← resolveExpr lambdaCtx f body + let rArgs ← resolveArguments lambdaCtx f args + return .Lambda (f a) rArgs rBody + | .Slice a start stop step => + let rStart ← match start.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + let rStop ← match stop.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + let rStep ← match step.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + return .Slice (f a) ⟨f start.ann, rStart⟩ ⟨f stop.ann, rStop⟩ ⟨f step.ann, rStep⟩ + | .TemplateStr a parts => + let mut rParts : Array ResolvedPythonExpr := #[] + for p in parts.val do + rParts := rParts.push (← resolveExpr ctx f p) + return .TemplateStr (f a) ⟨f parts.ann, rParts⟩ + | .Interpolation a value conv fmtSpec fmt => do + let rValue ← resolveExpr ctx f value + let rFmt ← match fmt.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + return .Interpolation (f a) rValue (resolveConstant f conv) (resolveInt f fmtSpec) ⟨f fmt.ann, rFmt⟩ + +partial def resolveAlias (f : SourceRange → ResolvedAnn) : StrataPython.alias SourceRange → StrataPython.alias ResolvedAnn + | .mk_alias a name asname => .mk_alias (f a) (mapAnnVal f name) (mapAnnOpt f (mapAnnVal f) asname) + +/-- Resolves a `with` item: uses `typeOfExpr` on the context expression to find the class, + then looks up `__enter__` and `__exit__` in its method list. Annotates with `.withCtx` + carrying both sigs so Translation can emit `StaticCall enter [mgr]` / `StaticCall exit [mgr]`. -/ +partial def resolveWithitem (ctx : Ctx) (f : SourceRange → ResolvedAnn) : StrataPython.withitem SourceRange → ResolveM (StrataPython.withitem ResolvedAnn) + | .mk_withitem a ctxExpr optVars => do + let enterId := PythonIdentifier.builtin "__enter__" + let exitId := PythonIdentifier.builtin "__exit__" + let info ← match ← typeOfExpr ctx ctxExpr with + | some (.Name _ className _) => + let classId := PythonIdentifier.fromAst className + match ctx[classId]? with + | some (.class_ _ _ methods _) => + let enterSig := methods.find? (fun (mName, _) => mName == enterId) |>.map (·.2) + let exitSig := methods.find? (fun (mName, _) => mName == exitId) |>.map (·.2) + match enterSig, exitSig with + | some es, some xs => pure (NodeInfo.withCtx es xs) + | _, _ => pure NodeInfo.unresolved + | _ => pure NodeInfo.unresolved + | _ => pure NodeInfo.unresolved + let rCtxExpr ← resolveExpr ctx f ctxExpr + let rOptVars ← match optVars.val with + | some v => pure (some (← resolveExpr ctx f v)) + | none => pure none + return .mk_withitem { sr := a, info } rCtxExpr ⟨f optVars.ann, rOptVars⟩ + +partial def resolveExcepthandler (ctx : Ctx) (f : SourceRange → ResolvedAnn) : StrataPython.excepthandler SourceRange → ResolveM (StrataPython.excepthandler ResolvedAnn) + | .ExceptHandler a ty name body => do + let handlerCtx := match name.val with + | some n => ctx.insert (PythonIdentifier.fromAst n) (CtxEntry.variable (annotationToPythonType Option.none)) + | none => ctx + let resolvedBody ← resolveBlock handlerCtx f body.val + let rTy ← match ty.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + return .ExceptHandler (f a) ⟨f ty.ann, rTy⟩ (mapAnnOpt f (mapAnnVal f) name) ⟨f body.ann, resolvedBody⟩ + +partial def resolveMatchCase (ctx : Ctx) (f : SourceRange → ResolvedAnn) : StrataPython.match_case SourceRange → ResolveM (StrataPython.match_case ResolvedAnn) + | .mk_match_case a pat guard body => do + let resolvedBody ← resolveBlock ctx f body.val + let rGuard ← match guard.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + return .mk_match_case (f a) (sorry) ⟨f guard.ann, rGuard⟩ ⟨f body.ann, resolvedBody⟩ + +/-- Resolves an array of statements sequentially, threading the growing context. + Each statement may extend the context (e.g., assignments, imports, defs) which + subsequent statements in the same block can see. -/ +partial def resolveBlock (ctx : Ctx) (f : SourceRange → ResolvedAnn) (stmts : Array PythonStmt) : ResolveM (Array ResolvedPythonStmt) := do + let mut c := ctx + let mut resolved : Array ResolvedPythonStmt := #[] + for stmt in stmts do + let (c', r) ← resolveStmt c f stmt + c := c' + resolved := resolved.push r + return resolved + +/-- Resolves a function definition. Takes the pre-computed `FuncSig` (from the ClassDef handler + or freshly extracted), extends the context with the function name, builds the body context, + and resolves the body. Returns the updated ctx and all resolved sub-trees for the caller to + assemble into `FunctionDef` or `AsyncFunctionDef`. -/ +partial def resolveFuncDef (ctx : Ctx) (f : SourceRange → ResolvedAnn) + (sig : FuncSig) + (a : SourceRange) (name : Ann String SourceRange) (args : StrataPython.arguments SourceRange) + (body : Ann PythonProgram SourceRange) (decorators : Ann (Array PythonExpr) SourceRange) + (returns : Ann (Option PythonExpr) SourceRange) (tc : Ann (Option (Ann String SourceRange)) SourceRange) + (typeParams : Ann (Array (StrataPython.type_param SourceRange)) SourceRange) := do + let ctx' := ctx.insert (PythonIdentifier.fromAst name) (.function sig) + let bodyCtx ← resolveFunctionBody ctx' f args body.val + let ann : ResolvedAnn := { sr := a, info := .funcDecl sig } + let resolvedBody ← resolveBlock bodyCtx f body.val + let rBody : Ann (Array ResolvedPythonStmt) ResolvedAnn := ⟨f body.ann, resolvedBody⟩ + let rArgs ← resolveArguments bodyCtx f args + let mut rDecs : Array ResolvedPythonExpr := #[] + for d in decorators.val do rDecs := rDecs.push (← resolveExpr ctx' f d) + let rRets ← match returns.val with + | some e => pure (some (← resolveExpr ctx' f e)) + | none => pure none + let mut rTps : Array (StrataPython.type_param ResolvedAnn) := #[] + for tp in typeParams.val do rTps := rTps.push (← resolveTypeParam ctx' f tp) + let rDecsAnn : Ann (Array ResolvedPythonExpr) ResolvedAnn := ⟨f decorators.ann, rDecs⟩ + let rRetsAnn : Ann (Option ResolvedPythonExpr) ResolvedAnn := ⟨f returns.ann, rRets⟩ + let rTpsAnn : Ann (Array (StrataPython.type_param ResolvedAnn)) ResolvedAnn := ⟨f typeParams.ann, rTps⟩ + return (ctx', ann, mapAnnVal f name, rArgs, rBody, rDecsAnn, rRetsAnn, mapAnnOpt f (mapAnnVal f) tc, rTpsAnn) + +/-- Spine type resolution. Monadic: may trigger demand-driven module loads when + traversing qualified type annotations (e.g. `boto3.S3`) through module contexts. -/ +partial def typeOfExpr (ctx : Ctx) : PythonExpr → ResolveM (Option PythonType) + | .Name _ n _ => match ctx[PythonIdentifier.fromAst n]? with + | some (.variable ty) => pure (some ty) + | _ => pure none + | .Attribute _ obj fieldName _ => do + match ← typeOfExpr ctx obj with + | some (.Name _ className _) => + let classId := PythonIdentifier.fromAst className + match ctx[classId]? with + | some (.class_ _ fields _ _) => + pure (fields.find? (fun (fName, _) => fName == PythonIdentifier.fromAst fieldName) |>.map (·.2)) + | some (.module_ moduleRaw) => + let moduleCtx : Ctx := moduleRaw.fold (fun c k v => c.insert k v) {} + let fieldId := PythonIdentifier.fromAst fieldName + match moduleCtx[fieldId]? with + | some (.variable ty) => pure (some ty) + | some (.class_ _ fields _ _) => + pure (fields.find? (fun (fName, _) => fName == fieldId) |>.map (·.2)) + | none => + let baseDir ← read + let components := className.val.splitOn "." + let moduleDir := components.foldl (· / ·) baseDir + let f : SourceRange → ResolvedAnn := fun sr => { sr, info := .irrelevant } + let (subCtx, _) ← resolveModuleComponent fieldName.val moduleDir f + match subCtx[fieldId]? with + | some (.variable ty) => pure (some ty) + | _ => pure none + | _ => pure none + | _ => pure none + | _ => pure none + | _ => pure none + +/-- Resolve one imported class method from its raw AST on demand. Extracts the + FuncSig, resolves the method body, records the resolved FunctionDef into + `demandedMethods` and the owning class into `demandedClasses` for the + pipeline to translate. Memoized by qualified name. -/ +partial def resolveMethodAstSig (ctx : Ctx) (f : SourceRange → ResolvedAnn) + (classId : PythonIdentifier) (fields : List (PythonIdentifier × PythonType)) + (mAst : PythonStmt) : ResolveM FuncSig := do + match mAst with + | .FunctionDef a mName mArgs body mDecs mReturns mTc mTypeParams => + let mId := PythonIdentifier.fromAst mName + let qualName := s!"{classId.val}@{mName.val}" + let sig ← extractFuncSig ctx f mId (some classId) mArgs mDecs.val mReturns body.val + let st ← get + unless st.demandedMethods.contains qualName do + let (_, ann, rName, rArgs, rBody, rDecs, rRets, rTc, rTps) ← + resolveFuncDef ctx f sig a mName mArgs body mDecs mReturns mTc mTypeParams + let resolvedStmt : ResolvedPythonStmt := .FunctionDef ann rName rArgs rBody rDecs rRets rTc rTps + modify fun s => { s with + demandedMethods := s.demandedMethods.insert qualName resolvedStmt + demandedClasses := s.demandedClasses.insert classId.val (classId, fields) } + pure sig + | _ => + pure { name := PythonIdentifier.builtin "?", className := some classId, params := .static {required := [], optional := [], kwonly := []}, returnType := anyType, locals := [] } + +/-- Resolve one imported top-level function / overload from its raw AST on demand. + Records the resolved FunctionDef into `demandedFunctions` under its + disambiguated Laurel name. Memoized. -/ +partial def resolveFunctionAstSig (ctx : Ctx) (f : SourceRange → ResolvedAnn) + (sig : FuncSig) (fAst : PythonStmt) : ResolveM Unit := do + match fAst with + | .FunctionDef a fName fArgs body fDecs fReturns fTc fTypeParams => + let key := sig.laurelName.text + let st ← get + unless st.demandedFunctions.contains key do + let (_, ann, rName, rArgs, rBody, rDecs, rRets, rTc, rTps) ← + resolveFuncDef ctx f sig a fName fArgs body fDecs fReturns fTc fTypeParams + -- Re-annotate the FunctionDef with the disambiguated sig so Translation emits client$N + let ann' : ResolvedAnn := { ann with info := .funcDecl sig } + let resolvedStmt : ResolvedPythonStmt := .FunctionDef ann' rName rArgs rBody rDecs rRets rTc rTps + modify fun s => { s with demandedFunctions := s.demandedFunctions.insert key resolvedStmt } + | _ => pure () + +/-- Resolves `receiver.method(...)` calls. Monadic: uses `typeOfExpr` which may + trigger demand-driven module loads. -/ +partial def resolveMethodCall (ctx : Ctx) (receiver : PythonExpr) (methodName : Ann String SourceRange) (callArgs : Array PythonExpr := #[]) : ResolveM NodeInfo := do + let methId := PythonIdentifier.fromAst methodName + let f : SourceRange → ResolvedAnn := fun sr => { sr, info := .irrelevant } + match ← typeOfExpr ctx receiver with + | some (.Name _ className _) => + let classId := PythonIdentifier.fromAst className + match ctx[classId]? with + | some (.class_ classId fields methods methodAsts) => + match methods.find? (fun (mName, _) => mName == methId) with + | some (_, sig) => pure (.funcCall sig) + | none => match methodAsts.find? (fun (mName, _) => mName == methId) with + | some (_, mAst) => do + let sig ← resolveMethodAstSig ctx f classId fields mAst + pure (.funcCall sig) + | none => pure .unresolved + | _ => pure .unresolved + | some (.Attribute _ (.Name _ modName _) clsName _) => + -- Qualified class type (e.g. boto3.S3): chase module → class, resolve method on demand + let modId := PythonIdentifier.fromAst modName + let baseDir ← read + -- Load the submodule `mod/cls` (e.g. boto3/S3) to find the class. + let (subCtx, _) ← resolveModuleComponent clsName.val (baseDir / modName.val) f + match subCtx[PythonIdentifier.fromAst clsName]? with + | some (.class_ classId fields methods methodAsts) => + match methods.find? (fun (mName, _) => mName == methId) with + | some (_, sig) => pure (.funcCall sig) + | none => match methodAsts.find? (fun (mName, _) => mName == methId) with + | some (_, mAst) => do + let sig ← resolveMethodAstSig subCtx f classId fields mAst + pure (.funcCall sig) + | none => pure .unresolved + | _ => + -- Fall back: maybe the name is a class directly in the parent module's ctx + match ctx[modId]? with + | some (.module_ moduleRaw) => + let moduleCtx : Ctx := moduleRaw.fold (fun c k v => c.insert k v) {} + match moduleCtx[PythonIdentifier.fromAst clsName]? with + | some (.class_ classId fields methods methodAsts) => + match methods.find? (fun (mName, _) => mName == methId) with + | some (_, sig) => pure (.funcCall sig) + | none => match methodAsts.find? (fun (mName, _) => mName == methId) with + | some (_, mAst) => do + let sig ← resolveMethodAstSig moduleCtx f classId fields mAst + pure (.funcCall sig) + | none => pure .unresolved + | _ => pure .unresolved + | _ => pure .unresolved + | _ => match receiver with + | .Name _ rName _ => + let rId := PythonIdentifier.fromAst rName + match ctx[rId]? with + | some (.module_ moduleRaw) => + let moduleCtx : Ctx := moduleRaw.fold (fun c k v => c.insert k v) {} + match moduleCtx[methId]? with + | some (.function sig) => pure (.funcCall sig) + | some (.overloadedFunction overloads) => + let matched := overloads.find? fun (_, olSig, _) => + matchOverload olSig callArgs + match matched with + | some (idx, sig, astOpt) => do + let sig' := { sig with overloadIndex := some idx } + match astOpt with + | some fAst => resolveFunctionAstSig moduleCtx f sig' fAst + | none => pure () + pure (.funcCall sig') + | none => pure .unresolved + | some (.class_ cId fields methods methodAsts) => + let initId := PythonIdentifier.builtin "__init__" + match methods.find? (fun (mName, _) => mName == initId) with + | some (_, sig) => pure (.classNew cId sig) + | none => match methodAsts.find? (fun (mName, _) => mName == initId) with + | some (_, mAst) => do + let sig ← resolveMethodAstSig moduleCtx f cId fields mAst + pure (.classNew cId sig) + | none => + let emptySig : FuncSig := { name := initId, className := some cId, params := .static {required := [], optional := [], kwonly := []}, returnType := anyType, locals := [] } + pure (.classNew cId emptySig) + | _ => pure .unresolved + | _ => pure .unresolved + | _ => pure .unresolved + +/-- Load a module component from disk and resolve it. Tries `dir/name.python.st.ion` + then `dir/name/__init__.python.st.ion`. Returns the module's resolved program and Ctx. -/ +partial def resolveModuleComponent (name : String) (dir : System.FilePath) (f : SourceRange → ResolvedAnn) : ResolveM (Ctx × ResolvedPythonProgram) := do + let ionPath := dir / (name ++ ".python.st.ion") + let initPath := dir / name / "__init__.python.st.ion" + let key := ionPath.toString + let state ← get + if let some cachedCtx := state.resolvedPaths[key]? then + return (cachedCtx, { stmts := #[], moduleLocals := [] }) + let loadResult ← do + match ← (StrataPython.readPythonStrata ionPath.toString).toBaseIO with + | .ok stmts => pure (some (ionPath, stmts)) + | .error _ => + match ← (StrataPython.readPythonStrata initPath.toString).toBaseIO with + | .ok stmts => pure (some (initPath, stmts)) + | .error _ => pure none + match loadResult with + | some (_, stmts) => + -- Index-only scan: top-level functions resolved eagerly (few, needed for overload + -- matching); class methods stored as raw ASTs for on-demand resolution; TypedDicts + -- and other assignments skipped. Avoids folding over thousands of irrelevant stmts. + let mut ctx : Ctx := builtinContext + for stmt in stmts do + match stmt with + | .FunctionDef _ fname fargs fbody fdecs freturns _ _ => + let nameId := PythonIdentifier.fromAst fname + if hasOverloadDecorator fdecs.val then + let overloads := match ctx[nameId]? with + | some (.overloadedFunction existing) => existing + | _ => [] + let idx := overloads.length + let sig ← extractFuncSig ctx f nameId none fargs fdecs.val freturns fbody.val + ctx := ctx.insert nameId (.overloadedFunction (overloads ++ [(idx, sig, some stmt)])) + else + match ctx[nameId]? with + | some (.overloadedFunction _) => pure () -- impl stub after overloads, keep overloads + | _ => + let sig ← extractFuncSig ctx f nameId none fargs fdecs.val freturns fbody.val + ctx := ctx.insert nameId (.function sig) + | .ClassDef _ cname _ _ cbody _ _ => + let classId := PythonIdentifier.fromAst cname + let fields := cbody.val.toList.filterMap fun s => match s with + | .AnnAssign _ (.Name _ n _) annotation _ _ => some (PythonIdentifier.fromAst n, annotation) + | _ => none + let methodAsts := cbody.val.toList.filterMap fun s => match s with + | .FunctionDef _ mName _ _ _ _ _ _ => some (PythonIdentifier.fromAst mName, s) + | .AsyncFunctionDef _ mName _ _ _ _ _ _ => some (PythonIdentifier.fromAst mName, s) + | _ => none + ctx := ctx.insert classId (.class_ classId fields [] methodAsts) + | _ => pure () -- TypedDicts, assignments, imports — not needed by callers + modify fun s => { s with resolvedPaths := s.resolvedPaths.insert key ctx } + pure (ctx, { stmts := #[], moduleLocals := [] }) + | none => pure ({}, { stmts := #[], moduleLocals := [] }) + +/-- Resolve a dotted module name (e.g. "boto3.AccessAnalyzer") by converting dots to path + separators and loading the final component. -/ +partial def resolveModule (dottedName : String) (dir : System.FilePath) (f : SourceRange → ResolvedAnn) : ResolveM (Ctx × ResolvedPythonProgram) := do + let components := dottedName.splitOn "." + let moduleDir := components.dropLast.foldl (· / ·) dir + match components.getLast? with + | some name => resolveModuleComponent name moduleDir f + | none => pure ({}, { stmts := #[], moduleLocals := [] }) + +/-- The core statement resolver. Threads the context as accumulator: + - `FunctionDef`/`AsyncFunctionDef` → reuses existing sig from ctx if already registered + (e.g., by ClassDef's pre-scan), otherwise extracts fresh. Annotates with `.funcDecl`. + - `ClassDef` → pre-scans body for fields and methods, registers class in ctx with full + method list, resolves body in classCtx (self typed as class, methods visible). + - `Import`/`ImportFrom` → extends ctx with module or imported names. + - `Assign`/`AnnAssign` → extends ctx with assigned names. + - `AugAssign` → annotates with operator sig (`.funcCall`) for Translation. + - Control flow → resolves sub-blocks in current ctx (no ctx extension from if/for/while). -/ +partial def resolveStmt (ctx : Ctx) (f : SourceRange → ResolvedAnn) (s : PythonStmt) : ResolveM (Ctx × ResolvedPythonStmt) := do + match s with + | .FunctionDef a name args body decorators returns tc typeParams => + let nameId := PythonIdentifier.fromAst name + if hasOverloadDecorator decorators.val then + let sig ← extractFuncSig ctx f nameId none args decorators.val returns body.val + let overloads := match ctx[nameId]? with + | some (.overloadedFunction existing) => existing + | _ => [] + let idx := overloads.length + let ctx' := ctx.insert nameId (.overloadedFunction (overloads ++ [(idx, sig, none)])) + let (_, ann, rName, rArgs, rBody, rDecs, rRets, rTc, rTps) ← + resolveFuncDef ctx f sig a name args body decorators returns tc typeParams + return (ctx', .FunctionDef ann rName rArgs rBody rDecs rRets rTc rTps) + else + match ctx[nameId]? with + | some (.overloadedFunction _) => + -- Non-@overload def after overloads = implementation stub. Keep the overload list. + let sig ← extractFuncSig ctx f nameId none args decorators.val returns body.val + let (_, ann, rName, rArgs, rBody, rDecs, rRets, rTc, rTps) ← + resolveFuncDef ctx f sig a name args body decorators returns tc typeParams + return (ctx, .FunctionDef ann rName rArgs rBody rDecs rRets rTc rTps) + | _ => + let sig ← match ctx[nameId]? with + | some (.function existingSig) => pure existingSig + | _ => extractFuncSig ctx f nameId none args decorators.val returns body.val + let (ctx', ann, rName, rArgs, rBody, rDecs, rRets, rTc, rTps) ← + resolveFuncDef ctx f sig a name args body decorators returns tc typeParams + return (ctx', .FunctionDef ann rName rArgs rBody rDecs rRets rTc rTps) + | .AsyncFunctionDef a name args body decorators returns tc typeParams => + let nameId := PythonIdentifier.fromAst name + let sig ← match ctx[nameId]? with + | some (.function existingSig) => pure existingSig + | _ => extractFuncSig ctx f nameId none args decorators.val returns body.val + let (ctx', ann, rName, rArgs, rBody, rDecs, rRets, rTc, rTps) ← + resolveFuncDef ctx f sig a name args body decorators returns tc typeParams + return (ctx', .AsyncFunctionDef ann rName rArgs rBody rDecs rRets rTc rTps) + | .ClassDef a name bases keywords body decorators typeParams => + let classId := PythonIdentifier.fromAst name + let classType : PythonType := .Name SourceRange.none ⟨SourceRange.none, name.val⟩ (.Load SourceRange.none) + let classLevelFields := body.val.toList.filterMap fun s => match s with + | .AnnAssign _ (.Name _ n _) annotation _ _ => some (PythonIdentifier.fromAst n, annotation) + | _ => Option.none + -- Also collect fields assigned in `__init__` as `self.: T = ...` or + -- `self. = ...`. Many classes declare fields ONLY in __init__ (no + -- class-level annotation); without this their composite has no fields, so + -- `self.field` / `obj.field` references fail to resolve ("'field' is not defined"). + let anyTy : PythonType := .Name SourceRange.none ⟨SourceRange.none, "Any"⟩ (.Load SourceRange.none) + let initFields : List (PythonIdentifier × PythonType) := + body.val.toList.flatMap fun s => match s with + | .FunctionDef _ mName _ ⟨_, mBody⟩ _ _ _ _ => + if mName.val == "__init__" then + mBody.toList.filterMap fun st => match st with + | .AnnAssign _ (.Attribute _ (.Name _ slf _) attr _) annotation _ _ => + if slf.val == "self" then some (PythonIdentifier.fromAst attr, annotation) else none + | .Assign _ targets _ _ => + match targets.val.toList with + | [.Attribute _ (.Name _ slf _) attr _] => + if slf.val == "self" then some (PythonIdentifier.fromAst attr, anyTy) else none + | _ => none + | _ => none + else [] + | _ => [] + -- Merge: class-level first, then __init__ fields not already declared. + let fields := classLevelFields ++ initFields.filter fun (fid, _) => + !classLevelFields.any fun (cid, _) => cid.val == fid.val + let mut methods : List (PythonIdentifier × FuncSig) := [] + for s in body.val.toList do + match s with + | .FunctionDef _ mName mArgs ⟨_, mBody⟩ mDecs mReturns _ _ => + let mId := PythonIdentifier.fromAst mName + let sig ← extractFuncSig ctx f mId (some classId) mArgs mDecs.val mReturns mBody + methods := methods ++ [(mId, sig)] + | .AsyncFunctionDef _ mName mArgs ⟨_, mBody⟩ mDecs mReturns _ _ => + let mId := PythonIdentifier.fromAst mName + let sig ← extractFuncSig ctx f mId (some classId) mArgs mDecs.val mReturns mBody + methods := methods ++ [(mId, sig)] + | _ => pure () + let ctx' := ctx.insert classId (CtxEntry.class_ classId fields methods) + let classCtx := ctx'.insert (PythonIdentifier.fromAst ⟨SourceRange.none, "self"⟩) (CtxEntry.variable classType) + let classCtx := methods.foldl (fun c (mId, mSig) => c.insert mId (CtxEntry.function mSig)) classCtx + let methodSigs := methods.map (·.2) + let resolvedBody ← resolveBlock classCtx f body.val + let mut rBases : Array ResolvedPythonExpr := #[] + for b in bases.val do rBases := rBases.push (← resolveExpr ctx' f b) + let mut rKeywords : Array (StrataPython.keyword ResolvedAnn) := #[] + for kw in keywords.val do rKeywords := rKeywords.push (← resolveKeyword ctx' f kw) + let mut rDecorators : Array ResolvedPythonExpr := #[] + for d in decorators.val do rDecorators := rDecorators.push (← resolveExpr ctx' f d) + let mut rTypeParams : Array (StrataPython.type_param ResolvedAnn) := #[] + for tp in typeParams.val do rTypeParams := rTypeParams.push (← resolveTypeParam ctx' f tp) + return (ctx', .ClassDef { sr := a, info := .classDecl classId fields methodSigs } (mapAnnVal f name) + ⟨f bases.ann, rBases⟩ + ⟨f keywords.ann, rKeywords⟩ + ⟨f body.ann, resolvedBody⟩ + ⟨f decorators.ann, rDecorators⟩ + ⟨f typeParams.ann, rTypeParams⟩) + | .Import a aliases => do + let baseDir ← read + let mut ctx' := ctx + for alias in aliases.val do + match alias with + | .mk_alias _ modName asName => + let registeredId := match asName.val with + | some aliasName => PythonIdentifier.fromAst aliasName + | none => PythonIdentifier.fromImport modName + let (moduleCtx, _) ← resolveModule modName.val baseDir f + ctx' := ctx'.insert registeredId (CtxEntry.module_ moduleCtx.inner.inner) + return (ctx', .Import (f a) (mapAnnArr f (resolveAlias f) aliases)) + | .ImportFrom a modName imports level => do + let baseDir ← read + let mut ctx' := ctx + match modName.val with + | some modAnn => + let (moduleCtx, _) ← resolveModule modAnn.val baseDir f + for imp in imports.val do + match imp with + | .mk_alias _ impName asName => + let registeredId := match asName.val with + | some aliasName => PythonIdentifier.fromAst aliasName + | none => PythonIdentifier.fromAst impName + match ctx'[registeredId]? with + | some _ => pure () + | none => + let impId := PythonIdentifier.fromAst impName + match moduleCtx[impId]? with + | some entry => ctx' := ctx'.insert registeredId entry + | none => ctx' := ctx'.insert registeredId CtxEntry.unresolved + | none => + for imp in imports.val do + match imp with + | .mk_alias _ impName asName => + let registeredId := match asName.val with + | some aliasName => PythonIdentifier.fromAst aliasName + | none => PythonIdentifier.fromAst impName + match ctx'[registeredId]? with + | some _ => pure () + | none => ctx' := ctx'.insert registeredId CtxEntry.unresolved + return (ctx', .ImportFrom (f a) (mapAnnOpt f (mapAnnVal f) modName) (mapAnnArr f (resolveAlias f) imports) (mapAnnOpt f (resolveInt f) level)) + | .Assign a targets value tc => do + let newNames := targets.val.toList.flatMap collectNamesFromTarget + let ctx' := newNames.foldl (fun c n => c.insert n (CtxEntry.variable (annotationToPythonType Option.none))) ctx + let mut rTargets : Array ResolvedPythonExpr := #[] + for t in targets.val do rTargets := rTargets.push (← resolveExpr ctx f t) + let rValue ← resolveExpr ctx f value + return (ctx', .Assign (f a) ⟨f targets.ann, rTargets⟩ rValue (mapAnnOpt f (mapAnnVal f) tc)) + | .AnnAssign a target ann value simple => do + let newNames := collectNamesFromTarget target + let rTarget ← resolveExpr ctx f target + let rAnn ← resolveExpr ctx f ann + let rValue ← match value.val with + | some v => pure (some (← resolveExpr ctx f v)) + | none => pure none + -- Prefer the RHS call's resolved return type (e.g. boto3.S3) over the bare + -- written annotation (e.g. S3), so method calls on the variable resolve + -- through the module and demand the class. + let varTy : PythonType := match rValue with + | some (.Call { info := .funcCall sig, .. } ..) => sig.returnType + | _ => ann + let ctx' := newNames.foldl (fun c n => c.insert n (CtxEntry.variable varTy)) ctx + return (ctx', .AnnAssign (f a) rTarget rAnn ⟨f value.ann, rValue⟩ (resolveInt f simple)) + | .AugAssign a target op value => do + let opSig : FuncSig := { name := .builtin (operatorToLaurel op), className := none, params := .static {required := [(.builtin "left", anyType), (.builtin "right", anyType)], optional := [], kwonly := []}, returnType := anyType, locals := [] } + let rTarget ← resolveExpr ctx f target + let rValue ← resolveExpr ctx f value + return (ctx, .AugAssign { sr := a, info := .funcCall opSig } rTarget (resolveOperator f op) rValue) + | .If a test body orelse => do + let rTest ← resolveExpr ctx f test + let rBody ← resolveBlock ctx f body.val + let rElse ← resolveBlock ctx f orelse.val + return (ctx, .If (f a) rTest ⟨f body.ann, rBody⟩ ⟨f orelse.ann, rElse⟩) + | .For a target iter body orelse tc => do + let rTarget ← resolveExpr ctx f target + let rIter ← resolveExpr ctx f iter + let rBody ← resolveBlock ctx f body.val + let rElse ← resolveBlock ctx f orelse.val + return (ctx, .For (f a) rTarget rIter ⟨f body.ann, rBody⟩ ⟨f orelse.ann, rElse⟩ (mapAnnOpt f (mapAnnVal f) tc)) + | .AsyncFor a target iter body orelse tc => do + let rTarget ← resolveExpr ctx f target + let rIter ← resolveExpr ctx f iter + let rBody ← resolveBlock ctx f body.val + let rElse ← resolveBlock ctx f orelse.val + return (ctx, .AsyncFor (f a) rTarget rIter ⟨f body.ann, rBody⟩ ⟨f orelse.ann, rElse⟩ (mapAnnOpt f (mapAnnVal f) tc)) + | .While a test body orelse => do + let rTest ← resolveExpr ctx f test + let rBody ← resolveBlock ctx f body.val + let rElse ← resolveBlock ctx f orelse.val + return (ctx, .While (f a) rTest ⟨f body.ann, rBody⟩ ⟨f orelse.ann, rElse⟩) + | .Try a body handlers orelse finalbody => do + let rBody ← resolveBlock ctx f body.val + let mut rHandlers : Array (StrataPython.excepthandler ResolvedAnn) := #[] + for h in handlers.val do + rHandlers := rHandlers.push (← resolveExcepthandler ctx f h) + let rElse ← resolveBlock ctx f orelse.val + let rFinally ← resolveBlock ctx f finalbody.val + return (ctx, .Try (f a) ⟨f body.ann, rBody⟩ ⟨f handlers.ann, rHandlers⟩ ⟨f orelse.ann, rElse⟩ ⟨f finalbody.ann, rFinally⟩) + | .TryStar a body handlers orelse finalbody => do + let rBody ← resolveBlock ctx f body.val + let mut rHandlers : Array (StrataPython.excepthandler ResolvedAnn) := #[] + for h in handlers.val do + rHandlers := rHandlers.push (← resolveExcepthandler ctx f h) + let rElse ← resolveBlock ctx f orelse.val + let rFinally ← resolveBlock ctx f finalbody.val + return (ctx, .TryStar (f a) ⟨f body.ann, rBody⟩ ⟨f handlers.ann, rHandlers⟩ ⟨f orelse.ann, rElse⟩ ⟨f finalbody.ann, rFinally⟩) + | .With a items body tc => do + let mut rItems : Array (StrataPython.withitem ResolvedAnn) := #[] + for item in items.val do rItems := rItems.push (← resolveWithitem ctx f item) + let rBody ← resolveBlock ctx f body.val + return (ctx, .With (f a) ⟨f items.ann, rItems⟩ ⟨f body.ann, rBody⟩ (mapAnnOpt f (mapAnnVal f) tc)) + | .AsyncWith a items body tc => do + let mut rItems : Array (StrataPython.withitem ResolvedAnn) := #[] + for item in items.val do rItems := rItems.push (← resolveWithitem ctx f item) + let rBody ← resolveBlock ctx f body.val + return (ctx, .AsyncWith (f a) ⟨f items.ann, rItems⟩ ⟨f body.ann, rBody⟩ (mapAnnOpt f (mapAnnVal f) tc)) + | .Return a value => do + let rValue ← match value.val with + | some v => pure (some (← resolveExpr ctx f v)) + | none => pure none + return (ctx, .Return (f a) ⟨f value.ann, rValue⟩) + | .Delete a targets => do + let mut rTargets : Array ResolvedPythonExpr := #[] + for t in targets.val do rTargets := rTargets.push (← resolveExpr ctx f t) + return (ctx, .Delete (f a) ⟨f targets.ann, rTargets⟩) + | .Raise a exc cause => do + let rExc ← match exc.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + let rCause ← match cause.val with + | some e => pure (some (← resolveExpr ctx f e)) + | none => pure none + return (ctx, .Raise (f a) ⟨f exc.ann, rExc⟩ ⟨f cause.ann, rCause⟩) + | .Assert a test msg => do + let rTest ← resolveExpr ctx f test + let rMsg ← match msg.val with + | some m => pure (some (← resolveExpr ctx f m)) + | none => pure none + return (ctx, .Assert (f a) rTest ⟨f msg.ann, rMsg⟩) + | .Expr a value => do + let rValue ← resolveExpr ctx f value + return (ctx, .Expr (f a) rValue) + | .Pass a => return (ctx, .Pass (f a)) + | .Break a => return (ctx, .Break (f a)) + | .Continue a => return (ctx, .Continue (f a)) + | .Global a names => return (ctx, .Global (f a) (mapAnnArr f (mapAnnVal f) names)) + | .Nonlocal a names => return (ctx, .Nonlocal (f a) (mapAnnArr f (mapAnnVal f) names)) + | .Match a subject cases => do + let rSubject ← resolveExpr ctx f subject + let mut resolvedCases : Array (StrataPython.match_case ResolvedAnn) := #[] + for c in cases.val do + resolvedCases := resolvedCases.push (← resolveMatchCase ctx f c) + return (ctx, .Match (f a) rSubject ⟨f cases.ann, resolvedCases⟩) + | .TypeAlias a name typeParams value => do + let rName ← resolveExpr ctx f name + let mut rTypeParams : Array (StrataPython.type_param ResolvedAnn) := #[] + for tp in typeParams.val do rTypeParams := rTypeParams.push (← resolveTypeParam ctx f tp) + let rValue ← resolveExpr ctx f value + return (ctx, .TypeAlias (f a) rName ⟨f typeParams.ann, rTypeParams⟩ rValue) +end + +/-- Result of resolving a program: the resolved AST plus the imported + declarations the program demanded (methods, functions, classes). -/ +structure ResolveResult where + program : ResolvedPythonProgram + /-- Resolved FunctionDef stmts for demanded imported methods + top-level functions. -/ + demandedStmts : Array ResolvedPythonStmt + /-- Demanded imported classes (id × fields) for Composite type emission. -/ + demandedClasses : List (PythonIdentifier × List (PythonIdentifier × PythonType)) + +/-- Entry point: resolves a full Python module. Folds `resolveStmt` over top-level + statements, threading the context. Imports are loaded on demand. -/ +def resolve (stmts : PythonProgram) (baseDir : System.FilePath := ".") : EIO String ResolveResult := do + let f : SourceRange → ResolvedAnn := fun sr => { sr, info := .irrelevant } + let moduleLocals := computeLocals stmts [] + let initCtx := moduleLocals.foldl (fun c (n, ty) => c.insert n (CtxEntry.variable ty)) builtinContext + let action : ResolveM ResolvedPythonProgram := do + let mut ctx := initCtx + let mut resolved : Array ResolvedPythonStmt := #[] + for stmt in stmts do + let (ctx', r) ← resolveStmt ctx f stmt + ctx := ctx' + resolved := resolved.push r + return { stmts := resolved, moduleLocals := moduleLocals } + let (prog, state) ← action.run baseDir |>.run {} + let demandedStmts := (state.demandedMethods.toList.map (·.2) ++ state.demandedFunctions.toList.map (·.2)).toArray + let demandedClasses := state.demandedClasses.toList.map (·.2) + return { program := prog, demandedStmts, demandedClasses } + +end -- public section +end StrataPython.Resolution diff --git a/StrataPython/StrataPython/Specs/Error.lean b/StrataPython/StrataPython/Specs/Error.lean new file mode 100644 index 0000000000..fcb2bf7c4d --- /dev/null +++ b/StrataPython/StrataPython/Specs/Error.lean @@ -0,0 +1,20 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module + +public import StrataDDM.Util.SourceRange + +public section +namespace StrataPython.Specs + +/-- An error encountered while processing a PySpec file. -/ +structure SpecError where + file : System.FilePath + loc : StrataDDM.SourceRange + message : String + +end StrataPython.Specs +end diff --git a/StrataPython/StrataPython/Translation.lean b/StrataPython/StrataPython/Translation.lean new file mode 100644 index 0000000000..353d769936 --- /dev/null +++ b/StrataPython/StrataPython/Translation.lean @@ -0,0 +1,903 @@ +/- + Copyright Strata Contributors + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module + +public import Strata.Languages.Laurel.LaurelAST +public import StrataPython.PythonDialect +public import StrataPython.Resolution +import StrataDDM.Util.SourceRange + +/-! +# Pass 2: Translation + +Structural recursion over the resolved Python AST. Pattern matches on +NodeInfo and emits Laurel constructs. Never constructs Laurel.Identifier +from strings — only forwards what Resolution provided. + +Input: ResolvedPythonProgram +Output: Laurel.Program +-/ + +namespace StrataPython.Translation + +open Strata (Uri FileRange) +open Strata.Laurel +open StrataDDM +open StrataPython +open StrataPython.Resolution + +public section + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Error +-- ═══════════════════════════════════════════════════════════════════════════════ + +/-- Errors that can occur during translation. -/ +inductive TransError where + /-- A Python construct with no Laurel equivalent. -/ + | unsupportedConstruct (msg : String) + /-- A bug in the translator (should never occur on well-resolved input). -/ + | internalError (msg : String) + /-- An error in the user's Python code detected during translation. -/ + | userError (range : SourceRange) (msg : String) + deriving Repr + +instance : ToString TransError where + toString + | .unsupportedConstruct msg => s!"Translation: unsupported construct: {msg}" + | .internalError msg => s!"Translation: internal error: {msg}" + | .userError _range msg => s!"User code error: {msg}" + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Monad (State for fresh counter + loop labels) +-- ═══════════════════════════════════════════════════════════════════════════════ + +/-- Mutable state threaded through translation: fresh name counter, source file path, + and a stack of loop break/continue labels for translating `break`/`continue`. -/ +structure TransState where + /-- Counter for generating unique temporary names. -/ + freshCounter : Nat := 0 + /-- Path of the source file being translated (used for metadata). -/ + filePath : System.FilePath := "" + /-- Stack of (break_label, continue_label) pairs for enclosing loops. -/ + loopLabels : List (Identifier × Identifier) := [] + /-- Module-level assignment type-aliases (`MyInt = int`), consulted by + `pythonTypeToHighType` so annotations referring to an alias resolve to the + aliased type rather than a phantom composite. Set once in `translateModule`. -/ + typeAliases : Std.HashMap String HighType := {} + deriving Inhabited + +abbrev BaseM := StateT TransState (Except TransError) + +/-- Writer monad for translation. Produces a value plus a list of emitted Laurel statements. + Allows expressions that need prefix statements (e.g., `classNew` emits `New` + `__init__`) + to `tell` those statements and return just the expression value. -/ +structure TransM (α : Type) where + /-- Run the writer, producing the value and accumulated statement list. -/ + run : BaseM (α × List StmtExprMd) + +instance : Monad TransM where + pure a := ⟨pure (a, [])⟩ + bind ma f := ⟨do + let (a, w1) ← ma.run + let (b, w2) ← (f a).run + pure (b, w1 ++ w2)⟩ + +instance : MonadLift BaseM TransM where + monadLift ma := ⟨do let a ← ma; pure (a, [])⟩ + +instance : MonadExceptOf TransError TransM where + throw e := ⟨throw e⟩ + tryCatch ma f := ⟨tryCatch ma.run (fun e => (f e).run)⟩ + +def tell (stmts : List StmtExprMd) : TransM Unit := ⟨pure ((), stmts)⟩ + +def listen (ma : TransM α) : TransM (α × List StmtExprMd) := ⟨do + let (a, stmts) ← ma.run + pure ((a, stmts), stmts)⟩ + +def pass (ma : TransM (α × (List StmtExprMd → List StmtExprMd))) : TransM α := ⟨do + let ((a, f), stmts) ← ma.run + pure (a, f stmts)⟩ + +def collect (ma : TransM α) : TransM (α × List StmtExprMd) := + liftM (α := α × List StmtExprMd) ma.run + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Smart Constructors +-- ═══════════════════════════════════════════════════════════════════════════════ + +private def sourceRangeToMd (filePath : System.FilePath) (sr : SourceRange) : Option FileRange := + some { file := .file filePath.toString, range := sr } + +def mkExpr (sr : SourceRange) (expr : StmtExpr) : TransM StmtExprMd := do + pure { val := expr, source := sourceRangeToMd (← get).filePath sr } + +private def defaultMd : Option FileRange := none +def mkExprDefault (expr : StmtExpr) : StmtExprMd := { val := expr, source := defaultMd } +def mkTypeDefault (ty : HighType) : HighTypeMd := { val := ty, source := defaultMd } + +/-- Default-source local-variable declaration (see `mkLocalDecl`). -/ +def mkLocalDeclDefault (id : Identifier) (ty : HighTypeMd) (init : Option StmtExprMd) : StmtExprMd := + match init with + | some v => mkExprDefault (.Assign [{ val := .Declare { name := id, type := ty }, source := defaultMd }] v) + | none => mkExprDefault (.Var (.Declare { name := id, type := ty })) + +/-- Wrap a `Variable` as a `.Var` statement-expression with source location. -/ +def mkVar (sr : SourceRange) (v : Variable) : TransM StmtExprMd := mkExpr sr (.Var v) + +/-- Extract the `Variable` from a `.Var` statement-expression (assignment targets are + always translated to `.Var` nodes). Preserves the source location. -/ +def toVarTarget (e : StmtExprMd) : VariableMd := + match e.val with + | .Var v => { val := v, source := e.source } + | _ => { val := .Local default, source := e.source } + +/-- Build a local-variable declaration. With an initializer this lowers to an + `Assign` to a `Declare` target (the canonical "declare + initialize" form + consumed by `LaurelToCoreSchemaPass`); without one it is a bare `Var (Declare …)`. -/ +def mkLocalDecl (sr : SourceRange) (id : Identifier) (ty : HighTypeMd) + (init : Option StmtExprMd) : TransM StmtExprMd := do + match init with + | some v => do + let target : VariableMd := { val := .Declare { name := id, type := ty }, source := sourceRangeToMd (← get).filePath sr } + mkExpr sr (.Assign [target] v) + | none => mkExpr sr (.Var (.Declare { name := id, type := ty })) + +def freshId (pfx : String) : TransM Identifier := do + let s ← get; set { s with freshCounter := s.freshCounter + 1 } + pure { text := s!"{pfx}_{s.freshCounter}", uniqueId := none } + +def pushLoopLabel (pfx : String) : TransM (Identifier × Identifier) := do + let s ← get + let bk : Identifier := { text := s!"{pfx}_break_{s.freshCounter}", uniqueId := none } + let ct : Identifier := { text := s!"{pfx}_continue_{s.freshCounter}", uniqueId := none } + set { s with freshCounter := s.freshCounter + 1, loopLabels := (bk, ct) :: s.loopLabels } + pure (bk, ct) + +def popLoopLabel : TransM Unit := modify fun s => { s with loopLabels := s.loopLabels.tail! } +def currentBreakLabel : TransM (Option Identifier) := do return (← get).loopLabels.head?.map (·.1) +def currentContinueLabel : TransM (Option Identifier) := do return (← get).loopLabels.head?.map (·.2) + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- PythonType → HighType +-- ═══════════════════════════════════════════════════════════════════════════════ + +/-- Map a resolved Python type annotation to a Laurel `HighType`. + +Base names map to Core types: `int`/`bool`/`str`/`float`/`None` to their +scalars, `Any`/`object` to `Any`, and the container names `dict`/`list` to the +homogeneous Core encodings `DictStrAny`/`ListAny`. A bare name that matches none +of these is a user-defined class (`.UserDefined`), which Translation emits as a +`Composite`. + +Subscripted generics carry the same meaning as their base: the parameterized +containers (`dict[...]`, `list[...]`, and the `typing` aliases `Dict`/`List`/ +`Tuple`/`Set`) map to the container encodings, and the type-level operators +(`Optional`/`Union`/`Literal`/`Unpack`/`NotRequired`/`Required`/`Type`) erase to +`Any`. A subscripted name with no concrete encoding is a user-defined generic +class (`.UserDefined`). The lowercase `dict`/`list` subscript cases must agree +with the bare-name cases — otherwise `body: dict[str, Any]` is typed `Composite` +while its dict-literal value is `DictStrAny`, and Core fails to unify the two. -/ +def pythonTypeToHighType (aliases : Std.HashMap String HighType := {}) : PythonType → HighType + | .Name _ n _ => match n.val with + | "int" => .TInt + | "bool" => .TBool + | "str" => .TString + -- Python `float` is a real: a float literal lowers to `LiteralDecimal : TReal` + -- and the prelude boxes via `from_float : real`. Annotating `float` as `TReal` + -- keeps annotation and literal in one domain (no `real`↔`float64` reconciliation). + | "float" => .TReal + | "None" => .TVoid + | "Any" | "object" => .TCore "Any" + | "dict" => .TCore "DictStrAny" + | "list" => .TCore "ListAny" + -- Module-level assignment type-alias (`MyInt = int`): resolve the name to the + -- aliased type instead of a phantom `UserDefined` composite. (mypy-valid; v2/kbd + -- only handle the 3.12 `.TypeAlias` node, not assignment-form aliases.) + | name => match aliases[name]? with + | some ty => ty + | none => .UserDefined { text := name, uniqueId := none } + | .Constant _ (.ConNone _) _ => .TVoid + | .BinOp _ _ (.BitOr _) _ => .TCore "Any" + | .Subscript _ (.Name _ n _) _ _ => match n.val with + | "dict" | "Dict" => .TCore "DictStrAny" + | "list" | "List" | "tuple" | "Tuple" | "set" | "Set" | "frozenset" => .TCore "ListAny" + | "Optional" | "Union" | "Type" + | "Literal" | "Unpack" | "NotRequired" | "Required" => .TCore "Any" + | other => .UserDefined { text := other, uniqueId := none } + | _ => .TCore "Any" + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Runtime Constants (extracted from runtime program interface) +-- ═══════════════════════════════════════════════════════════════════════════════ + +private def rt (name : String) : Identifier := { text := name, uniqueId := none } + +private def rtListAnyCons := rt "ListAny_cons" +private def rtListAnyNil := rt "ListAny_nil" +private def rtFromListAny := rt "from_ListAny" +private def rtDictStrAnyCons := rt "DictStrAny_cons" +private def rtDictStrAnyEmpty := rt "DictStrAny_empty" +private def rtFromDictStrAny := rt "from_DictStrAny" +private def rtFromNone := rt "from_None" +private def rtAnyGet := rt "Any_get" +private def rtAnySets := rt "Any_sets" +private def rtFromSlice := rt "from_Slice" +private def rtAnyAsInt := rt "Any..as_int!" +private def rtOptSome := rt "OptSome" +private def rtOptNone := rt "OptNone" +private def rtPAdd := rt "PAdd" +private def rtPIn := rt "PIn" +private def rtIsError := rt "isError" +private def rtToStringAny := rt "to_string_any" +private def rtLaurelResult := rt "LaurelResult" +private def rtMaybeExcept := rt "maybe_except" + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- ═══════════════════════════════════════════════════════════════════════════════ +-- The Structural Recursion +-- ═══════════════════════════════════════════════════════════════════════════════ + +/-- Runtime comparison-operator function name for a resolved cmpop, given its RHS operand. + `is`/`is not` are UNINTERPRETED (`PIs`/`PIsNot` have no model) EXCEPT against `None`: + Python's `is None`/`is not None` are identity checks, but in the `Any` value model None + is a singleton so identity coincides with equality — so we lower them to the MODELED + `PEq`/`PNEq` (which handle None via `isfrom_None`). Without this, `if x is None` is + inconclusive. Non-None `is` stays `PIs` (uninterpreted, sound-but-inconclusive — NOT an + error: matches the elaborator's tolerance and keeps unusual `is` usages analyzable). -/ +def cmpopResolvedToLaurel (op : StrataPython.cmpop ResolvedAnn) (rhs : StrataPython.expr ResolvedAnn) : String := + let rhsIsNone := match rhs with | .Constant _ (.ConNone _) _ => true | _ => false + match op with + | .Eq _ => "PEq" | .NotEq _ => "PNEq" | .Lt _ => "PLt" | .LtE _ => "PLe" + | .Gt _ => "PGt" | .GtE _ => "PGe" | .In _ => "PIn" | .NotIn _ => "PNotIn" + | .Is _ => if rhsIsNone then "PEq" else "PIs" + | .IsNot _ => if rhsIsNone then "PNEq" else "PIsNot" + +/-- Parse a Python float literal string (e.g. "0.0", "1.5", "1e10", "1_000.5") into a + `Decimal { mantissa, exponent }`. Returns `none` for inf/nan (unrepresentable). Mirrors + the proven PythonToLaurel `parseFloatString`. A Python float MUST become a `LiteralDecimal` + (which types as `TReal`, matching `from_float : real`), NOT a `LiteralString` — the latter + was a v2 workaround from before kbd had a decimal literal, and mis-types ("real with string"). -/ +private def parseFloatString (s : String) : Option Decimal := do + let lower := s.toLower + if lower == "inf" || lower == "-inf" || lower == "nan" then none + else + let s := s.replace "_" "" + 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 + match coeffStr.splitOn "." with + | [intPart, fracPart] => + let digits := intPart ++ fracPart + let mantissa ← digits.toInt? + some { mantissa, exponent := sciExp - fracPart.length } + | [intPart] => + let mantissa ← intPart.toInt? + some { mantissa, exponent := sciExp } + | _ => none + +mutual + +partial def translateExpr (e : StrataPython.expr ResolvedAnn) : TransM StmtExprMd := do + let sr := e.ann.sr + match e with + | .Constant _ (.ConPos _ n) _ => mkExpr sr (.LiteralInt n.val) + | .Constant _ (.ConNeg _ n) _ => mkExpr sr (.LiteralInt (-n.val)) + | .Constant _ (.ConString _ s) _ => mkExpr sr (.LiteralString s.val) + | .Constant _ (.ConTrue _) _ => mkExpr sr (.LiteralBool true) + | .Constant _ (.ConFalse _) _ => mkExpr sr (.LiteralBool false) + | .Constant _ (.ConNone _) _ => mkExpr sr (.StaticCall rtFromNone []) + | .Constant _ (.ConFloat _ f) _ => + match parseFloatString f.val with + | some d => mkExpr sr (.LiteralDecimal d) + | none => mkExpr sr .Hole -- inf/nan: unrepresentable as Decimal → sound hole + + | .Constant _ _ _ => mkExpr sr .Hole + | .Name ann _ _ => match ann.info with + | .variable name => mkExpr sr (.Var (.Local name.toLaurel)) + | .unresolved => mkExpr sr (.Hole (deterministic := false)) + | .irrelevant => mkExpr sr (.Hole (deterministic := false)) + | _ => panic! "Resolution bug: invalid NodeInfo on Name node" + | .Call ann func args kwargs => match ann.info with + | .funcCall sig => do + -- Prepend the receiver ONLY for instance methods (sig has a receiver slot). + -- A `.static` sig is a module/free function: its `.Attribute` base (e.g. the + -- module `boto3` in `boto3.client(...)`) is NOT an argument and must be dropped. + let receiver ← match sig.params, func with + | .instance _ _, .Attribute _ obj _ _ => pure [← translateExpr obj] + | _, _ => pure [] + let posArgs ← args.val.toList.mapM translateExpr + let kwargPairs ← kwargs.val.toList.filterMapM fun kw => match kw with + | .mk_keyword _ kwName kwExpr => do + let val ← translateExpr kwExpr + match kwName.val with | some n => pure (some (n.val, val)) | none => pure none + mkExpr sr (.StaticCall sig.laurelName (← sig.matchArgs (receiver ++ posArgs) kwargPairs translateExpr (mkKwargs := (do return some (← mkExpr sr (.Hole (deterministic := false))))))) + | .classNew cls initSig => do + let tmp ← freshId "new" + let tmpRef ← mkExpr sr (.Var (.Local tmp)) + let assignNew ← mkExpr sr (.Assign [toVarTarget tmpRef] (← mkExpr sr (.New cls.toLaurel))) + let posArgs ← args.val.toList.mapM translateExpr + let kwargPairs ← kwargs.val.toList.filterMapM fun kw => match kw with + | .mk_keyword _ kwName kwExpr => do + let val ← translateExpr kwExpr + match kwName.val with | some n => pure (some (n.val, val)) | none => pure none + let initCall ← mkExpr sr (.StaticCall initSig.laurelName (← initSig.matchArgs ([tmpRef] ++ posArgs) kwargPairs translateExpr (mkKwargs := (do return some (← mkExpr sr (.Hole (deterministic := false))))))) + tell [assignNew, initCall] + pure tmpRef + | .unresolved => mkExpr sr (.Hole (deterministic := false)) + | _ => mkExpr sr (.Hole (deterministic := false)) + | .BinOp ann left _ right => match ann.info with + | .funcCall sig => do + let l ← translateExpr left; let r ← translateExpr right + mkExpr sr (.StaticCall sig.laurelName (← sig.matchArgs [l, r] [] translateExpr)) + | _ => mkExpr sr .Hole + | .BoolOp ann _ operands => match ann.info with + | .funcCall sig => do + let exprs ← operands.val.toList.mapM translateExpr + match exprs with + | [] => mkExpr sr .Hole + | first :: rest => rest.foldlM (fun acc e => do + let args ← sig.matchArgs [acc, e] [] translateExpr + mkExpr sr (.StaticCall sig.laurelName args)) first + | _ => mkExpr sr .Hole + | .UnaryOp ann _ operand => match ann.info with + | .funcCall sig => do + mkExpr sr (.StaticCall sig.laurelName (← sig.matchArgs [← translateExpr operand] [] translateExpr)) + | _ => mkExpr sr .Hole + | .Compare ann left ops comparators => match ann.info with + | .funcCall sig => do + if comparators.val.size == 1 then + -- Use the None-aware op name (`is None`→PEq), NOT sig.laurelName (which is the + -- uninterpreted PIs/PIsNot for `is`/`is not`). matchArgs still wraps the operands. + let opName := cmpopResolvedToLaurel (ops.val[0]!) (comparators.val[0]!) + let l ← translateExpr left; let r ← translateExpr comparators.val[0]! + mkExpr sr (.StaticCall (rt opName) (← sig.matchArgs [l, r] [] translateExpr)) + else do + -- Chained comparison `e0 op0 e1 op1 e2 ...` lowers to + -- `(e0 op0 e1) and (e1 op1 e2) and ...`. Each operand is translated once; + -- each pairwise op uses its own runtime fn (PLt/PLe/PEq/...) named by the + -- resolved cmpop (None-aware). The conjunction uses `PAnd`. Operands are + -- Any-valued; the elaborator coerces them to the op params. + let operandExprs := #[left] ++ comparators.val + -- build operand translations once + let mut translated : Array StmtExprMd := #[] + for e in operandExprs do translated := translated.push (← translateExpr e) + -- fold pairwise comparisons joined by PAnd + let mut acc : Option StmtExprMd := none + for i in [0:ops.val.size] do + let opName := cmpopResolvedToLaurel (ops.val[i]!) (comparators.val[i]!) + let li := translated[i]! + let ri := translated[i+1]! + let cmp ← mkExpr sr (.StaticCall (rt opName) [li, ri]) + acc := some (← match acc with + | none => pure cmp + | some prev => mkExpr sr (.StaticCall (rt "PAnd") [prev, cmp])) + match acc with + | some result => pure result + | none => mkExpr sr .Hole -- unreachable: comparators non-empty + | _ => mkExpr sr .Hole + | .Attribute ann obj _ _ => match ann.info with + | .attribute name => do mkExpr sr (.Var (.Field (← translateExpr obj) name.toLaurel)) + | _ => mkExpr sr .Hole + | .Subscript _ container slice _ => do + let c ← translateExpr container + let idx ← match slice with + | .Slice _ start stop _ => do + let s ← match start.val with + | some e => translateExpr e + | none => mkExpr sr (.LiteralInt 0) + let e ← match stop.val with + | some e => mkExpr sr (.StaticCall rtOptSome [← translateExpr e]) + | none => mkExpr sr (.StaticCall rtOptNone []) + mkExpr sr (.StaticCall rtFromSlice [s, e]) + | _ => translateExpr slice + mkExpr sr (.StaticCall rtAnyGet [c, idx]) + | .List _ elts _ => do + let es ← elts.val.toList.mapM translateExpr + let nil ← mkExpr sr (.StaticCall rtListAnyNil []) + es.foldrM (fun e acc => mkExpr sr (.StaticCall rtListAnyCons [e, acc])) nil + | .Tuple _ elts _ => do + let es ← elts.val.toList.mapM translateExpr + let nil ← mkExpr sr (.StaticCall rtListAnyNil []) + es.foldrM (fun e acc => mkExpr sr (.StaticCall rtListAnyCons [e, acc])) nil + | .Dict _ keys vals => do + let ks ← keys.val.toList.mapM (fun k => match k with + | .some_expr _ e => translateExpr e | .missing_expr _ => mkExpr sr .Hole) + let vs ← vals.val.toList.mapM translateExpr + let empty ← mkExpr sr (.StaticCall rtDictStrAnyEmpty []) + (List.zip ks vs).foldrM (fun (k, v) acc => + mkExpr sr (.StaticCall rtDictStrAnyCons [k, v, acc])) empty + | .IfExp _ test body orelse => do + mkExpr sr (.IfThenElse (← translateExpr test) (← translateExpr body) (some (← translateExpr orelse))) + | .JoinedStr _ values => do + if values.val.isEmpty then mkExpr sr (.LiteralString "") + else do + let parts ← values.val.toList.mapM translateExpr + let init ← mkExpr sr (.LiteralString "") + parts.foldlM (fun acc p => mkExpr sr (.StaticCall rtPAdd [acc, p])) init + | .FormattedValue _ value _ _ => do + mkExpr sr (.StaticCall rtToStringAny [← translateExpr value]) + | _ => mkExpr sr .Hole + +where + ann (e : StrataPython.expr ResolvedAnn) : ResolvedAnn := match e with + | .Name a .. => a | .Constant a .. => a | .BinOp a .. => a | .Compare a .. => a + | .BoolOp a .. => a | .UnaryOp a .. => a | .Call a .. => a | .Attribute a .. => a + | .Subscript a .. => a | .List a .. => a | .Tuple a .. => a | .Dict a .. => a + | .Set a .. => a | .IfExp a .. => a | .JoinedStr a .. => a | .FormattedValue a .. => a + | .Lambda a .. => a | .ListComp a .. => a | .SetComp a .. => a | .DictComp a .. => a + | .GeneratorExp a .. => a | .NamedExpr a .. => a | .Slice a .. => a | .Starred a .. => a + | .Await a .. => a | .Yield a .. => a | .YieldFrom a .. => a | .TemplateStr a .. => a + | .Interpolation a .. => a + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Statement Translation +-- ═══════════════════════════════════════════════════════════════════════════════ + +partial def translateStmtList (stmts : List (StrataPython.stmt ResolvedAnn)) : TransM Unit := + stmts.forM translateStmt + +partial def execWriter (stmts : List (StrataPython.stmt ResolvedAnn)) : TransM (List StmtExprMd) := do + let (_, s) ← collect (translateStmtList stmts) + pure s + +partial def translateAssign (sr : SourceRange) (target : StrataPython.expr ResolvedAnn) + (value : StrataPython.expr ResolvedAnn) : TransM Unit := do + match value with + | .Call ann _ args kwargs => match ann.info with + | .classNew cls initSig => do + let targetExpr ← translateExpr target + let assignNew ← mkExpr sr (.Assign [toVarTarget targetExpr] (← mkExpr sr (.New cls.toLaurel))) + let posArgs ← args.val.toList.mapM translateExpr + let kwargPairs ← kwargs.val.toList.filterMapM fun kw => match kw with + | .mk_keyword _ kwName kwExpr => do + let val ← translateExpr kwExpr + match kwName.val with | some n => pure (some (n.val, val)) | none => pure none + let initCall ← mkExpr sr (.StaticCall initSig.laurelName (← initSig.matchArgs ([targetExpr] ++ posArgs) kwargPairs translateExpr (mkKwargs := (do return some (← mkExpr sr (.Hole (deterministic := false))))))) + tell [assignNew, initCall] + | _ => tell [← mkExpr sr (.Assign [toVarTarget (← translateExpr target)] (← translateExpr value))] + | _ => tell [← mkExpr sr (.Assign [toVarTarget (← translateExpr target)] (← translateExpr value))] + +partial def translateStmt (s : StrataPython.stmt ResolvedAnn) : TransM Unit := do + let sr := s.ann.sr + match s with + | .Assign _ targets value _ => do + if targets.val.size != 1 then + -- Multiple assignment targets: translate each target as a separate assign + -- (sound: Python chained assignment `a = b = expr` assigns the same value to all targets) + let rhsExpr ← translateExpr value + for target in targets.val.toList do + tell [← mkExpr sr (.Assign [toVarTarget (← translateExpr target)] rhsExpr)] + else + let target := targets.val[0]! + match target with + | .Tuple _ elts _ => do + let rhsExpr ← translateExpr value + let tmp ← freshId "unpack" + let tmpDecl ← mkLocalDecl sr tmp (mkTypeDefault (.TCore "Any")) (some rhsExpr) + let tmpRef ← mkExpr sr (.Var (.Local tmp)) + tell [tmpDecl] + unpackTargets sr elts.val.toList tmpRef + | .Subscript .. => do + subscriptWriteBack sr target (← translateExpr value) + | _ => translateAssign sr target value + + | .AnnAssign _ target _ value _ => do + match value.val with + | some val => translateAssign sr target val + | none => pure () + + | .AugAssign ann target _ value => match ann.info with + | .funcCall sig => do + let t ← translateExpr target; let v ← translateExpr value + let newVal ← mkExpr sr (.StaticCall sig.laurelName (← sig.matchArgs [t, v] [] translateExpr)) + match target with + | .Subscript .. => subscriptWriteBack sr target newVal + | _ => tell [← mkExpr sr (.Assign [toVarTarget t] newVal)] + | _ => tell [← mkExpr sr .Hole] + + | .If _ test body orelse => do + let cond ← translateExpr test + let thn ← mkExpr sr (.Block (← execWriter body.val.toList) none) + let els ← if orelse.val.isEmpty then pure none + else pure (some (← mkExpr sr (.Block (← execWriter orelse.val.toList) none))) + tell [← mkExpr sr (.IfThenElse cond thn els)] + + | .While _ test body _ => do + let (bk, ct) ← pushLoopLabel "loop" + let cond ← translateExpr test + let inner ← mkExpr sr (.Block (← execWriter body.val.toList) (some ct.text)) + let outer ← mkExpr sr (.Block [← mkExpr sr (.While cond [] none inner)] (some bk.text)) + popLoopLabel; tell [outer] + + | .For _ target iter body _ _ => do + let (bk, ct) ← pushLoopLabel "for" + let iterExpr ← translateExpr iter + let bodyStmts ← execWriter body.val.toList + let (havocStmts, assumeTarget) ← match target with + | .Tuple _ elts _ => do + let tmp ← freshId "for_iter" + let tmpRef ← mkExpr sr (.Var (.Local tmp)) + let decl ← mkLocalDecl sr tmp (mkTypeDefault (.TCore "Any")) none + let havoc ← mkExpr sr (.Assign [toVarTarget tmpRef] (← mkExpr sr (.Hole (deterministic := false)))) + let (_, unpacks) ← collect (unpackTargets sr elts.val.toList tmpRef) + pure ([decl, havoc] ++ unpacks, tmpRef) + | _ => do + let tgt ← translateExpr target + let havoc ← mkExpr sr (.Assign [toVarTarget tgt] (← mkExpr sr (.Hole (deterministic := false)))) + pure ([havoc], tgt) + let assume ← mkExpr sr (.Assume (← mkExpr sr (.StaticCall rtPIn [assumeTarget, iterExpr]))) + let inner ← mkExpr sr (.Block (havocStmts ++ [assume] ++ bodyStmts) (some ct.text)) + let outer ← mkExpr sr (.Block [inner] (some bk.text)) + popLoopLabel; tell [outer] + + | .Return _ value => do + match value.val with + | some expr => do + let e ← translateExpr expr + tell [← mkExpr sr (.Assign [{ val := .Local rtLaurelResult, source := sourceRangeToMd (← get).filePath sr }] e), ← mkExpr sr (.Exit "$body")] + | none => tell [← mkExpr sr (.Exit "$body")] + + | .Assert _ test _ => tell [← mkExpr sr (.Assert ({ condition := ← translateExpr test } : Condition))] + | .Expr _ (.Constant _ (.ConString _ _) _) => pure () + | .Expr _ value => tell [← translateExpr value] + | .Pass _ => pure () + | .Break _ => tell [← mkExpr sr (.Exit ((← currentBreakLabel).map (·.text) |>.getD "break"))] + | .Continue _ => tell [← mkExpr sr (.Exit ((← currentContinueLabel).map (·.text) |>.getD "continue"))] + + | .Try _ body handlers _ _ => translateTryExcept sr body handlers + | .TryStar _ body handlers _ _ => translateTryExcept sr body handlers + + | .With _ items body _ => do + let (pre, post) ← items.val.toList.foldlM (fun acc item => do + let (pre, post) := acc + match item with + | .mk_withitem ann ctxExpr optVars => do + let mgr ← translateExpr ctxExpr + match ann.info with + | .withCtx enterSig exitSig => + let enterCall ← mkExpr sr (.StaticCall enterSig.laurelName [mgr]) + let exitCall ← mkExpr sr (.StaticCall exitSig.laurelName [mgr]) + match optVars.val with + | some varExpr => + pure (pre ++ [← mkExpr sr (.Assign [toVarTarget (← translateExpr varExpr)] enterCall)], post ++ [exitCall]) + | none => pure (pre ++ [enterCall], post ++ [exitCall]) + | _ => + let enter ← mkExpr sr (.Hole (deterministic := false)) + let exit ← mkExpr sr (.Hole (deterministic := false)) + match optVars.val with + | some varExpr => + pure (pre ++ [← mkExpr sr (.Assign [toVarTarget (← translateExpr varExpr)] enter)], post ++ [exit]) + | none => pure (pre ++ [enter], post ++ [exit]) + ) (([] : List StmtExprMd), ([] : List StmtExprMd)) + let bodyStmts ← execWriter body.val.toList + tell (pre ++ bodyStmts ++ post) + + | .Raise _ exc _ => do + match exc.val with + | some excExpr => do + let errorExpr ← translateExpr excExpr + tell [← mkExpr sr (.Assign [{ val := .Local rtMaybeExcept, source := sourceRangeToMd (← get).filePath sr }] errorExpr)] + | none => tell [← mkExpr sr (.Assign [{ val := .Local rtMaybeExcept, source := sourceRangeToMd (← get).filePath sr }] (← mkExpr sr .Hole))] + + | .Import _ _ => pure () + | .ImportFrom _ _ _ _ => pure () + | .Global _ _ => pure () + | .Nonlocal _ _ => pure () + | .Delete _ _ => pure () + | .AsyncFor _ _ _ _ _ _ => tell [← mkExpr sr .Hole] + | .AsyncWith _ _ _ _ => tell [← mkExpr sr .Hole] + | .Match _ _ _ => tell [← mkExpr sr .Hole] + | .TypeAlias _ _ _ _ => pure () + | .FunctionDef _ _ _ _ _ _ _ _ => pure () + | .AsyncFunctionDef _ _ _ _ _ _ _ _ => pure () + | .ClassDef _ _ _ _ _ _ _ => pure () + +where + ann (s : StrataPython.stmt ResolvedAnn) : ResolvedAnn := match s with + | .FunctionDef a .. => a | .AsyncFunctionDef a .. => a | .ClassDef a .. => a + | .Return a .. => a | .Delete a .. => a | .Assign a .. => a | .AugAssign a .. => a + | .AnnAssign a .. => a | .For a .. => a | .AsyncFor a .. => a | .While a .. => a + | .If a .. => a | .With a .. => a | .AsyncWith a .. => a | .Raise a .. => a + | .Try a .. => a | .TryStar a .. => a | .Assert a .. => a | .Import a .. => a + | .ImportFrom a .. => a | .Global a .. => a | .Nonlocal a .. => a | .Expr a .. => a + | .Pass a => { sr := a.sr, info := .irrelevant } | .Break a => { sr := a.sr, info := .irrelevant } + | .Continue a => { sr := a.sr, info := .irrelevant } | .Match a .. => a | .TypeAlias a .. => a + +partial def unpackTargets (sr : SourceRange) (elts : List (StrataPython.expr ResolvedAnn)) + (sourceRef : StmtExprMd) : TransM Unit := do + for (elt, idx) in elts.zipIdx do + let getExpr ← mkExpr sr (.StaticCall rtAnyGet [sourceRef, ← mkExpr sr (.LiteralInt ↑idx)]) + match elt with + | .Tuple _ innerElts _ => do + let innerTmp ← freshId "unpack" + let innerRef ← mkExpr sr (.Var (.Local innerTmp)) + let innerDecl ← mkLocalDecl sr innerTmp (mkTypeDefault (.TCore "Any")) (some getExpr) + tell [innerDecl] + unpackTargets sr innerElts.val.toList innerRef + | _ => do + let tgt ← translateExpr elt + tell [← mkExpr sr (.Assign [toVarTarget tgt] getExpr)] + +partial def collectSubscriptChain (expr : StrataPython.expr ResolvedAnn) : TransM (StrataPython.expr ResolvedAnn × List (StrataPython.expr ResolvedAnn)) := do + match expr with + | .Subscript _ container slice _ => + let (root, innerIndices) ← collectSubscriptChain container + pure (root, innerIndices ++ [slice]) + | other => pure (other, []) + +/-- Write `rhs` back into the subscript target `a[i]...[j]` via `Any_sets`, then + assign the updated container to its root. Used by both plain and augmented + subscript assignment — a subscript is not an lvalue identifier. -/ +partial def subscriptWriteBack (sr : SourceRange) (target : StrataPython.expr ResolvedAnn) + (rhs : StmtExprMd) : TransM Unit := do + let (root, indices) ← collectSubscriptChain target + let rootExpr ← translateExpr root + let idxList ← indices.foldrM (fun idx acc => do + let idxExpr ← match idx with + | .Slice _ start stop _ => do + let s' ← match start.val with + | some e => mkExpr sr (.StaticCall rtAnyAsInt [← translateExpr e]) + | none => mkExpr sr (.LiteralInt 0) + let e' ← match stop.val with + | some e => mkExpr sr (.StaticCall rtOptSome [← mkExpr sr (.StaticCall rtAnyAsInt [← translateExpr e])]) + | none => mkExpr sr (.StaticCall rtOptNone []) + mkExpr sr (.StaticCall rtFromSlice [s', e']) + | _ => translateExpr idx + mkExpr sr (.StaticCall rtListAnyCons [idxExpr, acc]) + ) (← mkExpr sr (.StaticCall rtListAnyNil [])) + let setsCall ← mkExpr sr (.StaticCall rtAnySets [idxList, rootExpr, rhs]) + tell [← mkExpr sr (.Assign [toVarTarget rootExpr] setsCall)] + +/-- Wrap each body statement with an isError check that exits to the catcher block. -/ +private partial def wrapBodyWithErrorChecks (sr : SourceRange) (catchersLabel : String) + (bodyStmts : List StmtExprMd) : TransM (List StmtExprMd) := + bodyStmts.foldlM (fun acc stmt => do + let ref ← mkExpr sr (.Var (.Local rtMaybeExcept)) + let check ← mkExpr sr (.StaticCall rtIsError [ref]) + let ifCheck ← mkExpr sr (.IfThenElse check (← mkExpr sr (.Exit catchersLabel)) none) + pure (acc ++ [stmt, ifCheck])) [] + +/-- Translate exception handlers to their statement lists. -/ +private partial def translateHandlers (handlers : List (StrataPython.excepthandler ResolvedAnn)) + : TransM (List StmtExprMd) := do + let lists ← handlers.mapM fun handler => match handler with + | .ExceptHandler _ _ _ handlerBody => execWriter handlerBody.val.toList + pure lists.flatten + +partial def translateTryExcept (sr : SourceRange) + (body : Ann (Array (StrataPython.stmt ResolvedAnn)) ResolvedAnn) + (handlers : Ann (Array (StrataPython.excepthandler ResolvedAnn)) ResolvedAnn) : TransM Unit := do + let tryLabel := s!"try_end_{sr.start.byteIdx}" + let catchersLabel := s!"exception_handlers_{sr.start.byteIdx}" + let bodyStmts ← execWriter body.val.toList + let withChecks ← wrapBodyWithErrorChecks sr catchersLabel bodyStmts + let exitTry ← mkExpr sr (.Exit tryLabel) + let catchers ← mkExpr sr (.Block (withChecks ++ [exitTry]) (some catchersLabel)) + let handlerStmts ← translateHandlers handlers.val.toList + tell [← mkExpr sr (.Block ([catchers] ++ handlerStmts) (some tryLabel))] + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Function / Class / Module — reads NodeInfo directly +-- ═══════════════════════════════════════════════════════════════════════════════ + +/-- Rewrite identifiers in a precondition expression: each declared parameter name + `x` becomes the input name `$in_x`. Laurel `requires` clauses are evaluated in + the procedure's INPUT scope (where params are named `$in_x`), not the body scope + (where they are copied to locals `x`). -/ +partial def renameParamsToInputs (paramNames : List String) (e : StmtExprMd) : StmtExprMd := + let rw := renameParamsToInputs paramNames + let rwOpt := fun (o : Option StmtExprMd) => o.map rw + let rwList := fun (l : List StmtExprMd) => l.map rw + let rwVar := fun (v : VariableMd) => match v.val with + | .Local name => + if paramNames.contains name.text then { v with val := .Local { name with text := s!"$in_{name.text}" } } else v + | .Field t fn => { v with val := .Field (rw t) fn } + | .Declare _ => v + let rwVarList := fun (l : List VariableMd) => l.map rwVar + let val := match e.val with + | .Var v => .Var (rwVar { val := v, source := e.source }).val + | .IfThenElse c t el => .IfThenElse (rw c) (rw t) (rwOpt el) + | .Block ss l => .Block (rwList ss) l + | .Assign ts v => .Assign (rwVarList ts) (rw v) + | .PureFieldUpdate t fn nv => .PureFieldUpdate (rw t) fn (rw nv) + | .StaticCall c args => .StaticCall c (rwList args) + | .PrimitiveOp op args => .PrimitiveOp op (rwList args) + | .ReferenceEquals l r => .ReferenceEquals (rw l) (rw r) + | .AsType t ty => .AsType (rw t) ty + | .IsType t ty => .IsType (rw t) ty + | .InstanceCall t c args => .InstanceCall (rw t) c (rwList args) + | .Old v => .Old (rw v) + | .Fresh v => .Fresh (rw v) + | .Assert c => .Assert (c.mapCondition rw) + | .Assume c => .Assume (rw c) + | .Return v => .Return (rwOpt v) + | other => other + { e with val } + +private partial def buildProcInputs (aliases : Std.HashMap String HighType) (sig : FuncSig) : List Parameter := + sig.laurelDeclInputs.map fun (lId, pTy) => + { name := { text := s!"$in_{lId.text}", uniqueId := none }, type := mkTypeDefault (pythonTypeToHighType aliases pTy) } + +private partial def buildProcOutputs (aliases : Std.HashMap String HighType) (sig : FuncSig) : List Parameter := + -- LaurelResult is typed by the USER-DECLARED return type (v2 verbatim). The frontend + -- trusts the annotation; the coercion mechanism reconciles the body's Any-valued + -- expressions with the declared type at the `return e` assignment. (Do NOT type it + -- `Any` and recover via postconditions — that compensates for a deviation v2 never made.) + -- EXCEPTION (kbd-forced): a `-> None` return maps to `.TVoid`, which kbd's Laurel→Core + -- renders as a `bool` PLACEHOLDER (LaurelToCoreSchemaPass). But `return None` assigns + -- `from_None : Any` to LaurelResult, so the body needs an `Any` slot — `bool := Any` + -- fails kbd's Core type-checker ("unify bool with Any"). Python `None` IS a value + -- (`from_None`) in the Any model, so type the result `Any`. (v2's Core accepted void + -- here; kbd's stricter placeholder does not — same class of kbd adaptation as others.) + let retHigh := pythonTypeToHighType aliases sig.returnType + let resultTy := match retHigh with | .TVoid => HighType.TCore "Any" | t => t + [{ name := rtLaurelResult, type := mkTypeDefault resultTy }, + { name := rtMaybeExcept, type := mkTypeDefault (.TCore "Error") }] + +private partial def buildParamCopies (aliases : Std.HashMap String HighType) (sig : FuncSig) : List StmtExprMd := + sig.laurelDeclInputs.map fun (lId, pTy) => + mkLocalDeclDefault lId (mkTypeDefault (pythonTypeToHighType aliases pTy)) + (some (mkExprDefault (.Var (.Local { text := s!"$in_{lId.text}", uniqueId := none })))) + +private partial def buildLocalDecls (aliases : Std.HashMap String HighType) (sig : FuncSig) : List StmtExprMd := + sig.laurelLocals.map fun (lId, lTy) => + mkLocalDeclDefault lId (mkTypeDefault (pythonTypeToHighType aliases lTy)) none + +private partial def splitPreconditions (body : List (StrataPython.stmt ResolvedAnn)) + : List (StrataPython.stmt ResolvedAnn) × List (StrataPython.stmt ResolvedAnn) := + body.span fun s => match s with | .Assert _ _ _ => true | _ => false + +partial def translateFunction (sig : FuncSig) (body : Array (StrataPython.stmt ResolvedAnn)) + (sr : SourceRange) : TransM Procedure := do + let aliases := (← get).typeAliases + let inputs := buildProcInputs aliases sig + let outputs := buildProcOutputs aliases sig + let paramCopies := buildParamCopies aliases sig + let localDecls := buildLocalDecls aliases sig + let (preAsserts, restBody) := splitPreconditions body.toList + let paramNames := sig.laurelDeclInputs.map (·.1.text) + let preconditions ← preAsserts.mapM fun s => match s with + | .Assert _ test _ => do pure ({ condition := renameParamsToInputs paramNames (← translateExpr test) } : Condition) + | _ => throw (.internalError "non-Assert statement in precondition prefix") + let bodyStmts ← execWriter restBody + let bodyBlock ← mkExpr sr (.Block (paramCopies ++ localDecls ++ bodyStmts) none) + -- kbd has no Procedure.md field: the source range lives on the proc name's + -- `.source`. splitProcNames reads it (via getFileRange) to tell user procs from + -- prelude; without it every user proc looks like prelude and nothing is verified. + let procName := { sig.laurelName with source := sourceRangeToMd (← get).filePath sr } + pure { + name := procName + inputs, outputs, preconditions + decreases := none + isFunctional := false + -- v2 used `.Transparent bodyBlock` with no postconditions. The only kbd-forced + -- change is the modifies clause: field-mutating procs need `modifies *` + -- (wildcardModifies) so modifiesClausesTransformPass emits no frame condition; + -- empty modifies would impose a "heap unchanged" obligation contradicting the writes. + -- Postconditions stay EMPTY — the declared return type lives on the output param. + body := .Opaque [] (some bodyBlock) [← mkExpr sr .All] + } + +partial def translateClass (name : PythonIdentifier) (attributes : List (PythonIdentifier × PythonType)) + (_methods : List FuncSig) (body : Array (StrataPython.stmt ResolvedAnn)) + : TransM (TypeDefinition × List Procedure) := do + -- Composite fields keep their USER-DECLARED type. The Python frontend trusts user + -- annotations; the coercion mechanism handles impedance (e.g. boxing to `Any` where a + -- value context demands it). The box protocol stores/loads each field at its declared + -- type (`Box..`), so a field read synthesizes that type — `e ⇒ &{…l:A_l…} ⊢ e.l ⇒ A_l`. + let laurelFields := attributes.map fun (fId, fTy) => + ({ name := fId.toLaurel, isMutable := true, type := mkTypeDefault (pythonTypeToHighType {} fTy) } : Field) + let procResults ← body.toList.mapM fun stmt => match stmt with + | .FunctionDef ann _ _ fbody _ _ _ _ => match ann.info with + | .funcDecl sig => do pure (some (← translateFunction sig fbody.val ann.sr)) + | _ => pure none + | .AsyncFunctionDef ann _ _ fbody _ _ _ _ => match ann.info with + | .funcDecl sig => do pure (some (← translateFunction sig fbody.val ann.sr)) + | _ => pure none + | _ => pure none + let procs := procResults.filterMap id + -- Synthesize a default `Class@__init__` if the class defines none. Every `C(...)` + -- instantiation (classNew) emits a call to `C@__init__`; without a definition that + -- call fails to elaborate (lookupFuncSig miss) and takes the whole caller down. The + -- default ctor takes only `self : Class`, has empty body + the standard Any/Error + -- outputs. (Proven PythonToLaurel synthesizes the same default.) + let initName := s!"{name.toLaurel.text}@__init__" + let hasInit := procs.any (fun p => p.name.text == initName) + let procs := if hasInit then procs else + -- The default init takes NO inputs: Resolution's synthesized `initSig` for a class + -- with no `__init__` has no receiver slot, so `classNew` emits `Class@__init__()` + -- with zero args. The proc decl must match that arity (a `self` input would make the + -- Core call-arity check fail: "input length and args length mismatch"). + let defaultInit : Procedure := { + name := { text := initName, uniqueId := none } + inputs := [] + outputs := [{ name := rtLaurelResult, type := mkTypeDefault (.TCore "Any") }, + { name := rtMaybeExcept, type := mkTypeDefault (.TCore "Error") }] + preconditions := [] + decreases := none + isFunctional := false + body := .Opaque [] (some (mkExprDefault (.Block [] none))) [mkExprDefault .All] } + procs ++ [defaultInit] + let ct : CompositeType := { name := name.toLaurel, extending := [], fields := laurelFields, instanceProcedures := [] } + pure (.Composite ct, procs) + +partial def translateModule (program : ResolvedPythonProgram) : TransM Strata.Laurel.Program := do + -- Collect module-level assignment type-aliases: `MyInt = int` where the RHS is a + -- type-name expression. These resolve annotations like `x: MyInt` to the aliased + -- type rather than a phantom `UserDefined` composite. (Only bare `Name = ` + -- at module level; the RHS is interpreted via `pythonTypeToHighType` itself.) + let aliases : Std.HashMap String HighType := program.stmts.toList.foldl (init := {}) fun m stmt => + match stmt with + | .Assign _ targets value _ => + match targets.val.toList with + | [.Name _ tn _] => + -- RHS is a resolved type-name expr (`expr ResolvedAnn`); map its name string + -- through the same builtin table `pythonTypeToHighType` uses for `.Name`. + match value with + | .Name _ rn _ => + let ty := pythonTypeToHighType {} (.Name SourceRange.none ⟨SourceRange.none, rn.val⟩ (.Load SourceRange.none)) + m.insert tn.val ty + | _ => m + | _ => m + | _ => m + modify fun s => { s with typeAliases := aliases } + let init : List Procedure × List TypeDefinition × List (StrataPython.stmt ResolvedAnn) := ([], [], []) + let (procedures, types, otherStmts) ← program.stmts.toList.foldlM (fun (procs, tys, others) stmt => do + match stmt with + | .FunctionDef ann _ _ body _ _ _ _ => match ann.info with + | .funcDecl sig => + let proc ← translateFunction sig body.val ann.sr + pure (procs ++ [proc], tys, others) + | _ => pure (procs, tys, others) + | .AsyncFunctionDef ann _ _ body _ _ _ _ => match ann.info with + | .funcDecl sig => + let proc ← translateFunction sig body.val ann.sr + pure (procs ++ [proc], tys, others) + | _ => pure (procs, tys, others) + | .ClassDef ann _ _ _ body _ _ => match ann.info with + | .classDecl name fields methods => + let (td, ms) ← translateClass name fields methods body.val + pure (procs ++ ms, tys ++ [td], others) + | _ => pure (procs, tys, others) + | other => pure (procs, tys, others ++ [other]) + ) init + let procedures ← if otherStmts.isEmpty then pure procedures + else do + let sr : SourceRange := default + let nameId := rt "__name__" + let nameDecl ← mkLocalDecl sr nameId (mkTypeDefault .TString) (some (mkExprDefault (.LiteralString "__main__"))) + let localDecls := program.moduleLocals.map fun (lId, lTy) => + mkLocalDeclDefault lId.toLaurel (mkTypeDefault (pythonTypeToHighType aliases lTy)) none + let bodyStmts ← execWriter otherStmts + let bodyBlock ← mkExpr sr (.Block ([nameDecl] ++ localDecls ++ bodyStmts) none) + let mainOutputs : List Parameter := + [{ name := rtLaurelResult, type := mkTypeDefault (.TCore "Any") }, + { name := rtMaybeExcept, type := mkTypeDefault (.TCore "Error") }] + let mainName := { (rt "__main__") with source := sourceRangeToMd (← get).filePath sr } + let mainProc : Procedure := { name := mainName, inputs := [], outputs := mainOutputs, preconditions := [], decreases := none, isFunctional := false, body := .Opaque [] (some bodyBlock) [← mkExpr sr .All] } + pure (procedures ++ [mainProc]) + return { staticProcedures := procedures, staticFields := [], types, constants := [] } + +end -- mutual + +-- ═══════════════════════════════════════════════════════════════════════════════ +-- Runner +-- ═══════════════════════════════════════════════════════════════════════════════ + +/-- Entry point: translates a resolved Python program to a Laurel program. + Returns the Laurel program and final translator state, or a `TransError`. -/ +def runTranslation (program : ResolvedPythonProgram) + (filePath : String := "") + : Except TransError (Strata.Laurel.Program × TransState) := + (translateModule program).run.run { filePath := filePath } |>.map fun ((prog, _stmts), state) => (prog, state) + +end -- public section +end StrataPython.Translation diff --git a/StrataPython/StrataPythonTest/tests/test_zzsound.py b/StrataPython/StrataPythonTest/tests/test_zzsound.py new file mode 100644 index 0000000000..8ad27ecad0 --- /dev/null +++ b/StrataPython/StrataPythonTest/tests/test_zzsound.py @@ -0,0 +1,9 @@ +def test_countdown_wrong() -> int: + n: int = 5 + total: int = 0 + while n > 0: + total = total + n + n = n - 1 + assert total == 99, "FALSE: total is 15 not 99" + return total +test_countdown_wrong() diff --git a/StrataPython/StrataPythonTest/user_errors.txt b/StrataPython/StrataPythonTest/user_errors.txt new file mode 100644 index 0000000000..075f3e69c1 --- /dev/null +++ b/StrataPython/StrataPythonTest/user_errors.txt @@ -0,0 +1,4 @@ +(set-info :file "tests/test_with_void_enter.py") +(set-info :start 51) +(set-info :stop 75) +(set-info :error-message "Resolution failed: 'active' is not defined") diff --git a/StrataPython/lakefile.toml b/StrataPython/lakefile.toml index 56c270f67c..2ba0b658de 100644 --- a/StrataPython/lakefile.toml +++ b/StrataPython/lakefile.toml @@ -31,6 +31,11 @@ name = "pyAnalyzeLaurel" root = "Scripts.pyAnalyzeLaurel" needs = ["StrataPython"] +[[lean_exe]] +name = "pyAnalyzeV2" +root = "Scripts.pyAnalyzeV2" +needs = ["StrataPython"] + [[lean_exe]] name = "pyAnalyzeToGoto" root = "Scripts.pyAnalyzeToGoto" @@ -65,3 +70,8 @@ needs = ["StrataPython"] name = "pySpecs" root = "Scripts.pySpecs" needs = ["StrataPython"] + +[[lean_exe]] +name = "strata" +root = "StrataMain" +needs = ["StrataPython"] diff --git a/StrataPython/run_v2_benchmarks.sh b/StrataPython/run_v2_benchmarks.sh new file mode 100755 index 0000000000..059b05878f --- /dev/null +++ b/StrataPython/run_v2_benchmarks.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Run pyAnalyzeV2 (--solver z3, bugFinding) over the StrataInternalBenchmarks corpus. +# Raw per-file output -> /tmp/v2_bench_raw.txt. Tally computed from raw via awk (bash-3.2 +# safe; no `declare -A`). Usage: run_v2_benchmarks.sh [sample_per_set] (0/empty = all). +cd "$(dirname "$0")" || exit 1 +BIN=.lake/build/bin/pyAnalyzeV2 +BENCH=/Users/somayyas/workspace/StrataPythonBuildBackendWS/src/StrataInternalBenchmarks +RAW=/tmp/v2_bench_raw.txt +SAMPLE="${1:-0}" +: > "$RAW" +total=0 + +for set in aws_samples ecc_examples bash_party_examples service_benchmarks; do + ions=$(find "$BENCH/$set" -name "*.python.st.ion" 2>/dev/null | sort) + [ "$SAMPLE" -gt 0 ] 2>/dev/null && ions=$(echo "$ions" | head -"$SAMPLE") + for ion in $ions; do + [ -f "$ion" ] || continue + total=$((total+1)) + out=$(timeout 90 "$BIN" --solver z3 --check-mode bugFinding --check-level full "$ion" 2>&1) + code=$? + echo "===== [$set] $(basename "$ion") (exit $code) =====" >> "$RAW" + echo "$out" | grep -E '^DETAIL:|^RESULT:' | tail -2 >> "$RAW" + [ $code -ne 0 ] && ! echo "$out" | grep -q '^RESULT:' && echo " -> CRASH/timeout $code" >> "$RAW" + done +done + +echo "" +echo "============ pyAnalyzeV2 over StrataInternalBenchmarks (sample=$SAMPLE, total run=$total) ============" +echo "--- overall ---" +grep '^RESULT:' "$RAW" | sort | uniq -c | sort -rn +echo "crash/timeout (no RESULT): $(grep -c 'CRASH/timeout' "$RAW")" +echo "--- per set ---" +awk '/^===== \[/ { l=$0; sub(/^===== \[/,"",l); sub(/\].*/,"",l); s=l; next } + /^RESULT:/ { r=$0; sub(/^RESULT: /,"",r); print s"\t"r }' "$RAW" | sort | uniq -c +echo "TOTAL benchmarks run: $total" +echo "Raw: $RAW" diff --git a/StrataPython/run_v2_diff.sh b/StrataPython/run_v2_diff.sh new file mode 100644 index 0000000000..9832a9ac79 --- /dev/null +++ b/StrataPython/run_v2_diff.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Differential harness: run BOTH pyAnalyzeV2 (the port) and pyAnalyzeLaurel (proven +# oracle) on all 220 unit tests with the SAME flags, and compare the RESULT line. +# The honest correctness metric for the port is: does v2 match the oracle? +cd "$(dirname "$0")" || exit 1 +V2=.lake/build/bin/pyAnalyzeV2 +PAL=.lake/build/bin/pyAnalyzeLaurel +RAW=/tmp/v2_diff_raw.txt +: > "$RAW" + +match=0; mismatch=0; both_err=0; total=0 +mismatch_files="" + +for ion in StrataPythonTest/tests/*.python.st.ion; do + [ -f "$ion" ] || continue + total=$((total+1)) + base=$(basename "$ion" .python.st.ion) + pyf="StrataPythonTest/tests/${base}.py" + extra="" + [ -f "$pyf" ] && extra=$(grep '^# strata-args:' "$pyf" | sed 's/^# strata-args://' | head -1) + + v2out=$(timeout 180 "$V2" --solver z3 $extra "$ion" 2>&1) + palout=$(timeout 180 "$PAL" --solver z3 $extra "$ion" 2>&1) + v2r=$(echo "$v2out" | grep '^RESULT:' | tail -1) + palr=$(echo "$palout" | grep '^RESULT:' | tail -1) + # also compare the DETAIL pass/fail/inconclusive tally when both are "Analysis"/"Inconclusive" + v2d=$(echo "$v2out" | grep '^DETAIL:' | tail -1) + pald=$(echo "$palout" | grep '^DETAIL:' | tail -1) + + echo "===== $base =====" >> "$RAW" + echo " v2 : $v2r | $v2d" >> "$RAW" + echo " pal: $palr | $pald" >> "$RAW" + + if [ "$v2r" == "$palr" ] && [ "$v2d" == "$pald" ]; then + match=$((match+1)) + elif echo "$v2r" | grep -q "Internal error" && echo "$palr" | grep -q "Internal error"; then + both_err=$((both_err+1)) + echo " -> BOTH internal error" >> "$RAW" + else + mismatch=$((mismatch+1)); mismatch_files="$mismatch_files $base" + echo " -> MISMATCH" >> "$RAW" + fi +done + +echo "" +echo "============ pyAnalyzeV2 vs pyAnalyzeLaurel (oracle) over $total tests ============" +echo " EXACT MATCH (result+detail) : $match" +echo " both internal error : $both_err" +echo " MISMATCH : $mismatch" +echo " ----" +echo " agreement: $((match+both_err)) / $total" +[ -n "$mismatch_files" ] && echo " mismatch files:$mismatch_files" +echo "Raw: $RAW" diff --git a/StrataPython/run_v2_slice.lean b/StrataPython/run_v2_slice.lean new file mode 100644 index 0000000000..0325da2d2c --- /dev/null +++ b/StrataPython/run_v2_slice.lean @@ -0,0 +1,17 @@ +import StrataPython.PySpecPipeline +open Strata StrataPython + +def main (args : List String) : IO Unit := do + let path := args.headD "" + IO.println s!"=== pyAnalyzeV2ToCore on {path} ===" + match ← pyAnalyzeV2ToCore path with + | .error msg => IO.println s!"PIPELINE ERROR: {msg}" + | .ok (some core, errs) => + IO.println s!"REACHED CORE — {core.decls.length} Core decls, {errs.length} diagnostics" + for e in errs.take 10 do IO.println s!" diag: {repr e}" + | .ok (none, errs) => + IO.println s!"NO CORE — {errs.length} diagnostics" + let mut i := 0 + for e in errs do + if i < 10 then IO.println s!" [{i}] {repr e}" + i := i + 1 diff --git a/StrataPython/run_v2_slice2.lean b/StrataPython/run_v2_slice2.lean new file mode 100644 index 0000000000..2f71504e31 --- /dev/null +++ b/StrataPython/run_v2_slice2.lean @@ -0,0 +1,15 @@ +import StrataPython.PySpecPipeline +open Strata StrataPython + +def main (args : List String) : IO Unit := do + let path := args.headD "" + match ← pyAnalyzeV2ToCore path with + | .error msg => IO.println s!"PIPELINE ERROR: {msg}" + | .ok (some core, errs) => + IO.println s!"REACHED CORE — {core.decls.length} Core decls, {errs.length} diagnostics" + | .ok (none, errs) => + IO.println s!"NO CORE — {errs.length} diagnostics" + let mut i := 0 + for e in errs do + if i ≥ 10 ∧ i < 25 then IO.println s!" [{i}] {e.message}" + i := i + 1 diff --git a/StrataPython/run_v2_suite.sh b/StrataPython/run_v2_suite.sh new file mode 100755 index 0000000000..8202ea1fa7 --- /dev/null +++ b/StrataPython/run_v2_suite.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Run pyAnalyzeV2 over all 220 unit-test .python.st.ion files and tally RESULT lines. +# Raw per-file output goes to /tmp/v2_suite_raw.txt; this prints the tally + any +# crashes/errors so nothing is hidden. +cd "$(dirname "$0")" || exit 1 +BIN=.lake/build/bin/pyAnalyzeV2 +RAW=/tmp/v2_suite_raw.txt +: > "$RAW" + +total=0; success=0; failures=0; inconclusive=0; usererr=0; internalerr=0; known=0; crash=0; other=0 +crashed_files="" + +for ion in StrataPythonTest/tests/*.python.st.ion; do + [ -f "$ion" ] || continue + total=$((total+1)) + base=$(basename "$ion" .python.st.ion) + # honor per-file strata-args (e.g. bugFinding) from the paired .py, like run_py_analyze.sh + pyf="StrataPythonTest/tests/${base}.py" + extra="" + [ -f "$pyf" ] && extra=$(grep '^# strata-args:' "$pyf" | sed 's/^# strata-args://' | head -1) + out=$(timeout 180 "$BIN" --solver z3 $extra "$ion" 2>&1) + code=$? + echo "===== $base (exit $code) =====" >> "$RAW" + echo "$out" >> "$RAW" + result=$(echo "$out" | grep '^RESULT:' | tail -1) + if [ $code -ne 0 ] && [ -z "$result" ]; then + crash=$((crash+1)); crashed_files="$crashed_files $base(exit$code)" + echo " -> CRASH/timeout exit $code" >> "$RAW" + continue + fi + case "$result" in + *"Analysis success"*) success=$((success+1)) ;; + *"Failures found"*) failures=$((failures+1)) ;; + *"Inconclusive"*) inconclusive=$((inconclusive+1)) ;; + *"User error"*) usererr=$((usererr+1)) ;; + *"Internal error"*) internalerr=$((internalerr+1)) ;; + *"Known limitation"*) known=$((known+1)) ;; + *) other=$((other+1)); crashed_files="$crashed_files $base(noRESULT)" ;; + esac +done + +echo "" +echo "================ pyAnalyzeV2 over $total unit tests ================" +echo " Analysis success : $success" +echo " Failures found : $failures" +echo " Inconclusive : $inconclusive" +echo " User error : $usererr" +echo " Internal error : $internalerr" +echo " Known limitation : $known" +echo " CRASH/timeout : $crash" +echo " No RESULT/other : $other" +echo " ----" +echo " TOTAL : $((success+failures+inconclusive+usererr+internalerr+known+crash+other)) / $total" +[ -n "$crashed_files" ] && echo " problem files:$crashed_files" +echo "Raw output: $RAW" diff --git a/StrataPython/user_errors.txt b/StrataPython/user_errors.txt new file mode 100644 index 0000000000..d51b6adc20 --- /dev/null +++ b/StrataPython/user_errors.txt @@ -0,0 +1,4 @@ +(set-info :file "StrataPythonTest/tests/test_user_error_metadata.py") +(set-info :start 144) +(set-info :stop 166) +(set-info :error-message "Unknown method 'nonexistent_method'") diff --git a/lake-manifest.json b/lake-manifest.json index 457a2c0c69..218f788373 100644 --- a/lake-manifest.json +++ b/lake-manifest.json @@ -1,7 +1,14 @@ {"version": "1.1.0", "packagesDir": ".lake/packages", "packages": - [{"url": "https://github.com/leanprover-community/plausible.git", + [{"type": "path", + "scope": "", + "name": "Strata", + "manifestFile": "lake-manifest.json", + "inherited": true, + "dir": "StrataPython/..", + "configFile": "lakefile.toml"}, + {"url": "https://github.com/leanprover-community/plausible.git", "type": "git", "subDir": null, "scope": "", @@ -13,10 +20,10 @@ "configFile": "lakefile.toml"}, {"type": "path", "scope": "", - "name": "StrataCLI", + "name": "StrataPython", "manifestFile": "lake-manifest.json", "inherited": false, - "dir": "StrataCLI", + "dir": "StrataPython", "configFile": "lakefile.toml"}, {"type": "path", "scope": "", diff --git a/lakefile.toml b/lakefile.toml index b0837ea7ed..6c4bcf863b 100644 --- a/lakefile.toml +++ b/lakefile.toml @@ -8,6 +8,7 @@ lintDriver = "CheckImports" name = "StrataDDM" path = "StrataDDM" + [[require]] name = "plausible" git = "https://github.com/leanprover-community/plausible.git" @@ -48,3 +49,4 @@ root = "Scripts.CheckImports" name = "ImportStats" root = "Scripts.ImportStats" +