Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Fable.Build/FableLibrary/Python.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type BuildFableLibraryPython(?skipCore: bool, ?postFableBuildStage: unit -> unit
inputPatterns =
[
Path.Combine("src", "fable-library-py", "**", "*.py")
// Type stubs, so edits to them invalidate the cached build
Path.Combine("src", "fable-library-py", "**", "*.pyi")
Path.Combine("src", "fable-library-py", "**", "*.fs")
// Rust extension sources, so edits to them trigger a maturin rebuild
Path.Combine("src", "fable-library-py", "**", "*.rs")
Expand All @@ -29,6 +31,8 @@ type BuildFableLibraryPython(?skipCore: bool, ?postFableBuildStage: unit -> unit
override this.CopyStage() =
// Copy all Python/F# files to the build directory
Directory.GetFiles(this.LibraryDir, "*") |> Shell.copyFiles this.BuildDir
// Note: top-level *.pyi are deliberately not copied. They are stubs for the
// modules generated from F# (list, choice), and would shadow the real ones.
Directory.GetFiles(this.SourceDir, "*.py") |> Shell.copyFiles this.OutDir

// Python extension modules
Expand Down
14 changes: 12 additions & 2 deletions src/Fable.Transforms/Python/Fable2Python.Annotation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -520,15 +520,17 @@ let makeNumberTypeAnnotation com ctx kind info =
| UInt32 -> "uint32"
| Int64 -> "int64"
| UInt64 -> "uint64"
| Int32 -> "int32"
// Int32 is represented as a plain Python `int`
| Int32
| BigInt
| Int128
| UInt128
| NativeInt
| UNativeInt -> "int"
| Float16
| Float32 -> "float32"
| Float64 -> "float64"
// Float64 is a plain Python `float`
| Float64 -> "float"
| _ -> failwith $"Unsupported number type: %A{kind}"

match name with
Expand Down Expand Up @@ -786,6 +788,14 @@ let makeBuiltinTypeAnnotation com ctx typ repeatedGenerics kind =
// Use the type alias (clean name without underscore prefix)
let name = $"FSharpChoice_%d{List.length genArgs}"
fableModuleAnnotation com ctx "choice" name resolved, stmts
| Replacements.Util.FSharpSet key ->
// `FSharpSet` is invariant in its element type, so a bare `Any` here leaves
// Pyright to solve the element type from the arguments alone -- and a call
// like `singleton(1, cmp)` then solves it to `Literal[1]`, which will not
// unify with the `Literal[2]` of a sibling set. Annotating the binding gives
// bidirectional inference something to push down into those calls.
let resolved, stmts = resolveGenerics com ctx [ key ] repeatedGenerics
fableModuleAnnotation com ctx "set" "FSharpSet" resolved, stmts
| _ -> stdlibModuleTypeHint com ctx "typing" "Any" [] repeatedGenerics

let makeArgTypeAnnotation com ctx repeatedGenerics (id: Fable.Ident) =
Expand Down
8 changes: 6 additions & 2 deletions src/Fable.Transforms/Python/Fable2Python.Reflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -368,14 +368,18 @@ let transformTypeTest (com: IPythonCompiler) ctx range expr (typ: Fable.Type) :
| _, Fable.Type.Number(Int8, _) -> pyInstanceof (libValue com ctx "core" "int8") expr
| _, Fable.Type.Number(Int16, _) -> pyInstanceof (libValue com ctx "core" "int16") expr
| _, Fable.Type.Number(UInt16, _) -> pyInstanceof (libValue com ctx "core" "uint16") expr
| _, Fable.Type.Number(Int32, _) -> pyInstanceof (libValue com ctx "core" "int32") expr
// Int32 is a plain Python `int`. Exact-type matching is required: `bool`
// subclasses `int`, so `isinstance` would report `box true :? int` as true.
| _, Fable.Type.Number(Int32, _) -> pyTypeof "<class 'int'>" expr
| _, Fable.Type.Number(UInt32, _) -> pyInstanceof (libValue com ctx "core" "uint32") expr
| _, Fable.Type.Number(NativeInt, _)
| _, Fable.Type.Number(UNativeInt, _) -> pyInstanceof (Expression.name "int") expr
| _, Fable.Type.Number(Int64, _) -> pyInstanceof (libValue com ctx "core" "int64") expr
| _, Fable.Type.Number(UInt64, _) -> pyInstanceof (libValue com ctx "core" "uint64") expr
| _, Fable.Type.Number(Float32, _) -> pyInstanceof (libValue com ctx "core" "float32") expr
| _, Fable.Type.Number(Float64, _) -> pyInstanceof (libValue com ctx "core" "float64") expr
// Float64 is a plain Python `float`. Exact-type matching keeps it distinct
// from the Float32 wrapper, which is not a `float` subclass.
| _, Fable.Type.Number(Float64, _) -> pyTypeof "<class 'float'>" expr
| _, Fable.Type.Number(Decimal, _) -> pyTypeof "<class 'decimal.Decimal'>" expr
| _ -> pyInstanceof (Expression.name "int") expr

Expand Down
178 changes: 161 additions & 17 deletions src/Fable.Transforms/Python/Fable2Python.Transforms.fs
Original file line number Diff line number Diff line change
Expand Up @@ -424,14 +424,31 @@ let transformCast (com: IPythonCompiler) (ctx: Context) t e : Expression * State
let cons = libValue com ctx "core" "float32"
let value, stmts = com.TransformAsExpr(ctx, e)
Expression.call (cons, [ value ], ?loc = None), stmts
| Fable.Number(Float64, _), _ ->
let cons = libValue com ctx "core" "float64"
let value, stmts = com.TransformAsExpr(ctx, e)
Expression.call (cons, [ value ], ?loc = None), stmts
// Int32 is a plain Python `int`, so casting to it is a no-op -- unless the source
// is one of the wrapper widths. `Replacements.toInt` emits a bare `TypeCast` for
// every widening conversion (`needToCast` is false), so this is where e.g.
// `int (x: sbyte)` sheds its `Int8`. Leaving the wrapper in place would keep the
// arithmetic at the source width: `Int8(100) + 100` wraps to -56.
| Fable.Number(Int32, _), _ ->
let cons = libValue com ctx "core" "int32"
let value, stmts = com.TransformAsExpr(ctx, e)
Expression.call (cons, [ value ], ?loc = None), stmts
match e.Type with
// Already a plain `int` at runtime
| Fable.Number((Int32 | BigInt | NativeInt | UNativeInt), _) -> com.TransformAsExpr(ctx, e)
| Fable.Number _ ->
let value, stmts = com.TransformAsExpr(ctx, e)
libCall com ctx None "core" "int32" [ value ], stmts
| _ -> com.TransformAsExpr(ctx, e)
// Float64 is a plain Python `float`, and the same reasoning applies:
// `Replacements.toFloat` emits a bare `TypeCast` for every numeric source that is
// not bigint/decimal/int64, so `float (x: float32)` would otherwise stay a
// `Float32` and keep computing in single precision. Every wrapper implements
// `__float__`, so the builtin is enough and stays a single C-level call.
| Fable.Number(Float64, _), _ ->
match e.Type with
| Fable.Number(Float64, _) -> com.TransformAsExpr(ctx, e)
| Fable.Number _ ->
let value, stmts = com.TransformAsExpr(ctx, e)
Expression.call (Expression.name "float", [ value ], ?loc = None), stmts
| _ -> com.TransformAsExpr(ctx, e)
| _ -> com.TransformAsExpr(ctx, e)

let transformCurry (com: IPythonCompiler) (ctx: Context) expr arity : Expression * Statement list =
Expand Down Expand Up @@ -467,6 +484,10 @@ let transformValue (com: IPythonCompiler) (ctx: Context) r value : Expression *
let value =
match value.Type with
| Fable.String -> value
// Float64 is a plain Python `float`, whose `str` renders a whole
// double as "5.0" where .NET renders "5".
| Fable.Number(Float64, _) ->
Replacements.Util.Helper.LibCall(com, "exceptions", "to_string", Fable.String, [ value ])
| _ -> Helpers.toString value

let acc = makeBinOp None Fable.String acc value BinaryPlus
Expand All @@ -480,7 +501,8 @@ let transformValue (com: IPythonCompiler) (ctx: Context) r value : Expression *
| Fable.NumberValue.UInt8 x -> makeInteger com ctx r value.Type "uint8" x
| Fable.NumberValue.Int16 x -> makeInteger com ctx r value.Type "int16" x
| Fable.NumberValue.UInt16 x -> makeInteger com ctx r value.Type "uint16" x
| Fable.NumberValue.Int32 x -> makeInteger com ctx r value.Type "int32" x
// Int32 is represented as a plain Python `int`, so a literal is just a literal
| Fable.NumberValue.Int32 x -> Expression.intConstant (x, ?loc = r), []
| Fable.NumberValue.UInt32 x -> makeInteger com ctx r value.Type "uint32" x
| Fable.NumberValue.Int64 x -> makeInteger com ctx r value.Type "int64" x
| Fable.NumberValue.UInt64 x -> makeInteger com ctx r value.Type "uint64" x
Expand All @@ -490,16 +512,17 @@ let transformValue (com: IPythonCompiler) (ctx: Context) r value : Expression *
| Fable.NumberValue.NativeInt x -> Expression.intConstant (x, ?loc = r), []
| Fable.NumberValue.UNativeInt x -> Expression.intConstant (x, ?loc = r), []
// TODO: special consts also need attention
| Fable.NumberValue.Float64 x when x = infinity -> libValue com ctx "double" "float64.infinity", []
| Fable.NumberValue.Float64 x when x = -infinity -> libValue com ctx "double" "float64.negative_infinity", []
| Fable.NumberValue.Float64 x when Double.IsNaN(x) -> libValue com ctx "double" "float64.nan", []
| Fable.NumberValue.Float64 x when x = infinity -> com.GetImportExpr(ctx, "math", "inf"), []
| Fable.NumberValue.Float64 x when x = -infinity ->
Expression.unaryOp (UnaryMinus, com.GetImportExpr(ctx, "math", "inf")), []
| Fable.NumberValue.Float64 x when Double.IsNaN(x) -> com.GetImportExpr(ctx, "math", "nan"), []
| Fable.NumberValue.Float32 x when Single.IsNaN(x) ->
libCall com ctx r "core" "float32" [ Expression.stringConstant "nan" ], []
| Fable.NumberValue.Float16 x when Single.IsNaN(x) ->
libCall com ctx r "core" "float32" [ Expression.stringConstant "nan" ], []
| Fable.NumberValue.Float16 x -> makeFloat com ctx r value.Type "float32" (float x)
| Fable.NumberValue.Float32 x -> makeFloat com ctx r value.Type "float32" (float x)
| Fable.NumberValue.Float64 x -> makeFloat com ctx r value.Type "float64" (float x)
| Fable.NumberValue.Float64 x -> Expression.floatConstant (float x, ?loc = r), []
| Fable.NumberValue.Decimal x -> Py.Replacements.makeDecimal com r value.Type x |> transformAsExpr com ctx
| _ -> addErrorAndReturnNull com r $"Numeric literal is not supported: %A{v}", []
| Fable.NewArray(newKind, typ, kind) ->
Expand Down Expand Up @@ -943,15 +966,114 @@ let resolveExpr (ctx: Context) _t strategy pyExpr : Statement list =
| Some(Assign left) -> exprAsStatement ctx (assign None left pyExpr)
| Some(Target left) -> exprAsStatement ctx (assign None (left |> Expression.identifier) pyExpr)

let transformOperation com ctx range opKind tags : Expression * Statement list =
let transformOperation (com: IPythonCompiler) ctx range (t: Fable.Type) opKind tags : Expression * Statement list =
// Int32 is a plain Python `int`, so results that can leave the 32-bit range are
// normalized back into it. See `Util.isInt32WrapOp` for why one wrap per
// expression tree is equivalent to one per operation.
let isInt32 =
match t with
| Fable.Number(Int32, _) -> true
| _ -> false

// Float64 is a plain Python `float`, which IS an IEEE double, so arithmetic needs
// no adjustment. Only division by zero and remainder diverge.
let isFloat64 =
match t with
| Fable.Number(Float64, _) -> true
| _ -> false

/// A literal divisor that is known not to be zero lets `/` stay a bare operator.
let isNonZeroLiteral (e: Fable.Expr) =
match e with
| Fable.Value(Fable.NumberConstant(v, _), _) ->
match v with
| Fable.NumberValue.Float64 x -> x <> 0.0 && not (Double.IsNaN x)
| Fable.NumberValue.Int32 x -> x <> 0
| _ -> false
| _ -> false

match opKind with
| Fable.Unary(op, TransformExpr com ctx (expr, stmts)) -> Expression.unaryOp (op, expr, ?loc = range), stmts
| Fable.Unary(op, (operand: Fable.Expr)) ->
let expr, stmts = com.TransformAsExpr(ctx, operand)

// Only negation can leave the range, at Int32.MinValue. `~` maps in-range to
// in-range, and `+` is the identity.
let needsWrap = isInt32 && op = UnaryMinus

let expr =
if needsWrap then
stripInt32Wrap com ctx expr
else
expr

let result = Expression.unaryOp (op, expr, ?loc = range)

(if needsWrap then
wrapInt32 com ctx range result
else
result),
stmts
| Fable.Binary(op, left, right: Fable.Expr) ->
let typ = right.Type
let left_typ = left.Type
let leftFable, rightFable = left, right

// `<<` shifts the left operand only; the shift count is a separate value.
let wrapsResult =
isInt32
&& match op with
| BinaryPlus
| BinaryMinus
| BinaryMultiply
| BinaryShiftLeft -> true
| _ -> false

let left, stmts = com.TransformAsExpr(ctx, left)
let right, stmts' = com.TransformAsExpr(ctx, right)

// Strip an operand's own wrap only when this node re-applies one, and only
// when the *Fable* operand is one of the nodes this module wraps.
let left =
if wrapsResult && isInt32WrapOp leftFable then
stripInt32Wrap com ctx left
else
left

let right =
if wrapsResult && op <> BinaryShiftLeft && isInt32WrapOp rightFable then
stripInt32Wrap com ctx right
else
right

// .NET masks a shift count to the width of the type, so `1 <<< 32` is 1.
// Python applies the count as given, and raises ValueError if it is negative.
let right =
let isShift =
match op with
| BinaryShiftLeft
| BinaryShiftRightSignPropagating
| BinaryShiftRightZeroFill -> true
| _ -> false

if not (isInt32 && isShift) then
right
else
match right with
| Expression.Constant(value = IntLiteral v) ->
match v with
| :? int as i -> Expression.intConstant (i &&& 31)
| _ -> Expression.binOp (right, BitAnd, Expression.intConstant 31)
| _ -> Expression.binOp (right, BitAnd, Expression.intConstant 31)

let binOp (op: BinaryOperator) =
let result = Expression.binOp (left, op, right, ?loc = range)

(if wrapsResult then
wrapInt32 com ctx range result
else
result),
stmts @ stmts'

let compare op =
Expression.compare (left, [ op ], [ right ], ?loc = range), stmts @ stmts'

Expand Down Expand Up @@ -1005,6 +1127,17 @@ let transformOperation com ctx range opKind tags : Expression * Statement list =
| BinaryDivide, _ ->
// For integer division, we need to use the // operator
match typ with
// Int32 is a plain Python `int`, whose `//` floors where .NET truncates
// toward zero: `-7 // 2` is -4 in Python and -3 in .NET. `int(a / b)`
// truncates, and is exact here -- for int32 operands the true quotient is
// never within a rounding error of an integer it should not reach.
| Fable.Number(Int32, _) when isInt32 ->
Expression.call (
Expression.name "int",
[ Expression.binOp (left, Div, right, ?loc = range) ],
?loc = range
),
stmts @ stmts'
| Fable.Number(Int8, _)
| Fable.Number(Int16, _)
| Fable.Number(Int32, _)
Expand All @@ -1018,8 +1151,19 @@ let transformOperation com ctx range opKind tags : Expression * Statement list =
| Fable.Number(Float32, _)
| Fable.Number(Float64, _) -> Expression.binOp (left, Div, right, ?loc = range), stmts @ stmts'
| _ -> Expression.binOp (left, FloorDiv, right, ?loc = range), stmts @ stmts'
// Python raises ZeroDivisionError where .NET yields +/-inf or nan
| Fable.Number(Float64, _) when isFloat64 && not (isNonZeroLiteral rightFable) ->
libCall com ctx range "core" "op_division_float64" [ left; right ], stmts @ stmts'
| _ -> Expression.binOp (left, op, right, ?loc = range), stmts @ stmts'
| _ -> Expression.binOp (left, op, right, ?loc = range), stmts @ stmts'
| BinaryModulus, _ when isFloat64 ->
// Python's float `%` takes the sign of the divisor, .NET's takes the sign
// of the dividend, and Python raises on a zero divisor where .NET gives nan.
libCall com ctx range "core" "op_remainder_float64" [ left; right ], stmts @ stmts'
| BinaryModulus, _ when isInt32 ->
// Python's `%` takes the sign of the divisor, .NET's takes the sign of the
// dividend: `-5 % 3` is 1 in Python and -2 in .NET.
libCall com ctx range "core" "op_remainder_int32" [ left; right ], stmts @ stmts'
| _ -> binOp op

| Fable.Logical(op, TransformExpr com ctx (left, stmts), TransformExpr com ctx (right, stmts')) ->
Expression.boolOp (op, [ left; right ], ?loc = range), stmts @ stmts'
Expand Down Expand Up @@ -2591,7 +2735,7 @@ let rec transformAsExpr (com: IPythonCompiler) ctx (expr: Fable.Expr) : Expressi

| Fable.CurriedApply(callee, args, _, range) -> transformCurriedApply com ctx range callee args

| Fable.Operation(kind, tags, _, range) -> transformOperation com ctx range kind tags
| Fable.Operation(kind, tags, t, range) -> transformOperation com ctx range t kind tags

| Fable.Get(expr, kind, typ, range) -> transformGet com ctx range typ expr kind

Expand Down Expand Up @@ -2822,7 +2966,7 @@ let rec transformAsStatements (com: IPythonCompiler) ctx returnStrategy (expr: F
stmts @ resolveExpr ctx t returnStrategy e

| Fable.Operation(kind, tags, t, range) ->
let expr, stmts = transformOperation com ctx range kind tags
let expr, stmts = transformOperation com ctx range t kind tags
stmts @ (expr |> resolveExpr ctx t returnStrategy)

| Fable.Get(expr, kind, t, range) ->
Expand Down
Loading
Loading